pax_global_header00006660000000000000000000000064147745037760014536gustar00rootroot0000000000000052 comment=ea536c2e0c3d1c2ad9d578bca082ed0348062bc6 minio-client-0.0~20250403/000077500000000000000000000000001477450377600150025ustar00rootroot00000000000000minio-client-0.0~20250403/.github/000077500000000000000000000000001477450377600163425ustar00rootroot00000000000000minio-client-0.0~20250403/.github/ISSUE_TEMPLATE.md000066400000000000000000000002321477450377600210440ustar00rootroot00000000000000## Expected behavior ## Actual behavior ## Steps to reproduce the behavior ## mc --version - (paste output of `mc --version`) ## System information minio-client-0.0~20250403/.github/ISSUE_TEMPLATE/000077500000000000000000000000001477450377600205255ustar00rootroot00000000000000minio-client-0.0~20250403/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000004001477450377600232110ustar00rootroot00000000000000--- name: Bug report about: Bug report template title: '' labels: community, triage assignees: '' --- ## Expected behavior ## Actual behavior ## Steps to reproduce the behavior ## mc --version - (paste output of `mc --version`) ## System information minio-client-0.0~20250403/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002331477450377600225130ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: MinIO Community Support url: https://slack.min.io about: Please ask and answer questions here.minio-client-0.0~20250403/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000020521477450377600221420ustar00rootroot00000000000000## Community Contribution License All community contributions in this pull request are licensed to the project maintainers under the terms of the [Apache 2 license](https://www.apache.org/licenses/LICENSE-2.0). By creating this pull request I represent that I have the right to license the contributions to the project maintainers under the Apache 2 license. ## Description ## Motivation and Context ## How to test this PR? ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Optimization (provides speedup with no functional changes) - [ ] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist: - [ ] Fixes a regression (If yes, please add `commit-id` or `PR #` here) - [ ] Unit tests added/updated - [ ] Internal documentation updated - [ ] Create a documentation update request [here](https://github.com/minio/docs/issues/new?label=doc-change,title=Doc+Updated+Needed+For+PR+github.com%2fminio%2fmc%2fpull%2fNNNNN) minio-client-0.0~20250403/.github/lock.yml000066400000000000000000000022031477450377600200120ustar00rootroot00000000000000# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app # Number of days of inactivity before a closed issue or pull request is locked daysUntilLock: 365 # Skip issues and pull requests created before a given timestamp. Timestamp must # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable skipCreatedBefore: false # Issues and pull requests with these labels will be ignored. Set to `[]` to disable exemptLabels: [] # Label to add before locking, such as `outdated`. Set to `false` to disable lockLabel: false # Comment to post before locking. Set to `false` to disable lockComment: >- This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. # Assign `resolved` as the reason for locking. Set to `false` to disable setLockReason: true # Limit to only `issues` or `pulls` only: issues # Optionally, specify configuration settings just for `issues` or `pulls` # issues: # exemptLabels: # - help-wanted # lockLabel: outdated # pulls: # daysUntilLock: 30 # Repository to extend settings from # _extends: repo minio-client-0.0~20250403/.github/stale.yml000066400000000000000000000037031477450377600202000ustar00rootroot00000000000000# Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale daysUntilStale: 90 # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. daysUntilClose: 30 # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) onlyLabels: [] # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - "security" - "pending discussion" # Set to true to ignore issues in a project (defaults to false) exemptProjects: false # Set to true to ignore issues in a milestone (defaults to false) exemptMilestones: false # Set to true to ignore issues with an assignee (defaults to false) exemptAssignees: false # Label to use when marking as stale staleLabel: stale # Comment to post when marking as stale. Set to `false` to disable markComment: >- This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 21 days if no further activity occurs. Thank you for your contributions. # Comment to post when removing the stale label. # unmarkComment: > # Your comment here. # Comment to post when closing a stale Issue or Pull Request. # closeComment: > # Your comment here. # Limit the number of actions per hour, from 1-30. Default is 30 limitPerRun: 1 # Limit to only `issues` or `pulls` # only: issues # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': # pulls: # daysUntilStale: 30 # markComment: > # This pull request has been automatically marked as stale because it has not had # recent activity. It will be closed if no further activity occurs. Thank you # for your contributions. # issues: # exemptLabels: # - confirmed minio-client-0.0~20250403/.github/workflows/000077500000000000000000000000001477450377600203775ustar00rootroot00000000000000minio-client-0.0~20250403/.github/workflows/go-cross.yml000066400000000000000000000016751477450377600226670ustar00rootroot00000000000000name: Crosscompile on: pull_request: branches: - master # This ensures that previous jobs for the PR are canceled when the PR is # updated. concurrency: group: ${{ github.workflow }}-${{ github.head_ref }} cancel-in-progress: true permissions: contents: read jobs: build: name: Build Tests with Go ${{ matrix.go-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: go-version: [1.23.x] os: [ubuntu-latest] steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} check-latest: true - name: Build on ${{ matrix.os }} if: matrix.os == 'ubuntu-latest' env: CGO_ENABLED: 0 GO111MODULE: on run: | sudo sysctl net.ipv6.conf.all.disable_ipv6=0 sudo sysctl net.ipv6.conf.default.disable_ipv6=0 make crosscompile minio-client-0.0~20250403/.github/workflows/go.yml000066400000000000000000000050451477450377600215330ustar00rootroot00000000000000name: Go on: pull_request: branches: - master # This ensures that previous jobs for the PR are canceled when the PR is # updated. concurrency: group: ${{ github.workflow }}-${{ github.head_ref }} cancel-in-progress: true jobs: build: name: Test on Go ${{ matrix.go-version }} and ${{ matrix.os }} runs-on: ${{ matrix.os }} env: GO111MODULE: on strategy: matrix: go-version: [1.23.x] os: [ubuntu-latest, macos-latest, windows-latest] steps: - name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }} uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v4 - name: Build on ${{ matrix.os }} if: matrix.os == 'windows-latest' run: | go build --ldflags="-s -w" -o %GOPATH%\bin\mc.exe go test -v -race --timeout 30m ./... - name: Build on ${{ matrix.os }} if: matrix.os == 'macos-latest' run: | go build --ldflags="-s -w" -o %GOPATH%\bin\mc go test -v -race --timeout 30m ./... - name: Build on ${{ matrix.os }} if: matrix.os == 'ubuntu-latest' env: ACCESS_KEY: minioadmin SECRET_KEY: minioadmin ENABLE_HTTPS: 1 MINIO_CI_CD: 1 SERVER_ENDPOINT: localhost:9000 MC_TEST_ENABLE_HTTPS: true MC_TEST_SKIP_INSECURE: true MC_TEST_SKIP_BUILD: true run: | wget https://dl.min.io/server/minio/release/linux-amd64/minio && chmod +x minio mkdir -p ~/.minio/certs/ && cp testdata/localhost.crt ~/.minio/certs/public.crt && cp testdata/localhost.key ~/.minio/certs/private.key sudo cp testdata/localhost.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificates ./minio server /tmp/test-xl/{1...4}/ & sleep 10 make make test-race make verify ./functional-tests.sh vetchecks: # Run vet checks against one version. env: CGO_ENABLED: 0 runs-on: ubuntu-latest steps: - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.23.x - name: Checkout code uses: actions/checkout@v2 - name: Test 386 run: GOOS=linux GOARCH=386 go test -short ./... - name: Staticcheck # Run with defaults, but allow errors with other formats ST1005 run: go install honnef.co/go/tools/cmd/staticcheck@latest && make lint minio-client-0.0~20250403/.github/workflows/issues.yaml000066400000000000000000000005221477450377600225750ustar00rootroot00000000000000# @format name: Issue Workflow on: issues: types: - opened jobs: add-to-project: name: Add issue to project runs-on: ubuntu-latest steps: - uses: actions/add-to-project@v0.5.0 with: project-url: https://github.com/orgs/miniohq/projects/2 github-token: ${{ secrets.BOT_PAT }} minio-client-0.0~20250403/.github/workflows/vulncheck.yml000066400000000000000000000011001477450377600230740ustar00rootroot00000000000000name: VulnCheck on: pull_request: branches: - master push: branches: - master jobs: vulncheck: name: Analysis runs-on: ubuntu-latest steps: - name: Check out code into the Go module directory uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.23.6 - name: Get official govulncheck run: go install golang.org/x/vuln/cmd/govulncheck@latest shell: bash - name: Run govulncheck run: govulncheck ./... shell: bash minio-client-0.0~20250403/.gitignore000066400000000000000000000002201477450377600167640ustar00rootroot00000000000000cover.out *~ *.test release experimental coverage.txt *.tar.bz2 parts/ prime/ stage/ mc .run* .idea/ mc.RELEASE* mc.gz .DS_Store .vscode/ .bin/ minio-client-0.0~20250403/.golangci.yml000066400000000000000000000016031477450377600173660ustar00rootroot00000000000000version: "2" linters: default: none enable: - gomodguard - govet - ineffassign - misspell - revive - staticcheck - unused settings: misspell: locale: US exclusions: generated: lax rules: - path: (.+)\.go$ text: instead of using struct literal - path: (.+)\.go$ text: should have a package comment - path: (.+)\.go$ text: error strings should not be capitalized or end with punctuation or a newline - path: (.+)\.go$ text: error strings should not end with punctuation or newlines - path: (.+)\.go$ text: error strings should not be capitalized paths: - third_party$ - builtin$ - examples$ formatters: enable: - gofmt - gofumpt - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ minio-client-0.0~20250403/.goreleaser.yml000066400000000000000000000070151477450377600177360ustar00rootroot00000000000000project_name: mc release: name_template: "Version {{.Version}}" disable: true github: owner: minio name: mc env: - CGO_ENABLED=0 - GO111MODULE=on before: hooks: - make clean - go generate ./... - go mod tidy - go mod download builds: - goos: - linux - darwin - windows - freebsd goarch: - amd64 - arm64 - arm - ppc64le - s390x goarm: - 7 ignore: - goos: darwin goarch: arm64 - goos: darwin goarch: arm - goos: darwin goarch: ppc64le - goos: darwin goarch: s390x - goos: windows goarch: arm64 - goos: windows goarch: arm - goos: windows goarch: ppc64le - goos: windows goarch: s390x - goos: freebsd goarch: arm - goos: freebsd goarch: arm64 - goos: freebsd goarch: ppc64le - goos: freebsd goarch: s390x flags: - -tags=kqueue - -trimpath ldflags: - "-s -w -X github.com/minio/mc/cmd.Version={{.Version}} -X github.com/minio/mc/cmd.ReleaseTag={{.Tag}} -X github.com/minio/mc/cmd.CommitID={{.FullCommit}} -X github.com/minio/mc/cmd.ShortCommitID={{.ShortCommit}}" archives: - format: binary name_template: "{{ .Binary }}-release/{{ .Os }}-{{ .Arch }}/{{ .Binary }}.{{ .Version }}" nfpms: - id: minio package_name: mc vendor: MinIO, Inc. homepage: https://min.io/ maintainer: dev@min.io description: MinIO Client (mc) provides a modern alternative to UNIX commands like ls, cat, cp, mirror, diff, find etc. It supports filesystems and Amazon S3 compatible cloud storage service (AWS Signature v2 and v4). license: Apache 2.0 bindir: /usr/bin formats: - deb - rpm overrides: deb: file_name_template: "{{ .Binary }}-release/debs/{{ .ProjectName }}-{{ .Version }}_{{ .Arch }}" replacements: arm: armv7 files: "NOTICE": "/usr/share/mc/NOTICE" "CREDITS": "/usr/share/mc/CREDITS" "LICENSE": "/usr/share/mc/LICENSE" "README.md": "/usr/share/mc/README.md" rpm: file_name_template: "{{ .Binary }}-release/rpms/{{ .ProjectName }}-{{ .Version }}.{{ .Arch }}" replacements: amd64: x86_64 arm64: aarch64 arm: armv7 files: "NOTICE": "/usr/share/mc/NOTICE" "CREDITS": "/usr/share/mc/CREDITS" "LICENSE": "/usr/share/mc/LICENSE" "README.md": "/usr/share/mc/README.md" checksum: algorithm: sha256 signs: - signature: "${artifact}.asc" cmd: "sh" args: - '-c' - 'gpg --quiet --detach-sign -a ${artifact}' artifacts: all changelog: sort: asc filters: exclude: - '^Update yaml files' dockers: - goos: linux goarch: amd64 dockerfile: Dockerfile.release image_templates: - minio/mc:{{ .Tag }} - minio/mc:latest - goos: linux goarch: ppc64le dockerfile: Dockerfile.ppc64le.release image_templates: - minio/mc:{{ .Tag }}-ppc64le - goos: linux goarch: s390x dockerfile: Dockerfile.s390x.release image_templates: - minio/mc:{{ .Tag }}-s390x - goos: linux goarch: arm64 goarm: '' dockerfile: Dockerfile.arm64.release image_templates: - minio/mc:{{ .Tag }}-arm64 - goos: linux goarch: arm goarm: '7' dockerfile: Dockerfile.arm.release image_templates: - minio/mc:{{ .Tag }}-arm minio-client-0.0~20250403/.mailmap000066400000000000000000000013071477450377600164240ustar00rootroot00000000000000# Generate CONTRIBUTORS.md: contributors.sh # Tip for finding duplicates (besides scanning the output of CONTRIBUTORS.md for name # duplicates that aren't also email duplicates): scan the output of: # git log --format='%aE - %aN' | sort -uf # # For explanation on this file format: man git-shortlog Anand Babu (AB) Periasamy Anand Babu (AB) Periasamy Anand Babu (AB) Periasamy Anis Elleuch Frederick F. Kautz IV Harshavardhana Krishna Srinivas Bosky minio-client-0.0~20250403/CONFLICT.md000066400000000000000000000015151477450377600165270ustar00rootroot00000000000000## mc (MinIO Client) v/s Midnight Commander (mc) There has been some amount of [requests](https://github.com/minio/mc/issues?q=is%3Aissue+midnight+commander+is%3Aclosed) on renaming this project to avoid the conflict with Midnight Commander for Unix distributions. We struggled with this, it is harder to find a name sweeter and shorter than `mc` for MinIO Client. Besides `mc` is a single static binary and can reside inside your application and is fully self contained. Midnight Commander (mc) is a free software clone of Norton Commander (nc). MinIO and Midnight shares no code or ideas. Only their abbreviation matches. Package managers are free to choose a different name if they like. One such solution [pointed out](https://github.com/minio/mc/issues/873#issuecomment-267583013) by one of our community members. ``` mv ./mc ./mcli ```minio-client-0.0~20250403/CONTRIBUTING.md000066400000000000000000000030531477450377600172340ustar00rootroot00000000000000### Setup your mc Github Repository Fork [mc upstream](https://github.com/minio/mc/fork) source repository to your own personal repository. ``` $ mkdir -p $GOPATH/src/github.com/minio $ cd $GOPATH/src/github.com/minio $ git clone https://github.com/$USER_ID/mc $ cd mc $ make $ ./mc --help ``` ### Developer Guidelines ``mc`` welcomes your contribution. To make the process as seamless as possible, we ask for the following: * Go ahead and fork the project and make your changes. We encourage pull requests to discuss code changes. - Fork it - Create your feature branch (git checkout -b my-new-feature) - Commit your changes (git commit -am 'Add some feature') - Push to the branch (git push origin my-new-feature) - Create new Pull Request * If you have additional dependencies for ``mc``, ``mc`` manages its dependencies using `go mod` - Run `go get foo/bar` - Edit your code to import foo/bar - Run `GO111MODULE=on go mod tidy` from top-level folder * When you're ready to create a pull request, be sure to: - Have test cases for the new code. If you have questions about how to do it, please ask in your pull request. - Run `go fmt` - Squash your commits into a single commit. `git rebase -i`. It's okay to force update your pull request. - Make sure `make install` completes. * Read [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project - `mc` project is conformant with Golang style - if you happen to observe offending code, please feel free to send a pull request minio-client-0.0~20250403/CREDITS000066400000000000000000024322621477450377600160350ustar00rootroot00000000000000Go (the standard library) https://golang.org/ ---------------------------------------------------------------- Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ aead.dev/minisign https://aead.dev/minisign ---------------------------------------------------------------- MIT License Copyright (c) 2021 Andreas Auernhammer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/VividCortex/ewma https://github.com/VividCortex/ewma ---------------------------------------------------------------- The MIT License Copyright (c) 2013 VividCortex Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/acarl005/stripansi https://github.com/acarl005/stripansi ---------------------------------------------------------------- MIT License Copyright (c) 2018 Andrew Carlson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/aymanbagabas/go-osc52/v2 https://github.com/aymanbagabas/go-osc52/v2 ---------------------------------------------------------------- MIT License Copyright (c) 2022 Ayman Bagabas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/beorn7/perks https://github.com/beorn7/perks ---------------------------------------------------------------- Copyright (C) 2013 Blake Mizerany Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/cespare/xxhash/v2 https://github.com/cespare/xxhash/v2 ---------------------------------------------------------------- Copyright (c) 2016 Caleb Spare MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/charmbracelet/bubbles https://github.com/charmbracelet/bubbles ---------------------------------------------------------------- MIT License Copyright (c) 2020-2023 Charmbracelet, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/charmbracelet/bubbletea https://github.com/charmbracelet/bubbletea ---------------------------------------------------------------- MIT License Copyright (c) 2020-2023 Charmbracelet, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/charmbracelet/lipgloss https://github.com/charmbracelet/lipgloss ---------------------------------------------------------------- MIT License Copyright (c) 2021-2023 Charmbracelet, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/charmbracelet/x/ansi https://github.com/charmbracelet/x/ansi ---------------------------------------------------------------- MIT License Copyright (c) 2023 Charmbracelet, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/charmbracelet/x/exp/golden https://github.com/charmbracelet/x/exp/golden ---------------------------------------------------------------- MIT License Copyright (c) 2023 Charmbracelet, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/charmbracelet/x/term https://github.com/charmbracelet/x/term ---------------------------------------------------------------- MIT License Copyright (c) 2023 Charmbracelet, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/cheggaaa/pb https://github.com/cheggaaa/pb ---------------------------------------------------------------- Copyright (c) 2012-2015, Sergey Cherepanov All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/coreos/go-semver https://github.com/coreos/go-semver ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/coreos/go-systemd/v22 https://github.com/coreos/go-systemd/v22 ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/davecgh/go-spew https://github.com/davecgh/go-spew ---------------------------------------------------------------- ISC License Copyright (c) 2012-2016 Dave Collins Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================================ github.com/decred/dcrd/dcrec/secp256k1/v4 https://github.com/decred/dcrd/dcrec/secp256k1/v4 ---------------------------------------------------------------- ISC License Copyright (c) 2013-2017 The btcsuite developers Copyright (c) 2015-2024 The Decred developers Copyright (c) 2017 The Lightning Network Developers Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================================ github.com/dustin/go-humanize https://github.com/dustin/go-humanize ---------------------------------------------------------------- Copyright (c) 2005-2008 Dustin Sallings Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/erikgeiser/coninput https://github.com/erikgeiser/coninput ---------------------------------------------------------------- MIT License Copyright (c) 2021 Erik G. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/fatih/color https://github.com/fatih/color ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2013 Fatih Arslan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/fatih/structs https://github.com/fatih/structs ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2014 Fatih Arslan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/go-ini/ini https://github.com/go-ini/ini ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2014 Unknwon Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/go-logr/logr https://github.com/go-logr/logr ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/go-logr/stdr https://github.com/go-logr/stdr ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/go-ole/go-ole https://github.com/go-ole/go-ole ---------------------------------------------------------------- The MIT License (MIT) Copyright © 2013-2017 Yasuhiro Matsumoto, Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/goccy/go-json https://github.com/goccy/go-json ---------------------------------------------------------------- MIT License Copyright (c) 2020 Masaaki Goshima Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/gogo/protobuf https://github.com/gogo/protobuf ---------------------------------------------------------------- Copyright (c) 2013, The GoGo Authors. All rights reserved. Protocol Buffers for Go with Gadgets Go support for Protocol Buffers - Google's data interchange format Copyright 2010 The Go Authors. All rights reserved. https://github.com/golang/protobuf Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/golang-jwt/jwt/v4 https://github.com/golang-jwt/jwt/v4 ---------------------------------------------------------------- Copyright (c) 2012 Dave Grijalva Copyright (c) 2021 golang-jwt maintainers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/golang/protobuf https://github.com/golang/protobuf ---------------------------------------------------------------- Copyright 2010 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/google/go-cmp https://github.com/google/go-cmp ---------------------------------------------------------------- Copyright (c) 2017 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/google/shlex https://github.com/google/shlex ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/google/uuid https://github.com/google/uuid ---------------------------------------------------------------- Copyright (c) 2009,2014 Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/hashicorp/errwrap https://github.com/hashicorp/errwrap ---------------------------------------------------------------- Mozilla Public License, version 2.0 1. Definitions 1.1. “Contributor” means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. “Contributor Version” means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution. 1.3. “Contribution” means Covered Software of a particular Contributor. 1.4. “Covered Software” means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. “Incompatible With Secondary Licenses” means a. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or b. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. “Executable Form” means any form of the work other than Source Code Form. 1.7. “Larger Work” means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. “License” means this document. 1.9. “Licensable” means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. “Modifications” means any of the following: a. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or b. any new file in Source Code Form that contains any Covered Software. 1.11. “Patent Claims” of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. “Secondary License” means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. “Source Code Form” means the form of the work preferred for making modifications. 1.14. “You” (or “Your”) means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: a. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and b. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: a. for any code that a Contributor has removed from Covered Software; or b. for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or c. under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: a. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and b. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - “Incompatible With Secondary Licenses” Notice This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. ================================================================ github.com/hashicorp/go-multierror https://github.com/hashicorp/go-multierror ---------------------------------------------------------------- Mozilla Public License, version 2.0 1. Definitions 1.1. “Contributor” means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. “Contributor Version” means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution. 1.3. “Contribution” means Covered Software of a particular Contributor. 1.4. “Covered Software” means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. “Incompatible With Secondary Licenses” means a. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or b. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. “Executable Form” means any form of the work other than Source Code Form. 1.7. “Larger Work” means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. “License” means this document. 1.9. “Licensable” means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. “Modifications” means any of the following: a. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or b. any new file in Source Code Form that contains any Covered Software. 1.11. “Patent Claims” of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. “Secondary License” means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. “Source Code Form” means the form of the work preferred for making modifications. 1.14. “You” (or “Your”) means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: a. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and b. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: a. for any code that a Contributor has removed from Covered Software; or b. for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or c. under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: a. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and b. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - “Incompatible With Secondary Licenses” Notice This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. ================================================================ github.com/inconshreveable/mousetrap https://github.com/inconshreveable/mousetrap ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2022 Alan Shreve (@inconshreveable) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/jedib0t/go-pretty/v6 https://github.com/jedib0t/go-pretty/v6 ---------------------------------------------------------------- MIT License Copyright (c) 2018 jedib0t Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/juju/ratelimit https://github.com/juju/ratelimit ---------------------------------------------------------------- All files in this repository are licensed as follows. If you contribute to this repository, it is assumed that you license your contribution under the same license unless you state otherwise. All files Copyright (C) 2015 Canonical Ltd. unless otherwise specified in the file. This software is licensed under the LGPLv3, included below. As a special exception to the GNU Lesser General Public License version 3 ("LGPL3"), the copyright holders of this Library give you permission to convey to a third party a Combined Work that links statically or dynamically to this Library without providing any Minimal Corresponding Source or Minimal Application Code as set out in 4d or providing the installation information set out in section 4e, provided that you comply with the other provisions of LGPL3 and provided that you meet, for the Application the terms and conditions of the license(s) which apply to the Application. Except as stated in this special exception, the provisions of LGPL3 will continue to comply in full to this Library. If you modify this Library, you may apply this exception to your version of this Library, but you are not obliged to do so. If you do not wish to do so, delete this exception statement from your version. This exception does not (and cannot) modify any license terms which apply to the Application, with which you must still comply. GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ================================================================ github.com/klauspost/compress https://github.com/klauspost/compress ---------------------------------------------------------------- Copyright (c) 2012 The Go Authors. All rights reserved. Copyright (c) 2019 Klaus Post. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------------------ Files: gzhttp/* Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2016-2017 The New York Times Company Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ------------------ Files: s2/cmd/internal/readahead/* The MIT License (MIT) Copyright (c) 2015 Klaus Post Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------- Files: snappy/* Files: internal/snapref/* Copyright (c) 2011 The Snappy-Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ----------------- Files: s2/cmd/internal/filepathx/* Copyright 2016 The filepathx Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/klauspost/cpuid/v2 https://github.com/klauspost/cpuid/v2 ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2015 Klaus Post Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/kr/pretty https://github.com/kr/pretty ---------------------------------------------------------------- Copyright 2012 Keith Rarick Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/kr/text https://github.com/kr/text ---------------------------------------------------------------- Copyright 2012 Keith Rarick Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/kylelemons/godebug https://github.com/kylelemons/godebug ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/lestrrat-go/blackmagic https://github.com/lestrrat-go/blackmagic ---------------------------------------------------------------- MIT License Copyright (c) 2021 lestrrat-go Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/lestrrat-go/httpcc https://github.com/lestrrat-go/httpcc ---------------------------------------------------------------- MIT License Copyright (c) 2020 lestrrat-go Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/lestrrat-go/httprc https://github.com/lestrrat-go/httprc ---------------------------------------------------------------- MIT License Copyright (c) 2022 lestrrat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/lestrrat-go/iter https://github.com/lestrrat-go/iter ---------------------------------------------------------------- MIT License Copyright (c) 2020 lestrrat-go Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/lestrrat-go/jwx/v2 https://github.com/lestrrat-go/jwx/v2 ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2015 lestrrat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/lestrrat-go/option https://github.com/lestrrat-go/option ---------------------------------------------------------------- MIT License Copyright (c) 2021 lestrrat-go Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/lucasb-eyer/go-colorful https://github.com/lucasb-eyer/go-colorful ---------------------------------------------------------------- Copyright (c) 2013 Lucas Beyer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/lufia/plan9stats https://github.com/lufia/plan9stats ---------------------------------------------------------------- BSD 3-Clause License Copyright (c) 2019, KADOTA, Kyohei All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/mattn/go-colorable https://github.com/mattn/go-colorable ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2016 Yasuhiro Matsumoto Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/mattn/go-ieproxy https://github.com/mattn/go-ieproxy ---------------------------------------------------------------- MIT License Copyright (c) 2014 mattn Copyright (c) 2017 oliverpool Copyright (c) 2019 Adele Reed Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/mattn/go-isatty https://github.com/mattn/go-isatty ---------------------------------------------------------------- Copyright (c) Yasuhiro MATSUMOTO MIT License (Expat) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/mattn/go-runewidth https://github.com/mattn/go-runewidth ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2016 Yasuhiro Matsumoto Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/matttproud/golang_protobuf_extensions https://github.com/matttproud/golang_protobuf_extensions ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/minio/cli https://github.com/minio/cli ---------------------------------------------------------------- MIT License Copyright (c) 2016 Jeremy Saenz & Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/minio/crc64nvme https://github.com/minio/crc64nvme ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/minio/madmin-go/v3 https://github.com/minio/madmin-go/v3 ---------------------------------------------------------------- GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================================ github.com/minio/md5-simd https://github.com/minio/md5-simd ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/minio/minio-go/v7 https://github.com/minio/minio-go/v7 ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/minio/mux https://github.com/minio/mux ---------------------------------------------------------------- Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/minio/pkg/v3 https://github.com/minio/pkg/v3 ---------------------------------------------------------------- GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================================ github.com/minio/selfupdate https://github.com/minio/selfupdate ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/mitchellh/go-homedir https://github.com/mitchellh/go-homedir ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2013 Mitchell Hashimoto Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/muesli/ansi https://github.com/muesli/ansi ---------------------------------------------------------------- MIT License Copyright (c) 2021 Christian Muehlhaeuser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/muesli/cancelreader https://github.com/muesli/cancelreader ---------------------------------------------------------------- MIT License Copyright (c) 2022 Erik Geiser and Christian Muehlhaeuser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/muesli/reflow https://github.com/muesli/reflow ---------------------------------------------------------------- MIT License Copyright (c) 2019 Christian Muehlhaeuser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/muesli/termenv https://github.com/muesli/termenv ---------------------------------------------------------------- MIT License Copyright (c) 2019 Christian Muehlhaeuser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/munnerz/goautoneg https://github.com/munnerz/goautoneg ---------------------------------------------------------------- Copyright (c) 2011, Open Knowledge Foundation Ltd. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the Open Knowledge Foundation Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/olekukonko/tablewriter https://github.com/olekukonko/tablewriter ---------------------------------------------------------------- Copyright (C) 2014 by Oleku Konko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/philhofer/fwd https://github.com/philhofer/fwd ---------------------------------------------------------------- Copyright (c) 2014-2015, Philip Hofer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/pkg/xattr https://github.com/pkg/xattr ---------------------------------------------------------------- Copyright (c) 2012 Dave Cheney. All rights reserved. Copyright (c) 2014 Kuba Podgórski. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/pmezard/go-difflib https://github.com/pmezard/go-difflib ---------------------------------------------------------------- Copyright (c) 2013, Patrick Mezard All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/posener/complete https://github.com/posener/complete ---------------------------------------------------------------- The MIT License Copyright (c) 2017 Eyal Posener Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/power-devops/perfstat https://github.com/power-devops/perfstat ---------------------------------------------------------------- MIT License Copyright (c) 2020 Power DevOps Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/prometheus/client_golang https://github.com/prometheus/client_golang ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/prometheus/client_model https://github.com/prometheus/client_model ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/prometheus/common https://github.com/prometheus/common ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/prometheus/procfs https://github.com/prometheus/procfs ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/prometheus/prom2json https://github.com/prometheus/prom2json ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/prometheus/prometheus https://github.com/prometheus/prometheus ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/rivo/uniseg https://github.com/rivo/uniseg ---------------------------------------------------------------- MIT License Copyright (c) 2019 Oliver Kuederle Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/rjeczalik/notify https://github.com/rjeczalik/notify ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2014-2015 The Notify Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/rogpeppe/go-internal https://github.com/rogpeppe/go-internal ---------------------------------------------------------------- Copyright (c) 2018 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/rs/xid https://github.com/rs/xid ---------------------------------------------------------------- Copyright (c) 2015 Olivier Poitrey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/safchain/ethtool https://github.com/safchain/ethtool ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright (c) 2015 The Ethtool Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/secure-io/sio-go https://github.com/secure-io/sio-go ---------------------------------------------------------------- MIT License Copyright (c) 2019 SecureIO Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/segmentio/asm https://github.com/segmentio/asm ---------------------------------------------------------------- MIT License Copyright (c) 2021 Segment Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/shirou/gopsutil/v3 https://github.com/shirou/gopsutil/v3 ---------------------------------------------------------------- gopsutil is distributed under BSD license reproduced below. Copyright (c) 2014, WAKAYAMA Shirou All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the gopsutil authors nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------- internal/common/binary.go in the gopsutil is copied and modified from golang/encoding/binary.go. Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/shoenig/go-m1cpu https://github.com/shoenig/go-m1cpu ---------------------------------------------------------------- Mozilla Public License, version 2.0 1. Definitions 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means a. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or b. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: a. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or b. any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: a. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and b. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: a. for any code that a Contributor has removed from Covered Software; or b. for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or c. under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: a. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and b. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an "as is" basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================================ github.com/shoenig/test https://github.com/shoenig/test ---------------------------------------------------------------- Mozilla Public License, version 2.0 1. Definitions 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means a. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or b. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: a. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or b. any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: a. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and b. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: a. for any code that a Contributor has removed from Covered Software; or b. for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or c. under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: a. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and b. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an "as is" basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================================ github.com/stretchr/testify https://github.com/stretchr/testify ---------------------------------------------------------------- MIT License Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/tidwall/gjson https://github.com/tidwall/gjson ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2016 Josh Baker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/tidwall/match https://github.com/tidwall/match ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2016 Josh Baker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/tidwall/pretty https://github.com/tidwall/pretty ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2017 Josh Baker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/tinylib/msgp https://github.com/tinylib/msgp ---------------------------------------------------------------- Copyright (c) 2014 Philip Hofer Portions Copyright (c) 2009 The Go Authors (license at http://golang.org) where indicated Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ github.com/tklauser/go-sysconf https://github.com/tklauser/go-sysconf ---------------------------------------------------------------- BSD 3-Clause License Copyright (c) 2018-2022, Tobias Klauser All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ github.com/tklauser/numcpus https://github.com/tklauser/numcpus ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ github.com/vbauerster/mpb/v8 https://github.com/vbauerster/mpb/v8 ---------------------------------------------------------------- This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ================================================================ github.com/yusufpapurcu/wmi https://github.com/yusufpapurcu/wmi ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2013 Stack Exchange Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ go.etcd.io/etcd/api/v3 https://go.etcd.io/etcd/api/v3 ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ go.etcd.io/etcd/client/pkg/v3 https://go.etcd.io/etcd/client/pkg/v3 ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ go.etcd.io/etcd/client/v3 https://go.etcd.io/etcd/client/v3 ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ go.opentelemetry.io/auto/sdk https://go.opentelemetry.io/auto/sdk ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ go.opentelemetry.io/otel https://go.opentelemetry.io/otel ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ go.opentelemetry.io/otel/metric https://go.opentelemetry.io/otel/metric ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ go.opentelemetry.io/otel/sdk https://go.opentelemetry.io/otel/sdk ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ go.opentelemetry.io/otel/sdk/metric https://go.opentelemetry.io/otel/sdk/metric ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ go.opentelemetry.io/otel/trace https://go.opentelemetry.io/otel/trace ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ go.uber.org/goleak https://go.uber.org/goleak ---------------------------------------------------------------- The MIT License (MIT) Copyright (c) 2018 Uber Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ go.uber.org/multierr https://go.uber.org/multierr ---------------------------------------------------------------- Copyright (c) 2017-2021 Uber Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ go.uber.org/zap https://go.uber.org/zap ---------------------------------------------------------------- Copyright (c) 2016-2017 Uber Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================ golang.org/x/crypto https://golang.org/x/crypto ---------------------------------------------------------------- Copyright 2009 The Go Authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ golang.org/x/net https://golang.org/x/net ---------------------------------------------------------------- Copyright 2009 The Go Authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ golang.org/x/sync https://golang.org/x/sync ---------------------------------------------------------------- Copyright 2009 The Go Authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ golang.org/x/sys https://golang.org/x/sys ---------------------------------------------------------------- Copyright 2009 The Go Authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ golang.org/x/term https://golang.org/x/term ---------------------------------------------------------------- Copyright 2009 The Go Authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ golang.org/x/text https://golang.org/x/text ---------------------------------------------------------------- Copyright 2009 The Go Authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ google.golang.org/genproto/googleapis/api https://google.golang.org/genproto/googleapis/api ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ google.golang.org/genproto/googleapis/rpc https://google.golang.org/genproto/googleapis/rpc ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ google.golang.org/grpc https://google.golang.org/grpc ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ google.golang.org/protobuf https://google.golang.org/protobuf ---------------------------------------------------------------- Copyright (c) 2018 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ gopkg.in/check.v1 https://gopkg.in/check.v1 ---------------------------------------------------------------- Gocheck - A rich testing framework for Go Copyright (c) 2010-2013 Gustavo Niemeyer All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ gopkg.in/yaml.v2 https://gopkg.in/yaml.v2 ---------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ gopkg.in/yaml.v3 https://gopkg.in/yaml.v3 ---------------------------------------------------------------- This project is covered by two different licenses: MIT and Apache. #### MIT License #### The following files were ported to Go from C files of libyaml, and thus are still covered by their original MIT license, with the additional copyright staring in 2011 when the project was ported over: apic.go emitterc.go parserc.go readerc.go scannerc.go writerc.go yamlh.go yamlprivateh.go Copyright (c) 2006-2010 Kirill Simonov Copyright (c) 2006-2011 Kirill Simonov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ### Apache License ### All the remaining project files are covered by the Apache license: Copyright (c) 2011-2019 Canonical Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================ minio-client-0.0~20250403/Dockerfile000066400000000000000000000012571477450377600170010ustar00rootroot00000000000000FROM golang:1.22-alpine as build LABEL maintainer="MinIO Inc " ENV GOPATH /go ENV CGO_ENABLED 0 RUN apk add -U --no-cache ca-certificates RUN apk add -U curl RUN curl -s -q https://raw.githubusercontent.com/minio/mc/master/LICENSE -o /go/LICENSE RUN curl -s -q https://raw.githubusercontent.com/minio/mc/master/CREDITS -o /go/CREDITS RUN go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" "github.com/minio/mc@latest" FROM scratch COPY --from=build /go/bin/mc /usr/bin/mc COPY --from=build /go/CREDITS /licenses/CREDITS COPY --from=build /go/LICENSE /licenses/LICENSE COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ ENTRYPOINT ["mc"] minio-client-0.0~20250403/Dockerfile.dev000066400000000000000000000005231477450377600175510ustar00rootroot00000000000000FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 LABEL maintainer="MinIO Inc " COPY mc /usr/bin/ COPY CREDITS /licenses/CREDITS COPY LICENSE /licenses/LICENSE RUN \ microdnf update --nodocs --assumeyes && \ microdnf install ca-certificates --nodocs --assumeyes && \ microdnf clean all ENTRYPOINT ["mc"] minio-client-0.0~20250403/Dockerfile.hotfix000066400000000000000000000013631477450377600202770ustar00rootroot00000000000000FROM registry.access.redhat.com/ubi9/ubi-minimal:latest as build RUN microdnf update --nodocs --assumeyes && microdnf install ca-certificates --nodocs --assumeyes FROM registry.access.redhat.com/ubi9/ubi-micro:latest ARG TARGETARCH ARG RELEASE LABEL maintainer="MinIO Inc " # On RHEL the certificate bundle is located at: # - /etc/pki/tls/certs/ca-bundle.crt (RHEL 6) # - /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem (RHEL 7) COPY --from=build /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem /etc/pki/ca-trust/extracted/pem/ COPY CREDITS /licenses/CREDITS COPY LICENSE /licenses/LICENSE ADD https://dl.min.io/client/mc/hotfixes/linux-${TARGETARCH}/archive/mc.${RELEASE} /usr/bin/mc RUN chmod +x /usr/bin/mc ENTRYPOINT ["mc"] minio-client-0.0~20250403/Dockerfile.release000066400000000000000000000013641477450377600204170ustar00rootroot00000000000000FROM registry.access.redhat.com/ubi9/ubi-minimal:latest AS build RUN microdnf update --nodocs --assumeyes && microdnf install ca-certificates --nodocs --assumeyes FROM registry.access.redhat.com/ubi9/ubi-micro:latest ARG TARGETARCH ARG RELEASE LABEL maintainer="MinIO Inc " # On RHEL the certificate bundle is located at: # - /etc/pki/tls/certs/ca-bundle.crt (RHEL 6) # - /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem (RHEL 7) COPY --from=build /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem /etc/pki/ca-trust/extracted/pem/ COPY CREDITS /licenses/CREDITS COPY LICENSE /licenses/LICENSE ADD https://dl.minio.io/client/mc/release/linux-${TARGETARCH}/archive/mc.${RELEASE} /usr/bin/mc RUN chmod +x /usr/bin/mc ENTRYPOINT ["mc"] minio-client-0.0~20250403/Dockerfile.release.fips000066400000000000000000000013711477450377600213550ustar00rootroot00000000000000FROM registry.access.redhat.com/ubi9/ubi-minimal:latest AS build RUN microdnf update --nodocs --assumeyes && microdnf install ca-certificates --nodocs --assumeyes FROM registry.access.redhat.com/ubi9/ubi-micro:latest ARG TARGETARCH ARG RELEASE LABEL maintainer="MinIO Inc " # On RHEL the certificate bundle is located at: # - /etc/pki/tls/certs/ca-bundle.crt (RHEL 6) # - /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem (RHEL 7) COPY --from=build /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem /etc/pki/ca-trust/extracted/pem/ COPY CREDITS /licenses/CREDITS COPY LICENSE /licenses/LICENSE ADD https://dl.minio.io/client/mc/release/linux-${TARGETARCH}/archive/mc.${RELEASE}.fips /usr/bin/mc RUN chmod +x /usr/bin/mc ENTRYPOINT ["mc"] minio-client-0.0~20250403/Dockerfile.release.old_cpu000066400000000000000000000013641477450377600220430ustar00rootroot00000000000000FROM registry.access.redhat.com/ubi8/ubi-minimal:latest AS build RUN microdnf update --nodocs --assumeyes && microdnf install ca-certificates --nodocs --assumeyes FROM registry.access.redhat.com/ubi8/ubi-micro:latest ARG TARGETARCH ARG RELEASE LABEL maintainer="MinIO Inc " # On RHEL the certificate bundle is located at: # - /etc/pki/tls/certs/ca-bundle.crt (RHEL 6) # - /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem (RHEL 7) COPY --from=build /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem /etc/pki/ca-trust/extracted/pem/ COPY CREDITS /licenses/CREDITS COPY LICENSE /licenses/LICENSE ADD https://dl.minio.io/client/mc/release/linux-${TARGETARCH}/archive/mc.${RELEASE} /usr/bin/mc RUN chmod +x /usr/bin/mc ENTRYPOINT ["mc"] minio-client-0.0~20250403/LICENSE000066400000000000000000001033331477450377600160120ustar00rootroot00000000000000 GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . minio-client-0.0~20250403/Makefile000066400000000000000000000077761477450377600164630ustar00rootroot00000000000000PWD := $(shell pwd) GOPATH := $(shell go env GOPATH) LDFLAGS := $(shell go run buildscripts/gen-ldflags.go) TARGET_GOARCH ?= $(shell go env GOARCH) TARGET_GOOS ?= $(shell go env GOOS) VERSION ?= $(shell git describe --tags) TAG ?= "minio/mc:$(VERSION)" GOLANGCI_DIR = .bin/golangci/$(GOLANGCI_VERSION) GOLANGCI = $(GOLANGCI_DIR)/golangci-lint all: build checks: @echo "Checking dependencies" @(env bash $(PWD)/buildscripts/checkdeps.sh) getdeps: @mkdir -p ${GOPATH}/bin @echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOLANGCI_DIR) @echo "Installing stringer" && go install -v golang.org/x/tools/cmd/stringer@latest crosscompile: @(env bash $(PWD)/buildscripts/cross-compile.sh) verifiers: getdeps vet lint docker: build @docker build -t $(TAG) . -f Dockerfile.dev vet: @echo "Running $@" @GO111MODULE=on go vet github.com/minio/mc/... lint-fix: getdeps ## runs golangci-lint suite of linters with automatic fixes @echo "Running $@ check" @$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml --fix lint: getdeps @echo "Running $@ check" @$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml # Builds mc, runs the verifiers then runs the tests. check: test test: verifiers build @echo "Running unit tests" @GO111MODULE=on CGO_ENABLED=0 go test -tags kqueue ./... 1>/dev/null @echo "Running functional tests" @GO111MODULE=on MC_TEST_RUN_FULL_SUITE=true go test -race -v --timeout 20m ./... -run Test_FullSuite test-race: verifiers build @echo "Running unit tests under -race" @GO111MODULE=on go test -race -v --timeout 20m ./... 1>/dev/null # Verify mc binary verify: @echo "Verifying build with race" @GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/mc 1>/dev/null @echo "Running functional tests" @GO111MODULE=on MC_TEST_RUN_FULL_SUITE=true go test -race -v --timeout 20m ./... -run Test_FullSuite # Builds mc locally. build: checks @echo "Building mc binary to './mc'" @GO111MODULE=on GOOS=$(TARGET_GOOS) GOARCH=$(TARGET_GOARCH) CGO_ENABLED=0 go build -trimpath -tags kqueue --ldflags "$(LDFLAGS)" -o $(PWD)/mc hotfix-vars: $(eval LDFLAGS := $(shell MC_RELEASE="RELEASE" MC_HOTFIX="hotfix.$(shell git rev-parse --short HEAD)" go run buildscripts/gen-ldflags.go $(shell git describe --tags --abbrev=0 | \ sed 's#RELEASE\.\([0-9]\+\)-\([0-9]\+\)-\([0-9]\+\)T\([0-9]\+\)-\([0-9]\+\)-\([0-9]\+\)Z#\1-\2-\3T\4:\5:\6Z#'))) $(eval VERSION := $(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD)) $(eval TAG := "minio/mc:$(VERSION)") hotfix: hotfix-vars install ## builds mc binary with hotfix tags @mv -f ./mc ./mc.$(VERSION) @minisign -qQSm ./mc.$(VERSION) -s "${CRED_DIR}/minisign.key" < "${CRED_DIR}/minisign-passphrase" @sha256sum < ./mc.$(VERSION) | sed 's, -,mc.$(VERSION),g' > mc.$(VERSION).sha256sum hotfix-push: hotfix @scp -q -r mc.$(VERSION)* minio@dl-0.min.io:~/releases/client/mc/hotfixes/$(TARGET_GOOS)-$(TARGET_GOARCH)/archive/ @scp -q -r mc.$(VERSION)* minio@dl-1.min.io:~/releases/client/mc/hotfixes/$(TARGET_GOOS)-$(TARGET_GOARCH)/archive/ @echo "Published new hotfix binaries at https://dl.min.io/client/mc/hotfixes/$(TARGET_GOOS)-$(TARGET_GOARCH)/archive/mc.$(VERSION)" docker-hotfix-push: docker-hotfix @docker push -q $(TAG) && echo "Published new container $(TAG)" docker-hotfix: hotfix-push checks ## builds mc docker container with hotfix tags @echo "Building mc docker image '$(TAG)'" @docker build -q --no-cache -t $(TAG) --build-arg RELEASE=$(VERSION) . -f Dockerfile.hotfix # Builds MinIO and installs it to $GOPATH/bin. install: build @echo "Installing mc binary to '$(GOPATH)/bin/mc'" @mkdir -p $(GOPATH)/bin && cp -f $(PWD)/mc $(GOPATH)/bin/mc @echo "Installation successful. To learn more, try \"mc --help\"." clean: @echo "Cleaning up all the generated files" @find . -name '*.test' | xargs rm -fv @find . -name '*~' | xargs rm -fv @rm -rvf mc @rm -rvf build @rm -rvf release minio-client-0.0~20250403/NOTICE000066400000000000000000000005421477450377600157070ustar00rootroot00000000000000MinIO Client (C) 2014-2020 MinIO, Inc. This product includes software developed at MinIO, Inc. (https://min.io/). The MinIO project contains unmodified/modified subcomponents too with separate copyright notices and license terms. Your use of the source code for the these subcomponents is subject to the terms and conditions of the following licenses. minio-client-0.0~20250403/README.md000066400000000000000000000215001477450377600162570ustar00rootroot00000000000000# MinIO Client Quickstart Guide [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io) [![Go Report Card](https://goreportcard.com/badge/minio/mc)](https://goreportcard.com/report/minio/mc) [![Docker Pulls](https://img.shields.io/docker/pulls/minio/mc.svg?maxAge=604800)](https://hub.docker.com/r/minio/mc/) [![license](https://img.shields.io/badge/license-AGPL%20V3-blue)](https://github.com/minio/mc/blob/master/LICENSE) # Documentation - [MC documentation](https://min.io/docs/minio/linux/reference/minio-mc.html) MinIO Client (mc) provides a modern alternative to UNIX commands like ls, cat, cp, mirror, diff, find etc. It supports filesystems and Amazon S3 compatible cloud storage service (AWS Signature v2 and v4). ``` alias manage server credentials in configuration file admin manage MinIO servers anonymous manage anonymous access to buckets and objects batch manage batch jobs cp copy objects cat display object contents diff list differences in object name, size, and date between two buckets du summarize disk usage recursively encrypt manage bucket encryption config event manage object notifications find search for objects get get s3 object to local head display first 'n' lines of an object ilm manage bucket lifecycle idp manage MinIO IDentity Provider server configuration license license related commands legalhold manage legal hold for object(s) ls list buckets and objects mb make a bucket mv move objects mirror synchronize object(s) to a remote site od measure single stream upload and download ping perform liveness check pipe stream STDIN to an object put upload an object to a bucket quota manage bucket quota rm remove object(s) retention set retention for object(s) rb remove a bucket replicate configure server side bucket replication ready checks if the cluster is ready or not sql run sql queries on objects stat show object metadata support support related commands share generate URL for temporary access to an object tree list buckets and objects in a tree format tag manage tags for bucket and object(s) undo undo PUT/DELETE operations update update mc to latest release version manage bucket versioning watch listen for object notification events ``` ## Docker Container ### Stable ``` docker pull minio/mc docker run minio/mc ls play ``` ### Edge ``` docker pull minio/mc:edge docker run minio/mc:edge ls play ``` **Note:** Above examples run `mc` against MinIO [_play_ environment](#test-your-setup) by default. To run `mc` against other S3 compatible servers, start the container this way: ``` docker run -it --entrypoint=/bin/sh minio/mc ``` then use the [`mc alias` command](#add-a-cloud-storage-service). ### GitLab CI When using the Docker container in GitLab CI, you must [set the entrypoint to an empty string](https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#override-the-entrypoint-of-an-image). ``` deploy: image: name: minio/mc entrypoint: [''] stage: deploy before_script: - mc alias set minio $MINIO_HOST $MINIO_ACCESS_KEY $MINIO_SECRET_KEY script: - mc cp ``` ## macOS ### Homebrew Install mc packages using [Homebrew](http://brew.sh/) ``` brew install minio/stable/mc mc --help ``` ## GNU/Linux ### Binary Download | Platform | Architecture | URL | | ---------- | -------- |------| |GNU/Linux|64-bit Intel|https://dl.min.io/client/mc/release/linux-amd64/mc | |GNU/Linux|64-bit PPC|https://dl.min.io/client/mc/release/linux-ppc64le/mc | |GNU/Linux|64-bit ARM|https://dl.min.io/client/mc/release/linux-arm64/mc | |Linux/s390x|S390X|https://dl.min.io/client/mc/release/linux-s390x/mc | ``` wget https://dl.min.io/client/mc/release/linux-amd64/mc chmod +x mc ./mc --help ``` ## Microsoft Windows ### Binary Download | Platform | Architecture | URL | | ---------- | -------- |------| |Microsoft Windows|64-bit Intel|https://dl.min.io/client/mc/release/windows-amd64/mc.exe | ``` mc.exe --help ``` ## Install from Source Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://golang.org/doc/install). Minimum version required is [go1.22](https://golang.org/dl/#stable) ```sh go install github.com/minio/mc@latest ``` ## Add a Cloud Storage Service If you are planning to use `mc` only on POSIX compatible filesystems, you may skip this step and proceed to [everyday use](#everyday-use). To add one or more Amazon S3 compatible hosts, please follow the instructions below. `mc` stores all its configuration information in ``~/.mc/config.json`` file. ``` mc alias set --api --path ``` `` is simply a short name to your cloud storage service. S3 end-point, access and secret keys are supplied by your cloud storage provider. API signature is an optional argument. By default, it is set to "S3v4". Path is an optional argument. It is used to indicate whether dns or path style url requests are supported by the server. It accepts "on", "off" as valid values to enable/disable path style requests.. By default, it is set to "auto" and SDK automatically determines the type of url lookup to use. ### Example - MinIO Cloud Storage MinIO server startup banner displays URL, access and secret keys. ``` mc alias set minio http://192.168.1.51 BKIKJAA5BMMU2RHO6IBB V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12 ``` ### Example - Amazon S3 Cloud Storage Get your AccessKeyID and SecretAccessKey by following [AWS Credentials Guide](http://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html). ``` mc alias set s3 https://s3.amazonaws.com BKIKJAA5BMMU2RHO6IBB V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12 ``` **Note**: As an IAM user on Amazon S3 you need to make sure the user has full access to the buckets or set the following restricted policy for your IAM user ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowBucketStat", "Effect": "Allow", "Action": [ "s3:HeadBucket" ], "Resource": "*" }, { "Sid": "AllowThisBucketOnly", "Effect": "Allow", "Action": "s3:*", "Resource": [ "arn:aws:s3:::/*", "arn:aws:s3:::" ] } ] } ``` ### Example - Google Cloud Storage Get your AccessKeyID and SecretAccessKey by following [Google Credentials Guide](https://cloud.google.com/storage/docs/migrating?hl=en#keys) ``` mc alias set gcs https://storage.googleapis.com BKIKJAA5BMMU2RHO6IBB V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12 ``` ## Test Your Setup `mc` is pre-configured with https://play.min.io, aliased as "play". It is a hosted MinIO server for testing and development purpose. To test Amazon S3, simply replace "play" with "s3" or the alias you used at the time of setup. *Example:* List all buckets from https://play.min.io ``` mc ls play [2016-03-22 19:47:48 PDT] 0B my-bucketname/ [2016-03-22 22:01:07 PDT] 0B mytestbucket/ [2016-03-22 20:04:39 PDT] 0B mybucketname/ [2016-01-28 17:23:11 PST] 0B newbucket/ [2016-03-20 09:08:36 PDT] 0B s3git-test/ ``` Make a bucket `mb` command creates a new bucket. *Example:* ``` mc mb play/mybucket Bucket created successfully `play/mybucket`. ``` Copy Objects `cp` command copies data from one or more sources to a target. *Example:* ``` mc cp myobject.txt play/mybucket myobject.txt: 14 B / 14 B ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 100.00 % 41 B/s 0 ``` ## Everyday Use ### Shell aliases You may add shell aliases to override your common Unix tools. ``` alias ls='mc ls' alias cp='mc cp' alias cat='mc cat' alias mkdir='mc mb' alias pipe='mc pipe' alias find='mc find' ``` ### Shell autocompletion In case you are using bash, zsh or fish. Shell completion is embedded by default in `mc`, to install auto-completion use `mc --autocompletion`. Restart the shell, mc will auto-complete commands as shown below. ``` mc admin config diff find ls mirror policy session sql update watch cat cp event head mb pipe rm share stat version ``` ## Contribute to MinIO Project Please follow MinIO [Contributor's Guide](https://github.com/minio/mc/blob/master/CONTRIBUTING.md) ## License Use of `mc` is governed by the GNU AGPLv3 license that can be found in the [LICENSE](https://github.com/minio/mc/blob/master/LICENSE) file. minio-client-0.0~20250403/README_zh_CN.md000066400000000000000000000134541477450377600173510ustar00rootroot00000000000000# MinIO客户端快速入门指南 [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io) [![Go Report Card](https://goreportcard.com/badge/minio/mc)](https://goreportcard.com/report/minio/mc) [![Docker Pulls](https://img.shields.io/docker/pulls/minio/mc.svg?maxAge=604800)](https://hub.docker.com/r/minio/mc/) MinIO Client (mc)为ls,cat,cp,mirror,diff,find等UNIX命令提供了一种替代方案。它支持文件系统和兼容Amazon S3的云存储服务(AWS Signature v2和v4)。 ``` ls 列出文件和文件夹。 mb 创建一个存储桶或一个文件夹。 cat 显示文件和对象内容。 pipe 将一个STDIN重定向到一个对象或者文件或者STDOUT。 share 生成用于共享的URL。 cp 拷贝文件和对象。 mirror 给存储桶和文件夹做镜像。 find 基于参数查找文件。 diff 对两个文件夹或者存储桶比较差异。 rm 删除文件和对象。 events 管理对象通知。 watch 监听文件和对象的事件。 anonymous 管理访问策略。 session 为cp命令管理保存的会话。 config 管理mc配置文件。 update 检查软件更新。 version 输出版本信息。 ``` ## Docker容器 ### 稳定版 ``` docker pull minio/mc docker run minio/mc ls play ``` ### 尝鲜版 ``` docker pull minio/mc:edge docker run minio/mc:edge ls play ``` **注意:** 上述示例默认使用MinIO[演示环境](#test-your-setup)做演示,如果想用`mc`操作其它S3兼容的服务,采用下面的方式来启动容器: ``` docker run -it --entrypoint=/bin/sh minio/mc ``` 然后使用[`mc config`命令](#add-a-cloud-storage-service)。 ## macOS ### Homebrew 使用[Homebrew](http://brew.sh/)安装mc。 ``` brew install minio/stable/mc mc --help ``` ## GNU/Linux ### 下载二进制文件 | 平台 | CPU架构 | URL | | ---------- | -------- |------| |GNU/Linux|64-bit Intel|https://dl.min.io/client/mc/release/linux-amd64/mc | ``` chmod +x mc ./mc --help ``` ## Microsoft Windows ### 下载二进制文件 | 平台 | CPU架构 | URL | | ---------- | -------- |------| |Microsoft Windows|64-bit Intel|https://dl.min.io/client/mc/release/windows-amd64/mc.exe | ``` mc.exe --help ``` ## 通过源码安装 通过源码安装仅适用于开发人员和高级用户。`mc update`命令不支持基于源码安装的更新通知。请从https://min.io/download/#minio-client下载官方版本。 如果您没有Golang环境,请参照[如何安装Golang](https://golang.org/doc/install)。 ``` go get -d github.com/minio/mc cd ${GOPATH}/src/github.com/minio/mc make ``` ## 添加一个云存储服务 如果你打算仅在POSIX兼容文件系统中使用`mc`,那你可以直接略过本节,跳到[日常使用](#everyday-use)。 添加一个或多个S3兼容的服务,请参考下面说明。`mc`将所有的配置信息都存储在``~/.mc/config.json``文件中。 ``` mc alias set [--api API-SIGNATURE] ``` 别名就是给你的云存储服务起了一个短点的外号。S3 endpoint,access key和secret key是你的云存储服务提供的。API签名是可选参数,默认情况下,它被设置为"S3v4"。 ### 示例-MinIO云存储 从MinIO服务获得URL、access key和secret key。 ``` mc alias set minio http://192.168.1.51 BKIKJAA5BMMU2RHO6IBB V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12 --api s3v4 ``` ### 示例-Amazon S3云存储 参考[AWS Credentials指南](http://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html)获取你的AccessKeyID和SecretAccessKey。 ``` mc alias set s3 https://s3.amazonaws.com BKIKJAA5BMMU2RHO6IBB V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12 --api s3v4 ``` ### 示例-Google云存储 参考[Google Credentials Guide](https://cloud.google.com/storage/docs/migrating?hl=en#keys)获取你的AccessKeyID和SecretAccessKey。 ``` mc alias set gcs https://storage.googleapis.com BKIKJAA5BMMU2RHO6IBB V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12 --api s3v2 ``` 注意:Google云存储只支持旧版签名版本V2,所以你需要选择S3v2。 ## 验证 `mc`预先配置了云存储服务URL:https://play.min.io,别名“play”。它是一个用于研发和测试的MinIO服务。如果想测试Amazon S3,你可以将“play”替换为“s3”。 *示例:* 列出https://play.min.io上的所有存储桶。 ``` mc ls play [2016-03-22 19:47:48 PDT] 0B my-bucketname/ [2016-03-22 22:01:07 PDT] 0B mytestbucket/ [2016-03-22 20:04:39 PDT] 0B mybucketname/ [2016-01-28 17:23:11 PST] 0B newbucket/ [2016-03-20 09:08:36 PDT] 0B s3git-test/ ``` ## 日常使用 ### Shell别名 你可以添加shell别名来覆盖默认的Unix工具命令。 ``` alias ls='mc ls' alias cp='mc cp' alias cat='mc cat' alias mkdir='mc mb' alias pipe='mc pipe' alias find='mc find' ``` ### Shell自动补全 你也可以下载[`autocomplete/bash_autocomplete`](https://raw.githubusercontent.com/minio/mc/master/autocomplete/bash_autocomplete)到`/etc/bash_completion.d/`,然后将其重命名为`mc`。别忘了在这个文件运行source命令让其在你的当前shell上可用。 ``` sudo wget https://raw.githubusercontent.com/minio/mc/master/autocomplete/bash_autocomplete -O /etc/bash_completion.d/mc source /etc/bash_completion.d/mc ``` ``` mc admin config diff ls mirror policy session update watch cat cp events mb pipe rm share version ``` ## 了解更多 - [MinIO Client完全指南](https://min.io/docs/minio/linux/reference/minio-mc.html?ref=gh) - [MinIO快速入门](https://min.io/docs/minio/linux/index.html#quickstart-for-linux?ref=gh) - [MinIO官方文档](https://min.io/docs/minio/linux/index.html?ref=gh) ## 贡献 请遵守MinIO[贡献者指南](https://github.com/minio/mc/blob/master/docs/zh_CN/CONTRIBUTING.md) minio-client-0.0~20250403/_config.yml000066400000000000000000000000331477450377600171250ustar00rootroot00000000000000theme: jekyll-theme-minimalminio-client-0.0~20250403/buildscripts/000077500000000000000000000000001477450377600175115ustar00rootroot00000000000000minio-client-0.0~20250403/buildscripts/build.env000066400000000000000000000011621477450377600213220ustar00rootroot00000000000000## FIXME: ## In OSX, 'sort -V' option does not exist, hence ## we have our own version compare function. ## Once OSX has the option, below function is good enough. ## ## check_minimum_version() { ## versions=($(echo -e "$1\n$2" | sort -V)) ## return [ "$1" == "${versions[0]}" ] ## } ## check_minimum_version() { IFS='.' read -r -a varray1 <<< "$1" IFS='.' read -r -a varray2 <<< "$2" for i in "${!varray1[@]}"; do if [[ ${varray1[i]} -lt ${varray2[i]} ]]; then return 0 elif [[ ${varray1[i]} -gt ${varray2[i]} ]]; then return 1 fi done return 0 } minio-client-0.0~20250403/buildscripts/build.sh000077500000000000000000000100671477450377600211530ustar00rootroot00000000000000#!/bin/bash # # Copyright (c) 2015-2021 MinIO, Inc. # # This file is part of MinIO Object Storage stack # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # _init() { # Save release LDFLAGS LDFLAGS=$(go run buildscripts/gen-ldflags.go) # Extract release tag release_tag=$(echo $LDFLAGS | awk {'print $6'} | cut -f2 -d=) # Verify release tag. if [ -z "$release_tag" ]; then echo "Release tag cannot be empty. Please check return value of \`go run buildscripts/gen-ldflags.go\`" exit 1; fi # Extract release string. release_str=$(echo $MC_RELEASE | tr '[:upper:]' '[:lower:]') # Verify release string. if [ -z "$release_str" ]; then echo "Release string cannot be empty. Please set \`MC_RELEASE\` env variable." exit 1; fi # List of supported architectures SUPPORTED_OSARCH='linux/amd64 linux/ppc64le windows/amd64 darwin/amd64' ## System binaries CP=`which cp` SHASUM=`which shasum` SHA256SUM="${SHASUM} -a 256" SED=`which sed` } go_build() { local osarch=$1 os=$(echo $osarch | cut -f1 -d'/') arch=$(echo $osarch | cut -f2 -d'/') package=$(go list -f '{{.ImportPath}}') echo -n "-->" printf "%15s:%s\n" "${osarch}" "${package}" # Release binary name release_bin="$release_str/$os-$arch/$(basename $package).$release_tag" # Release binary downloadable name release_real_bin="$release_str/$os-$arch/$(basename $package)" # Release sha1sum name release_shasum="$release_str/$os-$arch/$(basename $package).${release_tag}.shasum" # Release sha1sum default release_shasum_default="$release_str/$os-$arch/$(basename $package).shasum" # Release sha256sum name release_sha256sum="$release_str/$os-$arch/$(basename $package).${release_tag}.sha256sum" # Release sha256sum default release_sha256sum_default="$release_str/$os-$arch/$(basename $package).sha256sum" # Go build to build the binary. CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -tags kqueue --ldflags "${LDFLAGS}" -o $release_bin # Create copy if [ $os == "windows" ]; then $CP -p $release_bin ${release_real_bin}.exe else $CP -p $release_bin $release_real_bin fi # Calculate sha1sum shasum_str=$(${SHASUM} ${release_bin}) echo ${shasum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_shasum $CP -p $release_shasum $release_shasum_default # Calculate sha256sum sha256sum_str=$(${SHA256SUM} ${release_bin}) echo ${sha256sum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_sha256sum $CP -p $release_sha256sum $release_sha256sum_default } main() { # Build releases. echo "Executing $release_str builds for OS: ${SUPPORTED_OSARCH}" echo "Choose an OS Arch from the below" for osarch in ${SUPPORTED_OSARCH}; do echo ${osarch} done read -p "If you want to build for all, Just press Enter: " chosen_osarch if [ "$chosen_osarch" = "" ] || [ "$chosen_osarch" = "all" ]; then for each_osarch in ${SUPPORTED_OSARCH}; do go_build ${each_osarch} done else local found=0 for each_osarch in ${SUPPORTED_OSARCH}; do if [ "$chosen_osarch" = "$each_osarch" ]; then found=1 fi done if [ ${found} -eq 1 ]; then go_build ${chosen_osarch} else echo "Unknown architecture \"${chosen_osarch}\"" exit 1 fi fi } # Run main. _init && main minio-client-0.0~20250403/buildscripts/checkdeps.sh000077500000000000000000000100421477450377600217760ustar00rootroot00000000000000#!/usr/bin/env bash # # Copyright (c) 2015-2021 MinIO, Inc. # # This file is part of MinIO Object Storage stack # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # # shellcheck source=buildscripts/build.env . "$(pwd)/buildscripts/build.env" _init() { shopt -s extglob ## Minimum required versions for build dependencies GIT_VERSION="1.0" GO_VERSION="1.13" OSX_VERSION="10.8" KNAME=$(uname -s) ARCH=$(uname -m) case "${KNAME}" in SunOS ) ARCH=$(isainfo -k) ;; esac } ## FIXME: ## In OSX, 'readlink -f' option does not exist, hence ## we have our own readlink -f behavior here. ## Once OSX has the option, below function is good enough. ## ## readlink() { ## return /bin/readlink -f "$1" ## } ## readlink() { TARGET_FILE=$1 cd `dirname $TARGET_FILE` TARGET_FILE=`basename $TARGET_FILE` # Iterate down a (possible) chain of symlinks while [ -L "$TARGET_FILE" ] do TARGET_FILE=$(env readlink $TARGET_FILE) cd `dirname $TARGET_FILE` TARGET_FILE=`basename $TARGET_FILE` done # Compute the canonicalized name by finding the physical path # for the directory we're in and appending the target file. PHYS_DIR=`pwd -P` RESULT=$PHYS_DIR/$TARGET_FILE echo $RESULT } assert_is_supported_arch() { case "${ARCH}" in x86_64 | amd64 | ppc64le | aarch64 | arm* | s390x | loong64 | loongarch64 ) return ;; *) echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, ppc64le, aarch64, arm*, s390x, loong64, loongarch64]" exit 1 esac } assert_is_supported_os() { case "${KNAME}" in Linux | FreeBSD | OpenBSD | NetBSD | DragonFly | SunOS ) return ;; Darwin ) osx_host_version=$(env sw_vers -productVersion) if ! check_minimum_version "${OSX_VERSION}" "${osx_host_version}"; then echo "OSX version '${osx_host_version}' is not supported. Minimum supported version: ${OSX_VERSION}" exit 1 fi return ;; *) echo "OS '${KNAME}' is not supported. Supported OS: [Linux, FreeBSD, OpenBSD, NetBSD, Darwin, DragonFly]" exit 1 esac } assert_check_golang_env() { if ! which go >/dev/null 2>&1; then echo "Cannot find go binary in your PATH configuration, please refer to Go installation document at https://golang.org/doc/install" exit 1 fi installed_go_version=$(go version | sed 's/^.* go\([0-9.]*\).*$/\1/') if ! check_minimum_version "${GO_VERSION}" "${installed_go_version}"; then echo "Go runtime version '${installed_go_version}' is unsupported. Minimum supported version: ${GO_VERSION} to compile." exit 1 fi } assert_check_deps() { # support unusual Git versions such as: 2.7.4 (Apple Git-66) installed_git_version=$(git version | perl -ne '$_ =~ m/git version (.*?)( |$)/; print "$1\n";') if ! check_minimum_version "${GIT_VERSION}" "${installed_git_version}"; then echo "Git version '${installed_git_version}' is not supported. Minimum supported version: ${GIT_VERSION}" exit 1 fi } main() { ## Check for supported arch assert_is_supported_arch ## Check for supported os assert_is_supported_os ## Check for Go environment assert_check_golang_env ## Check for dependencies assert_check_deps } _init && main "$@" minio-client-0.0~20250403/buildscripts/cross-compile.sh000077500000000000000000000032601477450377600226300ustar00rootroot00000000000000#!/bin/bash # # Copyright (c) 2015-2021 MinIO, Inc. # # This file is part of MinIO Object Storage stack # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # set -e # Enable tracing if set. [ -n "$BASH_XTRACEFD" ] && set -x function _init() { ## All binaries are static make sure to disable CGO. export CGO_ENABLED=0 ## List of architectures and OS to test coss compilation. SUPPORTED_OSARCH="linux/ppc64le linux/s390x linux/mips64 linux/amd64 windows/amd64 darwin/amd64 darwin/arm64 linux/arm64 linux/arm" } function _build() { local osarch=$1 IFS=/ read -r -a arr <<<"$osarch" os="${arr[0]}" arch="${arr[1]}" package=$(go list -f '{{.ImportPath}}') printf -- "--> %15s:%s\n" "${osarch}" "${package}" # Go build to build the binary. export GOOS=$os export GOARCH=$arch export GO111MODULE=on export CGO_ENABLED=0 go build -tags kqueue -o /dev/null } function main() { echo "Testing builds for OS/Arch: ${SUPPORTED_OSARCH}" for each_osarch in ${SUPPORTED_OSARCH}; do _build "${each_osarch}" done } _init && main "$@" minio-client-0.0~20250403/buildscripts/gen-ldflags.go000066400000000000000000000063271477450377600222330ustar00rootroot00000000000000//go:build ignore // +build ignore // Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package main import ( "fmt" "os" "os/exec" "strings" "time" ) func genLDFlags(version string) string { releaseTag, date := releaseTag(version) copyrightYear := fmt.Sprintf("%d", date.Year()) var ldflagsStr string ldflagsStr = "-s -w -X github.com/minio/mc/cmd.Version=" + version + " " ldflagsStr = ldflagsStr + "-X github.com/minio/mc/cmd.CopyrightYear=" + copyrightYear + " " ldflagsStr = ldflagsStr + "-X github.com/minio/mc/cmd.ReleaseTag=" + releaseTag + " " ldflagsStr = ldflagsStr + "-X github.com/minio/mc/cmd.CommitID=" + commitID() + " " ldflagsStr = ldflagsStr + "-X github.com/minio/mc/cmd.ShortCommitID=" + commitID()[:12] return ldflagsStr } // releaseTag prints release tag to the console for easy git tagging. func releaseTag(version string) (string, time.Time) { relPrefix := "DEVELOPMENT" if prefix := os.Getenv("MC_RELEASE"); prefix != "" { relPrefix = prefix } relSuffix := "" if hotfix := os.Getenv("MC_HOTFIX"); hotfix != "" { relSuffix = hotfix } relTag := strings.ReplaceAll(version, " ", "-") relTag = strings.ReplaceAll(relTag, ":", "-") t, err := time.Parse("2006-01-02T15-04-05Z", relTag) if err != nil { panic(err) } relTag = strings.ReplaceAll(relTag, ",", "") relTag = relPrefix + "." + relTag if relSuffix != "" { relTag += "." + relSuffix } return relTag, t } // commitID returns the abbreviated commit-id hash of the last commit. func commitID() string { // git log --format="%h" -n1 var ( commit []byte e error ) cmdName := "git" cmdArgs := []string{"log", "--format=%H", "-n1"} if commit, e = exec.Command(cmdName, cmdArgs...).Output(); e != nil { fmt.Fprintln(os.Stderr, "Error generating git commit-id: ", e) os.Exit(1) } return strings.TrimSpace(string(commit)) } func commitTime() time.Time { // git log --format=%cD -n1 var ( commitUnix []byte err error ) cmdName := "git" cmdArgs := []string{"log", "--format=%cI", "-n1"} if commitUnix, err = exec.Command(cmdName, cmdArgs...).Output(); err != nil { fmt.Fprintln(os.Stderr, "Error generating git commit-time: ", err) os.Exit(1) } t, err := time.Parse(time.RFC3339, strings.TrimSpace(string(commitUnix))) if err != nil { fmt.Fprintln(os.Stderr, "Error generating git commit-time: ", err) os.Exit(1) } return t.UTC() } func main() { var version string if len(os.Args) > 1 { version = os.Args[1] } else { version = commitTime().Format(time.RFC3339) } fmt.Println(genLDFlags(version)) } minio-client-0.0~20250403/cmd/000077500000000000000000000000001477450377600155455ustar00rootroot00000000000000minio-client-0.0~20250403/cmd/access-perms.go000066400000000000000000000042111477450377600204570ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" json "github.com/minio/colorjson" "github.com/minio/minio-go/v7/pkg/policy" ) // isValidAccessPERM - is provided access perm string supported. func (b accessPerms) isValidAccessPERM() bool { switch b { case accessNone, accessDownload, accessUpload, accessPrivate, accessPublic: return true } return false } func (b accessPerms) isValidAccessFile() bool { file, err := os.Open(string(b)) if err != nil { return false } defer file.Close() var policy policy.BucketAccessPolicy if json.NewDecoder(file).Decode(&policy) != nil { fatalIf(errDummy().Trace(), "Unable to parse access file.") return false } if policy.Version != "2012-10-17" { fatalIf(errDummy().Trace(), "Invalid policy version. Only 2012-10-17 is supported.") return false } for _, statement := range policy.Statements { if statement.Effect != "Allow" && statement.Effect != "Deny" { fatalIf(errDummy().Trace(), "Invalid policy effect. Only Allow and Deny are supported.") return false } } return true } // accessPerms - access level. type accessPerms string // different types of Access perm's currently supported by policy command. const ( accessNone = accessPerms("none") accessDownload = accessPerms("download") accessUpload = accessPerms("upload") accessPrivate = accessPerms("private") accessPublic = accessPerms("public") accessCustom = accessPerms("custom") ) minio-client-0.0~20250403/cmd/accounting-reader.go000066400000000000000000000114661477450377600214760ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "strings" "sync" "sync/atomic" "time" "github.com/fatih/color" "github.com/minio/pkg/v3/console" "github.com/cheggaaa/pb" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" ) // accounter keeps tabs of ongoing data transfer information. type accounter struct { current int64 total int64 startTime time.Time startValue int64 refreshRate time.Duration currentValue int64 finishOnce sync.Once isFinished chan struct{} } // Instantiate a new accounter. func newAccounter(total int64) *accounter { acct := &accounter{ total: total, startTime: time.Now(), startValue: 0, refreshRate: time.Millisecond * 200, isFinished: make(chan struct{}), currentValue: -1, } go acct.writer() return acct } // write calculate the final speed. func (a *accounter) write(current int64) (float64, time.Duration) { fromStart := time.Since(a.startTime) currentFromStart := current - a.startValue if currentFromStart > 0 { speed := float64(currentFromStart) / (float64(fromStart) / float64(time.Second)) return speed, fromStart } return 0.0, 0 } // writer update new accounting data for a specified refreshRate. func (a *accounter) writer() { a.Update() for { select { case <-a.isFinished: return case <-time.After(a.refreshRate): a.Update() } } } // accountStat cantainer for current stats captured. type accountStat struct { Status string `json:"status"` Total int64 `json:"total"` Transferred int64 `json:"transferred"` Duration time.Duration `json:"duration"` Speed float64 `json:"speed"` } func (c accountStat) JSON() string { c.Status = "success" accountMessageBytes, e := json.MarshalIndent(c, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(accountMessageBytes) } func (c accountStat) String() string { dspOrder := []col{colGreen} // Header dspOrder = append(dspOrder, colGrey) var printColors []*color.Color for _, c := range dspOrder { printColors = append(printColors, getPrintCol(c)) } tbl := console.NewTable(printColors, []bool{false, false, false, false}, 0) var builder strings.Builder cellText := make([][]string, 0, 2) cellText = append(cellText, []string{ "Total", "Transferred", "Duration", "Speed", }) speedBox := pb.Format(int64(c.Speed)).To(pb.U_BYTES).String() if speedBox == "" { speedBox = "0 MB" } else { speedBox = speedBox + "/s" } cellText = append(cellText, []string{ pb.Format(c.Total).To(pb.U_BYTES).String(), pb.Format(c.Transferred).To(pb.U_BYTES).String(), pb.Format(int64(c.Duration)).To(pb.U_DURATION).String(), speedBox, }) e := tbl.PopulateTable(&builder, cellText) fatalIf(probe.NewError(e), "unable to populate the table") return builder.String() } // Stat provides current stats captured. func (a *accounter) Stat() accountStat { var acntStat accountStat a.finishOnce.Do(func() { close(a.isFinished) acntStat.Total = a.total acntStat.Transferred = atomic.LoadInt64(&a.current) acntStat.Speed, acntStat.Duration = a.write(atomic.LoadInt64(&a.current)) }) return acntStat } // Update update with new values loaded atomically. func (a *accounter) Update() { c := atomic.LoadInt64(&a.current) if c != a.currentValue { a.write(c) a.currentValue = c } } // Set sets the current value atomically. func (a *accounter) Set(n int64) *accounter { atomic.StoreInt64(&a.current, n) return a } // Get gets current value atomically func (a *accounter) Get() int64 { return atomic.LoadInt64(&a.current) } func (a *accounter) SetTotal(n int64) { atomic.StoreInt64(&a.total, n) } // Add add to current value atomically. func (a *accounter) Add(n int64) int64 { return atomic.AddInt64(&a.current, n) } // Read implements Reader which internally updates current value. func (a *accounter) Read(p []byte) (n int, err error) { defer func() { // Upload retry can read one object twice; Avoid read to be greater than Total if n, t := a.Get(), atomic.LoadInt64(&a.total); t > 0 && n > t { a.Set(t) } }() n = len(p) a.Add(int64(n)) return } minio-client-0.0~20250403/cmd/admin-accesskey-create.go000066400000000000000000000055661477450377600224110ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminAccesskeyCreateFlags = []cli.Flag{ cli.StringFlag{ Name: "access-key", Usage: "set an access key for the account", }, cli.StringFlag{ Name: "secret-key", Usage: "set a secret key for the account", }, cli.StringFlag{ Name: "policy", Usage: "path to a JSON policy file", }, cli.StringFlag{ Name: "name", Usage: "friendly name for the account", }, cli.StringFlag{ Name: "description", Usage: "description for the account", }, cli.StringFlag{ Name: "expiry-duration", Usage: "duration before the access key expires", }, cli.StringFlag{ Name: "expiry", Usage: "expiry date for the access key", }, } var adminAccesskeyCreateCmd = cli.Command{ Name: "create", Usage: "create access key pairs for users", Action: mainAdminAccesskeyCreate, Before: setGlobalsFromContext, Flags: append(adminAccesskeyCreateFlags, globalFlags...), OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] [TARGET] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Create a new access key pair with the same policy as the authenticated user {{.Prompt}} {{.HelpName}} myminio/ 2. Create a new access key pair with custom access key and secret key {{.Prompt}} {{.HelpName}} myminio/ --access-key myaccesskey --secret-key mysecretkey 3. Create a new access key pair for user 'tester' that expires in 1 day {{.Prompt}} {{.HelpName}} myminio/ tester --expiry-duration 24h 4. Create a new access key pair for authenticated user that expires on 2025-01-01 {{.Prompt}} {{.HelpName}} --expiry 2025-01-01 5. Create a new access key pair for user 'tester' with a custom policy {{.Prompt}} {{.HelpName}} myminio/ tester --policy /path/to/policy.json 6. Create a new access key pair for user 'tester' with a custom name and description {{.Prompt}} {{.HelpName}} myminio/ tester --name "Tester's Access Key" --description "Access key for tester" `, } func mainAdminAccesskeyCreate(ctx *cli.Context) error { return commonAccesskeyCreate(ctx, false) } minio-client-0.0~20250403/cmd/admin-accesskey-disable.go000066400000000000000000000025651477450377600225450ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminAccesskeyDisableCmd = cli.Command{ Name: "disable", Usage: "disable an access key", Action: mainAdminAccesskeyDisable, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] [TARGET] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Disable access key {{.Prompt}} {{.HelpName}} myminio myaccesskey `, } func mainAdminAccesskeyDisable(ctx *cli.Context) error { return enableDisableAccesskey(ctx, false) } minio-client-0.0~20250403/cmd/admin-accesskey-edit.go000066400000000000000000000041631477450377600220630ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminAccesskeyEditFlags = []cli.Flag{ cli.StringFlag{ Name: "secret-key", Usage: "set a secret key for the account", }, cli.StringFlag{ Name: "policy", Usage: "path to a JSON policy file", }, cli.StringFlag{ Name: "name", Usage: "friendly name for the account", }, cli.StringFlag{ Name: "description", Usage: "description for the account", }, cli.StringFlag{ Name: "expiry-duration", Usage: "duration before the access key expires", }, cli.StringFlag{ Name: "expiry", Usage: "expiry date for the access key", }, } var adminAccesskeyEditCmd = cli.Command{ Name: "edit", Usage: "edit existing access keys", Action: mainAdminAccesskeyEdit, Before: setGlobalsFromContext, Flags: append(adminAccesskeyEditFlags, globalFlags...), OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] [TARGET] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Change the secret key for the access key "testkey" {{.Prompt}} {{.HelpName}} myminio/ testkey --secret-key 'xxxxxxx' 2. Change the expiry duration for the access key "testkey" {{.Prompt}} {{.HelpName}} myminio/ testkey ---expiry-duration 24h `, } func mainAdminAccesskeyEdit(ctx *cli.Context) error { return commonAccesskeyEdit(ctx) } minio-client-0.0~20250403/cmd/admin-accesskey-enable.go000066400000000000000000000025551477450377600223670ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminAccesskeyEnableCmd = cli.Command{ Name: "enable", Usage: "enable an access key", Action: mainAdminAccesskeyEnable, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] [TARGET] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable access key {{.Prompt}} {{.HelpName}} myminio myaccesskey `, } func mainAdminAccesskeyEnable(ctx *cli.Context) error { return enableDisableAccesskey(ctx, true) } minio-client-0.0~20250403/cmd/admin-accesskey-info.go000066400000000000000000000027771477450377600221020ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminAccesskeyInfoCmd = cli.Command{ Name: "info", Usage: "info about given access key pairs", Action: mainAdminAccesskeyInfo, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET ACCESSKEY [ACCESSKEY...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get info for the access key "testkey" {{.Prompt}} {{.HelpName}} local/ testkey 2. Get info for the access keys "testkey" and "testkey2" {{.Prompt}} {{.HelpName}} local/ testkey testkey2 `, } func mainAdminAccesskeyInfo(ctx *cli.Context) error { return commonAccesskeyInfo(ctx) } minio-client-0.0~20250403/cmd/admin-accesskey-list.go000066400000000000000000000117501477450377600221110ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "strings" "github.com/charmbracelet/lipgloss" humanize "github.com/dustin/go-humanize" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var adminAccesskeyListFlags = []cli.Flag{ cli.BoolFlag{ Name: "users-only", Usage: "only list user DNs", }, cli.BoolFlag{ Name: "temp-only", Usage: "only list temporary access keys", }, cli.BoolFlag{ Name: "svcacc-only", Usage: "only list service account access keys", }, cli.BoolFlag{ Name: "self", Usage: "list access keys for the authenticated user", }, cli.BoolFlag{ Name: "all", Usage: "list all access keys for all builtin users", }, } var adminAccesskeyListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list access key pairs for builtin users", Action: mainAdminAccesskeyList, Before: setGlobalsFromContext, Flags: append(adminAccesskeyListFlags, globalFlags...), OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [DN...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get list of all builtin users and associated access keys in local server {{.Prompt}} {{.HelpName}} local/ --all 2. Get list of access keys for the authenticated user in local server {{.Prompt}} {{.HelpName}} local/ --self 3. Get list of builtin users in local server {{.Prompt}} {{.HelpName}} local/ --all --users-only 4. Get list of all builtin users and associated temporary access keys in play server (if admin) {{.Prompt}} {{.HelpName}} play/ --temp-only 5. Get list of access keys associated with user 'foobar' {{.Prompt}} {{.HelpName}} play/ foobar 6. Get list of access keys associated with users 'foobar' and 'tester' {{.Prompt}} {{.HelpName}} play/ foobar tester 7. Get all users and access keys if admin, else get authenticated user and associated access keys {{.Prompt}} {{.HelpName}} local/ `, } type userAccesskeyList struct { Status string `json:"status"` User string `json:"user"` STSKeys []madmin.ServiceAccountInfo `json:"stsKeys"` ServiceAccounts []madmin.ServiceAccountInfo `json:"svcaccs"` LDAP bool `json:"ldap,omitempty"` } func (m userAccesskeyList) String() string { labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) o := strings.Builder{} userStr := "User" if m.LDAP { userStr = "DN" } o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render(userStr+":"), m.User)) if len(m.STSKeys) > 0 || len(m.ServiceAccounts) > 0 { o.WriteString(iFmt(2, "%s\n", labelStyle.Render("Access Keys:"))) } for _, k := range m.STSKeys { expiration := "never" if nilExpiry(k.Expiration) != nil { expiration = humanize.Time(*k.Expiration) } o.WriteString(iFmt(4, "%s, expires: %s, sts: true\n", k.AccessKey, expiration)) } for _, k := range m.ServiceAccounts { expiration := "never" if nilExpiry(k.Expiration) != nil { expiration = humanize.Time(*k.Expiration) } o.WriteString(iFmt(4, "%s, expires: %s, sts: false\n", k.AccessKey, expiration)) } return o.String() } func (m userAccesskeyList) JSON() string { jsonMessageBytes, e := json.MarshalIndent(m, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func mainAdminAccesskeyList(ctx *cli.Context) error { aliasedURL, tentativeAll, users, opts := commonAccesskeyList(ctx) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") accessKeysMap, e := client.ListAccessKeysBulk(globalContext, users, opts) if e != nil { if e.Error() == "Access Denied." && tentativeAll { // retry with self opts.All = false accessKeysMap, e = client.ListAccessKeysBulk(globalContext, users, opts) } fatalIf(probe.NewError(e), "Unable to list access keys.") } for user, accessKeys := range accessKeysMap { m := userAccesskeyList{ Status: "success", User: user, ServiceAccounts: accessKeys.ServiceAccounts, STSKeys: accessKeys.STSKeys, LDAP: false, } printMsg(m) } return nil } minio-client-0.0~20250403/cmd/admin-accesskey-remove.go000066400000000000000000000026641477450377600224370ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminAccesskeyRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "delete access key pairs for builtin users", Action: mainAdminAccesskeyRemove, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET ACCESSKEY FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove the access key "testkey" from local server {{.Prompt}} {{.HelpName}} local/ testkey `, } func mainAdminAccesskeyRemove(ctx *cli.Context) error { return commonAccesskeyRemove(ctx) } minio-client-0.0~20250403/cmd/admin-accesskey-sts-revoke.go000066400000000000000000000107751477450377600232460ustar00rootroot00000000000000// Copyright (c) 2015-2025 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var adminAccesskeySTSRevokeFlags = []cli.Flag{ cli.BoolFlag{ Name: "all", Usage: "revoke all STS accounts for the specified user", }, cli.BoolFlag{ Name: "self", Usage: "revoke all STS accounts for the authenticated user", }, cli.StringFlag{ Name: "token-type", Usage: "specify the token type to revoke", }, } var adminAccesskeySTSRevokeCmd = cli.Command{ Name: "sts-revoke", Usage: "revokes all STS accounts or specified types for the specified user", Action: mainAdminAccesskeySTSRevoke, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminAccesskeySTSRevokeFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS USER [--all | --token-type TOKEN_TYPE] Exactly one of --all or --token-type must be specified. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Revoke all STS accounts for user "user1" {{.Prompt}} {{.HelpName}} myminio user1 --all 2. Revoke STS accounts of a token type "app-1" for user "user1" {{.Prompt}} {{.HelpName}} myminio user1 --token-type app-1 3. Revoke all STS accounts for the authenticated user {{.Prompt}} {{.HelpName}} myminio --self 4. Revoke STS accounts of a token type "app-1" for the authenticated user {{.Prompt}} {{.HelpName}} myminio --self --token-type app-1 `, } type stsRevokeMessage struct { Status string `json:"status"` User string `json:"user"` TokenRevokeType string `json:"tokenRevokeType,omitempty"` } func (m stsRevokeMessage) String() string { userString := "user " + m.User if m.User == "" { userString = "authenticated user" } if m.TokenRevokeType == "" { return "Successfully revoked all STS accounts for " + userString } return "Successfully revoked all STS accounts of type " + m.TokenRevokeType + " for " + userString } func (m stsRevokeMessage) JSON() string { if m.Status == "" { m.Status = "success" } jsonMessageBytes, e := json.MarshalIndent(m, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // checkSTSRevokeSyntax - validate all the passed arguments func checkSTSRevokeSyntax(ctx *cli.Context) { if len(ctx.Args()) > 2 || len(ctx.Args()) == 0 { showCommandHelpAndExit(ctx, 1) } if !ctx.Bool("self") && ctx.Args().Get(1) == "" { fatalIf(errInvalidArgument().Trace(), "Must specify user or use --self flag.") } if ctx.Bool("self") && ctx.Args().Get(1) != "" { fatalIf(errInvalidArgument().Trace(), "Cannot specify user with --self flag.") } if (!ctx.Bool("all") && ctx.String("token-type") == "") || (ctx.Bool("all") && ctx.String("token-type") != "") { fatalIf(errDummy().Trace(), "Exactly one of --all or --token-type must be specified.") } } // mainAdminAccesskeySTSRevoke is the handle for "mc admin accesskey sts-revoke" command. func mainAdminAccesskeySTSRevoke(ctx *cli.Context) error { checkSTSRevokeSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) user := args.Get(1) // will be empty if --self flag is set tokenRevokeType := ctx.String("token-type") fullRevoke := ctx.Bool("all") // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") e := client.RevokeTokens(globalContext, madmin.RevokeTokensReq{ User: user, TokenRevokeType: tokenRevokeType, FullRevoke: fullRevoke, }) fatalIf(probe.NewError(e).Trace(args...), "Unable to revoke tokens for %s", user) printMsg(stsRevokeMessage{ User: user, TokenRevokeType: tokenRevokeType, }) return nil } minio-client-0.0~20250403/cmd/admin-accesskey.go000066400000000000000000000027261477450377600211430ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminAccesskeySubcommands = []cli.Command{ adminAccesskeyListCmd, adminAccesskeyRemoveCmd, adminAccesskeyInfoCmd, adminAccesskeyCreateCmd, adminAccesskeyEditCmd, adminAccesskeyEnableCmd, adminAccesskeyDisableCmd, adminAccesskeySTSRevokeCmd, } var adminAccesskeyCmd = cli.Command{ Name: "accesskey", Usage: "manage access keys defined in the MinIO server", Action: mainAdminAccesskey, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminAccesskeySubcommands, HideHelpCommand: true, } func mainAdminAccesskey(ctx *cli.Context) error { commandNotFound(ctx, adminAccesskeySubcommands) return nil } minio-client-0.0~20250403/cmd/admin-bucket-info.go000066400000000000000000000024601477450377600213720ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminBucketInfoFlags = []cli.Flag{} var adminBucketInfoCmd = cli.Command{ Name: "info", Usage: "display bucket information", Action: mainAdminBucketInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminBucketInfoFlags, globalFlags...), HideHelpCommand: true, } // mainAdminBucketInfo is the handler for "mc admin bucket info" command. func mainAdminBucketInfo(_ *cli.Context) error { deprecatedError("mc stat") return nil } minio-client-0.0~20250403/cmd/admin-bucket-quota.go000066400000000000000000000027651477450377600216000ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminQuotaFlags = []cli.Flag{ cli.StringFlag{ Name: "hard", Usage: "set a hard quota, disallowing writes after quota is reached", }, cli.BoolFlag{ Name: "clear", Usage: "clears bucket quota configured for bucket", }, } var adminBucketQuotaCmd = cli.Command{ Name: "quota", Usage: "manage bucket quota", Action: mainAdminBucketQuota, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminQuotaFlags, globalFlags...), HideHelpCommand: true, } // mainAdminBucketQuota is the handler for "mc admin bucket quota" command. func mainAdminBucketQuota(_ *cli.Context) error { deprecatedError("mc quota") return nil } minio-client-0.0~20250403/cmd/admin-bucket-remote-add.go000066400000000000000000000023641477450377600224630ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminBucketRemoteAddCmd = cli.Command{ Name: "add", Usage: "add a new remote target", Action: mainAdminBucketRemoteAdd, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelp: true, } // mainAdminBucketRemoteAdd is the handle for "mc admin bucket remote set" command. func mainAdminBucketRemoteAdd(_ *cli.Context) error { deprecatedError("mc replicate add") return nil } minio-client-0.0~20250403/cmd/admin-bucket-remote-add_test.go000066400000000000000000000053301477450377600235160ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "testing" ) func TestGetBandwidthInBytes(t *testing.T) { type args struct { bandwidthStr string } f1 := 999.1234567 * 1024 * 1024 f2 := 10.123456789 * 1024 * 1024 * 1024 f3 := 10000.123456789 * 1024 * 1024 * 1024 f4 := 0.001 * 1024 * 1024 * 1024 tests := []struct { name string args args want uint64 }{ { name: "1MegaByte", args: args{ bandwidthStr: "1Mi", }, want: 1024 * 1024, }, { name: "1MegaBit", args: args{ bandwidthStr: "1M", }, want: 1000000, }, { name: "1GigaByte", args: args{ bandwidthStr: "1G", }, want: 1000000000, }, { name: "1GibiByte", args: args{ bandwidthStr: "1Gi", }, want: 1024 * 1024 * 1024, }, { name: "FractionalMegaBytes", args: args{ bandwidthStr: "999.123456789123456789M", }, want: 999123456, }, { name: "FractionalGigaBytes", args: args{ bandwidthStr: "10.123456789123456789123456G", }, want: 10123456789, }, { name: "FractionalBigGigaBytes", args: args{ bandwidthStr: "10000.123456789123456789123456G", }, want: 10000123456789, }, { name: "FractionalMebiBytes", args: args{ bandwidthStr: "999.123456789123456789Mi", }, want: uint64(f1), }, { name: "FractionalGibiBytes", args: args{ bandwidthStr: "10.123456789123456789123456Gi", }, want: uint64(f2), }, { name: "FractionalBigGibiBytes", args: args{ bandwidthStr: "10000.123456789123456789123456Gi", }, want: uint64(f3), }, { name: "SmallGiga", args: args{ bandwidthStr: "0.001Gi", }, want: uint64(f4), }, { name: "LargeK", args: args{ bandwidthStr: "1024Ki", }, want: 1024 * 1024, }, } t.Parallel() for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { if got, err := getBandwidthInBytes(tt.args.bandwidthStr); err != nil || got != tt.want { t.Errorf("getBandwidthInBytes() = %v, want %v", got, tt.want) } }) } } minio-client-0.0~20250403/cmd/admin-bucket-remote-edit.go000066400000000000000000000023701477450377600226550ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminBucketRemoteEditCmd = cli.Command{ Name: "edit", Usage: "edit remote target", Action: mainAdminBucketRemoteEdit, Before: setGlobalsFromContext, OnUsageError: onUsageError, Flags: globalFlags, HideHelp: true, } // mainAdminBucketRemoteEdit is the handle for "mc admin bucket remote edit" command. func mainAdminBucketRemoteEdit(_ *cli.Context) error { deprecatedError("mc replicate update") return nil } minio-client-0.0~20250403/cmd/admin-bucket-remote-main.go000066400000000000000000000027401477450377600226550ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminBucketRemoteSubcommands = []cli.Command{ adminBucketRemoteAddCmd, adminBucketRemoteEditCmd, adminBucketRemoteRmCmd, } var adminBucketRemoteCmd = cli.Command{ Name: "remote", Usage: "configure remote target buckets", Action: mainadminBucketRemote, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminBucketRemoteSubcommands, HideHelpCommand: true, } // mainadminBucketRemote is the handle for "mc admin bucket remote" command. func mainadminBucketRemote(ctx *cli.Context) error { commandNotFound(ctx, adminBucketRemoteSubcommands) return nil // Sub-commands like "add", "ls", "rm" have their own main. } minio-client-0.0~20250403/cmd/admin-bucket-remote-remove.go000066400000000000000000000024321477450377600232240ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminBucketRemoteRmCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove configured remote target", Action: mainAdminBucketRemoteRemove, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelp: true, } // mainAdminBucketRemoteRemove is the handle for "mc admin bucket remote rm" command. func mainAdminBucketRemoteRemove(_ *cli.Context) error { deprecatedError("mc replicate rm") return nil } minio-client-0.0~20250403/cmd/admin-bucket.go000066400000000000000000000027061477450377600204440ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminBucketSubcommands = []cli.Command{ adminBucketRemoteCmd, adminBucketQuotaCmd, adminBucketInfoCmd, } var adminBucketCmd = cli.Command{ Name: "bucket", Usage: "manage buckets defined in the MinIO server", Action: mainAdminBucket, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminBucketSubcommands, HideHelpCommand: true, Hidden: true, } // mainAdminBucket is the handle for "mc admin bucket" command. func mainAdminBucket(ctx *cli.Context) error { commandNotFound(ctx, adminBucketSubcommands) return nil // Sub-commands like "quota", "remote" have their own main. } minio-client-0.0~20250403/cmd/admin-cluster-bucket-export.go000066400000000000000000000100451477450377600234350ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "io" "os" "path/filepath" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminClusterBucketExportCmd = cli.Command{ Name: "export", Usage: "backup bucket metadata to a zip file", Action: mainClusterBucketExport, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET/[BUCKET] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Save metadata of all buckets to a zip file. {{.Prompt}} {{.HelpName}} myminio `, } func checkBucketExportSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainClusterBucketExport - metadata export command func mainClusterBucketExport(ctx *cli.Context) error { // Check for command syntax checkBucketExportSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) console.SetColor("File", color.New(color.FgWhite, color.Bold)) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } // Compute bucket and object from the aliased URL aliasedURL = filepath.ToSlash(aliasedURL) aliasedURL = filepath.Clean(aliasedURL) _, bucket := url2Alias(aliasedURL) r, e := client.ExportBucketMetadata(context.Background(), bucket) fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to export bucket metadata.") if bucket == "" { bucket = "bucket" } // Create bucket metadata zip file tmpFile, e := os.CreateTemp("", fmt.Sprintf("%s-%s-metadata-", strings.ReplaceAll(aliasedURL, "/", "-"), bucket)) fatalIf(probe.NewError(e), "Unable to download file data.") ext := "zip" // Copy zip content to target download file _, e = io.Copy(tmpFile, r) fatalIf(probe.NewError(e), "Unable to download bucket metadata.") // Close everything r.Close() tmpFile.Close() // We use 4 bytes of the 32 bytes to identify the file. downloadPath := fmt.Sprintf("%s-%s-metadata.%s", aliasedURL, bucket, ext) // Create necessary directories. dir := filepath.Dir(downloadPath) if e := os.MkdirAll(dir, 0o755); e != nil { fatalIf(probe.NewError(e).Trace(dir), "Unable to create download directory") } fi, e := os.Stat(downloadPath) if e == nil && !fi.IsDir() { e = moveFile(downloadPath, downloadPath+"."+time.Now().Format(dateTimeFormatFilename)) fatalIf(probe.NewError(e), "Unable to create a backup of "+downloadPath) } else { if !os.IsNotExist(e) { fatal(probe.NewError(e), "Unable to download file data") } } fatalIf(probe.NewError(moveFile(tmpFile.Name(), downloadPath)), "Unable to rename downloaded data, file exists at %s", tmpFile.Name()) if !globalJSON { console.Infof("Bucket metadata successfully downloaded as %s\n", downloadPath) return nil } v := struct { File string `json:"file"` Key string `json:"key,omitempty"` }{ File: downloadPath, } b, e := json.Marshal(v) fatalIf(probe.NewError(e), "Unable to serialize data") console.Println(string(b)) return nil } minio-client-0.0~20250403/cmd/admin-cluster-bucket-import.go000066400000000000000000000160101477450377600234240ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "fmt" "io" "os" "path/filepath" "strings" "github.com/fatih/color" "github.com/klauspost/compress/zip" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminClusterBucketImportCmd = cli.Command{ Name: "import", Usage: "restore bucket metadata from a zip file", Action: mainClusterBucketImport, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET/[BUCKET] /path/to/backups/bucket-metadata.zip FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Recover bucket metadata for all buckets from previously saved bucket metadata backup. {{.Prompt}} {{.HelpName}} myminio /backups/myminio-bucket-metadata.zip `, } func checkBucketImportSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainClusterBucketImport - bucket metadata import command func mainClusterBucketImport(ctx *cli.Context) error { // Check for command syntax checkBucketImportSyntax(ctx) console.SetColor("Name", color.New(color.Bold, color.FgCyan)) console.SetColor("success", color.New(color.Bold, color.FgGreen)) console.SetColor("warning", color.New(color.Bold, color.FgYellow)) console.SetColor("errors", color.New(color.Bold, color.FgRed)) console.SetColor("statusMsg", color.New(color.Bold, color.FgHiWhite)) console.SetColor("failCell", color.New(color.FgRed)) console.SetColor("passCell", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) var r io.Reader var sz int64 f, e := os.Open(args.Get(1)) if e != nil { fatalIf(probe.NewError(e).Trace(args...), "Unable to get bucket metadata") } if st, e := f.Stat(); e == nil { sz = st.Size() } defer f.Close() r = f _, e = zip.NewReader(r.(io.ReaderAt), sz) fatalIf(probe.NewError(e).Trace(args...), fmt.Sprintf("Unable to read zip file %s", args.Get(1))) f, e = os.Open(args.Get(1)) fatalIf(probe.NewError(e).Trace(args...), "Unable to get bucket metadata") defer f.Close() // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } // Compute bucket and object from the aliased URL aliasedURL = filepath.ToSlash(aliasedURL) aliasedURL = filepath.Clean(aliasedURL) _, bucket := url2Alias(aliasedURL) rpt, e := client.ImportBucketMetadata(context.Background(), bucket, f) fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to import bucket metadata.") printMsg(importMetaMsg{ BucketMetaImportErrs: rpt, Status: "success", URL: aliasedURL, Op: ctx.Command.Name, }) return nil } type importMetaMsg struct { madmin.BucketMetaImportErrs Op string URL string `json:"url"` Status string `json:"status"` } func statusTick(s madmin.MetaStatus) string { switch { case s.Err != "": return console.Colorize("failCell", crossTickCell) case !s.IsSet: return blankCell default: return console.Colorize("passCell", tickCell) } } func (i importMetaMsg) String() string { m := i.Buckets totBuckets := len(m) totErrs := 0 for _, st := range m { if st.ObjectLock.Err != "" || st.Versioning.Err != "" || st.SSEConfig.Err != "" || st.Tagging.Err != "" || st.Lifecycle.Err != "" || st.Quota.Err != "" || st.Policy.Err != "" || st.Notification.Err != "" || st.Cors.Err != "" || st.Err != "" { totErrs++ } } var b strings.Builder numSch := "success" if totErrs > 0 { numSch = "warning" } msg := "\n" + console.Colorize(numSch, totBuckets-totErrs) + console.Colorize("statusMsg", "/") + console.Colorize("success", totBuckets) + console.Colorize("statusMsg", " buckets were imported successfully.") fmt.Fprintln(&b, msg) if totErrs > 0 { fmt.Fprintln(&b, console.Colorize("errors", "Errors: \n")) for bucket, st := range m { if st.ObjectLock.Err != "" || st.Versioning.Err != "" || st.SSEConfig.Err != "" || st.Tagging.Err != "" || st.Lifecycle.Err != "" || st.Quota.Err != "" || st.Policy.Err != "" || st.Notification.Err != "" || st.Cors.Err != "" || st.Err != "" { fmt.Fprintln(&b, printImportErrs(bucket, st)) } } } return b.String() } func (i importMetaMsg) JSON() string { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetIndent("", " ") // Disable escaping special chars to display XML tags correctly enc.SetEscapeHTML(false) fatalIf(probe.NewError(enc.Encode(i.Buckets)), "Unable to marshal into JSON.") return buf.String() } // pretty print import errors func printImportErrs(bucket string, r madmin.BucketStatus) string { var b strings.Builder placeHolder := "" key := fmt.Sprintf("%-10s: %s", "Name", bucket) fmt.Fprintln(&b, console.Colorize("Name", key)) if r.Err != "" { fmt.Fprintf(&b, "%2s%s %s", placeHolder, console.Colorize("errors", "Error: "), r.Err) fmt.Fprintln(&b) } if r.ObjectLock.IsSet { fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Object lock: ", statusTick(r.ObjectLock)) fmt.Fprintln(&b) } if r.Versioning.IsSet { fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Versioning: ", statusTick(r.Versioning)) fmt.Fprintln(&b) } if r.SSEConfig.IsSet { fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Encryption: ", statusTick(r.SSEConfig)) fmt.Fprintln(&b) } if r.Lifecycle.IsSet { fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Lifecycle: ", statusTick(r.Lifecycle)) fmt.Fprintln(&b) } if r.Notification.IsSet { fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Notification: ", statusTick(r.Notification)) fmt.Fprintln(&b) } if r.Quota.IsSet { fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Quota: ", statusTick(r.Quota)) fmt.Fprintln(&b) } if r.Policy.IsSet { fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Policy: ", statusTick(r.Policy)) fmt.Fprintln(&b) } if r.Tagging.IsSet { fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Tagging: ", statusTick(r.Tagging)) fmt.Fprintln(&b) } if r.Cors.IsSet { fmt.Fprintf(&b, "%2s%s %s", placeHolder, "CORS: ", statusTick(r.Cors)) fmt.Fprintln(&b) } return b.String() } minio-client-0.0~20250403/cmd/admin-cluster-bucket.go000066400000000000000000000027321477450377600221220ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminClusterBucketSubcommands = []cli.Command{ adminClusterBucketImportCmd, adminClusterBucketExportCmd, } var adminClusterBucketCmd = cli.Command{ Name: "bucket", Usage: "manage bucket metadata on MinIO cluster", Action: mainAdminClusterBucket, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminClusterBucketSubcommands, HideHelpCommand: true, } // mainAdminClusterBucket is the handle for "mc admin cluster bucket" command. func mainAdminClusterBucket(ctx *cli.Context) error { commandNotFound(ctx, adminClusterBucketSubcommands) return nil // Sub-commands like "export", "import" have their own main. } minio-client-0.0~20250403/cmd/admin-cluster-iam-export.go000066400000000000000000000076221477450377600227350ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "io" "os" "path/filepath" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) // iam export specific flags. var ( iamExportFlags = []cli.Flag{ cli.StringFlag{ Name: "output,o", Usage: "output iam export to a custom file path", }, } ) var adminClusterIAMExportCmd = cli.Command{ Name: "export", Usage: "exports IAM info to zipped file", Action: mainClusterIAMExport, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(iamExportFlags, globalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Download all IAM metadata for cluster into zip file. {{.Prompt}} {{.HelpName}} myminio 2. Download all IAM metadata to a custom file. {{.Prompt}} {{.HelpName}} myminio --output /tmp/myminio-iam.zip `, } func checkIAMExportSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainClusterIAMExport - metadata export command func mainClusterIAMExport(ctx *cli.Context) error { // Check for command syntax checkIAMExportSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := filepath.ToSlash(args.Get(0)) aliasedURL = filepath.Clean(aliasedURL) console.SetColor("File", color.New(color.FgWhite, color.Bold)) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } r, e := client.ExportIAM(context.Background()) fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to export IAM info.") // Create iam info zip file tmpFile, e := os.CreateTemp("", fmt.Sprintf("%s-iam-info", aliasedURL)) fatalIf(probe.NewError(e), "Unable to download file data.") ext := "zip" // Copy zip content to target download file _, e = io.Copy(tmpFile, r) fatalIf(probe.NewError(e), "Unable to download IAM info.") // Close everything r.Close() tmpFile.Close() downloadPath := fmt.Sprintf("%s-iam-info.%s", aliasedURL, ext) if ctx.String("output") != "" { downloadPath = ctx.String("output") } fi, e := os.Stat(downloadPath) if e == nil && !fi.IsDir() { e = moveFile(downloadPath, downloadPath+"."+time.Now().Format(dateTimeFormatFilename)) fatalIf(probe.NewError(e), "Unable to create a backup of "+downloadPath) } else { if !os.IsNotExist(e) { fatal(probe.NewError(e), "Unable to download file data") } } fatalIf(probe.NewError(moveFile(tmpFile.Name(), downloadPath)), "Unable to rename downloaded data, file exists at %s", tmpFile.Name()) if !globalJSON { console.Infof("IAM info successfully downloaded as %s\n", downloadPath) return nil } v := struct { File string `json:"file"` Key string `json:"key,omitempty"` }{ File: downloadPath, } b, e := json.Marshal(v) fatalIf(probe.NewError(e), "Unable to serialize data") console.Println(string(b)) return nil } minio-client-0.0~20250403/cmd/admin-cluster-iam-import.go000066400000000000000000000156621477450377600227310ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "io" "os" "path/filepath" "strings" "github.com/klauspost/compress/zip" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminClusterIAMImportCmd = cli.Command{ Name: "import", Usage: "imports IAM info from zipped file", Action: mainClusterIAMImport, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET/BUCKET /path/to/myminio-iam-info.zip FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Set IAM info from previously exported metadata zip file. {{.Prompt}} {{.HelpName}} myminio /tmp/myminio-iam-info.zip `, } type iamImportInfo madmin.ImportIAMResult func (i iamImportInfo) JSON() string { bs, e := json.MarshalIndent(madmin.ImportIAMResult(i), "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(bs) } func (i iamImportInfo) String() string { var messages []string info := madmin.ImportIAMResult(i) messages = append(messages, processIAMEntities(info.Skipped, "Skipped")...) messages = append(messages, processIAMEntities(info.Removed, "Removed")...) messages = append(messages, processIAMEntities(info.Added, "Added")...) messages = append(messages, processErrIAMEntities(info.Failed)...) return strings.Join(messages, "\n") } func processIAMEntities(entities madmin.IAMEntities, action string) []string { var messages []string if len(entities.Policies) > 0 { messages = append(messages, fmt.Sprintf("%s policies: %v", action, strings.Join(entities.Policies, ", "))) } if len(entities.Users) > 0 { messages = append(messages, fmt.Sprintf("%s users: %v", action, strings.Join(entities.Users, ", "))) } if len(entities.Groups) > 0 { messages = append(messages, fmt.Sprintf("%s groups: %v", action, strings.Join(entities.Groups, ", "))) } if len(entities.ServiceAccounts) > 0 { messages = append(messages, fmt.Sprintf("%s service accounts: %v", action, strings.Join(entities.ServiceAccounts, ", "))) } var users []string for _, pol := range entities.UserPolicies { for name := range pol { users = append(users, name) } } if len(users) > 0 { messages = append(messages, fmt.Sprintf("%s policies for users: %v", action, strings.Join(users, ", "))) } var groups []string for _, pol := range entities.GroupPolicies { for name := range pol { groups = append(groups, name) } } if len(groups) > 0 { messages = append(messages, fmt.Sprintf("%s policies for groups: %v", action, strings.Join(groups, ", "))) } var stsarr []string for _, pol := range entities.STSPolicies { for name := range pol { stsarr = append(stsarr, name) } } if len(stsarr) > 0 { messages = append(messages, fmt.Sprintf("%s policies for sts: %v", action, strings.Join(stsarr, ", "))) } return messages } func processErrIAMEntities(entities madmin.IAMErrEntities) []string { var messages []string var policies []string for _, entry := range entities.Policies { policies = append(policies, entry.Name) } if len(policies) > 0 { messages = append(messages, fmt.Sprintf("Failed to add policies: %v", strings.Join(policies, ", "))) } var users []string for _, entry := range entities.Users { users = append(users, entry.Name) } if len(users) > 0 { messages = append(messages, fmt.Sprintf("Failed to add users: %v", strings.Join(users, ", "))) } var groups []string for _, entry := range entities.Groups { groups = append(groups, entry.Name) } if len(groups) > 0 { messages = append(messages, fmt.Sprintf("Failed to add groups: %v", strings.Join(groups, ", "))) } var sas []string for _, entry := range entities.ServiceAccounts { sas = append(sas, entry.Name) } if len(sas) > 0 { messages = append(messages, fmt.Sprintf("Failed to add service accounts: %v", strings.Join(sas, ", "))) } var polusers []string for _, pol := range entities.UserPolicies { polusers = append(polusers, pol.Name) } if len(polusers) > 0 { messages = append(messages, fmt.Sprintf("Failed to add policies for users: %v", strings.Join(polusers, ", "))) } var polgroups []string for _, pol := range entities.GroupPolicies { polgroups = append(polgroups, pol.Name) } if len(polgroups) > 0 { messages = append(messages, fmt.Sprintf("Failed to add policies for groups: %v", strings.Join(polgroups, ", "))) } var polsts []string for _, pol := range entities.STSPolicies { polsts = append(polsts, pol.Name) } if len(polsts) > 0 { messages = append(messages, fmt.Sprintf("Failed to add policies for sts: %v", strings.Join(polsts, ", "))) } return messages } func checkIAMImportSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainClusterIAMImport - iam info import command func mainClusterIAMImport(ctx *cli.Context) error { // Check for command syntax checkIAMImportSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := filepath.ToSlash(args.Get(0)) aliasedURL = filepath.Clean(aliasedURL) var r io.Reader var sz int64 f, e := os.Open(args.Get(1)) if e != nil { fatalIf(probe.NewError(e).Trace(args...), "Unable to get IAM info") } if st, e := f.Stat(); e == nil { sz = st.Size() } defer f.Close() r = f _, e = zip.NewReader(r.(io.ReaderAt), sz) fatalIf(probe.NewError(e).Trace(args...), fmt.Sprintf("Unable to read zip file %s", args.Get(1))) f, e = os.Open(args.Get(1)) fatalIf(probe.NewError(e).Trace(args...), "Unable to get IAM info") defer f.Close() // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } iamr, e := client.ImportIAMV2(context.Background(), f) if e != nil { f.Seek(0, 0) e = client.ImportIAM(context.Background(), f) fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to import IAM info.") if !globalJSON { console.Infof("IAM info imported to %s from %s\n", aliasedURL, args.Get(1)) } } else { printMsg(iamImportInfo(iamr)) } return nil } minio-client-0.0~20250403/cmd/admin-cluster-iam.go000066400000000000000000000026651477450377600214200ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminClusterIAMSubcommands = []cli.Command{ adminClusterIAMImportCmd, adminClusterIAMExportCmd, } var adminClusterIAMCmd = cli.Command{ Name: "iam", Usage: "manage IAM info on MinIO cluster", Action: mainadminClusterIAM, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminClusterIAMSubcommands, HideHelpCommand: true, } // mainadminClusterIAM is the handle for "mc admin cluster bucket" command. func mainadminClusterIAM(ctx *cli.Context) error { commandNotFound(ctx, adminClusterIAMSubcommands) return nil // Sub-commands like "export", "import" have their own main. } minio-client-0.0~20250403/cmd/admin-cluster.go000066400000000000000000000026161477450377600206500ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminClusterSubcommands = []cli.Command{ adminClusterBucketCmd, adminClusterIAMCmd, } var adminClusterCmd = cli.Command{ Name: "cluster", Usage: "manage MinIO cluster metadata", Action: mainAdminCluster, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminClusterSubcommands, HideHelpCommand: true, } // mainAdminCluster is the handle for "mc admin cluster" command. func mainAdminCluster(ctx *cli.Context) error { commandNotFound(ctx, adminClusterSubcommands) return nil // Sub-commands like "bucket", "iam" have their own main. } minio-client-0.0~20250403/cmd/admin-config-export.go000066400000000000000000000066241477450377600217560ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "bytes" "fmt" "io" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminConfigExportCmd = cli.Command{ Name: "export", Usage: "export all config keys to STDOUT", Before: setGlobalsFromContext, Action: mainAdminConfigExport, OnUsageError: onUsageError, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: The output includes environment variables set on the server. These cannot be overridden from the client. 1. Export the current config from MinIO server {{.Prompt}} {{.HelpName}} play/ > config.txt `, } // configExportMessage container to hold locks information. type configExportMessage struct { Status string `json:"status"` Value []byte `json:"value"` } // String colorized service status message. func (u configExportMessage) String() string { console.SetColor("EnvVar", color.New(color.FgYellow)) bio := bufio.NewReader(bytes.NewReader(u.Value)) var lines []string for { s, e := bio.ReadString('\n') // Make lines displaying environment variables bold. if strings.HasPrefix(s, "# MINIO_") { s = strings.TrimPrefix(s, "# ") parts := strings.SplitN(s, "=", 2) s = fmt.Sprintf("# %s=%s", console.Colorize("EnvVar", parts[0]), parts[1]) lines = append(lines, s) } else { lines = append(lines, s) } if e == io.EOF { break } fatalIf(probe.NewError(e), "Unable to marshal to string.") } return strings.Join(lines, "") } // JSON jsonified service status Message message. func (u configExportMessage) JSON() string { u.Status = "success" statusJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(statusJSONBytes) } // checkAdminConfigExportSyntax - validate all the passed arguments func checkAdminConfigExportSyntax(ctx *cli.Context) { if !ctx.Args().Present() || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainAdminConfigExport(ctx *cli.Context) error { checkAdminConfigExportSyntax(ctx) // Export the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") // Call get config API buf, e := client.GetConfig(globalContext) fatalIf(probe.NewError(e), "Unable to get server config") // Print printMsg(configExportMessage{ Value: buf, }) return nil } minio-client-0.0~20250403/cmd/admin-config-get.go000066400000000000000000000106131477450377600212050ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "bytes" "fmt" "io" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminConfigGetCmd = cli.Command{ Name: "get", Usage: "interactively retrieve a config key parameters", Before: setGlobalsFromContext, Action: mainAdminConfigGet, OnUsageError: onUsageError, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: The output includes environment variables set on the server. These cannot be overridden from the client. 1. Get the current region setting on MinIO server. {{.Prompt}} {{.HelpName}} play/ region region name=us-east-1 2. Get the current notification settings for Webhook target on MinIO server {{.Prompt}} {{.HelpName}} myminio/ notify_webhook notify_webhook endpoint="http://localhost:8080" auth_token= queue_limit=10000 queue_dir="/home/events" 3. Get the current compression settings on MinIO server {{.Prompt}} {{.HelpName}} myminio/ compression compression extensions=".txt,.csv" mime_types="text/*" `, } type configGetMessage struct { Status string `json:"status"` Config []madmin.SubsysConfig `json:"config"` value []byte } // String colorized service status message. func (u configGetMessage) String() string { console.SetColor("EnvVar", color.New(color.FgYellow)) bio := bufio.NewReader(bytes.NewReader(u.value)) var lines []string for { s, e := bio.ReadString('\n') // Make lines displaying environment variables bold. if strings.HasPrefix(s, "# MINIO_") { s = strings.TrimPrefix(s, "# ") parts := strings.SplitN(s, "=", 2) s = fmt.Sprintf("# %s=%s", console.Colorize("EnvVar", parts[0]), parts[1]) lines = append(lines, s) } else { lines = append(lines, s) } if e == io.EOF { break } fatalIf(probe.NewError(e), "Unable to marshal to string.") } return strings.Join(lines, "") } // JSON jsonified service status Message message. func (u configGetMessage) JSON() string { u.Status = "success" var err error u.Config, err = madmin.ParseServerConfigOutput(string(u.value)) fatalIf(probe.NewError(err), "Unable to marshal into JSON.") statusJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(statusJSONBytes) } // checkAdminConfigGetSyntax - validate all the passed arguments func checkAdminConfigGetSyntax(ctx *cli.Context) { if !ctx.Args().Present() || len(ctx.Args()) < 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainAdminConfigGet(ctx *cli.Context) error { checkAdminConfigGetSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") if len(ctx.Args()) == 1 { // Call get config API hr, e := client.HelpConfigKV(globalContext, "", "", false) fatalIf(probe.NewError(e), "Unable to get help for the sub-system") // Print printMsg(configHelpMessage{ Value: hr, envOnly: false, }) return nil } subSys := strings.Join(args.Tail(), " ") // Call get config API buf, e := client.GetConfigKV(globalContext, subSys) fatalIf(probe.NewError(e), "Unable to get server '%s' config", args.Tail()) if globalJSON { printMsg(configGetMessage{ value: buf, }) } else { // Print printMsg(configGetMessage{ value: buf, }) } return nil } minio-client-0.0~20250403/cmd/admin-config-help.go000066400000000000000000000056761477450377600213730ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "strings" "text/tabwriter" "text/template" "github.com/fatih/color" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) // HelpTmpl template used by all sub-systems const HelpTmpl = `{{if ne .SubSys ""}}{{colorBlueBold "KEY:"}} {{if .MultipleTargets}}{{colorYellowBold .SubSys}}[:name]{{"\t"}}{{else}}{{colorYellowBold .SubSys}}{{"\t"}}{{end}}{{.Description}} {{colorBlueBold "ARGS:"}}{{range .KeysHelp}} {{if .Optional}}{{colorYellowBold .Key}}{{else}}{{colorRedBold .Key}}*{{end}}{{"\t"}}({{.Type}}){{"\t"}}{{.Description}}{{end}}{{else}}{{colorBlueBold "KEYS:"}}{{range .KeysHelp}} {{colorGreenBold .Key}}{{"\t"}}{{.Description}}{{end}}{{end}}` var funcMap = template.FuncMap{ "colorBlueBold": color.New(color.FgBlue, color.Bold).SprintfFunc(), "colorYellowBold": color.New(color.FgYellow, color.Bold).SprintfFunc(), "colorCyanBold": color.New(color.FgCyan, color.Bold).SprintFunc(), "colorRedBold": color.New(color.FgRed, color.Bold).SprintfFunc(), "colorGreenBold": color.New(color.FgGreen, color.Bold).SprintfFunc(), } // HelpTemplate - captures config help template var HelpTemplate = template.Must(template.New("config-help").Funcs(funcMap).Parse(HelpTmpl)) // HelpEnvTemplate - captures config help template var HelpEnvTemplate = template.Must(template.New("config-help-env").Funcs(funcMap).Parse(HelpTmpl)) // configHelpMessage container to hold locks information. type configHelpMessage struct { Status string `json:"status"` Value madmin.Help `json:"help"` envOnly bool } // String colorized service status message. func (u configHelpMessage) String() string { var s strings.Builder w := tabwriter.NewWriter(&s, 1, 8, 2, ' ', 0) var e error if !u.envOnly { e = HelpTemplate.Execute(w, u.Value) } else { e = HelpEnvTemplate.Execute(w, u.Value) } fatalIf(probe.NewError(e), "Unable to initialize template writer") w.Flush() return s.String() } // JSON jsonified service status Message message. func (u configHelpMessage) JSON() string { u.Status = "success" statusJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(statusJSONBytes) } minio-client-0.0~20250403/cmd/admin-config-history.go000066400000000000000000000104021477450377600221230ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "strings" "text/tabwriter" "text/template" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var historyListFlags = []cli.Flag{ cli.IntFlag{ Name: "count, n", Usage: "list only last 'n' entries", Value: 10, }, cli.BoolFlag{ Name: "clear, c", Usage: "clear all history", }, } var adminConfigHistoryCmd = cli.Command{ Name: "history", Usage: "show all historic configuration changes", Before: setGlobalsFromContext, Action: mainAdminConfigHistory, OnUsageError: onUsageError, Flags: append(append([]cli.Flag{}, globalFlags...), historyListFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List all history entries sorted by time. {{.Prompt}} {{.HelpName}} play/ `, } // History template used by all sub-systems const History = `{{range .}}{{colorYellowBold "RestoreId:"}} {{colorYellowBold .RestoreID}} Date: {{.CreateTime}} {{.Targets}} {{end}}` // HistoryTemplate - captures history list template var HistoryTemplate = template.Must(template.New("history-list").Funcs(funcMap).Parse(History)) type historyEntry struct { RestoreID string `json:"restoreId"` CreateTime string `json:"createTime"` Targets string `json:"targets"` } // configHistoryMessage container to hold locks information. type configHistoryMessage struct { Status string `json:"status"` Entries []historyEntry `json:"entries"` } // String colorized service status message. func (u configHistoryMessage) String() string { var s strings.Builder w := tabwriter.NewWriter(&s, 1, 8, 2, ' ', 0) e := HistoryTemplate.Execute(w, u.Entries) fatalIf(probe.NewError(e), "Unable to initialize template writer") w.Flush() return s.String() } // JSON jsonified service status Message message. func (u configHistoryMessage) JSON() string { u.Status = "success" statusJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(statusJSONBytes) } // checkAdminConfigHistorySyntax - validate all the passed arguments func checkAdminConfigHistorySyntax(ctx *cli.Context) { if !ctx.Args().Present() || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainAdminConfigHistory(ctx *cli.Context) error { checkAdminConfigHistorySyntax(ctx) console.SetColor("ConfigHistoryMessageRestoreID", color.New(color.Bold)) console.SetColor("ConfigHistoryMessageTime", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") if ctx.Bool("clear") { fatalIf(probe.NewError(client.ClearConfigHistoryKV(globalContext, "all")), "Unable to clear server configuration.") // Print printMsg(configHistoryMessage{}) return nil } chEntries, e := client.ListConfigHistoryKV(globalContext, ctx.Int("count")) fatalIf(probe.NewError(e), "Unable to list server history configuration.") hentries := make([]historyEntry, len(chEntries)) for i, chEntry := range chEntries { hentries[i] = historyEntry{ RestoreID: chEntry.RestoreID, CreateTime: chEntry.CreateTimeFormatted(), } hentries[i].Targets = chEntry.Data } // Print printMsg(configHistoryMessage{ Entries: hentries, }) return nil } minio-client-0.0~20250403/cmd/admin-config-import.go000066400000000000000000000061441477450377600217440ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "os" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminConfigImportCmd = cli.Command{ Name: "import", Usage: "import multiple config keys from STDIN", Before: setGlobalsFromContext, Action: mainAdminConfigImport, OnUsageError: onUsageError, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Import the new local config and apply to the MinIO server {{.Prompt}} {{.HelpName}} play/ < config.txt `, } // configImportMessage container to hold locks information. type configImportMessage struct { Status string `json:"status"` targetAlias string } // String colorized service status message. func (u configImportMessage) String() (msg string) { msg += console.Colorize("SetConfigSuccess", "Setting new key has been successful.\n") suggestion := fmt.Sprintf("mc admin service restart %s", u.targetAlias) msg += console.Colorize("SetConfigSuccess", fmt.Sprintf("Please restart your server with `%s`.\n", suggestion)) return msg } // JSON jsonified service status Message message. func (u configImportMessage) JSON() string { u.Status = "success" statusJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(statusJSONBytes) } // checkAdminConfigImportSyntax - validate all the passed arguments func checkAdminConfigImportSyntax(ctx *cli.Context) { if !ctx.Args().Present() || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainAdminConfigImport(ctx *cli.Context) error { checkAdminConfigImportSyntax(ctx) // Set color preference of command outputs console.SetColor("SetConfigSuccess", color.New(color.FgGreen, color.Bold)) // Import the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") // Call set config API fatalIf(probe.NewError(client.SetConfig(globalContext, os.Stdin)), "Unable to set server config") // Print printMsg(configImportMessage{ targetAlias: aliasedURL, }) return nil } minio-client-0.0~20250403/cmd/admin-config-reset.go000066400000000000000000000105761477450377600215600ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminConfigEnvFlags = []cli.Flag{ cli.BoolFlag{ Name: "env", Usage: "list all the env only help", }, } var adminConfigResetCmd = cli.Command{ Name: "reset", Usage: "interactively reset a config key parameters", Before: setGlobalsFromContext, Action: mainAdminConfigReset, OnUsageError: onUsageError, Flags: append(adminConfigEnvFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [CONFIG-KEY...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Reset MQTT notifcation target 'name1' settings to default values. {{.Prompt}} {{.HelpName}} myminio/ notify_mqtt:name1 2. Reset compression's 'extensions' setting to default value. {{.Prompt}} {{.HelpName}} myminio/ compression extensions 3. Reset site name and site region to default values. {{.Prompt}} {{.HelpName}} myminio/ site name region `, } // configResetMessage container to hold locks information. type configResetMessage struct { Status string `json:"status"` targetAlias string key string restart bool } // String colorized service status message. func (u configResetMessage) String() (msg string) { msg += console.Colorize("ResetConfigSuccess", fmt.Sprintf("'%s' is successfully reset.", u.key)) if u.restart { suggestion := fmt.Sprintf("mc admin service restart %s", u.targetAlias) msg += console.Colorize("ResetConfigSuccess", fmt.Sprintf("\nPlease restart your server with `%s`.", suggestion)) } return } // JSON jsonified service status message. func (u configResetMessage) JSON() string { u.Status = "success" statusJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(statusJSONBytes) } // checkAdminConfigResetSyntax - validate all the passed arguments func checkAdminConfigResetSyntax(ctx *cli.Context) { if !ctx.Args().Present() { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // main config set function func mainAdminConfigReset(ctx *cli.Context) error { // Check command arguments checkAdminConfigResetSyntax(ctx) // Reset color preference of command outputs console.SetColor("ResetConfigSuccess", color.New(color.FgGreen, color.Bold)) console.SetColor("ResetConfigFailure", color.New(color.FgRed, color.Bold)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") if len(ctx.Args()) == 1 { // Call get config API hr, e := client.HelpConfigKV(globalContext, "", "", ctx.Bool("env")) fatalIf(probe.NewError(e), "Unable to get help for the sub-system") // Print printMsg(configHelpMessage{ Value: hr, envOnly: ctx.Bool("env"), }) return nil } input := strings.Join(args.Tail(), " ") // Check if user has attempted to set values for _, k := range args.Tail() { if strings.Contains(k, "=") { e := fmt.Errorf("new settings may not be provided for sub-system keys") fatalIf(probe.NewError(e), "Unable to reset '%s' on the server", args.Tail()[0]) } } // Call reset config API restart, e := client.DelConfigKV(globalContext, input) fatalIf(probe.NewError(e), "Unable to reset '%s' on the server", input) // Print set config result printMsg(configResetMessage{ targetAlias: aliasedURL, restart: restart, key: input, }) return nil } minio-client-0.0~20250403/cmd/admin-config-restore.go000066400000000000000000000062571477450377600221220ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminConfigRestoreCmd = cli.Command{ Name: "restore", Usage: "rollback back changes to a specific config history", Before: setGlobalsFromContext, Action: mainAdminConfigRestore, OnUsageError: onUsageError, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET RESTOREID FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Restore 'restore-id' history key value on MinIO server. {{.Prompt}} {{.HelpName}} play/ `, } // configRestoreMessage container to hold locks information. type configRestoreMessage struct { Status string `json:"status"` RestoreID string `json:"restoreID"` targetAlias string } // String colorized service status message. func (u configRestoreMessage) String() (msg string) { suggestion := fmt.Sprintf("mc admin service restart %s", u.targetAlias) msg += console.Colorize("ConfigRestoreMessage", fmt.Sprintf("Please restart your server with `%s`.\n", suggestion)) msg += console.Colorize("ConfigRestoreMessage", "Restored "+u.RestoreID+" kv successfully.") return msg } // JSON jsonified service status Message message. func (u configRestoreMessage) JSON() string { u.Status = "success" statusJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(statusJSONBytes) } // checkAdminConfigRestoreSyntax - validate all the passed arguments func checkAdminConfigRestoreSyntax(ctx *cli.Context) { if !ctx.Args().Present() || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainAdminConfigRestore(ctx *cli.Context) error { checkAdminConfigRestoreSyntax(ctx) console.SetColor("ConfigRestoreMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") // Call get config API fatalIf(probe.NewError(client.RestoreConfigHistoryKV(globalContext, args.Get(1))), "Unable to restore server configuration.") // Print printMsg(configRestoreMessage{ RestoreID: args.Get(1), targetAlias: aliasedURL, }) return nil } minio-client-0.0~20250403/cmd/admin-config-set.go000066400000000000000000000077161477450377600212330ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminConfigSetCmd = cli.Command{ Name: "set", Usage: "interactively set a config key parameters", Before: setGlobalsFromContext, Action: mainAdminConfigSet, OnUsageError: onUsageError, Flags: append(adminConfigEnvFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable webhook notification target for MinIO server. {{.Prompt}} {{.HelpName}} myminio/ notify_webhook endpoint="http://localhost:8080/minio/events" 2. Change region name for the MinIO server to 'us-west-1'. {{.Prompt}} {{.HelpName}} myminio/ region name=us-west-1 3. Change healing settings on a distributed MinIO server setup. {{.Prompt}} {{.HelpName}} mydist/ heal max_delay=300ms max_io=50 `, } // configSetMessage container to hold locks information. type configSetMessage struct { Status string `json:"status"` targetAlias string restart bool } // String colorized service status message. func (u configSetMessage) String() (msg string) { msg += console.Colorize("SetConfigSuccess", "Successfully applied new settings.") if u.restart { suggestion := color.RedString("mc admin service restart %s", u.targetAlias) msg += console.Colorize("SetConfigSuccess", fmt.Sprintf("\nPlease restart your server '%s'.", suggestion)) } return } // JSON jsonified service status message. func (u configSetMessage) JSON() string { u.Status = "success" statusJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(statusJSONBytes) } // checkAdminConfigSetSyntax - validate all the passed arguments func checkAdminConfigSetSyntax(ctx *cli.Context) { if !ctx.Args().Present() && len(ctx.Args()) < 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // main config set function func mainAdminConfigSet(ctx *cli.Context) error { // Check command arguments checkAdminConfigSetSyntax(ctx) // Set color preference of command outputs console.SetColor("SetConfigSuccess", color.New(color.FgGreen, color.Bold)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") input := strings.Join(args.Tail(), " ") if !strings.Contains(input, madmin.KvSeparator) { // Call get config API hr, e := client.HelpConfigKV(globalContext, args.Get(1), args.Get(2), ctx.Bool("env")) fatalIf(probe.NewError(e), "Unable to get help for the sub-system") // Print printMsg(configHelpMessage{ Value: hr, envOnly: ctx.Bool("env"), }) return nil } // Call set config API restart, e := client.SetConfigKV(globalContext, input) fatalIf(probe.NewError(e), "Unable to set '%s' to server", input) // Print set config result printMsg(configSetMessage{ targetAlias: aliasedURL, restart: restart, }) return nil } minio-client-0.0~20250403/cmd/admin-config.go000066400000000000000000000027721477450377600204370ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminConfigSubcommands = []cli.Command{ adminConfigGetCmd, adminConfigSetCmd, adminConfigResetCmd, adminConfigHistoryCmd, adminConfigRestoreCmd, adminConfigExportCmd, adminConfigImportCmd, } var adminConfigCmd = cli.Command{ Name: "config", Usage: "manage MinIO server configuration", Action: mainAdminConfig, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminConfigSubcommands, HideHelpCommand: true, } // mainAdminConfig is the handle for "mc admin config" command. func mainAdminConfig(ctx *cli.Context) error { commandNotFound(ctx, adminConfigSubcommands) return nil // Sub-commands like "get", "set" have their own main. } minio-client-0.0~20250403/cmd/admin-console.go000066400000000000000000000040621477450377600206260ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strings" "github.com/minio/cli" ) var adminConsoleFlags = []cli.Flag{ cli.IntFlag{ Name: "limit, l", Usage: "show last n log entries", Value: 10, }, cli.StringFlag{ Name: "type, t", Usage: "list error logs by type. Valid options are '[minio, application, all]'", Value: "all", }, } var adminConsoleCmd = cli.Command{ Name: "console", Usage: "show MinIO logs", Action: mainAdminConsole, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminConsoleFlags, globalFlags...), Hidden: true, HideHelpCommand: true, CustomHelpTemplate: "This command is not supported now and replaced by 'admin logs' command. Please use 'mc admin logs'.\n", } // mainAdminConsole - the entry function of console command func mainAdminConsole(ctx *cli.Context) error { newCmd := []string{"mc admin logs"} var flgStr string if ctx.IsSet("limit") { flgStr = fmt.Sprintf("%s %d", "--last", ctx.Int("limit")) newCmd = append(newCmd, flgStr) } if ctx.IsSet("type") { flgStr = fmt.Sprintf("%s %s", "--type", strings.ToLower(ctx.String("type"))) newCmd = append(newCmd, flgStr) } newCmd = append(newCmd, ctx.Args()...) deprecatedError(strings.Join(newCmd, " ")) return nil } minio-client-0.0~20250403/cmd/admin-decom-cancel.go000066400000000000000000000076731477450377600215110ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "path/filepath" humanize "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminDecommissionCancelCmd = cli.Command{ Name: "cancel", Usage: "cancel an ongoing decommissioning of a pool", Action: mainAdminDecommissionCancel, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Cancel an ongoing decommissioning of a pool. {{.Prompt}} {{.HelpName}} myminio/ http://server{5...8}/disk{1...4} 2. Cancel all ongoing decommissioning of pools. {{.Prompt}} {{.HelpName}} myminio/ `, } // checkAdminDecommissionCancelSyntax - validate all the passed arguments func checkAdminDecommissionCancelSyntax(ctx *cli.Context) { if len(ctx.Args()) > 2 || len(ctx.Args()) == 0 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminDecommissionCancel is the handle for "mc admin decommission cancel" command. func mainAdminDecommissionCancel(ctx *cli.Context) error { checkAdminDecommissionCancelSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) aliasedURL = filepath.Clean(aliasedURL) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") if pool := args.Get(1); pool != "" { e := client.CancelDecommissionPool(globalContext, pool) fatalIf(probe.NewError(e).Trace(args...), "Unable to cancel decommissioning, please try again") return nil } poolStatuses, e := client.ListPoolsStatus(globalContext) fatalIf(probe.NewError(e).Trace(args...), "Unable to get status for all pools") var newPoolStatuses []madmin.PoolStatus for _, pool := range poolStatuses { if pool.Decommission != nil { if pool.Decommission.StartTime.IsZero() { continue } if pool.Decommission.Complete { continue } } newPoolStatuses = append(newPoolStatuses, pool) } dspOrder := []col{colGreen} // Header for i := 0; i < len(newPoolStatuses); i++ { dspOrder = append(dspOrder, colGrey) } var printColors []*color.Color for _, c := range dspOrder { printColors = append(printColors, getPrintCol(c)) } tbl := console.NewTable(printColors, []bool{false, false, false, false}, 0) cellText := make([][]string, len(newPoolStatuses)+1) cellText[0] = []string{ "ID", "Pools", "Capacity", "Status", } for idx, pool := range poolStatuses { idx++ totalSize := uint64(pool.Decommission.TotalSize) currentSize := uint64(pool.Decommission.CurrentSize) capacity := humanize.IBytes(totalSize-currentSize) + " (used) / " + humanize.IBytes(totalSize) + " (total)" status := "" if pool.Decommission != nil { if pool.Decommission.StartTime.IsZero() { continue } if pool.Decommission.Complete { continue } status = "Draining" } cellText[idx] = []string{ humanize.Ordinal(pool.ID + 1), pool.CmdLine, capacity, status, } } return tbl.DisplayTable(cellText) } minio-client-0.0~20250403/cmd/admin-decom-start.go000066400000000000000000000060611477450377600214070ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "path/filepath" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminDecommissionStartCmd = cli.Command{ Name: "start", Usage: "start decommissioning a pool", Action: mainAdminDecommissionStart, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Start decommissioning a pool for removal. {{.Prompt}} {{.HelpName}} myminio/ http://server{5...8}/disk{1...4} `, } // checkAdminDecommissionStartSyntax - validate all the passed arguments func checkAdminDecommissionStartSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // startDecomMessage is container for make bucket success and failure messages. type startDecomMessage struct { Status string `json:"status"` Pool string `json:"pool"` } // String colorized construct a string message. func (s startDecomMessage) String() string { return console.Colorize("DecomPool", "Decommission started successfully for `"+s.Pool+"`.") } // JSON jsonified decom message. func (s startDecomMessage) JSON() string { startDecomBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(startDecomBytes) } // mainAdminDecommissionStart is the handle for "mc admin decommission start" command. func mainAdminDecommissionStart(ctx *cli.Context) error { checkAdminDecommissionStartSyntax(ctx) // Additional command speific theme customization. console.SetColor("DecomPool", color.New(color.FgGreen, color.Bold)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) aliasedURL = filepath.Clean(aliasedURL) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") e := client.DecommissionPool(globalContext, args.Get(1)) fatalIf(probe.NewError(e).Trace(args...), "Unable to start decommission on the specified pool") printMsg(startDecomMessage{ Status: "success", Pool: args.Get(1), }) return nil } minio-client-0.0~20250403/cmd/admin-decom-status.go000066400000000000000000000135231477450377600215760ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "path/filepath" "time" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminDecommissionStatusCmd = cli.Command{ Name: "status", Usage: "show current decommissioning status", Action: mainAdminDecommissionStatus, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Show current decommissioning status. {{.Prompt}} {{.HelpName}} myminio/ http://server{5...8}/disk{1...4} 2. List all current decommissioning status of all pools. {{.Prompt}} {{.HelpName}} myminio/ `, } // checkAdminDecommissionStatusSyntax - validate all the passed arguments func checkAdminDecommissionStatusSyntax(ctx *cli.Context) { if len(ctx.Args()) > 2 || len(ctx.Args()) == 0 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminDecommissionStatus is the handle for "mc admin decomission status" command. func mainAdminDecommissionStatus(ctx *cli.Context) error { checkAdminDecommissionStatusSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) aliasedURL = filepath.Clean(aliasedURL) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") if pool := args.Get(1); pool != "" { poolStatus, e := client.StatusPool(globalContext, pool) fatalIf(probe.NewError(e).Trace(args...), "Unable to get status per pool") if globalJSON { statusJSONBytes, e := json.MarshalIndent(poolStatus, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") console.Println(string(statusJSONBytes)) return nil } var msg string if poolStatus.Decommission.Complete { msg = color.GreenString(fmt.Sprintf("Decommission of pool %s is complete, you may now remove it from server command line", poolStatus.CmdLine)) } else if poolStatus.Decommission.Failed { msg = color.GreenString(fmt.Sprintf("Decommission of pool %s failed, please retry again", poolStatus.CmdLine)) } else if poolStatus.Decommission.Canceled { msg = color.GreenString(fmt.Sprintf("Decommission of pool %s was canceled, you may start again", poolStatus.CmdLine)) } else if !poolStatus.Decommission.StartTime.IsZero() { usedStart := (poolStatus.Decommission.TotalSize - poolStatus.Decommission.StartSize) usedCurrent := (poolStatus.Decommission.TotalSize - poolStatus.Decommission.CurrentSize) duration := float64(time.Since(poolStatus.Decommission.StartTime)) / float64(time.Second) if usedStart > usedCurrent && duration > 10 { copied := uint64(usedStart - usedCurrent) speed := uint64(float64(copied) / duration) msg = "Decommissioning rate at " + humanize.IBytes(speed) + "/sec " + "[" + humanize.IBytes( uint64(usedCurrent)) + "/" + humanize.IBytes(uint64(poolStatus.Decommission.TotalSize)) + "]" msg += "\nStarted: " + humanize.RelTime(time.Now().UTC(), poolStatus.Decommission.StartTime, "", "ago") } else { msg = "Decommissioning is starting..." } msg = color.GreenString(msg) } else { errorIf(errDummy().Trace(args...), "This pool is currently not scheduled for decomissioning") return nil } fmt.Println(msg) return nil } poolStatuses, e := client.ListPoolsStatus(globalContext) fatalIf(probe.NewError(e).Trace(args...), "Unable to get status for all pools") if globalJSON { statusJSONBytes, e := json.MarshalIndent(poolStatuses, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") console.Println(string(statusJSONBytes)) return nil } dspOrder := []col{colGreen} // Header for i := 0; i < len(poolStatuses); i++ { dspOrder = append(dspOrder, colGrey) } var printColors []*color.Color for _, c := range dspOrder { printColors = append(printColors, getPrintCol(c)) } tbl := console.NewTable(printColors, []bool{false, false, false, false}, 0) cellText := make([][]string, len(poolStatuses)+1) cellText[0] = []string{ "ID", "Pools", "Drives Usage", "Status", } for idx, pool := range poolStatuses { idx++ totalSize := uint64(pool.Decommission.TotalSize) usedCurrent := uint64(pool.Decommission.TotalSize - pool.Decommission.CurrentSize) var capacity string if totalSize == 0 { capacity = "0% (total: 0B)" } else { capacity = fmt.Sprintf("%.1f%% (total: %s)", 100*float64(usedCurrent)/float64(totalSize), humanize.IBytes(totalSize)) } status := "Active" if pool.Decommission != nil { if pool.Decommission.Complete { status = "Complete" } else if pool.Decommission.Failed { status = "Draining(Failed)" } else if pool.Decommission.Canceled { status = "Draining(Canceled)" } else if !pool.Decommission.StartTime.IsZero() { status = "Draining" } } cellText[idx] = []string{ humanize.Ordinal(pool.ID + 1), pool.CmdLine, capacity, status, } } return tbl.DisplayTable(cellText) } minio-client-0.0~20250403/cmd/admin-decom.go000066400000000000000000000030331477450377600202500ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminDecommissionSubcommands = []cli.Command{ adminDecommissionStartCmd, adminDecommissionStatusCmd, adminDecommissionCancelCmd, } var adminDecommissionCmd = cli.Command{ Name: "decommission", Aliases: []string{"decom"}, Usage: "manage MinIO server pool decommissioning", Action: mainAdminDecommission, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminDecommissionSubcommands, HideHelpCommand: true, } // mainAdminDecommission is the handle for "mc admin decommission" command. func mainAdminDecommission(ctx *cli.Context) error { commandNotFound(ctx, adminDecommissionSubcommands) return nil // Sub-commands like "get", "set" have their own main. } minio-client-0.0~20250403/cmd/admin-group-add.go000066400000000000000000000112001477450377600210360ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminGroupAddCmd = cli.Command{ Name: "add", Usage: "add users to a new or existing group", Action: mainAdminGroupAdd, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET GROUPNAME MEMBERS... FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Add users 'fivecent' and 'tencent' to the group 'allcents': {{.Prompt}} {{.HelpName}} myminio allcents fivecent tencent 2. Add user "james" to group "staff", then add the "readwrite" policy to the group "staff". {{.Prompt}} {{.HelpName}} myminio staff james {{.Prompt}} mc admin policy attach myminio readwrite --group staff `, } // checkAdminGroupAddSyntax - validate all the passed arguments func checkAdminGroupAddSyntax(ctx *cli.Context) { if len(ctx.Args()) < 3 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // groupMessage container for content message structure type groupMessage struct { op string Status string `json:"status"` GroupName string `json:"groupName,omitempty"` Groups []string `json:"groups,omitempty"` Members []string `json:"members,omitempty"` GroupStatus string `json:"groupStatus,omitempty"` GroupPolicy string `json:"groupPolicy,omitempty"` } func (u groupMessage) String() string { switch u.op { case "list": var s []string for _, g := range u.Groups { s = append(s, console.Colorize("GroupMessage", g)) } return strings.Join(s, "\n") case "disable": return console.Colorize("GroupMessage", "Disabled group `"+u.GroupName+"` successfully.") case "enable": return console.Colorize("GroupMessage", "Enabled group `"+u.GroupName+"` successfully.") case "add": membersStr := fmt.Sprintf("`%s`", strings.Join(u.Members, ",")) return console.Colorize("GroupMessage", "Added members "+membersStr+" to group `"+u.GroupName+"` successfully.") case "remove": if len(u.Members) > 0 { membersStr := fmt.Sprintf("{%s}", strings.Join(u.Members, ",")) return console.Colorize("GroupMessage", "Removed members "+membersStr+" from group "+u.GroupName+" successfully.") } return console.Colorize("GroupMessage", "Removed group "+u.GroupName+" successfully.") case "info": return strings.Join([]string{ console.Colorize("GroupMessage", "Group: "+u.GroupName), console.Colorize("GroupMessage", "Status: "+u.GroupStatus), console.Colorize("GroupMessage", "Policy: "+u.GroupPolicy), console.Colorize("GroupMessage", "Members: "+strings.Join(u.Members, ",")), }, "\n") } return "" } func (u groupMessage) JSON() string { u.Status = "success" jsonMessageBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // mainAdminGroupAdd is the handle for "mc admin group add" command. func mainAdminGroupAdd(ctx *cli.Context) error { checkAdminGroupAddSyntax(ctx) console.SetColor("GroupMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") members := []string{} for i := 2; i < ctx.NArg(); i++ { members = append(members, args.Get(i)) } gAddRemove := madmin.GroupAddRemove{ Group: args.Get(1), Members: members, IsRemove: false, } fatalIf(probe.NewError(client.UpdateGroupMembers(globalContext, gAddRemove)).Trace(args...), "Unable to add new group") printMsg(groupMessage{ op: ctx.Command.Name, GroupName: args.Get(1), Members: members, }) return nil } minio-client-0.0~20250403/cmd/admin-group-disable.go000066400000000000000000000024141477450377600217200ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminGroupDisableCmd = cli.Command{ Name: "disable", Usage: "disable a group", Action: mainAdminGroupEnableDisable, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET GROUPNAME FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Disable group 'allcents'. {{.Prompt}} {{.HelpName}} myminio allcents `, } minio-client-0.0~20250403/cmd/admin-group-enable.go000066400000000000000000000051061477450377600215440ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminGroupEnableCmd = cli.Command{ Name: "enable", Usage: "enable a group", Action: mainAdminGroupEnableDisable, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET GROUPNAME FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable group 'allcents'. {{.Prompt}} {{.HelpName}} myminio allcents `, } // checkAdminGroupEnableSyntax - validate all the passed arguments func checkAdminGroupEnableSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminGroupEnableDisable is the handle for "mc admin group enable|disable" command. func mainAdminGroupEnableDisable(ctx *cli.Context) error { checkAdminGroupEnableSyntax(ctx) console.SetColor("GroupMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") group := args.Get(1) var status madmin.GroupStatus switch ctx.Command.Name { case "enable": status = madmin.GroupEnabled case "disable": status = madmin.GroupDisabled default: fatalIf(errInvalidArgument().Trace(ctx.Command.Name), "Invalid group status name") } e := client.SetGroupStatus(globalContext, group, status) fatalIf(probe.NewError(e).Trace(args...), "Unable set group status") printMsg(groupMessage{ op: ctx.Command.Name, GroupName: group, GroupStatus: string(status), }) return nil } minio-client-0.0~20250403/cmd/admin-group-info.go000066400000000000000000000044671477450377600212620ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminGroupInfoCmd = cli.Command{ Name: "info", Usage: "display group info", Action: mainAdminGroupInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET GROUPNAME FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get info on group 'allcents'. {{.Prompt}} {{.HelpName}} myminio allcents `, } // checkAdminGroupInfoSyntax - validate all the passed arguments func checkAdminGroupInfoSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminGroupInfo is the handle for "mc admin group info" command. func mainAdminGroupInfo(ctx *cli.Context) error { checkAdminGroupInfoSyntax(ctx) console.SetColor("GroupMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") group := args.Get(1) gd, e := client.GetGroupDescription(globalContext, group) fatalIf(probe.NewError(e).Trace(args...), "Unable to fetch group info") printMsg(groupMessage{ op: ctx.Command.Name, GroupName: group, GroupStatus: gd.Status, GroupPolicy: gd.Policy, Members: gd.Members, }) return nil } minio-client-0.0~20250403/cmd/admin-group-list.go000066400000000000000000000042511477450377600212710ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminGroupListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "display list of groups", Action: mainAdminGroupList, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List all groups. {{.Prompt}} {{.HelpName}} myminio `, } // checkAdminGroupListSyntax - validate all the passed arguments func checkAdminGroupListSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminGroupList is the handle for "mc admin group list" command. func mainAdminGroupList(ctx *cli.Context) error { checkAdminGroupListSyntax(ctx) console.SetColor("GroupMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") gs, e := client.ListGroups(globalContext) fatalIf(probe.NewError(e).Trace(args...), "Unable to list groups") printMsg(groupMessage{ op: ctx.Command.Name, Groups: gs, }) return nil } minio-client-0.0~20250403/cmd/admin-group-remove.go000066400000000000000000000052531477450377600216160ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminGroupRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove group or members from a group", Action: mainAdminGroupRemove, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET GROUPNAME [USERNAMES...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove members 'tencent' and 'fivecent' from group 'allcents'. {{.Prompt}} {{.HelpName}} myminio allcents tencent fivecent 2. Remove group 'allcents'. {{.Prompt}} {{.HelpName}} myminio allcents `, } // checkAdminGroupRemoveSyntax - validate all the passed arguments func checkAdminGroupRemoveSyntax(ctx *cli.Context) { if len(ctx.Args()) < 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminGroupRemove is the handle for "mc admin group remove" command. func mainAdminGroupRemove(ctx *cli.Context) error { checkAdminGroupRemoveSyntax(ctx) console.SetColor("GroupMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") members := []string{} for i := 2; i < ctx.NArg(); i++ { members = append(members, args.Get(i)) } gAddRemove := madmin.GroupAddRemove{ Group: args.Get(1), Members: members, IsRemove: true, } e := client.UpdateGroupMembers(globalContext, gAddRemove) fatalIf(probe.NewError(e).Trace(args...), "Could not perform remove operation") printMsg(groupMessage{ op: ctx.Command.Name, GroupName: args.Get(1), Members: members, }) return nil } minio-client-0.0~20250403/cmd/admin-group.go000066400000000000000000000027001477450377600203150ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminGroupSubcommands = []cli.Command{ adminGroupAddCmd, adminGroupRemoveCmd, adminGroupInfoCmd, adminGroupListCmd, adminGroupEnableCmd, adminGroupDisableCmd, } var adminGroupCmd = cli.Command{ Name: "group", Usage: "manage groups", Action: mainAdminGroup, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminGroupSubcommands, HideHelpCommand: true, } // mainAdminGroup is the handle for "mc admin config" command. func mainAdminGroup(ctx *cli.Context) error { commandNotFound(ctx, adminGroupSubcommands) return nil // Sub-commands like "get", "set" have their own main. } minio-client-0.0~20250403/cmd/admin-heal-result-item.go000066400000000000000000000111111477450377600223360ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "errors" "fmt" "github.com/minio/madmin-go/v3" ) type hri struct { *madmin.HealResultItem } func newHRI(i *madmin.HealResultItem) *hri { return &hri{i} } // getObjectHCCChange - returns before and after color change for // objects func (h hri) getObjectHCCChange() (b, a col, err error) { parityShards := h.ParityBlocks dataShards := h.DataBlocks onlineBefore, onlineAfter := h.GetOnlineCounts() surplusShardsBeforeHeal := onlineBefore - dataShards surplusShardsAfterHeal := onlineAfter - dataShards b, err = getHColCode(surplusShardsBeforeHeal, parityShards) if err != nil { err = fmt.Errorf("%w: surplusShardsBeforeHeal: %d, parityShards: %d", err, surplusShardsBeforeHeal, parityShards) return } a, err = getHColCode(surplusShardsAfterHeal, parityShards) if err != nil { err = fmt.Errorf("%w: surplusShardsAfterHeal: %d, parityShards: %d", err, surplusShardsAfterHeal, parityShards) } return } // getBucketHCCChange - fetches health color code for bucket healing // this does not return a Grey color since it does not have any meaning // for a bucket healing. Return green if the bucket is found in a drive, // yellow for missing, and red for everything else, grey for weird situations func (h hri) getBucketHCCChange() (b, a col, err error) { if h.HealResultItem == nil { return colGrey, colGrey, errors.New("empty result") } getColCode := func(drives []madmin.HealDriveInfo) (c col) { var missing, unavailable int for i := range drives { switch drives[i].State { case madmin.DriveStateOk: case madmin.DriveStateMissing: missing++ default: unavailable++ } } if unavailable > 0 { return colRed } if missing > 0 { return colYellow } return colGreen } a, b = colGrey, colGrey if len(h.Before.Drives) > 0 { b = getColCode(h.Before.Drives) } if len(h.After.Drives) > 0 { a = getColCode(h.After.Drives) } return } // getReplicatedFileHCCChange - fetches health color code for metadata // files that are replicated. func (h hri) getReplicatedFileHCCChange() (b, a col, err error) { getColCode := func(numAvail int) (c col, err error) { // calculate color code for replicated object similar // to erasure coded objects var quorum, surplus, parity int if h.SetCount > 0 { quorum = h.DiskCount/h.SetCount/2 + 1 surplus = numAvail/h.SetCount - quorum parity = h.DiskCount/h.SetCount - quorum } else { // in case of bucket healing, disk count is for the node // also explicitly set count would be set to invalid value of -1 quorum = h.DiskCount/2 + 1 surplus = numAvail - quorum parity = h.DiskCount - quorum } c, err = getHColCode(surplus, parity) return } onlineBefore, onlineAfter := h.GetOnlineCounts() b, err = getColCode(onlineBefore) if err != nil { return } a, err = getColCode(onlineAfter) return } func (h hri) makeHealEntityString() string { switch h.Type { case madmin.HealItemObject: return h.Bucket + "/" + h.Object case madmin.HealItemBucket: return h.Bucket case madmin.HealItemMetadata: return "[disk-format]" case madmin.HealItemBucketMetadata: return fmt.Sprintf("[bucket-metadata]%s/%s", h.Bucket, h.Object) } return "** unexpected **" } func (h hri) getHRTypeAndName() (typ, name string) { name = fmt.Sprintf("%s/%s", h.Bucket, h.Object) switch h.Type { case madmin.HealItemMetadata: typ = "system" name = h.Detail case madmin.HealItemBucketMetadata: typ = "system" name = "bucket-metadata:" + name case madmin.HealItemBucket: typ = "bucket" case madmin.HealItemObject: typ = "object" default: typ = fmt.Sprintf("!! Unknown heal result record %#v !!", h) name = typ } return } func (h hri) getHealResultStr() string { typ, name := h.getHRTypeAndName() switch h.Type { case madmin.HealItemMetadata, madmin.HealItemBucketMetadata: return typ + ":" + name default: return name } } minio-client-0.0~20250403/cmd/admin-heal-ui.go000066400000000000000000000277461477450377600205260ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "errors" "fmt" "math" "strings" "time" humanize "github.com/dustin/go-humanize" "github.com/fatih/color" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) const ( lineWidth = 80 ) var ( hColOrder = []col{colRed, colYellow, colGreen} hColTable = map[int][]int{ 1: {0, -1, 1}, 2: {0, 1, 2}, 3: {1, 2, 3}, 4: {1, 2, 4}, 5: {1, 3, 5}, 6: {2, 4, 6}, 7: {2, 4, 7}, 8: {2, 5, 8}, } ) func getHColCode(surplusShards, parityShards int) (c col, err error) { if parityShards < 1 || parityShards > 8 || surplusShards > parityShards { return c, errors.New("Invalid parity shard count/surplus shard count given") } if surplusShards < 0 { return colGrey, err } colRow := hColTable[parityShards] for index, val := range colRow { if val != -1 && surplusShards <= val { return hColOrder[index], err } } return c, errors.New("cannot get a heal color code") } type uiData struct { Bucket, Prefix string Client *madmin.AdminClient ClientToken string ForceStart bool HealOpts *madmin.HealOpts LastItem *hri // Total time since heal start HealDuration time.Duration // Accumulated statistics of heal result records BytesScanned int64 // Counter for objects, and another counter for all kinds of // items ObjectsScanned, ItemsScanned int64 // Counters for healed objects and all kinds of healed items ObjectsHealed, ItemsHealed int64 // Map from online drives to number of objects with that many // online drives. ObjectsByOnlineDrives map[int]int64 // Map of health color code to number of objects with that // health color code. HealthCols map[col]int64 // channel to receive a prompt string to indicate activity on // the terminal CurChan (<-chan string) } func (ui *uiData) updateStats(i madmin.HealResultItem) error { if i.Type == madmin.HealItemObject { // Objects whose size could not be found have -1 size // returned. if i.ObjectSize >= 0 { ui.BytesScanned += i.ObjectSize } ui.ObjectsScanned++ } ui.ItemsScanned++ beforeUp, afterUp := i.GetOnlineCounts() if afterUp > beforeUp { if i.Type == madmin.HealItemObject { ui.ObjectsHealed++ } ui.ItemsHealed++ } ui.ObjectsByOnlineDrives[afterUp]++ // Update health color stats: // Fetch health color after heal: var err error var afterCol col h := newHRI(&i) switch h.Type { case madmin.HealItemBucket: _, afterCol, err = h.getBucketHCCChange() case madmin.HealItemMetadata, madmin.HealItemBucketMetadata: _, afterCol, err = h.getReplicatedFileHCCChange() default: _, afterCol, err = h.getObjectHCCChange() } if err != nil { return err } ui.HealthCols[afterCol]++ return nil } func (ui *uiData) updateDuration(s *madmin.HealTaskStatus) { ui.HealDuration = UTCNow().Sub(s.StartTime) } func (ui *uiData) getProgress() (oCount, objSize, duration string) { oCount = humanize.Comma(ui.ObjectsScanned) duration = ui.HealDuration.Round(time.Second).String() bytesScanned := float64(ui.BytesScanned) // Compute unit for object size magnitudes := []float64{1 << 10, 1 << 20, 1 << 30, 1 << 40, 1 << 50, 1 << 60} units := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} var i int for i = 0; i < len(magnitudes); i++ { if bytesScanned <= magnitudes[i] { break } } numUnits := int(bytesScanned * (1 << 10) / magnitudes[i]) objSize = fmt.Sprintf("%d %s", numUnits, units[i]) return } func (ui *uiData) getPercentsNBars() (p map[col]float64, b map[col]string) { // barChar, emptyBarChar := "█", "░" barChar, emptyBarChar := "█", " " barLen := 12 sum := float64(ui.ItemsScanned) cols := []col{colGrey, colRed, colYellow, colGreen} p = make(map[col]float64, len(cols)) b = make(map[col]string, len(cols)) var filledLen int for _, col := range cols { v := float64(ui.HealthCols[col]) if sum == 0 { p[col] = 0 filledLen = 0 } else { p[col] = v * 100 / sum // round up the filled part filledLen = int(math.Ceil(float64(barLen) * v / sum)) } b[col] = strings.Repeat(barChar, filledLen) + strings.Repeat(emptyBarChar, barLen-filledLen) } return } func (ui *uiData) printItemsQuietly(s *madmin.HealTaskStatus) (err error) { lpad := func(s col) string { return fmt.Sprintf("%-6s", string(s)) } rpad := func(s col) string { return fmt.Sprintf("%6s", string(s)) } printColStr := func(before, after col) { console.PrintC("[" + lpad(before) + " -> " + rpad(after) + "] ") } var b, a col for _, item := range s.Items { h := newHRI(&item) hrStr := h.getHealResultStr() switch h.Type { case madmin.HealItemBucket: b, a, err = h.getBucketHCCChange() case madmin.HealItemMetadata, madmin.HealItemBucketMetadata: b, a, err = h.getReplicatedFileHCCChange() default: b, a, err = h.getObjectHCCChange() } if err != nil { errMsg := err.Error() if h.Detail != "" { errMsg = h.Detail } console.PrintC("[ERROR] ** ", hrStr, " **", ": ", errMsg, "\n") continue } printColStr(b, a) switch h.Type { case madmin.HealItemMetadata, madmin.HealItemBucketMetadata: console.PrintC(fmt.Sprintln("**", hrStr, "**")) default: console.PrintC(hrStr, "\n") } } return nil } func (ui *uiData) printStatsQuietly() { totalObjects, totalSize, totalTime := ui.getProgress() healedStr := fmt.Sprintf("Healed:\t%s/%s objects; %s in %s\n", humanize.Comma(ui.ObjectsHealed), totalObjects, totalSize, totalTime) console.PrintC(healedStr) } func (ui *uiData) printItemsJSON(s *madmin.HealTaskStatus) (err error) { type healRec struct { Status string `json:"status"` Error string `json:"error,omitempty"` Detail string `json:"detail,omitempty"` Type string `json:"type"` Name string `json:"name"` Before struct { Color string `json:"color"` Offline int `json:"offline"` Online int `json:"online"` Missing int `json:"missing"` Corrupted int `json:"corrupted"` Drives []madmin.HealDriveInfo `json:"drives"` } `json:"before"` After struct { Color string `json:"color"` Offline int `json:"offline"` Online int `json:"online"` Missing int `json:"missing"` Corrupted int `json:"corrupted"` Drives []madmin.HealDriveInfo `json:"drives"` } `json:"after"` Size int64 `json:"size"` } makeHR := func(h *hri) (r healRec) { r.Status = "success" r.Type, r.Name = h.getHRTypeAndName() var b, a col var err error switch h.Type { case madmin.HealItemBucket: b, a, err = h.getBucketHCCChange() case madmin.HealItemMetadata, madmin.HealItemBucketMetadata: b, a, err = h.getReplicatedFileHCCChange() default: if h.Type == madmin.HealItemObject { r.Size = h.ObjectSize } b, a, err = h.getObjectHCCChange() } if err != nil { r.Error = err.Error() } r.Detail = h.Detail r.Before.Color = strings.ToLower(string(b)) r.After.Color = strings.ToLower(string(a)) r.Before.Online, r.After.Online = h.GetOnlineCounts() r.Before.Missing, r.After.Missing = h.GetMissingCounts() r.Before.Corrupted, r.After.Corrupted = h.GetCorruptedCounts() r.Before.Offline, r.After.Offline = h.GetOfflineCounts() r.Before.Drives = h.Before.Drives r.After.Drives = h.After.Drives return r } for _, item := range s.Items { h := newHRI(&item) jsonBytes, e := json.MarshalIndent(makeHR(h), "", " ") fatalIf(probe.NewError(e), "Unable to marshal to JSON.") console.Println(string(jsonBytes)) } return nil } func (ui *uiData) printStatsJSON(_ *madmin.HealTaskStatus) { var summary struct { Status string `json:"status"` Error string `json:"error,omitempty"` Type string `json:"type"` ObjectsScanned int64 `json:"objects_scanned"` ObjectsHealed int64 `json:"objects_healed"` ItemsScanned int64 `json:"items_scanned"` ItemsHealed int64 `json:"items_healed"` Size int64 `json:"size"` ElapsedTime int64 `json:"duration"` } summary.Status = "success" summary.Type = "summary" summary.ObjectsScanned = ui.ObjectsScanned summary.ObjectsHealed = ui.ObjectsHealed summary.ItemsScanned = ui.ItemsScanned summary.ItemsHealed = ui.ItemsHealed summary.Size = ui.BytesScanned summary.ElapsedTime = int64(ui.HealDuration.Round(time.Second).Seconds()) jBytes, e := json.MarshalIndent(summary, "", " ") fatalIf(probe.NewError(e), "Unable to marshal to JSON.") console.Println(string(jBytes)) } func (ui *uiData) updateUI(s *madmin.HealTaskStatus) (err error) { itemCount := len(s.Items) h := ui.LastItem if itemCount > 0 { item := s.Items[itemCount-1] h = newHRI(&item) ui.LastItem = h } scannedStr := "** waiting for status from server **" if h != nil { scannedStr = lineTrunc(h.makeHealEntityString(), lineWidth-len("Scanned: ")) } totalObjects, totalSize, totalTime := ui.getProgress() healedStr := fmt.Sprintf("%s/%s objects; %s in %s", humanize.Comma(ui.ObjectsHealed), totalObjects, totalSize, totalTime) console.Print(console.Colorize("HealUpdateUI", fmt.Sprintf(" %s", <-ui.CurChan))) console.PrintC(fmt.Sprintf(" %s\n", scannedStr)) console.PrintC(fmt.Sprintf(" %s\n", healedStr)) dspOrder := []col{colGreen, colYellow, colRed, colGrey} printColors := []*color.Color{} for _, c := range dspOrder { printColors = append(printColors, getPrintCol(c)) } t := console.NewTable(printColors, []bool{false, true, true}, 4) percentMap, barMap := ui.getPercentsNBars() cellText := make([][]string, len(dspOrder)) for i := range cellText { cellText[i] = []string{ string(dspOrder[i]), fmt.Sprint(humanize.Comma(ui.HealthCols[dspOrder[i]])), fmt.Sprintf("%5.1f%% %s", percentMap[dspOrder[i]], barMap[dspOrder[i]]), } } t.DisplayTable(cellText) return nil } func (ui *uiData) UpdateDisplay(s *madmin.HealTaskStatus) { // Update state ui.updateDuration(s) for _, i := range s.Items { ui.updateStats(i) } // Update display switch { case globalJSON: ui.printItemsJSON(s) case globalQuiet: ui.printItemsQuietly(s) default: ui.updateUI(s) } } func (ui *uiData) healResumeMsg(aliasedURL string) string { var flags string if ui.HealOpts.Recursive { flags += "--recursive " } if ui.HealOpts.DryRun { flags += "--dry-run " } return fmt.Sprintf("Healing is backgrounded, to resume watching use `mc admin heal %s %s`", flags, aliasedURL) } func (ui *uiData) DisplayAndFollowHealStatus(aliasedURL string) (res madmin.HealTaskStatus, err error) { quitMsg := ui.healResumeMsg(aliasedURL) firstIter := true for { select { case <-globalContext.Done(): return res, errors.New(quitMsg) default: _, res, err = ui.Client.Heal(globalContext, ui.Bucket, ui.Prefix, *ui.HealOpts, ui.ClientToken, ui.ForceStart, false) if err != nil { return res, err } if firstIter { firstIter = false } else { if !globalQuiet && !globalJSON { console.RewindLines(8) } } ui.UpdateDisplay(&res) if res.Summary == "finished" { if globalJSON { ui.printStatsJSON(&res) } else if globalQuiet { ui.printStatsQuietly() } return res, nil } if res.Summary == "stopped" { return res, fmt.Errorf("Heal had an error - %s", res.FailureDetail) } time.Sleep(time.Second) } } } minio-client-0.0~20250403/cmd/admin-heal.go000066400000000000000000000533261477450377600201040ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "fmt" "math" "net/url" "os" "path/filepath" "sort" "strings" "time" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) const ( scanNormalMode = "normal" scanDeepMode = "deep" ) var adminHealFlags = []cli.Flag{ cli.IntFlag{ Name: "pool", Usage: "heal only the given pool", Hidden: true, }, cli.IntFlag{ Name: "set", Usage: "heal only the given set", Hidden: true, }, cli.StringFlag{ Name: "scan", Usage: "select the healing scan mode (normal/deep)", Value: scanNormalMode, Hidden: true, }, cli.BoolFlag{ Name: "recursive, r", Usage: "heal recursively", Hidden: true, }, cli.BoolFlag{ Name: "dry-run, n", Usage: "only inspect data, but do not mutate", Hidden: true, }, cli.BoolFlag{ Name: "force-start, f", Usage: "force start a new heal sequence", Hidden: true, }, cli.BoolFlag{ Name: "force-stop, s", Usage: "force stop a running heal sequence", Hidden: true, }, cli.BoolFlag{ Name: "force", Usage: "avoid showing a warning prompt", }, cli.BoolFlag{ Name: "remove", Usage: "remove dangling objects in heal sequence", Hidden: true, }, cli.StringFlag{ Name: "storage-class", Usage: "show server/drives failure tolerance with the given storage class", Hidden: true, }, cli.BoolFlag{ Name: "rewrite", Usage: "rewrite objects from older to newer format", Hidden: true, }, cli.BoolFlag{ Name: "verbose, v", Usage: "show verbose information", }, cli.BoolFlag{ Name: "all-drives, a", Usage: "select all drives for verbose printing", }, } var adminHealCmd = cli.Command{ Name: "heal", Usage: "monitor healing for bucket(s) and object(s) on MinIO server", Action: mainAdminHeal, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminHealFlags, globalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Monitor healing status on a running server at alias 'myminio': {{.Prompt}} {{.HelpName}} myminio/ `, } func checkAdminHealSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } // Check for scan argument scanArg := ctx.String("scan") scanArg = strings.ToLower(scanArg) if scanArg != scanNormalMode && scanArg != scanDeepMode { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // stopHealMessage is container for stop heal success and failure messages. type stopHealMessage struct { Status string `json:"status"` Alias string `json:"alias"` } // String colorized stop heal message. func (s stopHealMessage) String() string { return console.Colorize("HealStopped", "Heal stopped successfully at `"+s.Alias+"`.") } // JSON jsonified stop heal message. func (s stopHealMessage) JSON() string { stopHealJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(stopHealJSONBytes) } type setIndex struct { pool, set int } type poolInfo struct { tolerance int endpoints []string } type setInfo struct { totalDisks int readyDisksCount int // disks online and not in healing state readyDisksUsedSpace uint64 // the total used space of ready disks incapableDisks int } type serverInfo struct { pool int disks []madmin.Disk } func (s serverInfo) onlineDisksForSet(index setIndex) (setFound bool, count int) { for _, disk := range s.disks { if disk.PoolIndex != index.pool || disk.SetIndex != index.set { continue } setFound = true if disk.State == "ok" && !disk.Healing { count++ } } return } // Get all drives from set statuses func getAllDisks(sets []madmin.SetStatus) []madmin.Disk { var disks []madmin.Disk for _, set := range sets { disks = append(disks, set.Disks...) } return disks } // Get all pools id from all drives func getPoolsIndexes(disks []madmin.Disk) []int { m := make(map[int]struct{}) for _, d := range disks { m[d.PoolIndex] = struct{}{} } var pools []int for pool := range m { pools = append(pools, pool) } sort.Ints(pools) return pools } // Generate sets info from disks func generateSetsStatus(disks []madmin.Disk) map[setIndex]setInfo { m := make(map[setIndex]setInfo) for _, d := range disks { idx := setIndex{pool: d.PoolIndex, set: d.SetIndex} setSt, ok := m[idx] if !ok { setSt = setInfo{} } setSt.totalDisks++ if d.State != "ok" || d.Healing { setSt.incapableDisks++ } else { setSt.readyDisksCount++ setSt.readyDisksUsedSpace += d.UsedSpace } m[idx] = setSt } return m } // Return a map of server endpoints and the corresponding status func generateServersStatus(disks []madmin.Disk) map[string]serverInfo { m := make(map[string]serverInfo) for _, d := range disks { u, e := url.Parse(d.Endpoint) if e != nil { continue } endpoint := u.Host if endpoint == "" { endpoint = "local-pool" + humanize.Ordinal(d.PoolIndex+1) } serverSt, ok := m[endpoint] if !ok { serverSt = serverInfo{ pool: d.PoolIndex, } } serverSt.disks = append(serverSt.disks, d) m[endpoint] = serverSt } return m } // Return the list of endpoints of a given pool index func computePoolEndpoints(pool int, serversStatus map[string]serverInfo) []string { var endpoints []string for endpoint, server := range serversStatus { if server.pool != pool { continue } endpoints = append(endpoints, endpoint) } return endpoints } // Compute the tolerance of each node in a given pool func computePoolTolerance(pool, parity int, setsStatus map[setIndex]setInfo, serversStatus map[string]serverInfo) int { var ( onlineDisksPerSet = make(map[setIndex]int) tolerancePerSet = make(map[setIndex]int) ) for set, setStatus := range setsStatus { if set.pool != pool { continue } onlineDisksPerSet[set] = setStatus.totalDisks - setStatus.incapableDisks tolerancePerSet[set] = 0 for _, server := range serversStatus { if server.pool != pool { continue } canShutdown := true setFound, count := server.onlineDisksForSet(set) if !setFound { continue } minDisks := setStatus.totalDisks - parity if onlineDisksPerSet[set]-count < minDisks { canShutdown = false } if canShutdown { tolerancePerSet[set]++ onlineDisksPerSet[set] -= count } else { break } } } minServerTolerance := len(serversStatus) for _, tolerance := range tolerancePerSet { if tolerance < minServerTolerance { minServerTolerance = tolerance } } return minServerTolerance } // Extract offline nodes from offline full path endpoints func getOfflineNodes(endpoints []string) map[string]struct{} { offlineNodes := make(map[string]struct{}) for _, endpoint := range endpoints { offlineNodes[endpoint] = struct{}{} } return offlineNodes } // verboseBackgroundHealStatusMessage is container for stop heal success and failure messages. type verboseBackgroundHealStatusMessage struct { Status string `json:"status"` HealInfo madmin.BgHealState allDrives bool // Specify storage class to show servers/disks tolerance ToleranceForSC string `json:"-"` } // String colorized to show background heal status message. func (s verboseBackgroundHealStatusMessage) String() string { var msg strings.Builder parity, showTolerance := s.HealInfo.SCParity[s.ToleranceForSC] offlineEndpoints := getOfflineNodes(s.HealInfo.OfflineEndpoints) allDisks := getAllDisks(s.HealInfo.Sets) pools := getPoolsIndexes(allDisks) setsStatus := generateSetsStatus(allDisks) serversStatus := generateServersStatus(allDisks) poolsInfo := make(map[int]poolInfo) for _, pool := range pools { tolerance := computePoolTolerance(pool, parity, setsStatus, serversStatus) endpoints := computePoolEndpoints(pool, serversStatus) poolsInfo[pool] = poolInfo{tolerance: tolerance, endpoints: endpoints} } distributed := len(serversStatus) > 1 plural := "" if distributed { plural = "s" } fmt.Fprintf(&msg, "Server%s status:\n", plural) fmt.Fprintf(&msg, "==============\n") for _, pool := range pools { fmt.Fprintf(&msg, "Pool %s:\n", humanize.Ordinal(pool+1)) // Sort servers in this pool by name orderedEndpoints := make([]string, len(poolsInfo[pool].endpoints)) copy(orderedEndpoints, poolsInfo[pool].endpoints) sort.Strings(orderedEndpoints) for _, endpoint := range orderedEndpoints { // Print offline status if node is offline _, ok := offlineEndpoints[endpoint] if ok { stateText := console.Colorize("NodeFailed", "OFFLINE") fmt.Fprintf(&msg, " %s: %s\n", endpoint, stateText) continue } var serverHeader strings.Builder var serverHeaderPrinted bool serverStatus := serversStatus[endpoint] switch { case showTolerance: hdr := " %s: (Tolerance: %d server(s))\n" fmt.Fprintf(&serverHeader, hdr, endpoint, poolsInfo[serverStatus.pool].tolerance) default: hdr := " %s:\n" fmt.Fprintf(&serverHeader, hdr, endpoint) } for _, d := range serverStatus.disks { if d.PoolIndex != pool { continue } stateText := "" switch { case d.State == "ok" && d.Healing: stateText = console.Colorize("DiskHealing", "HEALING") case d.State == "ok": if !s.allDrives { continue } stateText = console.Colorize("DiskOK", "OK") default: stateText = console.Colorize("DiskFailed", d.State) } if !serverHeaderPrinted { serverHeaderPrinted = true fmt.Fprint(&msg, serverHeader.String()) } drivePath := d.DrivePath if drivePath == "" { if u, e := url.Parse(d.Endpoint); e == nil { drivePath = u.Path } } fmt.Fprintf(&msg, " + %s : %s\n", drivePath, stateText) thisSet := setsStatus[setIndex{d.PoolIndex, d.SetIndex}] if d.Healing && d.HealInfo != nil && !d.HealInfo.Finished { refUsedSpace := uint64(math.MaxUint64) if thisSet.readyDisksCount > 0 { // to avoid crashing refUsedSpace = thisSet.readyDisksUsedSpace / uint64(thisSet.readyDisksCount) } if refUsedSpace < d.UsedSpace { // normalize refUsedSpace = d.UsedSpace } fmt.Fprintf(&msg, " |__ Progress: %d%%\n", 100*d.UsedSpace/refUsedSpace) fmt.Fprintf(&msg, " |__ Started: %s\n", humanize.Time(d.HealInfo.Started)) if d.HealInfo.RetryAttempts > 0 { fmt.Fprintf(&msg, " |__ Retries: %d\n", d.HealInfo.RetryAttempts) } } fmt.Fprintf(&msg, " |__ Capacity: %s/%s\n", humanize.IBytes(d.UsedSpace), humanize.IBytes(d.TotalSpace)) if showTolerance { fmt.Fprintf(&msg, " |__ Tolerance: %d drive(s)\n", parity-thisSet.incapableDisks) } } if serverHeaderPrinted { fmt.Fprintf(&msg, "\n") } } } if showTolerance { fmt.Fprintf(&msg, "\n") fmt.Fprintf(&msg, "Server Failure Tolerance:\n") fmt.Fprintf(&msg, "========================\n") for i, pool := range poolsInfo { fmt.Fprintf(&msg, "Pool %s:\n", humanize.Ordinal(i+1)) fmt.Fprintf(&msg, " Tolerance : %d server(s)\n", pool.tolerance) fmt.Fprintf(&msg, " Nodes :") for _, endpoint := range pool.endpoints { fmt.Fprintf(&msg, " %s", endpoint) } fmt.Fprintf(&msg, "\n") } } summary := shortBackgroundHealStatusMessage{HealInfo: s.HealInfo} fmt.Fprint(&msg, "\n") fmt.Fprint(&msg, "Summary:\n") fmt.Fprint(&msg, "=======\n") fmt.Fprint(&msg, summary.String()) fmt.Fprint(&msg, "\n") return msg.String() } // JSON jsonified stop heal message. func (s verboseBackgroundHealStatusMessage) JSON() string { healJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(healJSONBytes) } // shortBackgroundHealStatusMessage is container for stop heal success and failure messages. type shortBackgroundHealStatusMessage struct { Status string `json:"status"` HealInfo madmin.BgHealState } // String colorized to show background heal status message. func (s shortBackgroundHealStatusMessage) String() string { healPrettyMsg := "" var ( itemsHealed uint64 bytesHealed uint64 itemsFailed uint64 bytesFailed uint64 itemsHealedPerSec float64 bytesHealedPerSec float64 startedAt time.Time setsExceedsStd int setsExceedsReduced int // The addition of Elapsed time of each parallel healing operation // this is needed to calculate the rate of healing accumulatedElapsedTime time.Duration ) var problematicDisks int leastPct := 100.0 for _, set := range s.HealInfo.Sets { setsStatus := generateSetsStatus(set.Disks) // Furthest along disk... var furthestHealingDisk *madmin.Disk missingInSet := 0 for _, disk := range set.Disks { // Ignore disk with non 'ok' status if disk.State != madmin.DriveStateOk { if disk.State != madmin.DriveStateUnformatted { missingInSet++ problematicDisks++ } continue } if disk.HealInfo != nil && !disk.HealInfo.Finished { missingInSet++ thisSet := setsStatus[setIndex{pool: disk.PoolIndex, set: disk.SetIndex}] refUsedSpace := uint64(math.MaxUint64) if thisSet.readyDisksCount > 0 { refUsedSpace = thisSet.readyDisksUsedSpace / uint64(thisSet.readyDisksCount) } if refUsedSpace < disk.UsedSpace { refUsedSpace = disk.UsedSpace } if refUsedSpace > 0 { if pct := float64(disk.UsedSpace) / float64(refUsedSpace); pct < leastPct { leastPct = pct } } else { // Unlikely to have max used space in an erasure set to be zero, but still set this to zero leastPct = 0 } disk := disk if furthestHealingDisk == nil { furthestHealingDisk = &disk continue } if disk.HealInfo.ItemsHealed+disk.HealInfo.ItemsFailed > furthestHealingDisk.HealInfo.ItemsHealed+furthestHealingDisk.HealInfo.ItemsFailed { furthestHealingDisk = &disk continue } } } if furthestHealingDisk != nil { disk := furthestHealingDisk // Approximate values itemsHealed += disk.HealInfo.ItemsHealed bytesHealed += disk.HealInfo.BytesDone bytesFailed += disk.HealInfo.BytesFailed itemsFailed += disk.HealInfo.ItemsFailed if !disk.HealInfo.Started.IsZero() { if !disk.HealInfo.Started.Before(startedAt) { startedAt = disk.HealInfo.Started } if !disk.HealInfo.LastUpdate.IsZero() { accumulatedElapsedTime += disk.HealInfo.LastUpdate.Sub(disk.HealInfo.Started) } bytesHealedPerSec += float64(time.Second) * float64(disk.HealInfo.BytesDone) / float64(disk.HealInfo.LastUpdate.Sub(disk.HealInfo.Started)) itemsHealedPerSec += float64(time.Second) * float64(disk.HealInfo.ItemsHealed+disk.HealInfo.ItemsFailed) / float64(disk.HealInfo.LastUpdate.Sub(disk.HealInfo.Started)) } if n, ok := s.HealInfo.SCParity["STANDARD"]; ok && missingInSet > n { setsExceedsStd++ } if n, ok := s.HealInfo.SCParity["REDUCED_REDUNDANCY"]; ok && missingInSet > n { setsExceedsReduced++ } } } if startedAt.IsZero() && itemsHealed == 0 { healPrettyMsg += "No active healing is detected for new disks" if problematicDisks > 0 { healPrettyMsg += fmt.Sprintf(", though %d offline disk(s) found.", problematicDisks) } else { healPrettyMsg += "." } return healPrettyMsg } // Objects healed information healPrettyMsg += fmt.Sprintf("Objects Healed: %s, %s (%s)\n", humanize.Comma(int64(itemsHealed)), humanize.IBytes(bytesHealed), humanize.CommafWithDigits(leastPct*100, 1)+"%") healPrettyMsg += fmt.Sprintf("Objects Failed: %s\n", humanize.Comma(int64(itemsFailed))) if accumulatedElapsedTime > 0 { healPrettyMsg += fmt.Sprintf("Heal rate: %d obj/s, %s/s\n", int64(itemsHealedPerSec), humanize.IBytes(uint64(bytesHealedPerSec))) } if problematicDisks > 0 { healPrettyMsg += "\n" healPrettyMsg += fmt.Sprintf("%d offline disk(s) found.", problematicDisks) } if setsExceedsStd > 0 { healPrettyMsg += "\n" healPrettyMsg += fmt.Sprintf("%d of %d sets exceeds standard parity count EC:%d lost/offline disks", setsExceedsStd, len(s.HealInfo.Sets), s.HealInfo.SCParity["STANDARD"]) } if setsExceedsReduced > 0 { healPrettyMsg += "\n" healPrettyMsg += fmt.Sprintf("%d of %d sets exceeds reduced parity count EC:%d lost/offline disks", setsExceedsReduced, len(s.HealInfo.Sets), s.HealInfo.SCParity["REDUCED_REDUNDANCY"]) } return healPrettyMsg } // JSON jsonified stop heal message. func (s shortBackgroundHealStatusMessage) JSON() string { healJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(healJSONBytes) } func transformScanArg(scanArg string) madmin.HealScanMode { switch scanArg { case "deep": return madmin.HealDeepScan } return madmin.HealNormalScan } // mainAdminHeal - the entry function of heal command func mainAdminHeal(ctx *cli.Context) error { // Check for command syntax checkAdminHealSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) console.SetColor("Heal", color.New(color.FgGreen, color.Bold)) console.SetColor("Dot", color.New(color.FgGreen, color.Bold)) console.SetColor("HealBackgroundTitle", color.New(color.FgGreen, color.Bold)) console.SetColor("HealBackground", color.New(color.Bold)) console.SetColor("HealUpdateUI", color.New(color.FgYellow, color.Bold)) console.SetColor("HealStopped", color.New(color.FgGreen, color.Bold)) console.SetColor("DiskHealing", color.New(color.FgYellow, color.Bold)) console.SetColor("DiskOK", color.New(color.FgGreen, color.Bold)) console.SetColor("DiskFailed", color.New(color.FgRed, color.Bold)) console.SetColor("NodeFailed", color.New(color.FgRed, color.Bold)) // Create a new MinIO Admin Client adminClnt, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } // Compute bucket and object from the aliased URL aliasedURL = filepath.ToSlash(aliasedURL) splits := splitStr(aliasedURL, "/", 3) bucket, prefix := splits[1], splits[2] clnt, err := newClient(aliasedURL) if err != nil { fatalIf(err.Trace(clnt.GetURL().String()), "Unable to create client for URL ", aliasedURL) return nil } // Return the background heal status when the user // doesn't pass a bucket or --recursive flag. if bucket == "" && !ctx.Bool("recursive") { bgHealStatus, e := adminClnt.BackgroundHealStatus(globalContext) fatalIf(probe.NewError(e), "Unable to get background heal status.") if ctx.Bool("verbose") { printMsg(verboseBackgroundHealStatusMessage{ Status: "success", HealInfo: bgHealStatus, allDrives: ctx.Bool("all-drives"), ToleranceForSC: strings.ToUpper(ctx.String("storage-class")), }) } else { printMsg(shortBackgroundHealStatusMessage{ Status: "success", HealInfo: bgHealStatus, }) } return nil } opts := madmin.HealOpts{ ScanMode: transformScanArg(ctx.String("scan")), Remove: ctx.Bool("remove"), Recursive: ctx.Bool("recursive"), DryRun: ctx.Bool("dry-run"), Recreate: ctx.Bool("rewrite"), } if ctx.IsSet("pool") { p := ctx.Int("pool") if p < 1 { fatalIf(errInvalidArgument(), "--pool takes a non zero positive number.") } p-- opts.Pool = &p } if ctx.IsSet("set") { s := ctx.Int("set") if s < 1 { fatalIf(errInvalidArgument(), "--set takes a non zero positive number.") } s-- opts.Set = &s } forceStart := ctx.Bool("force-start") forceStop := ctx.Bool("force-stop") if forceStop { _, _, e := adminClnt.Heal(globalContext, bucket, prefix, opts, "", forceStart, forceStop) fatalIf(probe.NewError(e), "Unable to stop healing.") printMsg(stopHealMessage{Status: "success", Alias: aliasedURL}) return nil } if opts.Recursive && opts.Pool == nil && opts.Set == nil && isTerminal() && !ctx.Bool("force") { fmt.Printf("You are about to scan and heal the whole namespace in all pools and sets, please confirm [y/N]: ") answer, e := bufio.NewReader(os.Stdin).ReadString('\n') fatalIf(probe.NewError(e), "Unable to parse user input.") if answer = strings.TrimSpace(strings.ToLower(answer)); answer != "y" && answer != "yes" { fmt.Println("Heal aborted!") return nil } } healStart, _, e := adminClnt.Heal(globalContext, bucket, prefix, opts, "", forceStart, false) fatalIf(probe.NewError(e), "Unable to start healing.") ui := uiData{ Bucket: bucket, Prefix: prefix, Client: adminClnt, ClientToken: healStart.ClientToken, ForceStart: forceStart, HealOpts: &opts, ObjectsByOnlineDrives: make(map[int]int64), HealthCols: make(map[col]int64), CurChan: cursorAnimate(), } res, e := ui.DisplayAndFollowHealStatus(aliasedURL) if e != nil { if res.FailureDetail != "" { data, _ := json.MarshalIndent(res, "", " ") traceStr := string(data) fatalIf(probe.NewError(e).Trace(aliasedURL, traceStr), "Unable to display heal status.") } else { fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to display heal status.") } } return nil } minio-client-0.0~20250403/cmd/admin-idp.go000066400000000000000000000024731477450377600177440ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminIDPCmd = cli.Command{ Name: "idp", Usage: "manage MinIO IDentity Provider server configuration", Action: mainAdminIDP, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, Hidden: true, CustomHelpTemplate: `This command's functionality has moved and this command is DEPRECATED. Please use commands under 'mc idp ldap|openid' instead. `, } func mainAdminIDP(_ *cli.Context) error { deprecatedError("mc idp ldap|openid") return nil } minio-client-0.0~20250403/cmd/admin-info.go000066400000000000000000000265331477450377600201260ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "errors" "fmt" "sort" "strconv" "strings" "time" "github.com/dustin/go-humanize" "github.com/dustin/go-humanize/english" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/pkg/v3/console" ) var adminInfoFlags = []cli.Flag{ cli.BoolFlag{ Name: "offline", Usage: "show only offline nodes/drives", }, } var adminInfoCmd = cli.Command{ Name: "info", Usage: "display MinIO server information", Action: mainAdminInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, adminInfoFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get server information of the 'play' MinIO server. {{.Prompt}} {{.HelpName}} play/ `, } type poolSummary struct { index int setsCount int drivesPerSet int driveTolerance int drivesTotalFreeSpace uint64 drivesTotalUsableSpace uint64 endpoints set.StringSet } type clusterInfo map[int]*poolSummary func clusterSummaryInfo(info madmin.InfoMessage) clusterInfo { summary := make(clusterInfo) for _, srv := range info.Servers { for _, disk := range srv.Disks { if disk.PoolIndex < 0 { continue } pool := summary[disk.PoolIndex] if pool == nil { pool = &poolSummary{ index: disk.PoolIndex, endpoints: set.NewStringSet(), driveTolerance: info.StandardParity(), } } if len(info.Backend.DrivesPerSet) > 0 { if disk.DiskIndex < (info.Backend.DrivesPerSet[disk.PoolIndex] - info.Backend.StandardSCParity) { pool.drivesTotalFreeSpace += disk.AvailableSpace pool.drivesTotalUsableSpace += disk.TotalSpace } } pool.endpoints.Add(srv.Endpoint) summary[disk.PoolIndex] = pool } } for idx := range info.Backend.TotalSets { pool := summary[idx] if pool != nil { pool.setsCount = info.Backend.TotalSets[idx] pool.drivesPerSet = info.Backend.DrivesPerSet[idx] summary[idx] = pool } } return summary } func endpointToPools(endpoint string, c clusterInfo) (pools []int) { for poolNumber, poolSummary := range c { if poolSummary.endpoints.Contains(endpoint) { pools = append(pools, poolNumber) } } sort.Ints(pools) return } // Wrap "Info" message together with fields "Status" and "Error" type clusterStruct struct { Status string `json:"status"` Error string `json:"error,omitempty"` Info madmin.InfoMessage `json:"info,omitempty"` onlyOffline bool } // String provides colorized info messages func (u clusterStruct) String() (msg string) { // Check cluster level "Status" field for error if u.Status == "error" { fatal(probe.NewError(errors.New(u.Error)), "Unable to get service info") } // If nothing has been collected, error out if u.Info.Servers == nil { fatal(probe.NewError(errors.New("Unable to get service info")), "") } // Initialization var totalOfflineNodes int // Color palette initialization console.SetColor("Info", color.New(color.FgGreen, color.Bold)) console.SetColor("InfoFail", color.New(color.FgRed, color.Bold)) console.SetColor("InfoWarning", color.New(color.FgYellow, color.Bold)) backendType := u.Info.BackendType() coloredDot := console.Colorize("Info", dot) if madmin.ItemState(u.Info.Mode) == madmin.ItemInitializing { coloredDot = console.Colorize("InfoWarning", dot) } sort.Slice(u.Info.Servers, func(i, j int) bool { return u.Info.Servers[i].Endpoint < u.Info.Servers[j].Endpoint }) clusterSummary := clusterSummaryInfo(u.Info) // Loop through each server and put together info for each one for _, srv := range u.Info.Servers { // Check if MinIO server is not online ("Mode" field), if srv.State != string(madmin.ItemOnline) { totalOfflineNodes++ // "PrintB" is color blue in console library package msg += fmt.Sprintf("%s %s\n", console.Colorize("InfoFail", dot), console.Colorize("PrintB", srv.Endpoint)) msg += fmt.Sprintf(" Uptime: %s\n", console.Colorize("InfoFail", srv.State)) if backendType == madmin.Erasure { // Info about drives on a server, only available for non-FS types var OffDrives int var OnDrives int var dispNoOfDrives string for _, disk := range srv.Disks { switch disk.State { case madmin.DriveStateOk, madmin.DriveStateUnformatted: OnDrives++ default: OffDrives++ } } totalDrivesPerServer := OnDrives + OffDrives dispNoOfDrives = strconv.Itoa(OnDrives) + "/" + strconv.Itoa(totalDrivesPerServer) msg += fmt.Sprintf(" Drives: %s %s\n", dispNoOfDrives, console.Colorize("InfoFail", "OK ")) } msg += "\n" // Continue to the next server continue } if u.onlyOffline { continue } // Print server title msg += fmt.Sprintf("%s %s\n", coloredDot, console.Colorize("PrintB", srv.Endpoint)) // Uptime msg += fmt.Sprintf(" Uptime: %s\n", console.Colorize("Info", humanize.RelTime(time.Now(), time.Now().Add(time.Duration(srv.Uptime)*time.Second), "", ""))) // Version version := srv.Version if strings.Contains(srv.Version, "DEVELOPMENT") { version = "" } msg += fmt.Sprintf(" Version: %s\n", version) // Network info, only available for non-FS types connectionAlive := 0 totalNodes := len(srv.Network) if srv.Network != nil && backendType == madmin.Erasure { for _, v := range srv.Network { if v == "online" { connectionAlive++ } } clr := "Info" if connectionAlive != totalNodes { clr = "InfoWarning" } displayNwInfo := strconv.Itoa(connectionAlive) + "/" + strconv.Itoa(totalNodes) msg += fmt.Sprintf(" Network: %s %s\n", displayNwInfo, console.Colorize(clr, "OK ")) } if backendType == madmin.Erasure { // Info about drives on a server, only available for non-FS types var OffDrives int var OnDrives int var dispNoOfDrives string for _, disk := range srv.Disks { switch disk.State { case madmin.DriveStateOk, madmin.DriveStateUnformatted: OnDrives++ default: OffDrives++ } } totalDrivesPerServer := OnDrives + OffDrives clr := "Info" if OnDrives != totalDrivesPerServer { clr = "InfoWarning" } dispNoOfDrives = strconv.Itoa(OnDrives) + "/" + strconv.Itoa(totalDrivesPerServer) msg += fmt.Sprintf(" Drives: %s %s\n", dispNoOfDrives, console.Colorize(clr, "OK ")) // Print pools belonging to this server var prettyPools []string for _, pool := range endpointToPools(srv.Endpoint, clusterSummary) { prettyPools = append(prettyPools, strconv.Itoa(pool+1)) } msg += fmt.Sprintf(" Pool: %s\n", console.Colorize("Info", fmt.Sprintf("%+v", strings.Join(prettyPools, ", ")))) } msg += "\n" } if backendType == madmin.Erasure { dspOrder := []col{colGreen} // Header for i := 0; i < len(clusterSummary); i++ { dspOrder = append(dspOrder, colGrey) } var printColors []*color.Color for _, c := range dspOrder { printColors = append(printColors, getPrintCol(c)) } tbl := console.NewTable(printColors, []bool{false, false, false, false}, 0) var builder strings.Builder cellText := make([][]string, 0, len(clusterSummary)+1) cellText = append(cellText, []string{ "Pool", "Drives Usage", "Erasure stripe size", "Erasure sets", }) var printSummary bool // Keep the pool order while printing the output for poolIdx := 0; poolIdx < len(clusterSummary); poolIdx++ { summary := clusterSummary[poolIdx] if summary == nil { break } totalSize := summary.drivesTotalUsableSpace usedCurrent := summary.drivesTotalUsableSpace - summary.drivesTotalFreeSpace var capacity string if totalSize > 0 { capacity = fmt.Sprintf("%.1f%% (total: %s)", 100*float64(usedCurrent)/float64(totalSize), humanize.IBytes(totalSize)) } if summary.drivesPerSet > 0 { printSummary = true } cellText = append(cellText, []string{ humanize.Ordinal(poolIdx + 1), capacity, strconv.Itoa(summary.drivesPerSet), strconv.Itoa(summary.setsCount), }) } if printSummary { e := tbl.PopulateTable(&builder, cellText) fatalIf(probe.NewError(e), "unable to populate the table") msg += builder.String() + "\n" } } // Summary on used space, total no of buckets and // total no of objects at the Cluster level usedTotal := humanize.IBytes(u.Info.Usage.Size) if u.Info.Buckets.Count > 0 { msg += fmt.Sprintf("%s Used, %s, %s", usedTotal, english.Plural(int(u.Info.Buckets.Count), "Bucket", ""), english.Plural(int(u.Info.Objects.Count), "Object", "")) if u.Info.Versions.Count > 0 { msg += ", " + english.Plural(int(u.Info.Versions.Count), "Version", "") } if u.Info.DeleteMarkers.Count > 0 { msg += ", " + english.Plural(int(u.Info.DeleteMarkers.Count), "Delete Marker", "") } msg += "\n" } if backendType == madmin.Erasure { if totalOfflineNodes != 0 { msg += fmt.Sprintf("%s offline, ", english.Plural(totalOfflineNodes, "node", "")) } // Summary on total no of online and total // number of offline drives at the Cluster level msg += fmt.Sprintf("%s online, %s offline, EC:%d\n", english.Plural(u.Info.Backend.OnlineDisks, "drive", ""), english.Plural(u.Info.Backend.OfflineDisks, "drive", ""), u.Info.Backend.StandardSCParity) } // Remove the last new line if any // since this is a String() function msg = strings.TrimSuffix(msg, "\n") return } // JSON jsonifies service status message. func (u clusterStruct) JSON() string { statusJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(statusJSONBytes) } // checkAdminInfoSyntax - validate arguments passed by a user func checkAdminInfoSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainAdminInfo(ctx *cli.Context) error { checkAdminInfoSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") clusterInfo := clusterStruct{ onlyOffline: ctx.Bool("offline"), } // Fetch info of all servers (cluster or single server) admInfo, e := client.ServerInfo(globalContext) if e != nil { clusterInfo.Status = "error" clusterInfo.Error = e.Error() } else { clusterInfo.Status = "success" clusterInfo.Error = "" } clusterInfo.Info = admInfo printMsg(clusterInfo) return nil } minio-client-0.0~20250403/cmd/admin-inspect.go000066400000000000000000000024451477450377600206340ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminInspectCmd = cli.Command{ Name: "inspect", Usage: "inspect files on MinIO server", Action: mainAdminInspect, OnUsageError: onUsageError, Before: setGlobalsFromContext, HideHelpCommand: true, Hidden: true, CustomHelpTemplate: "Please use 'mc support inspect'", } // mainAdminHeal - the entry function of heal command func mainAdminInspect(_ *cli.Context) error { deprecatedError("mc support inspect") return nil } minio-client-0.0~20250403/cmd/admin-kms-key-create.go000066400000000000000000000040341477450377600220040ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "os" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" "golang.org/x/term" ) var adminKMSCreateKeyCmd = cli.Command{ Name: "create", Usage: "creates a new master KMS key", Action: mainAdminKMSCreateKey, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [KEY_NAME] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Create a new master key named 'my-key' default master key. $ {{.HelpName}} play my-key `, } // adminKMSCreateKeyCmd is the handler for the "mc admin kms key create" command. func mainAdminKMSCreateKey(ctx *cli.Context) error { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } client, err := newAdminClient(ctx.Args().Get(0)) fatalIf(err, "Cannot get a configured admin connection.") keyID := ctx.Args().Get(1) e := client.CreateKey(globalContext, keyID) fatalIf(probe.NewError(e), "Failed to create master key") if term.IsTerminal(int(os.Stdout.Fd())) { console.Println(color.GreenString(fmt.Sprintf("Created master key `%s` successfully", keyID))) } return nil } minio-client-0.0~20250403/cmd/admin-kms-key-list.go000066400000000000000000000060141477450377600215140ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "os" "github.com/fatih/color" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminKMSKeyListCmd = cli.Command{ Name: "list", Usage: "request list of KMS master keys", Action: mainAdminKMSKeyList, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get list of master keys from a MinIO server/cluster. $ {{.HelpName}} play `, } // adminKMSKeyCmd is the handle for the "mc admin kms key" command. func mainAdminKMSKeyList(ctx *cli.Context) error { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } console.SetColor("KeyName", color.New(color.FgBlue)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") keys, e := client.ListKeys(globalContext, "*") fatalIf(probe.NewError(e).Trace(args...), "Unable to list KMS keys") var rows []table.Row kmsKeys := []string{} for idx, k := range keys { rows = append(rows, table.Row{idx + 1, k.Name}) kmsKeys = append(kmsKeys, k.Name) } if globalJSON { printMsg(kmsKeysMsg{ Status: "success", Target: aliasedURL, Keys: kmsKeys, }) return nil } t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.SetColumnConfigs([]table.ColumnConfig{{Align: text.AlignCenter}}) t.SetTitle("KMS Keys") t.AppendHeader(table.Row{"S N", "Name"}) t.AppendRows(rows) t.SetStyle(table.StyleLight) t.Render() return nil } type kmsKeysMsg struct { Status string `json:"status"` Target string `json:"target"` Keys []string `json:"keys"` } func (k kmsKeysMsg) JSON() string { kmsBytes, e := json.MarshalIndent(k, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(kmsBytes) } func (k kmsKeysMsg) String() string { return fmt.Sprintf("Keys: %s\n", k.Keys) } minio-client-0.0~20250403/cmd/admin-kms-key-status.go000066400000000000000000000070331477450377600220660ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminKMSKeyStatusCmd = cli.Command{ Name: "status", Usage: "request status information for a KMS master key", Action: mainAdminKMSKeyStatus, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [KEY_NAME] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get default master key and its status from a MinIO server/cluster. $ {{.HelpName}} play 2. Get the status of one particular master key from a MinIO server/cluster. $ {{.HelpName}} play my-master-key `, } // adminKMSKeyCmd is the handle for the "mc admin kms key" command. func mainAdminKMSKeyStatus(ctx *cli.Context) error { if len(ctx.Args()) == 0 || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } console.SetColor("StatusSuccess", color.New(color.FgGreen, color.Bold)) console.SetColor("StatusError", color.New(color.FgRed, color.Bold)) console.SetColor("StatusUnknown", color.New(color.FgYellow, color.Bold)) client, err := newAdminClient(ctx.Args().Get(0)) fatalIf(err, "Unable to get a configured admin connection.") var keyID string if len(ctx.Args()) == 2 { keyID = ctx.Args().Get(1) } status, e := client.GetKeyStatus(globalContext, keyID) fatalIf(probe.NewError(e), "Failed to get status information") printMsg(kmsKeyStatusMsg{ KeyID: status.KeyID, EncryptionErr: status.EncryptionErr, DecryptionErr: status.DecryptionErr, }) return nil } type kmsKeyStatusMsg struct { KeyID string `json:"keyId"` EncryptionErr string `json:"encryptionError,omitempty"` DecryptionErr string `json:"decryptionError,omitempty"` Status string `json:"status"` } func (s kmsKeyStatusMsg) JSON() string { s.Status = "success" kmsBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(kmsBytes) } func (s kmsKeyStatusMsg) String() string { msg := fmt.Sprintf("Key: %s\n", s.KeyID) success := console.Colorize("StatusSuccess", "✔") failure := console.Colorize("StatusError", "✗") dunno := console.Colorize("StatusUnknown", "?") formatStatus := func(name string, unknown bool, err string) string { st := "" switch { case !unknown && err == "": st = success case unknown: st = dunno case err != "": st = fmt.Sprintf("%s (%s)", failure, err) } return fmt.Sprintf(" - %s %s\n", name, st) } msg += formatStatus("Encryption", false, s.EncryptionErr) msg += formatStatus("Decryption", s.EncryptionErr != "", s.DecryptionErr) return msg } minio-client-0.0~20250403/cmd/admin-kms-key.go000066400000000000000000000026001477450377600205400ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminKMSKeySubcommands = []cli.Command{ adminKMSCreateKeyCmd, adminKMSKeyStatusCmd, adminKMSKeyListCmd, } var adminKMSKeyCmd = cli.Command{ Name: "key", Usage: "manage KMS master keys: Request key status information", Action: mainAdminKMSKey, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminKMSKeySubcommands, HideHelpCommand: true, } // mainAdminKMSKey is the handle for the "mc admin kms key" command. func mainAdminKMSKey(ctx *cli.Context) error { commandNotFound(ctx, adminKMSKeySubcommands) return nil } minio-client-0.0~20250403/cmd/admin-kms.go000066400000000000000000000024401477450377600177540ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminKMSSubcommands = []cli.Command{ adminKMSKeyCmd, } var adminKMSCmd = cli.Command{ Name: "kms", Usage: "perform KMS management operations", Action: mainAdminKMS, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminKMSSubcommands, HideHelpCommand: true, } // mainAdminKMS is the handle for the "mc admin kms" command. func mainAdminKMS(ctx *cli.Context) error { commandNotFound(ctx, adminKMSSubcommands) return nil } minio-client-0.0~20250403/cmd/admin-logs.go000066400000000000000000000144161477450377600201340ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) const logTimeFormat string = "15:04:05 MST 01/02/2006" var logsShowFlags = []cli.Flag{ cli.IntFlag{ Name: "last, l", Usage: "show last n log entries", Value: 10, }, cli.StringFlag{ Name: "type, t", Usage: "list error logs by type. Valid options are '[minio, application, all]'", Value: "all", }, } var adminLogsCmd = cli.Command{ Name: "logs", Usage: "show MinIO logs", OnUsageError: onUsageError, Action: mainAdminLogs, Before: setGlobalsFromContext, Flags: append(logsShowFlags, globalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [NODENAME] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Show logs for a MinIO server with alias 'myminio' {{.Prompt}} {{.HelpName}} myminio 2. Show last 5 log entries for node 'node1' for a MinIO server with alias 'myminio' {{.Prompt}} {{.HelpName}} --last 5 myminio node1 3. Show application errors in logs for a MinIO server with alias 'myminio' {{.Prompt}} {{.HelpName}} --type application myminio `, } func checkLogsShowSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 3 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // Extend madmin.LogInfo to add String() and JSON() methods type logMessage struct { Status string `json:"status"` madmin.LogInfo } // JSON - jsonify loginfo func (l logMessage) JSON() string { l.Status = "success" logJSON, e := json.MarshalIndent(&l, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(logJSON) } func getLogTime(lt string) string { tm, e := time.Parse(time.RFC3339Nano, lt) if e != nil { return lt } return tm.Format(logTimeFormat) } // String - return colorized loginfo as string. func (l logMessage) String() string { var hostStr string b := &strings.Builder{} if l.NodeName != "" { hostStr = fmt.Sprintf("%s ", colorizedNodeName(l.NodeName)) } log := l.LogInfo if log.ConsoleMsg != "" { if strings.HasPrefix(log.ConsoleMsg, "\n") { fmt.Fprintf(b, "%s\n", hostStr) log.ConsoleMsg = strings.TrimPrefix(log.ConsoleMsg, "\n") } fmt.Fprintf(b, "%s %s", hostStr, log.ConsoleMsg) return b.String() } if l.API != nil { apiString := "API: " + l.API.Name + "(" if l.API.Args != nil && l.API.Args.Bucket != "" { apiString = apiString + "bucket=" + l.API.Args.Bucket } if l.API.Args != nil && l.API.Args.Object != "" { apiString = apiString + ", object=" + l.API.Args.Object } apiString += ")" fmt.Fprintf(b, "\n%s %s", hostStr, console.Colorize("API", apiString)) } if l.Time != "" { fmt.Fprintf(b, "\n%s Time: %s", hostStr, getLogTime(l.Time)) } if l.DeploymentID != "" { fmt.Fprintf(b, "\n%s DeploymentID: %s", hostStr, l.DeploymentID) } if l.RequestID != "" { fmt.Fprintf(b, "\n%s RequestID: %s", hostStr, l.RequestID) } if l.RemoteHost != "" { fmt.Fprintf(b, "\n%s RemoteHost: %s", hostStr, l.RemoteHost) } if l.UserAgent != "" { fmt.Fprintf(b, "\n%s UserAgent: %s", hostStr, l.UserAgent) } if l.Trace != nil { if l.Trace.Message != "" { fmt.Fprintf(b, "\n%s Error: %s", hostStr, console.Colorize("LogMessage", l.Trace.Message)) } if l.Trace.Variables != nil { for key, value := range l.Trace.Variables { if value != "" { fmt.Fprintf(b, "\n%s %s=%s", hostStr, key, value) } } } if l.Trace.Source != nil { traceLength := len(l.Trace.Source) for i, element := range l.Trace.Source { fmt.Fprintf(b, "\n%s %8v: %s", hostStr, traceLength-i, element) } } } logMsg := strings.TrimPrefix(b.String(), "\n") return fmt.Sprintf("%s\n", logMsg) } // mainAdminLogs - the entry function of admin logs func mainAdminLogs(ctx *cli.Context) error { // Check for command syntax checkLogsShowSyntax(ctx) console.SetColor("LogMessage", color.New(color.Bold, color.FgRed)) console.SetColor("Api", color.New(color.Bold, color.FgWhite)) for _, c := range colors { console.SetColor(fmt.Sprintf("Node%d", c), color.New(c)) } aliasedURL := ctx.Args().Get(0) var node string if len(ctx.Args()) > 1 { node = ctx.Args().Get(1) } var last int if ctx.IsSet("last") { last = ctx.Int("last") if last <= 0 { fatalIf(errInvalidArgument().Trace(ctx.Args()...), "please set a proper limit, for example: '--last 5' to display last 5 logs, omit this flag to display all available logs") } } logType := strings.ToLower(ctx.String("type")) if logType != "minio" && logType != "application" && logType != "all" { fatalIf(errInvalidArgument().Trace(ctx.Args()...), "Invalid value for --type flag. Valid options are [minio, application, all]") } // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } ctxt, cancel := context.WithCancel(globalContext) defer cancel() // Start listening on all console log activity. logCh := client.GetLogs(ctxt, node, last, logType) for logInfo := range logCh { if logInfo.Err != nil { fatalIf(probe.NewError(logInfo.Err), "Unable to listen to console logs") } // drop nodeName from output if specified as cli arg if node != "" { logInfo.NodeName = "" } if logInfo.DeploymentID != "" { printMsg(logMessage{LogInfo: logInfo}) } } return nil } minio-client-0.0~20250403/cmd/admin-main.go000066400000000000000000000041011477450377600201020ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminFlags = []cli.Flag{} const ( // dot represents a list item, for eg. server status - online (green) or offline (red) dot = "●" // check represents successful operation check = "✔" ) var adminCmdSubcommands = []cli.Command{ adminServiceCmd, adminServerUpdateCmd, adminInfoCmd, adminInspectCmd, adminUserCmd, adminGroupCmd, adminPolicyCmd, adminReplicateCmd, adminIDPCmd, adminConfigCmd, adminDecommissionCmd, adminHealCmd, adminPrometheusCmd, adminKMSCmd, adminHealthCmd(), adminSubnetCmd, adminBucketCmd, adminTierCmd, adminSpeedtestCmd, adminProfileCmd, adminScannerCmd, adminTopCmd, adminTraceCmd, adminConsoleCmd, adminClusterCmd, adminRebalanceCmd, adminLogsCmd, adminAccesskeyCmd, } var adminCmd = cli.Command{ Name: "admin", Usage: "manage MinIO servers", Action: mainAdmin, Subcommands: adminCmdSubcommands, HideHelpCommand: true, Before: setGlobalsFromContext, Flags: append(adminFlags, globalFlags...), } const dateTimeFormatFilename = "2006-01-02T15-04-05.999999-07-00" // mainAdmin is the handle for "mc admin" command. func mainAdmin(ctx *cli.Context) error { commandNotFound(ctx, adminCmdSubcommands) return nil // Sub-commands like "service", "heal", "top" have their own main. } minio-client-0.0~20250403/cmd/admin-policy-add.go000066400000000000000000000024171477450377600212130ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminPolicyAddCmd = cli.Command{ Name: "add", Usage: "add an IAM policy", Action: mainAdminPolicyAdd, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, Hidden: true, CustomHelpTemplate: `Please use 'mc admin policy create'`, } func mainAdminPolicyAdd(_ *cli.Context) error { deprecatedError("mc admin policy create") return nil } minio-client-0.0~20250403/cmd/admin-policy-attach.go000066400000000000000000000072651477450377600217350ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) const ( errCodeChangeAlreadyApplied = "XMinioAdminPolicyChangeAlreadyApplied" ) var adminAttachPolicyFlags = []cli.Flag{ cli.StringFlag{ Name: "user, u", Usage: "attach policy to user", }, cli.StringFlag{ Name: "group, g", Usage: "attach policy to group", }, } var adminPolicyAttachCmd = cli.Command{ Name: "attach", Usage: "attach an IAM policy to a user or group", Action: mainAdminPolicyAttach, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminAttachPolicyFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET POLICY [POLICY...] [--user USER | --group GROUP] Exactly one of --user or --group is required. POLICY: Name of the policy on the MinIO server. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Attach the "readonly" policy to user "james". {{.Prompt}} {{.HelpName}} myminio readonly --user james 2. Attach the "audit-policy" and "acct-policy" policies to group "legal". {{.Prompt}} {{.HelpName}} myminio audit-policy acct-policy --group legal `, } // mainAdminPolicyAttach is the handler for "mc admin policy attach" command. func mainAdminPolicyAttach(ctx *cli.Context) error { return userAttachOrDetachPolicy(ctx, true) } func userAttachOrDetachPolicy(ctx *cli.Context, attach bool) error { if len(ctx.Args()) < 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } user := ctx.String("user") group := ctx.String("group") // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) policies := args[1:] req := madmin.PolicyAssociationReq{ User: user, Group: group, Policies: policies, } // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") var e error var res madmin.PolicyAssociationResp if attach { res, e = client.AttachPolicy(globalContext, req) } else { res, e = client.DetachPolicy(globalContext, req) } if e != nil && madmin.ToErrorResponse(e).Code != errCodeChangeAlreadyApplied { fatalIf(probe.NewError(e), "Unable to make user/group policy association") } var emptyResp madmin.PolicyAssociationResp if res.UpdatedAt.Equal(emptyResp.UpdatedAt) { // Older minio does not send a result, so we populate res manually to // simulate a result. TODO(aditya): remove this after newer minio is // released in a few months (Older API Deprecated in Jun 2023) if attach { res.PoliciesAttached = policies } else { res.PoliciesDetached = policies } } m := policyAssociationMessage{ attach: attach, Status: "success", PoliciesAttached: res.PoliciesAttached, PoliciesDetached: res.PoliciesDetached, User: user, Group: group, } printMsg(m) return nil } minio-client-0.0~20250403/cmd/admin-policy-create.go000066400000000000000000000102421477450377600217210ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "os" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminPolicyCreateCmd = cli.Command{ Name: "create", Usage: "create a new IAM policy", Action: mainAdminPolicyCreate, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET POLICYNAME POLICYFILE POLICYNAME: Name of the canned policy on MinIO server. POLICYFILE: Name of the policy file associated with the policy name. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Create a new canned policy 'writeonly'. {{.Prompt}} {{.HelpName}} myminio writeonly /tmp/writeonly.json `, } // checkAdminPolicyCreateSyntax - validate all the passed arguments func checkAdminPolicyCreateSyntax(ctx *cli.Context) { if len(ctx.Args()) != 3 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // userPolicyMessage container for content message structure type userPolicyMessage struct { op string Status string `json:"status"` Policy string `json:"policy,omitempty"` PolicyInfo madmin.PolicyInfo `json:"policyInfo,omitempty"` UserOrGroup string `json:"userOrGroup,omitempty"` IsGroup bool `json:"isGroup"` } func (u userPolicyMessage) accountType() string { switch u.op { case "attach", "detach": if u.IsGroup { return "group" } return "user" } return "" } func (u userPolicyMessage) String() string { switch u.op { case "info": buf, e := json.MarshalIndent(u.PolicyInfo, "", " ") fatalIf(probe.NewError(e), "Unable to marshal to JSON.") return string(buf) case "list": return console.Colorize("PolicyName", u.Policy) case "remove": return console.Colorize("PolicyMessage", "Removed policy `"+u.Policy+"` successfully.") case "create": return console.Colorize("PolicyMessage", "Created policy `"+u.Policy+"` successfully.") case "detach": return console.Colorize("PolicyMessage", fmt.Sprintf("Policy `%s` successfully detached from %s `%s`", u.Policy, u.accountType(), u.UserOrGroup)) case "attach": return console.Colorize("PolicyMessage", fmt.Sprintf("Policy `%s` successfully attached to %s `%s`", u.Policy, u.accountType(), u.UserOrGroup)) } return "" } func (u userPolicyMessage) JSON() string { u.Status = "success" jsonMessageBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // mainAdminPolicyCreate is the handle for "mc admin policy create" command. func mainAdminPolicyCreate(ctx *cli.Context) error { checkAdminPolicyCreateSyntax(ctx) console.SetColor("PolicyMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) policy, e := os.ReadFile(args.Get(2)) fatalIf(probe.NewError(e).Trace(args...), "Unable to get policy") // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") fatalIf(probe.NewError(client.AddCannedPolicy(globalContext, args.Get(1), policy)).Trace(args...), "Unable to create new policy") printMsg(userPolicyMessage{ op: ctx.Command.Name, Policy: args.Get(1), }) return nil } minio-client-0.0~20250403/cmd/admin-policy-detach.go000066400000000000000000000040171477450377600217110ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminDetachPolicyFlags = []cli.Flag{ cli.StringFlag{ Name: "user, u", Usage: "detach policy from user", }, cli.StringFlag{ Name: "group, g", Usage: "detach policy from group", }, } var adminPolicyDetachCmd = cli.Command{ Name: "detach", Usage: "detach an IAM policy from a user or group", Action: mainAdminPolicyDetach, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminDetachPolicyFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET POLICY [POLICY...] [--user USER | --group GROUP] Exactly one of --user or --group is required. POLICY: Name of the policy on the MinIO server. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Detach the "readonly" policy from user "james". {{.Prompt}} {{.HelpName}} myminio readonly --user james 2. Detach the "audit-policy" and "acct-policy" policies from group "legal". {{.Prompt}} {{.HelpName}} myminio audit-policy acct-policy --group legal `, } // mainAdmihPolicyDetach is the handler for "mc admin policy detach" command. func mainAdminPolicyDetach(ctx *cli.Context) error { return userAttachOrDetachPolicy(ctx, false) } minio-client-0.0~20250403/cmd/admin-policy-entities.go000066400000000000000000000061441477450377600223100ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var adminPolicyEntitiesFlags = []cli.Flag{ cli.StringSliceFlag{ Name: "user, u", Usage: "list policies associated with user(s)", }, cli.StringSliceFlag{ Name: "group, g", Usage: "list policies associated with group(s)", }, cli.StringSliceFlag{ Name: "policy, p", Usage: "list users or groups associated with policy", }, } var adminPolicyEntitiesCmd = cli.Command{ Name: "entities", Usage: "list policy association entities", Action: mainAdminPolicyEntities, Before: setGlobalsFromContext, Flags: append(adminPolicyEntitiesFlags, globalFlags...), OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List all entities associated with all policies {{.Prompt}} {{.HelpName}} play/ 2. List all entities associated with the policies 'finteam-policy' and 'mlteam-policy' {{.Prompt}} {{.HelpName}} play/ --policy finteam-policy --policy mlteam-policy 3. List all policies associated with a pair of user entities {{.Prompt}} {{.HelpName}} play/ --user bob --user james 4. List all policies associated with a pair of group entities {{.Prompt}} {{.HelpName}} play/ --group auditors --group accounting 5. List all entities associated with a policy, group and user {{.Prompt}} {{.HelpName}} play/ \ --policy finteam-policy --user bobfisher --group consulting `, } // mainAdminPolicyEntities is the handler for "mc admin policy entities" command. func mainAdminPolicyEntities(ctx *cli.Context) error { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) } usersToQuery := ctx.StringSlice("user") groupsToQuery := ctx.StringSlice("group") policiesToQuery := ctx.StringSlice("policy") args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") res, e := client.GetPolicyEntities(globalContext, madmin.PolicyEntitiesQuery{ Users: usersToQuery, Groups: groupsToQuery, Policy: policiesToQuery, }) fatalIf(probe.NewError(e), "Unable to fetch policy entities") printMsg(policyEntitiesFrom(res)) return nil } minio-client-0.0~20250403/cmd/admin-policy-info.go000066400000000000000000000070211477450377600214120ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var policyInfoFlags = []cli.Flag{ cli.StringFlag{ Name: "policy-file, f", Usage: "additionally (over-)write policy JSON to given file", }, } var adminPolicyInfoCmd = cli.Command{ Name: "info", Usage: "show info on an IAM policy", Action: mainAdminPolicyInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(policyInfoFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET POLICYNAME [OPTIONS...] POLICYNAME: Name of the policy on the MinIO server. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Show information on a given policy. {{.Prompt}} {{.HelpName}} myminio writeonly 2. Show information on a given policy and write the policy JSON content to /tmp/policy.json. {{.Prompt}} {{.HelpName}} myminio writeonly --policy-file /tmp/policy.json `, } // checkAdminPolicyInfoSyntax - validate all the passed arguments func checkAdminPolicyInfoSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func getPolicyInfo(client *madmin.AdminClient, policyName string) (*madmin.PolicyInfo, error) { pinfo, e := client.InfoCannedPolicyV2(globalContext, policyName) if e != nil { return nil, e } if pinfo.PolicyName == "" { // Likely server only supports the older version. // nolint:staticcheck pinfo.Policy, e = client.InfoCannedPolicy(globalContext, policyName) if e != nil { return nil, e } pinfo.PolicyName = policyName } return pinfo, nil } // mainAdminPolicyInfo is the handler for "mc admin policy info" command. func mainAdminPolicyInfo(ctx *cli.Context) error { checkAdminPolicyInfoSyntax(ctx) console.SetColor("PolicyMessage", color.New(color.FgGreen)) console.SetColor("Policy", color.New(color.FgBlue)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) policyName := args.Get(1) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection") pinfo, e := getPolicyInfo(client, policyName) fatalIf(probe.NewError(e).Trace(args...), "Unable to fetch policy") policyFile := ctx.String("policy-file") if policyFile != "" { f, e := os.Create(policyFile) fatalIf(probe.NewError(e).Trace(args...), "Could not open given policy file") _, e = f.Write(pinfo.Policy) fatalIf(probe.NewError(e).Trace(args...), "Could not write to given policy file") } printMsg(userPolicyMessage{ op: ctx.Command.Name, Policy: policyName, PolicyInfo: *pinfo, }) return nil } minio-client-0.0~20250403/cmd/admin-policy-list.go000066400000000000000000000043611477450377600214360ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminPolicyListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list all IAM policies", Action: mainAdminPolicyList, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List all policies on MinIO server. {{.Prompt}} {{.HelpName}} myminio `, } // checkAdminPolicyListSyntax - validate all the passed arguments func checkAdminPolicyListSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminPolicyList is the handle for "mc admin policy add" command. func mainAdminPolicyList(ctx *cli.Context) error { checkAdminPolicyListSyntax(ctx) console.SetColor("PolicyName", color.New(color.FgBlue)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") policies, e := client.ListCannedPolicies(globalContext) fatalIf(probe.NewError(e).Trace(args...), "Unable to list policy") for k := range policies { printMsg(userPolicyMessage{ op: ctx.Command.Name, Policy: k, }) } return nil } minio-client-0.0~20250403/cmd/admin-policy-remove.go000066400000000000000000000045041477450377600217570ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminPolicyRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove an IAM policy", Action: mainAdminPolicyRemove, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET POLICYNAME POLICYNAME: Name of the canned policy on MinIO server. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove 'writeonly' policy on MinIO server. {{.Prompt}} {{.HelpName}} myminio writeonly `, } // checkAdminPolicyRemoveSyntax - validate all the passed arguments func checkAdminPolicyRemoveSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminPolicyRemove is the handle for "mc admin policy remove" command. func mainAdminPolicyRemove(ctx *cli.Context) error { checkAdminPolicyRemoveSyntax(ctx) console.SetColor("PolicyMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") fatalIf(probe.NewError(client.RemoveCannedPolicy(globalContext, args.Get(1))).Trace(args...), "Unable to remove policy") printMsg(userPolicyMessage{ op: ctx.Command.Name, Policy: args.Get(1), }) return nil } minio-client-0.0~20250403/cmd/admin-policy-set.go000066400000000000000000000024371477450377600212600ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminPolicySetCmd = cli.Command{ Name: "set", Usage: "set IAM policy on a user or group", Action: mainAdminPolicySet, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, Hidden: true, CustomHelpTemplate: `Please use 'mc admin policy attach'`, } func mainAdminPolicySet(_ *cli.Context) error { deprecatedError("mc admin policy attach") return nil } minio-client-0.0~20250403/cmd/admin-policy-unset.go000066400000000000000000000024631477450377600216220ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminPolicyUnsetCmd = cli.Command{ Name: "unset", Usage: "unset an IAM policy for a user or group", Action: mainAdminPolicyUnsetErr, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, Hidden: true, CustomHelpTemplate: `Please use 'mc admin policy detach'`, } func mainAdminPolicyUnsetErr(_ *cli.Context) error { deprecatedError("mc admin policy detach") return nil } minio-client-0.0~20250403/cmd/admin-policy-update.go000066400000000000000000000024701477450377600217440ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminPolicyUpdateCmd = cli.Command{ Name: "update", Usage: "attach a new IAM policy to user or group", Action: mainAdminPolicyUpdateErr, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, Hidden: true, CustomHelpTemplate: `Please use 'mc admin policy attach'`, } func mainAdminPolicyUpdateErr(_ *cli.Context) error { deprecatedError("mc admin policy attach") return nil } minio-client-0.0~20250403/cmd/admin-policy.go000066400000000000000000000031341477450377600204620ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminPolicySubcommands = []cli.Command{ adminPolicyCreateCmd, adminPolicyRemoveCmd, adminPolicyListCmd, adminPolicyInfoCmd, adminPolicyAttachCmd, adminPolicyDetachCmd, adminPolicyEntitiesCmd, adminPolicyAddCmd, adminPolicySetCmd, adminPolicyUnsetCmd, adminPolicyUpdateCmd, } var adminPolicyCmd = cli.Command{ Name: "policy", Usage: "manage policies defined in the MinIO server", Action: mainAdminPolicy, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminPolicySubcommands, HideHelpCommand: true, } // mainAdminPolicy is the handle for "mc admin policy" command. func mainAdminPolicy(ctx *cli.Context) error { commandNotFound(ctx, adminPolicySubcommands) return nil // Sub-commands like "get", "set" have their own main. } minio-client-0.0~20250403/cmd/admin-profile-start.go000066400000000000000000000025101477450377600217530ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminProfileStartCmd = cli.Command{ Name: "start", Usage: "start recording profile data", Action: mainAdminProfileStart, OnUsageError: onUsageError, Before: setGlobalsFromContext, HideHelpCommand: true, Hidden: true, CustomHelpTemplate: "Please use 'mc support profile start'", } // mainAdminProfileStart - the entry function of profile command func mainAdminProfileStart(_ *cli.Context) error { deprecatedError("mc support profile start") return nil } minio-client-0.0~20250403/cmd/admin-profile-stop.go000066400000000000000000000025521477450377600216110ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminProfileStopCmd = cli.Command{ Name: "stop", Usage: "stop and download profile data", Action: mainAdminProfileStop, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, Hidden: true, CustomHelpTemplate: "Please use 'mc support profile stop'", } // mainAdminProfileStop - the entry function of profile stop command func mainAdminProfileStop(_ *cli.Context) error { deprecatedError("mc support profile stop") return nil } minio-client-0.0~20250403/cmd/admin-profile.go000066400000000000000000000026321477450377600206250ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminProfileSubcommands = []cli.Command{ adminProfileStartCmd, adminProfileStopCmd, } var adminProfileCmd = cli.Command{ Name: "profile", Usage: "generate profile data for debugging purposes", Action: mainAdminProfile, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminProfileSubcommands, HideHelpCommand: true, Hidden: true, } // mainAdminProfile is the handle for "mc admin profile" command. func mainAdminProfile(_ *cli.Context) error { deprecatedError("mc support profile") return nil } minio-client-0.0~20250403/cmd/admin-prometheus-generate.go000066400000000000000000000176031477450377600231540ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "net/url" "time" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" json "github.com/minio/colorjson" yaml "gopkg.in/yaml.v2" ) const ( defaultJobName = "minio-job" metricsV2BasePath = "/minio/v2/metrics" ) var prometheusFlags = append(metricsFlags, cli.BoolFlag{ Name: "public", Usage: "disable bearer token generation for scrape_configs", }) var adminPrometheusGenerateCmd = cli.Command{ Name: "generate", Usage: "generates prometheus config", Action: mainAdminPrometheusGenerate, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(prometheusFlags, globalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [METRIC-TYPE] METRIC-TYPE: valid values are api-version v2 ['cluster', 'node', 'bucket', 'resource']. defaults to 'cluster' if not specified. api-version v3 ["api", "system", "debug", "cluster", "ilm", "audit", "logger", "replication", "notification", "scanner"]. defaults to all if not specified. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES (v3): 1. Generate a default prometheus config. {{.Prompt}} {{.HelpName}} play --api-version v3 2. Generate prometheus config for api metrics. {{.Prompt}} {{.HelpName}} play api --api-version v3 3. Generate prometheus config for api metrics of bucket 'mybucket'. {{.Prompt}} {{.HelpName}} play api --bucket mybucket --api-version v3 4. Generate prometheus config for system metrics. {{.Prompt}} {{.HelpName}} play system --api-version v3 5. Generate prometheus config for debug metrics. {{.Prompt}} {{.HelpName}} play debug --api-version v3 6. Generate prometheus config for cluster metrics. {{.Prompt}} {{.HelpName}} play cluster --api-version v3 7. Generate prometheus config for ilm metrics. {{.Prompt}} {{.HelpName}} play ilm --api-version v3 8. Generate prometheus config for audit metrics. {{.Prompt}} {{.HelpName}} play audit --api-version v3 9. Generate prometheus config for logger metrics. {{.Prompt}} {{.HelpName}} play logger --api-version v3 10. Generate prometheus config for replication metrics. {{.Prompt}} {{.HelpName}} play replication --api-version v3 11. Generate prometheus config for replication metrics of bucket 'mybucket'. {{.Prompt}} {{.HelpName}} play replication --bucket mybucket --api-version v3 12. Generate prometheus config for notification metrics. {{.Prompt}} {{.HelpName}} play notification --api-version v3 13. Generate prometheus config for scanner metrics. {{.Prompt}} {{.HelpName}} play scanner --api-version v3 EXAMPLES (v2): 1. Generate a default prometheus config. {{.Prompt}} {{.HelpName}} play 2. Generate prometheus config for node metrics. {{.Prompt}} {{.HelpName}} play node 3. Generate prometheus config for bucket metrics. {{.Prompt}} {{.HelpName}} play bucket 4. Generate prometheus config for resource metrics. {{.Prompt}} {{.HelpName}} play resource 5. Generate prometheus config for cluster metrics. {{.Prompt}} {{.HelpName}} play cluster `, } // PrometheusConfig - container to hold the top level scrape config. type PrometheusConfig struct { ScrapeConfigs []ScrapeConfig `yaml:"scrape_configs,omitempty"` } // String colorized prometheus config yaml. func (c PrometheusConfig) String() string { b, e := yaml.Marshal(c) fatalIf(probe.NewError(e), "Unable to generate Prometheus config") return console.Colorize("yaml", string(b)) } // JSON jsonified prometheus config. func (c PrometheusConfig) JSON() string { jsonMessageBytes, e := json.MarshalIndent(c.ScrapeConfigs[0], "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // StatConfig - container to hold the targets config. type StatConfig struct { Targets []string `yaml:",flow" json:"targets"` } // String colorized stat config yaml. func (t StatConfig) String() string { b, e := yaml.Marshal(t) fatalIf(probe.NewError(e), "Unable to generate Prometheus config") return console.Colorize("yaml", string(b)) } // JSON jsonified stat config. func (t StatConfig) JSON() string { jsonMessageBytes, e := json.MarshalIndent(t.Targets, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // ScrapeConfig configures a scraping unit for Prometheus. type ScrapeConfig struct { JobName string `yaml:"job_name" json:"jobName"` BearerToken string `yaml:"bearer_token,omitempty" json:"bearerToken,omitempty"` MetricsPath string `yaml:"metrics_path,omitempty" json:"metricsPath"` Scheme string `yaml:"scheme,omitempty" json:"scheme"` StaticConfigs []StatConfig `yaml:"static_configs,omitempty" json:"staticConfigs"` } const ( defaultPrometheusJWTExpiry = 100 * 365 * 24 * time.Hour ) // checkAdminPrometheusSyntax - validate all the passed arguments func checkAdminPrometheusSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func generatePrometheusConfig(ctx *cli.Context) error { // Get the alias parameter from cli args := ctx.Args() alias := cleanAlias(args.Get(0)) if !isValidAlias(alias) { fatalIf(errInvalidAlias(alias), "Invalid alias.") } hostConfig := mustGetHostConfig(alias) if hostConfig == nil { fatalIf(errInvalidAliasedURL(alias), "No such alias `"+alias+"` found.") return nil } u, e := url.Parse(hostConfig.URL) if e != nil { return e } metricsSubSystem := args.Get(1) apiVer := ctx.String("api-version") jobName := defaultJobName metricsPath := "" switch apiVer { case "v2": if metricsSubSystem == "" { metricsSubSystem = "cluster" } validateV2Args(ctx, metricsSubSystem) if metricsSubSystem != "cluster" { jobName = defaultJobName + "-" + metricsSubSystem } metricsPath = metricsV2BasePath + "/" + metricsSubSystem case "v3": bucket := ctx.String("bucket") validateV3Args(metricsSubSystem, bucket) metricsPath = getMetricsV3Path(metricsSubSystem, bucket) if metricsSubSystem != "" { jobName = defaultJobName + "-" + metricsSubSystem } default: fatalIf(errInvalidArgument().Trace(), "Invalid api version `"+apiVer+"`") } config := PrometheusConfig{ ScrapeConfigs: []ScrapeConfig{ { JobName: jobName, MetricsPath: metricsPath, StaticConfigs: []StatConfig{ { Targets: []string{""}, }, }, }, }, } if !ctx.Bool("public") { token, e := getPrometheusToken(hostConfig) if e != nil { return e } // Setting the values config.ScrapeConfigs[0].BearerToken = token } config.ScrapeConfigs[0].Scheme = u.Scheme config.ScrapeConfigs[0].StaticConfigs[0].Targets[0] = u.Host printMsg(config) return nil } // mainAdminPrometheus is the handle for "mc admin prometheus generate" sub-command. func mainAdminPrometheusGenerate(ctx *cli.Context) error { console.SetColor("yaml", color.New(color.FgGreen)) checkAdminPrometheusSyntax(ctx) if err := generatePrometheusConfig(ctx); err != nil { return nil } return nil } minio-client-0.0~20250403/cmd/admin-prometheus-metrics-v3.go000066400000000000000000000060011477450377600233440ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "errors" "fmt" "net/http" "net/url" "strings" "github.com/minio/cli" "github.com/minio/minio-go/v7/pkg/set" ) var ( metricsV3Flags = []cli.Flag{ cli.StringFlag{ Name: "bucket", Usage: "bucket name to list metrics for. only applicable with api version v3 for metric type 'api, replication'", }, } metricsV3SubSystems = set.CreateStringSet("api", "system", "debug", "cluster", "ilm", "audit", "logger", "replication", "notification", "scanner") bucketMetricsSubSystems = set.CreateStringSet("api", "replication") ) const metricsV3EndPointRoot = "/minio/metrics/v3" func getMetricsV3Path(subsys string, bucket string) string { params := url.Values{} metricsPath := metricsV3EndPointRoot if len(bucket) > 0 { metricsPath += "/bucket" } if len(subsys) > 0 { metricsPath += "/" + subsys } if len(bucket) > 0 { // bucket specific metrics endpoints have '/bucket' prefix and bucket name as suffix. // e.g. bucket api metrics: /bucket/api/mybucket metricsPath += "/" + bucket } qparams := params.Encode() if len(qparams) > 0 { metricsPath += "?" + qparams } return metricsPath } func validateV3Args(subsys string, bucket string) { if subsys != "" && !metricsV3SubSystems.Contains(subsys) { fatalIf(errInvalidArgument().Trace(), "invalid metric type `"+subsys+"`. valid values are `"+ strings.Join(metricsV3SubSystems.ToSlice(), ", ")+"`") } if len(bucket) > 0 { bms := strings.Join(bucketMetricsSubSystems.ToSlice(), ", ") if len(subsys) == 0 { fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("metric type must be passed with --bucket. valid values are `%s`", bms)) } if !bucketMetricsSubSystems.Contains(subsys) { fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("--bucket is applicable only for metric types `%s`", bms)) } } } func printPrometheusMetricsV3(ctx *cli.Context, req prometheusMetricsReq) error { bucket := ctx.String("bucket") validateV3Args(req.subsystem, bucket) metricsURL := req.aliasURL + getMetricsV3Path(req.subsystem, bucket) resp, e := fetchMetrics(metricsURL, req.token) if e != nil { return e } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { printMsg(prometheusMetricsReader{Reader: resp.Body}) return nil } return errors.New(resp.Status) } minio-client-0.0~20250403/cmd/admin-prometheus-metrics.go000066400000000000000000000153541477450377600230310ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "errors" "io" "net/http" "os" "strings" "time" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/set" ) var metricsFlags = append(metricsV3Flags, cli.StringFlag{ Name: "api-version", Usage: "version of metrics api to use. valid values are ['v2', 'v3']. defaults to 'v2' if not specified.", Value: "v2", }) var metricsV2SubSystems = set.CreateStringSet("node", "bucket", "cluster", "resource") var adminPrometheusMetricsCmd = cli.Command{ Name: "metrics", Usage: "print prometheus metrics", OnUsageError: onUsageError, Action: mainSupportMetrics, Before: setGlobalsFromContext, Flags: append(globalFlags, metricsFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [METRIC-TYPE] METRIC-TYPE: valid values are api-version v2 ['cluster', 'node', 'bucket', 'resource']. defaults to 'cluster' if not specified. api-version v3 ["api", "system", "debug", "cluster", "ilm", "audit", "logger", "replication", "notification", "scanner"]. defaults to all if not specified. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES (v3): 1. API metrics {{.Prompt}} {{.HelpName}} play api --api-version v3 2. API metrics for the bucket 'mybucket' {{.Prompt}} {{.HelpName}} play api --bucket mybucket --api-version v3 3. System metrics {{.Prompt}} {{.HelpName}} play system --api-version v3 4. Debug metrics {{.Prompt}} {{.HelpName}} play debug --api-version v3 5. Cluster metrics {{.Prompt}} {{.HelpName}} play cluster --api-version v3 6. ILM metrics {{.Prompt}} {{.HelpName}} play ilm --api-version v3 7. Audit metrics {{.Prompt}} {{.HelpName}} play audit --api-version v3 8. Logger metrics {{.Prompt}} {{.HelpName}} play logger --api-version v3 9. Replication metrics {{.Prompt}} {{.HelpName}} play replication --api-version v3 10. Replication metrics for the bucket 'mybucket' {{.Prompt}} {{.HelpName}} play replication --bucket mybucket --api-version v3 11. Notification metrics {{.Prompt}} {{.HelpName}} play notification --api-version v3 12. Scanner metrics {{.Prompt}} {{.HelpName}} play scanner --api-version v3 EXAMPLES (v2): 1. Metrics reported cluster wide. {{.Prompt}} {{.HelpName}} play 2. Metrics reported at node level. {{.Prompt}} {{.HelpName}} play node 3. Metrics reported at bucket level. {{.Prompt}} {{.HelpName}} play bucket 4. Resource metrics. {{.Prompt}} {{.HelpName}} play resource `, } const metricsEndPointRoot = "/minio/v2/metrics/" type prometheusMetricsReq struct { aliasURL string token string subsystem string } // checkSupportMetricsSyntax - validate arguments passed by a user func checkSupportMetricsSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func fetchMetrics(metricsURL string, token string) (*http.Response, error) { req, e := http.NewRequest(http.MethodGet, metricsURL, nil) if e != nil { return nil, e } if token != "" { req.Header.Add("Authorization", "Bearer "+token) } client := httpClient(60 * time.Second) return client.Do(req) } func validateV2Args(ctx *cli.Context, subsys string) { for _, flag := range metricsV3Flags { flagName := flag.GetName() if ctx.IsSet(flagName) { fatalIf(errInvalidArgument().Trace(), "Flag `"+flagName+"` is not supported with v2 metrics") } } if !metricsV2SubSystems.Contains(subsys) { fatalIf(errInvalidArgument().Trace(), "invalid metric type `"+subsys+"`. valid values are `"+ strings.Join(metricsV2SubSystems.ToSlice(), ", ")+"`") } } func printPrometheusMetricsV2(ctx *cli.Context, req prometheusMetricsReq) error { subsys := req.subsystem if subsys == "" { subsys = "cluster" } validateV2Args(ctx, subsys) resp, e := fetchMetrics(req.aliasURL+metricsEndPointRoot+subsys, req.token) if e != nil { return e } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { printMsg(prometheusMetricsReader{Reader: resp.Body}) return nil } return errors.New(resp.Status) } // JSON returns jsonified message func (pm prometheusMetricsReader) JSON() string { results, e := madmin.ParsePrometheusResults(pm.Reader) fatalIf(probe.NewError(e), "Unable to parse Prometheus metrics.") jsonMessageBytes, e := json.MarshalIndent(results, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // String - returns the string representation of the prometheus metrics func (pm prometheusMetricsReader) String() string { _, e := io.Copy(os.Stdout, pm.Reader) fatalIf(probe.NewError(e), "Unable to read Prometheus metrics.") return "" } // prometheusMetricsReader mirrors the MetricFamily proto message. type prometheusMetricsReader struct { Reader io.Reader } func mainSupportMetrics(ctx *cli.Context) error { checkSupportMetricsSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() alias := cleanAlias(args.Get(0)) if !isValidAlias(alias) { fatalIf(errInvalidAlias(alias), "Invalid alias.") } hostConfig := mustGetHostConfig(alias) if hostConfig == nil { fatalIf(errInvalidAliasedURL(alias), "No such alias `"+alias+"` found.") return nil } token, e := getPrometheusToken(hostConfig) if e != nil { return e } metricsSubSystem := args.Get(1) apiVer := ctx.String("api-version") metricsReq := prometheusMetricsReq{ aliasURL: hostConfig.URL, token: token, subsystem: metricsSubSystem, } switch apiVer { case "v2": err := printPrometheusMetricsV2(ctx, metricsReq) fatalIf(probe.NewError(err), "Unable to list prometheus metrics with api-version v2.") case "v3": err := printPrometheusMetricsV3(ctx, metricsReq) fatalIf(probe.NewError(err), "Unable to list prometheus metrics with api-version v3.") default: fatalIf(errInvalidArgument().Trace(), "Invalid api version `"+apiVer+"`") } return nil } minio-client-0.0~20250403/cmd/admin-prometheus.go000066400000000000000000000030071477450377600213550ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminPrometheusSubcommands = []cli.Command{ adminPrometheusGenerateCmd, adminPrometheusMetricsCmd, } var adminPrometheusCmd = cli.Command{ Name: "prometheus", Usage: "manages prometheus config", Action: mainAdminPrometheus, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, Subcommands: adminPrometheusSubcommands, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} `, } // mainAdminPrometheus is the handle for "mc admin prometheus" command. func mainAdminPrometheus(ctx *cli.Context) error { commandNotFound(ctx, adminPrometheusSubcommands) return nil } minio-client-0.0~20250403/cmd/admin-rebalance-main.go000066400000000000000000000024661477450377600220300ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminRebalanceSubcommands = []cli.Command{ adminRebalanceStartCmd, adminRebalanceStatusCmd, adminRebalanceStopCmd, } var adminRebalanceCmd = cli.Command{ Name: "rebalance", Usage: "Manage MinIO rebalance", Action: mainAdminRebalance, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminRebalanceSubcommands, HideHelpCommand: true, } func mainAdminRebalance(ctx *cli.Context) error { commandNotFound(ctx, adminRebalanceSubcommands) return nil } minio-client-0.0~20250403/cmd/admin-rebalance-start.go000066400000000000000000000046371477450377600222430ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminRebalanceStartCmd = cli.Command{ Name: "start", Usage: "start rebalance operation", Action: mainAdminRebalanceStart, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS FLAGS: {{range .VisibleFlags}}{{.}} {{end}} xEXAMPLES: 1. Start rebalance on a MinIO deployment with alias myminio {{.Prompt}} {{.HelpName}} myminio `, } type rebalanceStartMsg struct { Status string `json:"status"` Target string `json:"url"` ID string `json:"id"` } func (r rebalanceStartMsg) JSON() string { r.Status = "success" b, e := json.MarshalIndent(r, "", " ") fatalIf(probe.NewError(e), "Unable to marshal to JSON") return string(b) } func (r rebalanceStartMsg) String() string { return console.Colorize("rebalanceStartMsg", fmt.Sprintf("Rebalance started for %s", r.Target)) } func mainAdminRebalanceStart(ctx *cli.Context) error { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) } console.SetColor("rebalanceStartMsg", color.New(color.FgGreen)) args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client") id, e := client.RebalanceStart(globalContext) fatalIf(probe.NewError(e), "Unable to start rebalance") printMsg(rebalanceStartMsg{ Target: aliasedURL, ID: id, }) return nil } minio-client-0.0~20250403/cmd/admin-rebalance-status.go000066400000000000000000000071001477450377600224150ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "encoding/json" "fmt" "strings" "time" humanize "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminRebalanceStatusCmd = cli.Command{ Name: "status", Usage: "summarize an ongoing rebalance operation", Action: mainAdminRebalanceStatus, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Summarize ongoing rebalance on a MinIO deployment with alias myminio {{.Prompt}} {{.HelpName}} myminio `, } func mainAdminRebalanceStatus(ctx *cli.Context) error { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) } args := ctx.Args() aliasedURL := args.Get(0) client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client") return err.ToGoError() } rInfo, e := client.RebalanceStatus(globalContext) fatalIf(probe.NewError(e), "Unable to get rebalance status") if globalJSON { b, e := json.Marshal(rInfo) fatalIf(probe.NewError(e), "Unable to marshal json") console.Println(string(b)) return nil } console.Println("Per-pool usage:") // col-headers colHeaders := make([]string, len(rInfo.Pools)) for i := range rInfo.Pools { colHeaders[i] = fmt.Sprintf("Pool-%d", i) } var ( totalBytes, totalObjects, totalVersions uint64 maxElapsed, maxETA time.Duration ) row := make([]string, len(rInfo.Pools)) for idx, pool := range rInfo.Pools { statusStr := fmt.Sprintf("%.2f%%", pool.Used*100) if pool.Status == "Started" { statusStr += " *" // indicating rebalance is in progress in this pool } row[idx] = statusStr // For summary values totalBytes += pool.Progress.Bytes totalObjects += pool.Progress.NumObjects totalVersions += pool.Progress.NumVersions if maxElapsed == 0 || maxElapsed < pool.Progress.Elapsed { maxElapsed = pool.Progress.Elapsed } if maxETA == 0 || maxETA < pool.Progress.ETA { maxETA = pool.Progress.ETA } } dspOrder := []col{colGreen, colGrey} var printColors []*color.Color for _, c := range dspOrder { printColors = append(printColors, getPrintCol(c)) } alignRights := make([]bool, len(rInfo.Pools)) tbl := console.NewTable(printColors, alignRights, 0) e = tbl.DisplayTable([][]string{colHeaders, row}) fatalIf(probe.NewError(e), "Unable to render table view") var b strings.Builder fmt.Fprintf(&b, "Summary: \n") fmt.Fprintf(&b, "Data: %s (%d objects, %d versions) \n", humanize.IBytes(totalBytes), totalObjects, totalVersions) fmt.Fprintf(&b, "Time: %s (%s to completion)", maxElapsed, maxETA) console.Println(b.String()) return nil } minio-client-0.0~20250403/cmd/admin-rebalance-stop.go000066400000000000000000000045731477450377600220720ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminRebalanceStopCmd = cli.Command{ Name: "stop", Usage: "stop an ongoing rebalance operation", Action: mainAdminRebalanceStop, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Stop an ongoing rebalance on a MinIO deployment with alias myminio {{.Prompt}} {{.HelpName}} myminio `, } type rebalanceStopMsg struct { Status string `json:"status"` Target string `json:"url"` } func (r rebalanceStopMsg) JSON() string { r.Status = "success" b, e := json.MarshalIndent(r, "", " ") fatalIf(probe.NewError(e), "Unable to marshal to JSON") return string(b) } func (r rebalanceStopMsg) String() string { return console.Colorize("rebalanceStopMsg", fmt.Sprintf("Rebalance stopped for %s", r.Target)) } func mainAdminRebalanceStop(ctx *cli.Context) error { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) } console.SetColor("rebalanceStopMsg", color.New(color.FgGreen)) args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client") fatalIf(probe.NewError(client.RebalanceStop(globalContext)), "Unable to stop rebalance operation") printMsg(rebalanceStopMsg{ Target: aliasedURL, }) return nil } minio-client-0.0~20250403/cmd/admin-replicate-add.go000066400000000000000000000070511477450377600216630ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminReplicateAddFlags = []cli.Flag{ cli.BoolFlag{ Name: "replicate-ilm-expiry", Usage: "replicate ILM expiry rules", }, } var adminReplicateAddCmd = cli.Command{ Name: "add", Usage: "add one or more sites for replication", Action: mainAdminReplicateAdd, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, adminReplicateAddFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS1 ALIAS2 [ALIAS3...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Add a site for cluster-level replication: {{.Prompt}} {{.HelpName}} minio1 minio2 2. Add a site for cluster-level replication with replication of ILM expiry rules: {{.Prompt}} {{.HelpName}} minio1 minio2 --replicate-ilm-expiry `, } type successMessage madmin.ReplicateAddStatus func (m successMessage) JSON() string { bs, e := json.MarshalIndent(madmin.ReplicateAddStatus(m), "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(bs) } func (m successMessage) String() string { v := madmin.ReplicateAddStatus(m) messages := []string{v.Status} if v.ErrDetail != "" { messages = append(messages, v.ErrDetail) } if v.InitialSyncErrorMessage != "" { messages = append(messages, v.InitialSyncErrorMessage) } return console.Colorize("UserMessage", strings.Join(messages, "\n")) } func mainAdminReplicateAdd(ctx *cli.Context) error { { // Check argument count argsNr := len(ctx.Args()) if argsNr < 2 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Need at least two arguments to add command.") } } console.SetColor("UserMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") ps := make([]madmin.PeerSite, 0, len(ctx.Args())) for _, clusterName := range ctx.Args() { admClient, err := newAdminClient(clusterName) fatalIf(err, "unable to initialize admin connection") ak, sk := admClient.GetAccessAndSecretKey() ps = append(ps, madmin.PeerSite{ Name: clusterName, Endpoint: admClient.GetEndpointURL().String(), AccessKey: ak, SecretKey: sk, }) } var opts madmin.SRAddOptions opts.ReplicateILMExpiry = ctx.Bool("replicate-ilm-expiry") res, e := client.SiteReplicationAdd(globalContext, ps, opts) fatalIf(probe.NewError(e).Trace(args...), "Unable to add sites for replication") printMsg(successMessage(res)) return nil } minio-client-0.0~20250403/cmd/admin-replicate-info.go000066400000000000000000000103231477450377600220620ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strconv" "strings" humanize "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminReplicateInfoCmd = cli.Command{ Name: "info", Usage: "get site replication information", Action: mainAdminReplicationInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS1 FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get Site Replication information: {{.Prompt}} {{.HelpName}} minio1 `, } type srInfo madmin.SiteReplicationInfo func (i srInfo) JSON() string { bs, e := json.MarshalIndent(madmin.SiteReplicationInfo(i), "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(bs) } func (i srInfo) String() string { var messages []string info := madmin.SiteReplicationInfo(i) if info.Enabled { messages = []string{ "SiteReplication enabled for:\n", } r := console.Colorize("THeaders", newPrettyTable(" | ", Field{"Deployment ID", 36}, Field{"Name", 15}, Field{"Endpoint", 46}, Field{"Sync", 4}, Field{"Bandwidth", 10}, Field{"ILM Expiry Replication", 25}, ).buildRow("Deployment ID", "Site Name", "Endpoint", "Sync", "Bandwidth", "ILM Expiry Replication")) messages = append(messages, r) r = console.Colorize("THeaders", newPrettyTable(" | ", Field{"Deployment ID", 36}, Field{"Name", 15}, Field{"Endpoint", 46}, Field{"Sync", 4}, Field{"Bandwidth", 10}, Field{"ILM Expiry Replication", 25}, ).buildRow("", "", "", "", "Per Bucket", "")) messages = append(messages, r) for _, peer := range info.Sites { var chk string if peer.SyncState == madmin.SyncEnabled { chk = check } limit := "N/A" // N/A means cluster bandwidth is not configured if peer.DefaultBandwidth.Limit > 0 { limit = humanize.Bytes(uint64(peer.DefaultBandwidth.Limit)) limit = fmt.Sprintf("%s/s", limit) } r := console.Colorize("TDetail", newPrettyTable(" | ", Field{"Deployment ID", 36}, Field{"Name", 15}, Field{"Endpoint", 46}, Field{"Sync", 4}, Field{"Bandwidth", 10}, Field{"ILM Expiry Replication", 25}, ).buildRow(peer.DeploymentID, peer.Name, peer.Endpoint, chk, limit, strconv.FormatBool(peer.ReplicateILMExpiry))) messages = append(messages, r) } } else { messages = []string{"SiteReplication is not enabled"} } return console.Colorize("UserMessage", strings.Join(messages, "\n")) } func mainAdminReplicationInfo(ctx *cli.Context) error { { // Check argument count argsNr := len(ctx.Args()) if argsNr != 1 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Need exactly one alias argument.") } } console.SetColor("UserMessage", color.New(color.FgGreen)) console.SetColor("THeaders", color.New(color.Bold, color.FgHiWhite)) console.SetColor("TDetail", color.New(color.Bold, color.FgCyan)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") info, e := client.SiteReplicationInfo(globalContext) fatalIf(probe.NewError(e).Trace(args...), "Unable to get cluster replication information") printMsg(srInfo(info)) return nil } minio-client-0.0~20250403/cmd/admin-replicate-remove.go000066400000000000000000000103501477450377600224240ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminReplicateRemoveFlags = []cli.Flag{ cli.BoolFlag{ Name: "all", Usage: "remove site replication from all participating sites", }, cli.BoolFlag{ Name: "force", Usage: "force removal of site(s) from site replication configuration", }, } var adminReplicateRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove one or more sites from site replication", Action: mainAdminReplicationRemoveStatus, OnUsageError: onUsageError, HiddenAliases: true, Before: setGlobalsFromContext, Flags: append(globalFlags, adminReplicateRemoveFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove site replication for all sites: {{.Prompt}} {{.HelpName}} minio2 --all --force 2. Remove site replication for site with site names alpha, baker from active cluster minio2: {{.Prompt}} {{.HelpName}} minio2 alpha baker --force `, } type srRemoveStatus struct { madmin.ReplicateRemoveStatus sites []string RemoveAll bool } func (i srRemoveStatus) JSON() string { ds, e := json.MarshalIndent(i.ReplicateRemoveStatus, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(ds) } func (i srRemoveStatus) String() string { if i.RemoveAll { return console.Colorize("UserMessage", "All site(s) were removed successfully") } if i.Status == madmin.ReplicateRemoveStatusSuccess { return console.Colorize("UserMessage", fmt.Sprintf("Following site(s) %s were removed successfully", i.sites)) } if len(i.sites) == 1 { return console.Colorize("UserMessage", fmt.Sprintf("Following site %s was removed partially, some operations failed:\nERROR: '%s'", i.sites, i.ErrDetail)) } return console.Colorize("UserMessage", fmt.Sprintf("Following site(s) %s were removed partially, some operations failed: \nERROR: '%s'", i.sites, i.ErrDetail)) } func checkAdminReplicateRemoveSyntax(ctx *cli.Context) { // Check argument count argsNr := len(ctx.Args()) if ctx.Bool("all") && argsNr > 1 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "") } if argsNr < 2 && !ctx.Bool("all") { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Need at least two arguments to remove command.") } if !ctx.Bool("force") { fatalIf(errDummy().Trace(), "Site removal requires --force flag. This operation is *IRREVERSIBLE*. Please review carefully before performing this *DANGEROUS* operation.") } } func mainAdminReplicationRemoveStatus(ctx *cli.Context) error { checkAdminReplicateRemoveSyntax(ctx) console.SetColor("UserMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) var rreq madmin.SRRemoveReq rreq.SiteNames = append(rreq.SiteNames, args.Tail()...) rreq.RemoveAll = ctx.Bool("all") // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") st, e := client.SiteReplicationRemove(globalContext, rreq) fatalIf(probe.NewError(e).Trace(args...), "Unable to remove cluster replication") printMsg(srRemoveStatus{ ReplicateRemoveStatus: st, sites: args.Tail(), RemoveAll: rreq.RemoveAll, }) return nil } minio-client-0.0~20250403/cmd/admin-replicate-resync-cancel.go000066400000000000000000000065651477450377600236720ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminReplicateResyncCancelCmd = cli.Command{ Name: "cancel", Usage: "cancel ongoing resync operation", Action: mainAdminReplicateResyncCancel, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS1 ALIAS2 FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Cancel ongoing resync of bucket data from minio1 to minio2 {{.Prompt}} {{.HelpName}} minio1 minio2 `, } type resyncCancelMessage madmin.SRResyncOpStatus func (m resyncCancelMessage) JSON() string { bs, e := json.MarshalIndent(madmin.SRResyncOpStatus(m), "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(bs) } func (m resyncCancelMessage) String() string { v := madmin.SRResyncOpStatus(m) messages := []string{} th := "ResyncMessage" if v.ErrDetail != "" { messages = append(messages, v.ErrDetail) th = "ResyncErr" } else { messages = append(messages, fmt.Sprintf("Site resync with ID %s canceled successfully.", v.ResyncID)) } return console.Colorize(th, strings.Join(messages, "\n")) } func mainAdminReplicateResyncCancel(ctx *cli.Context) error { // Check argument count argsNr := len(ctx.Args()) if argsNr != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } console.SetColor("ResyncMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") info, e := client.SiteReplicationInfo(globalContext) fatalIf(probe.NewError(e), "Unable to fetch site replication info.") peerClient := getClient(args.Get(1)) peerAdmInfo, e := peerClient.ServerInfo(globalContext) fatalIf(probe.NewError(e), "Unable to fetch server info of the peer.") var peer madmin.PeerInfo for _, site := range info.Sites { if peerAdmInfo.DeploymentID == site.DeploymentID { peer = site } } if peer.DeploymentID == "" { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "alias provided is not part of cluster replication.") } res, e := client.SiteReplicationResyncOp(globalContext, peer, madmin.SiteResyncCancel) fatalIf(probe.NewError(e).Trace(args...), "Unable to cancel replication resync") printMsg(resyncCancelMessage(res)) return nil } minio-client-0.0~20250403/cmd/admin-replicate-resync-start.go000066400000000000000000000065101477450377600235700ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strings" "github.com/minio/madmin-go/v3" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminReplicateResyncStartCmd = cli.Command{ Name: "start", Usage: "start resync to site", Action: mainAdminReplicateResyncStart, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS1 ALIAS2 FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Resync bucket data from minio1 to minio2 {{.Prompt}} {{.HelpName}} minio1 minio2 `, } type resyncMessage madmin.SRResyncOpStatus func (m resyncMessage) JSON() string { bs, e := json.MarshalIndent(madmin.SRResyncOpStatus(m), "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(bs) } func (m resyncMessage) String() string { v := madmin.SRResyncOpStatus(m) messages := []string{} th := "ResyncMessage" if v.ErrDetail != "" { messages = append(messages, v.ErrDetail) th = "ResyncErr" } else { messages = append(messages, fmt.Sprintf("Site resync started with ID %s", v.ResyncID)) } return console.Colorize(th, strings.Join(messages, "\n")) } func mainAdminReplicateResyncStart(ctx *cli.Context) error { // Check argument count argsNr := len(ctx.Args()) if argsNr != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } console.SetColor("ResyncMessage", color.New(color.FgGreen)) console.SetColor("ResyncErr", color.New(color.FgRed)) // Get the alias parameter from cli args := ctx.Args() // Create a new MinIO Admin Client client, err := newAdminClient(args.Get(0)) fatalIf(err, "Unable to initialize admin connection.") info, e := client.SiteReplicationInfo(globalContext) fatalIf(probe.NewError(e), "Unable to fetch site replication info.") peerClient := getClient(args.Get(1)) peerAdmInfo, e := peerClient.ServerInfo(globalContext) fatalIf(probe.NewError(e), "Unable to fetch server info of the peer.") var peer madmin.PeerInfo for _, site := range info.Sites { if peerAdmInfo.DeploymentID == site.DeploymentID { peer = site } } if peer.DeploymentID == "" { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "alias provided is not part of cluster replication.") } res, e := client.SiteReplicationResyncOp(globalContext, peer, madmin.SiteResyncStart) fatalIf(probe.NewError(e).Trace(args...), "Unable to start replication resync") printMsg(resyncMessage(res)) return nil } minio-client-0.0~20250403/cmd/admin-replicate-resync-status.go000066400000000000000000000156001477450377600237560ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "fmt" "strings" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" "github.com/olekukonko/tablewriter" ) var adminReplicateResyncStatusCmd = cli.Command{ Name: "status", Usage: "show site replication resync status", Action: mainAdminReplicationResyncStatus, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS1 ALIAS2 FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display status of resync from minio1 to minio2 {{.Prompt}} {{.HelpName}} minio1 minio2 `, } func mainAdminReplicationResyncStatus(ctx *cli.Context) error { // Check argument count argsNr := len(ctx.Args()) if argsNr != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } console.SetColor("ResyncMessage", color.New(color.FgGreen)) console.SetColor("THeader", color.New(color.Bold, color.FgHiWhite)) console.SetColor("THeader2", color.New(color.Bold, color.FgYellow)) console.SetColor("TDetail", color.New(color.Bold, color.FgCyan)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") info, e := client.SiteReplicationInfo(globalContext) fatalIf(probe.NewError(e), "Unable to fetch site replication info.") if !info.Enabled { console.Colorize("ResyncMessage", "SiteReplication is not enabled") return nil } peerClient := getClient(args.Get(1)) peerAdmInfo, e := peerClient.ServerInfo(globalContext) fatalIf(probe.NewError(e), "Unable to fetch server info of the peer.") var peer madmin.PeerInfo for _, site := range info.Sites { if peerAdmInfo.DeploymentID == site.DeploymentID { peer = site } } if peer.DeploymentID == "" { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "alias provided is not part of cluster replication.") } ctxt, cancel := context.WithCancel(globalContext) defer cancel() ui := tea.NewProgram(initResyncMetricsUI(peer.DeploymentID)) go func() { opts := madmin.MetricsOptions{ Type: madmin.MetricsSiteResync, ByDepID: peer.DeploymentID, } e := client.Metrics(ctxt, opts, func(metrics madmin.RealtimeMetrics) { if globalJSON { printMsg(metricsMessage{RealtimeMetrics: metrics}) return } if metrics.Aggregated.SiteResync != nil { sr := metrics.Aggregated.SiteResync ui.Send(sr) if sr.Complete() { cancel() return } } }) if e != nil && !errors.Is(e, context.Canceled) { fatalIf(probe.NewError(e).Trace(ctx.Args()...), "Unable to get resync status") } }() if !globalJSON { if _, e := ui.Run(); e != nil { cancel() fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to get resync status") } } return nil } func initResyncMetricsUI(deplID string) *resyncMetricsUI { s := spinner.New() s.Spinner = spinner.Points s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) return &resyncMetricsUI{ spinner: s, deplID: deplID, } } type resyncMetricsUI struct { current madmin.SiteResyncMetrics spinner spinner.Model quitting bool deplID string } func (m *resyncMetricsUI) Init() tea.Cmd { return m.spinner.Tick } func (m *resyncMetricsUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c": m.quitting = true return m, tea.Quit default: return m, nil } case *madmin.SiteResyncMetrics: m.current = *msg if msg.ResyncStatus == "Canceled" { m.quitting = true return m, tea.Quit } if msg.Complete() { m.quitting = true return m, tea.Quit } return m, nil case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd default: return m, nil } } func (m *resyncMetricsUI) View() string { var s strings.Builder // Set table header table := tablewriter.NewWriter(&s) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) var data [][]string addLine := func(prefix string, value interface{}) { data = append(data, []string{ prefix, whiteStyle.Render(fmt.Sprint(value)), }) } if !m.quitting { s.WriteString(m.spinner.View()) } else { if m.current.Complete() { if m.current.FailedCount == 0 { s.WriteString(m.spinner.Style.Render((tickCell + tickCell + tickCell))) } else { s.WriteString(m.spinner.Style.Render((crossTickCell + crossTickCell + crossTickCell))) } } } s.WriteString("\n") if m.current.ResyncID != "" { accElapsedTime := m.current.LastUpdate.Sub(m.current.StartTime) addLine("ResyncID: ", m.current.ResyncID) addLine("Status: ", m.current.ResyncStatus) addLine("Objects: ", m.current.ReplicatedCount) addLine("Versions: ", m.current.ReplicatedCount) addLine("FailedObjects: ", m.current.FailedCount) if accElapsedTime > 0 { bytesTransferredPerSec := float64(int64(time.Second)*m.current.ReplicatedSize) / float64(accElapsedTime) objectsPerSec := float64(int64(time.Second)*m.current.ReplicatedCount) / float64(accElapsedTime) addLine("Throughput: ", fmt.Sprintf("%s/s", humanize.IBytes(uint64(bytesTransferredPerSec)))) addLine("IOPs: ", fmt.Sprintf("%.2f objs/s", objectsPerSec)) } addLine("Transferred: ", humanize.IBytes(uint64(m.current.ReplicatedSize))) addLine("Elapsed: ", accElapsedTime.String()) addLine("CurrObjName: ", fmt.Sprintf("%s/%s", m.current.Bucket, m.current.Object)) } table.AppendBulk(data) table.Render() if m.quitting { s.WriteString("\n") } return s.String() } minio-client-0.0~20250403/cmd/admin-replicate-resync.go000066400000000000000000000025601477450377600224360ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminReplicateResyncSubcommands = []cli.Command{ adminReplicateResyncStartCmd, adminReplicateResyncStatusCmd, adminReplicateResyncCancelCmd, } var adminReplicateResyncCmd = cli.Command{ Name: "resync", Usage: "resync content to site", Action: mainAdminReplicateResync, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminReplicateResyncSubcommands, HideHelpCommand: true, } func mainAdminReplicateResync(ctx *cli.Context) error { commandNotFound(ctx, adminReplicateResyncSubcommands) return nil } minio-client-0.0~20250403/cmd/admin-replicate-status.go000066400000000000000000000763001477450377600224610ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "sort" "strings" "time" humanize "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/pkg/v3/console" ) var adminReplicateStatusFlags = []cli.Flag{ cli.BoolFlag{ Name: "buckets", Usage: "display only buckets", }, cli.BoolFlag{ Name: "policies", Usage: "display only policies", }, cli.BoolFlag{ Name: "users", Usage: "display only users", }, cli.BoolFlag{ Name: "groups", Usage: "display only groups", }, cli.BoolFlag{ Name: "ilm-expiry-rules", Usage: "display only ilm expiry rules", }, cli.BoolFlag{ Name: "all", Usage: "display all available site replication status", }, cli.StringFlag{ Name: "bucket", Usage: "display bucket sync status", }, cli.StringFlag{ Name: "policy", Usage: "display policy sync status", }, cli.StringFlag{ Name: "user", Usage: "display user sync status", }, cli.StringFlag{ Name: "group", Usage: "display group sync status", }, cli.StringFlag{ Name: "ilm-expiry-rule", Usage: "display ILM expiry rule sync status", }, } // Some cell values const ( tickCell string = "✔ " crossTickCell string = "✗ " blankCell string = " " fieldLen = 15 ) var adminReplicateStatusCmd = cli.Command{ Name: "status", Usage: "display site replication status", Action: mainAdminReplicationStatus, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, adminReplicateStatusFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display overall site replication status: {{.Prompt}} {{.HelpName}} minio1 2. Display site replication status of buckets across sites {{.Prompt}} {{.HelpName}} minio1 --buckets 3. Drill down and view site replication status of bucket "bucket" {{.Prompt}} {{.HelpName}} minio1 --bucket bucket 4. Drill down and view site replication status of user "foo" {{.Prompt}} {{.HelpName}} minio1 --user foo `, } type srStatus struct { madmin.SRStatusInfo opts madmin.SRStatusOptions } func (i srStatus) JSON() string { bs, e := json.MarshalIndent(madmin.SRStatusInfo(i.SRStatusInfo), "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(bs) } func (i srStatus) String() string { var messages []string ms := i.Metrics q := i.Metrics.Queued w := i.Metrics.ActiveWorkers // Color palette initialization console.SetColor("Summary", color.New(color.FgWhite, color.Bold)) console.SetColor("SummaryHdr", color.New(color.FgCyan, color.Bold)) console.SetColor("SummaryDtl", color.New(color.FgGreen, color.Bold)) coloredDot := console.Colorize("Status", dot) nameIDMap := make(map[string]string) var siteNames []string info := i.SRStatusInfo for dID := range info.Sites { sname := strings.ToTitle(info.Sites[dID].Name) siteNames = append(siteNames, sname) nameIDMap[sname] = dID } if !info.Enabled { messages = []string{"SiteReplication is not enabled"} return console.Colorize("UserMessage", strings.Join(messages, "\n")) } sort.Strings(siteNames) legendHdr := []string{"Site"} legendFields := []Field{{"Entity", 15}} for _, sname := range siteNames { legendHdr = append(legendHdr, sname) legendFields = append(legendFields, Field{"sname", 15}) } if i.opts.Buckets { messages = append(messages, console.Colorize("SummaryHdr", "Bucket replication status:")) switch i.MaxBuckets { case 0: messages = append(messages, console.Colorize("Summary", "No Buckets present\n")) default: msg := console.Colorize(i.getTheme(len(info.BucketStats) == 0), fmt.Sprintf("%d/%d Buckets in sync", info.MaxBuckets-len(info.BucketStats), info.MaxBuckets)) + "\n" messages = append(messages, fmt.Sprintf("%s %s", coloredDot, msg)) if len(i.BucketStats) > 0 { messages = append(messages, i.siteHeader(siteNames, "Bucket")) } var detailFields []Field for b, ssMap := range i.BucketStats { var details []string details = append(details, b) detailFields = append(detailFields, legendFields[0]) for _, sname := range siteNames { detailFields = append(detailFields, legendFields[0]) dID := nameIDMap[sname] ss := ssMap[dID] switch { case !ss.HasBucket: details = append(details, fmt.Sprintf("%s ", blankCell)) case ss.OLockConfigMismatch, ss.PolicyMismatch, ss.QuotaCfgMismatch, ss.ReplicationCfgMismatch, ss.TagMismatch: details = append(details, fmt.Sprintf("%s in-sync", crossTickCell)) default: details = append(details, fmt.Sprintf("%s in-sync", tickCell)) } } messages = append(messages, newPrettyTable(" | ", detailFields...).buildRow(details...)) messages = append(messages, "") } } } if i.opts.Policies { messages = append(messages, console.Colorize("SummaryHdr", "Policy replication status:")) switch i.MaxPolicies { case 0: messages = append(messages, console.Colorize("Summary", "No Policies present\n")) default: msg := console.Colorize(i.getTheme(len(i.PolicyStats) == 0), fmt.Sprintf("%d/%d Policies in sync", info.MaxPolicies-len(info.PolicyStats), info.MaxPolicies)) + "\n" messages = append(messages, fmt.Sprintf("%s %s", coloredDot, msg)) if len(i.PolicyStats) > 0 { messages = append(messages, i.siteHeader(siteNames, "Policy")) } var detailFields []Field for b, ssMap := range i.PolicyStats { var details []string details = append(details, b) detailFields = append(detailFields, legendFields[0]) for _, sname := range siteNames { detailFields = append(detailFields, legendFields[0]) dID := nameIDMap[sname] ss := ssMap[dID] switch { case !ss.HasPolicy: details = append(details, blankCell) case ss.PolicyMismatch: details = append(details, fmt.Sprintf("%s in-sync", crossTickCell)) default: details = append(details, fmt.Sprintf("%s in-sync", tickCell)) } } messages = append(messages, newPrettyTable(" | ", detailFields...).buildRow(details...)) } if len(i.PolicyStats) > 0 { messages = append(messages, "") } } } if i.opts.Users { messages = append(messages, console.Colorize("SummaryHdr", "User replication status:")) switch i.MaxUsers { case 0: messages = append(messages, console.Colorize("Summary", "No Users present\n")) default: msg := console.Colorize(i.getTheme(len(i.UserStats) == 0), fmt.Sprintf("%d/%d Users in sync", info.MaxUsers-len(i.UserStats), info.MaxUsers)) + "\n" messages = append(messages, fmt.Sprintf("%s %s", coloredDot, msg)) if len(i.UserStats) > 0 { messages = append(messages, i.siteHeader(siteNames, "User")) } var detailFields []Field for b, ssMap := range i.UserStats { var details []string details = append(details, b) detailFields = append(detailFields, legendFields[0]) for _, sname := range siteNames { detailFields = append(detailFields, legendFields[0]) dID := nameIDMap[sname] ss, ok := ssMap[dID] switch { case !ss.HasUser: details = append(details, blankCell) case !ok, ss.UserInfoMismatch: details = append(details, fmt.Sprintf("%s in-sync", crossTickCell)) default: details = append(details, fmt.Sprintf("%s in-sync", tickCell)) } } messages = append(messages, newPrettyTable(" | ", detailFields...).buildRow(details...)) } if len(i.UserStats) > 0 { messages = append(messages, "") } } } if i.opts.Groups { messages = append(messages, console.Colorize("SummaryHdr", "Group replication status:")) switch i.MaxGroups { case 0: messages = append(messages, console.Colorize("Summary", "No Groups present\n")) default: msg := console.Colorize(i.getTheme(len(i.GroupStats) == 0), fmt.Sprintf("%d/%d Groups in sync", i.MaxGroups-len(i.GroupStats), i.MaxGroups)) + "\n" messages = append(messages, fmt.Sprintf("%s %s", coloredDot, msg)) if len(i.GroupStats) > 0 { messages = append(messages, i.siteHeader(siteNames, "Group")) } var detailFields []Field for b, ssMap := range i.GroupStats { var details []string details = append(details, b) detailFields = append(detailFields, legendFields[0]) for _, sname := range siteNames { detailFields = append(detailFields, legendFields[0]) dID := nameIDMap[sname] ss := ssMap[dID] switch { case !ss.HasGroup: details = append(details, blankCell) case ss.GroupDescMismatch: details = append(details, fmt.Sprintf("%s in-sync", crossTickCell)) default: details = append(details, fmt.Sprintf("%s in-sync", tickCell)) } } messages = append(messages, newPrettyTable(" | ", detailFields...).buildRow(details...)) } if len(i.GroupStats) > 0 { messages = append(messages, "") } } } if i.opts.ILMExpiryRules { messages = append(messages, console.Colorize("SummaryHdr", "ILM Expiry Rules replication status:")) switch { case i.MaxILMExpiryRules == 0: messages = append(messages, console.Colorize("Summary", "No ILM Expiry Rules present\n")) case i.ILMExpiryStats == nil: messages = append(messages, console.Colorize("Summary", "Replication of ILM Expiry is not enabled\n")) default: msg := console.Colorize(i.getTheme(len(info.ILMExpiryStats) == 0), fmt.Sprintf("%d/%d ILM Expiry Rules in sync", info.MaxILMExpiryRules-len(info.ILMExpiryStats), info.MaxILMExpiryRules)) + "\n" messages = append(messages, fmt.Sprintf("%s %s", coloredDot, msg)) if len(i.ILMExpiryStats) > 0 { messages = append(messages, i.siteHeader(siteNames, "ILM Expiry Rules")) } var detailFields []Field for b, ssMap := range i.ILMExpiryStats { var details []string details = append(details, b) detailFields = append(detailFields, legendFields[0]) for _, sname := range siteNames { detailFields = append(detailFields, legendFields[0]) dID := nameIDMap[sname] ss := ssMap[dID] switch { case !ss.HasILMExpiryRules: details = append(details, blankCell) case ss.ILMExpiryRuleMismatch: details = append(details, fmt.Sprintf("%s in-sync", crossTickCell)) default: details = append(details, fmt.Sprintf("%s in-sync", tickCell)) } } messages = append(messages, newPrettyTable(" | ", detailFields...).buildRow(details...)) messages = append(messages, "") } } } switch i.opts.Entity { case madmin.SRBucketEntity: messages = append(messages, i.getBucketStatusSummary(siteNames, nameIDMap, "Bucket")...) case madmin.SRPolicyEntity: messages = append(messages, i.getPolicyStatusSummary(siteNames, nameIDMap, "Policy")...) case madmin.SRUserEntity: messages = append(messages, i.getUserStatusSummary(siteNames, nameIDMap, "User")...) case madmin.SRGroupEntity: messages = append(messages, i.getGroupStatusSummary(siteNames, nameIDMap, "Group")...) case madmin.SRILMExpiryRuleEntity: messages = append(messages, i.getILMExpiryStatusSummary(siteNames, nameIDMap, "ILMExpiryRule")...) } if i.opts.Metrics { uiFn := func(theme string) func(string) string { return func(s string) string { return console.Colorize(theme, s) } } singleTgt := len(ms.Metrics) == 1 maxui := uiFn("Peak") avgui := uiFn("Avg") valueui := uiFn("Value") messages = append(messages, console.Colorize("SummaryHdr", "Object replication status:")) messages = append(messages, console.Colorize("UptimeStr", fmt.Sprintf("Replication status since %s", uiFn("Uptime")(humanize.RelTime(time.Now(), time.Now().Add(time.Duration(ms.Uptime)*time.Second), "", "ago"))))) // for queue stats coloredDot := console.Colorize("qStatusOK", dot) if q.Curr.Count > q.Avg.Count { coloredDot = console.Colorize("qStatusWarn", dot) } var replicatedCount, replicatedSize int64 for _, m := range ms.Metrics { nodeName := m.Endpoint nodeui := uiFn(getNodeTheme(nodeName)) messages = append(messages, nodeui(nodeName)) messages = append(messages, fmt.Sprintf("Replicated: %s objects (%s)", humanize.Comma(int64(m.ReplicatedCount)), valueui(humanize.IBytes(uint64(m.ReplicatedSize))))) if singleTgt { // for single target - combine summary section into the target section messages = append(messages, fmt.Sprintf("Received: %s objects (%s)", humanize.Comma(int64(ms.ReplicaCount)), humanize.IBytes(uint64(ms.ReplicaSize)))) messages = append(messages, fmt.Sprintf("Queued: %s %s objects, (%s) (%s: %s objects, %s; %s: %s objects, %s)", coloredDot, humanize.Comma(int64(q.Curr.Count)), valueui(humanize.IBytes(uint64(q.Curr.Bytes))), avgui("avg"), humanize.Comma(int64(q.Avg.Count)), valueui(humanize.IBytes(uint64(q.Avg.Bytes))), maxui("max"), humanize.Comma(int64(q.Max.Count)), valueui(humanize.IBytes(uint64(q.Max.Bytes))))) messages = append(messages, fmt.Sprintf("Workers: %s (%s: %s; %s %s) ", humanize.Comma(int64(w.Curr)), avgui("avg"), humanize.Comma(int64(w.Avg)), maxui("max"), humanize.Comma(int64(w.Max)))) } else { replicatedCount += m.ReplicatedCount replicatedSize += m.ReplicatedSize } if m.XferStats != nil { tgtXfer, ok := m.XferStats[replication.Total] if ok { messages = append(messages, fmt.Sprintf("Transfer Rate: %s/s (avg: %s/s; max %s/s)", valueui(humanize.Bytes(uint64(tgtXfer.CurrRate))), valueui(humanize.Bytes(uint64(tgtXfer.AvgRate))), valueui(humanize.Bytes(uint64(tgtXfer.PeakRate))))) messages = append(messages, fmt.Sprintf("Latency: %s (avg: %s; max %s)", valueui(m.Latency.Curr.Round(time.Millisecond).String()), valueui(m.Latency.Avg.Round(time.Millisecond).String()), valueui(m.Latency.Max.Round(time.Millisecond).String()))) } } healthDot := console.Colorize("online", dot) if !m.Online { healthDot = console.Colorize("offline", dot) } currDowntime := time.Duration(0) if !m.Online && !m.LastOnline.IsZero() { currDowntime = UTCNow().Sub(m.LastOnline) } // normalize because total downtime is calculated at server side at heartbeat interval, may be slightly behind totalDowntime := m.TotalDowntime if currDowntime > totalDowntime { totalDowntime = currDowntime } var linkStatus string if m.Online { linkStatus = healthDot + fmt.Sprintf(" online (total downtime: %s)", timeDurationToHumanizedDuration(totalDowntime).String()) } else { linkStatus = healthDot + fmt.Sprintf(" offline %s (total downtime: %s)", timeDurationToHumanizedDuration(currDowntime).String(), valueui(timeDurationToHumanizedDuration(totalDowntime).String())) } messages = append(messages, fmt.Sprintf("Link: %s", linkStatus)) messages = append(messages, fmt.Sprintf("Errors: %s in last 1 minute; %s in last 1hr; %s since uptime", valueui(humanize.Comma(int64(m.Failed.LastMinute.Count))), valueui(humanize.Comma(int64(m.Failed.LastHour.Count))), valueui(humanize.Comma(int64(m.Failed.Totals.Count))))) messages = append(messages, "") } if !singleTgt { messages = append(messages, console.Colorize("SummaryHdr", "Summary:")) messages = append(messages, fmt.Sprintf("Replicated: %s objects (%s)", humanize.Comma(int64(replicatedCount)), valueui(humanize.IBytes(uint64(replicatedSize))))) messages = append(messages, fmt.Sprintf("Queued: %s %s objects, (%s) (%s: %s objects, %s; %s: %s objects, %s)", coloredDot, humanize.Comma(int64(q.Curr.Count)), valueui(humanize.IBytes(uint64(q.Curr.Bytes))), avgui("avg"), humanize.Comma(int64(q.Avg.Count)), valueui(humanize.IBytes(uint64(q.Avg.Bytes))), maxui("max"), humanize.Comma(int64(q.Max.Count)), valueui(humanize.IBytes(uint64(q.Max.Bytes))))) messages = append(messages, fmt.Sprintf("Received: %s objects (%s)", humanize.Comma(int64(ms.ReplicaCount)), humanize.IBytes(uint64(ms.ReplicaSize)))) } } return console.Colorize("UserMessage", strings.Join(messages, "\n")) } func (i srStatus) siteHeader(siteNames []string, legend string) string { legendHdr := []string{legend} legendFields := []Field{{"Entity", 15}} for _, sname := range siteNames { legendHdr = append(legendHdr, sname) legendFields = append(legendFields, Field{"sname", 15}) } return console.Colorize("SummaryHdr", newPrettyTable(" | ", legendFields..., ).buildRow(legendHdr...)) } func (i srStatus) getTheme(match bool) string { theme := "UserMessage" if !match { theme = "WarningMessage" } return theme } func (i srStatus) getBucketStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string { var messages []string coloredDot := console.Colorize("Status", dot) var found bool for _, st := range i.BucketStats[i.opts.EntityValue] { if st.HasBucket { found = true break } } if !found { messages = append(messages, console.Colorize("Summary", fmt.Sprintf("Bucket %s not found\n", i.opts.EntityValue))) return messages } messages = append(messages, console.Colorize("SummaryHdr", fmt.Sprintf("%s %s\n", coloredDot, console.Colorize("Summary", "Bucket config replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue)))) siteHdr := i.siteHeader(siteNames, legend) messages = append(messages, siteHdr) rowLegend := []string{"Tags", "Policy", "Quota", "Retention", "Encryption", "Replication"} detailFields := make([][]Field, len(rowLegend)) var retention, encryption, tags, bpolicies, quota, replication []string for i, row := range rowLegend { detailFields[i] = make([]Field, len(siteNames)+1) detailFields[i][0] = Field{"Entity", 15} switch i { case 0: tags = append(tags, row) case 1: bpolicies = append(bpolicies, row) case 2: quota = append(quota, row) case 3: retention = append(retention, row) case 4: encryption = append(encryption, row) case 5: replication = append(replication, row) } } rows := make([]string, len(rowLegend)) for j, sname := range siteNames { dID := nameIDMap[sname] ss := i.BucketStats[i.opts.EntityValue][dID] var theme, msgStr string for r := range rowLegend { switch r { case 0: theme, msgStr = syncStatus(ss.TagMismatch, ss.HasTagsSet) tags = append(tags, msgStr) detailFields[r][j+1] = Field{theme, fieldLen} case 1: theme, msgStr = syncStatus(ss.PolicyMismatch, ss.HasPolicySet) bpolicies = append(bpolicies, msgStr) detailFields[r][j+1] = Field{theme, fieldLen} case 2: theme, msgStr = syncStatus(ss.QuotaCfgMismatch, ss.HasQuotaCfgSet) quota = append(quota, msgStr) detailFields[r][j+1] = Field{theme, fieldLen} case 3: theme, msgStr = syncStatus(ss.OLockConfigMismatch, ss.HasOLockConfigSet) retention = append(retention, msgStr) detailFields[r][j+1] = Field{theme, fieldLen} case 4: theme, msgStr = syncStatus(ss.SSEConfigMismatch, ss.HasSSECfgSet) encryption = append(encryption, msgStr) detailFields[r][j+1] = Field{theme, fieldLen} case 5: theme, msgStr = syncStatus(ss.ReplicationCfgMismatch, ss.HasReplicationCfg) replication = append(replication, msgStr) detailFields[r][j+1] = Field{theme, fieldLen} } } } for r := range rowLegend { switch r { case 0: rows[r] = newPrettyTable(" | ", detailFields[r]...).buildRow(tags...) case 1: rows[r] = newPrettyTable(" | ", detailFields[r]...).buildRow(bpolicies...) case 2: rows[r] = newPrettyTable(" | ", detailFields[r]...).buildRow(quota...) case 3: rows[r] = newPrettyTable(" | ", detailFields[r]...).buildRow(retention...) case 4: rows[r] = newPrettyTable(" | ", detailFields[r]...).buildRow(encryption...) case 5: rows[r] = newPrettyTable(" | ", detailFields[r]...).buildRow(replication...) } } messages = append(messages, rows...) return messages } func (i srStatus) getPolicyStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string { var messages []string coloredDot := console.Colorize("Status", dot) var found bool for _, st := range i.PolicyStats[i.opts.EntityValue] { if st.HasPolicy { found = true break } } if !found { messages = append(messages, console.Colorize("Summary", fmt.Sprintf("Policy %s not found\n", i.opts.EntityValue))) return messages } rowLegend := []string{"Policy"} detailFields := make([][]Field, len(rowLegend)) var policies []string detailFields[0] = make([]Field, len(siteNames)+1) detailFields[0][0] = Field{"Entity", 15} policies = append(policies, "Policy") rows := make([]string, len(rowLegend)) for j, sname := range siteNames { dID := nameIDMap[sname] ss := i.PolicyStats[i.opts.EntityValue][dID] var theme, msgStr string for r := range rowLegend { switch r { case 0: theme, msgStr = syncStatus(ss.PolicyMismatch, ss.HasPolicy) policies = append(policies, msgStr) detailFields[r][j+1] = Field{theme, fieldLen} } } } for r := range rowLegend { switch r { case 0: rows[r] = newPrettyTable(" | ", detailFields[r]...).buildRow(policies...) } } messages = append(messages, console.Colorize("SummaryHdr", fmt.Sprintf("%s %s\n", coloredDot, console.Colorize("Summary", "Policy replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue)))) siteHdr := i.siteHeader(siteNames, legend) messages = append(messages, siteHdr) messages = append(messages, rows...) return messages } func (i srStatus) getUserStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string { var messages []string coloredDot := console.Colorize("Status", dot) var found bool for _, st := range i.UserStats[i.opts.EntityValue] { if st.HasUser { found = true break } } if !found { messages = append(messages, console.Colorize("Summary", fmt.Sprintf("User %s not found\n", i.opts.EntityValue))) return messages } rowLegend := []string{"Info", "Policy mapping"} detailFields := make([][]Field, len(rowLegend)) var users, policyMapping []string for i, row := range rowLegend { detailFields[i] = make([]Field, len(siteNames)+1) detailFields[i][0] = Field{"Entity", 15} switch i { case 0: users = append(users, row) default: policyMapping = append(policyMapping, row) } } rows := make([]string, len(rowLegend)) for j, sname := range siteNames { dID := nameIDMap[sname] ss := i.UserStats[i.opts.EntityValue][dID] var theme, msgStr string for r := range rowLegend { switch r { case 0: theme, msgStr = syncStatus(ss.UserInfoMismatch, ss.HasUser) users = append(users, msgStr) detailFields[r][j+1] = Field{theme, fieldLen} case 1: theme, msgStr = syncStatus(ss.PolicyMismatch, ss.HasPolicyMapping) policyMapping = append(policyMapping, msgStr) detailFields[r][j+1] = Field{theme, fieldLen} } } } for r := range rowLegend { switch r { case 0: rows[r] = newPrettyTable(" | ", detailFields[r]...).buildRow(users...) case 1: rows[r] = newPrettyTable(" | ", detailFields[r]...).buildRow(policyMapping...) } } messages = append(messages, console.Colorize("SummaryHdr", fmt.Sprintf("%s %s\n", coloredDot, console.Colorize("Summary", "User replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue)))) siteHdr := i.siteHeader(siteNames, legend) messages = append(messages, siteHdr) messages = append(messages, rows...) return messages } func (i srStatus) getGroupStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string { var messages []string coloredDot := console.Colorize("Status", dot) rowLegend := []string{"Info", "Policy mapping"} detailFields := make([][]Field, len(rowLegend)) var found bool for _, st := range i.GroupStats[i.opts.EntityValue] { if st.HasGroup { found = true break } } if !found { messages = append(messages, console.Colorize("Summary", fmt.Sprintf("Group %s not found\n", i.opts.EntityValue))) return messages } var groups, policyMapping []string for i, row := range rowLegend { detailFields[i] = make([]Field, len(siteNames)+1) detailFields[i][0] = Field{"Entity", 15} switch i { case 0: groups = append(groups, row) default: policyMapping = append(policyMapping, row) } } rows := make([]string, len(rowLegend)) // b := i.opts.EntityValue for j, sname := range siteNames { dID := nameIDMap[sname] ss := i.GroupStats[i.opts.EntityValue][dID] // sm := i.SRStatusInfo.StatsSummary var theme, msgStr string for r := range rowLegend { switch r { case 0: theme, msgStr = syncStatus(ss.GroupDescMismatch, ss.HasGroup) groups = append(groups, msgStr) detailFields[r][j+1] = Field{theme, fieldLen} case 1: theme, msgStr = syncStatus(ss.PolicyMismatch, ss.HasPolicyMapping) policyMapping = append(policyMapping, msgStr) detailFields[r][j+1] = Field{theme, fieldLen} } } } for r := range rowLegend { switch r { case 0: rows[r] = newPrettyTable(" | ", detailFields[r]...).buildRow(groups...) case 1: rows[r] = newPrettyTable(" | ", detailFields[r]...).buildRow(policyMapping...) } } messages = append(messages, console.Colorize("SummaryHdr", fmt.Sprintf("%s %s\n", coloredDot, console.Colorize("Summary", "Group replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue)))) siteHdr := i.siteHeader(siteNames, legend) messages = append(messages, siteHdr) messages = append(messages, rows...) return messages } func (i srStatus) getILMExpiryStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string { var messages []string coloredDot := console.Colorize("Status", dot) var found bool for _, st := range i.ILMExpiryStats[i.opts.EntityValue] { if st.HasILMExpiryRules { found = true break } } if !found { messages = append(messages, console.Colorize("Summary", fmt.Sprintf("ILM Expiry Rule %s not found\n", i.opts.EntityValue))) return messages } rowLegend := []string{"ILM Expiry Rule"} detailFields := make([][]Field, len(rowLegend)) var rules []string detailFields[0] = make([]Field, len(siteNames)+1) detailFields[0][0] = Field{"Entity", 15} rules = append(rules, "ILM Expiry Rule") rows := make([]string, len(rowLegend)) for j, sname := range siteNames { dID := nameIDMap[sname] ss := i.ILMExpiryStats[i.opts.EntityValue][dID] var theme, msgStr string for r := range rowLegend { switch r { case 0: theme, msgStr = syncStatus(ss.ILMExpiryRuleMismatch, ss.HasILMExpiryRules) rules = append(rules, msgStr) detailFields[r][j+1] = Field{theme, fieldLen} } } } for r := range rowLegend { switch r { case 0: rows[r] = newPrettyTable(" | ", detailFields[r]...).buildRow(rules...) } } messages = append(messages, console.Colorize("SummaryHdr", fmt.Sprintf("%s %s\n", coloredDot, console.Colorize("Summary", "ILM Expiry Rule replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue)))) siteHdr := i.siteHeader(siteNames, legend) messages = append(messages, siteHdr) messages = append(messages, rows...) return messages } // Calculate srstatus options for command line flags func srStatusOpts(ctx *cli.Context) (opts madmin.SRStatusOptions) { if (!ctx.IsSet("buckets") && !ctx.IsSet("users") && !ctx.IsSet("groups") && !ctx.IsSet("policies") && !ctx.IsSet("ilm-expiry-rules") && !ctx.IsSet("bucket") && !ctx.IsSet("user") && !ctx.IsSet("group") && !ctx.IsSet("policy") && !ctx.IsSet("ilm-expiry-rule") && !ctx.IsSet("all")) || ctx.IsSet("all") { opts.Buckets = true opts.Users = true opts.Groups = true opts.Policies = true opts.Metrics = true opts.ILMExpiryRules = true return } opts.Buckets = ctx.Bool("buckets") opts.Policies = ctx.Bool("policies") opts.Users = ctx.Bool("users") opts.Groups = ctx.Bool("groups") opts.ILMExpiryRules = ctx.Bool("ilm-expiry-rules") for _, name := range []string{"bucket", "user", "group", "policy", "ilm-expiry-rule"} { if ctx.IsSet(name) { opts.Entity = madmin.GetSREntityType(name) opts.EntityValue = ctx.String(name) break } } return } func mainAdminReplicationStatus(ctx *cli.Context) error { { // Check argument count argsNr := len(ctx.Args()) if argsNr != 1 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Need exactly one alias argument.") } groupStatus := ctx.IsSet("buckets") || ctx.IsSet("groups") || ctx.IsSet("users") || ctx.IsSet("policies") || ctx.IsSet("ilm-expiry-rules") indivStatus := ctx.IsSet("bucket") || ctx.IsSet("group") || ctx.IsSet("user") || ctx.IsSet("policy") || ctx.IsSet("ilm-expiry-rule") if groupStatus && indivStatus { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Cannot specify both (bucket|group|policy|user|ilm-expiry-rule) flag and one or more of buckets|groups|policies|users|ilm-expiry-rules) flag(s)") } setSlc := []bool{ctx.IsSet("bucket"), ctx.IsSet("user"), ctx.IsSet("group"), ctx.IsSet("policy"), ctx.IsSet("ilm-expiry-rule")} count := 0 for _, s := range setSlc { if s { count++ } } if count > 1 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Cannot specify more than one of --bucket, --policy, --user, --group, --ilm-expiry-rule flags at the same time") } } console.SetColor("UserMessage", color.New(color.FgGreen)) console.SetColor("WarningMessage", color.New(color.FgYellow)) for _, c := range colors { console.SetColor(fmt.Sprintf("Node%d", c), color.New(c, color.Bold)) } console.SetColor("Replicated", color.New(color.FgCyan)) console.SetColor("In-Queue", color.New(color.Bold, color.FgYellow)) console.SetColor("Avg", color.New(color.FgCyan)) console.SetColor("Peak", color.New(color.FgYellow)) console.SetColor("Value", color.New(color.FgWhite, color.Bold)) console.SetColor("Current", color.New(color.FgCyan)) console.SetColor("Uptime", color.New(color.Bold, color.FgWhite)) console.SetColor("UptimeStr", color.New(color.FgHiWhite)) console.SetColor("qStatusWarn", color.New(color.FgYellow, color.Bold)) console.SetColor("qStatusOK", color.New(color.FgGreen, color.Bold)) console.SetColor("online", color.New(color.FgGreen, color.Bold)) console.SetColor("offline", color.New(color.FgRed, color.Bold)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") opts := srStatusOpts(ctx) info, e := client.SRStatusInfo(globalContext, opts) fatalIf(probe.NewError(e).Trace(args...), "Unable to get cluster replication status") printMsg(srStatus{ SRStatusInfo: info, opts: opts, }) return nil } func syncStatus(mismatch, set bool) (string, string) { if !set { return "Entity", blankCell } if mismatch { return "Entity", crossTickCell } return "Entity", tickCell } minio-client-0.0~20250403/cmd/admin-replicate-update.go000066400000000000000000000163621477450377600224220ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "net/url" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminReplicateUpdateFlags = []cli.Flag{ cli.StringFlag{ Name: "deployment-id", Usage: "deployment id of the site, should be a unique value", }, cli.StringFlag{ Name: "endpoint", Usage: "endpoint for the site", }, cli.StringFlag{ Name: "mode", Usage: "change mode of replication for this target, valid values are ['sync', 'async'].", Value: "", }, cli.StringFlag{ Name: "sync", Usage: "enable synchronous replication for this target, valid values are ['enable', 'disable'].", Value: "disable", Hidden: true, // deprecated Jul 2023 }, cli.StringFlag{ Name: "bucket-bandwidth", Usage: "Set default bandwidth limit for bucket in bytes per second (K,B,G,T for metric and Ki,Bi,Gi,Ti for IEC units)", }, cli.BoolFlag{ Name: "disable-ilm-expiry-replication", Usage: "disable ILM expiry rules replication", }, cli.BoolFlag{ Name: "enable-ilm-expiry-replication", Usage: "enable ILM expiry rules replication", }, } var adminReplicateUpdateCmd = cli.Command{ Name: "update", Aliases: []string{"edit"}, HiddenAliases: true, Usage: "modify endpoint of site participating in site replication", Action: mainAdminReplicateUpdate, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, adminReplicateUpdateFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS --deployment-id [DEPLOYMENT-ID] --endpoint [NEW-ENDPOINT] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Edit a site endpoint participating in cluster-level replication: {{.Prompt}} {{.HelpName}} myminio --deployment-id c1758167-4426-454f-9aae-5c3dfdf6df64 --endpoint https://minio2:9000 2. Set default bucket bandwidth limit for replication from myminio to the peer cluster with deployment-id c1758167-4426-454f-9aae-5c3dfdf6df64 {{.Prompt}} {{.HelpName}} myminio --deployment-id c1758167-4426-454f-9aae-5c3dfdf6df64 --bucket-bandwidth "2G" 3. Disable replication of ILM expiry in cluster-level replication: {{.Prompt}} {{.HelpName}} myminio --disable-ilm-expiry-replication 4. Enable replication of ILM expiry in cluster-level replication: {{.Prompt}} {{.HelpName}} myminio --enable-ilm-expiry-replication `, } type updateSuccessMessage madmin.ReplicateEditStatus func (m updateSuccessMessage) JSON() string { bs, e := json.MarshalIndent(madmin.ReplicateEditStatus(m), "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(bs) } func (m updateSuccessMessage) String() string { v := madmin.ReplicateEditStatus(m) messages := []string{v.Status} if v.ErrDetail != "" { messages = append(messages, v.ErrDetail) } return console.Colorize("UserMessage", strings.Join(messages, "\n")) } func checkAdminReplicateUpdateSyntax(ctx *cli.Context) { // Check argument count argsNr := len(ctx.Args()) if argsNr < 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } if argsNr != 1 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Invalid arguments specified for edit command.") } } func mainAdminReplicateUpdate(ctx *cli.Context) error { checkAdminReplicateUpdateSyntax(ctx) console.SetColor("UserMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") if !ctx.IsSet("deployment-id") && !ctx.IsSet("disable-ilm-expiry-replication") && !ctx.IsSet("enable-ilm-expiry-replication") { fatalIf(errInvalidArgument(), "--deployment-id is a required flag") } if !ctx.IsSet("endpoint") && !ctx.IsSet("mode") && !ctx.IsSet("sync") && !ctx.IsSet("bucket-bandwidth") && !ctx.IsSet("disable-ilm-expiry-replication") && !ctx.IsSet("enable-ilm-expiry-replication") { fatalIf(errInvalidArgument(), "--endpoint, --mode, --bucket-bandwidth, --disable-ilm-expiry-replication or --enable-ilm-expiry-replication is a required flag") } if ctx.IsSet("mode") && ctx.IsSet("sync") { fatalIf(errInvalidArgument(), "either --sync or --mode flag should be specified") } if ctx.IsSet("disable-ilm-expiry-replication") && ctx.IsSet("enable-ilm-expiry-replication") { fatalIf(errInvalidArgument(), "either --disable-ilm-expiry-replication or --enable-ilm-expiry-replication flag should be specified") } if (ctx.IsSet("disable-ilm-expiry-replication") || ctx.IsSet("enable-ilm-expiry-replication")) && ctx.IsSet("deployment-id") { fatalIf(errInvalidArgument(), "--deployment-id should not be set with --disable-ilm-expiry-replication or --enable-ilm-expiry-replication") } var syncState string if ctx.IsSet("sync") { // for backward compatibility - deprecated Jul 2023 syncState = strings.ToLower(ctx.String("sync")) switch syncState { case "enable", "disable": default: fatalIf(errInvalidArgument().Trace(args...), "--sync can be either [enable|disable]") } } if ctx.IsSet("mode") { mode := strings.ToLower(ctx.String("mode")) switch mode { case "sync": syncState = "enable" case "async": syncState = "disable" default: fatalIf(errInvalidArgument().Trace(args...), "--mode can be either [sync|async]") } } var bwDefaults madmin.BucketBandwidth if ctx.IsSet("bucket-bandwidth") { bandwidthStr := ctx.String("bucket-bandwidth") bandwidth, e := getBandwidthInBytes(bandwidthStr) fatalIf(probe.NewError(e).Trace(bandwidthStr), "invalid bandwidth value") bwDefaults.Limit = bandwidth bwDefaults.IsSet = true } var ep string if ctx.IsSet("endpoint") { parsedURL := ctx.String("endpoint") u, e := url.Parse(parsedURL) if e != nil { fatalIf(errInvalidArgument().Trace(parsedURL), "Unsupported URL format %v", e) } ep = u.String() } var opts madmin.SREditOptions opts.DisableILMExpiryReplication = ctx.Bool("disable-ilm-expiry-replication") opts.EnableILMExpiryReplication = ctx.Bool("enable-ilm-expiry-replication") res, e := client.SiteReplicationEdit(globalContext, madmin.PeerInfo{ DeploymentID: ctx.String("deployment-id"), Endpoint: ep, SyncState: madmin.SyncStatus(syncState), DefaultBandwidth: bwDefaults, }, opts) fatalIf(probe.NewError(e).Trace(args...), "Unable to edit cluster replication site endpoint") printMsg(updateSuccessMessage(res)) return nil } minio-client-0.0~20250403/cmd/admin-replicate.go000066400000000000000000000026161477450377600211370ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminReplicateSubcommands = []cli.Command{ adminReplicateAddCmd, adminReplicateUpdateCmd, adminReplicateRemoveCmd, adminReplicateInfoCmd, adminReplicateStatusCmd, adminReplicateResyncCmd, } var adminReplicateCmd = cli.Command{ Name: "replicate", Usage: "manage MinIO site replication", Action: mainAdminReplicate, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminReplicateSubcommands, HideHelpCommand: true, } func mainAdminReplicate(ctx *cli.Context) error { commandNotFound(ctx, adminReplicateSubcommands) return nil } minio-client-0.0~20250403/cmd/admin-scanner-status.go000066400000000000000000000412321477450377600221360ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "bytes" "context" "errors" "fmt" "io" "os" "sort" "strconv" "strings" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/klauspost/compress/zstd" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" "github.com/olekukonko/tablewriter" ) var adminScannerInfoFlags = []cli.Flag{ cli.StringFlag{ Name: "nodes", Usage: "show only on matching servers, comma separate multiple", }, cli.IntFlag{ Name: "n", Usage: "number of requests to run before exiting. 0 for endless", Value: 0, }, cli.IntFlag{ Name: "interval", Usage: "interval between requests in seconds", Value: 3, }, cli.IntFlag{ Name: "max-paths", Usage: "maximum number of active paths to show. -1 for unlimited", Value: -1, }, cli.StringFlag{ Name: "in", Usage: "read previously saved json from file and replay", }, cli.StringFlag{ Name: "bucket", Usage: "show scan stats about a given bucket", }, } var adminScannerInfo = cli.Command{ Name: "status", Aliases: []string{"info"}, HiddenAliases: true, Usage: "summarize scanner events on MinIO server in real-time", Action: mainAdminScannerInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminScannerInfoFlags, globalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display current in-progress all scanner operations. {{.Prompt}} {{.HelpName}} myminio/ `, } // checkAdminTopAPISyntax - validate all the passed arguments func checkAdminScannerInfoSyntax(ctx *cli.Context) { if ctx.String("in") != "" { return } if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // bucketScanMsg container for content message structure type bucketScanMsg struct { Status string `json:"status"` Stats []madmin.BucketScanInfo `json:"stats"` } func (b bucketScanMsg) String() string { var sb strings.Builder sb.WriteString("\n") sort.Slice(b.Stats, func(i, j int) bool { return b.Stats[i].LastUpdate.Before(b.Stats[j].LastUpdate) }) pt := newPrettyTable(" | ", Field{"Pool", 5}, Field{"Set", 5}, Field{"LastUpdate", timeFieldMaxLen}, ) sb.WriteString(console.Colorize("Headers", pt.buildRow("Pool", "Set", "Last Update")) + "\n") now := time.Now().UTC() for i := range b.Stats { sb.WriteString( pt.buildRow( strconv.Itoa(b.Stats[i].Pool+1), strconv.Itoa(b.Stats[i].Set+1), humanize.RelTime(now, b.Stats[i].LastUpdate, "", "ago"), ) + "\n") } var ( earliestESScan time.Time // the earliest ES that completed a bucket scan latestESScan time.Time // the last ES that completed a bucket scan fullScan = true ) // Look for a bucket full scan inforation only if all // erasure sets completed at least 16 cycles for _, st := range b.Stats { if len(st.Completed) < 16 { fullScan = false break } if earliestESScan.IsZero() { // First stats earliestESScan = st.Completed[0] latestESScan = st.Completed[len(st.Completed)-1] continue } if earliestESScan.Before(st.Completed[0]) { earliestESScan = st.Completed[0] } if latestESScan.After(st.Completed[len(st.Completed)-1]) { latestESScan = st.Completed[len(st.Completed)-1] } } sb.WriteString("\n") if fullScan { took := latestESScan.Sub(earliestESScan) sb.WriteString( fmt.Sprintf( "%s %s (took %s)\n", console.Colorize("FullScan", "Full bucket scan: "), humanize.RelTime(now, latestESScan, "", "ago"), fmt.Sprintf("%dd%dh%dm", int(took.Hours()/24), int(took.Hours())%24, int(took.Minutes())%60)), ) } sb.WriteString("\n") return sb.String() } func (b bucketScanMsg) JSON() string { b.Status = "success" jsonMessageBytes, e := json.MarshalIndent(b, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func mainAdminScannerInfo(ctx *cli.Context) error { console.SetColor("Headers", color.New(color.Bold, color.FgHiGreen)) console.SetColor("FullScan", color.New(color.Bold, color.FgHiGreen)) checkAdminScannerInfoSyntax(ctx) ui := tea.NewProgram(initScannerMetricsUI(ctx.Int("max-paths"))) ctxt, cancel := context.WithCancel(globalContext) defer cancel() // Replay from file if inFile := ctx.String("in"); inFile != "" { go func() { if _, e := ui.Run(); e != nil { cancel() fatalIf(probe.NewError(e), "Unable to fetch scanner metrics") } }() f, e := os.Open(inFile) fatalIf(probe.NewError(e), "Unable to open input") defer f.Close() in := io.Reader(f) if strings.HasSuffix(inFile, ".zst") { zr, e := zstd.NewReader(in) fatalIf(probe.NewError(e), "Unable to open input") defer zr.Close() in = zr } sc := bufio.NewReader(in) var lastTime time.Time for { b, e := sc.ReadBytes('\n') if e == io.EOF { break } var metrics madmin.RealtimeMetrics e = json.Unmarshal(b, &metrics) if e != nil || metrics.Aggregated.Scanner == nil { continue } delay := metrics.Aggregated.Scanner.CollectedAt.Sub(lastTime) if !lastTime.IsZero() && delay > 0 { if delay > 3*time.Second { delay = 3 * time.Second } time.Sleep(delay) } ui.Send(metrics) lastTime = metrics.Aggregated.Scanner.CollectedAt } os.Exit(0) } // Create a new MinIO Admin Client aliasedURL := ctx.Args().Get(0) client, err := newAdminClient(aliasedURL) fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") if bucket := ctx.String("bucket"); bucket != "" { bucketStats, err := client.BucketScanInfo(globalContext, bucket) fatalIf(probe.NewError(err).Trace(aliasedURL), "Unable to get bucket stats.") printMsg(bucketScanMsg{Stats: bucketStats}) return nil } opts := madmin.MetricsOptions{ Type: madmin.MetricsScanner, N: ctx.Int("n"), Interval: time.Duration(ctx.Int("interval")) * time.Second, Hosts: strings.Split(ctx.String("nodes"), ","), ByHost: false, } if globalJSON { e := client.Metrics(ctxt, opts, func(metrics madmin.RealtimeMetrics) { printMsg(metricsMessage{RealtimeMetrics: metrics}) }) if e != nil && !errors.Is(e, context.Canceled) { fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch scanner metrics") } return nil } go func() { e := client.Metrics(ctxt, opts, func(metrics madmin.RealtimeMetrics) { ui.Send(metrics) }) if e != nil && !errors.Is(e, context.Canceled) { fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch scanner metrics") } }() if _, e := ui.Run(); e != nil { cancel() fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch scanner metrics") } return nil } type metricsMessage struct { madmin.RealtimeMetrics } func (s metricsMessage) JSON() string { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetIndent("", " ") enc.SetEscapeHTML(false) fatalIf(probe.NewError(enc.Encode(s)), "Unable to marshal into JSON.") return buf.String() } func (s metricsMessage) String() string { return s.JSON() } func initScannerMetricsUI(maxPaths int) *scannerMetricsUI { s := spinner.New() s.Spinner = spinner.Points s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) console.SetColor("metrics-duration", color.New(color.FgHiWhite)) console.SetColor("metrics-path", color.New(color.FgGreen)) console.SetColor("metrics-error", color.New(color.FgHiRed)) console.SetColor("metrics-title", color.New(color.FgCyan)) console.SetColor("metrics-top-title", color.New(color.FgHiCyan)) console.SetColor("metrics-number", color.New(color.FgHiWhite)) console.SetColor("metrics-zero", color.New(color.FgHiWhite)) console.SetColor("metrics-date", color.New(color.FgHiWhite)) return &scannerMetricsUI{ spinner: s, maxPaths: maxPaths, } } type scannerMetricsUI struct { current madmin.RealtimeMetrics spinner spinner.Model quitting bool maxPaths int } func (m *scannerMetricsUI) Init() tea.Cmd { return m.spinner.Tick } func (m *scannerMetricsUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.quitting { return m, tea.Quit } switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "esc", "ctrl+c": m.quitting = true return m, tea.Quit default: return m, nil } case madmin.RealtimeMetrics: m.current = msg if msg.Final { m.quitting = true return m, tea.Quit } return m, nil case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd } return m, nil } func (m *scannerMetricsUI) View() string { var s strings.Builder if !m.quitting { s.WriteString(fmt.Sprintf("%s %s\n", console.Colorize("metrics-top-title", "Scanner Activity:"), m.spinner.View())) } // Set table header - akin to k8s style // https://github.com/olekukonko/tablewriter#example-10---set-nowhitespace-and-tablepadding-option table := tablewriter.NewWriter(&s) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) writtenRows := 0 addRow := func(s string) { table.Append([]string{s}) writtenRows++ } _ = addRow addRowF := func(format string, vals ...interface{}) { s := fmt.Sprintf(format, vals...) table.Append([]string{s}) writtenRows++ } sc := m.current.Aggregated.Scanner if sc == nil { s.WriteString("(waiting for data)") return s.String() } title := metricsTitle ui := metricsUint64 addRow("") if sc.CurrentCycle == 0 && sc.CurrentStarted.IsZero() && sc.CyclesCompletedAt == nil { addRowF(" "+title("Scanning:")+" %d bucket(s)", sc.OngoingBuckets) } else { const wantCycles = 16 if len(sc.CyclesCompletedAt) < 2 { addRow("Last full scan time: Unknown (not enough data)") } else { addRow("Overall Statistics") addRow("------------------") sort.Slice(sc.CyclesCompletedAt, func(i, j int) bool { return sc.CyclesCompletedAt[i].After(sc.CyclesCompletedAt[j]) }) if len(sc.CyclesCompletedAt) >= wantCycles { sinceLast := sc.CyclesCompletedAt[0].Sub(sc.CyclesCompletedAt[wantCycles-1]) perMonth := float64(30*24*time.Hour) / float64(sinceLast) cycleTime := console.Colorize("metrics-number", fmt.Sprintf("%dd%dh%dm", int(sinceLast.Hours()/24), int(sinceLast.Hours())%24, int(sinceLast.Minutes())%60)) perms := console.Colorize("metrics-number", fmt.Sprintf("%.02f", perMonth)) addRowF(title("Last full scan time:")+" %s; Estimated %s/month", cycleTime, perms) } else { sinceLast := sc.CyclesCompletedAt[0].Sub(sc.CyclesCompletedAt[1]) * time.Duration(wantCycles) perMonth := float64(30*24*time.Hour) / float64(sinceLast) cycleTime := console.Colorize("metrics-number", fmt.Sprintf("%dd%dh%dm", int(sinceLast.Hours()/24), int(sinceLast.Hours())%24, int(sinceLast.Minutes())%60)) perms := console.Colorize("metrics-number", fmt.Sprintf("%.02f", perMonth)) addRowF(title("Est. full scan time:")+" %s; Estimated %s/month", cycleTime, perms) } } if sc.CurrentCycle > 0 { addRowF(title("Current cycle:")+" %s; Started: %v", ui(sc.CurrentCycle), console.Colorize("metrics-date", sc.CurrentStarted)) } else { addRowF(title("Current cycle:") + " (between cycles)") } } addRowF(title("Active drives:")+" %s", ui(uint64(len(sc.ActivePaths)))) getRate := func(x madmin.TimedAction) string { if x.AccTime > 0 { return fmt.Sprintf("; Rate: %v/day", ui(uint64(float64(24*time.Hour)/(float64(time.Minute)/float64(x.Count))))) } return "" } addRow("") addRow("Last Minute Statistics") addRow("----------------------") objs := uint64(0) x := sc.LastMinute.Actions["ScanObject"] { avg := x.Avg() addRowF(title("Objects Scanned:")+" %s objects; Avg: %v%s", ui(x.Count), metricsDuration(avg), getRate(x)) objs = x.Count } x = sc.LastMinute.Actions["ApplyVersion"] { avg := x.Avg() addRowF(title("Versions Scanned:")+" %s versions; Avg: %v%s", ui(x.Count), metricsDuration(avg), getRate(x)) } x = sc.LastMinute.Actions["HealCheck"] { avg := x.Avg() rate := "" if x.AccTime > 0 { rate = fmt.Sprintf("; Rate: %s/day", ui(uint64(float64(24*time.Hour)/(float64(time.Minute)/float64(x.Count))))) } addRowF(title("Versions Heal Checked:")+" %s versions; Avg: %v%s", ui(x.Count), metricsDuration(avg), rate) } x = sc.LastMinute.Actions["ReadMetadata"] addRowF(title("Read Metadata:")+" %s objects; Avg: %v, Size: %v bytes/obj", ui(x.Count), metricsDuration(x.Avg()), ui(x.AvgBytes())) x = sc.LastMinute.Actions["ILM"] addRowF(title("ILM checks:")+" %s versions; Avg: %v", ui(x.Count), metricsDuration(x.Avg())) x = sc.LastMinute.Actions["CheckReplication"] addRowF(title("Check Replication:")+" %s versions; Avg: %v", ui(x.Count), metricsDuration(x.Avg())) x = sc.LastMinute.Actions["TierObjSweep"] if x.Count > 0 { addRowF(title("Sweep Tiered:")+" %s versions; Avg: %v", ui(x.Count), metricsDuration(x.Avg())) } x = sc.LastMinute.Actions["CheckMissing"] addRowF(title("Verify Deleted:")+" %s folders; Avg: %v", ui(x.Count), metricsDuration(x.Avg())) x = sc.LastMinute.Actions["HealAbandonedObject"] if x.Count > 0 { addRowF(title(" Missing Objects:")+" %s objects healed; Avg: %v%s", ui(x.Count), metricsDuration(x.Avg()), getRate(x)) } x = sc.LastMinute.Actions["HealAbandonedVersion"] if x.Count > 0 { addRowF(title(" Missing Versions:")+" %s versions healed; Avg: %v%s; %v bytes/v", ui(x.Count), metricsDuration(x.Avg()), getRate(x), ui(x.AvgBytes())) } for k, x := range sc.LastMinute.ILM { const length = 17 k += ":" if len(k) < length { k += strings.Repeat(" ", length-len(k)) } addRowF(title("ILM, %s")+" %s actions; Avg: %v.", k, ui(x.Count), metricsDuration(x.Avg())) } x = sc.LastMinute.Actions["Yield"] { avg := fmt.Sprintf("%v", metricsDuration(x.Avg())) if objs > 0 { avg = console.Colorize("metrics-duration", fmt.Sprintf("%v/obj", metricsDuration(time.Duration(x.AccTime/objs)))) } addRowF(title("Yield:")+" %v total; Avg: %s", metricsDuration(time.Duration(x.AccTime)), avg) } if errs := m.current.Errors; len(errs) > 0 { addRow("------------------------------------------- Errors --------------------------------------------------") for _, s := range errs { addRow(console.Colorize("metrics-error", s)) } } if m.maxPaths != 0 && len(sc.ActivePaths) > 0 { addRow("------------------------------------- Currently Scanning Paths --------------------------------------") length := 100 if globalTermWidth > 5 { length = globalTermWidth } for i, s := range sc.ActivePaths { if i == m.maxPaths { break } if globalTermHeight > 5 && writtenRows >= globalTermHeight-5 { addRow(console.Colorize("metrics-path", fmt.Sprintf("( ... hiding %d more disk(s) .. )", len(sc.ActivePaths)-i))) break } if len(s) > length { s = s[:length-3] + "..." } s = strings.ReplaceAll(s, "\\", "/") addRow(console.Colorize("metrics-path", s)) } } table.Render() return s.String() } func metricsDuration(d time.Duration) string { if d == 0 { return console.Colorize("metrics-zero", "0ms") } if d > time.Millisecond { d = d.Round(time.Microsecond) } if d > time.Second { d = d.Round(time.Millisecond) } if d > time.Minute { d = d.Round(time.Second / 10) } return console.Colorize("metrics-duration", d) } func metricsUint64(v uint64) string { if v == 0 { return console.Colorize("metrics-zero", v) } return console.Colorize("metrics-number", v) } func metricsTitle(s string) string { return console.Colorize("metrics-title", s) } minio-client-0.0~20250403/cmd/admin-scanner-trace.go000066400000000000000000000132151477450377600217110ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminScannerTraceFlags = []cli.Flag{ cli.BoolFlag{ Name: "verbose, v", Usage: "print verbose trace", }, cli.StringSliceFlag{ Name: "funcname", Usage: "trace only matching func name (eg 'scanner.ScanObject')", }, cli.StringSliceFlag{ Name: "node", Usage: "trace only matching servers", }, cli.StringSliceFlag{ Name: "path", Usage: "trace only matching path", }, cli.BoolFlag{ Name: "filter-request", Usage: "trace calls only with request bytes greater than this threshold, use with filter-size", }, cli.BoolFlag{ Name: "filter-response", Usage: "trace calls only with response bytes greater than this threshold, use with filter-size", }, cli.BoolFlag{ Name: "response-duration", Usage: "trace calls only with response duration greater than this threshold (e.g. `5ms`)", }, cli.StringFlag{ Name: "filter-size", Usage: "filter size, use with filter (see UNITS)", }, } var adminScannerTraceCmd = cli.Command{ Name: "trace", Usage: "show trace for MinIO scanner operations", Action: mainAdminScannerTrace, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminScannerTraceFlags, globalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} UNITS --filter-size flags use with --filter-response or --filter-request accept human-readable case-insensitive number suffixes such as "k", "m", "g" and "t" referring to the metric units KB, MB, GB and TB respectively. Adding an "i" to these prefixes, uses the IEC units, so that "gi" refers to "gibibyte" or "GiB". A "b" at the end is also accepted. Without suffixes the unit is bytes. EXAMPLES: 1. Show scanner trace for MinIO server {{.Prompt}} {{.HelpName}} myminio 2. Show scanner trace for a specific path {{.Prompt}} {{.HelpName}} --path my-bucket/my-prefix/* myminio 3. Show trace for only ScanObject operations {{.Prompt}} {{.HelpName}} --funcname=scanner.ScanObject myminio 4. Avoid printing replication related S3 requests {{.Prompt}} {{.HelpName}} --request-header '!X-Minio-Source' myminio 5. Show trace only for ScanObject operations request bytes greater than 1MB {{.Prompt}} {{.HelpName}} --filter-request --filter-size 1MB myminio 6. Show trace only for ScanObject operations response bytes greater than 1MB {{.Prompt}} {{.HelpName}} --filter-response --filter-size 1MB myminio 7. Show trace only for requests operations duration greater than 5ms {{.Prompt}} {{.HelpName}} --response-duration 5ms myminio `, } func checkAdminScannerTraceSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } filterFlag := ctx.Bool("filter-request") || ctx.Bool("filter-response") if filterFlag && ctx.String("filter-size") == "" { // filter must use with filter-size flags showCommandHelpAndExit(ctx, 1) } } // mainAdminScannerTrace - the entry function of trace command func mainAdminScannerTrace(ctx *cli.Context) error { // Check for command syntax checkAdminScannerTraceSyntax(ctx) verbose := ctx.Bool("verbose") aliasedURL := ctx.Args().Get(0) console.SetColor("Stat", color.New(color.FgYellow)) console.SetColor("Request", color.New(color.FgCyan)) console.SetColor("Method", color.New(color.Bold, color.FgWhite)) console.SetColor("Host", color.New(color.Bold, color.FgGreen)) console.SetColor("FuncName", color.New(color.Bold, color.FgGreen)) console.SetColor("ReqHeaderKey", color.New(color.Bold, color.FgWhite)) console.SetColor("RespHeaderKey", color.New(color.Bold, color.FgCyan)) console.SetColor("HeaderValue", color.New(color.FgWhite)) console.SetColor("RespStatus", color.New(color.Bold, color.FgYellow)) console.SetColor("ErrStatus", color.New(color.Bold, color.FgRed)) console.SetColor("Response", color.New(color.FgGreen)) console.SetColor("Body", color.New(color.FgYellow)) for _, c := range colors { console.SetColor(fmt.Sprintf("Node%d", c), color.New(c)) } // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } ctxt, cancel := context.WithCancel(globalContext) defer cancel() opts, e := tracingOpts(ctx, []string{"scanner"}) fatalIf(probe.NewError(e), "Unable to start tracing") mopts := matchingOpts(ctx) // Start listening on all trace activity. traceCh := client.ServiceTrace(ctxt, opts) for traceInfo := range traceCh { if traceInfo.Err != nil { fatalIf(probe.NewError(traceInfo.Err), "Unable to listen to http trace") } if mopts.matches(traceInfo) { printTrace(verbose, traceInfo) } } return nil } minio-client-0.0~20250403/cmd/admin-scanner.go000066400000000000000000000025221477450377600206140ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminScannerSubcommands = []cli.Command{ adminScannerInfo, adminScannerTraceCmd, } var adminScannerCmd = cli.Command{ Name: "scanner", Usage: "provide MinIO scanner info", Action: mainAdminScanner, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminScannerSubcommands, HideHelpCommand: true, } // mainAdminScanner is the handle for "mc admin scanner" command. func mainAdminScanner(ctx *cli.Context) error { commandNotFound(ctx, adminScannerSubcommands) return nil } minio-client-0.0~20250403/cmd/admin-service-freeze.go000066400000000000000000000060721477450377600221050ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminServiceFreezeCmd = cli.Command{ Name: "freeze", Usage: "freeze S3 API calls on MinIO cluster", Action: mainAdminServiceFreeze, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, Hidden: true, // this command is hidden on purpose, please do not enable it. CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Freeze all S3 API calls on MinIO server at 'myminio/'. {{.Prompt}} {{.HelpName}} myminio/ `, } // serviceFreezeCommand is container for service freeze command success and failure messages. type serviceFreezeCommand struct { Status string `json:"status"` ServerURL string `json:"serverURL"` } // String colorized service freeze command message. func (s serviceFreezeCommand) String() string { return console.Colorize("ServiceFreeze", "Freeze command successfully sent to `"+s.ServerURL+"`.") } // JSON jsonified service freeze command message. func (s serviceFreezeCommand) JSON() string { serviceFreezeJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(serviceFreezeJSONBytes) } // checkAdminServiceFreezeSyntax - validate all the passed arguments func checkAdminServiceFreezeSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainAdminServiceFreeze(ctx *cli.Context) error { // Validate serivce freeze syntax. checkAdminServiceFreezeSyntax(ctx) // Set color. console.SetColor("ServiceFreeze", color.New(color.FgGreen, color.Bold)) console.SetColor("FailedServiceFreeze", color.New(color.FgRed, color.Bold)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") // Freeze the specified MinIO server fatalIf(probe.NewError(client.ServiceFreezeV2(globalContext)), "Unable to freeze the server.") // Success.. printMsg(serviceFreezeCommand{Status: "success", ServerURL: aliasedURL}) return nil } minio-client-0.0~20250403/cmd/admin-service-restart.go000066400000000000000000000222031477450377600223030ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "strings" "sync/atomic" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var serviceRestartFlag = []cli.Flag{ cli.BoolFlag{ Name: "dry-run", Usage: "do not attempt a restart, however verify the peer status", }, cli.BoolFlag{ Name: "wait, w", Usage: "wait for background initializations to complete", }, } var adminServiceRestartCmd = cli.Command{ Name: "restart", Usage: "restart a MinIO cluster", Action: mainAdminServiceRestart, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(serviceRestartFlag, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Restart MinIO server represented by its alias 'play'. {{.Prompt}} {{.HelpName}} play/ `, } type serviceRestartUI struct { current atomic.Value tbl *console.Table meter spinner.Model quitting bool } func (m *serviceRestartUI) add(msg serviceRestartMessage) { m.current.Store(msg) } func (m *serviceRestartUI) Init() tea.Cmd { return m.meter.Tick } func (m *serviceRestartUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.quitting { return m, tea.Quit } switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "esc", "ctrl+c": m.quitting = true return m, tea.Quit default: return m, nil } case spinner.TickMsg: var cmd tea.Cmd m.meter, cmd = m.meter.Update(msg) return m, cmd } return m, nil } func (m *serviceRestartUI) View() string { var s strings.Builder msgI := m.current.Load() if msgI == nil { s.WriteString("(waiting for data)") return s.String() } s.WriteString(fmt.Sprintf("Service status: %s ", m.meter.View())) msg := msgI.(serviceRestartMessage) state := msg.State switch state { case restarting: // Actually still restarting, no response yet from the server. s.WriteString("[RESTARTING]\n") case waiting: // Waiting on background initializations such as IAM and bucket metadata s.WriteString(console.Colorize("ServiceInitializing", "[WAITING]")) s.WriteString("\n") case done: m.quitting = true // Finished restarting and optionally waiting for the background initializations to complete. s.WriteString(console.Colorize("ServiceRestarted", "[DONE]")) s.WriteString("\n") var ( totalNodes = len(msg.Result.Results) totalOfflineNodes int totalHungNodes int ) for _, peerRes := range msg.Result.Results { if peerRes.Err != "" { totalOfflineNodes++ } else if len(peerRes.WaitingDrives) > 0 { totalHungNodes++ } } s.WriteString("Summary:\n") var cellText [][]string cellText = append(cellText, []string{ "Servers:", fmt.Sprintf("%d online, %d offline, %d hung", totalNodes-(totalOfflineNodes+totalHungNodes), totalOfflineNodes, totalHungNodes), }) cellText = append(cellText, []string{ "Restart Time:", msg.RestartDuration.String(), }) if msg.WaitingDuration > 0 { cellText = append(cellText, []string{ "Background Init Time:", msg.WaitingDuration.String(), }) } fatalIf(probe.NewError(m.tbl.PopulateTable(&s, cellText)), "unable to populate the table") } return s.String() } func initServiceRestartUI(rowCount int, currentCh chan serviceRestartMessage) *serviceRestartUI { var printColors []*color.Color for i := 0; i < rowCount; i++ { printColors = append(printColors, getPrintCol(colGreen)) } tbl := console.NewTable(printColors, []bool{false, false}, 4) meter := spinner.New() meter.Spinner = spinner.Meter meter.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) svcUI := &serviceRestartUI{tbl: tbl, meter: meter} go func() { for msg := range currentCh { if msg.Status != "" { svcUI.add(msg) } } }() return svcUI } const ( restarting = iota waiting done ) // serviceRestartMessage is container for service restart command success and failure messages. type serviceRestartMessage struct { Status string `json:"status"` ServerURL string `json:"serverURL"` Result madmin.ServiceActionResult `json:"result"` RestartDuration time.Duration `json:"restartDuration"` WaitingDuration time.Duration `json:"waitingDuration"` TimeTaken time.Duration `json:"timeTaken"` // deprecated use "restartDuration" instead. State int `json:"state"` } func (s serviceRestartMessage) String() string { return s.JSON() } // JSON jsonified service restart command message. func (s serviceRestartMessage) JSON() string { serviceRestartJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(serviceRestartJSONBytes) } // checkAdminServiceRestartSyntax - validate all the passed arguments func checkAdminServiceRestartSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainAdminServiceRestart(ctx *cli.Context) error { // Validate serivce restart syntax. checkAdminServiceRestartSyntax(ctx) ctxt, cancel := context.WithCancel(globalContext) defer cancel() // Set color. console.SetColor("ServiceOffline", color.New(color.FgRed, color.Bold)) console.SetColor("ServiceInitializing", color.New(color.FgYellow, color.Bold)) console.SetColor("ServiceRestarted", color.New(color.FgGreen, color.Bold)) console.SetColor("FailedServiceRestart", color.New(color.FgRed, color.Bold)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") rowCount := 2 toWait := ctx.Bool("wait") if toWait { rowCount = 3 } ch := make(chan serviceRestartMessage, 1) svcUI := initServiceRestartUI(rowCount, ch) go func() { t := time.Now() // Restart the specified MinIO server result, e := client.ServiceAction(ctxt, madmin.ServiceActionOpts{ Action: madmin.ServiceActionRestart, DryRun: ctx.Bool("dry-run"), }) if e != nil { // Attempt an older API server might be old // nolint:staticcheck // we need this fallback e = client.ServiceRestart(ctxt) } fatalIf(probe.NewError(e), "Unable to restart the server.") timeTaken := time.Since(t) restart := restarting if !toWait { restart = done } ch <- serviceRestartMessage{ Status: "success", ServerURL: aliasedURL, Result: result, RestartDuration: timeTaken, TimeTaken: timeTaken, State: restart, } if toWait { sleepInterval := 500 * time.Millisecond go func() { defer close(ch) wt := time.Now() // Start pinging the service until it is ready anonClient, err := newAnonymousClient(aliasedURL) fatalIf(err.Trace(aliasedURL), "unable to initialize anonymous client for`"+aliasedURL+"`.") for { healthCtx, healthCancel := context.WithTimeout(ctxt, 2*time.Second) // Fetch the health status of the specified MinIO server healthResult, healthErr := anonClient.Healthy(healthCtx, madmin.HealthOpts{}) healthCancel() switch { case healthErr == nil && healthResult.Healthy: ch <- serviceRestartMessage{ Status: "success", ServerURL: aliasedURL, Result: result, RestartDuration: timeTaken, TimeTaken: timeTaken, WaitingDuration: time.Since(wt), State: done, } return } ch <- serviceRestartMessage{ Status: "success", ServerURL: aliasedURL, Result: result, WaitingDuration: time.Since(wt), RestartDuration: timeTaken, TimeTaken: timeTaken, State: waiting, } time.Sleep(sleepInterval) } }() } else { close(ch) } }() if !globalJSON { ui := tea.NewProgram(svcUI) if _, e := ui.Run(); e != nil { cancel() fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to initialize service restart UI") } } else { for msg := range ch { printMsg(msg) if msg.State == done { break } } } return nil } minio-client-0.0~20250403/cmd/admin-service-stop.go000066400000000000000000000056311477450377600216120ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminServiceStopCmd = cli.Command{ Name: "stop", Usage: "stop a MinIO cluster", Action: mainAdminServiceStop, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, Hidden: true, // this command is hidden on purpose, please do not enable it. CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Stop MinIO server represented by its alias 'play'. {{.Prompt}} {{.HelpName}} play/ `, } // serviceStopMessage is container for make bucket success and failure messages. type serviceStopMessage struct { Status string `json:"status"` ServerURL string `json:"serverURL"` } // String colorized make bucket message. func (s serviceStopMessage) String() string { return console.Colorize("ServiceStop", "Stopped `"+s.ServerURL+"` successfully.") } // JSON jsonified make bucket message. func (s serviceStopMessage) JSON() string { serviceStopJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(serviceStopJSONBytes) } // checkAdminServiceStopSyntax - validate all the passed arguments func checkAdminServiceStopSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainAdminServiceStop(ctx *cli.Context) error { // Validate serivce stop syntax. checkAdminServiceStopSyntax(ctx) // Set color. console.SetColor("ServiceStop", color.New(color.FgGreen, color.Bold)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") // Stop the specified MinIO server fatalIf(probe.NewError(client.ServiceStopV2(globalContext)), "Unable to stop the server.") // Success.. printMsg(serviceStopMessage{Status: "success", ServerURL: aliasedURL}) return nil } minio-client-0.0~20250403/cmd/admin-service-unfreeze.go000066400000000000000000000064511477450377600224510ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminServiceUnfreezeCmd = cli.Command{ Name: "unfreeze", Usage: "unfreeze S3 API calls on MinIO cluster", Action: mainAdminServiceUnfreeze, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Unfreeze all S3 API calls on MinIO server at 'myminio/'. {{.Prompt}} {{.HelpName}} myminio/ `, } // serviceUnfreezeCommand is container for service unfreeze command success and failure messages. type serviceUnfreezeCommand struct { Status string `json:"status"` ServerURL string `json:"serverURL"` } // String colorized service unfreeze command message. func (s serviceUnfreezeCommand) String() string { return console.Colorize("ServiceUnfreeze", "Unfreeze command successfully sent to `"+s.ServerURL+"`.") } // JSON jsonified service unfreeze command message. func (s serviceUnfreezeCommand) JSON() string { serviceUnfreezeJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(serviceUnfreezeJSONBytes) } // checkAdminServiceUnfreezeSyntax - validate all the passed arguments func checkAdminServiceUnfreezeSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainAdminServiceUnfreeze(ctx *cli.Context) error { // Validate service unfreeze syntax. checkAdminServiceUnfreezeSyntax(ctx) // Set color. console.SetColor("ServiceUnfreeze", color.New(color.FgGreen, color.Bold)) console.SetColor("FailedServiceUnfreeze", color.New(color.FgRed, color.Bold)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") ctxt, cancel := context.WithCancel(globalContext) defer cancel() // Unfreeze the specified MinIO server e := client.ServiceUnfreezeV2(ctxt) if e != nil { // Attempt an older API server might be old // nolint:staticcheck // we need this fallback e = client.ServiceUnfreeze(ctxt) } // Unfreeze the specified MinIO server fatalIf(probe.NewError(e), "Unable to unfreeze the server.") // Success.. printMsg(serviceUnfreezeCommand{Status: "success", ServerURL: aliasedURL}) return nil } minio-client-0.0~20250403/cmd/admin-service.go000066400000000000000000000027121477450377600206240ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminServiceSubcommands = []cli.Command{ adminServiceRestartCmd, adminServiceStopCmd, adminServiceUnfreezeCmd, adminServiceFreezeCmd, } var adminServiceCmd = cli.Command{ Name: "service", Usage: "restart or unfreeze a MinIO cluster", Action: mainAdminService, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, Subcommands: adminServiceSubcommands, } // mainAdmin is the handle for "mc admin service" command. func mainAdminService(ctx *cli.Context) error { commandNotFound(ctx, adminServiceSubcommands) return nil // Sub-commands like "status", "restart" have their own main. } minio-client-0.0~20250403/cmd/admin-subnet-health.go000066400000000000000000000037611477450377600217340ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strings" "github.com/minio/cli" "github.com/minio/minio-go/v7/pkg/set" ) var adminSubnetHealthCmd = cli.Command{ Name: "health", Usage: "generate MinIO health report for SUBNET", OnUsageError: onUsageError, Action: mainSubnetHealth, Before: setGlobalsFromContext, Hidden: true, Flags: supportDiagFlags, // No need to append globalFlags as top level command would add them CustomHelpTemplate: "This command is deprecated and will be removed in a future release. Use 'mc support diag' instead.\n", } func mainSubnetHealth(ctx *cli.Context) error { boolValSet := set.CreateStringSet("true", "false") newCmd := []string{"mc support diag"} newCmd = append(newCmd, ctx.Args()...) for _, flg := range ctx.Command.Flags { flgName := flg.GetName() if !ctx.IsSet(flgName) { continue } // replace the deprecated --offline with --airgap if flgName == "offline" { flgName = "airgap" } flgStr := "--" + flgName flgVal := ctx.String(flgName) if !boolValSet.Contains(flgVal) { flgStr = fmt.Sprintf("%s \"%s\"", flgStr, flgVal) } newCmd = append(newCmd, flgStr) } deprecatedError(strings.Join(newCmd, " ")) return nil } minio-client-0.0~20250403/cmd/admin-subnet-register.go000066400000000000000000000023511477450377600223050ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminSubnetRegisterCmd = cli.Command{ Name: "register", Usage: "Register the MinIO Cluster with SUBNET", OnUsageError: onUsageError, Action: mainAdminRegister, Before: setGlobalsFromContext, Hidden: true, CustomHelpTemplate: "Please use 'mc support register'", } func mainAdminRegister(_ *cli.Context) error { deprecatedError("mc support register") return nil } minio-client-0.0~20250403/cmd/admin-subnet.go000066400000000000000000000027111477450377600204630ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var subnetHealthSubcommands = []cli.Command{ adminSubnetHealthCmd, adminSubnetRegisterCmd, } var adminSubnetCmd = cli.Command{ Name: "subnet", Usage: "Subnet related commands", Action: mainAdminSubnet, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: subnetHealthSubcommands, Hidden: true, } // mainAdminSubnet is the handle for "mc admin subnet" command. func mainAdminSubnet(_ *cli.Context) error { deprecatedError("mc support") return nil // Sub-commands like "health", "register" have their own main. } func adminHealthCmd() cli.Command { cmd := adminSubnetHealthCmd cmd.Hidden = true return cmd } minio-client-0.0~20250403/cmd/admin-tier-deprecated.go000066400000000000000000000136401477450377600222270ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminTierDepCmds = []cli.Command{ adminTierDepInfoCmd, adminTierDepListCmd, adminTierDepAddCmd, adminTierDepEditCmd, adminTierDepVerifyCmd, adminTierDepRmCmd, } var ( adminTierDepInfoCmd = cli.Command{ Name: "info", Usage: "display tier statistics", Action: mainAdminTierInfo, Hidden: true, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS [NAME] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Prints per-tier statistics of all remote tier targets configured on 'myminio': {{.Prompt}} {{.HelpName}} myminio 2. Print per-tier statistics of given tier name 'MINIOTIER-1': {{.Prompt}} {{.HelpName}} myminio MINIOTIER-1 `, } adminTierDepListCmd = cli.Command{ Name: "ls", Usage: "lists configured remote tier targets", Action: mainAdminTierList, Hidden: true, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List remote tier targets configured on 'myminio': {{.Prompt}} {{.HelpName}} myminio `, } adminTierDepAddCmd = cli.Command{ Name: "add", Usage: "add a new remote tier target", Action: mainAdminTierAdd, Hidden: true, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, adminTierAddFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TYPE ALIAS NAME [FLAGS] TYPE: Type of the cloud storage backend to add. Supported values are minio, s3, azure and gcs. NAME: Name of the remote tier target. e.g WARM-TIER FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Configure a new remote tier which transitions objects to a bucket in a MinIO deployment: {{.Prompt}} {{.HelpName}} minio myminio WARM-MINIO-TIER --endpoint https://warm-minio.com \ --access-key ACCESSKEY --secret-key SECRETKEY --bucket mybucket --prefix myprefix/ 2. Configure a new remote tier which transitions objects to a bucket in Azure Blob Storage: {{.Prompt}} {{.HelpName}} azure myminio AZTIER --account-name ACCOUNT-NAME --account-key ACCOUNT-KEY \ --bucket myazurebucket --prefix myazureprefix/ 3. Configure a new remote tier which transitions objects to a bucket in AWS S3 with STANDARD storage class: {{.Prompt}} {{.HelpName}} s3 myminio S3TIER --endpoint https://s3.amazonaws.com \ --access-key ACCESSKEY --secret-key SECRETKEY --bucket mys3bucket --prefix mys3prefix/ \ --storage-class "STANDARD" --region us-west-2 4. Configure a new remote tier which transitions objects to a bucket in Google Cloud Storage: {{.Prompt}} {{.HelpName}} gcs myminio GCSTIER --credentials-file /path/to/credentials.json \ --bucket mygcsbucket --prefix mygcsprefix/ `, } adminTierDepEditCmd = cli.Command{ Name: "edit", Usage: "update an existing remote tier configuration", Action: mainAdminTierEdit, Hidden: true, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, adminTierEditFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS NAME NAME: Name of remote tier. e.g WARM-TIER FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Update credentials for an existing Azure Blob Storage remote tier: {{.Prompt}} {{.HelpName}} myminio AZTIER --account-key ACCOUNT-KEY 2. Update credentials for an existing AWS S3 compatible remote tier: {{.Prompt}} {{.HelpName}} myminio S3TIER --access-key ACCESS-KEY --secret-key SECRET-KEY 3. Update credentials for an existing Google Cloud Storage remote tier: {{.Prompt}} {{.HelpName}} myminio GCSTIER --credentials-file /path/to/credentials.json `, } adminTierDepVerifyCmd = cli.Command{ Name: "verify", Usage: "verifies if remote tier configuration is valid", Action: mainAdminTierVerify, Hidden: true, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET NAME NAME: Name of remote tier target. e.g WARM-TIER FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Verify if a tier config is valid. {{.Prompt}} {{.HelpName}} myminio WARM-TIER `, } adminTierDepRmCmd = cli.Command{ Name: "rm", Usage: "removes an empty remote tier", Action: mainAdminTierRm, Hidden: true, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS NAME NAME: Name of remote tier target. e.g WARM-TIER FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove an empty tier by name 'WARM-TIER': {{.Prompt}} {{.HelpName}} myminio WARM-TIER `, } ) minio-client-0.0~20250403/cmd/admin-tier-main.go000066400000000000000000000023761477450377600210570ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminTierCmd = cli.Command{ Name: "tier", Usage: "manage remote tier targets for ILM transition", Action: mainAdminTier, Before: setGlobalsFromContext, Flags: globalFlags, Hidden: true, HideHelpCommand: true, Subcommands: adminTierDepCmds, } // mainAdminTier is the handle for "mc admin tier" command. func mainAdminTier(_ *cli.Context) error { deprecatedError("mc ilm tier") return nil } minio-client-0.0~20250403/cmd/admin-top-api.go000066400000000000000000000033531477450377600205370ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var adminTopAPIFlags = []cli.Flag{ cli.StringSliceFlag{ Name: "name", Usage: "summarize current calls for matching API name", }, cli.StringSliceFlag{ Name: "path", Usage: "summarize current API calls only on matching path", }, cli.StringSliceFlag{ Name: "node", Usage: "summarize current API calls only on matching servers", }, cli.BoolFlag{ Name: "errors, e", Usage: "summarize current API calls throwing only errors", }, } var adminTopAPICmd = cli.Command{ Name: "api", Usage: "summarize API events on MinIO server in real-time", Action: mainAdminTopAPI, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminTopAPIFlags, globalFlags...), Hidden: true, HideHelpCommand: true, CustomHelpTemplate: `Please use 'mc support top api' `, } func mainAdminTopAPI(_ *cli.Context) error { deprecatedError("mc support top api") return nil } minio-client-0.0~20250403/cmd/admin-top-locks.go000066400000000000000000000026661477450377600211070ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var topLocksFlag = []cli.Flag{ cli.BoolFlag{ Name: "stale", Usage: "list stale locks", }, cli.IntFlag{ Name: "count", Usage: "number of top locks", Hidden: true, Value: 10, }, } var adminTopLocksCmd = cli.Command{ Name: "locks", Usage: "get a list of the 10 oldest locks on a MinIO cluster.", Before: setGlobalsFromContext, Action: mainAdminTopLocks, OnUsageError: onUsageError, Flags: append(globalFlags, topLocksFlag...), CustomHelpTemplate: `Please use 'mc support top locks' `, } func mainAdminTopLocks(_ *cli.Context) error { deprecatedError("mc support top locks") return nil } minio-client-0.0~20250403/cmd/admin-top.go000066400000000000000000000025461477450377600177730ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminTopSubcommands = []cli.Command{ adminTopAPICmd, adminTopLocksCmd, } var adminTopCmd = cli.Command{ Name: "top", Usage: "provide top like statistics for MinIO", Action: mainAdminTop, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminTopSubcommands, HideHelpCommand: true, } // mainAdminTop is the handle for "mc admin top" command. func mainAdminTop(ctx *cli.Context) error { commandNotFound(ctx, adminTopSubcommands) return nil // Sub-commands like "locks" have their own main. } minio-client-0.0~20250403/cmd/admin-trace.go000066400000000000000000000762201477450377600202670ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "bytes" "context" "fmt" "hash/fnv" "io" "net/http" "net/url" "os" "path" "sort" "strings" "sync" "time" tea "github.com/charmbracelet/bubbletea" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/klauspost/compress/zstd" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminTraceFlags = []cli.Flag{ cli.BoolFlag{ Name: "verbose, v", Usage: "print verbose trace", }, cli.BoolFlag{ Name: "all, a", Usage: "trace all call types", }, cli.StringSliceFlag{ Name: "call", Usage: "trace only matching call types. See CALL TYPES below for list. (default: s3)", }, cli.IntSliceFlag{ Name: "status-code", Usage: "trace only matching status code", }, cli.StringSliceFlag{ Name: "method", Usage: "trace only matching HTTP method", }, cli.StringSliceFlag{ Name: "funcname", Usage: "trace only matching func name", }, cli.StringSliceFlag{ Name: "path", Usage: "trace only matching path", }, cli.StringSliceFlag{ Name: "node", Usage: "trace only matching servers", }, cli.StringSliceFlag{ Name: "request-header", Usage: "trace only matching request headers", }, cli.StringSliceFlag{ Name: "request-query", Usage: "trace only matching request queries", }, cli.BoolFlag{ Name: "errors, e", Usage: "trace only failed requests", }, cli.BoolFlag{ Name: "stats", Usage: "print statistical summary of all the traced calls", }, cli.IntFlag{ Name: "stats-n", Usage: "maximum number of stat entries", Value: 20, Hidden: true, }, cli.BoolFlag{ Name: "filter-request", Usage: "trace calls only with request bytes greater than this threshold, use with filter-size", }, cli.BoolFlag{ Name: "filter-response", Usage: "trace calls only with response bytes greater than this threshold, use with filter-size", }, cli.DurationFlag{ Name: "response-duration", Usage: "trace calls only with response duration greater than this threshold (e.g. `5ms`)", }, cli.StringFlag{ Name: "filter-size", Usage: "filter size, use with filter (see UNITS)", }, cli.StringFlag{ Name: "in", Usage: "read previously saved json from file and replay", }, } // traceCallTypes contains all call types and flags to apply when selected. var traceCallTypes = map[string]func(o *madmin.ServiceTraceOpts) (help string){ "storage": func(o *madmin.ServiceTraceOpts) string { o.Storage = true; return "Trace Storage calls" }, "internal": func(o *madmin.ServiceTraceOpts) string { o.Internal = true; return "Trace Internal RPC calls" }, "s3": func(o *madmin.ServiceTraceOpts) string { o.S3 = true; return "Trace S3 API calls" }, "os": func(o *madmin.ServiceTraceOpts) string { o.OS = true; return "Trace Operating System calls" }, "scanner": func(o *madmin.ServiceTraceOpts) string { o.Scanner = true; return "Trace Scanner calls" }, "bootstrap": func(o *madmin.ServiceTraceOpts) string { o.Bootstrap = true; return "Trace Bootstrap operations" }, "ilm": func(o *madmin.ServiceTraceOpts) string { o.ILM = true; return "Trace ILM operations" }, "ftp": func(o *madmin.ServiceTraceOpts) string { o.FTP = true; return "Trace FTP operations" }, "healing": func(o *madmin.ServiceTraceOpts) string { o.Healing = true return "Trace Healing operations (alias: heal)" }, "batch-replication": func(o *madmin.ServiceTraceOpts) string { o.BatchReplication = true return "Trace Batch Replication (alias: brep)" }, "batch-keyrotation": func(o *madmin.ServiceTraceOpts) string { o.BatchKeyRotation = true return "Trace Batch KeyRotation (alias: brot)" }, "batch-expiration": func(o *madmin.ServiceTraceOpts) string { o.BatchExpire = true return "Trace Batch Expiration (alias: bexp)" }, "decommission": func(o *madmin.ServiceTraceOpts) string { o.Decommission = true return "Trace Decommission operations (alias: decom)" }, "rebalance": func(o *madmin.ServiceTraceOpts) string { o.Rebalance = true return "Trace Server Pool Rebalancing operations" }, "replication-resync": func(o *madmin.ServiceTraceOpts) string { o.ReplicationResync = true return "Trace Replication Resync operations (alias: resync)" }, } // traceCallTypes contains aliases (short versions) of var traceCallTypeAliases = map[string]func(o *madmin.ServiceTraceOpts) string{ "heal": traceCallTypes["healing"], "decom": traceCallTypes["decommission"], "resync": traceCallTypes["replication-resync"], "brep": traceCallTypes["batch-replication"], "brot": traceCallTypes["batch-keyrotation"], "bexp": traceCallTypes["batch-expiration"], } func traceCallsHelp() string { var help []string o := madmin.ServiceTraceOpts{} const padkeyLen = 19 for k, fn := range traceCallTypes { pad := "" if len(k) < padkeyLen { pad = strings.Repeat(" ", padkeyLen-len(k)) } help = append(help, fmt.Sprintf(" %s: %s%s", k, pad, fn(&o))) } sort.Strings(help) return strings.Join(help, "\n") } var adminTraceCmd = cli.Command{ Name: "trace", Usage: "Show HTTP call trace for all incoming and internode on MinIO", Action: mainAdminTrace, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminTraceFlags, globalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} CALL TYPES: ` + traceCallsHelp() + ` UNITS --filter-size flags use with --filter-response or --filter-request accept human-readable case-insensitive number suffixes such as "k", "m", "g" and "t" referring to the metric units KB, MB, GB and TB respectively. Adding an "i" to these prefixes, uses the IEC units, so that "gi" refers to "gibibyte" or "GiB". A "b" at the end is also accepted. Without suffixes the unit is bytes. EXAMPLES: 1. Show verbose console trace for MinIO server {{.Prompt}} {{.HelpName}} -v -a myminio 2. Show trace only for failed requests for MinIO server {{.Prompt}} {{.HelpName}} -v -e myminio 3. Show verbose console trace for requests with '503' status code {{.Prompt}} {{.HelpName}} -v --status-code 503 myminio 4. Show console trace for a specific path {{.Prompt}} {{.HelpName}} --path my-bucket/my-prefix/* myminio 5. Show console trace for requests with '404' and '503' status code {{.Prompt}} {{.HelpName}} --status-code 404 --status-code 503 myminio 6. Show trace only for requests bytes greater than 1MB {{.Prompt}} {{.HelpName}} --filter-request --filter-size 1MB myminio 7. Show trace only for response bytes greater than 1MB {{.Prompt}} {{.HelpName}} --filter-response --filter-size 1MB myminio 8. Show trace only for requests operations duration greater than 5ms {{.Prompt}} {{.HelpName}} --response-duration 5ms myminio `, } const traceTimeFormat = "2006-01-02T15:04:05.000" var colors = []color.Attribute{color.FgCyan, color.FgWhite, color.FgYellow, color.FgGreen} func checkAdminTraceSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 && len(ctx.String("in")) == 0 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } filterFlag := ctx.Bool("filter-request") || ctx.Bool("filter-response") if filterFlag && ctx.String("filter-size") == "" { // filter must use with filter-size flags showCommandHelpAndExit(ctx, 1) } if ctx.Bool("all") && len(ctx.StringSlice("call")) > 0 { fatalIf(errDummy().Trace(), "You cannot specify both --all and --call flags at the same time.") } } func printTrace(verbose bool, traceInfo madmin.ServiceTraceInfo) { if verbose { printMsg(traceMessage{ServiceTraceInfo: traceInfo}) } else { printMsg(shortTrace(traceInfo)) } } type matchString struct { val string reverse bool } type matchOpts struct { statusCodes []int methods []string funcNames []string apiPaths []string nodes []string reqHeaders []matchString reqQueries []matchString requestSize uint64 responseSize uint64 } func (opts matchOpts) matches(traceInfo madmin.ServiceTraceInfo) bool { // Filter request path if passed by the user if len(opts.apiPaths) > 0 { matched := false for _, apiPath := range opts.apiPaths { if pathMatch(path.Join("/", apiPath), traceInfo.Trace.Path) { matched = true break } } if !matched { return false } } // Filter response status codes if passed by the user if len(opts.statusCodes) > 0 { matched := false for _, code := range opts.statusCodes { if traceInfo.Trace.HTTP != nil && traceInfo.Trace.HTTP.RespInfo.StatusCode == code { matched = true break } } if !matched { return false } } // Filter request method if passed by the user if len(opts.methods) > 0 { matched := false for _, method := range opts.methods { if traceInfo.Trace.HTTP != nil && traceInfo.Trace.HTTP.ReqInfo.Method == method { matched = true break } } if !matched { return false } } if len(opts.funcNames) > 0 { matched := false // Filter request function handler names if passed by the user. for _, funcName := range opts.funcNames { if nameMatch(funcName, traceInfo.Trace.FuncName) { matched = true break } } if !matched { return false } } if len(opts.nodes) > 0 { matched := false // Filter request by node if passed by the user. for _, node := range opts.nodes { if nameMatch(node, traceInfo.Trace.NodeName) { matched = true break } } if !matched { return false } } if len(opts.reqHeaders) > 0 && traceInfo.Trace.HTTP != nil { matched := false for _, hdr := range opts.reqHeaders { headerFound := false for traceHdr, traceVals := range traceInfo.Trace.HTTP.ReqInfo.Headers { for _, traceVal := range traceVals { if patternMatch(hdr.val, traceHdr+": "+traceVal) { headerFound = true goto exitFindingHeader } } } exitFindingHeader: if !hdr.reverse && headerFound || hdr.reverse && !headerFound { matched = true goto exitMatchingHeader } } exitMatchingHeader: if !matched { return false } } if len(opts.reqQueries) > 0 && traceInfo.Trace.HTTP != nil { matched := false for _, qry := range opts.reqQueries { queryFound := false v, err := url.ParseQuery(traceInfo.Trace.HTTP.ReqInfo.RawQuery) if err != nil { continue } for traceQuery, traceVals := range v { for _, traceVal := range traceVals { if patternMatch(qry.val, traceQuery+"="+traceVal) { queryFound = true goto exitFindingQuery } } } exitFindingQuery: if !qry.reverse && queryFound || qry.reverse && !queryFound { matched = true goto exitMatchingQuery } } exitMatchingQuery: if !matched { return false } } if opts.requestSize > 0 && traceInfo.Trace.HTTP.CallStats.InputBytes < int(opts.requestSize) { return false } if opts.responseSize > 0 && traceInfo.Trace.HTTP.CallStats.OutputBytes < int(opts.responseSize) { return false } return true } func matchingOpts(ctx *cli.Context) (opts matchOpts) { opts.statusCodes = ctx.IntSlice("status-code") opts.methods = ctx.StringSlice("method") opts.funcNames = ctx.StringSlice("funcname") opts.apiPaths = ctx.StringSlice("path") opts.nodes = ctx.StringSlice("node") for _, s := range ctx.StringSlice("request-header") { opts.reqHeaders = append(opts.reqHeaders, matchString{ reverse: strings.HasPrefix(s, "!"), val: strings.TrimPrefix(s, "!"), }) } for _, s := range ctx.StringSlice("request-query") { opts.reqQueries = append(opts.reqQueries, matchString{ reverse: strings.HasPrefix(s, "!"), val: strings.TrimPrefix(s, "!"), }) } var e error var requestSize, responseSize uint64 if ctx.Bool("filter-request") && ctx.String("filter-size") != "" { requestSize, e = humanize.ParseBytes(ctx.String("filter-size")) fatalIf(probe.NewError(e).Trace(ctx.String("filter-size")), "Unable to parse input bytes.") } if ctx.Bool("filter-response") && ctx.String("filter-size") != "" { responseSize, e = humanize.ParseBytes(ctx.String("filter-size")) fatalIf(probe.NewError(e).Trace(ctx.String("filter-size")), "Unable to parse input bytes.") } opts.requestSize = requestSize opts.responseSize = responseSize return } // Calculate tracing options for command line flags func tracingOpts(ctx *cli.Context, apis []string) (opts madmin.ServiceTraceOpts, e error) { opts.Threshold = ctx.Duration("response-duration") opts.OnlyErrors = ctx.Bool("errors") if ctx.Bool("all") { for _, fn := range traceCallTypes { fn(&opts) } } if len(apis) == 0 { // If api flag is not specified, then we will // trace only S3 requests by default. opts.S3 = true return } for _, api := range apis { for _, api := range strings.Split(api, ",") { fn, ok := traceCallTypes[api] if !ok { fn, ok = traceCallTypeAliases[api] } if !ok { return madmin.ServiceTraceOpts{}, fmt.Errorf("unknown call name: `%s`", api) } fn(&opts) } } return } // mainAdminTrace - the entry function of trace command func mainAdminTrace(ctx *cli.Context) error { // Check for command syntax checkAdminTraceSyntax(ctx) verbose := ctx.Bool("verbose") stats := ctx.Bool("stats") console.SetColor("Stat", color.New(color.FgYellow)) console.SetColor("Request", color.New(color.FgCyan)) console.SetColor("Method", color.New(color.Bold, color.FgWhite)) console.SetColor("Host", color.New(color.Bold, color.FgGreen)) console.SetColor("FuncName", color.New(color.Bold, color.FgGreen)) console.SetColor("ReqHeaderKey", color.New(color.Bold, color.FgWhite)) console.SetColor("RespHeaderKey", color.New(color.Bold, color.FgCyan)) console.SetColor("HeaderValue", color.New(color.FgWhite)) console.SetColor("RespStatus", color.New(color.Bold, color.FgYellow)) console.SetColor("ErrStatus", color.New(color.Bold, color.FgRed)) console.SetColor("Response", color.New(color.FgGreen)) console.SetColor("Extra", color.New(color.FgBlue)) console.SetColor("Body", color.New(color.FgYellow)) for _, c := range colors { console.SetColor(fmt.Sprintf("Node%d", c), color.New(c)) } var traceCh <-chan madmin.ServiceTraceInfo ctxt, cancel := context.WithCancel(globalContext) defer cancel() if inFile := ctx.String("in"); inFile != "" { stats = true ch := make(chan madmin.ServiceTraceInfo, 1000) traceCh = ch go func() { f, e := os.Open(inFile) fatalIf(probe.NewError(e), "Unable to open input") defer f.Close() in := io.Reader(f) if strings.HasSuffix(inFile, ".zst") { zr, e := zstd.NewReader(in) fatalIf(probe.NewError(e), "Unable to open input") defer zr.Close() in = zr } sc := bufio.NewReader(in) for ctxt.Err() == nil { b, e := sc.ReadBytes('\n') if e == io.EOF { break } var t shortTraceMsg e = json.Unmarshal(b, &t) if e != nil || t.Type == "Bootstrap" { // Ignore bootstrap, since their times skews averages. continue } ch <- madmin.ServiceTraceInfo{ Trace: madmin.TraceInfo{ TraceType: t.trcType, // TODO: Grab from string, once we can. NodeName: t.Host, FuncName: t.FuncName, Time: t.Time, Path: t.Path, Duration: t.Duration, Bytes: t.Size, Message: t.StatusMsg, Error: t.Error, Custom: t.Extra, HTTP: nil, HealResult: nil, }, Err: nil, } } close(ch) select {} }() } else { // Create a new MinIO Admin Client aliasedURL := ctx.Args().Get(0) client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } opts, e := tracingOpts(ctx, ctx.StringSlice("call")) fatalIf(probe.NewError(e), "Unable to start tracing") // Start listening on all trace activity. traceCh = client.ServiceTrace(ctxt, opts) } mopts := matchingOpts(ctx) if stats { filteredTraces := make(chan madmin.ServiceTraceInfo, 1) ui := tea.NewProgram(initTraceStatsUI(ctx.Bool("all"), ctx.Int("stats-n"), filteredTraces)) var te error go func() { for t := range traceCh { if t.Err != nil { te = t.Err ui.Kill() return } if mopts.matches(t) { filteredTraces <- t } } ui.Send(tea.Quit()) }() if _, e := ui.Run(); e != nil { cancel() if te != nil { e = te } aliasedURL := ctx.Args().Get(0) fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch http trace statistics") } return nil } for traceInfo := range traceCh { if traceInfo.Err != nil { fatalIf(probe.NewError(traceInfo.Err), "Unable to listen to http trace") } if mopts.matches(traceInfo) { printTrace(verbose, traceInfo) } } return nil } // Short trace record type shortTraceMsg struct { Status string `json:"status"` Host string `json:"host"` Time time.Time `json:"time"` Client string `json:"client"` CallStats *callStats `json:"callStats,omitempty"` Duration time.Duration `json:"duration"` TTFB time.Duration `json:"timeToFirstByte"` FuncName string `json:"api"` Path string `json:"path"` Query string `json:"query"` StatusCode int `json:"statusCode"` StatusMsg string `json:"statusMsg"` Type string `json:"type"` Size int64 `json:"size,omitempty"` Error string `json:"error"` Extra map[string]string `json:"extra"` trcType madmin.TraceType } type traceMessage struct { Status string `json:"status"` madmin.ServiceTraceInfo } type requestInfo struct { Time time.Time `json:"time"` Proto string `json:"proto"` Method string `json:"method"` Path string `json:"path,omitempty"` RawQuery string `json:"rawQuery,omitempty"` Headers map[string]string `json:"headers,omitempty"` Body string `json:"body,omitempty"` } type responseInfo struct { Time time.Time `json:"time"` Headers map[string]string `json:"headers,omitempty"` Body string `json:"body,omitempty"` StatusCode int `json:"statusCode,omitempty"` } type callStats struct { Rx int `json:"rx"` Tx int `json:"tx"` Duration time.Duration `json:"duration"` TTFB time.Duration `json:"timeToFirstByte"` } type verboseTrace struct { Type string `json:"type"` NodeName string `json:"host"` FuncName string `json:"api"` Time time.Time `json:"time"` Duration time.Duration `json:"duration"` Path string `json:"path"` Error string `json:"error,omitempty"` Message string `json:"message,omitempty"` RequestInfo *requestInfo `json:"request,omitempty"` ResponseInfo *responseInfo `json:"response,omitempty"` CallStats *callStats `json:"callStats,omitempty"` HealResult *madmin.HealResultItem `json:"healResult,omitempty"` Extra map[string]string `json:"extra,omitempty"` trcType madmin.TraceType } // return a struct with minimal trace info. func shortTrace(ti madmin.ServiceTraceInfo) shortTraceMsg { s := shortTraceMsg{} t := ti.Trace s.trcType = t.TraceType s.Type = t.TraceType.String() s.FuncName = t.FuncName s.Time = t.Time s.Path = t.Path s.Error = t.Error s.Host = t.NodeName s.Duration = t.Duration s.StatusMsg = t.Message s.Extra = t.Custom s.Size = t.Bytes switch t.TraceType { case madmin.TraceS3, madmin.TraceInternal: s.Query = t.HTTP.ReqInfo.RawQuery s.StatusCode = t.HTTP.RespInfo.StatusCode s.StatusMsg = http.StatusText(t.HTTP.RespInfo.StatusCode) s.Client = t.HTTP.ReqInfo.Client s.CallStats = &callStats{} s.CallStats.Duration = t.Duration s.CallStats.Rx = t.HTTP.CallStats.InputBytes s.CallStats.Tx = t.HTTP.CallStats.OutputBytes s.CallStats.TTFB = t.HTTP.CallStats.TimeToFirstByte } return s } func (s shortTraceMsg) JSON() string { s.Status = "success" buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetIndent("", " ") // Disable escaping special chars to display XML tags correctly enc.SetEscapeHTML(false) fatalIf(probe.NewError(enc.Encode(s)), "Unable to marshal into JSON.") return buf.String() } func (s shortTraceMsg) String() string { var hostStr string b := &strings.Builder{} if s.Host != "" { hostStr = colorizedNodeName(s.Host) } fmt.Fprintf(b, "%s ", s.Time.Local().Format(traceTimeFormat)) switch s.trcType { case madmin.TraceS3, madmin.TraceInternal: case madmin.TraceBootstrap: fmt.Fprintf(b, "[%s] %s %s %s", console.Colorize("RespStatus", strings.ToUpper(s.trcType.String())), console.Colorize("FuncName", s.FuncName), hostStr, s.StatusMsg, ) return b.String() default: if s.Error != "" { fmt.Fprintf(b, "[%s] %s %s %s err='%s' %2s", console.Colorize("RespStatus", strings.ToUpper(s.trcType.String())), console.Colorize("FuncName", s.FuncName), hostStr, s.Path, console.Colorize("ErrStatus", s.Error), console.Colorize("HeaderValue", s.Duration)) } else { sz := "" if s.Size != 0 { sz = fmt.Sprintf(" %s", humanize.IBytes(uint64(s.Size))) } fmt.Fprintf(b, "[%s] %s %s %s %2s%s", console.Colorize("RespStatus", strings.ToUpper(s.trcType.String())), console.Colorize("FuncName", s.FuncName), hostStr, s.Path, console.Colorize("HeaderValue", s.Duration), sz) } return b.String() } statusStr := fmt.Sprintf("%d %s", s.StatusCode, s.StatusMsg) if s.StatusCode >= http.StatusBadRequest { statusStr = console.Colorize("ErrStatus", statusStr) } else { statusStr = console.Colorize("RespStatus", statusStr) } fmt.Fprintf(b, "[%s] %s ", statusStr, console.Colorize("FuncName", s.FuncName)) fmt.Fprintf(b, "%s%s", hostStr, s.Path) if s.Query != "" { fmt.Fprintf(b, "?%s ", s.Query) } fmt.Fprintf(b, " %s ", s.Client) spaces := 15 - len(s.Client) fmt.Fprintf(b, "%*s", spaces, " ") fmt.Fprint(b, console.Colorize("HeaderValue", fmt.Sprintf(" %2s", s.CallStats.Duration.Round(time.Microsecond)))) spaces = 12 - len(fmt.Sprintf("%2s", s.CallStats.Duration.Round(time.Microsecond))) fmt.Fprintf(b, "%*s", spaces, " ") fmt.Fprint(b, console.Colorize("Stat", " ⇣ ")) fmt.Fprint(b, console.Colorize("HeaderValue", fmt.Sprintf(" %2s", s.CallStats.TTFB.Round(time.Nanosecond)))) spaces = 10 - len(fmt.Sprintf("%2s", s.CallStats.TTFB.Round(time.Nanosecond))) fmt.Fprintf(b, "%*s", spaces, " ") fmt.Fprint(b, console.Colorize("Stat", " ↑ ")) fmt.Fprint(b, console.Colorize("HeaderValue", humanize.IBytes(uint64(s.CallStats.Rx)))) fmt.Fprint(b, console.Colorize("Stat", " ↓ ")) fmt.Fprint(b, console.Colorize("HeaderValue", humanize.IBytes(uint64(s.CallStats.Tx)))) return b.String() } // colorize node name func colorizedNodeName(nodeName string) string { nodeHash := fnv.New32a() nodeHash.Write([]byte(nodeName)) nHashSum := nodeHash.Sum32() idx := nHashSum % uint32(len(colors)) return console.Colorize(fmt.Sprintf("Node%d", colors[idx]), nodeName) } func (t traceMessage) JSON() string { trc := verboseTrace{ trcType: t.Trace.TraceType, Type: t.Trace.TraceType.String(), NodeName: t.Trace.NodeName, FuncName: t.Trace.FuncName, Time: t.Trace.Time, Duration: t.Trace.Duration, Path: t.Trace.Path, Error: t.Trace.Error, HealResult: t.Trace.HealResult, Message: t.Trace.Message, Extra: t.Trace.Custom, } if t.Trace.HTTP != nil { rq := t.Trace.HTTP.ReqInfo rs := t.Trace.HTTP.RespInfo var ( rqHdrs = make(map[string]string) rspHdrs = make(map[string]string) ) for k, v := range rq.Headers { rqHdrs[k] = strings.Join(v, " ") } for k, v := range rs.Headers { rspHdrs[k] = strings.Join(v, " ") } trc.RequestInfo = &requestInfo{ Time: rq.Time, Proto: rq.Proto, Method: rq.Method, Path: rq.Path, RawQuery: rq.RawQuery, Body: string(rq.Body), Headers: rqHdrs, } trc.ResponseInfo = &responseInfo{ Time: rs.Time, Body: string(rs.Body), Headers: rspHdrs, StatusCode: rs.StatusCode, } trc.CallStats = &callStats{ Duration: t.Trace.Duration, Rx: t.Trace.HTTP.CallStats.InputBytes, Tx: t.Trace.HTTP.CallStats.OutputBytes, TTFB: t.Trace.HTTP.CallStats.TimeToFirstByte, } } buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetIndent("", " ") // Disable escaping special chars to display XML tags correctly enc.SetEscapeHTML(false) fatalIf(probe.NewError(enc.Encode(trc)), "Unable to marshal into JSON.") // strip off extra newline added by json encoder return strings.TrimSuffix(buf.String(), "\n") } func (t traceMessage) String() string { var nodeNameStr string b := &strings.Builder{} trc := t.Trace if trc.NodeName != "" { nodeNameStr = fmt.Sprintf("%s ", colorizedNodeName(trc.NodeName)) } extra := "" if len(t.Trace.Custom) > 0 { for k, v := range t.Trace.Custom { extra = fmt.Sprintf("%s %s=%s", extra, k, v) } extra = console.Colorize("Extra", extra) } switch trc.TraceType { case madmin.TraceS3, madmin.TraceInternal: if trc.HTTP == nil { return "" } case madmin.TraceBootstrap: fmt.Fprintf(b, "%s %s [%s] %s%s", nodeNameStr, console.Colorize("Request", fmt.Sprintf("[%s %s]", strings.ToUpper(trc.TraceType.String()), trc.FuncName)), trc.Time.Local().Format(traceTimeFormat), trc.Message, extra) return b.String() default: sz := "" if trc.Bytes != 0 { sz = fmt.Sprintf(" %s", humanize.IBytes(uint64(trc.Bytes))) } if trc.Error != "" { fmt.Fprintf(b, "%s %s [%s] %s%s err='%s' %s%s", nodeNameStr, console.Colorize("Request", fmt.Sprintf("[%s %s]", strings.ToUpper(trc.TraceType.String()), trc.FuncName)), trc.Time.Local().Format(traceTimeFormat), trc.Path, extra, console.Colorize("ErrStatus", trc.Error), trc.Duration, sz) } else { fmt.Fprintf(b, "%s %s [%s] %s%s %s%s", nodeNameStr, console.Colorize("Request", fmt.Sprintf("[%s %s]", strings.ToUpper(trc.TraceType.String()), trc.FuncName)), trc.Time.Local().Format(traceTimeFormat), trc.Path, extra, trc.Duration, sz) } return b.String() } ri := trc.HTTP.ReqInfo rs := trc.HTTP.RespInfo fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("Request", fmt.Sprintf("[REQUEST %s] ", trc.FuncName))) fmt.Fprintf(b, "[%s] %s\n", ri.Time.Local().Format(traceTimeFormat), console.Colorize("Host", fmt.Sprintf("[Client IP: %s]", ri.Client))) fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("Method", fmt.Sprintf("%s %s", ri.Method, ri.Path))) if ri.RawQuery != "" { fmt.Fprintf(b, "?%s", ri.RawQuery) } fmt.Fprint(b, "\n") fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("Method", fmt.Sprintf("Proto: %s\n", ri.Proto))) host, ok := ri.Headers["Host"] if ok { delete(ri.Headers, "Host") } hostStr := strings.Join(host, "") fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("Host", fmt.Sprintf("Host: %s\n", hostStr))) for k, v := range ri.Headers { fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("ReqHeaderKey", fmt.Sprintf("%s: ", k))+console.Colorize("HeaderValue", fmt.Sprintf("%s\n", strings.Join(v, "")))) } fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("Body", fmt.Sprintf("%s\n", string(ri.Body)))) fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("Response", "[RESPONSE] ")) fmt.Fprintf(b, "[%s] ", rs.Time.Local().Format(traceTimeFormat)) fmt.Fprint(b, console.Colorize("Stat", fmt.Sprintf("[ Duration %2s TTFB %2s ↑ %s ↓ %s ]\n", trc.Duration.Round(time.Microsecond), trc.HTTP.CallStats.TimeToFirstByte.Round(time.Nanosecond), humanize.IBytes(uint64(trc.HTTP.CallStats.InputBytes)), humanize.IBytes(uint64(trc.HTTP.CallStats.OutputBytes))))) statusStr := console.Colorize("RespStatus", fmt.Sprintf("%d %s", rs.StatusCode, http.StatusText(rs.StatusCode))) if rs.StatusCode != http.StatusOK { statusStr = console.Colorize("ErrStatus", fmt.Sprintf("%d %s", rs.StatusCode, http.StatusText(rs.StatusCode))) } fmt.Fprintf(b, "%s%s\n", nodeNameStr, statusStr) for k, v := range rs.Headers { fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("RespHeaderKey", fmt.Sprintf("%s: ", k))+console.Colorize("HeaderValue", fmt.Sprintf("%s\n", strings.Join(v, ",")))) } if len(extra) > 0 { fmt.Fprintf(b, "%s%s\n", nodeNameStr, extra) } fmt.Fprintf(b, "%s%s\n", nodeNameStr, console.Colorize("Body", string(rs.Body))) fmt.Fprint(b, nodeNameStr) return b.String() } type statItem struct { Name string Count int `json:"count"` Duration time.Duration `json:"duration"` Errors int `json:"errors,omitempty"` CallStatsCount int `json:"callStatsCount,omitempty"` CallStats callStats `json:"callStats,omitempty"` TTFB time.Duration `json:"ttfb,omitempty"` MaxTTFB time.Duration `json:"maxTTFB,omitempty"` MaxDur time.Duration `json:"maxDuration"` MinDur time.Duration `json:"minDuration"` Size int64 `json:"size"` } type statTrace struct { Calls map[string]statItem `json:"calls"` Oldest time.Time Latest time.Time mu sync.Mutex } func (s *statTrace) JSON() string { s.mu.Lock() defer s.mu.Unlock() buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetIndent("", " ") // Disable escaping special chars to display XML tags correctly enc.SetEscapeHTML(false) fatalIf(probe.NewError(enc.Encode(s)), "Unable to marshal into JSON.") // strip off extra newline added by json encoder return strings.TrimSuffix(buf.String(), "\n") } func (s *statTrace) String() string { return "" } func (s *statTrace) add(t madmin.ServiceTraceInfo) { id := t.Trace.FuncName s.mu.Lock() defer s.mu.Unlock() if t.Trace.TraceType != madmin.TraceBootstrap { // We can't use bootstrap to find start/end ended := t.Trace.Time.Add(t.Trace.Duration) if s.Oldest.IsZero() { s.Oldest = ended } if ended.After(s.Latest) { s.Latest = ended } } got := s.Calls[id] if got.Name == "" { got.Name = id } if got.MaxDur < t.Trace.Duration { got.MaxDur = t.Trace.Duration } if got.MinDur <= 0 { got.MinDur = t.Trace.Duration } if got.MinDur > t.Trace.Duration { got.MinDur = t.Trace.Duration } got.Count++ got.Duration += t.Trace.Duration if t.Trace.Error != "" { got.Errors++ } got.Size += t.Trace.Bytes if t.Trace.HTTP != nil { got.CallStatsCount++ got.CallStats.Rx += t.Trace.HTTP.CallStats.InputBytes got.CallStats.Tx += t.Trace.HTTP.CallStats.OutputBytes got.TTFB += t.Trace.HTTP.CallStats.TimeToFirstByte if got.MaxTTFB < t.Trace.HTTP.CallStats.TimeToFirstByte { got.MaxTTFB = t.Trace.HTTP.CallStats.TimeToFirstByte } } s.Calls[id] = got } minio-client-0.0~20250403/cmd/admin-update.go000066400000000000000000000115051477450377600204460ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "fmt" "os" "strings" "github.com/fatih/color" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminUpdateFlags = []cli.Flag{ cli.BoolFlag{ Name: "yes, y", Usage: "Confirms the server update", }, } var adminServerUpdateCmd = cli.Command{ Name: "update", Usage: "update all MinIO servers", Action: mainAdminServerUpdate, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminUpdateFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Update MinIO server represented by its alias 'play'. {{.Prompt}} {{.HelpName}} play/ 2. Update all MinIO servers in a distributed setup, represented by its alias 'mydist'. {{.Prompt}} {{.HelpName}} mydist/ `, } // serverUpdateMessage is container for ServerUpdate success and failure messages. type serverUpdateMessage struct { Status string `json:"status"` ServerURL string `json:"serverURL"` ServerUpdateStatus madmin.ServerUpdateStatusV2 `json:"serverUpdateStatus"` } // String colorized serverUpdate message. func (s serverUpdateMessage) String() string { var rows []table.Row for _, peerRes := range s.ServerUpdateStatus.Results { errStr := fmt.Sprintf("upgraded server from %s to %s: %s", peerRes.CurrentVersion, peerRes.UpdatedVersion, tickCell) if peerRes.Err != "" { errStr = peerRes.Err } else if len(peerRes.WaitingDrives) > 0 { errStr = fmt.Sprintf("%d drives are hung, process was upgraded. However OS reboot is recommended.", len(peerRes.WaitingDrives)) } rows = append(rows, table.Row{peerRes.Host, errStr}) } t := table.NewWriter() var s1 strings.Builder s1.WriteString("Server update request sent successfully `" + s.ServerURL + "`\n") t.SetOutputMirror(&s1) t.SetColumnConfigs([]table.ColumnConfig{{Align: text.AlignCenter}}) t.AppendHeader(table.Row{"Host", "Status"}) t.AppendRows(rows) t.SetStyle(table.StyleLight) t.Render() return console.Colorize("ServiceRestart", s1.String()) } // JSON jsonified server update message. func (s serverUpdateMessage) JSON() string { serverUpdateJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(serverUpdateJSONBytes) } // checkAdminServerUpdateSyntax - validate all the passed arguments func checkAdminServerUpdateSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainAdminServerUpdate(ctx *cli.Context) error { // Validate serivce update syntax. checkAdminServerUpdateSyntax(ctx) // Set color. console.SetColor("ServerUpdate", color.New(color.FgGreen, color.Bold)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") updateURL := args.Get(1) autoConfirm := ctx.Bool("yes") if isTerminal() && !autoConfirm { fmt.Printf("You are about to upgrade *MinIO Server*, please confirm [y/N]: ") answer, e := bufio.NewReader(os.Stdin).ReadString('\n') fatalIf(probe.NewError(e), "Unable to parse user input.") answer = strings.TrimSpace(answer) if answer = strings.ToLower(answer); answer != "y" && answer != "yes" { fmt.Println("Upgrade aborted!") return nil } } // Update the specified MinIO server, optionally also // with the provided update URL. us, e := client.ServerUpdateV2(globalContext, madmin.ServerUpdateOpts{ DryRun: ctx.Bool("dry-run"), UpdateURL: updateURL, }) fatalIf(probe.NewError(e), "Unable to update the server.") // Success.. printMsg(serverUpdateMessage{ Status: "success", ServerURL: aliasedURL, ServerUpdateStatus: us, }) return nil } minio-client-0.0~20250403/cmd/admin-user-add.go000066400000000000000000000142361477450377600206740ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "fmt" "os" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" "golang.org/x/term" ) var adminUserAddCmd = cli.Command{ Name: "add", Usage: "add a new user", Action: mainAdminUserAdd, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET ACCESSKEY SECRETKEY ACCESSKEY: Also called as username. SECRETKEY: Also called as password. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Add a new user 'foobar' to MinIO server. {{.DisableHistory}} {{.Prompt}} {{.HelpName}} myminio foobar foo12345 {{.EnableHistory}} 2. Add a new user 'foobar' to MinIO server, prompting for keys. {{.Prompt}} {{.HelpName}} myminio Enter Access Key: foobar Enter Secret Key: foobar12345 3. Add a new user 'foobar' to MinIO server using piped keys. {{.DisableHistory}} {{.Prompt}} echo -e "foobar\nfoobar12345" | {{.HelpName}} myminio {{.EnableHistory}} 4. Add a new user 'foobar' to MinIO server, then attach IAM policy "writeonly". {{.Prompt}} {{.HelpName}} myminio foobar foo12345 {{.Prompt}} mc admin policy attach myminio writeonly --user foobar `, } // checkAdminUserAddSyntax - validate all the passed arguments func checkAdminUserAddSyntax(ctx *cli.Context) { argsNr := len(ctx.Args()) if argsNr > 3 || argsNr < 1 { showCommandHelpAndExit(ctx, 1) } } // userGroup container for content message structure type userGroup struct { Name string `json:"name,omitempty"` Policies []string `json:"policies,omitempty"` } // userMessage container for content message structure type userMessage struct { op string Status string `json:"status"` // TODO: remove this? AccessKey string `json:"accessKey,omitempty"` SecretKey string `json:"secretKey,omitempty"` PolicyName string `json:"policyName,omitempty"` UserStatus string `json:"userStatus,omitempty"` MemberOf []userGroup `json:"memberOf,omitempty"` Authentication string `json:"authentication,omitempty"` } func (u userMessage) String() string { switch u.op { case "list": userFieldMaxLen := 9 accessFieldMaxLen := 20 policyFieldMaxLen := 20 // Create a new pretty table with cols configuration return newPrettyTable(" ", Field{"UserStatus", userFieldMaxLen}, Field{"AccessKey", accessFieldMaxLen}, Field{"PolicyName", policyFieldMaxLen}, ).buildRow(u.UserStatus, u.AccessKey, u.PolicyName) case "info": memberOf := []string{} for _, group := range u.MemberOf { memberOf = append(memberOf, group.Name) } lines := []string{ fmt.Sprintf("AccessKey: %s", u.AccessKey), fmt.Sprintf("Status: %s", u.UserStatus), fmt.Sprintf("PolicyName: %s", u.PolicyName), fmt.Sprintf("MemberOf: %s", memberOf), } if u.Authentication != "" { lines = append(lines, fmt.Sprintf("Authentication: %s", u.Authentication)) } return console.Colorize("UserMessage", strings.Join(lines, "\n")) case "remove": return console.Colorize("UserMessage", "Removed user `"+u.AccessKey+"` successfully.") case "disable": return console.Colorize("UserMessage", "Disabled user `"+u.AccessKey+"` successfully.") case "enable": return console.Colorize("UserMessage", "Enabled user `"+u.AccessKey+"` successfully.") case "add": return console.Colorize("UserMessage", "Added user `"+u.AccessKey+"` successfully.") } return "" } func (u userMessage) JSON() string { u.Status = "success" jsonMessageBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // fetchUserKeys - returns the access and secret key func fetchUserKeys(args cli.Args) (string, string) { accessKey := "" secretKey := "" console.SetColor(cred, color.New(color.FgYellow, color.Italic)) isTerminal := term.IsTerminal(int(os.Stdin.Fd())) reader := bufio.NewReader(os.Stdin) argCount := len(args) if argCount == 1 { if isTerminal { fmt.Printf("%s", console.Colorize(cred, "Enter Access Key: ")) } value, _, _ := reader.ReadLine() accessKey = string(value) } else { accessKey = args.Get(1) } if argCount == 1 || argCount == 2 { if isTerminal { fmt.Printf("%s", console.Colorize(cred, "Enter Secret Key: ")) bytePassword, _ := term.ReadPassword(int(os.Stdin.Fd())) fmt.Printf("\n") secretKey = string(bytePassword) } else { value, _, _ := reader.ReadLine() secretKey = string(value) } } else { secretKey = args.Get(2) } return accessKey, secretKey } // mainAdminUserAdd is the handle for "mc admin user add" command. func mainAdminUserAdd(ctx *cli.Context) error { checkAdminUserAddSyntax(ctx) console.SetColor("UserMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) accessKey, secretKey := fetchUserKeys(args) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") fatalIf(probe.NewError(client.AddUser(globalContext, accessKey, secretKey)).Trace(args...), "Unable to add new user") printMsg(userMessage{ op: ctx.Command.Name, AccessKey: accessKey, SecretKey: secretKey, UserStatus: "enabled", }) return nil } minio-client-0.0~20250403/cmd/admin-user-disable.go000066400000000000000000000044301477450377600215420ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminUserDisableCmd = cli.Command{ Name: "disable", Usage: "disable user", Action: mainAdminUserDisable, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET USERNAME FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Disable a user 'foobar' on MinIO server. {{.Prompt}} {{.HelpName}} myminio foobar `, } // checkAdminUserDisableSyntax - validate all the passed arguments func checkAdminUserDisableSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminUserDisable is the handle for "mc admin user disable" command. func mainAdminUserDisable(ctx *cli.Context) error { checkAdminUserDisableSyntax(ctx) console.SetColor("UserMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") e := client.SetUserStatus(globalContext, args.Get(1), madmin.AccountDisabled) fatalIf(probe.NewError(e).Trace(args...), "Unable to disable user") printMsg(userMessage{ op: ctx.Command.Name, AccessKey: args.Get(1), }) return nil } minio-client-0.0~20250403/cmd/admin-user-enable.go000066400000000000000000000044241477450377600213700ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminUserEnableCmd = cli.Command{ Name: "enable", Usage: "enable user", Action: mainAdminUserEnable, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET USERNAME FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable a disabled user 'foobar' on MinIO server. {{.Prompt}} {{.HelpName}} myminio foobar `, } // checkAdminUserEnableSyntax - validate all the passed arguments func checkAdminUserEnableSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminUserEnable is the handle for "mc admin user enable" command. func mainAdminUserEnable(ctx *cli.Context) error { checkAdminUserEnableSyntax(ctx) console.SetColor("UserMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") e := client.SetUserStatus(globalContext, args.Get(1), madmin.AccountEnabled) fatalIf(probe.NewError(e).Trace(args...), "Unable to enable user") printMsg(userMessage{ op: ctx.Command.Name, AccessKey: args.Get(1), }) return nil } minio-client-0.0~20250403/cmd/admin-user-info.go000066400000000000000000000060741477450377600211000ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strings" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminUserInfoCmd = cli.Command{ Name: "info", Usage: "display info of a user", Action: mainAdminUserInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET USERNAME FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display the info of a user "foobar". {{.Prompt}} {{.HelpName}} myminio foobar `, } // checkAdminUserAddSyntax - validate all the passed arguments func checkAdminUserInfoSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminUserInfo is the handler for "mc admin user info" command. func mainAdminUserInfo(ctx *cli.Context) error { checkAdminUserInfoSyntax(ctx) console.SetColor("UserMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") user, e := client.GetUserInfo(globalContext, args.Get(1)) fatalIf(probe.NewError(e).Trace(args...), "Unable to get user info") memberOf := []userGroup{} for _, group := range user.MemberOf { gd, e := client.GetGroupDescription(globalContext, group) fatalIf(probe.NewError(e).Trace(args...), "Unable to fetch group info") policies := []string{} if gd.Policy != "" { policies = strings.Split(gd.Policy, ",") } memberOf = append(memberOf, userGroup{ Name: gd.Name, Policies: policies, }) } printMsg(userMessage{ op: ctx.Command.Name, AccessKey: args.Get(1), PolicyName: user.PolicyName, UserStatus: string(user.Status), MemberOf: memberOf, Authentication: authInfoToUserMessage(user.AuthInfo), }) return nil } func authInfoToUserMessage(a *madmin.UserAuthInfo) string { if a == nil { return "" } authServer := "" if a.Type != madmin.BuiltinUserAuthType { authServer = "/" + a.AuthServer } return fmt.Sprintf("%s%s (%s)", a.Type, authServer, a.AuthServerUserID) } minio-client-0.0~20250403/cmd/admin-user-list.go000066400000000000000000000056411477450377600211170ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "strings" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminUserListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list all users", Action: mainAdminUserList, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List all users on MinIO server. {{.Prompt}} {{.HelpName}} myminio `, } // checkAdminUserListSyntax - validate all the passed arguments func checkAdminUserListSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminUserList is the handle for "mc admin user list" command. func mainAdminUserList(ctx *cli.Context) error { checkAdminUserListSyntax(ctx) // Additional command speific theme customization. console.SetColor("UserMessage", color.New(color.FgGreen)) console.SetColor("AccessKey", color.New(color.FgBlue)) console.SetColor("PolicyName", color.New(color.FgYellow)) console.SetColor("UserStatus", color.New(color.FgCyan)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") users, e := client.ListUsers(globalContext) fatalIf(probe.NewError(e).Trace(args...), "Unable to list user") for k, v := range users { memberOf := []userGroup{} for _, group := range v.MemberOf { gd, e := client.GetGroupDescription(globalContext, group) fatalIf(probe.NewError(e).Trace(args...), "Unable to fetch group info") policies := []string{} if gd.Policy != "" { policies = strings.Split(gd.Policy, ",") } memberOf = append(memberOf, userGroup{ Name: gd.Name, Policies: policies, }) } printMsg(userMessage{ op: ctx.Command.Name, AccessKey: k, PolicyName: v.PolicyName, MemberOf: memberOf, UserStatus: string(v.Status), }) } return nil } minio-client-0.0~20250403/cmd/admin-user-policy.go000066400000000000000000000060741477450377600214440ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "os" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" "github.com/minio/pkg/v3/policy" ) var adminUserPolicyCmd = cli.Command{ Name: "policy", Usage: "export user policies in JSON format", Action: mainAdminUserPolicy, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET USERNAME FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display the policy document of a user "foobar" in JSON format. {{.Prompt}} {{.HelpName}} myminio foobar `, } // checkAdminUserPolicySyntax - validate all the passed arguments func checkAdminUserPolicySyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminUserPolicy is the handler for "mc admin user policy" command. func mainAdminUserPolicy(ctx *cli.Context) error { checkAdminUserPolicySyntax(ctx) console.SetColor("UserMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") user, e := client.GetUserInfo(globalContext, args.Get(1)) fatalIf(probe.NewError(e).Trace(args...), "Unable to get user info") if user.PolicyName == "" { e = fmt.Errorf("policy not found for user %s", args.Get(1)) fatalIf(probe.NewError(e).Trace(args...), "Unable to fetch user policy document") } policyNames := strings.Split(user.PolicyName, ",") var policies []policy.Policy for _, policyName := range policyNames { if policyName == "" { continue } policyInfo, e := getPolicyInfo(client, policyName) fatalIf(probe.NewError(e).Trace(), "Unable to fetch user policy document for policy "+policyName) var policyObj policy.Policy if e := json.Unmarshal(policyInfo.Policy, &policyObj); e != nil { fatalIf(probe.NewError(e).Trace(), "Unable to unmarshal policy") } policies = append(policies, policyObj) } mergedPolicy := policy.MergePolicies(policies...) json.NewEncoder(os.Stdout).Encode(mergedPolicy) return nil } minio-client-0.0~20250403/cmd/admin-user-remove.go000066400000000000000000000043601477450377600214360ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminUserRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove user", Action: mainAdminUserRemove, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET USERNAME FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove a user 'foobar' on MinIO server. {{.Prompt}} {{.HelpName}} myminio foobar `, } // checkAdminUserRemoveSyntax - validate all the passed arguments func checkAdminUserRemoveSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainAdminUserRemove is the handle for "mc admin user remove" command. func mainAdminUserRemove(ctx *cli.Context) error { checkAdminUserRemoveSyntax(ctx) console.SetColor("UserMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") e := client.RemoveUser(globalContext, args.Get(1)) fatalIf(probe.NewError(e).Trace(args...), "Unable to remove %s", args.Get(1)) printMsg(userMessage{ op: ctx.Command.Name, AccessKey: args.Get(1), }) return nil } minio-client-0.0~20250403/cmd/admin-user-sts-info.go000066400000000000000000000075301477450377600217050ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" "github.com/minio/pkg/v3/policy" ) var adminUserSTSAcctSubcommands = []cli.Command{ adminUserSTSAcctInfoCmd, } var adminUserSTSAcctCmd = cli.Command{ Name: "sts", Usage: "manage STS accounts", Action: mainAdminUserSTSAcct, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminUserSTSAcctSubcommands, HideHelpCommand: true, } // mainAdminUserSTSAcct is the handle for "mc admin user sts" command. func mainAdminUserSTSAcct(ctx *cli.Context) error { commandNotFound(ctx, adminUserSTSAcctSubcommands) return nil } var adminUserSTSAcctInfoFlags = []cli.Flag{ cli.BoolFlag{ Name: "policy", Usage: "print policy in JSON format", }, } var adminUserSTSAcctInfoCmd = cli.Command{ Name: "info", Usage: "display temporary account info", Action: mainAdminUserSTSAcctInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminUserSTSAcctInfoFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS STS-ACCOUNT FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display information for the temporary account 'J123C4ZXEQN8RK6ND35I' {{.Prompt}} {{.HelpName}} myminio/ J123C4ZXEQN8RK6ND35I `, } // checkAdminUserSTSAcctInfoSyntax - validate all the passed arguments func checkAdminUserSTSAcctInfoSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) } } // mainAdminUserSTSAcctInfo is the handle for "mc admin user sts info" command. func mainAdminUserSTSAcctInfo(ctx *cli.Context) error { checkAdminUserSTSAcctInfoSyntax(ctx) console.SetColor("AccountMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) stsAccount := args.Get(1) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") stsInfo, e := client.TemporaryAccountInfo(globalContext, stsAccount) fatalIf(probe.NewError(e).Trace(args...), "Unable to get information of the specified service account") if ctx.Bool("policy") { if stsInfo.Policy == "" { fatalIf(errDummy().Trace(args...), "No policy found associated to the specified service account. Check the policy of its parent user.") } p, e := policy.ParseConfig(strings.NewReader(stsInfo.Policy)) fatalIf(probe.NewError(e).Trace(args...), "Unable to parse policy.") enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") fatalIf(probe.NewError(enc.Encode(p)).Trace(args...), "Unable to write policy to stdout.") return nil } printMsg(acctMessage{ op: svcAccOpInfo, AccessKey: stsAccount, AccountStatus: stsInfo.AccountStatus, ParentUser: stsInfo.ParentUser, ImpliedPolicy: stsInfo.ImpliedPolicy, Policy: json.RawMessage(stsInfo.Policy), Expiration: stsInfo.Expiration, }) return nil } minio-client-0.0~20250403/cmd/admin-user-svcacct-add.go000066400000000000000000000254321477450377600223200ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "crypto/rand" "encoding/base64" "fmt" "os" "strings" "time" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" "github.com/minio/pkg/v3/policy" ) var adminUserSvcAcctAddFlags = []cli.Flag{ cli.StringFlag{ Name: "access-key", Usage: "set an access key for the service account", }, cli.StringFlag{ Name: "secret-key", Usage: "set a secret key for the service account", }, cli.StringFlag{ Name: "policy", Usage: "path to a JSON policy file", }, cli.StringFlag{ Name: "name", Usage: "friendly name for the service account", }, cli.StringFlag{ Name: "description", Usage: "description for the service account", }, cli.StringFlag{ Name: "comment", Hidden: true, Usage: "description for the service account (DEPRECATED: use --description instead)", }, cli.StringFlag{ Name: "expiry", Usage: "time of expiration for the service account", }, } var adminUserSvcAcctAddCmd = cli.Command{ Name: "add", Usage: "add a new service account", Action: mainAdminUserSvcAcctAdd, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminUserSvcAcctAddFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS ACCOUNT [FLAGS] ACCOUNT: An account could be a regular MinIO user, STS or LDAP user. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Add a new service account for user 'foobar' to MinIO server with a name and description. {{.Prompt}} {{.HelpName}} myminio foobar --name uploaderKey --description "foobar uploader scripts" 2. Add a new service account to MinIO server with specified access key and secret key for user 'foobar'. {{.Prompt}} {{.HelpName}} myminio foobar --access-key "myaccesskey" --secret-key "mysecretkey" 3. Add a new service account to MinIO server with specified access key and random secret key for user 'foobar'. {{.Prompt}} {{.HelpName}} myminio foobar --access-key "myaccesskey" 4. Add a new service account to MinIO server with specified secret key and random access key for user 'foobar'. {{.Prompt}} {{.HelpName}} myminio foobar --secret-key "mysecretkey" 5. Add a new service account to MinIO server with specified expiry date in the future for user 'foobar'. {{.Prompt}} {{.HelpName}} myminio foobar --expiry 2023-06-24T10:00:00-07:00 `, } // checkAdminUserSvcAcctAddSyntax - validate all the passed arguments func checkAdminUserSvcAcctAddSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) } } // acctMessage container for content message structure type acctMessage struct { op acctOp Status string `json:"status"` AccessKey string `json:"accessKey,omitempty"` SecretKey string `json:"secretKey,omitempty"` ParentUser string `json:"parentUser,omitempty"` ImpliedPolicy bool `json:"impliedPolicy,omitempty"` Policy json.RawMessage `json:"policy,omitempty"` Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` AccountStatus string `json:"accountStatus,omitempty"` MemberOf []string `json:"memberOf,omitempty"` Expiration *time.Time `json:"expiration,omitempty"` } const ( accessFieldMaxLen = 20 ) type acctOp int const ( svcAccOpAdd = acctOp(iota) svcAccOpList svcAccOpInfo svcAccOpRemove svcAccOpDisable svcAccOpEnable svcAccOpSet stsAccOpInfo // Maximum length for MinIO access key. // There is no max length enforcement for access keys accessKeyMaxLen = 20 // Maximum length for Expiration timestamp expirationMaxLen = 29 // Alpha numeric table used for generating access keys. alphaNumericTable = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" // Total length of the alpha numeric table. alphaNumericTableLen = byte(len(alphaNumericTable)) // Maximum secret key length for MinIO, this // is used when autogenerating new credentials. // There is no max length enforcement for secret keys secretKeyMaxLen = 40 ) var supportedTimeFormats = []string{ "2006-01-02", "2006-01-02T15:04", "2006-01-02T15:04:05", time.RFC3339, } func (u acctMessage) String() string { switch u.op { case svcAccOpList: // Create a new pretty table with cols configuration return newPrettyTable(" | ", Field{"AccessKey", accessFieldMaxLen}, Field{"Expiration", expirationMaxLen}, ).buildRow(u.AccessKey, func() string { if u.Expiration != nil && !u.Expiration.IsZero() { return (*u.Expiration).String() } return "no-expiry" }()) case stsAccOpInfo, svcAccOpInfo: policyField := "" if u.ImpliedPolicy { policyField = "implied" } else { policyField = "embedded" } return console.Colorize("AccMessage", strings.Join( []string{ fmt.Sprintf("AccessKey: %s", u.AccessKey), fmt.Sprintf("ParentUser: %s", u.ParentUser), fmt.Sprintf("Status: %s", u.AccountStatus), fmt.Sprintf("Name: %s", u.Name), fmt.Sprintf("Description: %s", u.Description), fmt.Sprintf("Policy: %s", policyField), func() string { if u.Expiration != nil { return fmt.Sprintf("Expiration: %s", humanize.Time(*u.Expiration)) } return "Expiration: no-expiry" }(), }, "\n")) case svcAccOpRemove: return console.Colorize("AccMessage", "Removed service account `"+u.AccessKey+"` successfully.") case svcAccOpDisable: return console.Colorize("AccMessage", "Disabled service account `"+u.AccessKey+"` successfully.") case svcAccOpEnable: return console.Colorize("AccMessage", "Enabled service account `"+u.AccessKey+"` successfully.") case svcAccOpAdd: if u.Expiration != nil && !u.Expiration.IsZero() && !u.Expiration.Equal(timeSentinel) { return console.Colorize("AccMessage", fmt.Sprintf("Access Key: %s\nSecret Key: %s\nExpiration: %s", u.AccessKey, u.SecretKey, *u.Expiration)) } return console.Colorize("AccMessage", fmt.Sprintf("Access Key: %s\nSecret Key: %s\nExpiration: no-expiry", u.AccessKey, u.SecretKey)) case svcAccOpSet: return console.Colorize("AccMessage", "Edited service account `"+u.AccessKey+"` successfully.") } return "" } // generateCredentials - creates randomly generated credentials of maximum // allowed length. func generateCredentials() (accessKey, secretKey string, err *probe.Error) { readBytes := func(size int) (data []byte, e error) { data = make([]byte, size) var n int if n, e = rand.Read(data); e != nil { return nil, e } else if n != size { return nil, fmt.Errorf("Not enough data. Expected to read: %v bytes, got: %v bytes", size, n) } return data, nil } // Generate access key. keyBytes, e := readBytes(accessKeyMaxLen) if e != nil { return "", "", probe.NewError(e) } for i := 0; i < accessKeyMaxLen; i++ { keyBytes[i] = alphaNumericTable[keyBytes[i]%alphaNumericTableLen] } accessKey = string(keyBytes) // Generate secret key. keyBytes, e = readBytes(secretKeyMaxLen) if e != nil { return "", "", probe.NewError(e) } secretKey = strings.ReplaceAll(string([]byte(base64.StdEncoding.EncodeToString(keyBytes))[:secretKeyMaxLen]), "/", "+") return accessKey, secretKey, nil } func (u acctMessage) JSON() string { u.Status = "success" jsonMessageBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // mainAdminUserSvcAcctAdd is the handle for "mc admin user svcacct add" command. func mainAdminUserSvcAcctAdd(ctx *cli.Context) error { checkAdminUserSvcAcctAddSyntax(ctx) console.SetColor("AccMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) user := args.Get(1) accessKey := ctx.String("access-key") secretKey := ctx.String("secret-key") policyPath := ctx.String("policy") name := ctx.String("name") description := ctx.String("description") if description == "" { description = ctx.String("comment") } expiry := ctx.String("expiry") // generate access key and secret key if len(accessKey) <= 0 || len(secretKey) <= 0 { randomAccessKey, randomSecretKey, err := generateCredentials() if err != nil { fatalIf(err, "unable to generate randomized access credentials") } if len(accessKey) <= 0 { accessKey = randomAccessKey } if len(secretKey) <= 0 { secretKey = randomSecretKey } } // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") opts := madmin.AddServiceAccountReq{ AccessKey: accessKey, SecretKey: secretKey, Name: name, Description: description, TargetUser: user, } if policyPath != "" { // Validate the policy document and ensure it has at least when statement policyBytes, e := os.ReadFile(policyPath) fatalIf(probe.NewError(e), "unable to read the policy document") p, e := policy.ParseConfig(bytes.NewReader(policyBytes)) fatalIf(probe.NewError(e), "unable to parse the policy document") if p.IsEmpty() { fatalIf(errInvalidArgument(), "empty policies are not allowed") } opts.Policy = policyBytes } if expiry != "" { location, e := time.LoadLocation("Local") fatalIf(probe.NewError(e), "unable to load local location. verify your local TZ= settings") var found bool for _, format := range supportedTimeFormats { t, e := time.ParseInLocation(format, expiry, location) if e == nil { found = true opts.Expiration = &t break } } if !found { fatalIf(probe.NewError(fmt.Errorf("expiry argument is not matching any of the supported patterns")), "unable to parse the expiry argument") } } creds, e := client.AddServiceAccount(globalContext, opts) fatalIf(probe.NewError(e).Trace(args...), "Unable to add a new service account.") printMsg(acctMessage{ op: svcAccOpAdd, AccessKey: creds.AccessKey, SecretKey: creds.SecretKey, Expiration: &creds.Expiration, AccountStatus: "enabled", }) return nil } minio-client-0.0~20250403/cmd/admin-user-svcacct-disable.go000066400000000000000000000047241477450377600231740ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminUserSvcAcctDisableCmd = cli.Command{ Name: "disable", Usage: "disable a service account", Action: mainAdminUserSvcAcctDisable, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS SERVICE-ACCOUNT FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Disable a service account 'J123C4ZXEQN8RK6ND35I' on MinIO server. {{.Prompt}} {{.HelpName}} myminio/ J123C4ZXEQN8RK6ND35I `, } // checkAdminUserSvcAcctDisableSyntax - validate all the passed arguments func checkAdminUserSvcAcctDisableSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) } } // mainAdminUserSvcAcctDisable is the handle for "mc admin user svcacct disable" command. func mainAdminUserSvcAcctDisable(ctx *cli.Context) error { checkAdminUserSvcAcctDisableSyntax(ctx) console.SetColor("AccMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) svcAccount := args.Get(1) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") opts := madmin.UpdateServiceAccountReq{ NewStatus: "off", } e := client.UpdateServiceAccount(globalContext, svcAccount, opts) fatalIf(probe.NewError(e).Trace(args...), "Unable to disable the specified service account") printMsg(acctMessage{ op: svcAccOpDisable, AccessKey: svcAccount, }) return nil } minio-client-0.0~20250403/cmd/admin-user-svcacct-enable.go000066400000000000000000000047061477450377600230170ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminUserSvcAcctEnableCmd = cli.Command{ Name: "enable", Usage: "enable a service account", Action: mainAdminUserSvcAcctEnable, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS SERVICE-ACCOUNT FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable a service account 'J123C4ZXEQN8RK6ND35I' on MinIO server. {{.Prompt}} {{.HelpName}} myminio/ J123C4ZXEQN8RK6ND35I `, } // checkAdminUserSvcAcctEnableSyntax - validate all the passed arguments func checkAdminUserSvcAcctEnableSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) } } // mainAdminUserSvcAcctEnable is the handle for "mc admin user svcacct enable" command. func mainAdminUserSvcAcctEnable(ctx *cli.Context) error { checkAdminUserSvcAcctEnableSyntax(ctx) console.SetColor("AccMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) svcAccount := args.Get(1) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") opts := madmin.UpdateServiceAccountReq{ NewStatus: "on", } e := client.UpdateServiceAccount(globalContext, svcAccount, opts) fatalIf(probe.NewError(e).Trace(args...), "Unable to enable the specified service account") printMsg(acctMessage{ op: svcAccOpEnable, AccessKey: svcAccount, }) return nil } minio-client-0.0~20250403/cmd/admin-user-svcacct-info.go000066400000000000000000000065511477450377600225240ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" "github.com/minio/pkg/v3/policy" ) var adminUserSvcAcctInfoFlags = []cli.Flag{ cli.BoolFlag{ Name: "policy", Usage: "print policy in JSON format", }, } var adminUserSvcAcctInfoCmd = cli.Command{ Name: "info", Usage: "display service account info", Action: mainAdminUserSvcAcctInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminUserSvcAcctInfoFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS SERVICE-ACCOUNT FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display information for service account 'J123C4ZXEQN8RK6ND35I' {{.Prompt}} {{.HelpName}} myminio/ J123C4ZXEQN8RK6ND35I `, } // checkAdminUserSvcAcctInfoSyntax - validate all the passed arguments func checkAdminUserSvcAcctInfoSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) } } // mainAdminUserSvcAcctInfo is the handle for "mc admin user svcacct info" command. func mainAdminUserSvcAcctInfo(ctx *cli.Context) error { checkAdminUserSvcAcctInfoSyntax(ctx) console.SetColor("AccMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) svcAccount := args.Get(1) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") svcInfo, e := client.InfoServiceAccount(globalContext, svcAccount) fatalIf(probe.NewError(e).Trace(args...), "Unable to get information of the specified service account") if ctx.Bool("policy") { if svcInfo.Policy == "" { fatalIf(errDummy().Trace(args...), "No policy found associated to the specified service account. Check the policy of its parent user.") } p, e := policy.ParseConfig(strings.NewReader(svcInfo.Policy)) fatalIf(probe.NewError(e).Trace(args...), "Unable to parse policy.") enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") fatalIf(probe.NewError(enc.Encode(p)).Trace(args...), "Unable to write policy to stdout.") return nil } printMsg(acctMessage{ op: svcAccOpInfo, AccessKey: svcAccount, Name: svcInfo.Name, Description: svcInfo.Description, AccountStatus: svcInfo.AccountStatus, ParentUser: svcInfo.ParentUser, ImpliedPolicy: svcInfo.ImpliedPolicy, Policy: json.RawMessage(svcInfo.Policy), Expiration: svcInfo.Expiration, }) return nil } minio-client-0.0~20250403/cmd/admin-user-svcacct-list.go000066400000000000000000000063261477450377600225440ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminUserSvcAcctListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list services accounts", Action: mainAdminUserSvcAcctList, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS TARGET-ACCOUNT TARGET-ACCOUNT: Is either a MinIO user, LDAP account. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List all service accounts for user 'foobar'. {{.Prompt}} {{.HelpName}} myminio/ foobar `, } // checkAdminUserSvcAcctListSyntax - validate all the passed arguments func checkAdminUserSvcAcctListSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) } } // mainAdminUserSvcAcctList is the handle for "mc admin user svcacct ls" command. func mainAdminUserSvcAcctList(ctx *cli.Context) error { checkAdminUserSvcAcctListSyntax(ctx) console.SetColor("AccMessage", color.New(color.FgGreen)) console.SetColor("AccessKeyHeader", color.New(color.Bold, color.FgBlue)) console.SetColor("ExpirationHeader", color.New(color.Bold, color.FgCyan)) console.SetColor("AccessKey", color.New(color.FgBlue)) console.SetColor("Expiration", color.New(color.FgCyan)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) user := args.Get(1) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") svcList, e := client.ListServiceAccounts(globalContext, user) fatalIf(probe.NewError(e).Trace(args...), "Unable to list service accounts") if len(svcList.Accounts) > 0 { if !globalJSON { // Print table header var header string header += console.Colorize("Headers", newPrettyTable(" | ", Field{"AccessKeyHeader", accessFieldMaxLen}, Field{"ExpirationHeader", expirationMaxLen}, ).buildRow(" Access Key", "Expiry")) console.Println(header) } // Print table contents for _, svc := range svcList.Accounts { expiration := svc.Expiration if expiration.Equal(timeSentinel) { expiration = nil } printMsg(acctMessage{ op: svcAccOpList, AccessKey: svc.AccessKey, Expiration: expiration, }) } } else { if !globalJSON { console.Println("No service accounts found") } } return nil } minio-client-0.0~20250403/cmd/admin-user-svcacct-remove.go000066400000000000000000000045621477450377600230660ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminUserSvcAcctRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove a service account", Action: mainAdminUserSvcAcctRemove, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS SERVICE-ACCOUNT FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove a service account 'J123C4ZXEQN8RK6ND35I' from MinIO server. {{.Prompt}} {{.HelpName}} myminio/ J123C4ZXEQN8RK6ND35I `, } // checkAdminUserSvcAcctRemoveSyntax - validate all the passed arguments func checkAdminUserSvcAcctRemoveSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) } } // mainAdminUserSvcAcctRemove is the handle for "mc admin user svcacct rm" command. func mainAdminUserSvcAcctRemove(ctx *cli.Context) error { console.SetColor("AccMessage", color.New(color.FgGreen)) checkAdminUserSvcAcctRemoveSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) svcAccount := args.Get(1) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") e := client.DeleteServiceAccount(globalContext, svcAccount) fatalIf(probe.NewError(e).Trace(args...), "Unable to remove the specified service account") printMsg(acctMessage{ op: svcAccOpRemove, AccessKey: svcAccount, }) return nil } minio-client-0.0~20250403/cmd/admin-user-svcacct-set.go000066400000000000000000000104341477450377600223570ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "os" "time" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminUserSvcAcctSetFlags = []cli.Flag{ cli.StringFlag{ Name: "secret-key", Usage: "set a secret key for the service account", }, cli.StringFlag{ Name: "policy", Usage: "path to a JSON policy file", }, cli.StringFlag{ Name: "name", Usage: "name for the service account", }, cli.StringFlag{ Name: "description", Usage: "description for the service account", }, cli.StringFlag{ Name: "expiry", Usage: "time of expiration for the service account", }, } var adminUserSvcAcctSetCmd = cli.Command{ Name: "edit", Aliases: []string{"set"}, Usage: "edit an existing service account", Action: mainAdminUserSvcAcctSet, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminUserSvcAcctSetFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS SERVICE-ACCOUNT FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Change the secret key of the service account 'J123C4ZXEQN8RK6ND35I' in MinIO server. {{.Prompt}} {{.HelpName}} myminio/ 'J123C4ZXEQN8RK6ND35I' --secret-key 'xxxxxxx' 2. Change the expiry of the service account 'J123C4ZXEQN8RK6ND35I' in MinIO server. {{.Prompt}} {{.HelpName}} myminio/ 'J123C4ZXEQN8RK6ND35I' --expiry 2023-06-24T10:00:00-07:00 `, } // checkAdminUserSvcAcctSetSyntax - validate all the passed arguments func checkAdminUserSvcAcctSetSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) } } // mainAdminUserSvcAcctSet is the handle for "mc admin user svcacct set" command. func mainAdminUserSvcAcctSet(ctx *cli.Context) error { checkAdminUserSvcAcctSetSyntax(ctx) console.SetColor("AccMessage", color.New(color.FgGreen)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) svcAccount := args.Get(1) secretKey := ctx.String("secret-key") policyPath := ctx.String("policy") name := ctx.String("name") description := ctx.String("description") expiry := ctx.String("expiry") // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") var buf []byte if policyPath != "" { var e error buf, e = os.ReadFile(policyPath) fatalIf(probe.NewError(e), "Unable to open the policy document.") } var expiryTime time.Time var expiryPointer *time.Time if expiry != "" { location, e := time.LoadLocation("Local") if e != nil { fatalIf(probe.NewError(e), "Unable to parse the expiry argument.") } patternMatched := false for _, format := range supportedTimeFormats { t, e := time.ParseInLocation(format, expiry, location) if e == nil { patternMatched = true expiryTime = t expiryPointer = &expiryTime break } } if !patternMatched { fatalIf(probe.NewError(fmt.Errorf("expiry argument is not matching any of the supported patterns")), "unable to parse the expiry argument.") } } opts := madmin.UpdateServiceAccountReq{ NewPolicy: buf, NewSecretKey: secretKey, NewName: name, NewDescription: description, NewExpiration: expiryPointer, } e := client.UpdateServiceAccount(globalContext, svcAccount, opts) fatalIf(probe.NewError(e).Trace(args...), "Unable to edit the specified service account") printMsg(acctMessage{ op: svcAccOpSet, AccessKey: svcAccount, }) return nil } minio-client-0.0~20250403/cmd/admin-user-svcacct.go000066400000000000000000000030011477450377600215560ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminUserSvcAcctSubcommands = []cli.Command{ adminUserSvcAcctAddCmd, adminUserSvcAcctListCmd, adminUserSvcAcctRemoveCmd, adminUserSvcAcctInfoCmd, adminUserSvcAcctSetCmd, adminUserSvcAcctEnableCmd, adminUserSvcAcctDisableCmd, } var adminUserSvcAcctCmd = cli.Command{ Name: "svcacct", Usage: "manage service accounts", Action: mainAdminUserSvcAcct, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminUserSvcAcctSubcommands, HideHelpCommand: true, } // mainAdminUserSvcAcct is the handle for "mc admin user svcacct" command. func mainAdminUserSvcAcct(ctx *cli.Context) error { commandNotFound(ctx, adminUserSvcAcctSubcommands) return nil } minio-client-0.0~20250403/cmd/admin-user.go000066400000000000000000000027621477450377600201470ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var adminUserSubcommands = []cli.Command{ adminUserAddCmd, adminUserDisableCmd, adminUserEnableCmd, adminUserRemoveCmd, adminUserListCmd, adminUserInfoCmd, adminUserPolicyCmd, adminUserSvcAcctCmd, adminUserSTSAcctCmd, } var adminUserCmd = cli.Command{ Name: "user", Usage: "manage users", Action: mainAdminUser, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: adminUserSubcommands, HideHelpCommand: true, } // mainAdminUser is the handle for "mc admin config" command. func mainAdminUser(ctx *cli.Context) error { commandNotFound(ctx, adminUserSubcommands) return nil // Sub-commands like "get", "set" have their own main. } minio-client-0.0~20250403/cmd/alias-export.go000066400000000000000000000042571477450377600205140ustar00rootroot00000000000000package cmd import ( "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var aliasExportCmd = cli.Command{ Name: "export", ShortName: "e", Usage: "export configuration info to stdout", Action: mainAliasExport, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS Credentials to be exported will be in the following JSON format: { "url": "http://localhost:9000", "accessKey": "YJ0RI0F4R5HWY38MD873", "secretKey": "OHz5CT7xdMHiXnKZP0BmZ5P4G5UvWvVaxR8gljLG", "api": "s3v4", "path": "auto" } FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Export the provided 'alias' to credentials.json file: {{ .Prompt }} {{ .HelpName }} myminio/ > credentials.json 2. Export the credentials to standard output and pipe it to import command {{ .Prompt }} {{ .HelpName }} alias1/ | mc alias import alias2/ `, } // checkAliasExportSyntax - verifies input arguments to 'alias export'. func checkAliasExportSyntax(ctx *cli.Context) { args := ctx.Args() if ctx.NArg() == 0 { showCommandHelpAndExit(ctx, 1) } if ctx.NArg() > 1 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Incorrect number of arguments for alias export command.") } alias := cleanAlias(args.Get(0)) if !isValidAlias(alias) { fatalIf(errInvalidAlias(alias), "Unable to validate alias") } } // exportAlias - get an alias config func exportAlias(alias string) { mcCfgV10, err := loadMcConfig() fatalIf(err.Trace(globalMCConfigVersion), "Unable to load config `"+mustGetMcConfigPath()+"`.") cfg, ok := mcCfgV10.Aliases[alias] if !ok { fatalIf(errInvalidArgument().Trace(alias), "Unable to export credentials") } buf, e := json.Marshal(cfg) fatalIf(probe.NewError(e).Trace(alias), "Unable to export credentials") console.Println(string(buf)) } func mainAliasExport(cli *cli.Context) error { args := cli.Args() checkAliasExportSyntax(cli) exportAlias(cleanAlias(args.Get(0))) return nil } minio-client-0.0~20250403/cmd/alias-import.go000066400000000000000000000110111477450377600204670ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "encoding/json" "os" "strings" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" ) var aliasImportCmd = cli.Command{ Name: "import", ShortName: "i", Usage: "import configuration info to configuration file from a JSON formatted string ", Action: mainAliasImport, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS ./credentials.json Credentials to be imported must be in the following JSON format: { "url": "http://localhost:9000", "accessKey": "YJ0RI0F4R5HWY38MD873", "secretKey": "OHz5CT7xdMHiXnKZP0BmZ5P4G5UvWvVaxR8gljLG", "api": "s3v4", "path": "auto" } FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Import the provided credentials.json file as 'myminio' to the config: {{ .Prompt }} {{ .HelpName }} myminio/ ./credentials.json 2. Import the credentials through standard input as 'myminio' to the config: {{ .Prompt }} cat credentials.json | {{ .HelpName }} myminio/ `, } // checkAliasImportSyntax - verifies input arguments to 'alias import'. func checkAliasImportSyntax(ctx *cli.Context) { args := ctx.Args() argsNr := len(args) if argsNr == 0 { showCommandHelpAndExit(ctx, 1) } if argsNr > 2 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Incorrect number of arguments for alias Import command.") } alias := cleanAlias(args.Get(0)) if !isValidAlias(alias) { fatalIf(errInvalidAlias(alias), "Invalid alias.") } } func checkCredentialsSyntax(credentials aliasConfigV10) { if !isValidHostURL(credentials.URL) { fatalIf(errInvalidURL(credentials.URL), "Invalid URL.") } if !isValidAccessKey(credentials.AccessKey) { fatalIf(errInvalidArgument().Trace(credentials.AccessKey), "Invalid access key `"+credentials.AccessKey+"`.") } if !isValidSecretKey(credentials.SecretKey) { fatalIf(errInvalidArgument().Trace(), "Invalid secret key.") } if credentials.API != "" && !isValidAPI(credentials.API) { // Empty value set to default "S3v4". fatalIf(errInvalidArgument().Trace(credentials.API), "Unrecognized API signature. Valid options are `[S3v4, S3v2]`.") } if !isValidPath(credentials.Path) { fatalIf(errInvalidArgument().Trace(credentials.Path), "Unrecognized path value. Valid options are `[auto, on, off]`.") } } // importAlias - set an alias config based on imported values. func importAlias(alias string, aliasCfgV10 aliasConfigV10) aliasMessage { checkCredentialsSyntax(aliasCfgV10) mcCfgV10, err := loadMcConfig() fatalIf(err.Trace(globalMCConfigVersion), "Unable to load config `"+mustGetMcConfigPath()+"`.") // Add new host. mcCfgV10.Aliases[alias] = aliasCfgV10 fatalIf(saveMcConfig(mcCfgV10).Trace(alias), "Unable to import credentials to `"+mustGetMcConfigPath()+"`.") return aliasMessage{ Alias: alias, URL: mcCfgV10.Aliases[alias].URL, AccessKey: mcCfgV10.Aliases[alias].AccessKey, SecretKey: mcCfgV10.Aliases[alias].SecretKey, API: mcCfgV10.Aliases[alias].API, Path: mcCfgV10.Aliases[alias].Path, } } func mainAliasImport(cli *cli.Context) error { var ( args = cli.Args() alias = cleanAlias(args.Get(0)) ) checkAliasImportSyntax(cli) var credentialsJSON aliasConfigV10 credsFile := strings.TrimSpace(args.Get(1)) if credsFile == "" { credsFile = os.Stdin.Name() } input, e := os.ReadFile(credsFile) fatalIf(probe.NewError(e).Trace(args...), "Unable to parse credentials file") e = json.Unmarshal(input, &credentialsJSON) fatalIf(probe.NewError(e).Trace(args...), "Unable to parse input credentials") msg := importAlias(alias, credentialsJSON) msg.op = cli.Command.Name printMsg(msg) return nil } minio-client-0.0~20250403/cmd/alias-list.go000066400000000000000000000117321477450377600201420ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "sort" "strings" "github.com/minio/cli" "github.com/minio/pkg/v3/console" "github.com/minio/pkg/v3/env" "github.com/fatih/color" ) var aliasListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list aliases in configuration file", Action: func(ctx *cli.Context) error { return mainAliasList(ctx, false) }, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [ALIAS] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List all aliases. {{.Prompt}} {{.HelpName}} 2. List a specific alias. {{.Prompt}} {{.HelpName}} s3 `, } // Input argument validator.. func checkAliasListSyntax(ctx *cli.Context) { args := ctx.Args() if len(ctx.Args()) > 1 { fatalIf(errInvalidArgument().Trace(args...), "Incorrect number of arguments to alias list command.") } } func mainAliasList(ctx *cli.Context, deprecated bool) error { checkAliasListSyntax(ctx) // Additional command specific theme customization. console.SetColor("Alias", color.New(color.FgCyan, color.Bold)) console.SetColor("URL", color.New(color.FgYellow)) console.SetColor("AccessKey", color.New(color.FgCyan)) console.SetColor("SecretKey", color.New(color.FgCyan)) console.SetColor("API", color.New(color.FgBlue)) console.SetColor("Path", color.New(color.FgCyan)) console.SetColor("Src", color.New(color.FgCyan)) alias := cleanAlias(ctx.Args().Get(0)) aliasesMsgs := listAliases(alias, deprecated) // List all configured hosts. for i := range aliasesMsgs { aliasesMsgs[i].op = "list" } printAliases(aliasesMsgs...) return nil } // Prints all the aliases func printAliases(aliases ...aliasMessage) { maxAlias := 0 for _, alias := range aliases { if len(alias.Alias) > maxAlias { maxAlias = len(alias.Alias) } } for _, alias := range aliases { if !globalJSON { // Format properly for alignment based on alias length only in non json mode. alias.Alias = fmt.Sprintf("%-*.*s", maxAlias, maxAlias, alias.Alias) } if alias.AccessKey == "" || alias.SecretKey == "" { alias.AccessKey = "" alias.SecretKey = "" alias.API = "" } printMsg(alias) } } // byAlias is a collection satisfying sort.Interface type byAlias []aliasMessage func (d byAlias) Len() int { return len(d) } func (d byAlias) Swap(i, j int) { d[i], d[j] = d[j], d[i] } func (d byAlias) Less(i, j int) bool { return d[i].Alias < d[j].Alias } func buildAliasMessage(alias string, deprecated bool, aliasCfg *aliasConfigV10) aliasMessage { aliasMsg := aliasMessage{ prettyPrint: false, Alias: alias, URL: aliasCfg.URL, AccessKey: aliasCfg.AccessKey, SecretKey: aliasCfg.SecretKey, API: aliasCfg.API, Src: aliasCfg.Src, } if deprecated { aliasMsg.Lookup = aliasCfg.Path } else { aliasMsg.Path = aliasCfg.Path } return aliasMsg } // listAliases - list one or all aliases func listAliases(alias string, deprecated bool) (aliases []aliasMessage) { // If specific alias is requested, look for it and print. if alias != "" { aliasCfg := mustGetHostConfig(alias) if aliasCfg != nil { return []aliasMessage{buildAliasMessage(alias, deprecated, aliasCfg)} } fatalIf(errInvalidAliasedURL(alias), "No such alias `"+alias+"` found.") } // list alias from the environment variable. for _, envK := range env.List(mcEnvHostPrefix) { aliasCfg, _ := expandAliasFromEnv(env.Get(envK, "")) if aliasCfg == nil { continue } alias := strings.ReplaceAll(envK, mcEnvHostPrefix, "") aliases = append(aliases, buildAliasMessage(alias, deprecated, aliasCfg)) } // list alias from the customized configuration. for s, aliasCfg := range aliasToConfigMap { aliases = append(aliases, buildAliasMessage(s, deprecated, aliasCfg)) } // list alias from the default configuration. conf, err := loadMcConfig() fatalIf(err.Trace(globalMCConfigVersion), "Unable to load config version `"+globalMCConfigVersion+"`.") for k, v := range conf.Aliases { v.Src = mustGetMcConfigPath() aliases = append(aliases, buildAliasMessage(k, deprecated, &v)) } // Sort by alias names lexically. sort.Sort(byAlias(aliases)) return } minio-client-0.0~20250403/cmd/alias-main.go000066400000000000000000000074711477450377600201200ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) // Configure an alias in MinIO Client // // ---- // NOTE: that the alias command only writes values to the config file. // It does not use any configuration values from the environment variables. // // One needs to edit configuration file manually, this is purposefully done // so to avoid taking credentials over cli arguments. It is a security precaution // ---- // var aliasFlags = []cli.Flag{} var aliasSubcommands = []cli.Command{ aliasSetCmd, aliasListCmd, aliasRemoveCmd, aliasImportCmd, aliasExportCmd, } var aliasCmd = cli.Command{ Name: "alias", Usage: "manage server credentials in configuration file", Action: mainAlias, Before: setGlobalsFromContext, HideHelpCommand: true, Flags: append(aliasFlags, globalFlags...), Subcommands: aliasSubcommands, } // mainAlias is the handle for "mc alias" command. provides sub-commands which write configuration data in json format to config file. func mainAlias(ctx *cli.Context) error { commandNotFound(ctx, aliasSubcommands) return nil // Sub-commands like add, list and remove have their own main. } // aliasMessage container for content message structure type aliasMessage struct { op string prettyPrint bool Status string `json:"status"` Alias string `json:"alias"` URL string `json:"URL"` AccessKey string `json:"accessKey,omitempty"` SecretKey string `json:"secretKey,omitempty"` API string `json:"api,omitempty"` Path string `json:"path,omitempty"` Src string `json:"src,omitempty"` // Deprecated field, replaced by Path Lookup string `json:"lookup,omitempty"` } // Print the config information of one alias, when prettyPrint flag // is activated, fields contents are cut and '...' will be added to // show a pretty table of all aliases configurations func (h aliasMessage) String() string { switch h.op { case "list": // Create a new pretty table with cols configuration t := newPrettyRecord(2, Row{"Alias", "Alias"}, Row{"URL", "URL"}, Row{"AccessKey", "AccessKey"}, Row{"SecretKey", "SecretKey"}, Row{"API", "API"}, Row{"Path", "Path"}, Row{"Src", "Src"}, ) // Handle deprecated lookup path := h.Path if path == "" { path = h.Lookup } return t.buildRecord(h.Alias, h.URL, h.AccessKey, h.SecretKey, h.API, path, h.Src) case "remove": return console.Colorize("AliasMessage", "Removed `"+h.Alias+"` successfully.") case "add": // add is deprecated fallthrough case "set": return console.Colorize("AliasMessage", "Added `"+h.Alias+"` successfully.") case "import": return console.Colorize("AliasMessage", "Imported `"+h.Alias+"` successfully.") default: return "" } } // JSON jsonified host message func (h aliasMessage) JSON() string { h.Status = "success" jsonMessageBytes, e := json.MarshalIndent(h, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } minio-client-0.0~20250403/cmd/alias-remove.go000066400000000000000000000056751477450377600204750ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/pkg/v3/console" ) var aliasRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove an alias from configuration file", Action: func(ctx *cli.Context) error { return mainAliasRemove(ctx) }, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove "goodisk" alias from the configuration. {{.Prompt}} {{.HelpName}} goodisk `, } // checkAliasRemoveSyntax - verifies input arguments to 'alias remove'. func checkAliasRemoveSyntax(ctx *cli.Context) { args := ctx.Args() if len(ctx.Args()) != 1 { fatalIf(errInvalidArgument().Trace(args...), "Incorrect number of arguments for alias remove command.") } alias := cleanAlias(args.Get(0)) if !isValidAlias(alias) { fatalIf(errDummy().Trace(alias), "Invalid alias `"+alias+"`.") } } // mainAliasRemove is the handle for "mc alias rm" command. func mainAliasRemove(ctx *cli.Context) error { checkAliasRemoveSyntax(ctx) console.SetColor("AliasMessage", color.New(color.FgGreen)) args := ctx.Args() alias := args.Get(0) aliasMsg := removeAlias(alias) // Remove an alias aliasMsg.op = "remove" printMsg(aliasMsg) return nil } // aliasMustExist confirms that a given alias is present in Aliases array, returns error if not found func aliasMustExist(alias string) { hostConfig := mustGetHostConfig(alias) if hostConfig == nil { fatalIf(errInvalidAliasedURL(alias), "No such alias `"+alias+"` found.") } } // removeAlias - removes an alias. func removeAlias(alias string) aliasMessage { conf, err := loadMcConfig() fatalIf(err.Trace(globalMCConfigVersion), "Unable to load config version `"+globalMCConfigVersion+"`.") // check if alias is valid aliasMustExist(alias) // Remove the alias from the config. delete(conf.Aliases, alias) err = saveMcConfig(conf) fatalIf(err.Trace(alias), "Unable to save the delete alias in config version `"+globalMCConfigVersion+"`.") return aliasMessage{Alias: alias} } minio-client-0.0~20250403/cmd/alias-set.go000066400000000000000000000272501477450377600177640ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "context" "crypto/tls" "crypto/x509" "fmt" "math/rand" "net/http" "os" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" "golang.org/x/term" ) const cred = "YellowItalics" var aliasSetFlags = []cli.Flag{ cli.StringFlag{ Name: "path", Value: "auto", Usage: "bucket path lookup supported by the server. Valid options are '[auto, on, off]'", }, cli.StringFlag{ Name: "api", Usage: "API signature. Valid options are '[S3v4, S3v2]'", }, } var aliasSetCmd = cli.Command{ Name: "set", ShortName: "s", Usage: "set a new alias to configuration file", Action: func(cli *cli.Context) error { return mainAliasSet(cli, false) }, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(aliasSetFlags, globalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS URL ACCESSKEY SECRETKEY FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Add MinIO service under "myminio" alias. For security reasons turn off bash history momentarily. {{.DisableHistory}} {{.Prompt}} {{.HelpName}} myminio http://localhost:9000 minio minio123 {{.EnableHistory}} 2. Add MinIO service under "myminio" alias, to use dns style bucket lookup. For security reasons turn off bash history momentarily. {{.DisableHistory}} {{.Prompt}} {{.HelpName}} myminio http://localhost:9000 minio minio123 --api "s3v4" --path "off" {{.EnableHistory}} 3. Add Amazon S3 storage service under "mys3" alias. For security reasons turn off bash history momentarily. {{.DisableHistory}} {{.Prompt}} {{.HelpName}} mys3 https://s3.amazonaws.com \ BKIKJAA5BMMU2RHO6IBB V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12 {{.EnableHistory}} 4. Add Amazon S3 storage service under "mys3" alias, prompting for keys. {{.Prompt}} {{.HelpName}} mys3 https://s3.amazonaws.com --api "s3v4" --path "off" Enter Access Key: BKIKJAA5BMMU2RHO6IBB Enter Secret Key: V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12 5. Add Amazon S3 storage service under "mys3" alias using piped keys. {{.DisableHistory}} {{.Prompt}} echo -e "BKIKJAA5BMMU2RHO6IBB\nV8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12" | \ {{.HelpName}} mys3 https://s3.amazonaws.com --api "s3v4" --path "off" {{.EnableHistory}} `, } // checkAliasSetSyntax - verifies input arguments to 'alias set'. func checkAliasSetSyntax(ctx *cli.Context, accessKey, secretKey string, deprecated bool) { args := ctx.Args() argsNr := len(args) if argsNr == 0 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } if argsNr > 4 || argsNr < 2 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Incorrect number of arguments for alias set command.") } alias := cleanAlias(args.Get(0)) url := args.Get(1) api := ctx.String("api") path := ctx.String("path") bucketLookup := ctx.String("lookup") if !isValidAlias(alias) { fatalIf(errInvalidAlias(alias), "Invalid alias.") } if !isValidHostURL(url) { fatalIf(errInvalidURL(url), "Invalid URL.") } if !isValidAccessKey(accessKey) { fatalIf(errInvalidArgument().Trace(accessKey), "Invalid access key `"+accessKey+"`.") } if !isValidSecretKey(secretKey) { fatalIf(errInvalidArgument().Trace(secretKey), "Invalid secret key `"+secretKey+"`.") } if api != "" && !isValidAPI(api) { // Empty value set to default "S3v4". fatalIf(errInvalidArgument().Trace(api), "Unrecognized API signature. Valid options are `[S3v4, S3v2]`.") } if deprecated { if !isValidLookup(bucketLookup) { fatalIf(errInvalidArgument().Trace(bucketLookup), "Unrecognized bucket lookup. Valid options are `[dns,auto, path]`.") } } else { if !isValidPath(path) { fatalIf(errInvalidArgument().Trace(bucketLookup), "Unrecognized path value. Valid options are `[auto, on, off]`.") } } } // setAlias - set an alias config. func setAlias(alias string, aliasCfgV10 aliasConfigV10) aliasMessage { mcCfgV10, err := loadMcConfig() fatalIf(err.Trace(globalMCConfigVersion), "Unable to load config `"+mustGetMcConfigPath()+"`.") // Add new host. mcCfgV10.Aliases[alias] = aliasCfgV10 err = saveMcConfig(mcCfgV10) fatalIf(err.Trace(alias), "Unable to update hosts in config version `"+mustGetMcConfigPath()+"`.") return aliasMessage{ Alias: alias, URL: aliasCfgV10.URL, AccessKey: aliasCfgV10.AccessKey, SecretKey: aliasCfgV10.SecretKey, API: aliasCfgV10.API, Path: aliasCfgV10.Path, } } // probeS3Signature - auto probe S3 server signature: issue a Stat call // using v4 signature then v2 in case of failure. func probeS3Signature(ctx context.Context, accessKey, secretKey, url string, peerCert *x509.Certificate) (string, *probe.Error) { probeBucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "probe-bsign-") // Test s3 connection for API auto probe s3Config := &Config{ // S3 connection parameters Insecure: globalInsecure, AccessKey: accessKey, SecretKey: secretKey, HostURL: urlJoinPath(url, probeBucketName), Debug: globalDebug, ConnReadDeadline: globalConnReadDeadline, ConnWriteDeadline: globalConnWriteDeadline, UploadLimit: int64(globalLimitUpload), DownloadLimit: int64(globalLimitDownload), } if peerCert != nil { configurePeerCertificate(s3Config, peerCert) } probeSignatureType := func(stype string) (string, *probe.Error) { s3Config.Signature = stype s3Client, err := S3New(s3Config) if err != nil { return "", err } if _, err := s3Client.Stat(ctx, StatOptions{}); err != nil { e := err.ToGoError() if _, ok := e.(BucketDoesNotExist); ok { // Bucket doesn't exist, means signature probing worked successfully. return stype, nil } // AccessDenied means Stat() is not allowed but credentials are valid. // AccessDenied is only returned when policy doesn't allow HeadBucket // operations. if minio.ToErrorResponse(err.ToGoError()).Code == "AccessDenied" { return stype, nil } // For any other errors we fail. return "", err.Trace(s3Config.Signature) } return stype, nil } stype, err := probeSignatureType("s3v4") if err != nil { if stype, err = probeSignatureType("s3v2"); err != nil { return "", err.Trace("s3v4", "s3v2") } return stype, nil } return stype, nil } // BuildS3Config constructs an S3 Config and does // signature auto-probe when needed. func BuildS3Config(ctx context.Context, alias, url, accessKey, secretKey, api, path string, peerCert *x509.Certificate) (*Config, *probe.Error) { s3Config := NewS3Config(alias, url, &aliasConfigV10{ AccessKey: accessKey, SecretKey: secretKey, URL: url, Path: path, }) if peerCert != nil { configurePeerCertificate(s3Config, peerCert) } // If api is provided we do not auto probe signature, this is // required in situations when signature type is provided by the user. if api != "" { s3Config.Signature = api return s3Config, nil } // Probe S3 signature version api, err := probeS3Signature(ctx, accessKey, secretKey, url, peerCert) if err != nil { return nil, err.Trace(url, accessKey, api, path) } s3Config.Signature = api // Success. return s3Config, nil } // fetchAliasKeys - returns the user accessKey and secretKey func fetchAliasKeys(args cli.Args) (string, string) { accessKey := "" secretKey := "" console.SetColor(cred, color.New(color.FgYellow, color.Italic)) isTerminal := term.IsTerminal(int(os.Stdin.Fd())) reader := bufio.NewReader(os.Stdin) argsNr := len(args) if argsNr == 2 { if isTerminal { fmt.Printf("%s", console.Colorize(cred, "Enter Access Key: ")) } value, _, _ := reader.ReadLine() accessKey = string(value) } else { accessKey = args.Get(2) } if argsNr == 2 || argsNr == 3 { if isTerminal { fmt.Printf("%s", console.Colorize(cred, "Enter Secret Key: ")) bytePassword, _ := term.ReadPassword(int(os.Stdin.Fd())) fmt.Printf("\n") secretKey = string(bytePassword) } else { value, _, _ := reader.ReadLine() secretKey = string(value) } } else { secretKey = args.Get(3) } return accessKey, secretKey } func mainAliasSet(cli *cli.Context, deprecated bool) error { console.SetColor("AliasMessage", color.New(color.FgGreen)) var ( args = cli.Args() alias = cleanAlias(args.Get(0)) url = trimTrailingSeparator(args.Get(1)) api = cli.String("api") path = cli.String("path") peerCert *x509.Certificate err *probe.Error ) // Support deprecated lookup flag if deprecated { lookup := strings.ToLower(strings.TrimSpace(cli.String("lookup"))) switch lookup { case "", "auto": path = "auto" case "path": path = "on" case "dns": path = "off" default: } } accessKey, secretKey := fetchAliasKeys(args) checkAliasSetSyntax(cli, accessKey, secretKey, deprecated) ctx, cancelAliasAdd := context.WithCancel(globalContext) defer cancelAliasAdd() if !globalInsecure && !globalJSON && term.IsTerminal(int(os.Stdout.Fd())) { peerCert, err = promptTrustSelfSignedCert(ctx, url, alias) fatalIf(err.Trace(alias, url, accessKey), "Unable to initialize new alias from the provided credentials.") } s3Config, err := BuildS3Config(ctx, alias, url, accessKey, secretKey, api, path, peerCert) fatalIf(err.Trace(alias, url, accessKey), "Unable to initialize new alias from the provided credentials.") msg := setAlias(alias, aliasConfigV10{ URL: s3Config.HostURL, AccessKey: s3Config.AccessKey, SecretKey: s3Config.SecretKey, API: s3Config.Signature, Path: path, }) // Add an alias with specified credentials. msg.op = "set" if deprecated { msg.op = "add" } printMsg(msg) return nil } // configurePeerCertificate adds the peer certificate to the // TLS root CAs of s3Config. Once configured, any client // initialized with this config trusts the given peer certificate. func configurePeerCertificate(s3Config *Config, peerCert *x509.Certificate) { tr, ok := s3Config.Transport.(*http.Transport) if !ok { return } switch { case tr == nil: if globalRootCAs != nil { globalRootCAs.AddCert(peerCert) } tr = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: newCustomDialContext(&Config{}), DialTLSContext: newCustomDialTLSContext(&tls.Config{RootCAs: globalRootCAs}), MaxIdleConnsPerHost: 256, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 10 * time.Second, DisableCompression: true, } case tr.TLSClientConfig == nil || tr.TLSClientConfig.RootCAs == nil: if globalRootCAs != nil { globalRootCAs.AddCert(peerCert) } tr.DialTLSContext = newCustomDialTLSContext(&tls.Config{RootCAs: globalRootCAs}) default: tr.TLSClientConfig.RootCAs.AddCert(peerCert) tr.DialTLSContext = newCustomDialTLSContext(tr.TLSClientConfig) } s3Config.Transport = tr } minio-client-0.0~20250403/cmd/anonymous-main.go000066400000000000000000000355301477450377600210540ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "io" "net/url" "os" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var anonymousFlags = []cli.Flag{ cli.BoolFlag{ Name: "recursive, r", Usage: "list recursively", }, } // Manage anonymous access to buckets and objects. var anonymousCmd = cli.Command{ Name: "anonymous", Usage: "manage anonymous access to buckets and objects", Action: mainAnonymous, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(anonymousFlags, globalFlags...), CustomHelpTemplate: `Name: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] set PERMISSION TARGET {{.HelpName}} [FLAGS] set-json FILE TARGET {{.HelpName}} [FLAGS] get TARGET {{.HelpName}} [FLAGS] get-json TARGET {{.HelpName}} [FLAGS] list TARGET {{if .VisibleFlags}} FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} PERMISSION: Allowed policies are: [private, public, download, upload]. FILE: A valid S3 anonymous JSON filepath. EXAMPLES: 1. Set bucket to "download" on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} set download s3/mybucket 2. Set bucket to "public" on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} set public s3/shared 3. Set bucket to "upload" on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} set upload s3/incoming 4. Set anonymous to "public" for bucket with prefix on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} set public s3/public-commons/images 5. Set a custom prefix based bucket anonymous on Amazon S3 cloud storage using a JSON file. {{.Prompt}} {{.HelpName}} set-json /path/to/anonymous.json s3/public-commons/images 6. Get bucket permissions. {{.Prompt}} {{.HelpName}} get s3/shared 7. Get bucket permissions in JSON format. {{.Prompt}} {{.HelpName}} get-json s3/shared 8. List policies set to a specified bucket. {{.Prompt}} {{.HelpName}} list s3/shared 9. List public object URLs recursively. {{.Prompt}} {{.HelpName}} --recursive links s3/shared/ `, } // anonymousRules contains anonymous rule type anonymousRules struct { Resource string `json:"resource"` Allow string `json:"allow"` } // String colorized access message. func (s anonymousRules) String() string { return console.Colorize("Anonymous", s.Resource+" => "+s.Allow+"") } // JSON jsonified anonymous message. func (s anonymousRules) JSON() string { anonymousJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(anonymousJSONBytes) } // anonymousMessage is container for anonymous command on bucket success and failure messages. type anonymousMessage struct { Operation string `json:"operation"` Status string `json:"status"` Bucket string `json:"bucket"` Perms accessPerms `json:"permission"` Anonymous map[string]interface{} `json:"anonymous,omitempty"` } // String colorized access message. func (s anonymousMessage) String() string { if s.Operation == "set" { return console.Colorize("Anonymous", "Access permission for `"+s.Bucket+"` is set to `"+string(s.Perms)+"`") } if s.Operation == "get" { return console.Colorize("Anonymous", "Access permission for `"+s.Bucket+"`"+" is `"+string(s.Perms)+"`") } if s.Operation == "set-json" { return console.Colorize("Anonymous", "Access permission for `"+s.Bucket+"`"+" is set from `"+string(s.Perms)+"`") } if s.Operation == "get-json" { anonymous, e := json.MarshalIndent(s.Anonymous, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(anonymous) } // nothing to print return "" } // JSON jsonified anonymous message. func (s anonymousMessage) JSON() string { anonymousJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(anonymousJSONBytes) } // anonymousLinksMessage is container for anonymous links command type anonymousLinksMessage struct { Status string `json:"status"` URL string `json:"url"` } // String colorized access message. func (s anonymousLinksMessage) String() string { return console.Colorize("Anonymous", s.URL) } // JSON jsonified anonymous message. func (s anonymousLinksMessage) JSON() string { anonymousJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(anonymousJSONBytes) } // checkAnonymousSyntax check for incoming syntax. func checkAnonymousSyntax(ctx *cli.Context) { argsLength := len(ctx.Args()) // Always print a help message when we have extra arguments if argsLength > 3 { showCommandHelpAndExit(ctx, 1) // last argument is exit code. } // Always print a help message when no arguments specified if argsLength < 1 { showCommandHelpAndExit(ctx, 1) } firstArg := ctx.Args().Get(0) secondArg := ctx.Args().Get(1) // More syntax checking switch accessPerms(firstArg) { case "set": // Always expect three arguments when setting a anonymous permission. if argsLength != 3 { showCommandHelpAndExit(ctx, 1) } if accessPerms(secondArg) != accessNone && accessPerms(secondArg) != accessDownload && accessPerms(secondArg) != accessUpload && accessPerms(secondArg) != accessPrivate && accessPerms(secondArg) != accessPublic { fatalIf(errDummy().Trace(), "Unrecognized permission `"+secondArg+"`. Allowed values are [private, public, download, upload].") } case "set-json": // Always expect three arguments when setting a anonymous permission. if argsLength != 3 { showCommandHelpAndExit(ctx, 1) } case "get", "get-json": // get or get-json always expects two arguments if argsLength != 2 { showCommandHelpAndExit(ctx, 1) } case "list": // Always expect an argument after list cmd if argsLength != 2 { showCommandHelpAndExit(ctx, 1) } case "links": // Always expect an argument after links cmd if argsLength != 2 { showCommandHelpAndExit(ctx, 1) } default: showCommandHelpAndExit(ctx, 1) } } // Convert an accessPerms to a string recognizable by minio-go func accessPermToString(perm accessPerms) string { anonymous := "" switch perm { case accessNone, accessPrivate: anonymous = "none" case accessDownload: anonymous = "readonly" case accessUpload: anonymous = "writeonly" case accessPublic: anonymous = "readwrite" case accessCustom: anonymous = "custom" } return anonymous } // doSetAccess do set access. func doSetAccess(ctx context.Context, targetURL string, targetPERMS accessPerms) *probe.Error { clnt, err := newClient(targetURL) if err != nil { return err.Trace(targetURL) } anonymous := accessPermToString(targetPERMS) if err = clnt.SetAccess(ctx, anonymous, false); err != nil { return err.Trace(targetURL, string(targetPERMS)) } return nil } // doSetAccessJSON do set access JSON. func doSetAccessJSON(ctx context.Context, targetURL string, targetPERMS accessPerms) *probe.Error { clnt, err := newClient(targetURL) if err != nil { return err.Trace(targetURL) } fileReader, e := os.Open(string(targetPERMS)) if e != nil { fatalIf(probe.NewError(e).Trace(), "Unable to set anonymous for `"+targetURL+"`.") } defer fileReader.Close() const maxJSONSize = 120 * 1024 // 120KiB configBuf := make([]byte, maxJSONSize+1) n, e := io.ReadFull(fileReader, configBuf) if e == nil { return probe.NewError(bytes.ErrTooLarge).Trace(targetURL) } if e != io.ErrUnexpectedEOF { return probe.NewError(e).Trace(targetURL) } configBytes := configBuf[:n] if err = clnt.SetAccess(ctx, string(configBytes), true); err != nil { return err.Trace(targetURL, string(targetPERMS)) } return nil } // Convert a minio-go permission to accessPerms type func stringToAccessPerm(perm string) accessPerms { var anonymous accessPerms switch perm { case "none": anonymous = accessPrivate case "readonly": anonymous = accessDownload case "writeonly": anonymous = accessUpload case "readwrite": anonymous = accessPublic case "private": anonymous = accessPrivate case "custom": anonymous = accessCustom } return anonymous } // doGetAccess do get access. func doGetAccess(ctx context.Context, targetURL string) (perms accessPerms, anonymousStr string, err *probe.Error) { clnt, err := newClient(targetURL) if err != nil { return "", "", err.Trace(targetURL) } perm, anonymousJSON, err := clnt.GetAccess(ctx) if err != nil { return "", "", err.Trace(targetURL) } return stringToAccessPerm(perm), anonymousJSON, nil } // doGetAccessRules do get access rules. func doGetAccessRules(ctx context.Context, targetURL string) (r map[string]string, err *probe.Error) { clnt, err := newClient(targetURL) if err != nil { return map[string]string{}, err.Trace(targetURL) } return clnt.GetAccessRules(ctx) } // Run anonymous list command func runAnonymousListCmd(args cli.Args) { ctx, cancelAnonymousList := context.WithCancel(globalContext) defer cancelAnonymousList() targetURL := args.First() policies, err := doGetAccessRules(ctx, targetURL) if err != nil { switch err.ToGoError().(type) { case APINotImplemented: fatalIf(err.Trace(), "Unable to list policies of a non S3 url `"+targetURL+"`.") default: fatalIf(err.Trace(targetURL), "Unable to list policies of target `"+targetURL+"`.") } } for k, v := range policies { printMsg(anonymousRules{Resource: k, Allow: v}) } } // Run anonymous links command func runAnonymousLinksCmd(args cli.Args, recursive bool) { ctx, cancelAnonymousLinks := context.WithCancel(globalContext) defer cancelAnonymousLinks() // Get alias/bucket/prefix argument targetURL := args.First() // Fetch all policies associated to the passed url policies, err := doGetAccessRules(ctx, targetURL) if err != nil { switch err.ToGoError().(type) { case APINotImplemented: fatalIf(err.Trace(), "Unable to list policies of a non S3 url `"+targetURL+"`.") default: fatalIf(err.Trace(targetURL), "Unable to list policies of target `"+targetURL+"`.") } } // Extract alias from the passed argument, we'll need it to // construct new pathes to list public objects alias, path := url2Alias(targetURL) // Iterate over anonymous rules to fetch public urls, then search // for objects under those urls for k, v := range policies { // Trim the asterisk in anonymous rules anonymousPath := strings.TrimSuffix(k, "*") // Check if current anonymous prefix is related to the url passed by the user if !strings.HasPrefix(anonymousPath, path) { continue } // Check if the found anonymous has read permission perm := stringToAccessPerm(v) if perm != accessDownload && perm != accessPublic { continue } // Construct the new path to search for public objects newURL := alias + "/" + anonymousPath clnt, err := newClient(newURL) fatalIf(err.Trace(newURL), "Unable to initialize target `"+targetURL+"`.") // Search for public objects for content := range clnt.List(globalContext, ListOptions{Recursive: recursive, ShowDir: DirFirst}) { if content.Err != nil { errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") continue } if content.Type.IsDir() && recursive { continue } // Encode public URL u, e := url.Parse(content.URL.String()) errorIf(probe.NewError(e), "Unable to parse url `%s`.", content.URL) publicURL := u.String() // Construct the message to be displayed to the user msg := anonymousLinksMessage{ Status: "success", URL: publicURL, } // Print the found object printMsg(msg) } } } // Run anonymous cmd to fetch set permission func runAnonymousCmd(args cli.Args) { ctx, cancelAnonymous := context.WithCancel(globalContext) defer cancelAnonymous() var targetURL, anonymousStr string var perms accessPerms var probeErr *probe.Error operation := args.First() switch operation { case "set": perms = accessPerms(args.Get(1)) if !perms.isValidAccessPERM() { fatalIf(errDummy().Trace(), "Invalid access permission: `"+string(perms)+"`.") } targetURL = args.Get(2) probeErr = doSetAccess(ctx, targetURL, perms) if probeErr == nil { perms, _, probeErr = doGetAccess(ctx, targetURL) } case "set-json": perms = accessPerms(args.Get(1)) if !perms.isValidAccessFile() { fatalIf(errDummy().Trace(), "Invalid access file: `"+string(perms)+"`.") } targetURL = args.Get(2) probeErr = doSetAccessJSON(ctx, targetURL, perms) case "get", "get-json": targetURL = args.Get(1) perms, anonymousStr, probeErr = doGetAccess(ctx, targetURL) default: fatalIf(errDummy().Trace(), "Invalid operation: `"+operation+"`.") } // Upon error exit. if probeErr != nil { switch probeErr.ToGoError().(type) { case APINotImplemented: fatalIf(probeErr.Trace(), "Unable to "+operation+" anonymous of a non S3 url `"+targetURL+"`.") default: fatalIf(probeErr.Trace(targetURL, string(perms)), "Unable to "+operation+" anonymous `"+string(perms)+"` for `"+targetURL+"`.") } } anonymousJSON := map[string]interface{}{} if anonymousStr != "" { e := json.Unmarshal([]byte(anonymousStr), &anonymousJSON) fatalIf(probe.NewError(e), "Unable to unmarshal custom anonymous file.") } printMsg(anonymousMessage{ Status: "success", Operation: operation, Bucket: targetURL, Perms: perms, Anonymous: anonymousJSON, }) } func mainAnonymous(ctx *cli.Context) error { // check 'anonymous' cli arguments. checkAnonymousSyntax(ctx) // Additional command speific theme customization. console.SetColor("Anonymous", color.New(color.FgGreen, color.Bold)) switch ctx.Args().First() { case "set", "set-json", "get", "get-json": // anonymous set [private|public|download|upload] alias/bucket/prefix // anonymous set-json path-to-anonymous-json-file alias/bucket/prefix // anonymous get alias/bucket/prefix // anonymous get-json alias/bucket/prefix runAnonymousCmd(ctx.Args()) case "list": // anonymous list alias/bucket/prefix runAnonymousListCmd(ctx.Args().Tail()) case "links": // anonymous links alias/bucket/prefix runAnonymousLinksCmd(ctx.Args().Tail(), ctx.Bool("recursive")) default: // Shows command example and exit showCommandHelpAndExit(ctx, 1) } return nil } minio-client-0.0~20250403/cmd/arg-kvs.go000066400000000000000000000033351477450377600174520ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd // argKV - is a shorthand of each key value. type argKV struct { Key string `json:"key"` Value string `json:"value"` } // argKVS - is a shorthand for some wrapper functions // to operate on list of key values. type argKVS []argKV // Empty - return if kv is empty func (kvs argKVS) Empty() bool { return len(kvs) == 0 } // Set sets a value, if not sets a default value. func (kvs *argKVS) Set(key, value string) { for i, kv := range *kvs { if kv.Key == key { (*kvs)[i] = argKV{ Key: key, Value: value, } return } } *kvs = append(*kvs, argKV{ Key: key, Value: value, }) } // Get - returns the value of a key, if not found returns empty. func (kvs argKVS) Get(key string) string { v, ok := kvs.Lookup(key) if ok { return v } return "" } // Lookup - lookup a key in a list of argKVS func (kvs argKVS) Lookup(key string) (string, bool) { for _, kv := range kvs { if kv.Key == key { return kv.Value, true } } return "", false } minio-client-0.0~20250403/cmd/auto-complete.go000066400000000000000000000450501477450377600206560ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" "path/filepath" "sort" "strings" "github.com/minio/cli" "github.com/posener/complete" ) // fsComplete knows how to complete file/dir names by the given path type fsComplete struct{} // predictPathWithTilde completes an FS path which starts with a `~/` func (fs fsComplete) predictPathWithTilde(a complete.Args) []string { homeDir, e := os.UserHomeDir() if e != nil || homeDir == "" { return nil } // Clean the home directory path homeDir = strings.TrimRight(homeDir, "/") // Replace the first occurrence of ~ with the real path and complete a.Last = strings.Replace(a.Last, "~", homeDir, 1) predictions := complete.PredictFiles("*").Predict(a) // Restore ~ to avoid disturbing the completion user experience for i := range predictions { predictions[i] = strings.Replace(predictions[i], homeDir, "~", 1) } return predictions } func (fs fsComplete) Predict(a complete.Args) []string { if strings.HasPrefix(a.Last, "~/") { return fs.predictPathWithTilde(a) } return complete.PredictFiles("*").Predict(a) } func completeAdminConfigKeys(aliasPath, keyPrefix string) (prediction []string) { // Convert alias/bucket/incompl to alias/bucket/ to list its contents parentDirPath := filepath.Dir(aliasPath) + "/" clnt, err := newAdminClient(parentDirPath) if err != nil { return nil } h, e := clnt.HelpConfigKV(globalContext, "", "", false) if e != nil { return nil } for _, hkv := range h.KeysHelp { if strings.HasPrefix(hkv.Key, keyPrefix) { prediction = append(prediction, hkv.Key) } } return prediction } // Complete S3 path. If the prediction result is only one directory, // then recursively scans it. This is needed to satisfy posener/complete // (look at posener/complete.PredictFiles) func completeS3Path(s3Path string) (prediction []string) { // Convert alias/bucket/incompl to alias/bucket/ to list its contents parentDirPath := filepath.Dir(s3Path) + "/" clnt, err := newClient(parentDirPath) if err != nil { return nil } // Calculate alias from the path alias := splitStr(s3Path, "/", 3)[0] // List dirPath content and only pick elements that corresponds // to the path that we want to complete for content := range clnt.List(globalContext, ListOptions{Recursive: false, ShowDir: DirFirst}) { cmplS3Path := alias + getKey(content) if content.Type.IsDir() { if !strings.HasSuffix(cmplS3Path, "/") { cmplS3Path += "/" } } if strings.HasPrefix(cmplS3Path, s3Path) { prediction = append(prediction, cmplS3Path) } } // If completion found only one directory, recursively scan it. if len(prediction) == 1 && strings.HasSuffix(prediction[0], "/") { prediction = append(prediction, completeS3Path(prediction[0])...) } return } type adminConfigComplete struct{} func (adm adminConfigComplete) Predict(a complete.Args) (prediction []string) { defer func() { sort.Strings(prediction) }() loadMcConfig = loadMcConfigFactory() conf, err := loadMcConfig() if err != nil { return } // We have already predicted the keys, we are done. if len(a.Completed) == 3 { return } arg := a.Last lastArg := a.LastCompleted if _, ok := conf.Aliases[filepath.Clean(a.LastCompleted)]; !ok { if strings.IndexByte(arg, '/') == -1 { // Only predict alias since '/' is not found for alias := range conf.Aliases { if strings.HasPrefix(alias, arg) { prediction = append(prediction, alias+"/") } } } else { prediction = completeAdminConfigKeys(arg, "") } } else { prediction = completeAdminConfigKeys(lastArg, arg) } return } // s3Complete knows how to complete an mc s3 path type s3Complete struct { deepLevel int } func (s3 s3Complete) Predict(a complete.Args) (prediction []string) { defer func() { sort.Strings(prediction) }() loadMcConfig = loadMcConfigFactory() conf, err := loadMcConfig() if err != nil { return nil } arg := a.Last if strings.IndexByte(arg, '/') == -1 { // Only predict alias since '/' is not found for alias := range conf.Aliases { if strings.HasPrefix(alias, arg) { prediction = append(prediction, alias+"/") } } if len(prediction) == 1 && strings.HasSuffix(prediction[0], "/") { prediction = append(prediction, completeS3Path(prediction[0])...) } } else { // Complete S3 path until the specified path deep level if s3.deepLevel > 0 { if strings.Count(arg, "/") >= s3.deepLevel { return []string{arg} } } // Predict S3 path prediction = completeS3Path(arg) } return } // aliasComplete only completes aliases type aliasComplete struct{} func (al aliasComplete) Predict(a complete.Args) (prediction []string) { defer func() { sort.Strings(prediction) }() loadMcConfig = loadMcConfigFactory() conf, err := loadMcConfig() if err != nil { return nil } arg := a.Last for alias := range conf.Aliases { if strings.HasPrefix(alias, arg) { prediction = append(prediction, alias+"/") } } return } var ( adminConfigCompleter = adminConfigComplete{} s3Completer = s3Complete{} aliasCompleter = aliasComplete{} fsCompleter = fsComplete{} ) // The list of all commands supported by mc with their mapping // with their bash completer function var completeCmds = map[string]complete.Predictor{ // S3 API level commands "/ls": complete.PredictOr(s3Completer, fsCompleter), "/cp": complete.PredictOr(s3Completer, fsCompleter), "/mv": complete.PredictOr(s3Completer, fsCompleter), "/rm": complete.PredictOr(s3Completer, fsCompleter), "/rb": complete.PredictOr(s3Complete{deepLevel: 2}, fsCompleter), "/cat": complete.PredictOr(s3Completer, fsCompleter), "/head": complete.PredictOr(s3Completer, fsCompleter), "/diff": complete.PredictOr(s3Completer, fsCompleter), "/find": complete.PredictOr(s3Completer, fsCompleter), "/mirror": complete.PredictOr(s3Completer, fsCompleter), "/pipe": complete.PredictOr(s3Completer, fsCompleter), "/stat": complete.PredictOr(s3Completer, fsCompleter), "/watch": complete.PredictOr(s3Completer, fsCompleter), "/anonymous": complete.PredictOr(s3Completer, fsCompleter), "/tree": complete.PredictOr(s3Complete{deepLevel: 2}, fsCompleter), "/du": complete.PredictOr(s3Complete{deepLevel: 2}, fsCompleter), "/retention/set": s3Completer, "/retention/clear": s3Completer, "/retention/info": s3Completer, "/legalhold/set": s3Completer, "/legalhold/clear": s3Completer, "/legalhold/info": s3Completer, "/sql": s3Completer, "/mb": aliasCompleter, "/event/add": s3Complete{deepLevel: 2}, "/event/list": s3Complete{deepLevel: 2}, "/event/remove": s3Complete{deepLevel: 2}, "/encrypt/set": s3Complete{deepLevel: 2}, "/encrypt/info": s3Complete{deepLevel: 2}, "/encrypt/clear": s3Complete{deepLevel: 2}, "/replicate/add": s3Complete{deepLevel: 2}, "/replicate/edit": s3Complete{deepLevel: 2}, "/replicate/update": s3Complete{deepLevel: 2}, "/replicate/list": s3Complete{deepLevel: 2}, "/replicate/remove": s3Complete{deepLevel: 2}, "/replicate/backlog": s3Complete{deepLevel: 2}, "/replicate/export": s3Complete{deepLevel: 2}, "/replicate/import": s3Complete{deepLevel: 2}, "/replicate/status": s3Complete{deepLevel: 2}, "/replicate/resync/start": s3Complete{deepLevel: 3}, "/replicate/resync/status": s3Complete{deepLevel: 3}, "/tag/list": s3Completer, "/tag/remove": s3Completer, "/tag/set": s3Completer, "/version/info": s3Complete{deepLevel: 2}, "/version/enable": s3Complete{deepLevel: 2}, "/version/suspend": s3Complete{deepLevel: 2}, "/lock/compliance": s3Completer, "/lock/governance": s3Completer, "/lock/clear": s3Completer, "/lock/info": s3Completer, "/share/download": s3Completer, "/share/list": nil, "/share/upload": s3Completer, "/ilm/list": s3Complete{deepLevel: 2}, "/ilm/add": s3Complete{deepLevel: 2}, "/ilm/edit": s3Complete{deepLevel: 2}, "/ilm/remove": s3Complete{deepLevel: 2}, "/ilm/export": s3Complete{deepLevel: 2}, "/ilm/import": s3Complete{deepLevel: 2}, "/ilm/restore": s3Completer, "/ilm/rule/list": s3Complete{deepLevel: 2}, "/ilm/rule/add": s3Complete{deepLevel: 2}, "/ilm/rule/edit": s3Complete{deepLevel: 2}, "/ilm/rule/remove": s3Complete{deepLevel: 2}, "/ilm/rule/export": s3Complete{deepLevel: 2}, "/ilm/rule/import": s3Complete{deepLevel: 2}, "/ilm/rule/restore": s3Completer, "/undo": s3Completer, // Admin API commands MinIO only. "/admin/heal": s3Completer, "/admin/info": aliasCompleter, "/admin/logs": aliasCompleter, "/admin/config/get": adminConfigCompleter, "/admin/config/set": adminConfigCompleter, "/admin/config/reset": adminConfigCompleter, "/admin/config/import": aliasCompleter, "/admin/config/export": aliasCompleter, "/admin/config/history": aliasCompleter, "/admin/config/restore": aliasCompleter, "/admin/decom/start": aliasCompleter, "/admin/decom/status": aliasCompleter, "/admin/decom/cancel": aliasCompleter, "/admin/decommission/start": aliasCompleter, "/admin/decommission/status": aliasCompleter, "/admin/decommission/cancel": aliasCompleter, "/admin/rebalance/start": aliasCompleter, "/admin/rebalance/status": aliasCompleter, "/admin/rebalance/stop": aliasCompleter, "/admin/trace": aliasCompleter, "/admin/speedtest": aliasCompleter, "/admin/console": aliasCompleter, "/admin/update": aliasCompleter, "/admin/inspect": s3Completer, "/admin/top/locks": aliasCompleter, "/admin/top/api": aliasCompleter, "/admin/scanner/status": aliasCompleter, "/admin/scanner/trace": aliasCompleter, "/admin/service/stop": aliasCompleter, "/admin/service/restart": aliasCompleter, "/admin/service/freeze": aliasCompleter, "/admin/service/unfreeze": aliasCompleter, "/admin/prometheus/generate": aliasCompleter, "/admin/prometheus/metrics": aliasCompleter, "/admin/profile/start": aliasCompleter, "/admin/profile/stop": aliasCompleter, "/idp/openid/add": aliasCompleter, "/idp/openid/update": aliasCompleter, "/idp/openid/remove": aliasCompleter, "/idp/openid/list": aliasCompleter, "/idp/openid/info": aliasCompleter, "/idp/openid/enable": aliasCompleter, "/idp/openid/disable": aliasCompleter, "/idp/ldap/add": aliasCompleter, "/idp/ldap/update": aliasCompleter, "/idp/ldap/remove": aliasCompleter, "/idp/ldap/list": aliasCompleter, "/idp/ldap/info": aliasCompleter, "/idp/ldap/enable": aliasCompleter, "/idp/ldap/disable": aliasCompleter, "/idp/ldap/policy/entities": aliasCompleter, "/idp/ldap/policy/attach": aliasCompleter, "/idp/ldap/policy/detach": aliasCompleter, "/idp/ldap/accesskey/create": aliasCompleter, "/idp/ldap/accesskey/create-with-login": aliasCompleter, "/idp/ldap/accesskey/list": aliasCompleter, "/idp/ldap/accesskey/ls": aliasCompleter, "/idp/ldap/accesskey/remove": aliasCompleter, "/idp/ldap/accesskey/rm": aliasCompleter, "/idp/ldap/accesskey/info": aliasCompleter, "/idp/ldap/accesskey/edit": aliasCompleter, "/idp/ldap/accesskey/enable": aliasCompleter, "/idp/ldap/accesskey/disable": aliasCompleter, "/idp/ldap/accesskey/sts-revoke": aliasCompleter, "/admin/accesskey/create": aliasCompleter, "/admin/accesskey/list": aliasCompleter, "/admin/accesskey/ls": aliasCompleter, "/admin/accesskey/remove": aliasCompleter, "/admin/accesskey/rm": aliasCompleter, "/admin/accesskey/info": aliasCompleter, "/admin/accesskey/edit": aliasCompleter, "/admin/accesskey/enable": aliasCompleter, "/admin/accesskey/disable": aliasCompleter, "/admin/accesskey/sts-revoke": aliasCompleter, "/admin/policy/info": aliasCompleter, "/admin/policy/update": aliasCompleter, "/admin/policy/add": aliasCompleter, "/admin/policy/remove": aliasCompleter, "/admin/policy/create": aliasCompleter, "/admin/policy/list": aliasCompleter, "/admin/policy/attach": aliasCompleter, "/admin/policy/detach": aliasCompleter, "/admin/policy/entities": aliasCompleter, "/admin/user/add": aliasCompleter, "/admin/user/disable": aliasCompleter, "/admin/user/enable": aliasCompleter, "/admin/user/list": aliasCompleter, "/admin/user/remove": aliasCompleter, "/admin/user/info": aliasCompleter, "/admin/user/policy": aliasCompleter, "/admin/user/svcacct/add": aliasCompleter, "/admin/user/svcacct/list": aliasCompleter, "/admin/user/svcacct/remove": aliasCompleter, "/admin/user/svcacct/info": aliasCompleter, "/admin/user/svcacct/edit": aliasCompleter, "/admin/user/svcacct/set": aliasCompleter, "/admin/user/svcacct/enable": aliasCompleter, "/admin/user/svcacct/disable": aliasCompleter, "/admin/user/sts/info": aliasCompleter, "/admin/group/add": aliasCompleter, "/admin/group/disable": aliasCompleter, "/admin/group/enable": aliasCompleter, "/admin/group/list": aliasCompleter, "/admin/group/remove": aliasCompleter, "/admin/group/info": aliasCompleter, "/admin/bucket/remote/add": aliasCompleter, "/admin/bucket/remote/edit": aliasCompleter, "/admin/bucket/remote/remove": aliasCompleter, "/admin/bucket/quota": aliasCompleter, "/admin/bucket/info": s3Complete{deepLevel: 2}, "/admin/kms/key/create": aliasCompleter, "/admin/kms/key/status": aliasCompleter, "/admin/kms/key/list": aliasCompleter, "/admin/subnet/health": aliasCompleter, "/admin/subnet/register": aliasCompleter, "/admin/tier/add": nil, "/admin/tier/edit": nil, "/admin/tier/list": nil, "/admin/tier/info": nil, "/admin/tier/remove": nil, "/admin/tier/verify": nil, "/ilm/tier/info": nil, "/ilm/tier/list": nil, "/ilm/tier/add": nil, "/ilm/tier/update": nil, "/ilm/tier/check": nil, "/ilm/tier/remove": nil, "/admin/replicate/add": aliasCompleter, "/admin/replicate/update": aliasCompleter, "/admin/replicate/edit": aliasCompleter, "/admin/replicate/info": aliasCompleter, "/admin/replicate/status": aliasCompleter, "/admin/replicate/remove": aliasCompleter, "/admin/replicate/resync/start": aliasCompleter, "/admin/replicate/resync/cancel": aliasCompleter, "/admin/replicate/resync/status": aliasCompleter, "/admin/cluster/bucket/export": aliasCompleter, "/admin/cluster/bucket/import": aliasCompleter, "/admin/cluster/iam/export": aliasCompleter, "/admin/cluster/iam/import": aliasCompleter, "/alias/set": nil, "/alias/list": aliasCompleter, "/alias/remove": aliasCompleter, "/alias/import": nil, "/alias/export": aliasCompleter, "/support/callhome": aliasCompleter, "/support/register": aliasCompleter, "/support/diag": aliasCompleter, "/support/profile": aliasCompleter, "/support/proxy/set": aliasCompleter, "/support/proxy/show": aliasCompleter, "/support/proxy/remove": aliasCompleter, "/support/inspect": aliasCompleter, "/support/perf": aliasCompleter, "/support/metrics": aliasCompleter, "/support/status": aliasCompleter, "/support/top/locks": aliasCompleter, "/support/top/api": aliasCompleter, "/support/top/drive": aliasCompleter, "/support/top/disk": aliasCompleter, "/support/top/net": aliasCompleter, "/support/top/rpc": aliasCompleter, "/support/upload": aliasCompleter, "/license/register": aliasCompleter, "/license/info": aliasCompleter, "/license/update": aliasCompleter, "/update": nil, "/ready": aliasCompleter, "/ping": aliasCompleter, "/od": nil, "/batch/generate": aliasCompleter, "/batch/start": aliasCompleter, "/batch/list": aliasCompleter, "/batch/status": aliasCompleter, "/batch/describe": aliasCompleter, "/batch/cancel": aliasCompleter, "/quota/set": aliasCompleter, "/quota/info": aliasCompleter, "/quota/clear": aliasCompleter, "/put": complete.PredictOr(s3Completer, fsCompleter), "/get": complete.PredictOr(s3Completer, fsCompleter), "/cors/set": s3Complete{deepLevel: 2}, "/cors/get": s3Complete{deepLevel: 2}, "/cors/remove": s3Complete{deepLevel: 2}, } // flagsToCompleteFlags transforms a cli.Flag to complete.Flags // understood by posener/complete library. func flagsToCompleteFlags(flags []cli.Flag) complete.Flags { complFlags := make(complete.Flags) for _, f := range flags { for _, s := range strings.Split(f.GetName(), ",") { var flagName string s = strings.TrimSpace(s) if len(s) == 1 { flagName = "-" + s } else { flagName = "--" + s } complFlags[flagName] = complete.PredictNothing } } return complFlags } // This function recursively transforms cli.Command to complete.Command // understood by posener/complete library. func cmdToCompleteCmd(cmd cli.Command, parentPath string) complete.Command { var complCmd complete.Command complCmd.Sub = make(complete.Commands) for _, subCmd := range cmd.Subcommands { if subCmd.Hidden { continue } complCmd.Sub[subCmd.Name] = cmdToCompleteCmd(subCmd, parentPath+"/"+cmd.Name) for _, alias := range subCmd.Aliases { complCmd.Sub[alias] = cmdToCompleteCmd(subCmd, parentPath+"/"+cmd.Name) } } complCmd.Flags = flagsToCompleteFlags(cmd.Flags) complCmd.Args = completeCmds[parentPath+"/"+cmd.Name] return complCmd } // Main function to answer to bash completion calls func mainComplete() error { // Recursively register all commands and subcommands // along with global and local flags complCmds := make(complete.Commands) for _, cmd := range appCmds { if cmd.Hidden { continue } complCmds[cmd.Name] = cmdToCompleteCmd(cmd, "") for _, alias := range cmd.Aliases { complCmds[alias] = cmdToCompleteCmd(cmd, "") } } complFlags := flagsToCompleteFlags(globalFlags) mcComplete := complete.Command{ Sub: complCmds, GlobalFlags: complFlags, } // Answer to bash completion call complete.New(filepath.Base(os.Args[0]), mcComplete).Run() return nil } minio-client-0.0~20250403/cmd/auto-complete_test.go000066400000000000000000000030601477450377600217100ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "testing" "github.com/minio/cli" ) func TestAutoCompletionCompletness(t *testing.T) { var checkCompletion func(cmd cli.Command, cmdPath string) error checkCompletion = func(cmd cli.Command, cmdPath string) error { if cmd.Subcommands != nil { for _, subCmd := range cmd.Subcommands { if cmd.Hidden { continue } err := checkCompletion(subCmd, cmdPath+"/"+subCmd.Name) if err != nil { return err } } return nil } _, ok := completeCmds[cmdPath] if !ok && !cmd.Hidden { return fmt.Errorf("Completion for `%s` not found", cmdPath) } return nil } for _, cmd := range appCmds { if cmd.Hidden { continue } err := checkCompletion(cmd, "/"+cmd.Name) if err != nil { t.Fatalf("Missing completion function: %v", err) } } } minio-client-0.0~20250403/cmd/batch-cancel.go000066400000000000000000000060221477450377600204000ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var batchCancelFlags = []cli.Flag{ cli.StringFlag{ Name: "id", Usage: "job id", }, } var batchCancelCmd = cli.Command{ Name: "cancel", Usage: "cancel ongoing batch job", Action: mainBatchCancel, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(batchCancelFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET JOBFILE FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Cancel ongoing batch job: {{.Prompt}} {{.HelpName}} myminio `, } // batchCancelMessage container for file batchCancel messages type batchCancelMessage struct { Status string `json:"status"` JobID string `json:"job-id"` } // String colorized batchCancel message func (c batchCancelMessage) String() string { return console.Colorize("batchCancel", fmt.Sprintf("Successfully canceled batch job `%s`", c.JobID)) } // JSON jsonified batchCancel message func (c batchCancelMessage) JSON() string { c.Status = "success" batchCancelMessageBytes, e := json.MarshalIndent(c, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(batchCancelMessageBytes) } // checkBatchCancelSyntax - validate all the passed arguments func checkBatchCancelSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainBatchCancel is the handle for "mc batch cancel" command. func mainBatchCancel(ctx *cli.Context) error { checkBatchCancelSyntax(ctx) console.SetColor("BatchCancel", color.New(color.FgGreen, color.Bold)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) jobID := args.Get(1) // Start a new MinIO Admin Client adminClient, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") ctxt, cancel := context.WithCancel(globalContext) defer cancel() e := adminClient.CancelBatchJob(ctxt, jobID) fatalIf(probe.NewError(e), "Unable to cancel job") printMsg(batchCancelMessage{ Status: "Canceled", JobID: jobID, }) return nil } minio-client-0.0~20250403/cmd/batch-describe.go000066400000000000000000000042251477450377600207360ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" ) var batchDescribeCmd = cli.Command{ Name: "describe", Usage: "describe job definition for a job", Action: mainBatchDescribe, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET JOBID FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Describe current batch job definition: {{.Prompt}} {{.HelpName}} myminio KwSysDpxcBU9FNhGkn2dCf `, } // checkBatchDescribeSyntax - validate all the passed arguments func checkBatchDescribeSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainBatchDescribe is the handle for "mc batch create" command. func mainBatchDescribe(ctx *cli.Context) error { checkBatchDescribeSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) jobID := args.Get(1) // Start a new MinIO Admin Client adminClient, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") ctxt, cancel := context.WithCancel(globalContext) defer cancel() job, e := adminClient.DescribeBatchJob(ctxt, jobID) fatalIf(probe.NewError(e), "Unable to fetch the job definition") fmt.Println(job) return nil } minio-client-0.0~20250403/cmd/batch-generate.go000066400000000000000000000073651477450377600207600ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var batchGenerateCmd = cli.Command{ Name: "generate", Usage: "generate a new batch job definition", Action: mainBatchGenerate, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET JOBTYPE JOBTYPE: - replicate - keyrotate - expire Use the special value "list" to request server to list supported job types. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Generate a new batch 'replication' job definition: {{.Prompt}} {{.HelpName}} myminio replicate > replication.yaml 2. List all supported job types: {{.Prompt}} {{.HelpName}} myminio list `, } // checkBatchGenerateSyntax - validate all the passed arguments func checkBatchGenerateSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainBatchGenerate is the handle for "mc batch generate" command. func mainBatchGenerate(ctx *cli.Context) error { checkBatchGenerateSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) jobType := args.Get(1) // Start a new MinIO Admin Client adminClient, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") if jobType == "list" { supportedJobTypes, apiUnavailable, e := adminClient.GetSupportedBatchJobTypes(globalContext) if e != nil { fatalIf(probe.NewError(e), "Unable to list supported job types") } if apiUnavailable { // Fallback to default supported jobs. supportedJobTypes = madmin.SupportedJobTypes } if globalJSON { j, err := json.Marshal(supportedJobTypes) if err != nil { fatalIf(probe.NewError(err), "Unable to marshal supported job types") } fmt.Println(string(j)) } else { for _, jobType := range supportedJobTypes { fmt.Println(jobType) } } return nil } // Try GenerateV2 API. template, apiUnavailable, e := adminClient.GenerateBatchJobV2(globalContext, madmin.GenerateBatchJobOpts{ Type: madmin.BatchJobType(jobType), }) if e != nil { fatalIf(probe.NewError(e), "Unable to generate template for %s", args.Get(1)) } // Check if server supports GenerateV2 API. if !apiUnavailable { fmt.Println(template) return nil } // As API is not supported we fallback to returning the static job template. var found bool for _, job := range madmin.SupportedJobTypes { if jobType == string(job) { found = true break } } if !found { fatalIf(errInvalidArgument().Trace(jobType), "Unable to generate a job template for the specified job type") } out, e := adminClient.GenerateBatchJob(globalContext, madmin.GenerateBatchJobOpts{ Type: madmin.BatchJobType(jobType), }) fatalIf(probe.NewError(e), "Unable to generate %s", args.Get(1)) fmt.Println(string(out)) return nil } minio-client-0.0~20250403/cmd/batch-list.go000066400000000000000000000076251477450377600201400ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "strings" humanize "github.com/dustin/go-humanize" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/olekukonko/tablewriter" ) var batchListFlags = []cli.Flag{ cli.StringFlag{ Name: "type", Usage: "list all current batch jobs via job type", }, } var batchListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list all current batch jobs", Action: mainBatchList, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(batchListFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List all current batch jobs: {{.Prompt}} {{.HelpName}} myminio 2. List all current batch jobs of type 'replicate': {{.Prompt}} {{.HelpName}} myminio/ --type "replicate" `, } // batchListMessage container for file batchList messages type batchListMessage struct { Status string `json:"status"` Jobs []madmin.BatchJobResult `json:"jobs"` } // String colorized batchList message func (c batchListMessage) String() string { if len(c.Jobs) == 0 { return "currently no jobs are running" } var s strings.Builder // Set table header table := tablewriter.NewWriter(&s) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) table.SetHeader([]string{"ID", "TYPE", "USER", "STARTED"}) data := make([][]string, 0, 4) for _, job := range c.Jobs { data = append(data, []string{ job.ID, string(job.Type), job.User, humanize.Time(job.Started), }) } table.AppendBulk(data) table.Render() return s.String() } // JSON jsonified batchList message func (c batchListMessage) JSON() string { c.Status = "success" batchListMessageBytes, e := json.MarshalIndent(c, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(batchListMessageBytes) } // checkBatchListSyntax - validate all the passed arguments func checkBatchListSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainBatchList is the handle for "mc batch create" command. func mainBatchList(ctx *cli.Context) error { checkBatchListSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Start a new MinIO Admin Client adminClient, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") ctxt, cancel := context.WithCancel(globalContext) defer cancel() res, e := adminClient.ListBatchJobs(ctxt, &madmin.ListBatchJobsFilter{ ByJobType: ctx.String("type"), }) fatalIf(probe.NewError(e), "Unable to list jobs") printMsg(batchListMessage{ Status: "success", Jobs: res.Jobs, }) return nil } minio-client-0.0~20250403/cmd/batch-main.go000066400000000000000000000026541477450377600201060ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var batchSubcommands = []cli.Command{ batchGenerateCmd, batchStartCmd, batchListCmd, batchStatusCmd, batchDescribeCmd, // batchSuspendResumeCmd, batchCancelCmd, } var batchCmd = cli.Command{ Name: "batch", Usage: "manage batch jobs", Action: mainBatch, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: batchSubcommands, HideHelpCommand: true, } // mainBatch is the handle for "mc batch" command. func mainBatch(ctx *cli.Context) error { commandNotFound(ctx, batchSubcommands) return nil // Sub-commands like "generate", "list", "info" have their own main. } minio-client-0.0~20250403/cmd/batch-start.go000066400000000000000000000061331477450377600203130ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "os" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var batchStartCmd = cli.Command{ Name: "start", Usage: "start a new batch job", Action: mainBatchStart, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET JOBFILE FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Start a new batch 'replication' job: {{.Prompt}} {{.HelpName}} myminio ./replication.yaml `, } // batchStartMessage container for file batchStart messages type batchStartMessage struct { Status string `json:"status"` Result madmin.BatchJobResult `json:"result"` } // String colorized batchStart message func (c batchStartMessage) String() string { return console.Colorize("BatchStart", fmt.Sprintf("Successfully started '%s' job `%s` on '%s'", c.Result.Type, c.Result.ID, c.Result.Started)) } // JSON jsonified batchStart message func (c batchStartMessage) JSON() string { c.Status = "success" batchStartMessageBytes, e := json.MarshalIndent(c, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(batchStartMessageBytes) } // checkBatchStartSyntax - validate all the passed arguments func checkBatchStartSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainBatchStart is the handle for "mc batch create" command. func mainBatchStart(ctx *cli.Context) error { checkBatchStartSyntax(ctx) console.SetColor("BatchStart", color.New(color.FgGreen, color.Bold)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Start a new MinIO Admin Client adminClient, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") buf, e := os.ReadFile(args.Get(1)) fatalIf(probe.NewError(e), "Unable to read %s", args.Get(1)) ctxt, cancel := context.WithCancel(globalContext) defer cancel() res, e := adminClient.StartBatchJob(ctxt, string(buf)) fatalIf(probe.NewError(e), "Unable to start job") printMsg(batchStartMessage{ Status: "success", Result: res, }) return nil } minio-client-0.0~20250403/cmd/batch-status.go000066400000000000000000000163741477450377600205110ustar00rootroot00000000000000package cmd import ( "context" "errors" "fmt" "strings" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/dustin/go-humanize" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" "github.com/olekukonko/tablewriter" ) var batchStatusCmd = cli.Command{ Name: "status", Usage: "summarize job events on MinIO server in real-time", Action: mainBatchStatus, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET JOBID FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display current in-progress JOB events. {{.Prompt}} {{.HelpName}} myminio/ KwSysDpxcBU9FNhGkn2dCf `, } // batchJobStatusMessage container for batch job status messages type batchJobStatusMessage struct { Status string `json:"status"` Metric madmin.JobMetric `json:"metric"` } // JSON jsonified batchJobStatusMessage message func (c batchJobStatusMessage) JSON() string { batchJobStatusMessageBytes, e := json.MarshalIndent(c, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(batchJobStatusMessageBytes) } func (c batchJobStatusMessage) String() string { return c.JSON() } // checkBatchStatusSyntax - validate all the passed arguments func checkBatchStatusSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainBatchStatus(ctx *cli.Context) error { checkBatchStatusSyntax(ctx) aliasedURL := ctx.Args().Get(0) jobID := ctx.Args().Get(1) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") ctxt, cancel := context.WithCancel(globalContext) defer cancel() _, e := client.DescribeBatchJob(ctxt, jobID) nosuchJob := madmin.ToErrorResponse(e).Code == "XMinioAdminNoSuchJob" if nosuchJob { e = nil if !globalJSON { console.Infoln("Unable to find an active job, attempting to list from previously run jobs") } } fatalIf(probe.NewError(e), "Unable to lookup job status") ui := tea.NewProgram(initBatchJobMetricsUI(jobID)) if nosuchJob { go func() { res, e := client.BatchJobStatus(ctxt, jobID) fatalIf(probe.NewError(e), "Unable to lookup job status") if globalJSON { printMsg(batchJobStatusMessage{ Status: "success", Metric: res.LastMetric, }) if res.LastMetric.Complete || res.LastMetric.Failed { cancel() return } } else { ui.Send(res.LastMetric) } }() } else { go func() { opts := madmin.MetricsOptions{ Type: madmin.MetricsBatchJobs, ByJobID: jobID, Interval: time.Second, } e := client.Metrics(ctxt, opts, func(metrics madmin.RealtimeMetrics) { if globalJSON { if metrics.Aggregated.BatchJobs == nil { cancel() return } job, ok := metrics.Aggregated.BatchJobs.Jobs[jobID] if !ok { cancel() return } m := batchJobStatusMessage{ Status: "in-progress", Metric: job, } switch { case job.Complete: m.Status = "complete" case job.Failed: m.Status = "failed" default: // leave as is with in-progress } printMsg(m) if job.Complete || job.Failed { cancel() return } } else { ui.Send(metrics.Aggregated.BatchJobs.Jobs[jobID]) } }) if e != nil && !errors.Is(e, context.Canceled) { fatalIf(probe.NewError(e).Trace(ctx.Args()...), "Unable to get current batch status") } }() } if !globalJSON { if _, e := ui.Run(); e != nil { cancel() fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to get current batch status") } } else { <-ctxt.Done() } return nil } func initBatchJobMetricsUI(jobID string) *batchJobMetricsUI { s := spinner.New() s.Spinner = spinner.Points s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) return &batchJobMetricsUI{ spinner: s, jobID: jobID, } } type batchJobMetricsUI struct { metric madmin.JobMetric spinner spinner.Model quitting bool jobID string } func (m *batchJobMetricsUI) Init() tea.Cmd { return m.spinner.Tick } func (m *batchJobMetricsUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c": m.quitting = true return m, tea.Quit default: return m, nil } case madmin.JobMetric: m.metric = msg if msg.Complete || msg.Failed { m.quitting = true return m, tea.Quit } return m, nil case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd default: return m, nil } } func (m *batchJobMetricsUI) View() string { var s strings.Builder // Set table header table := tablewriter.NewWriter(&s) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) var data [][]string addLine := func(prefix string, value interface{}) { data = append(data, []string{ prefix, whiteStyle.Render(fmt.Sprint(value)), }) } if !m.quitting { s.WriteString(m.spinner.View()) } else { if m.metric.Complete { s.WriteString(m.spinner.Style.Render((tickCell + tickCell + tickCell))) } else if m.metric.Failed { s.WriteString(m.spinner.Style.Render((crossTickCell + crossTickCell + crossTickCell))) } } s.WriteString("\n") switch m.metric.JobType { case string(madmin.BatchJobReplicate): accElapsedTime := m.metric.LastUpdate.Sub(m.metric.StartTime) addLine("JobType: ", m.metric.JobType) addLine("Objects: ", m.metric.Replicate.Objects) addLine("Versions: ", m.metric.Replicate.Objects) addLine("FailedObjects: ", m.metric.Replicate.ObjectsFailed) if accElapsedTime > 0 { bytesTransferredPerSec := float64(m.metric.Replicate.BytesTransferred) / accElapsedTime.Seconds() objectsPerSec := float64(int64(time.Second)*m.metric.Replicate.Objects) / float64(accElapsedTime) addLine("Throughput: ", fmt.Sprintf("%s/s", humanize.IBytes(uint64(bytesTransferredPerSec)))) addLine("IOPs: ", fmt.Sprintf("%.2f objs/s", objectsPerSec)) } addLine("Transferred: ", humanize.IBytes(uint64(m.metric.Replicate.BytesTransferred))) addLine("Elapsed: ", accElapsedTime.Round(time.Second).String()) addLine("CurrObjName: ", m.metric.Replicate.Object) case string(madmin.BatchJobExpire): addLine("JobType: ", m.metric.JobType) addLine("Objects: ", m.metric.Expired.Objects) addLine("FailedObjects: ", m.metric.Expired.ObjectsFailed) addLine("CurrObjName: ", m.metric.Expired.Object) if !m.metric.LastUpdate.IsZero() { accElapsedTime := m.metric.LastUpdate.Sub(m.metric.StartTime) addLine("Elapsed: ", accElapsedTime.String()) } } table.AppendBulk(data) table.Render() if m.quitting { s.WriteString("\n") } return s.String() } minio-client-0.0~20250403/cmd/build-constants.go000066400000000000000000000022301477450377600212020ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd var ( // Version - version time.RFC3339. Version = "DEVELOPMENT.GOGET" // ReleaseTag - release tag in TAG.%Y-%m-%dT%H-%M-%SZ. ReleaseTag = "DEVELOPMENT.GOGET" // CommitID - latest commit id. CommitID = "DEVELOPMENT.GOGET" // ShortCommitID - first 12 characters from CommitID. ShortCommitID = CommitID[:12] // CopyrightYear - dynamic value of the copyright end year CopyrightYear = "0000" ) minio-client-0.0~20250403/cmd/cat-main.go000066400000000000000000000240331477450377600175670ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "errors" "fmt" "io" "os" "strings" "syscall" "time" "unicode" "unicode/utf8" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" ) var catFlags = []cli.Flag{ cli.StringFlag{ Name: "rewind", Usage: "display an earlier object version", }, cli.StringFlag{ Name: "version-id, vid", Usage: "display a specific version of an object", }, cli.BoolFlag{ Name: "zip", Usage: "extract from remote zip file (MinIO server source only)", }, cli.Int64Flag{ Name: "offset", Usage: "start offset", }, cli.Int64Flag{ Name: "tail", Usage: "tail number of bytes at ending of file", }, cli.IntFlag{ Name: "part-number", Usage: "download only a specific part number", }, } // Display contents of a file. var catCmd = cli.Command{ Name: "cat", Usage: "display object contents", Action: mainCat, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(append(catFlags, encCFlag), globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Stream an object from Amazon S3 cloud storage to mplayer standard input. {{.Prompt}} {{.HelpName}} s3/mysql-backups/kubecon-mysql-operator.mpv | mplayer - 2. Concatenate contents of file1.txt and stdin to standard output. {{.Prompt}} {{.HelpName}} file1.txt - > file.txt 3. Concatenate multiple files to one. {{.Prompt}} {{.HelpName}} part.* > complete.img 4. Save an encrypted object from Amazon S3 cloud storage to a local file. {{.Prompt}} {{.HelpName}} --enc-c "play/my-bucket/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" s3/mysql-backups/backups-201810.gz > /mnt/data/recent.gz 5. Display the content of encrypted object. In case the encryption key contains non-printable character like tab, pass the base64 encoded string as key. {{.Prompt}} {{.HelpName}} --enc-c "play/my-bucket/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" play/my-bucket/my-object 6. Display the content of an object 10 days earlier {{.Prompt}} {{.HelpName}} --rewind 10d play/my-bucket/my-object 7. Display the content of a particular object version {{.Prompt}} {{.HelpName}} --vid "3ddac055-89a7-40fa-8cd3-530a5581b6b8" play/my-bucket/my-object `, } // checkCatSyntax - validate all the passed arguments func checkCatSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // prettyStdout replaces some non printable characters // with format to be better viewable by the user type prettyStdout struct { // Internal data to pretty-print writer io.Writer // Internal buffer which contains pretty printed // form of binary (no printable) characters buffer *bytes.Buffer } // newPrettyStdout returns an initialized prettyStdout struct func newPrettyStdout(w io.Writer) *prettyStdout { return &prettyStdout{ writer: w, buffer: bytes.NewBuffer([]byte{}), } } // Read() returns pretty printed binary characters func (s prettyStdout) Write(input []byte) (int, error) { inputLen := len(input) // Convert no printable characters to '^?' // and fill into s.buffer for len(input) > 0 { r, size := utf8.DecodeRune(input) if unicode.IsPrint(r) || unicode.IsSpace(r) { s.buffer.WriteRune(r) } else { s.buffer.WriteString("^?") } input = input[size:] } bufLen := s.buffer.Len() // Copy all buffer content to the writer (stdout) n, e := s.buffer.WriteTo(s.writer) if e != nil { return 0, e } if int(n) != bufLen { return 0, errors.New("error when writing to stdout") } return inputLen, nil } type catOpts struct { args []string versionID string timeRef time.Time startO int64 tailO int64 partN int isZip bool stdinMode bool } // parseCatSyntax performs command-line input validation for cat command. func parseCatSyntax(ctx *cli.Context) catOpts { // Validate command-line arguments. checkCatSyntax(ctx) var o catOpts o.args = ctx.Args() o.versionID = ctx.String("version-id") rewind := ctx.String("rewind") if o.versionID != "" && rewind != "" { fatalIf(errInvalidArgument().Trace(), "You cannot specify --version-id and --rewind at the same time") } if o.versionID != "" && len(o.args) != 1 { fatalIf(errInvalidArgument().Trace(), "You need to pass at least one argument if --version-id is specified") } for _, arg := range o.args { if strings.HasPrefix(arg, "-") && len(arg) > 1 { fatalIf(probe.NewError(errors.New("")), fmt.Sprintf("Unknown flag `%s` passed.", arg)) } } o.stdinMode = len(o.args) == 0 o.timeRef = parseRewindFlag(rewind) o.isZip = ctx.Bool("zip") o.startO = ctx.Int64("offset") o.tailO = ctx.Int64("tail") o.partN = ctx.Int("part-number") if o.tailO != 0 && o.startO != 0 { fatalIf(errInvalidArgument().Trace(), "You cannot specify both --tail and --offset") } if o.tailO < 0 || o.startO < 0 { fatalIf(errInvalidArgument().Trace(), "You cannot specify negative --tail or --offset") } if o.isZip && (o.tailO != 0 || o.startO != 0) { fatalIf(errInvalidArgument().Trace(), "You cannot combine --zip with --tail or --offset") } if o.stdinMode && (o.isZip || o.startO != 0 || o.tailO != 0) { fatalIf(errInvalidArgument().Trace(), "You cannot use --zip --tail or --offset with stdin") } if (o.tailO != 0 || o.startO != 0) && o.partN > 0 { fatalIf(errInvalidArgument().Trace(), "You cannot use --part-number with --tail or --offset") } return o } // catURL displays contents of a URL to stdout. func catURL(ctx context.Context, sourceURL string, encKeyDB map[string][]prefixSSEPair, o catOpts) *probe.Error { var reader io.ReadCloser size := int64(-1) switch sourceURL { case "-": reader = os.Stdin default: versionID := o.versionID var err *probe.Error // Try to stat the object, the purpose is to: // 1. extract the size of S3 object so we can check if the size of the // downloaded object is equal to the original one. FS files // are ignored since some of them have zero size though they // have contents like files under /proc. // 2. extract the version ID if rewind flag is passed if client, content, err := url2Stat(ctx, url2StatOptions{ urlStr: sourceURL, versionID: o.versionID, fileAttr: false, encKeyDB: encKeyDB, timeRef: o.timeRef, isZip: o.isZip, ignoreBucketExistsCheck: false, }); err == nil { if o.versionID == "" { versionID = content.VersionID } if o.tailO > 0 && content.Size > 0 { o.startO = content.Size - o.tailO if o.startO < 0 { // Return all. o.startO = 0 } } if client.GetURL().Type == objectStorage { size = content.Size - o.startO if size < 0 { err := probe.NewError(fmt.Errorf("specified offset (%d) bigger than file (%d)", o.startO, content.Size)) return err.Trace(sourceURL) } } if o.partN != 0 { size = int64(-1) } } else { return err.Trace(sourceURL) } gopts := GetOptions{VersionID: versionID, Zip: o.isZip, RangeStart: o.startO, PartNumber: o.partN} if reader, err = getSourceStreamFromURL(ctx, sourceURL, encKeyDB, getSourceOpts{ GetOptions: gopts, preserve: false, }); err != nil { return err.Trace(sourceURL) } defer reader.Close() } return catOut(reader, size).Trace(sourceURL) } // catOut reads from reader stream and writes to stdout. Also check the length of the // read bytes against size parameter (if not -1) and return the appropriate error func catOut(r io.Reader, size int64) *probe.Error { var n int64 var e error var stdout io.Writer // In case of a user showing the object content in a terminal, // avoid printing control and other bad characters to avoid // terminal session corruption if isTerminal() { stdout = newPrettyStdout(os.Stdout) } else { stdout = os.Stdout } // Read till EOF. if n, e = io.Copy(stdout, r); e != nil { switch e := e.(type) { case *os.PathError: if e.Err == syscall.EPIPE { // stdout closed by the user. Gracefully exit. return nil } return probe.NewError(e) default: return probe.NewError(e) } } if size != -1 && n < size { return probe.NewError(UnexpectedEOF{ TotalSize: size, TotalWritten: n, }) } if size != -1 && n > size { return probe.NewError(UnexpectedEOF{ TotalSize: size, TotalWritten: n, }) } return nil } // mainCat is the main entry point for cat command. func mainCat(cliCtx *cli.Context) error { ctx, cancelCat := context.WithCancel(globalContext) defer cancelCat() encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) fatalIf(err, "Unable to parse encryption keys.") // check 'cat' cli arguments. o := parseCatSyntax(cliCtx) // handle std input data. if o.stdinMode { fatalIf(catOut(os.Stdin, -1).Trace(), "Unable to read from standard input.") return nil } // if Args contain `-`, we need to preserve its order specially. if len(o.args) > 0 && o.args[0] == "-" { for i, arg := range os.Args { if arg == "cat" { // Overwrite cliCtx.Args with os.Args. o.args = os.Args[i+1:] break } } } // Convert arguments to URLs: expand alias, fix format. for _, url := range o.args { fatalIf(catURL(ctx, url, encKeyDB, o).Trace(url), "Unable to read from `"+url+"`.") } return nil } minio-client-0.0~20250403/cmd/cat_test.go000066400000000000000000000036711477450377600177110ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "io" "testing" ) func TestPrettyStdout(t *testing.T) { testCases := []struct { originText string prettyText string }{ {"", ""}, {"text", "text"}, // Check with new lines {"text\r\n", "text\r\n"}, {"\ttext\n", "\ttext\n"}, // Print some unicode characters and check if it is not altered {"добро пожаловать.", "добро пожаловать."}, // Print colored text {"\x1b\x5b\x33\x31\x6d\x66\x61\x69\x6c", "^?[31mfail"}, // Print clear screen {"\x1b\x63", "^?c"}, // Random data {"\x3d\xef\xd2\xb5", "=�ҵ"}, } for i, testCase := range testCases { reader := bytes.NewReader([]byte(testCase.originText)) fakeStdout := bytes.NewBuffer([]byte("")) n, err := io.Copy(newPrettyStdout(fakeStdout), reader) if err != nil { t.Fatalf("Test %d: %v\n", i+1, err) } if int(n) != len(testCase.originText) { t.Fatalf("Test %d: copy error\n", i+1) } prettyText, err := io.ReadAll(fakeStdout) if err != nil { t.Fatalf("Test %d: %v", i+1, err) } if string(prettyText) != testCase.prettyText { t.Fatalf("Test %d: expected output `%s`, found output `%s`", i+1, testCase.prettyText, string(prettyText)) } } } minio-client-0.0~20250403/cmd/certs.go000066400000000000000000000053711477450377600172220ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" "path/filepath" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/certs" ) // getCertsDir - return the full path of certs dir func getCertsDir() (string, *probe.Error) { p, err := getMcConfigDir() if err != nil { return "", err.Trace() } return filepath.Join(p, globalMCCertsDir), nil } // isCertsDirExists - verify if certs directory exists. func isCertsDirExists() bool { certsDir, err := getCertsDir() fatalIf(err.Trace(), "Unable to determine certs folder.") if _, e := os.Stat(certsDir); e != nil { return false } return true } // createCertsDir - create MinIO Client certs folder func createCertsDir() *probe.Error { p, err := getCertsDir() if err != nil { return err.Trace() } if e := os.MkdirAll(p, 0o700); e != nil { return probe.NewError(e) } return nil } // getCAsDir - return the full path of CAs dir func getCAsDir() (string, *probe.Error) { p, err := getCertsDir() if err != nil { return "", err.Trace() } return filepath.Join(p, globalMCCAsDir), nil } // mustGetCAsDir - return the full path of CAs dir or empty string when an error occurs func mustGetCAsDir() string { p, err := getCAsDir() if err != nil { return "" } return p } // isCAsDirExists - verify if CAs directory exists. func isCAsDirExists() bool { CAsDir, err := getCAsDir() fatalIf(err.Trace(), "Unable to determine CAs folder.") if _, e := os.Stat(CAsDir); e != nil { return false } return true } // createCAsDir - create MinIO Client CAs folder func createCAsDir() *probe.Error { p, err := getCAsDir() if err != nil { return err.Trace() } if e := os.MkdirAll(p, 0o700); e != nil { return probe.NewError(e) } return nil } // loadRootCAs fetches CA files provided in MinIO config and adds them to globalRootCAs // Currently under Windows, there is no way to load system + user CAs at the same time func loadRootCAs() { var e error globalRootCAs, e = certs.GetRootCAs(mustGetCAsDir()) if e != nil { fatalIf(probe.NewError(e), "Unable to load certificates.") } } minio-client-0.0~20250403/cmd/cli_test.go000066400000000000000000000025561477450377600177120ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "testing" "github.com/minio/cli" ) func TestCLIOnUsageError(t *testing.T) { var checkOnUsageError func(cli.Command, string) checkOnUsageError = func(cmd cli.Command, parentCmd string) { if cmd.Subcommands != nil { for _, subCmd := range cmd.Subcommands { if cmd.Hidden { continue } checkOnUsageError(subCmd, parentCmd+" "+cmd.Name) } return } if !cmd.Hidden && cmd.OnUsageError == nil { t.Errorf("On usage error for `%s` not found", parentCmd+" "+cmd.Name) } } for _, cmd := range appCmds { if cmd.Hidden { continue } checkOnUsageError(cmd, "") } } minio-client-0.0~20250403/cmd/client-admin.go000066400000000000000000000121341477450377600204410ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "crypto/tls" "fmt" "net/http" "net/url" "sync" "time" "github.com/mattn/go-ieproxy" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/httptracer" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/credentials" ) // NewAdminFactory encloses New function with client cache. func NewAdminFactory() func(config *Config) (*madmin.AdminClient, *probe.Error) { clientCache := make(map[uint32]*madmin.AdminClient) mutex := &sync.Mutex{} // Return New function. return func(config *Config) (*madmin.AdminClient, *probe.Error) { // Creates a parsed URL. targetURL, e := url.Parse(config.HostURL) if e != nil { return nil, probe.NewError(e) } hostName := targetURL.Host confSum := getConfigHash(config) useTLS := isHostTLS(config) // Lookup previous cache by hash. mutex.Lock() defer mutex.Unlock() var api *madmin.AdminClient var found bool if api, found = clientCache[confSum]; !found { transport := config.getTransport() credsChain, err := config.getCredsChain() if err != nil { return nil, err } creds := credentials.NewChainCredentials(credsChain) // Not found. Instantiate a new MinIO var e error api, e = madmin.NewWithOptions(hostName, &madmin.Options{ Creds: creds, Secure: useTLS, }) if e != nil { return nil, probe.NewError(e) } // Set custom transport. api.SetCustomTransport(transport) // Set app info. api.SetAppInfo(config.AppName, config.AppVersion) // Cache the new MinIO Client with hash of config as key. clientCache[confSum] = api } // Store the new api object. return api, nil } } // newAdminClient gives a new client interface func newAdminClient(aliasedURL string) (*madmin.AdminClient, *probe.Error) { alias, urlStrFull, aliasCfg, err := expandAlias(aliasedURL) if err != nil { return nil, err.Trace(aliasedURL) } // Verify if the aliasedURL is a real URL, fail in those cases // indicating the user to add alias. if aliasCfg == nil && urlRgx.MatchString(aliasedURL) { return nil, errInvalidAliasedURL(aliasedURL).Trace(aliasedURL) } if aliasCfg == nil { return nil, probe.NewError(fmt.Errorf("No valid configuration found for '%s' host alias", urlStrFull)) } s3Config := NewS3Config(alias, urlStrFull, aliasCfg) s3Client, err := s3AdminNew(s3Config) if err != nil { return nil, err.Trace(alias, urlStrFull) } return s3Client, nil } func newAnonymousClient(aliasedURL string) (*madmin.AnonymousClient, *probe.Error) { _, urlStrFull, aliasCfg, err := expandAlias(aliasedURL) if err != nil { return nil, err.Trace(aliasedURL) } // Verify if the aliasedURL is a real URL, fail in those cases // indicating the user to add alias. if aliasCfg == nil && urlRgx.MatchString(aliasedURL) { return nil, errInvalidAliasedURL(aliasedURL).Trace(aliasedURL) } if aliasCfg == nil { return nil, probe.NewError(fmt.Errorf("No valid configuration found for '%s' host alias", urlStrFull)) } // Creates a parsed URL. targetURL, e := url.Parse(urlStrFull) if e != nil { return nil, probe.NewError(e) } // By default enable HTTPs. useTLS := targetURL.Scheme != "http" // Construct an anonymous client anonClient, e := madmin.NewAnonymousClient(targetURL.Host, useTLS) if e != nil { return nil, probe.NewError(e) } // Set custom transport var transport http.RoundTripper = &http.Transport{ Proxy: ieproxy.GetProxyFunc(), DialContext: newCustomDialContext(&Config{}), DialTLSContext: newCustomDialTLSContext(&tls.Config{ RootCAs: globalRootCAs, MinVersion: tls.VersionTLS12, InsecureSkipVerify: globalInsecure, }), MaxIdleConnsPerHost: 256, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 10 * time.Second, // Set this value so that the underlying transport round-tripper // doesn't try to auto decode the body of objects with // content-encoding set to `gzip`. // // Refer: // https://golang.org/src/net/http/transport.go?h=roundTrip#L1843 DisableCompression: true, } if globalDebug { transport = httptracer.GetNewTraceTransport(newTraceV4(), transport) } anonClient.SetCustomTransport(transport) return anonClient, nil } // s3AdminNew returns an initialized minioAdmin structure. If debug is enabled, // it also enables an internal trace transport. var s3AdminNew = NewAdminFactory() minio-client-0.0~20250403/cmd/client-admin_test.go000066400000000000000000000031441477450377600215010ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // # This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "io" "net/http" "strconv" ) type adminPolicyHandler struct { endpoint string name string policy []byte } func (h adminPolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if ak := r.Header.Get("Authorization"); len(ak) == 0 { w.WriteHeader(http.StatusForbidden) return } switch r.Method { case "PUT": length, e := strconv.Atoi(r.Header.Get("Content-Length")) if e != nil { w.WriteHeader(http.StatusBadRequest) return } var buffer bytes.Buffer if _, e = io.CopyN(&buffer, r.Body, int64(length)); e != nil { w.WriteHeader(http.StatusInternalServerError) return } if len(h.policy) != buffer.Len() { w.WriteHeader(http.StatusBadRequest) return } w.Header().Set("Content-Length", "0") w.WriteHeader(http.StatusOK) default: w.WriteHeader(http.StatusForbidden) } } minio-client-0.0~20250403/cmd/client-errors.go000066400000000000000000000136301477450377600206670ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "time" ) /// Collection of standard errors // APINotImplemented - api not implemented type APINotImplemented struct { API string APIType string } func (e APINotImplemented) Error() string { return "`" + e.API + "` is not supported for `" + e.APIType + "`." } // InvalidArgument - passed argument is invalid for this operation type InvalidArgument struct{} func (e InvalidArgument) Error() string { return "invalid argument" } // GenericBucketError - generic bucket operations error type GenericBucketError struct { Bucket string } // BucketDoesNotExist - bucket does not exist. type BucketDoesNotExist GenericBucketError func (e BucketDoesNotExist) Error() string { return "Bucket `" + e.Bucket + "` does not exist." } // BucketExists - bucket exists. type BucketExists GenericBucketError func (e BucketExists) Error() string { return "Bucket `" + e.Bucket + "` exists." } // BucketNameEmpty - bucket name empty (http://goo.gl/wJlzDz) type BucketNameEmpty struct{} func (e BucketNameEmpty) Error() string { return "Bucket name cannot be empty." } // ObjectNameEmpty - object name empty. type ObjectNameEmpty struct{} func (e ObjectNameEmpty) Error() string { return "Object name cannot be empty." } // BucketInvalid - bucket name invalid. type BucketInvalid struct { Bucket string } func (e BucketInvalid) Error() string { return "Bucket name `" + e.Bucket + "` not valid." } // ObjectAlreadyExists - typed return for MethodNotAllowed type ObjectAlreadyExists struct { Object string } func (e ObjectAlreadyExists) Error() string { return "Object `" + e.Object + "` already exists." } // ObjectAlreadyExistsAsDirectory - typed return for XMinioObjectExistsAsDirectory type ObjectAlreadyExistsAsDirectory struct { Object string } func (e ObjectAlreadyExistsAsDirectory) Error() string { return "Object `" + e.Object + "` already exists as directory." } // ObjectOnGlacier - object is of storage class glacier. type ObjectOnGlacier struct { Object string } func (e ObjectOnGlacier) Error() string { return "Object `" + e.Object + "` is on Glacier storage." } // GenericFileError - generic file error. type GenericFileError struct { Path string } // PathNotADirectory - this path does not correspond to a directory type PathNotADirectory GenericFileError func (e PathNotADirectory) Error() string { return "Requested path `" + e.Path + "` not a directory" } // PathNotFound (ENOENT) - file not found. type PathNotFound GenericFileError func (e PathNotFound) Error() string { return "Requested path `" + e.Path + "` not found" } // PathIsNotRegular (ENOTREG) - file is not a regular file. type PathIsNotRegular GenericFileError func (e PathIsNotRegular) Error() string { return "Requested path `" + e.Path + "` is not a regular file." } // PathInsufficientPermission (EPERM) - permission denied. type PathInsufficientPermission GenericFileError func (e PathInsufficientPermission) Error() string { return "Insufficient permissions to access this path `" + e.Path + "`" } // BrokenSymlink (ENOTENT) - file has broken symlink. type BrokenSymlink GenericFileError func (e BrokenSymlink) Error() string { return "Requested path `" + e.Path + "` has broken symlink" } // TooManyLevelsSymlink (ELOOP) - file has too many levels of symlinks. type TooManyLevelsSymlink GenericFileError func (e TooManyLevelsSymlink) Error() string { return "Requested path `" + e.Path + "` has too many levels of symlinks" } // EmptyPath (EINVAL) - invalid argument. type EmptyPath struct{} func (e EmptyPath) Error() string { return "Invalid path, path cannot be empty" } // ObjectMissing (EINVAL) - object key missing. type ObjectMissing struct { timeRef time.Time } func (e ObjectMissing) Error() string { if !e.timeRef.IsZero() { return "Object did not exist at `" + e.timeRef.Format(time.RFC1123) + "`" } return "Object does not exist" } // ObjectIsDeleteMarker - object is a delete marker as latest type ObjectIsDeleteMarker struct{} func (e ObjectIsDeleteMarker) Error() string { return "Object is marked as deleted" } // UnexpectedShortWrite - write wrote less bytes than expected. type UnexpectedShortWrite struct { InputSize int WriteSize int } func (e UnexpectedShortWrite) Error() string { msg := fmt.Sprintf("Wrote less data than requested. Expected `%d` bytes, but only wrote `%d` bytes.", e.InputSize, e.WriteSize) return msg } // UnexpectedEOF (EPIPE) - reader closed prematurely. type UnexpectedEOF struct { TotalSize int64 TotalWritten int64 } func (e UnexpectedEOF) Error() string { msg := fmt.Sprintf("Input reader closed pre-maturely. Expected `%d` bytes, but only received `%d` bytes.", e.TotalSize, e.TotalWritten) return msg } // UnexpectedExcessRead - reader wrote more data than requested. type UnexpectedExcessRead UnexpectedEOF func (e UnexpectedExcessRead) Error() string { msg := fmt.Sprintf("Received excess data on input reader. Expected only `%d` bytes, but received `%d` bytes.", e.TotalSize, e.TotalWritten) return msg } // SameFile - source and destination are same files. type SameFile struct { Source, Destination string } func (e SameFile) Error() string { return fmt.Sprintf("'%s' and '%s' are the same file", e.Source, e.Destination) } minio-client-0.0~20250403/cmd/client-fs.go000066400000000000000000001244701477450377600177700ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "fmt" "io" "os" "path" "path/filepath" "runtime" "sort" "strconv" "strings" "syscall" "time" "github.com/google/uuid" "github.com/pkg/xattr" "github.com/rjeczalik/notify" xfilepath "github.com/minio/filepath" "github.com/minio/mc/pkg/disk" "github.com/minio/mc/pkg/hookreader" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/cors" "github.com/minio/minio-go/v7/pkg/encrypt" "github.com/minio/minio-go/v7/pkg/lifecycle" "github.com/minio/minio-go/v7/pkg/notification" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/pkg/v3/console" ) // filesystem client type fsClient struct { PathURL *ClientURL } const ( partSuffix = ".part.minio" slashSeperator = "/" metadataKey = "X-Amz-Meta-Mc-Attrs" metadataKeyS3Cmd = "X-Amz-Meta-S3cmd-Attrs" ) // GOOS specific ignore list. var ignoreFiles = map[string][]string{ "darwin": {"*.DS_Store"}, "default": {"lost+found"}, } // fsNew - instantiate a new fs func fsNew(path string) (Client, *probe.Error) { if strings.TrimSpace(path) == "" { return nil, probe.NewError(EmptyPath{}) } absPath, e := filepath.Abs(path) if e != nil { return nil, probe.NewError(e) } // filepath.Abs removes the trailing slash in a path // but we still need it because fsClient.List() does not // traverse a directory without a trailing slash in the name if path[len(path)-1] == filepath.Separator { absPath += string(filepath.Separator) } return &fsClient{ PathURL: newClientURL(normalizePath(absPath)), }, nil } //lint:ignore U1000 Used on some platforms. func isNotSupported(e error) bool { if e == nil { return false } errno := e.(*xattr.Error) if errno == nil { return false } // check if filesystem supports extended attributes return errno.Err == syscall.ENOTSUP || errno.Err == syscall.EOPNOTSUPP } // isIgnoredFile returns true if 'filename' is on the exclude list. func isIgnoredFile(filename string) bool { matchFile := filepath.Base(filename) // OS specific ignore list. for _, ignoredFile := range ignoreFiles[runtime.GOOS] { matched, e := filepath.Match(ignoredFile, matchFile) if e != nil { panic(e) } if matched { return true } } // Default ignore list for all OSes. for _, ignoredFile := range ignoreFiles["default"] { matched, e := filepath.Match(ignoredFile, matchFile) if e != nil { panic(e) } if matched { return true } } return false } // URL get url. func (f *fsClient) GetURL() ClientURL { return *f.PathURL } // Select replies a stream of query results. func (f *fsClient) Select(_ context.Context, _ string, _ encrypt.ServerSide, _ SelectObjectOpts) (io.ReadCloser, *probe.Error) { return nil, probe.NewError(APINotImplemented{ API: "Select", APIType: "filesystem", }) } // Watches for all fs events on an input path. func (f *fsClient) Watch(_ context.Context, options WatchOptions) (*WatchObject, *probe.Error) { eventChan := make(chan []EventInfo) errorChan := make(chan *probe.Error) doneChan := make(chan struct{}) // Make the channel buffered to ensure no event is dropped. Notify will drop // an event if the receiver is not able to keep up the sending pace. in, out := PipeChan(1000) var fsEvents []notify.Event for _, event := range options.Events { switch event { case "put": fsEvents = append(fsEvents, EventTypePut...) case "delete": fsEvents = append(fsEvents, EventTypeDelete...) case "get": fsEvents = append(fsEvents, EventTypeGet...) default: // Event type not supported by FS client, such as // bucket creation or deletion, ignore it. } } // Set up a watchpoint listening for events within a directory tree rooted // at current working directory. Dispatch remove events to c. recursivePath := f.PathURL.Path if options.Recursive { recursivePath = f.PathURL.Path + "..." } if e := notify.Watch(recursivePath, in, fsEvents...); e != nil { return nil, probe.NewError(e) } // wait for doneChan to close the watcher, eventChan and errorChan go func() { <-doneChan close(eventChan) close(errorChan) notify.Stop(in) // At this point, notify is guaranteed to not write // in 'in' channel so we can close it. close(in) }() timeFormatFS := "2006-01-02T15:04:05.000Z" // Get fsnotify notifications for events and errors, and sent them // using eventChan and errorChan go func() { for event := range out { if isIgnoredFile(event.Path()) { continue } var i os.FileInfo if IsPutEvent(event.Event()) { // Look for any writes, send a response to indicate a full copy. var e error i, e = os.Stat(event.Path()) if e != nil { if os.IsNotExist(e) { continue } errorChan <- probe.NewError(e) continue } if i.IsDir() { // we want files continue } eventChan <- []EventInfo{{ Time: UTCNow().Format(timeFormatFS), Size: i.Size(), Path: event.Path(), Type: notification.ObjectCreatedPut, }} } else if IsDeleteEvent(event.Event()) { eventChan <- []EventInfo{{ Time: UTCNow().Format(timeFormatFS), Path: event.Path(), Type: notification.ObjectRemovedDelete, }} } else if IsGetEvent(event.Event()) { eventChan <- []EventInfo{{ Time: UTCNow().Format(timeFormatFS), Path: event.Path(), Type: notification.ObjectAccessedGet, }} } } }() return &WatchObject{ EventInfoChan: eventChan, ErrorChan: errorChan, DoneChan: doneChan, }, nil } func preserveAttributes(fd *os.File, attr map[string]string) *probe.Error { if val, ok := attr["mode"]; ok { mode, e := strconv.ParseUint(val, 0, 32) if e == nil { // Attempt to change the file mode. if e = fd.Chmod(os.FileMode(mode)); e != nil { return probe.NewError(e) } } } var uid, gid int var e error if val, ok := attr["uid"]; ok { uid, e = strconv.Atoi(val) if e != nil { uid = -1 } } if val, ok := attr["gid"]; ok { gid, e = strconv.Atoi(val) if e != nil { gid = -1 } } // Attempt to change the owner. if e = fd.Chown(uid, gid); e != nil { return probe.NewError(e) } return nil } /// Object operations. func (f *fsClient) put(_ context.Context, reader io.Reader, size int64, progress io.Reader, opts PutOptions) (int64, *probe.Error) { // ContentType is not handled on purpose. // For filesystem this is a redundant information. // Extract dir name. objectDir, objectName := filepath.Split(f.PathURL.Path) if objectDir != "" { // Create any missing top level directories. if e := os.MkdirAll(objectDir, 0o777); e != nil { err := f.toClientError(e, f.PathURL.Path) return 0, err.Trace(f.PathURL.Path) } // Check if object name is empty, it must be an empty directory if objectName == "" { return 0, nil } } objectPath := f.PathURL.Path if err := checkPathLength(objectPath); err != nil { return 0, err } // Write to a temporary file "objectpath/uuid" before commit. objectPartPath := filepath.Join(filepath.Dir(objectPath), uuid.NewString()) // We cannot resume this operation, then we // should remove any partial download if any. defer os.Remove(objectPartPath) tmpFile, e := os.OpenFile(objectPartPath, os.O_CREATE|os.O_WRONLY, 0o666) if e != nil { err := f.toClientError(e, f.PathURL.Path) return 0, err.Trace(f.PathURL.Path) } attr := make(map[string]string) if _, ok := opts.metadata[metadataKey]; ok && opts.isPreserve { attr, e = parseAttribute(opts.metadata) if e != nil { tmpFile.Close() return 0, probe.NewError(e) } err := preserveAttributes(tmpFile, attr) if err != nil { console.Println(console.Colorize("Error", fmt.Sprintf("unable to preserve attributes, continuing to copy the content %s\n", err.ToGoError()))) } } totalWritten, e := io.Copy(tmpFile, hookreader.NewHook(reader, progress)) if e != nil { tmpFile.Close() return 0, probe.NewError(e) } // Close the input reader as well, if possible. closer, ok := reader.(io.Closer) if ok { if e = closer.Close(); e != nil { tmpFile.Close() return totalWritten, probe.NewError(e) } } // Close the file before renaming, we need to do this // specifically for windows users - windows explicitly // disallows renames on Open() fd's by default. if e = tmpFile.Close(); e != nil { return totalWritten, probe.NewError(e) } // Following verification is needed only for input size greater than '0'. if size > 0 { // Unexpected EOF reached (less data was written than expected). if totalWritten < size { return totalWritten, probe.NewError(UnexpectedEOF{ TotalSize: size, TotalWritten: totalWritten, }) } // Unexpected ExcessRead (more data was written than expected). if totalWritten > size { return totalWritten, probe.NewError(UnexpectedExcessRead{ TotalSize: size, TotalWritten: totalWritten, }) } } // Safely completed put. Now commit by renaming to actual filename. if e = os.Rename(objectPartPath, objectPath); e != nil { err := f.toClientError(e, objectPath) return totalWritten, err.Trace(objectPartPath, objectPath) } if len(attr) != 0 && opts.isPreserve { atime, mtime, err := parseAtimeMtime(attr) if err != nil { return totalWritten, err.Trace() } if !atime.IsZero() && !mtime.IsZero() { if e := os.Chtimes(objectPath, atime, mtime); e != nil { return totalWritten, probe.NewError(e) } } } return totalWritten, nil } // Put - create a new file with metadata. func (f *fsClient) Put(ctx context.Context, reader io.Reader, size int64, progress io.Reader, opts PutOptions) (int64, *probe.Error) { return f.put(ctx, reader, size, progress, opts) } // checkPathLength - returns error if given path name length more than 255 func checkPathLength(pathName string) *probe.Error { // Apple OS X path length is limited to 1016 if runtime.GOOS == "darwin" && len(pathName) > 1016 { return probe.NewError(errors.New("file name too long")) } // Disallow more than 32,767 characters on windows, there // are no known name_max limits on Windows. // Refer: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry if runtime.GOOS == "windows" && len(pathName) > 32767 { return probe.NewError(errors.New("file name too long")) } // On Unix we reject paths if they are just '.', '..' or '/' if pathName == "." || pathName == ".." || pathName == "/" { return probe.NewError(errors.New("file access denied")) } // Check each path segment length is > 255 on all Unix // platforms, look for this value as NAME_MAX in // /usr/include/linux/limits.h var count int64 for _, p := range pathName { switch p { case '/': count = 0 // Reset case '\\': if runtime.GOOS == "windows" { count = 0 } else { count++ } default: count++ } if count > 255 { return probe.NewError(errors.New("file name too long")) } } // Success. return nil } func (f *fsClient) putN(_ context.Context, reader io.Reader, size int64, progress io.Reader, opts PutOptions) (int64, *probe.Error) { // ContentType is not handled on purpose. // For filesystem this is a redundant information. // Extract dir name. objectDir, objectName := filepath.Split(f.PathURL.Path) if objectDir != "" { // Create any missing top level directories. if e := os.MkdirAll(objectDir, 0o777); e != nil { err := f.toClientError(e, f.PathURL.Path) return 0, err.Trace(f.PathURL.Path) } // Check if object name is empty, it must be an empty directory if objectName == "" { return 0, nil } } objectPath := f.PathURL.Path if err := checkPathLength(objectPath); err != nil { return 0, err } // Write to a temporary file ""objectpath/uuid"" before commit. objectPartPath := filepath.Join(filepath.Dir(objectPath), uuid.NewString()) // We cannot resume this operation, then we // should remove any partial download if any. defer os.Remove(objectPartPath) tmpFile, e := os.OpenFile(objectPartPath, os.O_CREATE|os.O_WRONLY, 0o666) if e != nil { err := f.toClientError(e, f.PathURL.Path) return 0, err.Trace(f.PathURL.Path) } attr := make(map[string]string) if _, ok := opts.metadata[metadataKey]; ok && opts.isPreserve { attr, e = parseAttribute(opts.metadata) if e != nil { tmpFile.Close() return 0, probe.NewError(e) } err := preserveAttributes(tmpFile, attr) if err != nil { console.Println(console.Colorize("Error", fmt.Sprintf("unable to preserve attributes, continuing to copy the content %s\n", err.ToGoError()))) } } totalWritten, e := io.CopyN(tmpFile, hookreader.NewHook(reader, progress), size) if e != nil { tmpFile.Close() return 0, probe.NewError(e) } // Close the input reader as well, if possible. closer, ok := reader.(io.Closer) if ok { if e = closer.Close(); e != nil { tmpFile.Close() return totalWritten, probe.NewError(e) } } // Close the file before renaming, we need to do this // specifically for windows users - windows explicitly // disallows renames on Open() fd's by default. if e = tmpFile.Close(); e != nil { return totalWritten, probe.NewError(e) } // Following verification is needed only for input size greater than '0'. if size > 0 { // Unexpected ExcessRead (more data was written than expected). if totalWritten > size { return totalWritten, probe.NewError(UnexpectedExcessRead{ TotalSize: size, TotalWritten: totalWritten, }) } } // Safely completed put. Now commit by renaming to actual filename. if e = os.Rename(objectPartPath, objectPath); e != nil { err := f.toClientError(e, objectPath) return totalWritten, err.Trace(objectPartPath, objectPath) } if len(attr) != 0 && opts.isPreserve { atime, mtime, err := parseAtimeMtime(attr) if err != nil { return totalWritten, err.Trace() } if !atime.IsZero() && !mtime.IsZero() { if e := os.Chtimes(objectPath, atime, mtime); e != nil { return totalWritten, probe.NewError(e) } } } return totalWritten, nil } // PutPart - create a new file with metadata, reading up to N bytes. func (f *fsClient) PutPart(ctx context.Context, reader io.Reader, size int64, progress io.Reader, opts PutOptions) (int64, *probe.Error) { if size < 0 { return f.put(ctx, reader, size, progress, opts) } return f.putN(ctx, reader, size, progress, opts) } // ShareDownload - share download not implemented for filesystem. func (f *fsClient) ShareDownload(_ context.Context, _ string, _ time.Duration) (string, *probe.Error) { return "", probe.NewError(APINotImplemented{ API: "ShareDownload", APIType: "filesystem", }) } // ShareUpload - share upload not implemented for filesystem. func (f *fsClient) ShareUpload(_ context.Context, _ bool, _ time.Duration, _ string) (string, map[string]string, *probe.Error) { return "", nil, probe.NewError(APINotImplemented{ API: "ShareUpload", APIType: "filesystem", }) } // Copy - copy data from source to destination func (f *fsClient) Copy(ctx context.Context, source string, opts CopyOptions, progress io.Reader) *probe.Error { rc, e := os.Open(source) if e != nil { err := f.toClientError(e, source) return err.Trace(source) } defer rc.Close() putOpts := PutOptions{ metadata: opts.metadata, isPreserve: opts.isPreserve, } destination := f.PathURL.Path if _, err := f.put(ctx, rc, opts.size, progress, putOpts); err != nil { return err.Trace(destination, source) } return nil } // Get returns reader and any additional metadata. func (f *fsClient) Get(_ context.Context, opts GetOptions) (io.ReadCloser, *ClientContent, *probe.Error) { fileData, e := os.Open(f.PathURL.Path) if e != nil { err := f.toClientError(e, f.PathURL.Path) return nil, nil, err.Trace(f.PathURL.Path) } if opts.RangeStart != 0 { _, e := fileData.Seek(opts.RangeStart, io.SeekStart) if e != nil { err := f.toClientError(e, f.PathURL.Path) return nil, nil, err.Trace(f.PathURL.Path) } } fi, e := fileData.Stat() if e != nil { return nil, nil, probe.NewError(e) } content := &ClientContent{} content.URL = *f.PathURL content.Size = fi.Size() content.Time = fi.ModTime() content.Type = fi.Mode() content.Metadata = map[string]string{ "Content-Type": guessURLContentType(f.PathURL.Path), } path := f.PathURL.String() // Populates meta data with file system attribute only in case of // when preserve flag is passed. if opts.Preserve { fileAttr, err := disk.GetFileSystemAttrs(path) if err != nil { return nil, content, nil } metaData, pErr := getAllXattrs(path) if pErr != nil { return nil, content, nil } for k, v := range metaData { content.Metadata[k] = v } content.Metadata[metadataKey] = fileAttr } return fileData, content, nil } // Check if the given error corresponds to ENOTEMPTY for unix // and ERROR_DIR_NOT_EMPTY for windows (directory not empty). func isSysErrNotEmpty(err error) bool { if err == syscall.ENOTEMPTY { return true } if pathErr, ok := err.(*os.PathError); ok { if runtime.GOOS == "windows" { if errno, _ok := pathErr.Err.(syscall.Errno); _ok && errno == 0x91 { // ERROR_DIR_NOT_EMPTY return true } } if pathErr.Err == syscall.ENOTEMPTY { return true } } return false } // deleteFile deletes a file path if its empty. If it's successfully deleted, // it will recursively delete empty parent directories // until it finds one with files in it. Returns nil for a non-empty directory. func deleteFile(basePath, deletePath string) error { // Attempt to remove path. if e := os.Remove(deletePath); e != nil { if isSysErrNotEmpty(e) { return nil } if os.IsNotExist(e) { return nil } return e } // Trailing slash is removed when found to ensure // slashpath.Dir() to work as intended. parentPath := strings.TrimSuffix(deletePath, slashSeperator) parentPath = path.Dir(parentPath) if !strings.HasPrefix(parentPath, basePath) { // If parentPath jumps out of the original basePath, // make sure to cancel such calls, we don't want // to be deleting more than we should. return nil } if parentPath != "." { return deleteFile(basePath, parentPath) } return nil } // Remove - remove entry read from clientContent channel. func (f *fsClient) Remove(_ context.Context, isIncomplete, _, _, _ bool, contentCh <-chan *ClientContent) <-chan RemoveResult { resultCh := make(chan RemoveResult) // Goroutine reads from contentCh and removes the entry in content. go func() { defer close(resultCh) for content := range contentCh { if content.Err != nil { resultCh <- RemoveResult{ Err: content.Err, } continue } name := content.URL.Path // Add partSuffix for incomplete uploads. if isIncomplete { name += partSuffix } e := deleteFile(f.PathURL.Path, name) if e == nil { res := RemoveResult{} res.ObjectName = content.URL.Path resultCh <- res continue } if os.IsNotExist(e) { // ignore if path already removed. continue } if os.IsPermission(e) { // Ignore permission error. resultCh <- RemoveResult{ Err: probe.NewError(PathInsufficientPermission{ Path: content.URL.Path, }), } } else { resultCh <- RemoveResult{ Err: probe.NewError(e), } return } } }() return resultCh } // ListBuckets returns the list of directories inside a base path func (f *fsClient) ListBuckets(_ context.Context) ([]*ClientContent, *probe.Error) { // save pathURL and file path for further usage. pathURL := *f.PathURL path := pathURL.Path st, e := os.Stat(path) if e != nil { if os.IsNotExist(e) { return nil, probe.NewError(PathNotFound{Path: path}) } return nil, probe.NewError(e) } if !st.Mode().IsDir() { return nil, probe.NewError(PathNotADirectory{Path: path}) } // List directories (buckets) inside path in a sorted way files, e := readDir(path) if e != nil { return nil, probe.NewError(e) } bucketsList := make([]*ClientContent, 0, len(files)) for _, file := range files { fi := file if fi.Mode()&os.ModeSymlink == os.ModeSymlink { fp := filepath.Join(path, fi.Name()) fi, e = os.Stat(fp) if e != nil { // Ignore all errors on symlinks continue } } if isIgnoredFile(fi.Name()) { continue } if !fi.Mode().IsDir() { continue } pathURL = *f.PathURL pathURL.Path = filepath.Join(pathURL.Path, fi.Name()) bucketsList = append(bucketsList, &ClientContent{ URL: pathURL, Time: fi.ModTime(), Type: fi.Mode(), Err: nil, }) } return bucketsList, nil } // List - list files and folders. func (f *fsClient) List(_ context.Context, opts ListOptions) <-chan *ClientContent { contentCh := make(chan *ClientContent, 1) filteredCh := make(chan *ClientContent, 1) if opts.ListZip { contentCh <- &ClientContent{ Err: probe.NewError(errors.New("zip listing not supported for local files")), } close(filteredCh) return filteredCh } if opts.Recursive { if opts.ShowDir == DirNone { go f.listRecursiveInRoutine(contentCh) } else { go f.listDirOpt(contentCh, opts.Incomplete, opts.WithMetadata, opts.ShowDir) } } else { go f.listInRoutine(contentCh) } // This function filters entries from any listing go routine // created previously. If isIncomplete is activated, we will // only show partly uploaded files, go func() { for c := range contentCh { if opts.Incomplete { if !strings.HasSuffix(c.URL.Path, partSuffix) { continue } // Strip part suffix c.URL.Path = strings.Split(c.URL.Path, partSuffix)[0] } else { if strings.HasSuffix(c.URL.Path, partSuffix) { continue } } // Send to filtered channel filteredCh <- c } defer close(filteredCh) }() return filteredCh } // byDirName implements sort.Interface. type byDirName []os.FileInfo func (f byDirName) Len() int { return len(f) } func (f byDirName) Less(i, j int) bool { // For directory add an ending separator fortrue lexical // order. if f[i].Mode().IsDir() { return f[i].Name()+string(filepath.Separator) < f[j].Name() } // For directory add an ending separator for true lexical // order. if f[j].Mode().IsDir() { return f[i].Name() < f[j].Name()+string(filepath.Separator) } return f[i].Name() < f[j].Name() } func (f byDirName) Swap(i, j int) { f[i], f[j] = f[j], f[i] } // readDir reads the directory named by dirname and returns // a list of sorted directory entries. func readDir(dirname string) ([]os.FileInfo, error) { f, e := os.Open(dirname) if e != nil { return nil, e } list, e := f.Readdir(-1) if e != nil { return nil, e } defer f.Close() sort.Sort(byDirName(list)) return list, nil } // listPrefixes - list all files for any given prefix. func (f *fsClient) listPrefixes(prefix string, contentCh chan<- *ClientContent) { dirName := filepath.Dir(prefix) files, e := readDir(dirName) if e != nil { err := f.toClientError(e, dirName) contentCh <- &ClientContent{ Err: err.Trace(dirName), } return } for _, fi := range files { // Skip ignored files. if isIgnoredFile(fi.Name()) { continue } file := filepath.Join(dirName, fi.Name()) if fi.Mode()&os.ModeSymlink == os.ModeSymlink { st, e := os.Stat(file) if e != nil { // Ignore any errors on symlink continue } if strings.HasPrefix(file, prefix) { contentCh <- &ClientContent{ URL: *newClientURL(file), Time: st.ModTime(), Size: st.Size(), Type: st.Mode(), Err: nil, } continue } } if strings.HasPrefix(file, prefix) { contentCh <- &ClientContent{ URL: *newClientURL(file), Time: fi.ModTime(), Size: fi.Size(), Type: fi.Mode(), Err: nil, } } } } func (f *fsClient) listInRoutine(contentCh chan<- *ClientContent) { // close the channel when the function returns. defer close(contentCh) // save pathURL and file path for further usage. pathURL := *f.PathURL fpath := pathURL.Path fst, err := f.fsStat(false) if err != nil { if _, ok := err.ToGoError().(PathNotFound); ok { // If file does not exist treat it like a prefix and list all prefixes if any. prefix := fpath f.listPrefixes(prefix, contentCh) return } // For all other errors we return genuine error back to the caller. contentCh <- &ClientContent{Err: err.Trace(fpath)} return } // Now if the file exists and doesn't end with a separator ('/') do not traverse it. // If the directory doesn't end with a separator, do not traverse it. if !strings.HasSuffix(fpath, string(pathURL.Separator)) && fst.Mode().IsDir() && fpath != "." { f.listPrefixes(fpath, contentCh) return } // If we really see the directory. switch fst.Mode().IsDir() { case true: files, e := readDir(fpath) if e != nil { contentCh <- &ClientContent{Err: probe.NewError(e)} return } for _, file := range files { fi := file if fi.Mode()&os.ModeSymlink == os.ModeSymlink { fp := filepath.Join(fpath, fi.Name()) fi, e = os.Stat(fp) if e != nil { // Ignore all errors on symlinks continue } } if fi.Mode().IsRegular() || fi.Mode().IsDir() { pathURL = *f.PathURL pathURL.Path = filepath.Join(pathURL.Path, fi.Name()) // Skip ignored files. if isIgnoredFile(fi.Name()) { continue } contentCh <- &ClientContent{ URL: pathURL, Time: fi.ModTime(), Size: fi.Size(), Type: fi.Mode(), Err: nil, } } } default: contentCh <- &ClientContent{ URL: pathURL, Time: fst.ModTime(), Size: fst.Size(), Type: fst.Mode(), Err: nil, } } } // List files recursively using non-recursive mode. func (f *fsClient) listDirOpt(contentCh chan *ClientContent, isIncomplete, _ bool, dirOpt DirOpt) { defer close(contentCh) // Trim trailing / or \. currentPath := f.PathURL.Path currentPath = strings.TrimSuffix(currentPath, "/") if runtime.GOOS == "windows" { currentPath = strings.TrimSuffix(currentPath, `\`) } // Closure function reads currentPath and sends to contentCh. If a directory is found, it lists the directory content recursively. var listDir func(currentPath string) bool listDir = func(currentPath string) (isStop bool) { files, e := readDir(currentPath) if e != nil { if os.IsNotExist(e) { contentCh <- &ClientContent{ Err: probe.NewError(PathNotFound{ Path: currentPath, }), } return false } if os.IsPermission(e) { contentCh <- &ClientContent{ Err: probe.NewError(PathInsufficientPermission{ Path: currentPath, }), } return false } contentCh <- &ClientContent{Err: probe.NewError(e)} return true } for _, file := range files { name := filepath.Join(currentPath, file.Name()) content := ClientContent{ URL: *newClientURL(name), Time: file.ModTime(), Size: file.Size(), Type: file.Mode(), Err: nil, } if file.Mode().IsDir() { if dirOpt == DirFirst && !isIncomplete { contentCh <- &content } if listDir(filepath.Join(name)) { return true } if dirOpt == DirLast && !isIncomplete { contentCh <- &content } continue } contentCh <- &content } return false } // listDir() does not send currentPath to contentCh. We send it here depending on dirOpt. if dirOpt == DirFirst && !isIncomplete { contentCh <- &ClientContent{URL: *newClientURL(currentPath), Type: os.ModeDir} } listDir(currentPath) if dirOpt == DirLast && !isIncomplete { contentCh <- &ClientContent{URL: *newClientURL(currentPath), Type: os.ModeDir} } } func (f *fsClient) listRecursiveInRoutine(contentCh chan *ClientContent) { // close channels upon return. defer close(contentCh) var dirName string var filePrefix string pathURL := *f.PathURL if runtime.GOOS == "windows" { pathURL.Path = filepath.FromSlash(pathURL.Path) pathURL.Separator = os.PathSeparator } visitFS := func(fp string, fi os.FileInfo, e error) error { // If file path ends with filepath.Separator and equals to root path, skip it. if strings.HasSuffix(fp, string(pathURL.Separator)) { if fp == dirName { return nil } } // We would never need to print system root path '/'. if fp == "/" { return nil } // Ignore files from ignore list. if isIgnoredFile(fi.Name()) { return nil } /// In following situations we need to handle listing properly. // - When filepath is '/usr' and prefix is '/usr/bi' // - When filepath is '/usr/bin/subdir' and prefix is '/usr/bi' // - Do not check filePrefix if its '.' if filePrefix != "." { if !strings.HasPrefix(fp, filePrefix) && !strings.HasPrefix(filePrefix, fp) { if e == nil { if fi.IsDir() { return xfilepath.ErrSkipDir } return nil } } // - Skip when fp is /usr and prefix is '/usr/bi' // - Do not check filePrefix if its '.' if filePrefix != "." { if !strings.HasPrefix(fp, filePrefix) { return nil } } } if e != nil { // If operation is not permitted, we throw quickly back. if strings.Contains(e.Error(), "operation not permitted") { contentCh <- &ClientContent{ Err: probe.NewError(e), } return nil } if os.IsPermission(e) { contentCh <- &ClientContent{ Err: probe.NewError(PathInsufficientPermission{Path: fp}), } return nil } return e } if fi.Mode()&os.ModeSymlink == os.ModeSymlink { fi, e = os.Stat(fp) if e != nil { // Ignore any errors for symlink return nil } } if fi.Mode().IsRegular() { contentCh <- &ClientContent{ URL: *newClientURL(fp), Time: fi.ModTime(), Size: fi.Size(), Type: fi.Mode(), Err: nil, } } return nil } // No prefix to be filtered by default. filePrefix = "" // if f.Path ends with filepath.Separator - assuming it to be a directory and moving on. if strings.HasSuffix(pathURL.Path, string(pathURL.Separator)) { dirName = pathURL.Path } else { // if not a directory, take base path to navigate through WalkFunc. dirName = filepath.Dir(pathURL.Path) if !strings.HasSuffix(dirName, string(pathURL.Separator)) { // basepath truncates the filepath.Separator, // add it diligently useful for trimming file path inside WalkFunc dirName = dirName + string(pathURL.Separator) } // filePrefix is kept for filtering incoming contents through WalkFunc. filePrefix = pathURL.Path } // walks invokes our custom function. e := xfilepath.Walk(dirName, visitFS) if e != nil { contentCh <- &ClientContent{ Err: probe.NewError(e), } } } // MakeBucket - create a new bucket. func (f *fsClient) MakeBucket(_ context.Context, _ string, _, _ bool) *probe.Error { // TODO: ignoreExisting has no effect currently. In the future, we want // to call os.Mkdir() when ignoredExisting is disabled and os.MkdirAll() // otherwise. // NOTE: withLock=true has no meaning here. e := os.MkdirAll(f.PathURL.Path, 0o777) if e != nil { return probe.NewError(e) } return nil } // RemoveBucket - remove a bucket func (f *fsClient) RemoveBucket(_ context.Context, forceRemove bool) *probe.Error { var e error if forceRemove { e = os.RemoveAll(f.PathURL.Path) } else { e = os.Remove(f.PathURL.Path) } return probe.NewError(e) } // Set object lock configuration of bucket. func (f *fsClient) SetObjectLockConfig(_ context.Context, _ minio.RetentionMode, _ uint64, _ minio.ValidityUnit) *probe.Error { return probe.NewError(APINotImplemented{ API: "SetObjectLockConfig", APIType: "filesystem", }) } // Get object lock configuration of bucket. func (f *fsClient) GetObjectLockConfig(_ context.Context) (status string, mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit, err *probe.Error) { return "", "", 0, "", probe.NewError(APINotImplemented{ API: "GetObjectLockConfig", APIType: "filesystem", }) } // GetAccessRules - unsupported API func (f *fsClient) GetAccessRules(_ context.Context) (map[string]string, *probe.Error) { return map[string]string{}, probe.NewError(APINotImplemented{ API: "GetBucketPolicy", APIType: "filesystem", }) } // Set object retention for a given object. func (f *fsClient) PutObjectRetention(_ context.Context, _ string, _ minio.RetentionMode, _ time.Time, _ bool) *probe.Error { return probe.NewError(APINotImplemented{ API: "PutObjectRetention", APIType: "filesystem", }) } func (f *fsClient) GetObjectRetention(_ context.Context, _ string) (minio.RetentionMode, time.Time, *probe.Error) { return "", time.Time{}, probe.NewError(APINotImplemented{ API: "GetObjectRetention", APIType: "filesystem", }) } // Set object legal hold for a given object. func (f *fsClient) PutObjectLegalHold(_ context.Context, _ string, _ minio.LegalHoldStatus) *probe.Error { return probe.NewError(APINotImplemented{ API: "PutObjectLegalHold", APIType: "filesystem", }) } // Get object legal hold for a given object. func (f *fsClient) GetObjectLegalHold(_ context.Context, _ string) (minio.LegalHoldStatus, *probe.Error) { return "", probe.NewError(APINotImplemented{ API: "GetObjectLegalHold", APIType: "filesystem", }) } // GetAccess - get access policy permissions. func (f *fsClient) GetAccess(_ context.Context) (access, policyJSON string, err *probe.Error) { // For windows this feature is not implemented. if runtime.GOOS == "windows" { return "", "", probe.NewError(APINotImplemented{API: "GetAccess", APIType: "filesystem"}) } st, err := f.fsStat(false) if err != nil { return "", "", err.Trace(f.PathURL.String()) } if !st.Mode().IsDir() { return "", "", probe.NewError(APINotImplemented{API: "GetAccess", APIType: "filesystem"}) } // Mask with os.ModePerm to get only inode permissions switch st.Mode() & os.ModePerm { case os.FileMode(0o777): return "readwrite", "", nil case os.FileMode(0o555): return "readonly", "", nil case os.FileMode(0o333): return "writeonly", "", nil } return "none", "", nil } // SetAccess - set access policy permissions. func (f *fsClient) SetAccess(_ context.Context, access string, isJSON bool) *probe.Error { // For windows this feature is not implemented. // JSON policy for fs is not yet implemented. if runtime.GOOS == "windows" || isJSON { return probe.NewError(APINotImplemented{API: "SetAccess", APIType: "filesystem"}) } st, err := f.fsStat(false) if err != nil { return err.Trace(f.PathURL.String()) } if !st.Mode().IsDir() { return probe.NewError(APINotImplemented{API: "SetAccess", APIType: "filesystem"}) } var mode os.FileMode switch access { case "readonly": mode = os.FileMode(0o555) case "writeonly": mode = os.FileMode(0o333) case "readwrite": mode = os.FileMode(0o777) case "none": mode = os.FileMode(0o755) } e := os.Chmod(f.PathURL.Path, mode) if e != nil { return probe.NewError(e) } return nil } // Stat - get metadata from path. func (f *fsClient) Stat(_ context.Context, opts StatOptions) (content *ClientContent, err *probe.Error) { st, err := f.fsStat(opts.incomplete) if err != nil { return nil, err.Trace(f.PathURL.String()) } content = &ClientContent{} content.URL = *f.PathURL content.Size = st.Size() content.Time = st.ModTime() content.Type = st.Mode() content.Metadata = map[string]string{ "Content-Type": guessURLContentType(f.PathURL.Path), } path := f.PathURL.String() // Populates meta data with file system attribute only in case of // when preserve flag is passed. if opts.preserve { fileAttr, err := disk.GetFileSystemAttrs(path) if err != nil { return content, nil } metaData, pErr := getAllXattrs(path) if pErr != nil { return content, nil } for k, v := range metaData { content.Metadata[k] = v } content.Metadata[metadataKey] = fileAttr } return content, nil } // toClientError error constructs a typed client error for known filesystem errors. func (f *fsClient) toClientError(e error, fpath string) *probe.Error { if os.IsPermission(e) { return probe.NewError(PathInsufficientPermission{Path: fpath}) } if os.IsNotExist(e) { return probe.NewError(PathNotFound{Path: fpath}) } if errors.Is(e, syscall.ELOOP) { return probe.NewError(TooManyLevelsSymlink{Path: fpath}) } return probe.NewError(e) } // fsStat - wrapper function to get file stat. func (f *fsClient) fsStat(isIncomplete bool) (os.FileInfo, *probe.Error) { fpath := f.PathURL.Path // Check if the path corresponds to a directory and returns // the successful result whether isIncomplete is specified or not. st, e := os.Stat(fpath) if e == nil && st.IsDir() { return st, nil } if isIncomplete { fpath += partSuffix } st, e = os.Stat(fpath) if e != nil { return nil, f.toClientError(e, fpath) } return st, nil } func (f *fsClient) AddUserAgent(_, _ string) { } // Get Object Tags func (f *fsClient) GetTags(_ context.Context, _ string) (map[string]string, *probe.Error) { return nil, probe.NewError(APINotImplemented{ API: "GetObjectTagging", APIType: "filesystem", }) } // Set Object tags func (f *fsClient) SetTags(_ context.Context, _, _ string) *probe.Error { return probe.NewError(APINotImplemented{ API: "SetObjectTagging", APIType: "filesystem", }) } // Delete object tags func (f *fsClient) DeleteTags(_ context.Context, _ string) *probe.Error { return probe.NewError(APINotImplemented{ API: "DeleteObjectTagging", APIType: "filesystem", }) } // Get lifecycle configuration for a given bucket, not implemented. func (f *fsClient) GetLifecycle(_ context.Context) (*lifecycle.Configuration, time.Time, *probe.Error) { return nil, time.Time{}, probe.NewError(APINotImplemented{ API: "GetLifecycle", APIType: "filesystem", }) } // Set lifecycle configuration for a given bucket, not implemented. func (f *fsClient) SetLifecycle(_ context.Context, _ *lifecycle.Configuration) *probe.Error { return probe.NewError(APINotImplemented{ API: "SetLifecycle", APIType: "filesystem", }) } // Get version info for a bucket, not implemented. func (f *fsClient) GetVersion(_ context.Context) (minio.BucketVersioningConfiguration, *probe.Error) { return minio.BucketVersioningConfiguration{}, probe.NewError(APINotImplemented{ API: "GetVersion", APIType: "filesystem", }) } // SetVersion - Set version configuration on a bucket, not implemented func (f *fsClient) SetVersion(_ context.Context, _ string, _ []string, _ bool) *probe.Error { return probe.NewError(APINotImplemented{ API: "SetVersion", APIType: "filesystem", }) } // Get replication configuration for a given bucket, not implemented. func (f *fsClient) GetReplication(_ context.Context) (replication.Config, *probe.Error) { return replication.Config{}, probe.NewError(APINotImplemented{ API: "GetReplication", APIType: "filesystem", }) } // Set replication configuration for a given bucket, not implemented. func (f *fsClient) SetReplication(_ context.Context, _ *replication.Config, _ replication.Options) *probe.Error { return probe.NewError(APINotImplemented{ API: "SetReplication", APIType: "filesystem", }) } // Remove replication configuration for a given bucket. Not implemented func (f *fsClient) RemoveReplication(_ context.Context) *probe.Error { return probe.NewError(APINotImplemented{ API: "RemoveReplication", APIType: "filesystem", }) } // GetReplicationMetrics - Get replication metrics for a given bucket, not implemented. func (f *fsClient) GetReplicationMetrics(_ context.Context) (replication.MetricsV2, *probe.Error) { return replication.MetricsV2{}, probe.NewError(APINotImplemented{ API: "GetReplicationMetrics", APIType: "filesystem", }) } // ResetReplication - kicks off replication again on previously replicated objects if existing object // replication is enabled in the replication config, not implemented func (f *fsClient) ResetReplication(_ context.Context, _ time.Duration, _ string) (rinfo replication.ResyncTargetsInfo, err *probe.Error) { return rinfo, probe.NewError(APINotImplemented{ API: "ResetReplication", APIType: "filesystem", }) } // ReplicationResyncStatus - gets status of replication resync for this target arn func (f *fsClient) ReplicationResyncStatus(_ context.Context, _ string) (rinfo replication.ResyncTargetsInfo, err *probe.Error) { return rinfo, probe.NewError(APINotImplemented{ API: "ReplicationResyncStatus", APIType: "filesystem", }) } // Get encryption info for a bucket, not implemented. func (f *fsClient) GetEncryption(_ context.Context) (string, string, *probe.Error) { return "", "", probe.NewError(APINotImplemented{ API: "GetEncryption", APIType: "filesystem", }) } // SetEncryption - Set encryption configuration on a bucket, not implemented func (f *fsClient) SetEncryption(_ context.Context, _, _ string) *probe.Error { return probe.NewError(APINotImplemented{ API: "SetEncryption", APIType: "filesystem", }) } // DeleteEncryption - removes encryption configuration on a bucket, not implemented func (f *fsClient) DeleteEncryption(_ context.Context) *probe.Error { return probe.NewError(APINotImplemented{ API: "DeleteEncryption", APIType: "filesystem", }) } // Gets bucket infoOA func (f *fsClient) GetBucketInfo(_ context.Context) (BucketInfo, *probe.Error) { return BucketInfo{}, probe.NewError(APINotImplemented{ API: "GetBucketInfo", APIType: "filesystem", }) } // Restore object - not implemented func (f *fsClient) Restore(_ context.Context, _ string, _ int) *probe.Error { return probe.NewError(APINotImplemented{ API: "Restore", APIType: "filesystem", }) } // OD Get - not implemented func (f *fsClient) GetPart(_ context.Context, _ int) (io.ReadCloser, *probe.Error) { return nil, probe.NewError(APINotImplemented{ API: "GetPart", APIType: "filesystem", }) } // GetBucketCors - not implemented func (f *fsClient) GetBucketCors(_ context.Context) (*cors.Config, *probe.Error) { return nil, probe.NewError(APINotImplemented{ API: "GetBucketCors", APIType: "filesystem", }) } // SetBucketCors - not implemented func (f *fsClient) SetBucketCors(_ context.Context, _ []byte) *probe.Error { return probe.NewError(APINotImplemented{ API: "SetBucketCors", APIType: "filesystem", }) } // DeleteBucketCors - not implemented func (f *fsClient) DeleteBucketCors(_ context.Context) *probe.Error { return probe.NewError(APINotImplemented{ API: "DeleteBucketCors", APIType: "filesystem", }) } minio-client-0.0~20250403/cmd/client-fs_darwin.go000066400000000000000000000046631477450377600213350ustar00rootroot00000000000000//go:build darwin // +build darwin // Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/pkg/xattr" "github.com/rjeczalik/notify" ) var ( // EventTypePut contains the notify events that will cause a put (writer) EventTypePut = []notify.Event{notify.Create, notify.Write, notify.Rename} // EventTypeDelete contains the notify events that will cause a delete (remove) EventTypeDelete = []notify.Event{notify.Remove} // EventTypeGet contains the notify events that will cause a get (read) EventTypeGet = []notify.Event{} // On macOS, FreeBSD, Solaris this is not available. ) // IsGetEvent checks if the event return is a get event. func IsGetEvent(_ notify.Event) bool { return false } // IsPutEvent checks if the event returned is a put event func IsPutEvent(event notify.Event) bool { for _, ev := range EventTypePut { if event&ev != 0 { return true } } return false } // IsDeleteEvent checks if the event returned is a delete event func IsDeleteEvent(event notify.Event) bool { return event¬ify.Remove != 0 } // getXAttr fetches the extended attribute for a particular key on // file func getXAttr(path, key string) (string, error) { data, e := xattr.Get(path, key) if e != nil { return "", e } return string(data), nil } // getAllXattrs returns the extended attributes for a file if supported // by the OS func getAllXattrs(path string) (map[string]string, error) { xMetadata := make(map[string]string) list, e := xattr.List(path) if e != nil { if isNotSupported(e) { return nil, nil } return nil, e } for _, key := range list { xMetadata[key], e = getXAttr(path, key) if e != nil { if isNotSupported(e) { return nil, nil } return nil, e } } return xMetadata, nil } minio-client-0.0~20250403/cmd/client-fs_freebsd.go000066400000000000000000000046231477450377600214570ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/pkg/xattr" "github.com/rjeczalik/notify" ) var ( // EventTypePut contains the notify events that will cause a put (writer) EventTypePut = []notify.Event{notify.Create, notify.Write, notify.Rename} // EventTypeDelete contains the notify events that will cause a delete (remove) EventTypeDelete = []notify.Event{notify.Remove} // EventTypeGet contains the notify events that will cause a get (read) EventTypeGet = []notify.Event{} // On macOS, FreeBSD, Solaris this is not available. ) // IsGetEvent checks if the event return is a get event. func IsGetEvent(event notify.Event) bool { return false } // IsPutEvent checks if the event returned is a put event func IsPutEvent(event notify.Event) bool { for _, ev := range EventTypePut { if event&ev != 0 { return true } } return false } // IsDeleteEvent checks if the event returned is a delete event func IsDeleteEvent(event notify.Event) bool { return event¬ify.Remove != 0 } // getXAttr fetches the extended attribute for a particular key on // file func getXAttr(path, key string) (string, error) { data, e := xattr.Get(path, key) if e != nil { return "", e } return string(data), nil } // getAllXattrs returns the extended attributes for a file if supported // by the OS func getAllXattrs(path string) (map[string]string, error) { xMetadata := make(map[string]string) list, e := xattr.List(path) if e != nil { if isNotSupported(e) { return nil, nil } return nil, e } for _, key := range list { xMetadata[key], e = getXAttr(path, key) if e != nil { if isNotSupported(e) { return nil, nil } return nil, e } } return xMetadata, nil } minio-client-0.0~20250403/cmd/client-fs_linux.go000066400000000000000000000054461477450377600212100ustar00rootroot00000000000000//go:build linux // +build linux // Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "encoding/hex" "strings" "unicode/utf8" "github.com/pkg/xattr" "github.com/rjeczalik/notify" ) var ( // EventTypePut contains the notify events that will cause a put (write) EventTypePut = []notify.Event{notify.InCloseWrite | notify.InMovedTo} // EventTypeDelete contains the notify events that will cause a delete (remove) EventTypeDelete = []notify.Event{notify.InDelete | notify.InDeleteSelf | notify.InMovedFrom} // EventTypeGet contains the notify events that will cause a get (read) EventTypeGet = []notify.Event{notify.InAccess | notify.InOpen} ) // IsGetEvent checks if the event return is a get event. func IsGetEvent(event notify.Event) bool { for _, ev := range EventTypeGet { if event&ev != 0 { return true } } return false } // IsPutEvent checks if the event returned is a put event func IsPutEvent(event notify.Event) bool { for _, ev := range EventTypePut { if event&ev != 0 { return true } } return false } // IsDeleteEvent checks if the event returned is a delete event func IsDeleteEvent(event notify.Event) bool { for _, ev := range EventTypeDelete { if event&ev != 0 { return true } } return false } // getXAttr fetches the extended attribute for a particular key on file func getXAttr(path, key string) (string, error) { data, e := xattr.Get(path, key) if e != nil { return "", e } if utf8.ValidString(string(data)) { return string(data), nil } return hex.EncodeToString(data), nil } // getAllXattrs returns the extended attributes for a file if supported by the OS func getAllXattrs(path string) (map[string]string, error) { xMetadata := make(map[string]string) list, e := xattr.List(path) if e != nil { if isNotSupported(e) { return nil, nil } return nil, e } for _, key := range list { // filter out system specific xattr if strings.HasPrefix(key, "system.") { continue } xMetadata[key], e = getXAttr(path, key) if e != nil { if isNotSupported(e) { return nil, nil } return nil, e } } return xMetadata, nil } minio-client-0.0~20250403/cmd/client-fs_netbsd.go000066400000000000000000000046231477450377600213240ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/pkg/xattr" "github.com/rjeczalik/notify" ) var ( // EventTypePut contains the notify events that will cause a put (writer) EventTypePut = []notify.Event{notify.Create, notify.Write, notify.Rename} // EventTypeDelete contains the notify events that will cause a delete (remove) EventTypeDelete = []notify.Event{notify.Remove} // EventTypeGet contains the notify events that will cause a get (read) EventTypeGet = []notify.Event{} // On macOS, FreeBSD, Solaris this is not available. ) // IsGetEvent checks if the event return is a get event. func IsGetEvent(event notify.Event) bool { return false } // IsPutEvent checks if the event returned is a put event func IsPutEvent(event notify.Event) bool { for _, ev := range EventTypePut { if event&ev != 0 { return true } } return false } // IsDeleteEvent checks if the event returned is a delete event func IsDeleteEvent(event notify.Event) bool { return event¬ify.Remove != 0 } // getXAttr fetches the extended attribute for a particular key on // file func getXAttr(path, key string) (string, error) { data, e := xattr.Get(path, key) if e != nil { return "", e } return string(data), nil } // getAllXattrs returns the extended attributes for a file if supported // by the OS func getAllXattrs(path string) (map[string]string, error) { xMetadata := make(map[string]string) list, e := xattr.List(path) if e != nil { if isNotSupported(e) { return nil, nil } return nil, e } for _, key := range list { xMetadata[key], e = getXAttr(path, key) if e != nil { if isNotSupported(e) { return nil, nil } return nil, e } } return xMetadata, nil } minio-client-0.0~20250403/cmd/client-fs_other.go000066400000000000000000000036271477450377600211710ustar00rootroot00000000000000//go:build solaris || openbsd // +build solaris openbsd // Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/rjeczalik/notify" var ( // EventTypePut contains the notify events that will cause a put (writer) EventTypePut = []notify.Event{notify.Create, notify.Write, notify.Rename} // EventTypeDelete contains the notify events that will cause a delete (remove) EventTypeDelete = []notify.Event{notify.Remove} // EventTypeGet contains the notify events that will cause a get (read) EventTypeGet = []notify.Event{} // On macOS, FreeBSD, Solaris this is not available. ) // IsGetEvent checks if the event return is a get event. func IsGetEvent(event notify.Event) bool { return false } // IsPutEvent checks if the event returned is a put event func IsPutEvent(event notify.Event) bool { for _, ev := range EventTypePut { if event&ev != 0 { return true } } return false } // IsDeleteEvent checks if the event returned is a delete event func IsDeleteEvent(event notify.Event) bool { return event¬ify.Remove != 0 } // getAtllXAttrs returns the extended attributes for a file if supported // by the OS func getAllXattrs(path string) (map[string]string, error) { return nil, nil } minio-client-0.0~20250403/cmd/client-fs_test.go000066400000000000000000000253751477450377600210330ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "io" "os" "path/filepath" "runtime" checkv1 "gopkg.in/check.v1" ) // Test list files in a folder. func (s *TestSuite) TestList(c *checkv1.C) { root, e := os.MkdirTemp(os.TempDir(), "fs-") c.Assert(e, checkv1.IsNil) defer os.RemoveAll(root) // Create multiple files. objectPath := filepath.Join(root, "object1") fsClient, err := fsNew(objectPath) c.Assert(err, checkv1.IsNil) data := "hello" reader := bytes.NewReader([]byte(data)) var n int64 n, err = fsClient.Put(context.Background(), reader, int64(len(data)), nil, PutOptions{ metadata: map[string]string{ "Content-Type": "application/octet-stream", }, }, ) c.Assert(err, checkv1.IsNil) c.Assert(n, checkv1.Equals, int64(len(data))) objectPath = filepath.Join(root, "object2") fsClient, err = fsNew(objectPath) c.Assert(err, checkv1.IsNil) reader = bytes.NewReader([]byte(data)) n, err = fsClient.Put(context.Background(), reader, int64(len(data)), nil, PutOptions{ metadata: map[string]string{ "Content-Type": "application/octet-stream", }, }) c.Assert(err, checkv1.IsNil) c.Assert(n, checkv1.Equals, int64(len(data))) fsClient, err = fsNew(root) c.Assert(err, checkv1.IsNil) // Verify previously create files and list them. var contents []*ClientContent for content := range fsClient.List(globalContext, ListOptions{ShowDir: DirNone}) { if content.Err != nil { err = content.Err break } contents = append(contents, content) } c.Assert(err, checkv1.IsNil) c.Assert(len(contents), checkv1.Equals, 1) c.Assert(contents[0].Type.IsDir(), checkv1.Equals, true) // Create another file. objectPath = filepath.Join(root, "test1/newObject1") fsClient, err = fsNew(objectPath) c.Assert(err, checkv1.IsNil) reader = bytes.NewReader([]byte(data)) n, err = fsClient.Put(context.Background(), reader, int64(len(data)), nil, PutOptions{ metadata: map[string]string{ "Content-Type": "application/octet-stream", }, }) c.Assert(err, checkv1.IsNil) c.Assert(n, checkv1.Equals, int64(len(data))) fsClient, err = fsNew(root) c.Assert(err, checkv1.IsNil) contents = nil // List non recursive to list only top level files. for content := range fsClient.List(globalContext, ListOptions{ShowDir: DirNone}) { if content.Err != nil { err = content.Err break } contents = append(contents, content) } c.Assert(err, checkv1.IsNil) c.Assert(len(contents), checkv1.Equals, 1) c.Assert(contents[0].Type.IsDir(), checkv1.Equals, true) fsClient, err = fsNew(root) c.Assert(err, checkv1.IsNil) contents = nil // List recursively all files and verify. for content := range fsClient.List(globalContext, ListOptions{Recursive: true, ShowDir: DirNone}) { if content.Err != nil { err = content.Err break } contents = append(contents, content) } c.Assert(err, checkv1.IsNil) c.Assert(len(contents), checkv1.Equals, 3) var regularFiles int var regularDirs int // Test number of expected files and directories. for _, content := range contents { if content.Type.IsRegular() { regularFiles++ continue } if content.Type.IsDir() { regularDirs++ continue } } c.Assert(regularDirs, checkv1.Equals, 0) c.Assert(regularFiles, checkv1.Equals, 3) // Create an ignored file and list to verify if its ignored. objectPath = filepath.Join(root, "test1/.DS_Store") fsClient, err = fsNew(objectPath) c.Assert(err, checkv1.IsNil) reader = bytes.NewReader([]byte(data)) n, err = fsClient.Put(context.Background(), reader, int64(len(data)), nil, PutOptions{ metadata: map[string]string{ "Content-Type": "application/octet-stream", }, }) c.Assert(err, checkv1.IsNil) c.Assert(n, checkv1.Equals, int64(len(data))) fsClient, err = fsNew(root) c.Assert(err, checkv1.IsNil) contents = nil // List recursively all files and verify. for content := range fsClient.List(globalContext, ListOptions{Recursive: true, ShowDir: DirNone}) { if content.Err != nil { err = content.Err break } contents = append(contents, content) } c.Assert(err, checkv1.IsNil) switch runtime.GOOS { case "darwin": c.Assert(len(contents), checkv1.Equals, 3) default: c.Assert(len(contents), checkv1.Equals, 4) } regularFiles = 0 // Test number of expected files. for _, content := range contents { if content.Type.IsRegular() { regularFiles++ continue } } switch runtime.GOOS { case "darwin": c.Assert(regularFiles, checkv1.Equals, 3) default: c.Assert(regularFiles, checkv1.Equals, 4) } } // Test put bucket aka 'mkdir()' operation. func (s *TestSuite) TestPutBucket(c *checkv1.C) { root, e := os.MkdirTemp(os.TempDir(), "fs-") c.Assert(e, checkv1.IsNil) defer os.RemoveAll(root) bucketPath := filepath.Join(root, "bucket") fsClient, err := fsNew(bucketPath) c.Assert(err, checkv1.IsNil) err = fsClient.MakeBucket(context.Background(), "us-east-1", true, false) c.Assert(err, checkv1.IsNil) } // Test stat bucket aka 'stat()' operation. func (s *TestSuite) TestStatBucket(c *checkv1.C) { root, e := os.MkdirTemp(os.TempDir(), "fs-") c.Assert(e, checkv1.IsNil) defer os.RemoveAll(root) bucketPath := filepath.Join(root, "bucket") fsClient, err := fsNew(bucketPath) c.Assert(err, checkv1.IsNil) err = fsClient.MakeBucket(context.Background(), "us-east-1", true, false) c.Assert(err, checkv1.IsNil) _, err = fsClient.Stat(context.Background(), StatOptions{}) c.Assert(err, checkv1.IsNil) } // Test bucket acl fails for directories. func (s *TestSuite) TestBucketACLFails(c *checkv1.C) { root, e := os.MkdirTemp(os.TempDir(), "fs-") c.Assert(e, checkv1.IsNil) defer os.RemoveAll(root) bucketPath := filepath.Join(root, "bucket") fsClient, err := fsNew(bucketPath) c.Assert(err, checkv1.IsNil) err = fsClient.MakeBucket(context.Background(), "us-east-1", true, false) c.Assert(err, checkv1.IsNil) // On windows setting permissions is not supported. if runtime.GOOS != "windows" { err = fsClient.SetAccess(context.Background(), "readonly", false) c.Assert(err, checkv1.IsNil) _, _, err = fsClient.GetAccess(context.Background()) c.Assert(err, checkv1.IsNil) } } // Test creating a file. func (s *TestSuite) TestPut(c *checkv1.C) { root, e := os.MkdirTemp(os.TempDir(), "fs-") c.Assert(e, checkv1.IsNil) defer os.RemoveAll(root) objectPath := filepath.Join(root, "object") fsClient, err := fsNew(objectPath) c.Assert(err, checkv1.IsNil) data := "hello" reader := bytes.NewReader([]byte(data)) var n int64 n, err = fsClient.Put(context.Background(), reader, int64(len(data)), nil, PutOptions{ metadata: map[string]string{ "Content-Type": "application/octet-stream", }, }, ) c.Assert(err, checkv1.IsNil) c.Assert(n, checkv1.Equals, int64(len(data))) } // Test read a file. func (s *TestSuite) TestGet(c *checkv1.C) { root, e := os.MkdirTemp(os.TempDir(), "fs-") c.Assert(e, checkv1.IsNil) defer os.RemoveAll(root) objectPath := filepath.Join(root, "object") fsClient, err := fsNew(objectPath) c.Assert(err, checkv1.IsNil) data := "hello" var reader io.Reader reader = bytes.NewReader([]byte(data)) n, err := fsClient.Put(context.Background(), reader, int64(len(data)), nil, PutOptions{ metadata: map[string]string{ "Content-Type": "application/octet-stream", }, }) c.Assert(err, checkv1.IsNil) c.Assert(n, checkv1.Equals, int64(len(data))) reader, _, err = fsClient.Get(context.Background(), GetOptions{}) c.Assert(err, checkv1.IsNil) var results bytes.Buffer _, e = io.Copy(&results, reader) c.Assert(e, checkv1.IsNil) c.Assert([]byte(data), checkv1.DeepEquals, results.Bytes()) } // Test get range in a file. func (s *TestSuite) TestGetRange(c *checkv1.C) { root, e := os.MkdirTemp(os.TempDir(), "fs-") c.Assert(e, checkv1.IsNil) defer os.RemoveAll(root) objectPath := filepath.Join(root, "object") fsClient, err := fsNew(objectPath) c.Assert(err, checkv1.IsNil) data := "hello world" var reader io.Reader reader = bytes.NewReader([]byte(data)) n, err := fsClient.Put(context.Background(), reader, int64(len(data)), nil, PutOptions{ metadata: map[string]string{ "Content-Type": "application/octet-stream", }, }) c.Assert(err, checkv1.IsNil) c.Assert(n, checkv1.Equals, int64(len(data))) reader, _, err = fsClient.Get(context.Background(), GetOptions{}) c.Assert(err, checkv1.IsNil) var results bytes.Buffer buf := make([]byte, 5) m, e := reader.(io.ReaderAt).ReadAt(buf, 0) c.Assert(e, checkv1.IsNil) c.Assert(m, checkv1.Equals, 5) _, e = results.Write(buf) c.Assert(e, checkv1.IsNil) c.Assert([]byte("hello"), checkv1.DeepEquals, results.Bytes()) } // Test stat file. func (s *TestSuite) TestStatObject(c *checkv1.C) { root, e := os.MkdirTemp(os.TempDir(), "fs-") c.Assert(e, checkv1.IsNil) defer os.RemoveAll(root) objectPath := filepath.Join(root, "object") fsClient, err := fsNew(objectPath) c.Assert(err, checkv1.IsNil) data := "hello" dataLen := len(data) reader := bytes.NewReader([]byte(data)) n, err := fsClient.Put(context.Background(), reader, int64(dataLen), nil, PutOptions{ metadata: map[string]string{ "Content-Type": "application/octet-stream", }, }, ) c.Assert(err, checkv1.IsNil) c.Assert(n, checkv1.Equals, int64(len(data))) content, err := fsClient.Stat(context.Background(), StatOptions{}) c.Assert(err, checkv1.IsNil) c.Assert(content.Size, checkv1.Equals, int64(dataLen)) } // Test copy. func (s *TestSuite) TestCopy(c *checkv1.C) { root, e := os.MkdirTemp(os.TempDir(), "fs-") c.Assert(e, checkv1.IsNil) defer os.RemoveAll(root) sourcePath := filepath.Join(root, "source") targetPath := filepath.Join(root, "target") fsClientTarget, err := fsNew(targetPath) c.Assert(err, checkv1.IsNil) fsClientSource, err := fsNew(sourcePath) c.Assert(err, checkv1.IsNil) data := "hello world" reader := bytes.NewReader([]byte(data)) n, err := fsClientSource.Put(context.Background(), reader, int64(len(data)), nil, PutOptions{ metadata: map[string]string{ "Content-Type": "application/octet-stream", }, }) c.Assert(err, checkv1.IsNil) c.Assert(n, checkv1.Equals, int64(len(data))) err = fsClientTarget.Copy(context.Background(), sourcePath, CopyOptions{size: int64(len(data))}, nil) c.Assert(err, checkv1.IsNil) } minio-client-0.0~20250403/cmd/client-fs_windows.go000066400000000000000000000041051477450377600215320ustar00rootroot00000000000000//go:build windows // +build windows // Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/rjeczalik/notify" var ( // EventTypePut contains the notify events that will cause a put (writer) EventTypePut = []notify.Event{notify.Create, notify.Write, notify.Rename, notify.FileNotifyChangeFileName, notify.FileNotifyChangeDirName} // EventTypeDelete contains the notify events that will cause a delete (remove) EventTypeDelete = []notify.Event{notify.Remove} // EventTypeGet contains the notify events that will cause a get (read) EventTypeGet = []notify.Event{notify.FileNotifyChangeLastAccess} ) // IsGetEvent checks if the event return is a get event. func IsGetEvent(event notify.Event) bool { return event¬ify.FileNotifyChangeLastAccess != 0 } // IsPutEvent checks if the event returned is a put event func IsPutEvent(event notify.Event) bool { if event¬ify.FileActionRenamedOldName != 0 { return false } for _, ev := range EventTypePut { if event&ev != 0 { return true } } return false } // IsDeleteEvent checks if the event returned is a delete event func IsDeleteEvent(event notify.Event) bool { return event¬ify.Remove != 0 || event¬ify.FileActionRenamedOldName != 0 } // getAllXattrs returns the extended attributes for a file if supported // by the OS func getAllXattrs(_ string) (map[string]string, error) { return nil, nil } minio-client-0.0~20250403/cmd/client-s3-trace_v2.go000066400000000000000000000043441477450377600214050ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "net/http" "net/http/httputil" "strings" "github.com/minio/mc/pkg/httptracer" "github.com/minio/pkg/v3/console" ) // traceV2 - tracing structure for signature version '2'. type traceV2 struct{} // newTraceV2 - initialize Trace structure func newTraceV2() httptracer.HTTPTracer { return traceV2{} } // Request - Trace HTTP Request func (t traceV2) Request(req *http.Request) (err error) { origAuth := req.Header.Get("Authorization") if strings.TrimSpace(origAuth) != "" { // Authorization (S3 v2 signature) Format: // Authorization: AWS AKIAJVA5BMMU2RHO6IO1:Y10YHUZ0DTUterAUI6w3XKX7Iqk= // Set a temporary redacted auth req.Header.Set("Authorization", "AWS **REDACTED**:**REDACTED**") var reqTrace []byte reqTrace, err = httputil.DumpRequestOut(req, false) // Only display header if err == nil { console.Debug(string(reqTrace)) } // Undo req.Header.Set("Authorization", origAuth) } return err } // Response - Trace HTTP Response func (t traceV2) Response(resp *http.Response) (err error) { var respTrace []byte // For errors we make sure to dump response body as well. if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent && resp.StatusCode != http.StatusNoContent { respTrace, err = httputil.DumpResponse(resp, true) } else { respTrace, err = httputil.DumpResponse(resp, false) } if err == nil { console.Debug(string(respTrace)) } if resp.TLS != nil { printTLSCertInfo(resp.TLS) } return err } minio-client-0.0~20250403/cmd/client-s3-trace_v4.go000066400000000000000000000056001477450377600214030ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "net/http" "net/http/httputil" "regexp" "strings" "github.com/minio/mc/pkg/httptracer" "github.com/minio/pkg/v3/console" ) // traceV4 - tracing structure for signature version '4'. type traceV4 struct{} // newTraceV4 - initialize Trace structure func newTraceV4() httptracer.HTTPTracer { return traceV4{} } // Request - Trace HTTP Request func (t traceV4) Request(req *http.Request) (err error) { origAuth := req.Header.Get("Authorization") printTrace := func() error { reqTrace, rerr := httputil.DumpRequestOut(req, false) // Only display header if rerr == nil { console.Debug(string(reqTrace)) } return rerr } if strings.TrimSpace(origAuth) != "" { // Authorization (S3 v4 signature) Format: // Authorization: AWS4-HMAC-SHA256 Credential=AKIAJNACEGBGMXBHLEZA/20150524/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=bbfaa693c626021bcb5f911cd898a1a30206c1fad6bad1e0eb89e282173bd24c // Strip out accessKeyID from: Credential=////aws4_request regCred := regexp.MustCompile("Credential=([A-Z0-9]+)/") newAuth := regCred.ReplaceAllString(origAuth, "Credential=**REDACTED**/") // Strip out 256-bit signature from: Signature=<256-bit signature> regSign := regexp.MustCompile("Signature=([[0-9a-f]+)") newAuth = regSign.ReplaceAllString(newAuth, "Signature=**REDACTED**") // Set a temporary redacted auth req.Header.Set("Authorization", newAuth) err = printTrace() // Undo req.Header.Set("Authorization", origAuth) } else { err = printTrace() } return err } // Response - Trace HTTP Response func (t traceV4) Response(resp *http.Response) (err error) { var respTrace []byte // For errors we make sure to dump response body as well. if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent && resp.StatusCode != http.StatusNoContent { respTrace, err = httputil.DumpResponse(resp, true) } else { respTrace, err = httputil.DumpResponse(resp, false) } if err == nil { console.Debug(string(respTrace)) } if resp.TLS != nil { printTLSCertInfo(resp.TLS) } return err } minio-client-0.0~20250403/cmd/client-s3.go000066400000000000000000002607461477450377600177140ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "crypto/tls" "encoding/json" "errors" "fmt" "hash/fnv" "io" "net" "net/http" "net/url" "os" "path" "path/filepath" "regexp" "sort" "strings" "sync" "sync/atomic" "time" "github.com/minio/minio-go/v7/pkg/cors" "github.com/minio/pkg/v3/env" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/encrypt" "github.com/minio/minio-go/v7/pkg/lifecycle" "github.com/minio/minio-go/v7/pkg/notification" "github.com/minio/minio-go/v7/pkg/policy" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/minio-go/v7/pkg/s3utils" "github.com/minio/minio-go/v7/pkg/sse" "github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/pkg/v3/mimedb" "github.com/minio/mc/pkg/deadlineconn" "github.com/minio/mc/pkg/probe" ) // S3Client construct type S3Client struct { sync.Mutex targetURL *ClientURL api *minio.Client virtualStyle bool } const ( amazonHostNameAccelerated = "s3-accelerate.amazonaws.com" googleHostName = "storage.googleapis.com" serverEncryptionKeyPrefix = "x-amz-server-side-encryption" defaultRecordDelimiter = "\n" defaultFieldDelimiter = "," ) const ( recordDelimiterType = "recorddelimiter" fieldDelimiterType = "fielddelimiter" quoteCharacterType = "quotechar" quoteEscapeCharacterType = "quoteescchar" quoteFieldsType = "quotefields" fileHeaderType = "fileheader" commentCharType = "commentchar" typeJSONType = "type" // AmzObjectLockMode sets object lock mode AmzObjectLockMode = "X-Amz-Object-Lock-Mode" // AmzObjectLockRetainUntilDate sets object lock retain until date AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date" // AmzObjectLockLegalHold sets object lock legal hold AmzObjectLockLegalHold = "X-Amz-Object-Lock-Legal-Hold" amzObjectSSEKMSKeyID = "X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id" amzObjectSSE = "X-Amz-Server-Side-Encryption" ) type dialContext func(ctx context.Context, network, addr string) (net.Conn, error) // newCustomDialContext setups a custom dialer for any external communication and proxies. func newCustomDialContext(c *Config) dialContext { return func(ctx context.Context, network, addr string) (net.Conn, error) { dialer := &net.Dialer{ Timeout: 10 * time.Second, KeepAlive: 15 * time.Second, } if ip, ok := globalResolvers[addr]; ok { if _, port, err := net.SplitHostPort(addr); err == nil { addr = net.JoinHostPort(ip.String(), port) } else { addr = ip.String() } } conn, err := dialer.DialContext(ctx, network, addr) if err != nil { return nil, err } dconn := deadlineconn.New(conn). WithReadDeadline(c.ConnReadDeadline). WithWriteDeadline(c.ConnWriteDeadline) return dconn, nil } } // newCustomDialTLSContext setups a custom TLS dialer for any external communication and proxies. func newCustomDialTLSContext(tlsConf *tls.Config) dialContext { return func(ctx context.Context, network, addr string) (net.Conn, error) { dialer := &tls.Dialer{ NetDialer: &net.Dialer{ Timeout: 10 * time.Second, KeepAlive: 15 * time.Second, }, Config: tlsConf, } if ip, ok := globalResolvers[addr]; ok { dialer.Config = dialer.Config.Clone() if host, port, err := net.SplitHostPort(addr); err == nil { dialer.Config.ServerName = host // Set SNI addr = net.JoinHostPort(ip.String(), port) } else { dialer.Config.ServerName = addr // Set SNI addr = ip.String() } } return dialer.DialContext(ctx, network, addr) } } var timeSentinel = time.Unix(0, 0).UTC() // getConfigHash returns the Hash for che *Config func getConfigHash(config *Config) uint32 { // Creates a parsed URL. targetURL := newClientURL(config.HostURL) // Save if target supports virtual host style. hostName := targetURL.Host // Generate a hash out of s3Conf. confHash := fnv.New32a() confHash.Write([]byte(hostName + config.AccessKey + config.SecretKey + config.SessionToken)) confSum := confHash.Sum32() return confSum } // isHostTLS returns true if the Host URL is https func isHostTLS(config *Config) bool { // By default enable HTTPs. useTLS := true targetURL := newClientURL(config.HostURL) if targetURL.Scheme == "http" { useTLS = false } return useTLS } type notifyExpiringTLS struct { transport http.RoundTripper } var globalExpiringCerts sync.Map func (n notifyExpiringTLS) RoundTrip(req *http.Request) (res *http.Response, err error) { if n.transport == nil { return nil, errors.New("invalid transport") } res, err = n.transport.RoundTrip(req) if err != nil || res.TLS == nil || len(res.TLS.PeerCertificates) == 0 { return res, err } cert := res.TLS.PeerCertificates[0] // leaf certificate validityDur := cert.NotAfter.Sub(cert.NotBefore) // Warn if less than 10% of time left and it is less than 28 days. if time.Until(cert.NotAfter) < time.Duration(min(0.1*float64(validityDur), 28*24*float64(time.Hour))) { globalExpiringCerts.Store(req.Host, cert.NotAfter) } return res, err } // useTrailingHeaders will enable trailing headers on S3 clients. var useTrailingHeaders atomic.Bool // newFactory encloses New function with client cache. func newFactory() func(config *Config) (Client, *probe.Error) { clientCache := make(map[uint32]*minio.Client) var mutex sync.Mutex // Return New function. return func(config *Config) (Client, *probe.Error) { // Creates a parsed URL. targetURL := newClientURL(config.HostURL) // Save if target supports virtual host style. hostName := targetURL.Host confSum := getConfigHash(config) useTLS := isHostTLS(config) // Instantiate s3 s3Clnt := &S3Client{} // Save the target URL. s3Clnt.targetURL = targetURL s3Clnt.virtualStyle = isVirtualHostStyle(hostName, config.Lookup) isS3AcceleratedEndpoint := isAmazonAccelerated(hostName) if s3Clnt.virtualStyle { // If Google URL replace it with 'storage.googleapis.com' if isGoogle(hostName) { hostName = googleHostName } } // Lookup previous cache by hash. mutex.Lock() defer mutex.Unlock() var api *minio.Client var found bool if api, found = clientCache[confSum]; !found { transport := config.getTransport() credsChain, err := config.getCredsChain() if err != nil { return nil, err } var e error options := minio.Options{ Creds: credentials.NewChainCredentials(credsChain), Secure: useTLS, Region: env.Get("MC_REGION", env.Get("AWS_REGION", "")), BucketLookup: config.Lookup, Transport: transport, TrailingHeaders: useTrailingHeaders.Load(), } api, e = minio.New(hostName, &options) if e != nil { return nil, probe.NewError(e) } // If Amazon Accelerated URL is requested enable it. if isS3AcceleratedEndpoint { api.SetS3TransferAccelerate(amazonHostNameAccelerated) } // Set app info. api.SetAppInfo(config.AppName, config.AppVersion) // Cache the new MinIO Client with hash of config as key. clientCache[confSum] = api } // Store the new api object. s3Clnt.api = api return s3Clnt, nil } } // S3New returns an initialized S3Client structure. If debug is enabled, // it also enables an internal trace transport. var S3New = newFactory() // GetURL get url. func (c *S3Client) GetURL() ClientURL { return c.targetURL.Clone() } // AddNotificationConfig - Add bucket notification func (c *S3Client) AddNotificationConfig(ctx context.Context, arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error { bucket, _ := c.url2BucketAndObject() accountArn, err := notification.NewArnFromString(arn) if err != nil { return probe.NewError(invalidArgumentErr(err)).Untrace() } nc := notification.NewConfig(accountArn) // Get any enabled notification. mb, e := c.api.GetBucketNotification(ctx, bucket) if e != nil { return probe.NewError(e) } // Configure events for _, event := range events { switch event { case "put": nc.AddEvents(notification.ObjectCreatedAll) case "delete": nc.AddEvents(notification.ObjectRemovedAll) case "get": nc.AddEvents(notification.ObjectAccessedAll) case "replica": nc.AddEvents(notification.EventType("s3:Replication:*")) case "ilm": nc.AddEvents(notification.EventType("s3:ObjectRestore:*")) nc.AddEvents(notification.EventType("s3:ObjectTransition:*")) case "scanner": nc.AddEvents(notification.EventType("s3:Scanner:ManyVersions")) nc.AddEvents(notification.EventType("s3:Scanner:BigPrefix")) default: return errInvalidArgument().Trace(events...) } } if prefix != "" { nc.AddFilterPrefix(prefix) } if suffix != "" { nc.AddFilterSuffix(suffix) } switch accountArn.Service { case "sns": if !mb.AddTopic(nc) { return errInvalidArgument().Trace("Overlapping Topic configs") } case "sqs": if !mb.AddQueue(nc) { return errInvalidArgument().Trace("Overlapping Queue configs") } case "lambda": if !mb.AddLambda(nc) { return errInvalidArgument().Trace("Overlapping lambda configs") } default: return errInvalidArgument().Trace(accountArn.Service) } // Set the new bucket configuration if err := c.api.SetBucketNotification(ctx, bucket, mb); err != nil { if ignoreExisting && strings.Contains(err.Error(), "An object key name filtering rule defined with overlapping prefixes, overlapping suffixes, or overlapping combinations of prefixes and suffixes for the same event types") { return nil } return probe.NewError(err) } return nil } // RemoveNotificationConfig - Remove bucket notification func (c *S3Client) RemoveNotificationConfig(ctx context.Context, arn, event, prefix, suffix string) *probe.Error { bucket, _ := c.url2BucketAndObject() // Remove all notification configs if arn is empty if arn == "" { if err := c.api.RemoveAllBucketNotification(ctx, bucket); err != nil { return probe.NewError(err) } return nil } mb, e := c.api.GetBucketNotification(ctx, bucket) if e != nil { return probe.NewError(e) } accountArn, err := notification.NewArnFromString(arn) if err != nil { return probe.NewError(invalidArgumentErr(err)).Untrace() } // if we are passed filters for either events, suffix or prefix, then only delete the single event that matches // the arguments if event != "" || suffix != "" || prefix != "" { // Translate events to type events for comparison events := strings.Split(event, ",") var eventsTyped []notification.EventType for _, e := range events { switch e { case "put": eventsTyped = append(eventsTyped, notification.ObjectCreatedAll) case "delete": eventsTyped = append(eventsTyped, notification.ObjectRemovedAll) case "get": eventsTyped = append(eventsTyped, notification.ObjectAccessedAll) case "replica": eventsTyped = append(eventsTyped, notification.EventType("s3:Replication:*")) case "ilm": eventsTyped = append(eventsTyped, notification.EventType("s3:ObjectRestore:*")) eventsTyped = append(eventsTyped, notification.EventType("s3:ObjectTransition:*")) case "scanner": eventsTyped = append(eventsTyped, notification.EventType("s3:Scanner:ManyVersions")) eventsTyped = append(eventsTyped, notification.EventType("s3:Scanner:BigPrefix")) default: return errInvalidArgument().Trace(events...) } } var err error // based on the arn type, we'll look for the event in the corresponding sublist and delete it if there's a match switch accountArn.Service { case "sns": err = mb.RemoveTopicByArnEventsPrefixSuffix(accountArn, eventsTyped, prefix, suffix) case "sqs": err = mb.RemoveQueueByArnEventsPrefixSuffix(accountArn, eventsTyped, prefix, suffix) case "lambda": err = mb.RemoveLambdaByArnEventsPrefixSuffix(accountArn, eventsTyped, prefix, suffix) default: return errInvalidArgument().Trace(accountArn.Service) } if err != nil { return probe.NewError(err) } } else { // remove all events for matching arn switch accountArn.Service { case "sns": mb.RemoveTopicByArn(accountArn) case "sqs": mb.RemoveQueueByArn(accountArn) case "lambda": mb.RemoveLambdaByArn(accountArn) default: return errInvalidArgument().Trace(accountArn.Service) } } // Set the new bucket configuration if e := c.api.SetBucketNotification(ctx, bucket, mb); e != nil { return probe.NewError(e) } return nil } // NotificationConfig notification config type NotificationConfig struct { ID string `json:"id"` Arn string `json:"arn"` Events []string `json:"events"` Prefix string `json:"prefix"` Suffix string `json:"suffix"` } // ListNotificationConfigs - List notification configs func (c *S3Client) ListNotificationConfigs(ctx context.Context, arn string) ([]NotificationConfig, *probe.Error) { var configs []NotificationConfig bucket, _ := c.url2BucketAndObject() mb, e := c.api.GetBucketNotification(ctx, bucket) if e != nil { return nil, probe.NewError(e) } // Generate pretty event names from event types prettyEventNames := func(eventsTypes []notification.EventType) []string { var result []string for _, eventType := range eventsTypes { result = append(result, string(eventType)) } return result } getFilters := func(config notification.Config) (prefix, suffix string) { if config.Filter == nil { return } for _, filter := range config.Filter.S3Key.FilterRules { if strings.ToLower(filter.Name) == "prefix" { prefix = filter.Value } if strings.ToLower(filter.Name) == "suffix" { suffix = filter.Value } } return prefix, suffix } for _, config := range mb.TopicConfigs { if arn != "" && config.Topic != arn { continue } prefix, suffix := getFilters(config.Config) configs = append(configs, NotificationConfig{ ID: config.ID, Arn: config.Topic, Events: prettyEventNames(config.Events), Prefix: prefix, Suffix: suffix, }) } for _, config := range mb.QueueConfigs { if arn != "" && config.Queue != arn { continue } prefix, suffix := getFilters(config.Config) configs = append(configs, NotificationConfig{ ID: config.ID, Arn: config.Queue, Events: prettyEventNames(config.Events), Prefix: prefix, Suffix: suffix, }) } for _, config := range mb.LambdaConfigs { if arn != "" && config.Lambda != arn { continue } prefix, suffix := getFilters(config.Config) configs = append(configs, NotificationConfig{ ID: config.ID, Arn: config.Lambda, Events: prettyEventNames(config.Events), Prefix: prefix, Suffix: suffix, }) } return configs, nil } // Supported content types var supportedContentTypes = []string{ "csv", "json", "gzip", "bzip2", } // set the SelectObjectOutputSerialization struct using options passed in by client. If unspecified, // default S3 API specified defaults func selectObjectOutputOpts(selOpts SelectObjectOpts, i minio.SelectObjectInputSerialization) minio.SelectObjectOutputSerialization { var isOK bool var recDelim, fldDelim, quoteChar, quoteEscChar, qf string o := minio.SelectObjectOutputSerialization{} if _, ok := selOpts.OutputSerOpts["json"]; ok { jo := minio.JSONOutputOptions{} if recDelim, isOK = selOpts.OutputSerOpts["json"][recordDelimiterType]; !isOK { recDelim = "\n" } jo.SetRecordDelimiter(recDelim) o.JSON = &jo } if _, ok := selOpts.OutputSerOpts["csv"]; ok { ocsv := minio.CSVOutputOptions{} if recDelim, isOK = selOpts.OutputSerOpts["csv"][recordDelimiterType]; !isOK { recDelim = defaultRecordDelimiter } ocsv.SetRecordDelimiter(recDelim) if fldDelim, isOK = selOpts.OutputSerOpts["csv"][fieldDelimiterType]; !isOK { fldDelim = defaultFieldDelimiter } ocsv.SetFieldDelimiter(fldDelim) if quoteChar, isOK = selOpts.OutputSerOpts["csv"][quoteCharacterType]; isOK { ocsv.SetQuoteCharacter(quoteChar) } if quoteEscChar, isOK = selOpts.OutputSerOpts["csv"][quoteEscapeCharacterType]; isOK { ocsv.SetQuoteEscapeCharacter(quoteEscChar) } if qf, isOK = selOpts.OutputSerOpts["csv"][quoteFieldsType]; isOK { ocsv.SetQuoteFields(minio.CSVQuoteFields(qf)) } o.CSV = &ocsv } // default to CSV output if options left unspecified if o.CSV == nil && o.JSON == nil { if i.JSON != nil { j := minio.JSONOutputOptions{} j.SetRecordDelimiter("\n") o.JSON = &j } else { ocsv := minio.CSVOutputOptions{} ocsv.SetRecordDelimiter(defaultRecordDelimiter) ocsv.SetFieldDelimiter(defaultFieldDelimiter) o.CSV = &ocsv } } return o } func trimCompressionFileExts(name string) string { return strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(name, ".gz"), ".bz"), ".bz2") } // set the SelectObjectInputSerialization struct using options passed in by client. If unspecified, // default S3 API specified defaults func selectObjectInputOpts(selOpts SelectObjectOpts, object string) minio.SelectObjectInputSerialization { var isOK bool var recDelim, fldDelim, quoteChar, quoteEscChar, fileHeader, commentChar, typ string i := minio.SelectObjectInputSerialization{} if _, ok := selOpts.InputSerOpts["parquet"]; ok { iparquet := minio.ParquetInputOptions{} i.Parquet = &iparquet } if _, ok := selOpts.InputSerOpts["json"]; ok { j := minio.JSONInputOptions{} if typ = selOpts.InputSerOpts["json"][typeJSONType]; typ != "" { j.SetType(minio.JSONType(typ)) } i.JSON = &j } if _, ok := selOpts.InputSerOpts["csv"]; ok { icsv := minio.CSVInputOptions{} icsv.SetRecordDelimiter(defaultRecordDelimiter) if recDelim, isOK = selOpts.InputSerOpts["csv"][recordDelimiterType]; isOK { icsv.SetRecordDelimiter(recDelim) } if fldDelim, isOK = selOpts.InputSerOpts["csv"][fieldDelimiterType]; isOK { icsv.SetFieldDelimiter(fldDelim) } if quoteChar, isOK = selOpts.InputSerOpts["csv"][quoteCharacterType]; isOK { icsv.SetQuoteCharacter(quoteChar) } if quoteEscChar, isOK = selOpts.InputSerOpts["csv"][quoteEscapeCharacterType]; isOK { icsv.SetQuoteEscapeCharacter(quoteEscChar) } if fileHeader, isOK = selOpts.InputSerOpts["csv"][fileHeaderType]; isOK { icsv.SetFileHeaderInfo(minio.CSVFileHeaderInfo(fileHeader)) } if commentChar, isOK = selOpts.InputSerOpts["csv"][commentCharType]; isOK { icsv.SetComments(commentChar) } i.CSV = &icsv } if i.CSV == nil && i.JSON == nil && i.Parquet == nil { ext := filepath.Ext(trimCompressionFileExts(object)) if strings.Contains(ext, "csv") { icsv := minio.CSVInputOptions{} icsv.SetRecordDelimiter(defaultRecordDelimiter) icsv.SetFieldDelimiter(defaultFieldDelimiter) icsv.SetFileHeaderInfo(minio.CSVFileHeaderInfoUse) i.CSV = &icsv } if strings.Contains(ext, "parquet") || strings.Contains(object, ".parquet") { iparquet := minio.ParquetInputOptions{} i.Parquet = &iparquet } if strings.Contains(ext, "json") { ijson := minio.JSONInputOptions{} ijson.SetType(minio.JSONLinesType) i.JSON = &ijson } } if i.CompressionType == "" { i.CompressionType = selectCompressionType(selOpts, object) } return i } // get client specified compression type or default compression type from file extension func selectCompressionType(selOpts SelectObjectOpts, object string) minio.SelectCompressionType { ext := filepath.Ext(object) contentType := mimedb.TypeByExtension(ext) if selOpts.CompressionType != "" { return selOpts.CompressionType } if strings.Contains(ext, "parquet") || strings.Contains(object, ".parquet") { return minio.SelectCompressionNONE } if contentType != "" { if strings.Contains(contentType, "gzip") { return minio.SelectCompressionGZIP } else if strings.Contains(contentType, "bzip") { return minio.SelectCompressionBZIP } } return minio.SelectCompressionNONE } // Select - select object content wrapper. func (c *S3Client) Select(ctx context.Context, expression string, sse encrypt.ServerSide, selOpts SelectObjectOpts) (io.ReadCloser, *probe.Error) { opts := minio.SelectObjectOptions{ Expression: expression, ExpressionType: minio.QueryExpressionTypeSQL, // Set any encryption headers ServerSideEncryption: sse, } bucket, object := c.url2BucketAndObject() opts.InputSerialization = selectObjectInputOpts(selOpts, object) opts.OutputSerialization = selectObjectOutputOpts(selOpts, opts.InputSerialization) reader, e := c.api.SelectObjectContent(ctx, bucket, object, opts) if e != nil { return nil, probe.NewError(e) } return reader, nil } func (c *S3Client) notificationToEventsInfo(ninfo notification.Info) []EventInfo { eventsInfo := make([]EventInfo, len(ninfo.Records)) for i, record := range ninfo.Records { bucketName := record.S3.Bucket.Name var key string // Unescape only if needed, look for URL encoded content. if strings.Contains(record.S3.Object.Key, "%2F") { var e error key, e = url.QueryUnescape(record.S3.Object.Key) if e != nil { key = record.S3.Object.Key } } else { key = record.S3.Object.Key } u := c.targetURL.Clone() u.Path = path.Join(string(u.Separator), bucketName, key) if strings.HasPrefix(record.EventName, "s3:ObjectCreated:") { if strings.HasPrefix(record.EventName, "s3:ObjectCreated:Copy") { eventsInfo[i] = EventInfo{ Time: record.EventTime, Size: record.S3.Object.Size, UserMetadata: record.S3.Object.UserMetadata, Path: u.String(), Type: notification.ObjectCreatedCopy, Host: record.Source.Host, Port: record.Source.Port, UserAgent: record.Source.UserAgent, } } else if strings.HasPrefix(record.EventName, "s3:ObjectCreated:PutRetention") { eventsInfo[i] = EventInfo{ Time: record.EventTime, Size: record.S3.Object.Size, UserMetadata: record.S3.Object.UserMetadata, Path: u.String(), Type: notification.EventType("s3:ObjectCreated:PutRetention"), Host: record.Source.Host, Port: record.Source.Port, UserAgent: record.Source.UserAgent, } } else if strings.HasPrefix(record.EventName, "s3:ObjectCreated:PutLegalHold") { eventsInfo[i] = EventInfo{ Time: record.EventTime, Size: record.S3.Object.Size, UserMetadata: record.S3.Object.UserMetadata, Path: u.String(), Type: notification.EventType("s3:ObjectCreated:PutLegalHold"), Host: record.Source.Host, Port: record.Source.Port, UserAgent: record.Source.UserAgent, } } else { eventsInfo[i] = EventInfo{ Time: record.EventTime, Size: record.S3.Object.Size, UserMetadata: record.S3.Object.UserMetadata, Path: u.String(), Type: notification.ObjectCreatedPut, Host: record.Source.Host, Port: record.Source.Port, UserAgent: record.Source.UserAgent, } } } else { eventsInfo[i] = EventInfo{ Time: record.EventTime, Size: record.S3.Object.Size, UserMetadata: record.S3.Object.UserMetadata, Path: u.String(), Type: notification.EventType(record.EventName), Host: record.Source.Host, Port: record.Source.Port, UserAgent: record.Source.UserAgent, } } } return eventsInfo } // Watch - Start watching on all bucket events for a given account ID. func (c *S3Client) Watch(ctx context.Context, options WatchOptions) (*WatchObject, *probe.Error) { // Extract bucket and object. bucket, object := c.url2BucketAndObject() // Validation if bucket == "" && object != "" { return nil, errInvalidArgument().Trace(bucket, object) } if object != "" && options.Prefix != "" { return nil, errInvalidArgument().Trace(options.Prefix, object) } // Flag set to set the notification. var events []string for _, event := range options.Events { switch event { case "put": events = append(events, string(notification.ObjectCreatedAll)) case "delete": events = append(events, string(notification.ObjectRemovedAll)) case "get": events = append(events, string(notification.ObjectAccessedAll)) case "replica": events = append(events, "s3:Replication:*") // TODO: add it to minio-go as constant case "ilm": events = append(events, "s3:ObjectRestore:*", "s3:ObjectTransition:*") // TODO: add it to minio-go as constant case "bucket-creation": events = append(events, string(notification.BucketCreatedAll)) case "bucket-removal": events = append(events, string(notification.BucketRemovedAll)) case "scanner": events = append(events, "s3:Scanner:ManyVersions", "s3:Scanner:BigPrefix") default: return nil, errInvalidArgument().Trace(event) } } wo := &WatchObject{ EventInfoChan: make(chan []EventInfo), ErrorChan: make(chan *probe.Error), DoneChan: make(chan struct{}), } listenCtx, listenCancel := context.WithCancel(ctx) var eventsCh <-chan notification.Info if bucket != "" { if object != "" && options.Prefix == "" { options.Prefix = object } eventsCh = c.api.ListenBucketNotification(listenCtx, bucket, options.Prefix, options.Suffix, events) } else { eventsCh = c.api.ListenNotification(listenCtx, "", "", events) } go func() { defer close(wo.EventInfoChan) defer close(wo.ErrorChan) defer listenCancel() for { // Start listening on all bucket events. select { case notificationInfo, ok := <-eventsCh: if !ok { return } if notificationInfo.Err != nil { var perr *probe.Error if minio.ToErrorResponse(notificationInfo.Err).Code == "NotImplemented" { perr = probe.NewError(APINotImplemented{ API: "Watch", APIType: c.GetURL().String(), }) } else { perr = probe.NewError(notificationInfo.Err) } wo.Errors() <- perr } else { wo.Events() <- c.notificationToEventsInfo(notificationInfo) } case <-wo.DoneChan: return } } }() return wo, nil } // Get - get object with GET options. func (c *S3Client) Get(ctx context.Context, opts GetOptions) (io.ReadCloser, *ClientContent, *probe.Error) { bucket, object := c.url2BucketAndObject() o := minio.GetObjectOptions{ ServerSideEncryption: opts.SSE, VersionID: opts.VersionID, PartNumber: opts.PartNumber, } if opts.Zip { o.Set("x-minio-extract", "true") } if opts.RangeStart != 0 { err := o.SetRange(opts.RangeStart, 0) if err != nil { return nil, nil, probe.NewError(err) } } if opts.Preserve { o.Set("X-Amz-Tagging-Directive", "ACCESS") } // Disallow automatic decompression for some objects with content-encoding set. o.Set("Accept-Encoding", "identity") cr := minio.Core{Client: c.api} reader, objectInfo, _, e := cr.GetObject(ctx, bucket, object, o) if e != nil { errResponse := minio.ToErrorResponse(e) if errResponse.Code == "NoSuchBucket" { return nil, nil, probe.NewError(BucketDoesNotExist{ Bucket: bucket, }) } if errResponse.Code == "InvalidBucketName" { return nil, nil, probe.NewError(BucketInvalid{ Bucket: bucket, }) } if errResponse.Code == "NoSuchKey" { return nil, nil, probe.NewError(ObjectMissing{}) } return nil, nil, probe.NewError(e) } return reader, c.objectInfo2ClientContent(bucket, objectInfo), nil } // Copy - copy object, uses server side copy API. Also uses an abstracted API // such that large file sizes will be copied in multipart manner on server // side. func (c *S3Client) Copy(ctx context.Context, source string, opts CopyOptions, progress io.Reader) *probe.Error { dstBucket, dstObject := c.url2BucketAndObject() if dstBucket == "" { return probe.NewError(BucketNameEmpty{}) } metadata := make(map[string]string, len(opts.metadata)) for k, v := range opts.metadata { metadata[k] = v } delete(metadata, "X-Amz-Storage-Class") if opts.storageClass != "" { metadata["X-Amz-Storage-Class"] = opts.storageClass } tokens := splitStr(source, string(c.targetURL.Separator), 3) // Source object srcOpts := minio.CopySrcOptions{ Bucket: tokens[1], Object: tokens[2], Encryption: opts.srcSSE, VersionID: opts.versionID, } destOpts := minio.CopyDestOptions{ Bucket: dstBucket, Object: dstObject, Encryption: opts.tgtSSE, Progress: progress, Size: opts.size, } if lockModeStr, ok := metadata[AmzObjectLockMode]; ok { destOpts.Mode = minio.RetentionMode(strings.ToUpper(lockModeStr)) delete(metadata, AmzObjectLockMode) } if retainUntilDateStr, ok := metadata[AmzObjectLockRetainUntilDate]; ok { delete(metadata, AmzObjectLockRetainUntilDate) if t, e := time.Parse(time.RFC3339, retainUntilDateStr); e == nil { destOpts.RetainUntilDate = t.UTC() } } if lh, ok := metadata[AmzObjectLockLegalHold]; ok { destOpts.LegalHold = minio.LegalHoldStatus(lh) delete(metadata, AmzObjectLockLegalHold) } // Assign metadata after irrelevant parts are delete above destOpts.UserMetadata = metadata destOpts.ReplaceMetadata = len(metadata) > 0 var e error if opts.disableMultipart || opts.size < 64*1024*1024 { _, e = c.api.CopyObject(ctx, destOpts, srcOpts) } else { _, e = c.api.ComposeObject(ctx, destOpts, srcOpts) } if e != nil { errResponse := minio.ToErrorResponse(e) if errResponse.Code == "AccessDenied" { return probe.NewError(PathInsufficientPermission{ Path: c.targetURL.String(), }) } if errResponse.Code == "NoSuchBucket" { return probe.NewError(BucketDoesNotExist{ Bucket: dstBucket, }) } if errResponse.Code == "InvalidBucketName" { return probe.NewError(BucketInvalid{ Bucket: dstBucket, }) } if errResponse.Code == "NoSuchKey" { return probe.NewError(ObjectMissing{}) } return probe.NewError(e) } return nil } // Put - upload an object with custom metadata. func (c *S3Client) Put(ctx context.Context, reader io.Reader, size int64, progress io.Reader, putOpts PutOptions) (int64, *probe.Error) { bucket, object := c.url2BucketAndObject() if bucket == "" { return 0, probe.NewError(BucketNameEmpty{}) } metadata := make(map[string]string, len(putOpts.metadata)) for k, v := range putOpts.metadata { metadata[k] = v } // Do not copy storage class, it needs to be specified in putOpts delete(metadata, "X-Amz-Storage-Class") contentType, ok := metadata["Content-Type"] if ok { delete(metadata, "Content-Type") } else { // Set content-type if not specified. contentType = "application/octet-stream" } cacheControl, ok := metadata["Cache-Control"] if ok { delete(metadata, "Cache-Control") } contentEncoding, ok := metadata["Content-Encoding"] if ok { delete(metadata, "Content-Encoding") } contentDisposition, ok := metadata["Content-Disposition"] if ok { delete(metadata, "Content-Disposition") } contentLanguage, ok := metadata["Content-Language"] if ok { delete(metadata, "Content-Language") } var tagsMap map[string]string tagsHdr, ok := metadata["X-Amz-Tagging"] if ok { tagsSet, e := tags.Parse(tagsHdr, true) if e != nil { return 0, probe.NewError(e) } tagsMap = tagsSet.ToMap() delete(metadata, "X-Amz-Tagging") } lockModeStr, ok := metadata[AmzObjectLockMode] lockMode := minio.RetentionMode("") if ok { lockMode = minio.RetentionMode(strings.ToUpper(lockModeStr)) delete(metadata, AmzObjectLockMode) } retainUntilDate := timeSentinel retainUntilDateStr, ok := metadata[AmzObjectLockRetainUntilDate] if ok { delete(metadata, AmzObjectLockRetainUntilDate) if t, e := time.Parse(time.RFC3339, retainUntilDateStr); e == nil { retainUntilDate = t.UTC() } } disableSha256 := putOpts.checksum.IsSet() // pre-emptively disable sha256 payload, if checksum is set. opts := minio.PutObjectOptions{ UserMetadata: metadata, UserTags: tagsMap, Progress: progress, ContentType: contentType, CacheControl: cacheControl, ContentDisposition: contentDisposition, ContentEncoding: contentEncoding, ContentLanguage: contentLanguage, StorageClass: strings.ToUpper(putOpts.storageClass), ServerSideEncryption: putOpts.sse, SendContentMd5: putOpts.md5, Checksum: putOpts.checksum, DisableMultipart: putOpts.disableMultipart, DisableContentSha256: disableSha256, PartSize: putOpts.multipartSize, NumThreads: putOpts.multipartThreads, ConcurrentStreamParts: putOpts.concurrentStream, // if enabled honors NumThreads for piped() uploads } if !retainUntilDate.IsZero() && !retainUntilDate.Equal(timeSentinel) { opts.RetainUntilDate = retainUntilDate } if lockModeStr != "" { opts.Mode = lockMode opts.SendContentMd5 = true } if lh, ok := metadata[AmzObjectLockLegalHold]; ok { delete(metadata, AmzObjectLockLegalHold) opts.LegalHold = minio.LegalHoldStatus(strings.ToUpper(lh)) opts.SendContentMd5 = true } if putOpts.ifNotExists { // Only supported in newer MinIO releases. opts.SetMatchETagExcept("*") } ui, e := c.api.PutObject(ctx, bucket, object, reader, size, opts) if e != nil { errResponse := minio.ToErrorResponse(e) if errResponse.Code == "UnexpectedEOF" || e == io.EOF { return ui.Size, probe.NewError(UnexpectedEOF{ TotalSize: size, TotalWritten: ui.Size, }) } if errResponse.Code == "AccessDenied" { return ui.Size, probe.NewError(PathInsufficientPermission{ Path: c.targetURL.String(), }) } if errResponse.Code == "MethodNotAllowed" { return ui.Size, probe.NewError(ObjectAlreadyExists{ Object: object, }) } if errResponse.Code == "XMinioObjectExistsAsDirectory" { return ui.Size, probe.NewError(ObjectAlreadyExistsAsDirectory{ Object: object, }) } if errResponse.Code == "NoSuchBucket" { return ui.Size, probe.NewError(BucketDoesNotExist{ Bucket: bucket, }) } if errResponse.Code == "InvalidBucketName" { return ui.Size, probe.NewError(BucketInvalid{ Bucket: bucket, }) } if errResponse.Code == "NoSuchKey" { return ui.Size, probe.NewError(ObjectMissing{}) } return ui.Size, probe.NewError(e) } return ui.Size, nil } // PutPart - upload an object with custom metadata. (Same as Put) func (c *S3Client) PutPart(ctx context.Context, reader io.Reader, size int64, progress io.Reader, putOpts PutOptions) (int64, *probe.Error) { return c.Put(ctx, reader, size, progress, putOpts) } // Remove incomplete uploads. func (c *S3Client) removeIncompleteObjects(ctx context.Context, bucket string, objectsCh <-chan minio.ObjectInfo) <-chan minio.RemoveObjectResult { removeObjectErrorCh := make(chan minio.RemoveObjectResult) // Goroutine reads from objectsCh and sends error to removeObjectErrorCh if any. go func() { defer close(removeObjectErrorCh) for info := range objectsCh { if err := c.api.RemoveIncompleteUpload(ctx, bucket, info.Key); err != nil { removeObjectErrorCh <- minio.RemoveObjectResult{ObjectName: info.Key, Err: err} } } }() return removeObjectErrorCh } // AddUserAgent - add custom user agent. func (c *S3Client) AddUserAgent(app, version string) { c.api.SetAppInfo(app, version) } // RemoveResult returns the error or result of the removed objects. type RemoveResult struct { minio.RemoveObjectResult BucketName string Err *probe.Error } // Remove - remove object or bucket(s). func (c *S3Client) Remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass, isForceDel bool, contentCh <-chan *ClientContent) <-chan RemoveResult { resultCh := make(chan RemoveResult) prevBucket := "" // Maintain objectsCh, statusCh for each bucket var objectsCh chan minio.ObjectInfo var statusCh <-chan minio.RemoveObjectResult opts := minio.RemoveObjectsOptions{ GovernanceBypass: isBypass, } go func() { defer close(resultCh) if isForceDel { bucket, object := c.url2BucketAndObject() if e := c.api.RemoveObject(ctx, bucket, object, minio.RemoveObjectOptions{ ForceDelete: isForceDel, }); e != nil { resultCh <- RemoveResult{ Err: probe.NewError(e), } return } resultCh <- RemoveResult{ BucketName: bucket, RemoveObjectResult: minio.RemoveObjectResult{ ObjectName: object, }, } return } _, object := c.url2BucketAndObject() if isRemoveBucket && object != "" { resultCh <- RemoveResult{ Err: probe.NewError(errors.New( "use `mc rm` command to delete prefixes, or point your" + " bucket directly, `mc rb //`"), ), } return } for { select { case <-ctx.Done(): resultCh <- RemoveResult{ Err: probe.NewError(ctx.Err()), } return case content, ok := <-contentCh: if !ok { goto breakout } // Convert content.URL.Path to objectName for objectsCh. bucket, objectName := c.splitPath(content.URL.Path) objectVersionID := content.VersionID // We don't treat path when bucket is // empty, just skip it when it happens. if bucket == "" { continue } // Init objectsCh the first time. if prevBucket == "" { objectsCh = make(chan minio.ObjectInfo) prevBucket = bucket if isIncomplete { statusCh = c.removeIncompleteObjects(ctx, bucket, objectsCh) } else { statusCh = c.api.RemoveObjectsWithResult(ctx, bucket, objectsCh, opts) } } if prevBucket != bucket { if objectsCh != nil { close(objectsCh) } for removeStatus := range statusCh { if removeStatus.Err != nil { resultCh <- RemoveResult{ BucketName: bucket, Err: probe.NewError(removeStatus.Err), } } else { resultCh <- RemoveResult{ BucketName: bucket, RemoveObjectResult: removeStatus, } } } // Remove bucket if it qualifies. if isRemoveBucket && !isIncomplete { if e := c.api.RemoveBucket(ctx, prevBucket); e != nil { resultCh <- RemoveResult{ BucketName: bucket, Err: probe.NewError(e), } return } } // Re-init objectsCh for next bucket objectsCh = make(chan minio.ObjectInfo) if isIncomplete { statusCh = c.removeIncompleteObjects(ctx, bucket, objectsCh) } else { statusCh = c.api.RemoveObjectsWithResult(ctx, bucket, objectsCh, opts) } prevBucket = bucket } if objectName != "" { // Send object name once but continuously checks for pending // errors in parallel, the reason is that minio-go RemoveObjects // can block if there is any pending error not received yet. sent := false for !sent { select { case objectsCh <- minio.ObjectInfo{ Key: objectName, VersionID: objectVersionID, }: sent = true case removeStatus := <-statusCh: if removeStatus.Err != nil { resultCh <- RemoveResult{ BucketName: bucket, Err: probe.NewError(removeStatus.Err), } } else { resultCh <- RemoveResult{ BucketName: bucket, RemoveObjectResult: removeStatus, } } } } } else { // end of bucket - close the objectsCh if objectsCh != nil { close(objectsCh) } objectsCh = nil } } } breakout: // Close objectsCh at end of contentCh if objectsCh != nil { close(objectsCh) } // Write remove objects status to resultCh if statusCh != nil { for removeStatus := range statusCh { if removeStatus.Err != nil { removeStatus.Err = errors.New(strings.Replace( removeStatus.Err.Error(), "Object is WORM protected", "Object, '"+removeStatus.ObjectName+" (Version ID="+ removeStatus.ObjectVersionID+")' is WORM protected", 1)) // If the removeStatus error message is: // "Object is WORM protected and cannot be overwritten", // it is too generic. We have the object's name and vid. // Adding the object's name and version id into the error msg resultCh <- RemoveResult{ Err: probe.NewError(removeStatus.Err), } } else { resultCh <- RemoveResult{ BucketName: prevBucket, RemoveObjectResult: removeStatus, } } } } // Remove last bucket if it qualifies. if isRemoveBucket && prevBucket != "" && !isIncomplete { if e := c.api.RemoveBucket(ctx, prevBucket); e != nil { resultCh <- RemoveResult{ BucketName: prevBucket, Err: probe.NewError(e), } return } } }() return resultCh } // MakeBucket - make a new bucket. func (c *S3Client) MakeBucket(ctx context.Context, region string, ignoreExisting, withLock bool) *probe.Error { bucket, object := c.url2BucketAndObject() if bucket == "" { return probe.NewError(BucketNameEmpty{}) } if object != "" { if !strings.HasSuffix(object, string(c.targetURL.Separator)) { object += string(c.targetURL.Separator) } var retried bool for { _, e := c.api.PutObject(ctx, bucket, object, bytes.NewReader([]byte("")), 0, // Always send Content-MD5 to succeed with bucket with // locking enabled. There is no performance hit since // this is always an empty object minio.PutObjectOptions{SendContentMd5: true}, ) if e == nil { return nil } if retried { return probe.NewError(e) } switch minio.ToErrorResponse(e).Code { case "NoSuchBucket": opts := minio.MakeBucketOptions{Region: region, ObjectLocking: withLock} if e = c.api.MakeBucket(ctx, bucket, opts); e != nil { return probe.NewError(e) } retried = true continue } return probe.NewError(e) } } var e error opts := minio.MakeBucketOptions{Region: region, ObjectLocking: withLock} if e = c.api.MakeBucket(ctx, bucket, opts); e != nil { // Ignore bucket already existing error when ignoreExisting flag is enabled if ignoreExisting { switch minio.ToErrorResponse(e).Code { case "BucketAlreadyOwnedByYou": fallthrough case "BucketAlreadyExists": return nil } } return probe.NewError(e) } return nil } // RemoveBucket removes a bucket, forcibly if asked func (c *S3Client) RemoveBucket(ctx context.Context, forceRemove bool) *probe.Error { bucket, object := c.url2BucketAndObject() if bucket == "" { return probe.NewError(BucketNameEmpty{}) } if object != "" { return probe.NewError(BucketInvalid{c.joinPath(bucket, object)}) } opts := minio.RemoveBucketOptions{ForceDelete: forceRemove} if e := c.api.RemoveBucketWithOptions(ctx, bucket, opts); e != nil { return probe.NewError(e) } return nil } // GetAccessRules - get configured policies from the server func (c *S3Client) GetAccessRules(ctx context.Context) (map[string]string, *probe.Error) { bucket, object := c.url2BucketAndObject() if bucket == "" { return map[string]string{}, probe.NewError(BucketNameEmpty{}) } policies := map[string]string{} policyStr, e := c.api.GetBucketPolicy(ctx, bucket) if e != nil { return nil, probe.NewError(e) } if policyStr == "" { return policies, nil } var p policy.BucketAccessPolicy if e = json.Unmarshal([]byte(policyStr), &p); e != nil { return nil, probe.NewError(e) } policyRules := policy.GetPolicies(p.Statements, bucket, object) // Hide policy data structure at this level for k, v := range policyRules { policies[k] = string(v) } return policies, nil } // GetAccess get access policy permissions. func (c *S3Client) GetAccess(ctx context.Context) (string, string, *probe.Error) { bucket, object := c.url2BucketAndObject() if bucket == "" { return "", "", probe.NewError(BucketNameEmpty{}) } policyStr, e := c.api.GetBucketPolicy(ctx, bucket) if e != nil { return "", "", probe.NewError(e) } if policyStr == "" { return string(policy.BucketPolicyNone), policyStr, nil } var p policy.BucketAccessPolicy if e = json.Unmarshal([]byte(policyStr), &p); e != nil { return "", "", probe.NewError(e) } pType := string(policy.GetPolicy(p.Statements, bucket, object)) if pType == string(policy.BucketPolicyNone) && policyStr != "" { pType = "custom" } return pType, policyStr, nil } // SetAccess set access policy permissions. func (c *S3Client) SetAccess(ctx context.Context, bucketPolicy string, isJSON bool) *probe.Error { bucket, object := c.url2BucketAndObject() if bucket == "" { return probe.NewError(BucketNameEmpty{}) } if isJSON { if e := c.api.SetBucketPolicy(ctx, bucket, bucketPolicy); e != nil { return probe.NewError(e) } return nil } policyStr, e := c.api.GetBucketPolicy(ctx, bucket) if e != nil { return probe.NewError(e) } p := policy.BucketAccessPolicy{Version: "2012-10-17"} if policyStr != "" { if e = json.Unmarshal([]byte(policyStr), &p); e != nil { return probe.NewError(e) } } p.Statements = policy.SetPolicy(p.Statements, policy.BucketPolicy(bucketPolicy), bucket, object) if len(p.Statements) == 0 { if e = c.api.SetBucketPolicy(ctx, bucket, ""); e != nil { return probe.NewError(e) } return nil } policyB, e := json.Marshal(p) if e != nil { return probe.NewError(e) } if e = c.api.SetBucketPolicy(ctx, bucket, string(policyB)); e != nil { return probe.NewError(e) } return nil } // listObjectWrapper - select ObjectList mode depending on arguments func (c *S3Client) listObjectWrapper(ctx context.Context, bucket, object string, isRecursive bool, timeRef time.Time, withVersions, withDeleteMarkers, metadata bool, maxKeys int, zip bool) <-chan minio.ObjectInfo { if !timeRef.IsZero() || withVersions { return c.listVersions(ctx, bucket, object, ListOptions{Recursive: isRecursive, TimeRef: timeRef, WithOlderVersions: withVersions, WithDeleteMarkers: withDeleteMarkers}) } if isGoogle(c.targetURL.Host) { // Google Cloud S3 layer doesn't implement ListObjectsV2 implementation // https://github.com/minio/mc/issues/3073 return c.api.ListObjects(ctx, bucket, minio.ListObjectsOptions{Prefix: object, Recursive: isRecursive, UseV1: true, MaxKeys: maxKeys}) } opts := minio.ListObjectsOptions{Prefix: object, Recursive: isRecursive, WithMetadata: metadata, MaxKeys: maxKeys} if zip { // If prefix ends with .zip, add a slash. if strings.HasSuffix(object, ".zip") { opts.Prefix = object + "/" } opts.Set("x-minio-extract", "true") } return c.api.ListObjects(ctx, bucket, opts) } func (c *S3Client) statIncompleteUpload(ctx context.Context, bucket, object string) (*ClientContent, *probe.Error) { nonRecursive := false objectMetadata := &ClientContent{} // Prefix to pass to minio-go listing in order to fetch a given object/directory prefix := strings.TrimRight(object, string(c.targetURL.Separator)) for objectMultipartInfo := range c.api.ListIncompleteUploads(ctx, bucket, prefix, nonRecursive) { if objectMultipartInfo.Err != nil { return nil, probe.NewError(objectMultipartInfo.Err) } if objectMultipartInfo.Key == object { objectMetadata.BucketName = bucket objectMetadata.URL = c.targetURL.Clone() objectMetadata.Time = objectMultipartInfo.Initiated objectMetadata.Size = objectMultipartInfo.Size objectMetadata.Type = os.FileMode(0o664) objectMetadata.Metadata = map[string]string{} return objectMetadata, nil } if strings.HasSuffix(objectMultipartInfo.Key, string(c.targetURL.Separator)) { objectMetadata.BucketName = bucket objectMetadata.URL = c.targetURL.Clone() objectMetadata.Type = os.ModeDir objectMetadata.Metadata = map[string]string{} return objectMetadata, nil } } return nil, probe.NewError(ObjectMissing{}) } // Stat - send a 'HEAD' on a bucket or object to fetch its metadata. It also returns // a DIR type content if a prefix does exist in the server. func (c *S3Client) Stat(ctx context.Context, opts StatOptions) (*ClientContent, *probe.Error) { c.Lock() defer c.Unlock() bucket, path := c.url2BucketAndObject() // Bucket name cannot be empty, stat on URL has no meaning. if bucket == "" { url := c.targetURL.Clone() url.Path = string(c.targetURL.Separator) return &ClientContent{ URL: url, Size: 0, Type: os.ModeDir, BucketName: bucket, }, nil } if path == "" { content, err := c.bucketStat(ctx, BucketStatOptions{ bucket: bucket, ignoreBucketExists: opts.ignoreBucketExists, }) if err != nil { return nil, err.Trace(bucket) } return content, nil } // If the request is for incomplete upload stat, handle it here. if opts.incomplete { return c.statIncompleteUpload(ctx, bucket, path) } // The following code tries to calculate if a given prefix/object does really exist // using minio-go listing API. The following inputs are supported: // - /path/to/existing/object // - /path/to/existing_directory // - /path/to/existing_directory/ // - /path/to/directory_marker // - /path/to/directory_marker/ // Start with a HEAD request first to return object metadata information. // If the object is not found, continue to look for a directory marker or a prefix if !strings.HasSuffix(path, string(c.targetURL.Separator)) && opts.timeRef.IsZero() { o := minio.StatObjectOptions{ServerSideEncryption: opts.sse, VersionID: opts.versionID} if opts.isZip { o.Set("x-minio-extract", "true") } o.Set("x-amz-checksum-mode", "ENABLED") ctnt, err := c.getObjectStat(ctx, bucket, path, o) if err == nil { return ctnt, nil } // Ignore object missing error but return for other errors if !errors.As(err.ToGoError(), &ObjectMissing{}) && !errors.As(err.ToGoError(), &ObjectIsDeleteMarker{}) { return nil, err } // when versionID is specified we do not have to perform List() operation // when headOnly is specified we do not have to perform List() operation if (opts.versionID != "" || opts.headOnly) && errors.As(err.ToGoError(), &ObjectMissing{}) || errors.As(err.ToGoError(), &ObjectIsDeleteMarker{}) { return nil, probe.NewError(ObjectMissing{opts.timeRef}) } // The object is not found, look for a directory marker or a prefix path += string(c.targetURL.Separator) } nonRecursive := false maxKeys := 1 for objectStat := range c.listObjectWrapper(ctx, bucket, path, nonRecursive, opts.timeRef, opts.includeVersions, opts.includeVersions, false, maxKeys, opts.isZip) { if objectStat.Err != nil { return nil, probe.NewError(objectStat.Err) } // In case of a directory marker if path == objectStat.Key { return c.objectInfo2ClientContent(bucket, objectStat), nil } if strings.HasPrefix(objectStat.Key, path) { // An object inside the prefix is found, then the prefix exists. return c.prefixInfo2ClientContent(bucket, path), nil } } return nil, probe.NewError(ObjectMissing{opts.timeRef}) } // getObjectStat returns the metadata of an object from a HEAD call. func (c *S3Client) getObjectStat(ctx context.Context, bucket, object string, opts minio.StatObjectOptions) (*ClientContent, *probe.Error) { objectStat, e := c.api.StatObject(ctx, bucket, object, opts) objectMetadata := c.objectInfo2ClientContent(bucket, objectStat) if e != nil { errResponse := minio.ToErrorResponse(e) if errResponse.Code == "AccessDenied" { return nil, probe.NewError(PathInsufficientPermission{Path: c.targetURL.String()}) } if errResponse.Code == "NoSuchBucket" { return nil, probe.NewError(BucketDoesNotExist{ Bucket: bucket, }) } if errResponse.Code == "InvalidBucketName" { return nil, probe.NewError(BucketInvalid{ Bucket: bucket, }) } if errResponse.Code == "NoSuchKey" { if objectMetadata.IsDeleteMarker { return nil, probe.NewError(ObjectIsDeleteMarker{}) } return nil, probe.NewError(ObjectMissing{}) } return nil, probe.NewError(e) } // HEAD with a version ID will not return version in the response headers if objectMetadata.VersionID == "" { objectMetadata.VersionID = opts.VersionID } return objectMetadata, nil } func isAmazon(host string) bool { return s3utils.IsAmazonEndpoint(url.URL{Host: host}) } func isAmazonChina(host string) bool { amazonS3ChinaHost := regexp.MustCompile(`^s3\.(cn.*?)\.amazonaws\.com\.cn$`) parts := amazonS3ChinaHost.FindStringSubmatch(host) return len(parts) > 1 } func isAmazonAccelerated(host string) bool { return host == "s3-accelerate.amazonaws.com" } func isGoogle(host string) bool { return s3utils.IsGoogleEndpoint(url.URL{Host: host}) } // Figure out if the URL is of 'virtual host' style. // Use lookup from config to see if dns/path style look // up should be used. If it is set to "auto", use virtual // style for supported hosts such as Amazon S3 and Google // Cloud Storage. Otherwise, default to path style func isVirtualHostStyle(host string, lookup minio.BucketLookupType) bool { if lookup == minio.BucketLookupDNS { return true } if lookup == minio.BucketLookupPath { return false } return isAmazon(host) && !isAmazonChina(host) || isGoogle(host) || isAmazonAccelerated(host) } func url2BucketAndObject(u *ClientURL) (bucketName, objectName string) { tokens := splitStr(u.Path, string(u.Separator), 3) return tokens[1], tokens[2] } // url2BucketAndObject gives bucketName and objectName from URL path. func (c *S3Client) url2BucketAndObject() (bucketName, objectName string) { return url2BucketAndObject(c.targetURL) } // splitPath split path into bucket and object. func (c *S3Client) splitPath(path string) (bucketName, objectName string) { path = strings.TrimPrefix(path, string(c.targetURL.Separator)) // Handle path if its virtual style. if c.virtualStyle { hostIndex := strings.Index(c.targetURL.Host, "s3") if hostIndex == -1 { hostIndex = strings.Index(c.targetURL.Host, "s3-accelerate") } if hostIndex == -1 { hostIndex = strings.Index(c.targetURL.Host, "storage.googleapis") } if hostIndex > 0 { bucketName = c.targetURL.Host[:hostIndex-1] objectName = path return bucketName, objectName } } tokens := splitStr(path, string(c.targetURL.Separator), 2) return tokens[0], tokens[1] } /// Bucket API operations. func (c *S3Client) listVersions(ctx context.Context, b, o string, opts ListOptions) chan minio.ObjectInfo { objectInfoCh := make(chan minio.ObjectInfo) go func() { defer close(objectInfoCh) c.listVersionsRoutine(ctx, b, o, opts, objectInfoCh) }() return objectInfoCh } func (c *S3Client) listVersionsRoutine(ctx context.Context, b, o string, opts ListOptions, objectInfoCh chan minio.ObjectInfo) { var buckets []string if b == "" { bucketsInfo, err := c.api.ListBuckets(ctx) if err != nil { select { case <-ctx.Done(): case objectInfoCh <- minio.ObjectInfo{ Err: err, }: } return } for _, b := range bucketsInfo { buckets = append(buckets, b.Name) } } else { buckets = append(buckets, b) } for _, b := range buckets { var skipKey string for objectVersion := range c.api.ListObjects(ctx, b, minio.ListObjectsOptions{ Prefix: o, Recursive: opts.Recursive, WithVersions: true, WithMetadata: opts.WithMetadata, }) { if objectVersion.Err != nil { select { case <-ctx.Done(): return case objectInfoCh <- objectVersion: } continue } if !opts.WithOlderVersions && skipKey == objectVersion.Key { // Skip current version if not asked to list all versions // and we already listed the current object key name continue } if opts.TimeRef.IsZero() || objectVersion.LastModified.Before(opts.TimeRef) { skipKey = objectVersion.Key // Skip if this is a delete marker and we are not asked to list it if !opts.WithDeleteMarkers && objectVersion.IsDeleteMarker { continue } select { case <-ctx.Done(): return case objectInfoCh <- objectVersion: } } } } } // ListBuckets - list buckets func (c *S3Client) ListBuckets(ctx context.Context) ([]*ClientContent, *probe.Error) { buckets, err := c.api.ListBuckets(ctx) if err != nil { return nil, probe.NewError(err) } bucketsList := make([]*ClientContent, 0, len(buckets)) for _, b := range buckets { bucketsList = append(bucketsList, c.bucketInfo2ClientContent(b)) } return bucketsList, nil } // List - list at delimited path, if not recursive. func (c *S3Client) List(ctx context.Context, opts ListOptions) <-chan *ClientContent { c.Lock() defer c.Unlock() contentCh := make(chan *ClientContent) go func() { defer close(contentCh) if !opts.TimeRef.IsZero() || opts.WithOlderVersions { c.versionedList(ctx, contentCh, opts) } else { c.unversionedList(ctx, contentCh, opts) } }() return contentCh } // versionedList returns objects versions if the S3 backend supports versioning, // it falls back to the regular listing if not. func (c *S3Client) versionedList(ctx context.Context, contentCh chan *ClientContent, opts ListOptions) { b, o := c.url2BucketAndObject() switch { case b == "" && o == "": buckets, err := c.api.ListBuckets(ctx) if err != nil { select { case <-ctx.Done(): case contentCh <- &ClientContent{ Err: probe.NewError(err), }: } return } if opts.Recursive { sortBucketsNameWithSlash(buckets) } for _, bucket := range buckets { if opts.ShowDir != DirLast { select { case <-ctx.Done(): return case contentCh <- c.bucketInfo2ClientContent(bucket): } } for objectVersion := range c.listVersions(ctx, bucket.Name, "", opts) { if objectVersion.Err != nil { if minio.ToErrorResponse(objectVersion.Err).Code == "NotImplemented" { goto noVersioning } select { case <-ctx.Done(): return case contentCh <- &ClientContent{ Err: probe.NewError(objectVersion.Err), }: } continue } select { case <-ctx.Done(): return case contentCh <- c.objectInfo2ClientContent(bucket.Name, objectVersion): } } if opts.ShowDir == DirLast { select { case <-ctx.Done(): return case contentCh <- c.bucketInfo2ClientContent(bucket): } } } return default: for objectVersion := range c.listVersions(ctx, b, o, opts) { if objectVersion.Err != nil { if minio.ToErrorResponse(objectVersion.Err).Code == "NotImplemented" { goto noVersioning } select { case <-ctx.Done(): return case contentCh <- &ClientContent{ Err: probe.NewError(objectVersion.Err), }: } continue } select { case <-ctx.Done(): return case contentCh <- c.objectInfo2ClientContent(b, objectVersion): } } return } noVersioning: c.unversionedList(ctx, contentCh, opts) } // unversionedList is the non versioned S3 listing func (c *S3Client) unversionedList(ctx context.Context, contentCh chan *ClientContent, opts ListOptions) { if opts.Incomplete { if opts.Recursive { c.listIncompleteRecursiveInRoutine(ctx, contentCh, opts) } else { c.listIncompleteInRoutine(ctx, contentCh) } } else { if opts.Recursive { c.listRecursiveInRoutine(ctx, contentCh, opts) } else { c.listInRoutine(ctx, contentCh, opts) } } } func (c *S3Client) listIncompleteInRoutine(ctx context.Context, contentCh chan *ClientContent) { // get bucket and object from URL. b, o := c.url2BucketAndObject() switch { case b == "" && o == "": buckets, err := c.api.ListBuckets(ctx) if err != nil { select { case <-ctx.Done(): case contentCh <- &ClientContent{ Err: probe.NewError(err), }: } return } isRecursive := false for _, bucket := range buckets { for object := range c.api.ListIncompleteUploads(ctx, bucket.Name, o, isRecursive) { if object.Err != nil { select { case <-ctx.Done(): return case contentCh <- &ClientContent{ Err: probe.NewError(object.Err), }: } return } content := &ClientContent{} url := c.targetURL.Clone() // Join bucket with - incoming object key. url.Path = c.buildAbsPath(bucket.Name, object.Key) switch { case strings.HasSuffix(object.Key, string(c.targetURL.Separator)): // We need to keep the trailing Separator, do not use filepath.Join(). content.URL = url content.Time = time.Now() content.Type = os.ModeDir default: content.URL = url content.Size = object.Size content.Time = object.Initiated content.Type = os.ModeTemporary } select { case <-ctx.Done(): return case contentCh <- content: } } } default: isRecursive := false for object := range c.api.ListIncompleteUploads(ctx, b, o, isRecursive) { if object.Err != nil { select { case <-ctx.Done(): case contentCh <- &ClientContent{ Err: probe.NewError(object.Err), }: } return } content := &ClientContent{} url := c.targetURL.Clone() // Join bucket with - incoming object key. url.Path = c.buildAbsPath(b, object.Key) switch { case strings.HasSuffix(object.Key, string(c.targetURL.Separator)): // We need to keep the trailing Separator, do not use filepath.Join(). content.URL = url content.Time = time.Now() content.Type = os.ModeDir default: content.URL = url content.Size = object.Size content.Time = object.Initiated content.Type = os.ModeTemporary } select { case <-ctx.Done(): return case contentCh <- content: } } } } func (c *S3Client) listIncompleteRecursiveInRoutine(ctx context.Context, contentCh chan *ClientContent, opts ListOptions) { // get bucket and object from URL. b, o := c.url2BucketAndObject() switch { case b == "" && o == "": buckets, err := c.api.ListBuckets(ctx) if err != nil { select { case <-ctx.Done(): case contentCh <- &ClientContent{ Err: probe.NewError(err), }: } return } sortBucketsNameWithSlash(buckets) isRecursive := true for _, bucket := range buckets { if opts.ShowDir != DirLast { select { case <-ctx.Done(): return case contentCh <- c.bucketInfo2ClientContent(bucket): } } for object := range c.api.ListIncompleteUploads(ctx, bucket.Name, o, isRecursive) { if object.Err != nil { select { case <-ctx.Done(): return case contentCh <- &ClientContent{ Err: probe.NewError(object.Err), }: } return } url := c.targetURL.Clone() url.Path = c.buildAbsPath(bucket.Name, object.Key) content := &ClientContent{} content.URL = url content.Size = object.Size content.Time = object.Initiated content.Type = os.ModeTemporary select { case <-ctx.Done(): return case contentCh <- content: } } if opts.ShowDir == DirLast { select { case <-ctx.Done(): return case contentCh <- c.bucketInfo2ClientContent(bucket): } } } default: isRecursive := true for object := range c.api.ListIncompleteUploads(ctx, b, o, isRecursive) { if object.Err != nil { select { case <-ctx.Done(): return case contentCh <- &ClientContent{ Err: probe.NewError(object.Err), }: } return } url := c.targetURL.Clone() // Join bucket and incoming object key. url.Path = c.buildAbsPath(b, object.Key) content := &ClientContent{} content.URL = url content.Size = object.Size content.Time = object.Initiated content.Type = os.ModeTemporary select { case <-ctx.Done(): return case contentCh <- content: } } } } // Join bucket and object name, keep the leading slash for directory markers func (c *S3Client) joinPath(bucket string, objects ...string) string { p := bucket for _, o := range objects { p += string(c.targetURL.Separator) + o } return p } // Build new absolute URL path by joining path segments with URL path separator. func (c *S3Client) buildAbsPath(bucket string, objects ...string) string { return string(c.targetURL.Separator) + c.joinPath(bucket, objects...) } // Convert objectInfo to ClientContent func (c *S3Client) bucketInfo2ClientContent(bucket minio.BucketInfo) *ClientContent { content := &ClientContent{} url := c.targetURL.Clone() url.Path = c.buildAbsPath(bucket.Name) content.URL = url content.BucketName = bucket.Name content.Size = 0 content.Time = bucket.CreationDate content.Type = os.ModeDir return content } // Convert objectInfo to ClientContent func (c *S3Client) prefixInfo2ClientContent(bucket, prefix string) *ClientContent { // Join bucket and incoming object key. if bucket == "" { panic("should never happen, bucket cannot be empty") } content := &ClientContent{} url := c.targetURL.Clone() url.Path = c.buildAbsPath(bucket, prefix) content.URL = url content.BucketName = bucket content.Type = os.ModeDir content.Time = time.Now() return content } // Convert objectInfo to ClientContent func (c *S3Client) objectInfo2ClientContent(bucket string, entry minio.ObjectInfo) *ClientContent { content := &ClientContent{} url := c.targetURL.Clone() // Join bucket and incoming object key. if bucket == "" { panic("should never happen, bucket cannot be empty") } url.Path = c.buildAbsPath(bucket, entry.Key) content.URL = url content.BucketName = bucket content.Size = entry.Size content.ETag = entry.ETag content.Time = entry.LastModified content.Expires = entry.Expires content.Expiration = entry.Expiration content.ExpirationRuleID = entry.ExpirationRuleID content.VersionID = entry.VersionID content.StorageClass = entry.StorageClass content.IsDeleteMarker = entry.IsDeleteMarker content.IsLatest = entry.IsLatest content.Restore = entry.Restore content.Metadata = map[string]string{} content.UserMetadata = map[string]string{} content.Tags = entry.UserTags content.ReplicationStatus = entry.ReplicationStatus for k, v := range entry.UserMetadata { content.UserMetadata[k] = v } for k := range entry.Metadata { content.Metadata[k] = entry.Metadata.Get(k) } attr, _ := parseAttribute(content.UserMetadata) if len(attr) > 0 { _, mtime, _ := parseAtimeMtime(attr) if !mtime.IsZero() { content.Time = mtime } } attr, _ = parseAttribute(content.Metadata) if len(attr) > 0 { _, mtime, _ := parseAtimeMtime(attr) if !mtime.IsZero() { content.Time = mtime } } if strings.HasSuffix(entry.Key, string(c.targetURL.Separator)) { content.Type = os.ModeDir if content.Time.IsZero() { content.Time = time.Now() } } else { content.Type = os.FileMode(0o664) } setChecksum := func(k, v string) { if v != "" { content.Checksum = map[string]string{k: v} } } setChecksum("CRC32", entry.ChecksumCRC32) setChecksum("CRC32C", entry.ChecksumCRC32C) setChecksum("SHA1", entry.ChecksumSHA1) setChecksum("SHA256", entry.ChecksumSHA256) setChecksum("CRC64NVME", entry.ChecksumCRC64NVME) return content } // Returns bucket stat info of current bucket. func (c *S3Client) bucketStat(ctx context.Context, opts BucketStatOptions) (*ClientContent, *probe.Error) { if !opts.ignoreBucketExists { exists, e := c.api.BucketExists(ctx, opts.bucket) if e != nil { return nil, probe.NewError(e) } if !exists { return nil, probe.NewError(BucketDoesNotExist{Bucket: opts.bucket}) } } return &ClientContent{ URL: c.targetURL.Clone(), BucketName: opts.bucket, Time: time.Unix(0, 0), Type: os.ModeDir, }, nil } func (c *S3Client) listInRoutine(ctx context.Context, contentCh chan *ClientContent, opts ListOptions) { // get bucket and object from URL. b, o := c.url2BucketAndObject() if opts.ListZip && (b == "" || o == "") { contentCh <- &ClientContent{ Err: probe.NewError(errors.New("listing zip files must provide bucket and object")), } return } switch { case b == "" && o == "": buckets, e := c.api.ListBuckets(ctx) if e != nil { contentCh <- &ClientContent{ Err: probe.NewError(e), } return } for _, bucket := range buckets { contentCh <- c.bucketInfo2ClientContent(bucket) } case b != "" && !strings.HasSuffix(c.targetURL.Path, string(c.targetURL.Separator)) && o == "": content, err := c.bucketStat(ctx, BucketStatOptions{bucket: b}) if err != nil { contentCh <- &ClientContent{Err: err.Trace(b)} return } contentCh <- content default: isRecursive := false for object := range c.listObjectWrapper(ctx, b, o, isRecursive, time.Time{}, false, false, opts.WithMetadata, -1, opts.ListZip) { if object.Err != nil { contentCh <- &ClientContent{ Err: probe.NewError(object.Err), } return } contentCh <- c.objectInfo2ClientContent(b, object) } } } // S3 offers a range of storage classes designed for // different use cases, following list captures these. const ( // General purpose. // s3StorageClassStandard = "STANDARD" // Infrequent access. // s3StorageClassInfrequent = "STANDARD_IA" // Reduced redundancy access. // s3StorageClassRedundancy = "REDUCED_REDUNDANCY" // Archive access. s3StorageClassGlacier = "GLACIER" ) // Sorting buckets name with an additional '/' to make sure that a // site-wide listing returns sorted output. This is crucial for // correct diff/mirror calculation. func sortBucketsNameWithSlash(bucketsInfo []minio.BucketInfo) { sort.Slice(bucketsInfo, func(i, j int) bool { return bucketsInfo[i].Name+"/" < bucketsInfo[j].Name+"/" }) } func (c *S3Client) listRecursiveInRoutine(ctx context.Context, contentCh chan *ClientContent, opts ListOptions) { // get bucket and object from URL. b, o := c.url2BucketAndObject() switch { case b == "" && o == "": buckets, err := c.api.ListBuckets(ctx) if err != nil { contentCh <- &ClientContent{ Err: probe.NewError(err), } return } sortBucketsNameWithSlash(buckets) for _, bucket := range buckets { if opts.ShowDir == DirFirst { contentCh <- c.bucketInfo2ClientContent(bucket) } isRecursive := true for object := range c.listObjectWrapper(ctx, bucket.Name, o, isRecursive, time.Time{}, false, false, opts.WithMetadata, -1, opts.ListZip) { if object.Err != nil { contentCh <- &ClientContent{ Err: probe.NewError(object.Err), } return } contentCh <- c.objectInfo2ClientContent(bucket.Name, object) } if opts.ShowDir == DirLast { contentCh <- c.bucketInfo2ClientContent(bucket) } } default: isRecursive := true for object := range c.listObjectWrapper(ctx, b, o, isRecursive, time.Time{}, false, false, opts.WithMetadata, -1, opts.ListZip) { if object.Err != nil { contentCh <- &ClientContent{ Err: probe.NewError(object.Err), } return } contentCh <- c.objectInfo2ClientContent(b, object) } } } // ShareDownload - get a usable presigned object url to share. func (c *S3Client) ShareDownload(ctx context.Context, versionID string, expires time.Duration) (string, *probe.Error) { bucket, object := c.url2BucketAndObject() // No additional request parameters are set for the time being. reqParams := make(url.Values) if versionID != "" { reqParams.Set("versionId", versionID) } presignedURL, e := c.api.PresignedGetObject(ctx, bucket, object, expires, reqParams) if e != nil { return "", probe.NewError(e) } return presignedURL.String(), nil } // ShareUpload - get data for presigned post http form upload. func (c *S3Client) ShareUpload(ctx context.Context, isRecursive bool, expires time.Duration, contentType string) (string, map[string]string, *probe.Error) { bucket, object := c.url2BucketAndObject() p := minio.NewPostPolicy() if e := p.SetExpires(UTCNow().Add(expires)); e != nil { return "", nil, probe.NewError(e) } if strings.TrimSpace(contentType) != "" || contentType != "" { // No need to verify for error here, since we have stripped out spaces. p.SetContentType(contentType) } if e := p.SetBucket(bucket); e != nil { return "", nil, probe.NewError(e) } if isRecursive { if e := p.SetKeyStartsWith(object); e != nil { return "", nil, probe.NewError(e) } } else { if e := p.SetKey(object); e != nil { return "", nil, probe.NewError(e) } } u, m, e := c.api.PresignedPostPolicy(ctx, p) if e != nil { return "", nil, probe.NewError(e) } return u.String(), m, nil } // SetObjectLockConfig - Set object lock configurataion of bucket. func (c *S3Client) SetObjectLockConfig(ctx context.Context, mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit) *probe.Error { bucket, object := c.url2BucketAndObject() if bucket == "" || object != "" { return errInvalidArgument().Trace(bucket, object) } // FIXME: This is too ugly, fix minio-go vuint := (uint)(validity) if mode != "" && vuint > 0 && unit != "" { e := c.api.SetBucketObjectLockConfig(ctx, bucket, &mode, &vuint, &unit) if e != nil { return probe.NewError(e).Trace(c.GetURL().String()) } return nil } if mode == "" && vuint == 0 && unit == "" { e := c.api.SetBucketObjectLockConfig(ctx, bucket, nil, nil, nil) if e != nil { return probe.NewError(e).Trace(c.GetURL().String()) } return nil } return errInvalidArgument().Trace(c.GetURL().String()) } // PutObjectRetention - Set object retention for a given object. func (c *S3Client) PutObjectRetention(ctx context.Context, versionID string, mode minio.RetentionMode, retainUntilDate time.Time, bypassGovernance bool) *probe.Error { bucket, object := c.url2BucketAndObject() var ( modePtr *minio.RetentionMode retainUntilDatePtr *time.Time ) if mode != "" && retainUntilDate.IsZero() { return errInvalidArgument().Trace(c.GetURL().String()) } if mode != "" { modePtr = &mode retainUntilDatePtr = &retainUntilDate } opts := minio.PutObjectRetentionOptions{ VersionID: versionID, RetainUntilDate: retainUntilDatePtr, Mode: modePtr, GovernanceBypass: bypassGovernance, } e := c.api.PutObjectRetention(ctx, bucket, object, opts) if e != nil { return probe.NewError(e).Trace(c.GetURL().String()) } return nil } // GetObjectRetention - Get object retention for a given object. func (c *S3Client) GetObjectRetention(ctx context.Context, versionID string) (minio.RetentionMode, time.Time, *probe.Error) { bucket, object := c.url2BucketAndObject() if object == "" { return "", time.Time{}, probe.NewError(ObjectNameEmpty{}).Trace(c.GetURL().String()) } modePtr, untilPtr, e := c.api.GetObjectRetention(ctx, bucket, object, versionID) if e != nil { return "", time.Time{}, probe.NewError(e).Trace(c.GetURL().String()) } var ( mode minio.RetentionMode until time.Time ) if modePtr != nil { mode = *modePtr } if untilPtr != nil { until = *untilPtr } return mode, until, nil } // PutObjectLegalHold - Set object legal hold for a given object. func (c *S3Client) PutObjectLegalHold(ctx context.Context, versionID string, lhold minio.LegalHoldStatus) *probe.Error { bucket, object := c.url2BucketAndObject() if lhold.IsValid() { opts := minio.PutObjectLegalHoldOptions{ Status: &lhold, VersionID: versionID, } e := c.api.PutObjectLegalHold(ctx, bucket, object, opts) if e != nil { return probe.NewError(e).Trace(c.GetURL().String()) } return nil } return errInvalidArgument().Trace(c.GetURL().String()) } // GetObjectLegalHold - Get object legal hold for a given object. func (c *S3Client) GetObjectLegalHold(ctx context.Context, versionID string) (minio.LegalHoldStatus, *probe.Error) { var lhold minio.LegalHoldStatus bucket, object := c.url2BucketAndObject() opts := minio.GetObjectLegalHoldOptions{ VersionID: versionID, } lhPtr, e := c.api.GetObjectLegalHold(ctx, bucket, object, opts) if e != nil { errResp := minio.ToErrorResponse(e) if errResp.Code != "NoSuchObjectLockConfiguration" { return "", probe.NewError(e).Trace(c.GetURL().String()) } return "", nil } // lhPtr can be nil if there is no legalhold status set if lhPtr != nil { lhold = *lhPtr } return lhold, nil } // GetObjectLockConfig - Get object lock configuration of bucket. func (c *S3Client) GetObjectLockConfig(ctx context.Context) (string, minio.RetentionMode, uint64, minio.ValidityUnit, *probe.Error) { bucket, object := c.url2BucketAndObject() if bucket == "" || object != "" { return "", "", 0, "", errInvalidArgument().Trace(bucket, object) } status, mode, validity, unit, e := c.api.GetObjectLockConfig(ctx, bucket) if e != nil { return "", "", 0, "", probe.NewError(e).Trace(c.GetURL().String()) } if mode != nil && validity != nil && unit != nil { // FIXME: this is too ugly, fix minio-go vuint64 := uint64(*validity) return status, *mode, vuint64, *unit, nil } return status, "", 0, "", nil } // GetTags - Get tags of bucket or object. func (c *S3Client) GetTags(ctx context.Context, versionID string) (map[string]string, *probe.Error) { bucketName, objectName := c.url2BucketAndObject() if bucketName == "" { return nil, probe.NewError(BucketNameEmpty{}) } if objectName == "" { if versionID != "" { return nil, probe.NewError(errors.New("getting bucket tags does not support versioning parameters")) } tags, err := c.api.GetBucketTagging(ctx, bucketName) if err != nil { return nil, probe.NewError(err) } return tags.ToMap(), nil } tags, err := c.api.GetObjectTagging(ctx, bucketName, objectName, minio.GetObjectTaggingOptions{VersionID: versionID}) if err != nil { return nil, probe.NewError(err) } return tags.ToMap(), nil } // SetTags - Set tags of bucket or object. func (c *S3Client) SetTags(ctx context.Context, versionID, tagString string) *probe.Error { bucketName, objectName := c.url2BucketAndObject() if bucketName == "" { return probe.NewError(BucketNameEmpty{}) } tags, err := tags.Parse(tagString, objectName != "") if err != nil { return probe.NewError(err) } if objectName == "" { if versionID != "" { return probe.NewError(errors.New("setting bucket tags does not support versioning parameters")) } err = c.api.SetBucketTagging(ctx, bucketName, tags) } else { err = c.api.PutObjectTagging(ctx, bucketName, objectName, tags, minio.PutObjectTaggingOptions{VersionID: versionID}) } if err != nil { return probe.NewError(err) } return nil } // DeleteTags - Delete tags of bucket or object func (c *S3Client) DeleteTags(ctx context.Context, versionID string) *probe.Error { bucketName, objectName := c.url2BucketAndObject() if bucketName == "" { return probe.NewError(BucketNameEmpty{}) } var err error if objectName == "" { if versionID != "" { return probe.NewError(errors.New("setting bucket tags does not support versioning parameters")) } err = c.api.RemoveBucketTagging(ctx, bucketName) } else { err = c.api.RemoveObjectTagging(ctx, bucketName, objectName, minio.RemoveObjectTaggingOptions{VersionID: versionID}) } if err != nil { return probe.NewError(err) } return nil } // GetLifecycle - Get current lifecycle configuration. func (c *S3Client) GetLifecycle(ctx context.Context) (*lifecycle.Configuration, time.Time, *probe.Error) { bucket, _ := c.url2BucketAndObject() if bucket == "" { return nil, time.Time{}, probe.NewError(BucketNameEmpty{}) } config, updatedAt, e := c.api.GetBucketLifecycleWithInfo(ctx, bucket) if e != nil { return nil, time.Time{}, probe.NewError(e) } return config, updatedAt, nil } // SetLifecycle - Set lifecycle configuration on a bucket func (c *S3Client) SetLifecycle(ctx context.Context, config *lifecycle.Configuration) *probe.Error { bucket, _ := c.url2BucketAndObject() if bucket == "" { return probe.NewError(BucketNameEmpty{}) } if e := c.api.SetBucketLifecycle(ctx, bucket, config); e != nil { return probe.NewError(e) } return nil } // GetVersion - gets bucket version info. func (c *S3Client) GetVersion(ctx context.Context) (config minio.BucketVersioningConfiguration, err *probe.Error) { bucket, _ := c.url2BucketAndObject() if bucket == "" { return config, probe.NewError(BucketNameEmpty{}) } var e error config, e = c.api.GetBucketVersioning(ctx, bucket) if e != nil { return config, probe.NewError(e) } return config, nil } // SetVersion - Set version configuration on a bucket func (c *S3Client) SetVersion(ctx context.Context, status string, prefixes []string, excludeFolders bool) *probe.Error { bucket, _ := c.url2BucketAndObject() if bucket == "" { return probe.NewError(BucketNameEmpty{}) } var err error switch status { case "enable": if len(prefixes) > 0 || excludeFolders { vc := minio.BucketVersioningConfiguration{ Status: minio.Enabled, ExcludeFolders: excludeFolders, } if len(prefixes) > 0 { eprefixes := make([]minio.ExcludedPrefix, 0, len(prefixes)) for _, prefix := range prefixes { eprefixes = append(eprefixes, minio.ExcludedPrefix{Prefix: prefix}) } vc.ExcludedPrefixes = eprefixes } err = c.api.SetBucketVersioning(ctx, bucket, vc) } else { err = c.api.EnableVersioning(ctx, bucket) } case "suspend": err = c.api.SuspendVersioning(ctx, bucket) default: return probe.NewError(fmt.Errorf("Invalid versioning status")) } return probe.NewError(err) } // GetReplication - gets replication configuration for a given bucket. func (c *S3Client) GetReplication(ctx context.Context) (replication.Config, *probe.Error) { bucket, _ := c.url2BucketAndObject() if bucket == "" { return replication.Config{}, probe.NewError(BucketNameEmpty{}) } replicationCfg, e := c.api.GetBucketReplication(ctx, bucket) if e != nil { return replication.Config{}, probe.NewError(e) } return replicationCfg, nil } // RemoveReplication - removes replication configuration for a given bucket. func (c *S3Client) RemoveReplication(ctx context.Context) *probe.Error { bucket, _ := c.url2BucketAndObject() if bucket == "" { return probe.NewError(BucketNameEmpty{}) } e := c.api.RemoveBucketReplication(ctx, bucket) return probe.NewError(e) } // SetReplication sets replication configuration for a given bucket. func (c *S3Client) SetReplication(ctx context.Context, cfg *replication.Config, opts replication.Options) *probe.Error { bucket, objectPrefix := c.url2BucketAndObject() if bucket == "" { return probe.NewError(BucketNameEmpty{}) } opts.Prefix = objectPrefix switch opts.Op { case replication.AddOption: if e := cfg.AddRule(opts); e != nil { return probe.NewError(e) } case replication.SetOption: if e := cfg.EditRule(opts); e != nil { return probe.NewError(e) } case replication.RemoveOption: if e := cfg.RemoveRule(opts); e != nil { return probe.NewError(e) } case replication.ImportOption: default: return probe.NewError(fmt.Errorf("Invalid replication option")) } if e := c.api.SetBucketReplication(ctx, bucket, *cfg); e != nil { return probe.NewError(e) } return nil } // GetReplicationMetrics - Get replication metrics for a given bucket. func (c *S3Client) GetReplicationMetrics(ctx context.Context) (replication.MetricsV2, *probe.Error) { bucket, _ := c.url2BucketAndObject() if bucket == "" { return replication.MetricsV2{}, probe.NewError(BucketNameEmpty{}) } metrics, e := c.api.GetBucketReplicationMetricsV2(ctx, bucket) if e != nil { return replication.MetricsV2{}, probe.NewError(e) } return metrics, nil } // ResetReplication - kicks off replication again on previously replicated objects if existing object // replication is enabled in the replication config.Optional to provide a timestamp func (c *S3Client) ResetReplication(ctx context.Context, before time.Duration, tgtArn string) (rinfo replication.ResyncTargetsInfo, err *probe.Error) { bucket, _ := c.url2BucketAndObject() if bucket == "" { return rinfo, probe.NewError(BucketNameEmpty{}) } rinfo, e := c.api.ResetBucketReplicationOnTarget(ctx, bucket, before, tgtArn) if e != nil { return rinfo, probe.NewError(e) } return rinfo, nil } // ReplicationResyncStatus - gets status of replication resync for this target arn func (c *S3Client) ReplicationResyncStatus(ctx context.Context, arn string) (rinfo replication.ResyncTargetsInfo, err *probe.Error) { bucket, _ := c.url2BucketAndObject() if bucket == "" { return rinfo, probe.NewError(BucketNameEmpty{}) } rinfo, e := c.api.GetBucketReplicationResyncStatus(ctx, bucket, arn) if e != nil { return rinfo, probe.NewError(e) } return rinfo, nil } // GetEncryption - gets bucket encryption info. func (c *S3Client) GetEncryption(ctx context.Context) (algorithm, keyID string, err *probe.Error) { bucket, _ := c.url2BucketAndObject() if bucket == "" { return "", "", probe.NewError(BucketNameEmpty{}) } config, e := c.api.GetBucketEncryption(ctx, bucket) if e != nil { return "", "", probe.NewError(e) } for _, rule := range config.Rules { algorithm = rule.Apply.SSEAlgorithm if rule.Apply.KmsMasterKeyID != "" { keyID = rule.Apply.KmsMasterKeyID break } } return algorithm, keyID, nil } // SetEncryption - Set encryption configuration on a bucket func (c *S3Client) SetEncryption(ctx context.Context, encType, kmsKeyID string) *probe.Error { bucket, _ := c.url2BucketAndObject() if bucket == "" { return probe.NewError(BucketNameEmpty{}) } var config *sse.Configuration switch strings.ToLower(encType) { case "sse-kms": config = sse.NewConfigurationSSEKMS(kmsKeyID) case "sse-s3": config = sse.NewConfigurationSSES3() default: return probe.NewError(fmt.Errorf("Invalid encryption algorithm %s", encType)) } if err := c.api.SetBucketEncryption(ctx, bucket, config); err != nil { return probe.NewError(err) } return nil } // DeleteEncryption - removes encryption configuration on a bucket func (c *S3Client) DeleteEncryption(ctx context.Context) *probe.Error { bucket, _ := c.url2BucketAndObject() if bucket == "" { return probe.NewError(BucketNameEmpty{}) } if err := c.api.RemoveBucketEncryption(ctx, bucket); err != nil { return probe.NewError(err) } return nil } // GetBucketInfo gets info about a bucket func (c *S3Client) GetBucketInfo(ctx context.Context) (BucketInfo, *probe.Error) { var b BucketInfo bucket, object := c.url2BucketAndObject() if bucket == "" { return b, probe.NewError(BucketNameEmpty{}) } if object != "" { return b, probe.NewError(InvalidArgument{}) } content, err := c.bucketStat(ctx, BucketStatOptions{bucket: bucket}) if err != nil { return b, err.Trace(bucket) } b.Key = bucket b.URL = content.URL b.Size = content.Size b.Type = content.Type b.Date = content.Time if vcfg, err := c.GetVersion(ctx); err == nil { b.Versioning.Status = vcfg.Status b.Versioning.MFADelete = vcfg.MFADelete } if enabled, mode, validity, unit, err := c.api.GetObjectLockConfig(ctx, bucket); err == nil { if mode != nil { b.Locking.Mode = *mode } b.Locking.Enabled = enabled if validity != nil && unit != nil { vuint64 := uint64(*validity) b.Locking.Validity = fmt.Sprintf("%d%s", vuint64, unit) } } if rcfg, err := c.GetReplication(ctx); err == nil { if !rcfg.Empty() { b.Replication.Enabled = true } } if algo, keyID, err := c.GetEncryption(ctx); err == nil { b.Encryption.Algorithm = algo b.Encryption.KeyID = keyID } if pType, policyStr, err := c.GetAccess(ctx); err == nil { b.Policy.Type = pType b.Policy.Text = policyStr } location, e := c.api.GetBucketLocation(ctx, bucket) if e != nil { return b, probe.NewError(e) } b.Location = location if tags, err := c.GetTags(ctx, ""); err == nil { b.Tagging = tags } if lfc, _, err := c.GetLifecycle(ctx); err == nil { b.ILM.Config = lfc } if nfc, err := c.api.GetBucketNotification(ctx, bucket); err == nil { b.Notification.Config = nfc } return b, nil } // Restore gets a copy of an archived object func (c *S3Client) Restore(ctx context.Context, versionID string, days int) *probe.Error { bucket, object := c.url2BucketAndObject() if bucket == "" { return probe.NewError(BucketNameEmpty{}) } if object == "" { return probe.NewError(ObjectNameEmpty{}) } req := minio.RestoreRequest{} req.SetDays(days) req.SetGlacierJobParameters(minio.GlacierJobParameters{Tier: minio.TierExpedited}) if err := c.api.RestoreObject(ctx, bucket, object, versionID, req); err != nil { return probe.NewError(err) } return nil } // GetPart gets an object in a given number of parts func (c *S3Client) GetPart(ctx context.Context, part int) (io.ReadCloser, *probe.Error) { bucket, object := c.url2BucketAndObject() if bucket == "" { return nil, probe.NewError(BucketNameEmpty{}) } if object == "" { return nil, probe.NewError(ObjectNameEmpty{}) } getOO := minio.GetObjectOptions{} if part > 0 { getOO.PartNumber = part } reader, e := c.api.GetObject(ctx, bucket, object, getOO) if e != nil { return nil, probe.NewError(e) } return reader, nil } // SetBucketCors - Set bucket cors configuration. func (c *S3Client) SetBucketCors(ctx context.Context, corsXML []byte) *probe.Error { bucketName, _ := c.url2BucketAndObject() if bucketName == "" { return probe.NewError(BucketNameEmpty{}) } corsCfg, err := cors.ParseBucketCorsConfig(bytes.NewReader(corsXML)) if err != nil { return probe.NewError(err) } err = c.api.SetBucketCors(ctx, bucketName, corsCfg) if err != nil { return probe.NewError(err) } return nil } // GetBucketCors - Get bucket cors configuration. func (c *S3Client) GetBucketCors(ctx context.Context) (*cors.Config, *probe.Error) { bucketName, _ := c.url2BucketAndObject() if bucketName == "" { return nil, probe.NewError(BucketNameEmpty{}) } corsCfg, err := c.api.GetBucketCors(ctx, bucketName) if err != nil { return nil, probe.NewError(err) } return corsCfg, nil } // DeleteBucketCors - Delete bucket cors configuration. func (c *S3Client) DeleteBucketCors(ctx context.Context) *probe.Error { bucketName, _ := c.url2BucketAndObject() if bucketName == "" { return probe.NewError(BucketNameEmpty{}) } err := c.api.SetBucketCors(ctx, bucketName, nil) if err != nil { return probe.NewError(err) } return nil } minio-client-0.0~20250403/cmd/client-s3_test.go000066400000000000000000000274701477450377600207460ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd // bucketHandler is an http.Handler that verifies bucket responses and validates incoming requests import ( "bytes" "context" "io" "net/http" "net/http/httptest" "strconv" "github.com/minio/minio-go/v7" checkv1 "gopkg.in/check.v1" ) type bucketHandler struct { resource string } func (h bucketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": // Handler for incoming getBucketLocation request. if _, ok := r.URL.Query()["location"]; ok { response := []byte("") w.Header().Set("Content-Length", strconv.Itoa(len(response))) w.Write(response) return } switch r.URL.Path { case "/": // Handler for incoming ListBuckets request. response := []byte("bucket2015-05-20T23:05:09.230Zminiominio") w.Header().Set("Content-Length", strconv.Itoa(len(response))) w.Write(response) case "/bucket/": // Handler for incoming ListObjects request. response := []byte("259d04a13802ae09c7e41be50ccc6baaobject2015-05-21T18:24:21.097Z22061miniominioSTANDARDfalse1000testbucket") w.Header().Set("Content-Length", strconv.Itoa(len(response))) w.Write(response) } case "PUT": switch r.URL.Path { case h.resource: w.WriteHeader(http.StatusOK) default: w.WriteHeader(http.StatusBadRequest) } case "HEAD": switch r.URL.Path { case h.resource: w.WriteHeader(http.StatusOK) default: w.WriteHeader(http.StatusForbidden) } } } // objectHandler is an http.Handler that verifies object responses and validates incoming requests type objectHandler struct { resource string data []byte } func (h objectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if ak := r.Header.Get("Authorization"); len(ak) == 0 { w.WriteHeader(http.StatusForbidden) return } switch r.Method { case http.MethodPut: // Handler for PUT object request. length, e := strconv.Atoi(r.Header.Get("Content-Length")) if e != nil { w.WriteHeader(http.StatusBadRequest) return } var buffer bytes.Buffer if _, e = io.CopyN(&buffer, r.Body, int64(length)); e != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("ETag", "9af2f8218b150c351ad802c6f3d66abe") w.WriteHeader(http.StatusOK) case http.MethodHead: // Handler for Stat object request. if r.URL.Path != h.resource { w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Content-Length", strconv.Itoa(len(h.data))) w.Header().Set("Last-Modified", UTCNow().Format(http.TimeFormat)) w.Header().Set("ETag", "9af2f8218b150c351ad802c6f3d66abe") w.WriteHeader(http.StatusOK) case http.MethodPost: // Handler for multipart upload request. if _, ok := r.URL.Query()["uploads"]; ok { if r.URL.Path == h.resource { response := []byte("bucketobjectEXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-") w.Header().Set("Content-Length", strconv.Itoa(len(response))) w.Write(response) return } } if _, ok := r.URL.Query()["uploadId"]; ok { if r.URL.Path == h.resource { response := []byte("http://bucket.s3.amazonaws.com/objectbucketobject\"3858f62230ac3c915f300c664312c11f-9\"") w.Header().Set("Content-Length", strconv.Itoa(len(response))) w.Write(response) return } } if r.URL.Path != h.resource { w.WriteHeader(http.StatusNotFound) return } case http.MethodGet: // Handler for get bucket location request. if _, ok := r.URL.Query()["location"]; ok { response := []byte("") w.Header().Set("Content-Length", strconv.Itoa(len(response))) w.Write(response) return } // Handler for list multipart upload request. if _, ok := r.URL.Query()["uploads"]; ok { if r.URL.Path == "/bucket/" { response := []byte("bucket1000false") w.Header().Set("Content-Length", strconv.Itoa(len(response))) w.Write(response) return } } if r.URL.Path != h.resource { w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Content-Length", strconv.Itoa(len(h.data))) w.Header().Set("Last-Modified", UTCNow().Format(http.TimeFormat)) w.Header().Set("ETag", "9af2f8218b150c351ad802c6f3d66abe") w.WriteHeader(http.StatusOK) io.Copy(w, bytes.NewReader(h.data)) } } type stsHandler struct { endpoint string jwt []byte } func (h stsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := ParseForm(r); err != nil { w.WriteHeader(http.StatusInternalServerError) return } switch r.Method { case http.MethodPost: token := r.Form.Get("WebIdentityToken") if token == string(h.jwt) { response := []byte("7NL5BR739GUQ0ZOD4JNBA2mxZSxPnHNhSduedUHczsXZpVSSssOLpDruUmTV0001-01-01T00:00:00ZeyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiI3Tkw1QlI3MzlHVVEwWk9ENEpOQiIsImV4cCI6MTY5OTYwMzMwNiwicGFyZW50IjoibWluaW8iLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaVlXUnRhVzQ2S2lKZGZTeDdJa1ZtWm1WamRDSTZJa0ZzYkc5M0lpd2lRV04wYVc5dUlqcGJJbXR0Y3pvcUlsMTlMSHNpUldabVpXTjBJam9pUVd4c2IzY2lMQ0pCWTNScGIyNGlPbHNpY3pNNktpSmRMQ0pTWlhOdmRYSmpaU0k2V3lKaGNtNDZZWGR6T25Nek9qbzZLaUpkZlYxOSJ9.uuE_x7PO8QoPfUk9KzUELoAqxihIknZAvJLl5aYJjwpSjJYFTPLp6EvuyJX2hc18s9HzeiJ-vU0dPzsy50dXmg") w.Header().Set("Content-Length", strconv.Itoa(len(response))) w.Header().Set("Content-Type", "application/xml") w.Header().Set("Server", "MinIO") w.Write(response) return } response := []byte("AccessDeniedAccess denied: Invalid Token") w.Header().Set("Content-Length", strconv.Itoa(len(response))) w.Header().Set("Content-Type", "application/xml") w.Write(response) return } } // Test bucket operations. func (s *TestSuite) TestBucketOperations(c *checkv1.C) { bucket := bucketHandler{ resource: "/bucket/", } server := httptest.NewServer(bucket) defer server.Close() conf := new(Config) conf.HostURL = server.URL + bucket.resource conf.AccessKey = "WLGDGYAQYIGI833EV05A" conf.SecretKey = "BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF" conf.Signature = "S3v4" s3c, err := S3New(conf) c.Assert(err, checkv1.IsNil) err = s3c.MakeBucket(context.Background(), "us-east-1", true, false) c.Assert(err, checkv1.IsNil) conf.HostURL = server.URL + string(s3c.GetURL().Separator) s3c, err = S3New(conf) c.Assert(err, checkv1.IsNil) for content := range s3c.List(globalContext, ListOptions{ShowDir: DirNone}) { c.Assert(content.Err, checkv1.IsNil) c.Assert(content.Type.IsDir(), checkv1.Equals, true) } conf.HostURL = server.URL + "/bucket" s3c, err = S3New(conf) c.Assert(err, checkv1.IsNil) for content := range s3c.List(globalContext, ListOptions{ShowDir: DirNone}) { c.Assert(content.Err, checkv1.IsNil) c.Assert(content.Type.IsDir(), checkv1.Equals, true) } conf.HostURL = server.URL + "/bucket/" s3c, err = S3New(conf) c.Assert(err, checkv1.IsNil) for content := range s3c.List(globalContext, ListOptions{ShowDir: DirNone}) { c.Assert(content.Err, checkv1.IsNil) c.Assert(content.Type.IsRegular(), checkv1.Equals, true) } } // Test all object operations. func (s *TestSuite) TestObjectOperations(c *checkv1.C) { object := objectHandler{ resource: "/bucket/object", data: []byte("Hello, World"), } server := httptest.NewServer(object) defer server.Close() conf := new(Config) conf.HostURL = server.URL + object.resource conf.AccessKey = "WLGDGYAQYIGI833EV05A" conf.SecretKey = "BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF" conf.Signature = "S3v4" s3c, err := S3New(conf) c.Assert(err, checkv1.IsNil) var reader io.Reader reader = bytes.NewReader(object.data) n, err := s3c.Put(context.Background(), reader, int64(len(object.data)), nil, PutOptions{ metadata: map[string]string{ "Content-Type": "application/octet-stream", }, }) c.Assert(err, checkv1.IsNil) c.Assert(n, checkv1.Equals, int64(len(object.data))) reader, _, err = s3c.Get(context.Background(), GetOptions{}) c.Assert(err, checkv1.IsNil) var buffer bytes.Buffer { _, err := io.Copy(&buffer, reader) c.Assert(err, checkv1.IsNil) c.Assert(buffer.Bytes(), checkv1.DeepEquals, object.data) } } var testSelectCompressionTypeCases = []struct { opts SelectObjectOpts object string compressionType minio.SelectCompressionType }{ {SelectObjectOpts{CompressionType: minio.SelectCompressionNONE}, "a.gzip", minio.SelectCompressionNONE}, {SelectObjectOpts{CompressionType: minio.SelectCompressionBZIP}, "a.gz", minio.SelectCompressionBZIP}, {SelectObjectOpts{}, "t.parquet", minio.SelectCompressionNONE}, {SelectObjectOpts{}, "x.csv.gz", minio.SelectCompressionGZIP}, {SelectObjectOpts{}, "x.json.bz2", minio.SelectCompressionBZIP}, {SelectObjectOpts{}, "b.gz", minio.SelectCompressionGZIP}, {SelectObjectOpts{}, "k.bz2", minio.SelectCompressionBZIP}, {SelectObjectOpts{}, "a.csv", minio.SelectCompressionNONE}, {SelectObjectOpts{}, "a.json", minio.SelectCompressionNONE}, } // TestSelectCompressionType - tests compression type returned // by method func (s *TestSuite) TestSelectCompressionType(c *checkv1.C) { for _, test := range testSelectCompressionTypeCases { cType := selectCompressionType(test.opts, test.object) c.Assert(cType, checkv1.DeepEquals, test.compressionType) } } minio-client-0.0~20250403/cmd/client-sts_test.go000066400000000000000000000124751477450377600212310ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "io" "log" "net/http/httptest" "os" "testing" ) func TestSTSS3Operation(t *testing.T) { sts := stsHandler{ endpoint: "/", jwt: []byte("eyJhbGciOiJSUzI1NiIsImtpZCI6Inc0dFNjMEc5Tk0wQWhGaWJYaWIzbkpRZkRKeDc1dURRTUVpOTNvTHJ0OWcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxMTIyNjg0LCJpYXQiOjE2OTk1ODY2ODQsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJtaW5pby10ZW5hbnQtMSIsInBvZCI6eyJuYW1lIjoic2V0dXAtYnVja2V0LXJ4aHhiIiwidWlkIjoiNmNhMzhjMmItYTdkMC00M2Y0LWE0NjMtZjdlNjU4MGUyZDdiIn0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJtYy1qb2Itc2EiLCJ1aWQiOiI3OTc4NzJjZC1kMjkwLTRlM2EtYjYyMC00ZGFkYzZhNzUyMTYifSwid2FybmFmdGVyIjoxNjk5NTkwMjkxfSwibmJmIjoxNjk5NTg2Njg0LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6bWluaW8tdGVuYW50LTE6bWMtam9iLXNhIn0.fBJckmoQFyJ9bUgKZv6jzBESd9ccX_HFPPBZ17Gz_CsQ5wXrMqnvoMs1mcv6QKWsDsvSnWnw_tcW0cjvVkXb2mKmioKLzqV4ihGbiWzwk2e1xDohn8fizdQkf64bXpncjGdEGv8oi9A4300jfLMfg53POriMyEAQMeIDKPOI9qx913xjGni2w2H49mjLfnFnRaj9osvy17425dNIrMC6GDFq3rcq6Z_cdDmL18Jwsjy1xDsAhUzmOclr-VI3AeSnuD4fbf6jhbKE14qVUjLmIBf__B5NhESiaFNwxFYjonZyi357Nx93CD1wai28tNRSODx7BiPHLxk8SyzY0CP0sQ"), } tmpfile, errFs := os.CreateTemp("", "jwt") if errFs != nil { log.Fatal(errFs) } defer os.Remove(tmpfile.Name()) // clean up if _, errFs := tmpfile.Write(sts.jwt); errFs != nil { log.Fatal(errFs) } if errFs := tmpfile.Close(); errFs != nil { log.Fatal(errFs) } stsServer := httptest.NewServer(sts) defer stsServer.Close() t.Setenv("MC_STS_ENDPOINT_test", stsServer.URL+sts.endpoint) t.Setenv("MC_WEB_IDENTITY_TOKEN_FILE_test", tmpfile.Name()) object := objectHandler{ resource: "/bucket/object", data: []byte("Hello, World"), } server := httptest.NewServer(object) defer server.Close() conf := new(Config) conf.Alias = "test" conf.HostURL = server.URL + object.resource s3c, err := S3New(conf) if err != nil { t.Fatal(err) } var reader io.Reader = bytes.NewReader(object.data) n, err := s3c.Put(context.Background(), reader, int64(len(object.data)), nil, PutOptions{ metadata: map[string]string{ "Content-Type": "application/octet-stream", }, }) if err != nil { t.Fatal(err) } if n != int64(len(object.data)) { t.Fatalf("expected %d, got %d", n, len(object.data)) } } func TestAdminSTSOperation(t *testing.T) { sts := stsHandler{ endpoint: "/", jwt: []byte("eyJhbGciOiJSUzI1NiIsImtpZCI6Inc0dFNjMEc5Tk0wQWhGaWJYaWIzbkpRZkRKeDc1dURRTUVpOTNvTHJ0OWcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxMTg3NzEwLCJpYXQiOjE2OTk2NTE3MTAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJtaW5pby10ZW5hbnQtMSIsInBvZCI6eyJuYW1lIjoic2V0dXAtYnVja2V0LXQ4eGdjIiwidWlkIjoiNjZhYjlkZWItNzkwMC00YTFlLTgzMDgtMTkwODIwZmQ3NDY5In0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJtYy1qb2Itc2EiLCJ1aWQiOiI3OTc4NzJjZC1kMjkwLTRlM2EtYjYyMC00ZGFkYzZhNzUyMTYifSwid2FybmFmdGVyIjoxNjk5NjU1MzE3fSwibmJmIjoxNjk5NjUxNzEwLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6bWluaW8tdGVuYW50LTE6bWMtam9iLXNhIn0.rY7dpAh8GBTViH9Ges7tRhgyihdFWEN0DwXchelmZg58VOI526S-YfbCqrxksTs8Iu0fp1rmk1cUj7FGDh3AOv2RphHjoWci1802zKkHgH0iOEbKMp3jHXwfyHda8CyrSCPycGzClueCf1ae91wd_0lgK9lOR1qqY1HuDeXqSEAUIGrfh1VcP2n95Zc07EY-Uh3XjJE4drtgusACEK5n3P3WtN9s0m0GomEGQzF5ZJczxLGpHBKMQ5VDhMksVKdBAsx9xHgSx84aUhKQViYilAL-8PRj-RZA9s_IpEymAh5R37dKzAO8Fqq0nG7fVbH_ifzw3xhHiG92BhHldBDqEQ"), } tmpfile, errFs := os.CreateTemp("", "jwt") if errFs != nil { log.Fatal(errFs) } defer os.Remove(tmpfile.Name()) // clean up if _, errFs := tmpfile.Write(sts.jwt); errFs != nil { log.Fatal(errFs) } if errFs := tmpfile.Close(); errFs != nil { log.Fatal(errFs) } stsServer := httptest.NewServer(sts) defer stsServer.Close() t.Setenv("MC_STS_ENDPOINT_test", stsServer.URL+sts.endpoint) t.Setenv("MC_WEB_IDENTITY_TOKEN_FILE_test", tmpfile.Name()) handler := adminPolicyHandler{ endpoint: "/minio/admin/v3/add-canned-policy?name=", name: "test", policy: []byte(` { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:*" ], "Resource": [ "arn:aws:s3:::test-bucket", "arn:aws:s3:::test-bucket/*" ] } ] }`), } server := httptest.NewServer(handler) defer server.Close() conf := new(Config) conf.Alias = "test" conf.Debug = true conf.Insecure = true conf.HostURL = server.URL + handler.endpoint + handler.name s3c, err := s3AdminNew(conf) if err != nil { t.Fatal(err) } e := s3c.AddCannedPolicy(context.Background(), handler.name, handler.policy) if e != nil { t.Fatal(e) } } minio-client-0.0~20250403/cmd/client-url.go000066400000000000000000000214121477450377600201520ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "path/filepath" "regexp" "runtime" "strings" "time" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/mimedb" ) // ClientURL url client url structure type ClientURL struct { Type ClientURLType Scheme string Host string Path string SchemeSeparator string Separator rune } // ClientURLType - enum of different url types type ClientURLType int // url2StatOptions - convert url to stat options type url2StatOptions struct { urlStr, versionID string fileAttr bool encKeyDB map[string][]prefixSSEPair timeRef time.Time isZip bool headOnly bool ignoreBucketExistsCheck bool } // enum types const ( objectStorage = iota // MinIO and S3 compatible cloud storage fileSystem // POSIX compatible file systems ) // Maybe rawurl is of the form scheme:path. (Scheme must be [a-zA-Z][a-zA-Z0-9+-.]*) // If so, return scheme, path; else return "", rawurl. func getScheme(rawurl string) (scheme, path string) { urlSplits := strings.Split(rawurl, "://") if len(urlSplits) == 2 { scheme, uri := urlSplits[0], "//"+urlSplits[1] // ignore numbers in scheme validScheme := regexp.MustCompile("^[a-zA-Z]+$") if uri != "" { if validScheme.MatchString(scheme) { return scheme, uri } } } return "", rawurl } // Assuming s is of the form [s delimiter s]. // If so, return s, [delimiter]s or return s, s if cutdelimiter == true // If no delimiter found return s, "". func splitSpecial(s, delimiter string, cutdelimiter bool) (string, string) { i := strings.Index(s, delimiter) if i < 0 { // if delimiter not found return as is. return s, "" } // if delimiter should be removed, remove it. if cutdelimiter { return s[0:i], s[i+len(delimiter):] } // return split strings with delimiter return s[0:i], s[i:] } // getHost - extract host from authority string, we do not support ftp style username@ yet. func getHost(authority string) (host string) { i := strings.LastIndex(authority, "@") if i >= 0 { // TODO support, username@password style userinfo, useful for ftp support. return } return authority } // newClientURL returns an abstracted URL for filesystems and object storage. func newClientURL(urlStr string) *ClientURL { scheme, rest := getScheme(urlStr) if strings.HasPrefix(rest, "//") { // if rest has '//' prefix, skip them var authority string authority, rest = splitSpecial(rest[2:], "/", false) if rest == "" { rest = "/" } host := getHost(authority) if host != "" && (scheme == "http" || scheme == "https") { return &ClientURL{ Scheme: scheme, Type: objectStorage, Host: host, Path: rest, SchemeSeparator: "://", Separator: '/', } } } return &ClientURL{ Type: fileSystem, Path: rest, Separator: filepath.Separator, } } // joinURLs join two input urls and returns a url func joinURLs(url1, url2 *ClientURL) *ClientURL { var url1Path, url2Path string url1Path = filepath.ToSlash(url1.Path) url2Path = filepath.ToSlash(url2.Path) if strings.HasSuffix(url1Path, "/") { url1.Path = url1Path + strings.TrimPrefix(url2Path, "/") } else { url1.Path = url1Path + "/" + strings.TrimPrefix(url2Path, "/") } return url1 } // Clone the url into a new object. func (u ClientURL) Clone() ClientURL { return ClientURL{ Type: u.Type, Scheme: u.Scheme, Host: u.Host, Path: u.Path, SchemeSeparator: u.SchemeSeparator, Separator: u.Separator, } } // String convert URL into its canonical form. func (u ClientURL) String() string { var buf bytes.Buffer // if fileSystem no translation needed, return as is. if u.Type == fileSystem { return u.Path } // if objectStorage convert from any non standard paths to a supported URL path style. if u.Type == objectStorage { buf.WriteString(u.Scheme) buf.WriteByte(':') buf.WriteString("//") if h := u.Host; h != "" { buf.WriteString(h) } switch runtime.GOOS { case "windows": if u.Path != "" && u.Path[0] != '\\' && u.Host != "" && u.Path[0] != '/' { buf.WriteByte('/') } buf.WriteString(strings.ReplaceAll(u.Path, "\\", "/")) default: if u.Path != "" && u.Path[0] != '/' && u.Host != "" { buf.WriteByte('/') } buf.WriteString(u.Path) } } return buf.String() } // urlJoinPath Join a path to existing URL. func urlJoinPath(url1, url2 string) string { u1 := newClientURL(url1) u2 := newClientURL(url2) return joinURLs(u1, u2).String() } // url2Stat returns stat info for URL - supports bucket, object and a prefixe with or without a trailing slash func url2Stat(ctx context.Context, opts url2StatOptions) (client Client, content *ClientContent, err *probe.Error) { client, err = newClient(opts.urlStr) if err != nil { return nil, nil, err.Trace(opts.urlStr) } alias, _ := url2Alias(opts.urlStr) sse := getSSE(opts.urlStr, opts.encKeyDB[alias]) content, err = client.Stat(ctx, StatOptions{ preserve: opts.fileAttr, sse: sse, timeRef: opts.timeRef, versionID: opts.versionID, isZip: opts.isZip, ignoreBucketExists: opts.ignoreBucketExistsCheck, headOnly: opts.headOnly, }) if err != nil { return nil, nil, err.Trace(opts.urlStr) } return client, content, nil } // firstURL2Stat returns the stat info of the first object having the specified prefix func firstURL2Stat(ctx context.Context, prefix string, timeRef time.Time, isZip bool) (client Client, content *ClientContent, err *probe.Error) { client, err = newClient(prefix) if err != nil { return nil, nil, err.Trace(prefix) } content = <-client.List(ctx, ListOptions{Recursive: true, TimeRef: timeRef, Count: 1, ListZip: isZip}) if content == nil { return nil, nil, probe.NewError(ObjectMissing{timeRef: timeRef}).Trace(prefix) } if content.Err != nil { return nil, nil, content.Err.Trace(prefix) } return client, content, nil } // url2Alias separates alias and path from the URL. Aliased URL is of // the form alias/path/to/blah. func url2Alias(aliasedURL string) (alias, path string) { // Save aliased url. urlStr := aliasedURL // Convert '/' on windows to filepath.Separator. urlStr = filepath.FromSlash(urlStr) if runtime.GOOS == "windows" { // Remove '/' prefix before alias if any to support '\\home' alias // style under Windows urlStr = strings.TrimPrefix(urlStr, string(filepath.Separator)) } // Remove everything after alias (i.e. after '/'). urlParts := strings.SplitN(urlStr, string(filepath.Separator), 2) if len(urlParts) == 2 { // Convert windows style path separator to Unix style. return urlParts[0], urlParts[1] } return urlParts[0], "" } // guessURLContentType - guess content-type of the URL. // on failure just return 'application/octet-stream'. func guessURLContentType(urlStr string) string { url := newClientURL(urlStr) contentType := mimedb.TypeByExtension(filepath.Ext(url.Path)) return contentType } // urlParts - split URL into parts. func urlParts(urlStr string) []string { // Convert '/' on windows to filepath.Separator. urlStr = filepath.FromSlash(urlStr) if runtime.GOOS == "windows" { // Remove '/' prefix before alias if any to support '\\home' alias // style under Windows urlStr = strings.TrimPrefix(urlStr, string(filepath.Separator)) } // Remove everything after alias (i.e. after '/'). return strings.Split(urlStr, string(filepath.Separator)) } // isURLPrefix - check if source and destination be subdirectories of each other func isURLPrefix(src string, dest string) bool { srcURLParts := urlParts(src) dstURLParts := urlParts(dest) minIndex := min(len(srcURLParts), len(dstURLParts)) isPrefix := true for i := 0; i < minIndex; i++ { // if one of the URLs ends with '/' and other does not if (i == minIndex-1) && (dstURLParts[i] == "" || srcURLParts[i] == "" || dstURLParts[i] == "*" || srcURLParts[i] == "*") { continue } if srcURLParts[i] != dstURLParts[i] { isPrefix = false break } } return isPrefix } minio-client-0.0~20250403/cmd/client-url_test.go000066400000000000000000000055201477450377600212130ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "testing" checkv1 "gopkg.in/check.v1" ) // TestURL - tests url parsing and fields. func (s *TestSuite) TestURL(c *checkv1.C) { urlStr := "foo?.go" url := newClientURL(urlStr) c.Assert(url.Path, checkv1.Equals, "foo?.go") urlStr = "https://s3.amazonaws.com/mybucket/foo?.go" url = newClientURL(urlStr) c.Assert(url.Scheme, checkv1.Equals, "https") c.Assert(url.Host, checkv1.Equals, "s3.amazonaws.com") c.Assert(url.Path, checkv1.Equals, "/mybucket/foo?.go") } // TestURLJoinPath - tests joining two different urls. func (s *TestSuite) TestURLJoinPath(c *checkv1.C) { // Join two URLs url1 := "http://s3.mycompany.io/dev" url2 := "http://s3.aws.amazon.com/mybucket/bin/zgrep" url := urlJoinPath(url1, url2) c.Assert(url, checkv1.Equals, "http://s3.mycompany.io/dev/mybucket/bin/zgrep") // Join URL and a path url1 = "http://s3.mycompany.io/dev" url2 = "mybucket/bin/zgrep" url = urlJoinPath(url1, url2) c.Assert(url, checkv1.Equals, "http://s3.mycompany.io/dev/mybucket/bin/zgrep") // Check if it strips URL2's tailing `/` url1 = "http://s3.mycompany.io/dev" url2 = "mybucket/bin/" url = urlJoinPath(url1, url2) c.Assert(url, checkv1.Equals, "http://s3.mycompany.io/dev/mybucket/bin/") } func Test_isURLPrefix(t *testing.T) { type args struct { src string dest string } tests := []struct { name string args args want bool }{ {"test1", args{"s3/test", "s3/test/test"}, true}, {"test2", args{"s3/test/", "s3/test/test"}, true}, {"test3", args{"s3/test/test", "s3/test/"}, true}, {"test4", args{"s3/test/test", "s3/test/test.123"}, false}, {"test5", args{"s3/test/", "s3/test/test/test/test"}, true}, {"test6", args{"s3/test/*", "s3/test/test/"}, true}, {"test7", args{"s3/test/*", "s3/test1/test/"}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := isURLPrefix(tt.args.src, tt.args.dest); got != tt.want { t.Errorf("isURLPrefix() = %v, want %v", got, tt.want) } if got := isURLPrefix(tt.args.dest, tt.args.src); got != tt.want { t.Errorf("isURLPrefix() = %v, want %v", got, tt.want) } }) } } minio-client-0.0~20250403/cmd/client.go000066400000000000000000000336261477450377600173640ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "crypto/tls" "fmt" "io" "math/rand" "net/http" "net/url" "os" "strings" "time" "github.com/klauspost/compress/gzhttp" "github.com/minio/mc/pkg/httptracer" "github.com/minio/mc/pkg/limiter" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/cors" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/encrypt" "github.com/minio/minio-go/v7/pkg/lifecycle" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/pkg/v3/env" ) // DirOpt - list directory option. type DirOpt int8 const ( // DirNone - do not include directories in the list. DirNone DirOpt = iota // DirFirst - include directories before objects in the list. DirFirst // DirLast - include directories after objects in the list. DirLast ) // GetOptions holds options of the GET operation type GetOptions struct { SSE encrypt.ServerSide VersionID string Zip bool RangeStart int64 PartNumber int Preserve bool } // PutOptions holds options for PUT operation type PutOptions struct { metadata map[string]string sse encrypt.ServerSide md5, disableMultipart bool isPreserve bool storageClass string multipartSize uint64 multipartThreads uint concurrentStream bool ifNotExists bool checksum minio.ChecksumType } // StatOptions holds options of the HEAD operation type StatOptions struct { incomplete bool preserve bool sse encrypt.ServerSide timeRef time.Time versionID string includeVersions bool isZip bool ignoreBucketExists bool headOnly bool } // BucketStatOptions - bucket stat. type BucketStatOptions struct { bucket string ignoreBucketExists bool } // ListOptions holds options for listing operation type ListOptions struct { Recursive bool Incomplete bool WithMetadata bool WithOlderVersions bool WithDeleteMarkers bool ListZip bool TimeRef time.Time ShowDir DirOpt Count int } // CopyOptions holds options for copying operation type CopyOptions struct { versionID string size int64 srcSSE, tgtSSE encrypt.ServerSide metadata map[string]string disableMultipart bool isPreserve bool storageClass string } // Client - client interface type Client interface { // Common operations Stat(ctx context.Context, opts StatOptions) (content *ClientContent, err *probe.Error) List(ctx context.Context, opts ListOptions) <-chan *ClientContent // Bucket operations MakeBucket(ctx context.Context, region string, ignoreExisting, withLock bool) *probe.Error RemoveBucket(ctx context.Context, forceRemove bool) *probe.Error ListBuckets(ctx context.Context) ([]*ClientContent, *probe.Error) // Object lock config SetObjectLockConfig(ctx context.Context, mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit) *probe.Error GetObjectLockConfig(ctx context.Context) (status string, mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit, perr *probe.Error) // Access policy operations. GetAccess(ctx context.Context) (access, policyJSON string, err *probe.Error) GetAccessRules(ctx context.Context) (policyRules map[string]string, err *probe.Error) SetAccess(ctx context.Context, access string, isJSON bool) *probe.Error // I/O operations Copy(ctx context.Context, source string, opts CopyOptions, progress io.Reader) *probe.Error // Runs select expression on object storage on specific files. Select(ctx context.Context, expression string, sse encrypt.ServerSide, opts SelectObjectOpts) (io.ReadCloser, *probe.Error) // I/O operations with metadata. Get(ctx context.Context, opts GetOptions) (reader io.ReadCloser, content *ClientContent, err *probe.Error) Put(ctx context.Context, reader io.Reader, size int64, progress io.Reader, opts PutOptions) (n int64, err *probe.Error) // Object Locking related API PutObjectRetention(ctx context.Context, versionID string, mode minio.RetentionMode, retainUntilDate time.Time, bypassGovernance bool) *probe.Error GetObjectRetention(ctx context.Context, versionID string) (minio.RetentionMode, time.Time, *probe.Error) PutObjectLegalHold(ctx context.Context, versionID string, hold minio.LegalHoldStatus) *probe.Error GetObjectLegalHold(ctx context.Context, versionID string) (minio.LegalHoldStatus, *probe.Error) // I/O operations with expiration ShareDownload(ctx context.Context, versionID string, expires time.Duration) (string, *probe.Error) ShareUpload(context.Context, bool, time.Duration, string) (string, map[string]string, *probe.Error) // Watch events Watch(ctx context.Context, options WatchOptions) (*WatchObject, *probe.Error) // Delete operations Remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass, isForceDel bool, contentCh <-chan *ClientContent) (errorCh <-chan RemoveResult) // GetURL returns back internal url GetURL() ClientURL AddUserAgent(app, version string) // Tagging operations GetTags(ctx context.Context, versionID string) (map[string]string, *probe.Error) SetTags(ctx context.Context, versionID, tags string) *probe.Error DeleteTags(ctx context.Context, versionID string) *probe.Error // Lifecycle operations GetLifecycle(ctx context.Context) (*lifecycle.Configuration, time.Time, *probe.Error) SetLifecycle(ctx context.Context, config *lifecycle.Configuration) *probe.Error // Versioning operations GetVersion(ctx context.Context) (minio.BucketVersioningConfiguration, *probe.Error) SetVersion(ctx context.Context, status string, prefixes []string, excludeFolders bool) *probe.Error // Replication operations GetReplication(ctx context.Context) (replication.Config, *probe.Error) SetReplication(ctx context.Context, cfg *replication.Config, opts replication.Options) *probe.Error RemoveReplication(ctx context.Context) *probe.Error GetReplicationMetrics(ctx context.Context) (replication.MetricsV2, *probe.Error) ResetReplication(ctx context.Context, before time.Duration, arn string) (replication.ResyncTargetsInfo, *probe.Error) ReplicationResyncStatus(ctx context.Context, arn string) (rinfo replication.ResyncTargetsInfo, err *probe.Error) // Encryption operations GetEncryption(ctx context.Context) (string, string, *probe.Error) SetEncryption(ctx context.Context, algorithm, kmsKeyID string) *probe.Error DeleteEncryption(ctx context.Context) *probe.Error // Bucket info operation GetBucketInfo(ctx context.Context) (BucketInfo, *probe.Error) // Restore an object Restore(ctx context.Context, versionID string, days int) *probe.Error // OD operations GetPart(ctx context.Context, part int) (io.ReadCloser, *probe.Error) PutPart(ctx context.Context, reader io.Reader, size int64, progress io.Reader, opts PutOptions) (n int64, err *probe.Error) // Cors operations GetBucketCors(ctx context.Context) (*cors.Config, *probe.Error) SetBucketCors(ctx context.Context, corsXML []byte) *probe.Error DeleteBucketCors(ctx context.Context) *probe.Error } // ClientContent - Content container for content metadata type ClientContent struct { URL ClientURL BucketName string // only valid and set for client-type objectStorage Time time.Time Size int64 Type os.FileMode StorageClass string Metadata map[string]string Tags map[string]string UserMetadata map[string]string Checksum map[string]string ETag string Expires time.Time Expiration time.Time ExpirationRuleID string RetentionEnabled bool RetentionMode string RetentionDuration string BypassGovernance bool LegalHoldEnabled bool LegalHold string VersionID string IsDeleteMarker bool IsLatest bool ReplicationStatus string Restore *minio.RestoreInfo Err *probe.Error } // Config - see http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html type Config struct { Alias string AccessKey string SecretKey string SessionToken string Signature string HostURL string AppName string AppVersion string Debug bool Insecure bool Lookup minio.BucketLookupType ConnReadDeadline time.Duration ConnWriteDeadline time.Duration UploadLimit int64 DownloadLimit int64 Transport http.RoundTripper } // getCredsChain returns an []credentials.Provider array for the config // and the STS configuration (if present) func (config *Config) getCredsChain() ([]credentials.Provider, *probe.Error) { var credsChain []credentials.Provider // if an STS endpoint is set, we will add that to the chain if stsEndpoint := env.Get("MC_STS_ENDPOINT_"+config.Alias, ""); stsEndpoint != "" { // set AWS_WEB_IDENTITY_TOKEN_FILE is MC_WEB_IDENTITY_TOKEN_FILE is set if val := env.Get("MC_WEB_IDENTITY_TOKEN_FILE_"+config.Alias, ""); val != "" { os.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", val) if val := env.Get("MC_ROLE_ARN_"+config.Alias, ""); val != "" { os.Setenv("AWS_ROLE_ARN", val) } if val := env.Get("MC_ROLE_SESSION_NAME_"+config.Alias, randString(32, rand.NewSource(time.Now().UnixNano()), "mc-session-name-")); val != "" { os.Setenv("AWS_ROLE_SESSION_NAME", val) } } stsEndpointURL, err := url.Parse(stsEndpoint) if err != nil { return nil, probe.NewError(fmt.Errorf("Error parsing sts endpoint: %w", err)) } credsSts := &credentials.IAM{ Client: &http.Client{ Transport: config.getTransport(), }, Endpoint: stsEndpointURL.String(), } credsChain = append(credsChain, credsSts) } signType := credentials.SignatureV4 if strings.EqualFold(config.Signature, "s3v2") { signType = credentials.SignatureV2 } // Credentials creds := &credentials.Static{ Value: credentials.Value{ AccessKeyID: config.AccessKey, SecretAccessKey: config.SecretKey, SessionToken: config.SessionToken, SignerType: signType, }, } credsChain = append(credsChain, creds) return credsChain, nil } // getTransport returns a corresponding *http.Transport for the *Config // set withS3v2 bool to true to add traceV2 tracer. func (config *Config) getTransport() http.RoundTripper { if config.Transport == nil { config.initTransport(true) } return config.Transport } func (config *Config) isTLS() bool { if stsEndpoint := env.Get("MC_STS_ENDPOINT_"+config.Alias, ""); stsEndpoint != "" { stsEndpointURL, err := url.Parse(stsEndpoint) if err != nil { return false } return isHostTLS(config) || stsEndpointURL.Scheme == "https" } return isHostTLS(config) } func (config *Config) initTransport(withS3v2 bool) { var transport http.RoundTripper useTLS := config.isTLS() if config.Transport != nil { transport = config.Transport } else { tr := &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: newCustomDialContext(config), MaxIdleConnsPerHost: 1024, WriteBufferSize: 32 << 10, // 32KiB moving up from 4KiB default ReadBufferSize: 32 << 10, // 32KiB moving up from 4KiB default IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 10 * time.Second, // Set this value so that the underlying transport round-tripper // doesn't try to auto decode the body of objects with // content-encoding set to `gzip`. // // Refer: // https://golang.org/src/net/http/transport.go?h=roundTrip#L1843 DisableCompression: true, } if useTLS { tr.DialTLSContext = newCustomDialTLSContext(&tls.Config{ RootCAs: globalRootCAs, MinVersion: tls.VersionTLS12, InsecureSkipVerify: config.Insecure, }) // Because we create a custom TLSClientConfig, we have to opt-in to HTTP/2. // See https://github.com/golang/go/issues/14275 // // TODO: Enable http2.0 when upstream issues related to HTTP/2 are fixed. // // if e = http2.ConfigureTransport(tr); e != nil { // return nil, probe.NewError(e) // } } if len(globalCustomHeader) > 0 { transport = &headerTransport{ RoundTripper: tr, customHeader: globalCustomHeader.Clone(), } } else { transport = tr } } transport = limiter.New(config.UploadLimit, config.DownloadLimit, transport) if config.Debug { if strings.EqualFold(config.Signature, "S3v4") { transport = httptracer.GetNewTraceTransport(newTraceV4(), transport) } else if strings.EqualFold(config.Signature, "S3v2") && withS3v2 { transport = httptracer.GetNewTraceTransport(newTraceV2(), transport) } } else { if !globalJSONLine && !globalJSON { transport = notifyExpiringTLS{transport: transport} } } transport = gzhttp.Transport(transport) config.Transport = transport } // SelectObjectOpts - opts entered for select API type SelectObjectOpts struct { InputSerOpts map[string]map[string]string OutputSerOpts map[string]map[string]string CompressionType minio.SelectCompressionType } type headerTransport struct { http.RoundTripper customHeader http.Header } func (h *headerTransport) RoundTrip(request *http.Request) (*http.Response, error) { for k, v := range h.customHeader { request.Header[k] = v } return h.RoundTripper.RoundTrip(request) } minio-client-0.0~20250403/cmd/common-methods.go000066400000000000000000000451401477450377600210310ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "io" "net/http" "os" "path/filepath" "regexp" "strconv" "strings" "time" "golang.org/x/net/http/httpguts" "github.com/dustin/go-humanize" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/encrypt" "github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/pkg/v3/env" ) // Check if the passed URL represents a folder. It may or may not exist yet. // If it exists, we can easily check if it is a folder, if it doesn't exist, // we can guess if the url is a folder from how it looks. func isAliasURLDir(ctx context.Context, aliasURL string, keys map[string][]prefixSSEPair, timeRef time.Time, ignoreBucketExists bool) (bool, *ClientContent) { // If the target url exists, check if it is a directory // and return immediately. _, targetContent, err := url2Stat(ctx, url2StatOptions{ urlStr: aliasURL, versionID: "", fileAttr: false, encKeyDB: keys, timeRef: timeRef, isZip: false, ignoreBucketExistsCheck: ignoreBucketExists, }) if err == nil { return targetContent.Type.IsDir(), targetContent } _, expandedURL, _ := mustExpandAlias(aliasURL) // Check if targetURL is an FS or S3 aliased url if expandedURL == aliasURL { // This is an FS url, check if the url has a separator at the end return strings.HasSuffix(aliasURL, string(filepath.Separator)), targetContent } // This is an S3 url, then: // *) If alias format is specified, return false // *) If alias/bucket is specified, return true // *) If alias/bucket/prefix, check if prefix has // has a trailing slash. pathURL := filepath.ToSlash(aliasURL) fields := strings.Split(pathURL, "/") switch len(fields) { // Nothing or alias format case 0, 1: return false, targetContent // alias/bucket format case 2: return true, targetContent } // default case.. // alias/bucket/prefix format return strings.HasSuffix(pathURL, "/"), targetContent } // getSourceStreamMetadataFromURL gets a reader from URL. func getSourceStreamMetadataFromURL(ctx context.Context, aliasedURL, versionID string, timeRef time.Time, encKeyDB map[string][]prefixSSEPair, zip bool) (reader io.ReadCloser, content *ClientContent, err *probe.Error, ) { alias, urlStrFull, _, err := expandAlias(aliasedURL) if err != nil { return nil, nil, err.Trace(aliasedURL) } if !timeRef.IsZero() { _, content, err := url2Stat(ctx, url2StatOptions{urlStr: aliasedURL, versionID: "", fileAttr: false, encKeyDB: nil, timeRef: timeRef, isZip: false, ignoreBucketExistsCheck: false}) if err != nil { return nil, nil, err } versionID = content.VersionID } return getSourceStream(ctx, alias, urlStrFull, getSourceOpts{ GetOptions: GetOptions{ SSE: getSSE(aliasedURL, encKeyDB[alias]), VersionID: versionID, Zip: zip, }, }) } type getSourceOpts struct { GetOptions preserve bool } // getSourceStreamFromURL gets a reader from URL. func getSourceStreamFromURL(ctx context.Context, urlStr string, encKeyDB map[string][]prefixSSEPair, opts getSourceOpts) (reader io.ReadCloser, err *probe.Error) { alias, urlStrFull, _, err := expandAlias(urlStr) if err != nil { return nil, err.Trace(urlStr) } opts.SSE = getSSE(urlStr, encKeyDB[alias]) reader, _, err = getSourceStream(ctx, alias, urlStrFull, opts) return reader, err } // Verify if reader is a generic ReaderAt func isReadAt(reader io.Reader) (ok bool) { var v *os.File v, ok = reader.(*os.File) if ok { // Stdin, Stdout and Stderr all have *os.File type // which happen to also be io.ReaderAt compatible // we need to add special conditions for them to // be ignored by this function. for _, f := range []string{ "/dev/stdin", "/dev/stdout", "/dev/stderr", } { if f == v.Name() { ok = false break } } } return } // getSourceStream gets a reader from URL. func getSourceStream(ctx context.Context, alias, urlStr string, opts getSourceOpts) (reader io.ReadCloser, content *ClientContent, err *probe.Error) { sourceClnt, err := newClientFromAlias(alias, urlStr) if err != nil { return nil, nil, err.Trace(alias, urlStr) } reader, content, err = sourceClnt.Get(ctx, opts.GetOptions) if err != nil { return nil, nil, err.Trace(alias, urlStr) } return reader, content, nil } // putTargetRetention sets retention headers if any func putTargetRetention(ctx context.Context, alias, urlStr string, metadata map[string]string) *probe.Error { targetClnt, err := newClientFromAlias(alias, urlStr) if err != nil { return err.Trace(alias, urlStr) } lockModeStr, ok := metadata[AmzObjectLockMode] lockMode := minio.RetentionMode("") if ok { lockMode = minio.RetentionMode(lockModeStr) delete(metadata, AmzObjectLockMode) } retainUntilDateStr, ok := metadata[AmzObjectLockRetainUntilDate] retainUntilDate := timeSentinel if ok { delete(metadata, AmzObjectLockRetainUntilDate) if t, e := time.Parse(time.RFC3339, retainUntilDateStr); e == nil { retainUntilDate = t.UTC() } } if err := targetClnt.PutObjectRetention(ctx, "", lockMode, retainUntilDate, false); err != nil { return err.Trace(alias, urlStr) } return nil } // putTargetStream writes to URL from Reader. func putTargetStream(ctx context.Context, alias, urlStr, mode, until, legalHold string, reader io.Reader, size int64, progress io.Reader, opts PutOptions) (int64, *probe.Error) { targetClnt, err := newClientFromAlias(alias, urlStr) if err != nil { return 0, err.Trace(alias, urlStr) } if mode != "" { opts.metadata[AmzObjectLockMode] = mode } if until != "" { opts.metadata[AmzObjectLockRetainUntilDate] = until } if legalHold != "" { opts.metadata[AmzObjectLockLegalHold] = legalHold } n, err := targetClnt.Put(ctx, reader, size, progress, opts) if err != nil { return n, err.Trace(alias, urlStr) } return n, nil } // putTargetStreamWithURL writes to URL from reader. If length=-1, read until EOF. func putTargetStreamWithURL(urlStr string, reader io.Reader, size int64, opts PutOptions) (int64, *probe.Error) { alias, urlStrFull, _, err := expandAlias(urlStr) if err != nil { return 0, err.Trace(alias, urlStr) } contentType := guessURLContentType(urlStr) if opts.metadata == nil { opts.metadata = map[string]string{} } opts.metadata["Content-Type"] = contentType return putTargetStream(context.Background(), alias, urlStrFull, "", "", "", reader, size, nil, opts) } // copySourceToTargetURL copies to targetURL from source. func copySourceToTargetURL(ctx context.Context, alias, urlStr, source, sourceVersionID, mode, until, legalHold string, size int64, progress io.Reader, opts CopyOptions) *probe.Error { targetClnt, err := newClientFromAlias(alias, urlStr) if err != nil { return err.Trace(alias, urlStr) } opts.versionID = sourceVersionID opts.size = size opts.metadata[AmzObjectLockMode] = mode opts.metadata[AmzObjectLockRetainUntilDate] = until opts.metadata[AmzObjectLockLegalHold] = legalHold err = targetClnt.Copy(ctx, source, opts, progress) if err != nil { return err.Trace(alias, urlStr) } return nil } func filterMetadata(metadata map[string]string) map[string]string { newMetadata := map[string]string{} for k, v := range metadata { if httpguts.ValidHeaderFieldName(k) && httpguts.ValidHeaderFieldValue(v) { newMetadata[k] = v } } for k := range metadata { if strings.HasPrefix(http.CanonicalHeaderKey(k), http.CanonicalHeaderKey(serverEncryptionKeyPrefix)) { delete(newMetadata, k) } } return newMetadata } // getAllMetadata - returns a map of user defined function // by combining the usermetadata of object and values passed by attr keyword func getAllMetadata(ctx context.Context, sourceAlias, sourceURLStr string, srcSSE encrypt.ServerSide, urls URLs) (map[string]string, *probe.Error) { metadata := make(map[string]string) sourceClnt, err := newClientFromAlias(sourceAlias, sourceURLStr) if err != nil { return nil, err.Trace(sourceAlias, sourceURLStr) } st, err := sourceClnt.Stat(ctx, StatOptions{preserve: true, sse: srcSSE}) if err != nil { return nil, err.Trace(sourceAlias, sourceURLStr) } for k, v := range st.Metadata { metadata[http.CanonicalHeaderKey(k)] = v } for k, v := range urls.TargetContent.UserMetadata { metadata[http.CanonicalHeaderKey(k)] = v } return filterMetadata(metadata), nil } // uploadSourceToTargetURL - uploads to targetURL from source. // optionally optimizes copy for object sizes <= 5GiB by using // server side copy operation. func uploadSourceToTargetURL(ctx context.Context, uploadOpts uploadSourceToTargetURLOpts) URLs { sourceAlias := uploadOpts.urls.SourceAlias sourceURL := uploadOpts.urls.SourceContent.URL sourceVersion := uploadOpts.urls.SourceContent.VersionID targetAlias := uploadOpts.urls.TargetAlias targetURL := uploadOpts.urls.TargetContent.URL length := uploadOpts.urls.SourceContent.Size sourcePath := filepath.ToSlash(filepath.Join(sourceAlias, uploadOpts.urls.SourceContent.URL.Path)) targetPath := filepath.ToSlash(filepath.Join(targetAlias, uploadOpts.urls.TargetContent.URL.Path)) srcSSE := getSSE(sourcePath, uploadOpts.encKeyDB[sourceAlias]) tgtSSE := getSSE(targetPath, uploadOpts.encKeyDB[targetAlias]) var err *probe.Error metadata := map[string]string{} var mode, until, legalHold string // add object retention fields in metadata for target, if target wants // to override defaults from source, usually happens in `cp` command. // for the most part source metadata is copied over. if uploadOpts.urls.TargetContent.RetentionEnabled { m := minio.RetentionMode(strings.ToUpper(uploadOpts.urls.TargetContent.RetentionMode)) if !m.IsValid() { return uploadOpts.urls.WithError(probe.NewError(errors.New("invalid retention mode")).Trace(targetURL.String())) } var dur uint64 var unit minio.ValidityUnit dur, unit, err = parseRetentionValidity(uploadOpts.urls.TargetContent.RetentionDuration) if err != nil { return uploadOpts.urls.WithError(err.Trace(targetURL.String())) } mode = uploadOpts.urls.TargetContent.RetentionMode until, err = getRetainUntilDate(dur, unit) if err != nil { return uploadOpts.urls.WithError(err.Trace(sourceURL.String())) } } // add object legal hold fields in metadata for target, if target wants // to override defaults from source, usually happens in `cp` command. // for the most part source metadata is copied over. if uploadOpts.urls.TargetContent.LegalHoldEnabled { switch minio.LegalHoldStatus(uploadOpts.urls.TargetContent.LegalHold) { case minio.LegalHoldDisabled: case minio.LegalHoldEnabled: default: return uploadOpts.urls.WithError(errInvalidArgument().Trace(uploadOpts.urls.TargetContent.LegalHold)) } legalHold = uploadOpts.urls.TargetContent.LegalHold } for k, v := range uploadOpts.urls.SourceContent.UserMetadata { metadata[http.CanonicalHeaderKey(k)] = v } for k, v := range uploadOpts.urls.SourceContent.Metadata { metadata[http.CanonicalHeaderKey(k)] = v } // Optimize for server side copy if the host is same. if sourceAlias == targetAlias && !uploadOpts.isZip && !uploadOpts.urls.checksum.IsSet() { // preserve new metadata and save existing ones. if uploadOpts.preserve { currentMetadata, err := getAllMetadata(ctx, sourceAlias, sourceURL.String(), srcSSE, uploadOpts.urls) if err != nil { return uploadOpts.urls.WithError(err.Trace(sourceURL.String())) } for k, v := range currentMetadata { metadata[k] = v } } // Get metadata from target content as well for k, v := range uploadOpts.urls.TargetContent.Metadata { metadata[http.CanonicalHeaderKey(k)] = v } // Get userMetadata from target content as well for k, v := range uploadOpts.urls.TargetContent.UserMetadata { metadata[http.CanonicalHeaderKey(k)] = v } sourcePath := filepath.ToSlash(sourceURL.Path) if uploadOpts.urls.SourceContent.RetentionEnabled { err = putTargetRetention(ctx, targetAlias, targetURL.String(), metadata) return uploadOpts.urls.WithError(err.Trace(sourceURL.String())) } opts := CopyOptions{ srcSSE: srcSSE, tgtSSE: tgtSSE, metadata: filterMetadata(metadata), disableMultipart: uploadOpts.urls.DisableMultipart, isPreserve: uploadOpts.preserve, storageClass: uploadOpts.urls.TargetContent.StorageClass, } err = copySourceToTargetURL(ctx, targetAlias, targetURL.String(), sourcePath, sourceVersion, mode, until, legalHold, length, uploadOpts.progress, opts) } else { if uploadOpts.urls.SourceContent.RetentionEnabled { // preserve new metadata and save existing ones. if uploadOpts.preserve { currentMetadata, err := getAllMetadata(ctx, sourceAlias, sourceURL.String(), srcSSE, uploadOpts.urls) if err != nil { return uploadOpts.urls.WithError(err.Trace(sourceURL.String())) } for k, v := range currentMetadata { metadata[k] = v } } // Get metadata from target content as well for k, v := range uploadOpts.urls.TargetContent.Metadata { metadata[http.CanonicalHeaderKey(k)] = v } // Get userMetadata from target content as well for k, v := range uploadOpts.urls.TargetContent.UserMetadata { metadata[http.CanonicalHeaderKey(k)] = v } err = putTargetRetention(ctx, targetAlias, targetURL.String(), metadata) return uploadOpts.urls.WithError(err.Trace(sourceURL.String())) } // Proceed with regular stream copy. var ( content *ClientContent reader io.ReadCloser ) reader, content, err = getSourceStream(ctx, sourceAlias, sourceURL.String(), getSourceOpts{ GetOptions: GetOptions{ VersionID: sourceVersion, SSE: srcSSE, Zip: uploadOpts.isZip, Preserve: uploadOpts.preserve, }, }) if err != nil { return uploadOpts.urls.WithError(err.Trace(sourceURL.String())) } defer reader.Close() if uploadOpts.updateProgressTotal { pg, ok := uploadOpts.progress.(*progressBar) if ok { pg.SetTotal(content.Size) } } metadata := make(map[string]string, len(content.Metadata)) for k, v := range content.Metadata { metadata[k] = v } // Get metadata from target content as well for k, v := range uploadOpts.urls.TargetContent.Metadata { metadata[http.CanonicalHeaderKey(k)] = v } // Get userMetadata from target content as well for k, v := range uploadOpts.urls.TargetContent.UserMetadata { metadata[http.CanonicalHeaderKey(k)] = v } if content.Tags != nil { tags, err := tags.NewTags(content.Tags, true) if err != nil { return uploadOpts.urls.WithError(probe.NewError(err)) } metadata["X-Amz-Tagging"] = tags.String() delete(metadata, "X-Amz-Tagging-Count") } var e error var multipartSize uint64 var multipartThreads int var v string if uploadOpts.multipartSize == "" { v = env.Get("MC_UPLOAD_MULTIPART_SIZE", "") } else { v = uploadOpts.multipartSize } if v != "" { multipartSize, e = humanize.ParseBytes(v) if e != nil { return uploadOpts.urls.WithError(probe.NewError(e)) } } if uploadOpts.multipartThreads == "" { multipartThreads, e = strconv.Atoi(env.Get("MC_UPLOAD_MULTIPART_THREADS", "4")) } else { multipartThreads, e = strconv.Atoi(uploadOpts.multipartThreads) } if e != nil { return uploadOpts.urls.WithError(probe.NewError(e)) } putOpts := PutOptions{ metadata: filterMetadata(metadata), sse: tgtSSE, storageClass: uploadOpts.urls.TargetContent.StorageClass, md5: uploadOpts.urls.MD5, disableMultipart: uploadOpts.urls.DisableMultipart, isPreserve: uploadOpts.preserve, multipartSize: multipartSize, multipartThreads: uint(multipartThreads), ifNotExists: uploadOpts.ifNotExists, checksum: uploadOpts.urls.checksum, } if isReadAt(reader) || length == 0 { _, err = putTargetStream(ctx, targetAlias, targetURL.String(), mode, until, legalHold, reader, length, uploadOpts.progress, putOpts) } else { _, err = putTargetStream(ctx, targetAlias, targetURL.String(), mode, until, legalHold, io.LimitReader(reader, length), length, uploadOpts.progress, putOpts) } } if err != nil { return uploadOpts.urls.WithError(err.Trace(sourceURL.String())) } return uploadOpts.urls.WithError(nil) } // newClientFromAlias gives a new client interface for matching // alias entry in the mc config file. If no matching host config entry // is found, fs client is returned. func newClientFromAlias(alias, urlStr string) (Client, *probe.Error) { alias, _, hostCfg, err := expandAlias(alias) if err != nil { return nil, err.Trace(alias, urlStr) } if hostCfg == nil { // No matching host config. So we treat it like a // filesystem. fsClient, fsErr := fsNew(urlStr) if fsErr != nil { return nil, fsErr.Trace(alias, urlStr) } return fsClient, nil } s3Config := NewS3Config(alias, urlStr, hostCfg) s3Client, err := S3New(s3Config) if err != nil { return nil, err.Trace(alias, urlStr) } return s3Client, nil } // urlRgx - verify if aliased url is real URL. var urlRgx = regexp.MustCompile("^https?://") // newClient gives a new client interface func newClient(aliasedURL string) (Client, *probe.Error) { alias, urlStrFull, hostCfg, err := expandAlias(aliasedURL) if err != nil { return nil, err.Trace(aliasedURL) } // Verify if the aliasedURL is a real URL, fail in those cases // indicating the user to add alias. if hostCfg == nil && urlRgx.MatchString(aliasedURL) { return nil, errInvalidAliasedURL(aliasedURL).Trace(aliasedURL) } return newClientFromAlias(alias, urlStrFull) } // ParseForm parses a http.Request form and populates the array func ParseForm(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } for k, v := range r.PostForm { if _, ok := r.Form[k]; !ok { r.Form[k] = v } } return nil } type uploadSourceToTargetURLOpts struct { urls URLs progress io.Reader encKeyDB map[string][]prefixSSEPair preserve, isZip bool multipartSize string multipartThreads string updateProgressTotal bool ifNotExists bool } minio-client-0.0~20250403/cmd/config-deprecated.go000066400000000000000000000053261477450377600214450ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var configCmd = cli.Command{ Name: "config", Usage: "configure MinIO client", Action: func(ctx *cli.Context) error { cli.ShowCommandHelp(ctx, ctx.Args().First()) return nil }, Hidden: true, Before: setGlobalsFromContext, HideHelpCommand: true, Flags: globalFlags, Subcommands: []cli.Command{ configHostCmd, }, } var configHostCmd = cli.Command{ Name: "host", Usage: "add, remove and list hosts in configuration file", Action: func(ctx *cli.Context) error { cli.ShowCommandHelp(ctx, ctx.Args().First()) return nil }, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: []cli.Command{ configHostAddCmd, configHostRemoveCmd, configHostListCmd, }, HideHelpCommand: true, } var configHostAddFlags = []cli.Flag{ cli.StringFlag{ Name: "lookup", Value: "auto", Usage: "bucket lookup supported by the server. Valid options are '[dns, path, auto]'", }, cli.StringFlag{ Name: "api", Usage: "API signature. Valid options are '[S3v4, S3v2]'", }, } var configHostAddCmd = cli.Command{ Name: "add", ShortName: "a", Usage: "add a new host to configuration file", Action: func(cli *cli.Context) error { return mainAliasSet(cli, true) }, Before: setGlobalsFromContext, Flags: append(configHostAddFlags, globalFlags...), HideHelpCommand: true, } var configHostListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list hosts in configuration file", Action: func(cli *cli.Context) error { return mainAliasList(cli, true) }, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, } var configHostRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove a host from configuration file", Action: func(cli *cli.Context) error { return mainAliasRemove(cli) }, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, } minio-client-0.0~20250403/cmd/config-fix.go000066400000000000000000000236671477450377600201430ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "os" "path/filepath" "runtime" "strings" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" "github.com/minio/pkg/v3/quick" ) func fixConfig() { // Migrate config location on windows fixConfigLocation() // Fix config V3 fixConfigV3() // Fix config V6 fixConfigV6() // Fix config V6 for hosts fixConfigV6ForHosts() /* No more fixing job. Here after we bump the version for changes always. */ } // ConfigAnyVersion is a generic structure to parse any // config.json version file and only extracts its version number type ConfigAnyVersion struct { Version string } // ///////////////// Broken Config V3 /////////////////// type brokenHostConfigV3 struct { AccessKeyID string SecretAccessKey string } type brokenConfigV3 struct { Version string ACL string Access string Aliases map[string]string Hosts map[string]brokenHostConfigV3 } // newConfigV3 - get new config broken version 3. func newBrokenConfigV3() *brokenConfigV3 { conf := new(brokenConfigV3) conf.Version = "3" conf.Aliases = make(map[string]string) conf.Hosts = make(map[string]brokenHostConfigV3) return conf } // Fix config version `3`. Some v3 config files are written without // proper hostConfig JSON tags. They may also contain unused ACL and // Access fields. Rewrite the hostConfig with proper fields using JSON // tags and drop the unused (ACL, Access) fields. func fixConfigV3() { if !isMcConfigExists() { return } // Check if this is the correct version to fix configAllVersions, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config.") if configAllVersions.Version() != "3" { return } brokenCfgV3 := newBrokenConfigV3() brokenMcCfgV3, e := quick.LoadConfig(mustGetMcConfigPath(), nil, brokenCfgV3) fatalIf(probe.NewError(e), "Unable to load config.") cfgV3 := newConfigV3() isMutated := false for k, v := range brokenMcCfgV3.Data().(*brokenConfigV3).Aliases { cfgV3.Aliases[k] = v } for host, brokenHostCfgV3 := range brokenMcCfgV3.Data().(*brokenConfigV3).Hosts { // If any of these fields contains any real value anytime, // it means we have already fixed the broken configuration. // We don't have to regenerate again. if brokenHostCfgV3.AccessKeyID != "" && brokenHostCfgV3.SecretAccessKey != "" { isMutated = true } // Use the correct hostConfig with JSON tags in it. cfgV3.Hosts[host] = hostConfigV3(brokenHostCfgV3) } // We blindly drop ACL and Access fields from the broken config v3. if isMutated { mcNewConfigV3, e := quick.NewConfig(cfgV3, nil) fatalIf(probe.NewError(e), "Unable to initialize quick config for config version `3`.") e = mcNewConfigV3.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e), "Unable to save config version `3`.") console.Infof("Successfully fixed %s broken config for version `3`.\n", mustGetMcConfigPath()) } } // If the host key does not have http(s), fix it. func fixConfigV6ForHosts() { if !isMcConfigExists() { return } // Check the current config version configAllVersions, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config.") if configAllVersions.Version() != "6" { return } brokenMcCfgV6, e := quick.LoadConfig(mustGetMcConfigPath(), nil, newConfigV6()) fatalIf(probe.NewError(e), "Unable to load config.") newCfgV6 := newConfigV6() isMutated := false // Copy aliases. for k, v := range brokenMcCfgV6.Data().(*configV6).Aliases { newCfgV6.Aliases[k] = v } url := &ClientURL{} // Copy hosts. for host, hostCfgV6 := range brokenMcCfgV6.Data().(*configV6).Hosts { // Already fixed - Copy and move on. if strings.HasPrefix(host, "https") || strings.HasPrefix(host, "http") { newCfgV6.Hosts[host] = hostCfgV6 continue } // If host entry does not contain "http(s)", introduce a new entry and delete the old one. if host == "s3.amazonaws.com" || host == "storage.googleapis.com" || host == "localhost:9000" || host == "127.0.0.1:9000" || host == "play.min.io:9000" || host == "dl.min.io:9000" { console.Infoln("Found broken host entries, replacing " + host + " with https://" + host + ".") url.Host = host url.Scheme = "https" url.SchemeSeparator = "://" newCfgV6.Hosts[url.String()] = hostCfgV6 isMutated = true continue } } if isMutated { // Save the new config back to the disk. mcCfgV6, e := quick.NewConfig(newCfgV6, nil) fatalIf(probe.NewError(e), "Unable to initialize quick config for config version `v6`.") e = mcCfgV6.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e), "Unable to save config version `v6`.") } } // fixConfigV6 - fix all the unnecessary glob URLs present in existing config version 6. func fixConfigV6() { if !isMcConfigExists() { return } configAllVersions, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config.") if configAllVersions.Version() != "6" { return } config, e := quick.NewConfig(newConfigV6(), nil) fatalIf(probe.NewError(e), "Unable to initialize config.") e = config.Load(mustGetMcConfigPath()) fatalIf(probe.NewError(e).Trace(mustGetMcConfigPath()), "Unable to load config.") newConfig := new(configV6) isMutated := false newConfig.Aliases = make(map[string]string) newConfig.Hosts = make(map[string]hostConfigV6) newConfig.Version = "6" newConfig.Aliases = config.Data().(*configV6).Aliases for host, hostCfg := range config.Data().(*configV6).Hosts { if strings.Contains(host, "*") { fatalIf(errInvalidArgument(), fmt.Sprintf("Glob style `*` pattern matching is no longer supported. Please fix `%s` entry manually.", host)) } if strings.Contains(host, "*s3*") || strings.Contains(host, "*.s3*") { console.Infoln("Found glob url, replacing " + host + " with s3.amazonaws.com") newConfig.Hosts["s3.amazonaws.com"] = hostCfg isMutated = true continue } if strings.Contains(host, "s3*") { console.Infoln("Found glob url, replacing " + host + " with s3.amazonaws.com") newConfig.Hosts["s3.amazonaws.com"] = hostCfg isMutated = true continue } if strings.Contains(host, "*amazonaws.com") || strings.Contains(host, "*.amazonaws.com") { console.Infoln("Found glob url, replacing " + host + " with s3.amazonaws.com") newConfig.Hosts["s3.amazonaws.com"] = hostCfg isMutated = true continue } if strings.Contains(host, "*storage.googleapis.com") { console.Infoln("Found glob url, replacing " + host + " with storage.googleapis.com") newConfig.Hosts["storage.googleapis.com"] = hostCfg isMutated = true continue } if strings.Contains(host, "localhost:*") { console.Infoln("Found glob url, replacing " + host + " with localhost:9000") newConfig.Hosts["localhost:9000"] = hostCfg isMutated = true continue } if strings.Contains(host, "127.0.0.1:*") { console.Infoln("Found glob url, replacing " + host + " with 127.0.0.1:9000") newConfig.Hosts["127.0.0.1:9000"] = hostCfg isMutated = true continue } // Other entries are hopefully OK. Copy them blindly. newConfig.Hosts[host] = hostCfg } if isMutated { newConf, e := quick.NewConfig(newConfig, nil) fatalIf(probe.NewError(e), "Unable to initialize newly fixed config.") e = newConf.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e).Trace(mustGetMcConfigPath()), "Unable to save newly fixed config path.") console.Infof("Successfully fixed %s broken config for version `6`.\n", mustGetMcConfigPath()) } } // fixConfigLocation will resolve the possible duplicate location of Windows config files. // If there is duplicate configs, it will use the currently enabled config location and // move it to the 'normalized' location. // See https://github.com/minio/mc/pull/2898 func fixConfigLocation() { if runtime.GOOS != "windows" || mcCustomConfigDir != mustGetMcConfigDir() { return } if !strings.HasSuffix(strings.ToLower(filepath.Base(os.Args[0])), ".exe") { // Most likely scenario, command was called as 'mc'. // If there is a config at legacyLoc+".exe", rename it. legacyLoc := mcCustomConfigDir + ".exe" unusedLoc := mcCustomConfigDir + ".unused" s, e := os.Stat(legacyLoc) if e != nil || !s.IsDir() { return } _ = os.Rename(legacyLoc, unusedLoc) return } // mc was called with '.exe'; // config can have changed location. _, e := os.Stat(mcCustomConfigDir) wantExists := !os.IsNotExist(e) legFileName := mcCustomConfigDir + ".exe" stat, e := os.Stat(legFileName) legExists := !os.IsNotExist(e) && stat.IsDir() switch { case legExists && wantExists: // Both exist and mc was called with legacy path (.exe) // Rename the 'mc' config and move the legacy location one to where we want it. backupdir := fmt.Sprintf("%s.unused\\", mcCustomConfigDir) _ = os.RemoveAll(backupdir) e := os.Rename(mcCustomConfigDir, backupdir) fatalIf(probe.NewError(e), fmt.Sprintln("Renaming unused config", mcCustomConfigDir, "->", backupdir, "failed. Please rename/remove file.")) fallthrough case !wantExists && legExists: e := os.Rename(legFileName, mcCustomConfigDir) fatalIf(probe.NewError(e), fmt.Sprintln("Migrating config location", legFileName, "->", mcCustomConfigDir, "failed. Please move config file.")) default: // Legacy does not exist. } } minio-client-0.0~20250403/cmd/config-migrate.go000066400000000000000000000431141477450377600207720ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strings" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" "github.com/minio/pkg/v3/quick" ) // migrate config files from the any older version to the latest. func migrateConfig() { // Migrate config V1 to V101 migrateConfigV1ToV101() // Migrate config V101 to V2 migrateConfigV101ToV2() // Migrate config V2 to V3 migrateConfigV2ToV3() // Migrate config V3 to V4 migrateConfigV3ToV4() // Migrate config V4 to V5 migrateConfigV4ToV5() // Migrate config V5 to V6 migrateConfigV5ToV6() // Migrate config V6 to V7 migrateConfigV6ToV7() // Migrate config V7 to V8 migrateConfigV7ToV8() // Migrate config V8 to V9 migrateConfigV8ToV9() // Migrate config V9 to V10 migrateConfigV9ToV10() } // Migrate from config version 1.0 to 1.0.1. Populate example entries and save it back. func migrateConfigV1ToV101() { if !isMcConfigExists() { return } // Check the config version and quit early if the actual version is out of this function scope anyCfg, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config version `1`.") if anyCfg.Version() != "1.0.0" { return } mcCfgV1, e := quick.LoadConfig(mustGetMcConfigPath(), nil, newConfigV1()) fatalIf(probe.NewError(e), "Unable to load config version `1`.") // 1.0.1 is compatible to 1.0.0. We are just adding new entries. cfgV101 := newConfigV101() // Copy aliases. for k, v := range mcCfgV1.Data().(*configV1).Aliases { cfgV101.Aliases[k] = v } // Copy hosts. for k, hostCfgV1 := range mcCfgV1.Data().(*configV1).Hosts { cfgV101.Hosts[k] = hostConfigV101(hostCfgV1) } // Example localhost entry. if _, ok := cfgV101.Hosts["localhost:*"]; !ok { cfgV101.Hosts["localhost:*"] = hostConfigV101{} } // Example loopback IP entry. if _, ok := cfgV101.Hosts["127.0.0.1:*"]; !ok { cfgV101.Hosts["127.0.0.1:*"] = hostConfigV101{} } // Example AWS entry. // Look for glob string (not glob match). We used to support glob based key matching earlier. if _, ok := cfgV101.Hosts["*.s3*.amazonaws.com"]; !ok { cfgV101.Hosts["*.s3*.amazonaws.com"] = hostConfigV101{ AccessKeyID: "YOUR-ACCESS-KEY-ID-HERE", SecretAccessKey: "YOUR-SECRET-ACCESS-KEY-HERE", } } // Save the new config back to the disk. mcCfgV101, e := quick.NewConfig(cfgV101, nil) fatalIf(probe.NewError(e), "Unable to initialize quick config for config version `1.0.1`.") e = mcCfgV101.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e), "Unable to save config version `1.0.1`.") console.Infof("Successfully migrated %s from version `1.0.0` to version `1.0.1`.\n", mustGetMcConfigPath()) } // Migrate from config `1.0.1` to `2`. Drop semantic versioning and move to integer versioning. No other changes. func migrateConfigV101ToV2() { if !isMcConfigExists() { return } // Check the config version and quit early if the actual version is out of this function scope anyCfg, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config version `1`.") if anyCfg.Version() != "1.0.1" { return } mcCfgV101, e := quick.LoadConfig(mustGetMcConfigPath(), nil, newConfigV101()) fatalIf(probe.NewError(e), "Unable to load config version `1.0.1`.") // update to newer version cfgV2 := newConfigV2() // Copy aliases. for k, v := range mcCfgV101.Data().(*configV101).Aliases { cfgV2.Aliases[k] = v } // Copy hosts. for k, hostCfgV101 := range mcCfgV101.Data().(*configV101).Hosts { cfgV2.Hosts[k] = hostConfigV2(hostCfgV101) } mcCfgV2, e := quick.NewConfig(cfgV2, nil) fatalIf(probe.NewError(e), "Unable to initialize quick config for config version `2`.") e = mcCfgV2.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e), "Unable to save config version `2`.") console.Infof("Successfully migrated %s from version `1.0.1` to version `2`.\n", mustGetMcConfigPath()) } // Migrate from config `2` to `3`. Use `-` separated names for // hostConfig using struct json tags. func migrateConfigV2ToV3() { if !isMcConfigExists() { return } // Check the config version and quit early if the actual version is out of this function scope anyCfg, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config version.") if anyCfg.Version() != "2" { return } mcCfgV2, e := quick.LoadConfig(mustGetMcConfigPath(), nil, newConfigV2()) fatalIf(probe.NewError(e), "Unable to load mc config V2.") cfgV3 := newConfigV3() // Copy aliases. for k, v := range mcCfgV2.Data().(*configV2).Aliases { cfgV3.Aliases[k] = v } // Copy hosts. for k, hostCfgV2 := range mcCfgV2.Data().(*configV2).Hosts { // New hostConfV3 uses struct json tags. cfgV3.Hosts[k] = hostConfigV3(hostCfgV2) } mcNewCfgV3, e := quick.NewConfig(cfgV3, nil) fatalIf(probe.NewError(e), "Unable to initialize quick config for config version `3`.") e = mcNewCfgV3.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e), "Unable to save config version `3`.") console.Infof("Successfully migrated %s from version `2` to version `3`.\n", mustGetMcConfigPath()) } // Migrate from config version `3` to `4`. Introduce API Signature // field in host config. Also Use JavaScript notation for field names. func migrateConfigV3ToV4() { if !isMcConfigExists() { return } // Check the config version and quit early if the actual version is out of this function scope anyCfg, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config version.") if anyCfg.Version() != "3" { return } mcCfgV3, e := quick.LoadConfig(mustGetMcConfigPath(), nil, newConfigV3()) fatalIf(probe.NewError(e), "Unable to load mc config V3.") cfgV4 := newConfigV4() for k, v := range mcCfgV3.Data().(*configV3).Aliases { cfgV4.Aliases[k] = v } // New hostConfig has API signature. All older entries were V4 // only. So it is safe to assume V4 as default for all older // entries. // HostConfigV4 als uses JavaScript naming notation for struct JSON tags. for host, hostCfgV3 := range mcCfgV3.Data().(*configV3).Hosts { cfgV4.Hosts[host] = hostConfigV4{ AccessKeyID: hostCfgV3.AccessKeyID, SecretAccessKey: hostCfgV3.SecretAccessKey, Signature: "v4", } } mcNewCfgV4, e := quick.NewConfig(cfgV4, nil) fatalIf(probe.NewError(e), "Unable to initialize quick config for config version `4`.") e = mcNewCfgV4.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e), "Unable to save config version `4`.") console.Infof("Successfully migrated %s from version `3` to version `4`.\n", mustGetMcConfigPath()) } // Migrate config version `4` to `5`. Rename hostConfigV4.Signature -> hostConfigV5.API. func migrateConfigV4ToV5() { if !isMcConfigExists() { return } // Check the config version and quit early if the actual version is out of this function scope anyCfg, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config version.") if anyCfg.Version() != "4" { return } mcCfgV4, e := quick.LoadConfig(mustGetMcConfigPath(), nil, newConfigV4()) fatalIf(probe.NewError(e), "Unable to load mc config V4.") cfgV5 := newConfigV5() for k, v := range mcCfgV4.Data().(*configV4).Aliases { cfgV5.Aliases[k] = v } for host, hostCfgV4 := range mcCfgV4.Data().(*configV4).Hosts { cfgV5.Hosts[host] = hostConfigV5{ AccessKeyID: hostCfgV4.AccessKeyID, SecretAccessKey: hostCfgV4.SecretAccessKey, API: "v4", // Rename from .Signature to .API } } mcNewCfgV5, e := quick.NewConfig(cfgV5, nil) fatalIf(probe.NewError(e), "Unable to initialize quick config for config version `5`.") e = mcNewCfgV5.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e), "Unable to save config version `5`.") console.Infof("Successfully migrated %s from version `4` to version `5`.\n", mustGetMcConfigPath()) } // Migrate config version `5` to `6`. Add google cloud storage servers // to host config. Also remove "." from s3 aws glob rule. func migrateConfigV5ToV6() { if !isMcConfigExists() { return } // Check the config version and quit early if the actual version is out of this function scope anyCfg, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config version.") if anyCfg.Version() != "5" { return } mcCfgV5, e := quick.LoadConfig(mustGetMcConfigPath(), nil, newConfigV5()) fatalIf(probe.NewError(e), "Unable to load mc config V5.") cfgV6 := newConfigV6() // Add new Google Cloud Storage alias. cfgV6.Aliases["gcs"] = "https://storage.googleapis.com" for k, v := range mcCfgV5.Data().(*configV5).Aliases { cfgV6.Aliases[k] = v } // Add defaults. cfgV6.Hosts["*s3*amazonaws.com"] = hostConfigV6{ AccessKeyID: "YOUR-ACCESS-KEY-ID-HERE", SecretAccessKey: "YOUR-SECRET-ACCESS-KEY-HERE", API: "S3v4", } cfgV6.Hosts["*storage.googleapis.com"] = hostConfigV6{ AccessKeyID: "YOUR-ACCESS-KEY-ID-HERE", SecretAccessKey: "YOUR-SECRET-ACCESS-KEY-HERE", API: "S3v2", } for host, hostCfgV5 := range mcCfgV5.Data().(*configV5).Hosts { // Find any matching s3 entry and copy keys from it to newer generalized glob entry. if strings.Contains(host, "s3") { if (hostCfgV5.AccessKeyID == "YOUR-ACCESS-KEY-ID-HERE") || (hostCfgV5.SecretAccessKey == "YOUR-SECRET-ACCESS-KEY-HERE") || hostCfgV5.AccessKeyID == "" || hostCfgV5.SecretAccessKey == "" { continue // Skip defaults. } // Now we have real keys set by the user. Copy // them over to newer glob rule. // Original host entry has "." in the glob rule. host = "*s3*amazonaws.com" // Use this glob entry. } cfgV6.Hosts[host] = hostConfigV6(hostCfgV5) } mcNewCfgV6, e := quick.NewConfig(cfgV6, nil) fatalIf(probe.NewError(e), "Unable to initialize quick config for config version `6`.") e = mcNewCfgV6.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e), "Unable to save config version `6`.") console.Infof("Successfully migrated %s from version `5` to version `6`.\n", mustGetMcConfigPath()) } // Migrate config version `6` to `7'. Remove alias map and introduce // named Host config. Also no more glob match for host config entries. func migrateConfigV6ToV7() { if !isMcConfigExists() { return } // Check the config version and quit early if the actual version is out of this function scope anyCfg, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config version.") if anyCfg.Version() != "6" { return } mcCfgV6, e := quick.LoadConfig(mustGetMcConfigPath(), nil, newConfigV6()) fatalIf(probe.NewError(e), "Unable to load mc config V6.") cfgV7 := newConfigV7() aliasIndex := 0 // old Aliases. oldAliases := mcCfgV6.Data().(*configV6).Aliases // We dropped alias support in v7. We only need to migrate host configs. for host, hostCfgV6 := range mcCfgV6.Data().(*configV6).Hosts { // Look through old aliases, if found any matching save those entries. for aliasName, aliasedHost := range oldAliases { if aliasedHost == host { cfgV7.Hosts[aliasName] = hostConfigV7{ URL: host, AccessKey: hostCfgV6.AccessKeyID, SecretKey: hostCfgV6.SecretAccessKey, API: hostCfgV6.API, } continue } } switch host { case "https://s3.amazonaws.com": // Only one entry can exist for "s3" domain. cfgV7.Hosts["s3"] = hostConfigV7{ URL: host, AccessKey: hostCfgV6.AccessKeyID, SecretKey: hostCfgV6.SecretAccessKey, API: hostCfgV6.API, } case "https://storage.googleapis.com": // Only one entry can exist for "gcs" domain. cfgV7.Hosts["gcs"] = hostConfigV7{ URL: host, AccessKey: hostCfgV6.AccessKeyID, SecretKey: hostCfgV6.SecretAccessKey, API: hostCfgV6.API, } default: // Assign a generic "cloud1", cloud2..." key // for all other entries that has valid keys set. alias := fmt.Sprintf("cloud%d", aliasIndex) aliasIndex++ cfgV7.Hosts[alias] = hostConfigV7{ URL: host, AccessKey: hostCfgV6.AccessKeyID, SecretKey: hostCfgV6.SecretAccessKey, API: hostCfgV6.API, } } } // Load default settings. cfgV7.loadDefaults() mcNewCfgV7, e := quick.NewConfig(cfgV7, nil) fatalIf(probe.NewError(e), "Unable to initialize quick config for config version `7`.") e = mcNewCfgV7.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e), "Unable to save config version `7`.") console.Infof("Successfully migrated %s from version `6` to version `7`.\n", mustGetMcConfigPath()) } // Migrate config version `7` to `8'. Remove hosts // 'play.min.io:9002' and 'dl.min.io:9000'. func migrateConfigV7ToV8() { if !isMcConfigExists() { return } // Check the config version and quit early if the actual version is out of this function scope anyCfg, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config version.") if anyCfg.Version() != "7" { return } mcCfgV7, e := quick.LoadConfig(mustGetMcConfigPath(), nil, newConfigV7()) fatalIf(probe.NewError(e), "Unable to load mc config V7.") cfgV8 := newConfigV8() // We dropped alias support in v7. We only need to migrate host configs. for host, hostCfgV7 := range mcCfgV7.Data().(*configV7).Hosts { // Ignore 'player', 'play' and 'dl' aliases. if host == "player" || host == "dl" || host == "play" { continue } hostCfgV8 := hostConfigV8{} hostCfgV8.URL = hostCfgV7.URL hostCfgV8.AccessKey = hostCfgV7.AccessKey hostCfgV8.SecretKey = hostCfgV7.SecretKey hostCfgV8.API = hostCfgV7.API cfgV8.Hosts[host] = hostCfgV8 } // Load default settings. cfgV8.loadDefaults() mcNewCfgV8, e := quick.NewConfig(cfgV8, nil) fatalIf(probe.NewError(e), "Unable to initialize quick config for config version `8`.") e = mcNewCfgV8.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e), "Unable to save config version `8`.") console.Infof("Successfully migrated %s from version `7` to version `8`.\n", mustGetMcConfigPath()) } // Migrate config version `8` to `9'. Add optional field virtual func migrateConfigV8ToV9() { if !isMcConfigExists() { return } // Check the config version and quit early if the actual version is out of this function scope anyCfg, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config version.") if anyCfg.Version() != "8" { return } mcCfgV8, e := quick.LoadConfig(mustGetMcConfigPath(), nil, newConfigV8()) fatalIf(probe.NewError(e), "Unable to load mc config V8.") cfgV9 := newConfigV9() // We dropped alias support in v8. We only need to migrate host configs. for host, hostCfgV8 := range mcCfgV8.Data().(*configV8).Hosts { // Ignore 'player', 'play' and 'dl' aliases. if host == "player" || host == "dl" || host == "play" { continue } hostCfgV9 := hostConfigV9{} hostCfgV9.URL = hostCfgV8.URL hostCfgV9.AccessKey = hostCfgV8.AccessKey hostCfgV9.SecretKey = hostCfgV8.SecretKey hostCfgV9.API = hostCfgV8.API hostCfgV9.Lookup = "auto" cfgV9.Hosts[host] = hostCfgV9 } mcNewCfgV9, e := quick.NewConfig(cfgV9, nil) fatalIf(probe.NewError(e), "Unable to initialize quick config for config version `9`.") e = mcNewCfgV9.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e), "Unable to save config version `9`.") console.Infof("Successfully migrated %s from version `8` to version `9`.\n", mustGetMcConfigPath()) } // Migrate config version `9` to `10'. Rename 'hosts' to 'aliases' and 'lookup' to 'path' func migrateConfigV9ToV10() { if !isMcConfigExists() { return } // Check the config version and quit early if the actual version is out of this function scope anyCfg, e := quick.LoadConfig(mustGetMcConfigPath(), nil, &ConfigAnyVersion{}) fatalIf(probe.NewError(e), "Unable to load config version.") if anyCfg.Version() != "9" { return } mcCfgV9, e := quick.LoadConfig(mustGetMcConfigPath(), nil, newConfigV9()) fatalIf(probe.NewError(e), "Unable to load mc config V8.") cfgV10 := newConfigV10() isEmpty := true // We dropped alias support in v8. We only need to migrate host configs. for host, hostCfgV9 := range mcCfgV9.Data().(*configV9).Hosts { isEmpty = false hostCfgV10 := aliasConfigV10{} hostCfgV10.URL = hostCfgV9.URL hostCfgV10.AccessKey = hostCfgV9.AccessKey hostCfgV10.SecretKey = hostCfgV9.SecretKey hostCfgV10.API = hostCfgV9.API switch hostCfgV9.Lookup { case "dns": hostCfgV10.Path = "off" case "path": hostCfgV10.Path = "on" default: hostCfgV10.Path = "auto" } cfgV10.Aliases[host] = hostCfgV10 } if isEmpty { // Load default settings. cfgV10.loadDefaults() } mcNewCfgV10, e := quick.NewConfig(cfgV10, nil) fatalIf(probe.NewError(e), "Unable to initialize quick config for config version `10`.") e = mcNewCfgV10.Save(mustGetMcConfigPath()) fatalIf(probe.NewError(e), "Unable to save config version `10`.") console.Infof("Successfully migrated %s from version `9` to version `10`.\n", mustGetMcConfigPath()) } minio-client-0.0~20250403/cmd/config-old.go000066400000000000000000000210501477450377600201130ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd // ///////////////// Config V1 /////////////////// type hostConfigV1 struct { AccessKeyID string SecretAccessKey string } type configV1 struct { Version string Aliases map[string]string Hosts map[string]hostConfigV1 } // newConfigV1() - get new config version 1.0.0 func newConfigV1() *configV1 { conf := new(configV1) conf.Version = "1.0.0" // make sure to allocate map's otherwise Golang // exits silently without providing any errors conf.Aliases = make(map[string]string) conf.Hosts = make(map[string]hostConfigV1) return conf } // ///////////////// Config V101 /////////////////// type hostConfigV101 hostConfigV1 type configV101 struct { Version string Aliases map[string]string Hosts map[string]hostConfigV101 } // newConfigV101() - get new config version 1.0.1 func newConfigV101() *configV101 { conf := new(configV101) conf.Version = "1.0.1" conf.Aliases = make(map[string]string) conf.Hosts = make(map[string]hostConfigV101) return conf } // ///////////////// Config V2 /////////////////// type hostConfigV2 hostConfigV1 type configV2 struct { Version string Aliases map[string]string Hosts map[string]hostConfigV2 } // newConfigV2() - get new config version 2 func newConfigV2() *configV2 { conf := new(configV2) conf.Version = "2" conf.Aliases = make(map[string]string) conf.Hosts = make(map[string]hostConfigV2) return conf } // ///////////////// Config V3 /////////////////// type hostConfigV3 struct { AccessKeyID string `json:"access-key-id"` SecretAccessKey string `json:"secret-access-key"` } type configV3 struct { Version string `json:"version"` Aliases map[string]string `json:"alias"` Hosts map[string]hostConfigV3 `json:"hosts"` } // newConfigV3 - get new config version 3. func newConfigV3() *configV3 { conf := new(configV3) conf.Version = "3" conf.Aliases = make(map[string]string) conf.Hosts = make(map[string]hostConfigV3) return conf } // ///////////////// Config V4 /////////////////// type hostConfigV4 struct { AccessKeyID string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` Signature string `json:"signature"` } type configV4 struct { Version string `json:"version"` Aliases map[string]string `json:"alias"` Hosts map[string]hostConfigV4 `json:"hosts"` } func newConfigV4() *configV4 { conf := new(configV4) conf.Version = "4" conf.Aliases = make(map[string]string) conf.Hosts = make(map[string]hostConfigV4) return conf } // ///////////////// Config V5 /////////////////// type hostConfigV5 struct { AccessKeyID string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` API string `json:"api"` } type configV5 struct { Version string `json:"version"` Aliases map[string]string `json:"alias"` Hosts map[string]hostConfigV5 `json:"hosts"` } func newConfigV5() *configV5 { conf := new(configV5) conf.Version = "5" conf.Aliases = make(map[string]string) conf.Hosts = make(map[string]hostConfigV5) return conf } // ///////////////// Config V6 /////////////////// type hostConfigV6 struct { AccessKeyID string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` API string `json:"api"` } type configV6 struct { Version string `json:"version"` Aliases map[string]string `json:"alias"` Hosts map[string]hostConfigV6 `json:"hosts"` } // newConfigV6 - new config version '6'. func newConfigV6() *configV6 { conf := new(configV6) conf.Version = "6" conf.Aliases = make(map[string]string) conf.Hosts = make(map[string]hostConfigV6) return conf } // ///////////////// Config V6 /////////////////// // hostConfig configuration of a host - version '7'. type hostConfigV7 struct { URL string `json:"url"` AccessKey string `json:"accessKey"` SecretKey string `json:"secretKey"` API string `json:"api"` } // configV7 config version. type configV7 struct { Version string `json:"version"` Hosts map[string]hostConfigV7 `json:"hosts"` } // newConfigV7 - new config version '7'. func newConfigV7() *configV7 { cfg := new(configV7) cfg.Version = "7" cfg.Hosts = make(map[string]hostConfigV7) return cfg } func (c *configV7) loadDefaults() { // MinIO server running locally. c.setHost("local", hostConfigV7{ URL: "http://localhost:9000", AccessKey: "", SecretKey: "", API: "S3v4", }) // Amazon S3 cloud storage service. c.setHost("s3", hostConfigV7{ URL: "https://s3.amazonaws.com", AccessKey: defaultAccessKey, SecretKey: defaultSecretKey, API: "S3v4", }) // Google cloud storage service. c.setHost("gcs", hostConfigV7{ URL: "https://storage.googleapis.com", AccessKey: defaultAccessKey, SecretKey: defaultSecretKey, API: "S3v2", }) // MinIO anonymous server for demo. c.setHost("play", hostConfigV7{ URL: "https://play.min.io", AccessKey: "", SecretKey: "", API: "S3v4", }) // MinIO demo server with public secret and access keys. c.setHost("player", hostConfigV7{ URL: "https://play.min.io:9002", AccessKey: "Q3AM3UQ867SPQQA43P2F", SecretKey: "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", API: "S3v4", }) // MinIO public download service. c.setHost("dl", hostConfigV7{ URL: "https://dl.min.io:9000", AccessKey: "", SecretKey: "", API: "S3v4", }) } // SetHost sets host config if not empty. func (c *configV7) setHost(alias string, cfg hostConfigV7) { if _, ok := c.Hosts[alias]; !ok { c.Hosts[alias] = cfg } } // ///////////////// Config V8 /////////////////// // configV8 config version. // hostConfig configuration of a host. type hostConfigV8 struct { URL string `json:"url"` AccessKey string `json:"accessKey"` SecretKey string `json:"secretKey"` API string `json:"api"` } type configV8 struct { Version string `json:"version"` Hosts map[string]hostConfigV8 `json:"hosts"` } // newConfigV8 - new config version. func newConfigV8() *configV8 { cfg := new(configV8) cfg.Version = globalMCConfigVersion cfg.Hosts = make(map[string]hostConfigV8) return cfg } // SetHost sets host config if not empty. func (c *configV8) setHost(alias string, cfg hostConfigV8) { if _, ok := c.Hosts[alias]; !ok { c.Hosts[alias] = cfg } } // load default values for missing entries. func (c *configV8) loadDefaults() { // MinIO server running locally. c.setHost("local", hostConfigV8{ URL: "http://localhost:9000", AccessKey: "", SecretKey: "", API: "S3v4", }) // Amazon S3 cloud storage service. c.setHost("s3", hostConfigV8{ URL: "https://s3.amazonaws.com", AccessKey: defaultAccessKey, SecretKey: defaultSecretKey, API: "S3v4", }) // Google cloud storage service. c.setHost("gcs", hostConfigV8{ URL: "https://storage.googleapis.com", AccessKey: defaultAccessKey, SecretKey: defaultSecretKey, API: "S3v2", }) // MinIO anonymous server for demo. c.setHost("play", hostConfigV8{ URL: "https://play.min.io", AccessKey: "Q3AM3UQ867SPQQA43P2F", SecretKey: "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", API: "S3v4", }) } /////////////////// Config V9 /////////////////// // hostConfig configuration of a host. type hostConfigV9 struct { URL string `json:"url"` AccessKey string `json:"accessKey"` SecretKey string `json:"secretKey"` SessionToken string `json:"sessionToken,omitempty"` API string `json:"api"` Lookup string `json:"lookup"` } // configV8 config version. type configV9 struct { Version string `json:"version"` Hosts map[string]hostConfigV9 `json:"hosts"` } func newConfigV9() *configV9 { cfg := new(configV9) cfg.Version = "9" cfg.Hosts = make(map[string]hostConfigV9) return cfg } /////////////////// Config V10 /////////////////// // RESERVED FOR FUTURE minio-client-0.0~20250403/cmd/config-utils.go000066400000000000000000000046311477450377600205030ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "strings" var validAPIs = []string{"S3v4", "S3v2"} const ( accessKeyMinLen = 3 secretKeyMinLen = 8 ) // isValidAccessKey - validate access key for right length. func isValidAccessKey(accessKey string) bool { if accessKey == "" { return true } return len(accessKey) >= accessKeyMinLen } // isValidSecretKey - validate secret key for right length. func isValidSecretKey(secretKey string) bool { if secretKey == "" { return true } return len(secretKey) >= secretKeyMinLen } // trimTrailingSeparator - Remove trailing separator. func trimTrailingSeparator(hostURL string) string { separator := string(newClientURL(hostURL).Separator) return strings.TrimSuffix(hostURL, separator) } // isValidHostURL - validate input host url. func isValidHostURL(hostURL string) (ok bool) { if strings.TrimSpace(hostURL) != "" { url := newClientURL(hostURL) if url.Scheme == "https" || url.Scheme == "http" { if url.Path == "/" { ok = true } } } return ok } // isValidAPI - Validates if API signature string of supported type. func isValidAPI(api string) (ok bool) { switch strings.ToLower(api) { case "s3v2", "s3v4": ok = true } return ok } // isValidLookup - validates if bucket lookup is of valid type func isValidLookup(lookup string) (ok bool) { l := strings.ToLower(strings.TrimSpace(lookup)) for _, v := range []string{"dns", "path", "auto"} { if l == v { return true } } return false } // isValidPath - validates the alias path config func isValidPath(path string) (ok bool) { l := strings.ToLower(strings.TrimSpace(path)) for _, v := range []string{"on", "off", "auto"} { if l == v { return true } } return false } minio-client-0.0~20250403/cmd/config-utils_test.go000066400000000000000000000045651477450377600215500ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "testing" // Tests valid host URL functionality. func TestValidHostURL(t *testing.T) { testCases := []struct { hostURL string isHost bool }{ { hostURL: "https://localhost:9000", isHost: true, }, { hostURL: "/", isHost: false, }, } for _, testCase := range testCases { isHost := isValidHostURL(testCase.hostURL) if testCase.isHost != isHost { t.Fatalf("Expected %t, got %t", testCase.isHost, isHost) } } } func TestIsValidAPI(t *testing.T) { equalAssert(isValidAPI("s3V2"), true, t) equalAssert(isValidAPI("S3v2"), true, t) equalAssert(isValidAPI("s3"), false, t) } func equalAssert(ok1, ok2 bool, t *testing.T) { if ok1 != ok2 { t.Errorf("Expected %t, got %t", ok2, ok1) } } // Tests valid and invalid secret keys. func TestValidSecretKeys(t *testing.T) { equalAssert(isValidSecretKey("aaa"), false, t) equalAssert(isValidSecretKey(""), true, t) equalAssert(isValidSecretKey("password"), true, t) equalAssert(isValidSecretKey("password%%"), true, t) equalAssert(isValidSecretKey("BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF"), true, t) } // Tests valid and invalid access keys. func TestValidAccessKeys(t *testing.T) { equalAssert(isValidAccessKey("aa"), false, t) equalAssert(isValidAccessKey(""), true, t) equalAssert(isValidAccessKey("adm"), true, t) equalAssert(isValidAccessKey("admin"), true, t) equalAssert(isValidAccessKey("$$%%%%%3333"), true, t) equalAssert(isValidAccessKey("c67W2-r4MAyAYScRl"), true, t) equalAssert(isValidAccessKey("EXOb76bfeb1234562iu679f11588"), true, t) equalAssert(isValidAccessKey("BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF"), true, t) } minio-client-0.0~20250403/cmd/config-v10.go000066400000000000000000000101401477450377600177410ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "sync" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/quick" ) const ( defaultAccessKey = "YOUR-ACCESS-KEY-HERE" defaultSecretKey = "YOUR-SECRET-KEY-HERE" ) var ( // set once during first load. cacheCfgV10 *configV10 // All access to mc config file should be synchronized. cfgMutex = &sync.RWMutex{} ) // aliasConfig configuration of an alias. type aliasConfigV10 struct { URL string `json:"url"` AccessKey string `json:"accessKey"` SecretKey string `json:"secretKey"` SessionToken string `json:"sessionToken,omitempty"` API string `json:"api"` Path string `json:"path"` License string `json:"license,omitempty"` APIKey string `json:"apiKey,omitempty"` Src string `json:"src,omitempty"` } // configV10 config version. type configV10 struct { Version string `json:"version"` Aliases map[string]aliasConfigV10 `json:"aliases"` } // newConfigV10 - new config version. func newConfigV10() *configV10 { cfg := new(configV10) cfg.Version = globalMCConfigVersion cfg.Aliases = make(map[string]aliasConfigV10) return cfg } // SetAlias sets host config if not empty. func (c *configV10) setAlias(alias string, cfg aliasConfigV10) { if _, ok := c.Aliases[alias]; !ok { c.Aliases[alias] = cfg } } // load default values for missing entries. func (c *configV10) loadDefaults() { // MinIO server running locally. c.setAlias("local", aliasConfigV10{ URL: "http://localhost:9000", AccessKey: "", SecretKey: "", API: "S3v4", Path: "auto", }) // Amazon S3 cloud storage service. c.setAlias("s3", aliasConfigV10{ URL: "https://s3.amazonaws.com", AccessKey: defaultAccessKey, SecretKey: defaultSecretKey, API: "S3v4", Path: "dns", }) // Google cloud storage service. c.setAlias("gcs", aliasConfigV10{ URL: "https://storage.googleapis.com", AccessKey: defaultAccessKey, SecretKey: defaultSecretKey, API: "S3v2", Path: "dns", }) // MinIO anonymous server for demo. c.setAlias("play", aliasConfigV10{ URL: "https://play.min.io", AccessKey: "Q3AM3UQ867SPQQA43P2F", SecretKey: "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", API: "S3v4", Path: "auto", }) } // loadConfigV10 - loads a new config. func loadConfigV10() (*configV10, *probe.Error) { cfgMutex.RLock() defer cfgMutex.RUnlock() // If already cached, return the cached value. if cacheCfgV10 != nil { return cacheCfgV10, nil } if !isMcConfigExists() { return nil, errInvalidArgument().Trace() } // Initialize a new config loader. qc, e := quick.NewConfig(newConfigV10(), nil) if e != nil { return nil, probe.NewError(e) } // Load config at configPath, fails if config is not // accessible, malformed or version missing. if e = qc.Load(mustGetMcConfigPath()); e != nil { return nil, probe.NewError(e) } cfgV10 := qc.Data().(*configV10) // Cache config. cacheCfgV10 = cfgV10 // Success. return cfgV10, nil } // saveConfigV10 - saves an updated config. func saveConfigV10(cfgV10 *configV10) *probe.Error { cfgMutex.Lock() defer cfgMutex.Unlock() qs, e := quick.NewConfig(cfgV10, nil) if e != nil { return probe.NewError(e) } // update the cache. cacheCfgV10 = cfgV10 e = qs.Save(mustGetMcConfigPath()) if e != nil { return probe.NewError(e).Trace(mustGetMcConfigPath()) } return nil } minio-client-0.0~20250403/cmd/config-validate.go000066400000000000000000000042161477450377600211330ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strings" ) // Check if version of the config is valid func validateConfigVersion(config *configV10) (bool, string) { if config.Version != globalMCConfigVersion { return false, fmt.Sprintf("Config version '%s' does not match mc config version '%s', please update your binary.\n", config.Version, globalMCConfigVersion) } return true, "" } // Verifies the config file of the MinIO Client func validateConfigFile(config *configV10) (bool, []string) { ok, err := validateConfigVersion(config) validationSuccessful := true var errors []string if !ok { validationSuccessful = false errors = append(errors, err) } aliases := config.Aliases for _, aliasConfig := range aliases { aliasConfigHealthOk, aliasErrors := validateConfigHost(aliasConfig) if !aliasConfigHealthOk { validationSuccessful = false errors = append(errors, aliasErrors...) } } return validationSuccessful, errors } func validateConfigHost(host aliasConfigV10) (bool, []string) { validationSuccessful := true var hostErrors []string if !isValidAPI(strings.ToLower(host.API)) { validationSuccessful = false hostErrors = append(hostErrors, errInvalidAPISignature(host.API, host.URL).ToGoError().Error()) } if !isValidHostURL(host.URL) { validationSuccessful = false hostErrors = append(hostErrors, errInvalidURL(host.URL).ToGoError().Error()) } return validationSuccessful, hostErrors } minio-client-0.0~20250403/cmd/config.go000066400000000000000000000233421477450377600173450ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "fmt" "net/url" "os" "path/filepath" "regexp" "runtime" "strings" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/env" "github.com/mitchellh/go-homedir" ) // mcCustomConfigDir contains the whole path to config dir. Only access via get/set functions. var mcCustomConfigDir string // setMcConfigDir - set a custom MinIO Client config folder. func setMcConfigDir(configDir string) { mcCustomConfigDir = configDir } // getMcConfigDir - construct MinIO Client config folder. func getMcConfigDir() (string, *probe.Error) { if mcCustomConfigDir != "" { return mcCustomConfigDir, nil } homeDir, e := homedir.Dir() if e != nil { return "", probe.NewError(e) } configDir := filepath.Join(homeDir, defaultMCConfigDir()) return configDir, nil } // Return default default mc config directory. // Generally you want to use getMcConfigDir which returns custom overrides. func defaultMCConfigDir() string { if runtime.GOOS == "windows" { // For windows the path is slightly different cmd := filepath.Base(os.Args[0]) if strings.HasSuffix(strings.ToLower(cmd), ".exe") { cmd = cmd[:strings.LastIndex(cmd, ".")] } return fmt.Sprintf("%s\\", cmd) } return fmt.Sprintf(".%s/", filepath.Base(os.Args[0])) } // mustGetMcConfigDir - construct MinIO Client config folder or fail func mustGetMcConfigDir() (configDir string) { configDir, err := getMcConfigDir() fatalIf(err.Trace(), "Unable to get mcConfigDir.") return configDir } // createMcConfigDir - create MinIO Client config folder func createMcConfigDir() *probe.Error { p, err := getMcConfigDir() if err != nil { return err.Trace() } if e := os.MkdirAll(p, 0o700); e != nil { return probe.NewError(e) } return nil } // getMcConfigPath - construct MinIO Client configuration path func getMcConfigPath() (string, *probe.Error) { if mcCustomConfigDir != "" { return filepath.Join(mcCustomConfigDir, globalMCConfigFile), nil } dir, err := getMcConfigDir() if err != nil { return "", err.Trace() } return filepath.Join(dir, globalMCConfigFile), nil } // mustGetMcConfigPath - similar to getMcConfigPath, ignores errors func mustGetMcConfigPath() string { path, err := getMcConfigPath() fatalIf(err.Trace(), "Unable to get mcConfigPath.") return path } // newMcConfig - initializes a new version '10' config. func newMcConfig() *configV10 { cfg := newConfigV10() cfg.loadDefaults() return cfg } // loadMcConfigCached - returns loadMcConfig with a closure for config cache. func loadMcConfigFactory() func() (*configV10, *probe.Error) { // Load once and cache in a closure. cfgCache, err := loadConfigV10() // loadMcConfig - reads configuration file and returns config. return func() (*configV10, *probe.Error) { return cfgCache, err } } // loadMcConfig - returns configuration, initialized later. var loadMcConfig func() (*configV10, *probe.Error) // saveMcConfig - saves configuration file and returns error if any. func saveMcConfig(config *configV10) *probe.Error { if config == nil { return errInvalidArgument().Trace() } err := createMcConfigDir() if err != nil { return err.Trace(mustGetMcConfigDir()) } // Save the config. if err := saveConfigV10(config); err != nil { return err.Trace(mustGetMcConfigPath()) } // Refresh the config cache. loadMcConfig = loadMcConfigFactory() return nil } // isMcConfigExists returns err if config doesn't exist. func isMcConfigExists() bool { configFile, err := getMcConfigPath() if err != nil { return false } if _, e := os.Stat(configFile); e != nil { return false } return true } // cleanAlias removes any forbidden trailing slashes or backslashes // before any validation to avoid annoying mc complaints. func cleanAlias(s string) string { s = strings.TrimSuffix(s, "/") s = strings.TrimSuffix(s, "\\") return s } // isValidAlias - Check if alias valid. func isValidAlias(alias string) bool { return regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9-_]*$").MatchString(alias) } // getAliasConfig retrieves host specific configuration such as access keys, signature type. func getAliasConfig(alias string) (*aliasConfigV10, *probe.Error) { mcCfg, err := loadMcConfig() if err != nil { return nil, err.Trace(alias) } // if host is exact return quickly. if _, ok := mcCfg.Aliases[alias]; ok { hostCfg := mcCfg.Aliases[alias] hostCfg.Src = mustGetMcConfigPath() return &hostCfg, nil } // return error if cannot be matched. return nil, errNoMatchingHost(alias).Trace(alias) } // mustGetHostConfig retrieves host specific configuration such as access keys, signature type. func mustGetHostConfig(alias string) *aliasConfigV10 { // look for it in the environment variable first. aliasCfg, _ := expandAliasFromEnv(env.Get(mcEnvHostPrefix+alias, "")) // If alias is not found, // look for it in the customized configuration. if aliasCfg == nil { aliasCfg = aliasToConfigMap[alias] } if aliasCfg == nil { aliasCfg, _ = getAliasConfig(alias) } return aliasCfg } var ( hostKeys = regexp.MustCompile("^(https?://)(.*?):(.*)@(.*?)$") hostKeyTokens = regexp.MustCompile("^(https?://)(.*?):(.*?):(.*)@(.*?)$") ) // parse url usually obtained from env. func parseEnvURLStr(envURL string) (*url.URL, string, string, string, *probe.Error) { var accessKey, secretKey, sessionToken string var parsedURL string if hostKeyTokens.MatchString(envURL) { parts := hostKeyTokens.FindStringSubmatch(envURL) if len(parts) != 6 { return nil, "", "", "", errInvalidArgument().Trace(envURL) } accessKey = parts[2] secretKey = parts[3] sessionToken = parts[4] parsedURL = fmt.Sprintf("%s%s", parts[1], parts[5]) } else if hostKeys.MatchString(envURL) { parts := hostKeys.FindStringSubmatch(envURL) if len(parts) != 5 { return nil, "", "", "", errInvalidArgument().Trace(envURL) } accessKey = parts[2] secretKey = parts[3] parsedURL = fmt.Sprintf("%s%s", parts[1], parts[4]) } var u *url.URL var e error if parsedURL != "" { u, e = url.Parse(parsedURL) } else { u, e = url.Parse(envURL) } if e != nil { return nil, "", "", "", probe.NewError(e) } // Look for if URL has invalid values and return error. if (u.Scheme != "http" && u.Scheme != "https") || (u.Path != "/" && u.Path != "") || u.Opaque != "" || u.ForceQuery || u.RawQuery != "" || u.Fragment != "" { return nil, "", "", "", errInvalidArgument().Trace(u.String()) } if accessKey == "" && secretKey == "" { if u.User != nil { accessKey = u.User.Username() secretKey, _ = u.User.Password() } } return u, accessKey, secretKey, sessionToken, nil } const ( mcEnvHostPrefix = "MC_HOST_" mcEnvConfigFile = "MC_CONFIG_ENV_FILE" ) var aliasToConfigMap = make(map[string]*aliasConfigV10) func readAliasesFromFile(envConfigFile string) *probe.Error { r, e := os.Open(envConfigFile) if e != nil { return probe.NewError(e).Trace(envConfigFile) } defer r.Close() scanner := bufio.NewScanner(r) for scanner.Scan() { envLine := scanner.Text() strs := strings.SplitN(envLine, "=", 2) if len(strs) != 2 { return probe.NewError(fmt.Errorf("parsing error at %s", envLine)).Trace(envConfigFile) } alias := strings.TrimPrefix(strs[0], mcEnvHostPrefix) if len(alias) == 0 { return probe.NewError(fmt.Errorf("parsing error at %s", envLine)).Trace(envConfigFile) } aliasConfig, err := expandAliasFromEnv(strs[1]) if err != nil { return err.Trace(envLine) } aliasConfig.Src = envConfigFile aliasToConfigMap[alias] = aliasConfig } if e := scanner.Err(); e != nil { return probe.NewError(e).Trace(envConfigFile) } return nil } func expandAliasFromEnv(envURL string) (*aliasConfigV10, *probe.Error) { u, accessKey, secretKey, sessionToken, err := parseEnvURLStr(envURL) if err != nil { return nil, err.Trace(envURL) } return &aliasConfigV10{ URL: u.String(), API: "S3v4", AccessKey: accessKey, SecretKey: secretKey, SessionToken: sessionToken, Src: "env", }, nil } // expandAlias expands aliased URL if any match is found, returns as is otherwise. func expandAlias(aliasedURL string) (alias, urlStr string, aliasCfg *aliasConfigV10, err *probe.Error) { // Extract alias from the URL. alias, path := url2Alias(aliasedURL) if env.IsSet(mcEnvHostPrefix + alias) { aliasCfg, err = expandAliasFromEnv(env.Get(mcEnvHostPrefix+alias, "")) if err != nil { return "", "", nil, err.Trace(aliasedURL) } return alias, urlJoinPath(aliasCfg.URL, path), aliasCfg, nil } aliasCfg = aliasToConfigMap[alias] if aliasCfg != nil { return alias, urlJoinPath(aliasCfg.URL, path), aliasCfg, nil } // Find the matching alias entry and expand the URL. if aliasCfg = mustGetHostConfig(alias); aliasCfg != nil { return alias, urlJoinPath(aliasCfg.URL, path), aliasCfg, nil } return "", aliasedURL, nil, nil // No matching entry found. Return original URL as is. } // mustExpandAlias expands aliased URL if any match is found, returns as is otherwise. func mustExpandAlias(aliasedURL string) (alias, urlStr string, aliasCfg *aliasConfigV10) { alias, urlStr, aliasCfg, _ = expandAlias(aliasedURL) return alias, urlStr, aliasCfg } minio-client-0.0~20250403/cmd/config_test.go000066400000000000000000000073141477450377600204050ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "testing" // Tests valid host URL functionality. func TestParseEnvURLStr(t *testing.T) { testCases := []struct { hostURL string accessKey string secretKey string sessionToken string hostname string port string }{ { hostURL: "https://minio:minio1#23@localhost:9000", accessKey: "minio", secretKey: "minio1#23", hostname: "localhost", port: "9000", }, { hostURL: "https://minio:minio123@@localhost:9000", accessKey: "minio", secretKey: "minio123@", hostname: "localhost", port: "9000", }, { hostURL: "https://minio:minio@123@@localhost:9000", accessKey: "minio", secretKey: "minio@123@", hostname: "localhost", port: "9000", }, { hostURL: "https://localhost:9000", accessKey: "", secretKey: "", hostname: "localhost", port: "9000", }, { hostURL: "https://minio:minio123:token@localhost:9000", accessKey: "minio", secretKey: "minio123", sessionToken: "token", hostname: "localhost", port: "9000", }, { hostURL: "https://minio:minio@123:token@@localhost:9000", accessKey: "minio", secretKey: "minio@123", sessionToken: "token@", hostname: "localhost", port: "9000", }, { hostURL: "https://minio@localhost:9000", accessKey: "minio", hostname: "localhost", port: "9000", }, { hostURL: "https://minio:@localhost:9000", accessKey: "minio", hostname: "localhost", port: "9000", }, { hostURL: "https://:@localhost:9000", hostname: "localhost", port: "9000", }, { hostURL: "https://:minio123@localhost:9000", hostname: "localhost", secretKey: "minio123", port: "9000", }, { hostURL: "https://:minio123:token@localhost:9000", hostname: "localhost", secretKey: "minio123", sessionToken: "token", port: "9000", }, { hostURL: "https://:minio123:@localhost:9000", hostname: "localhost", secretKey: "minio123", port: "9000", }, } for _, testCase := range testCases { t.Run("", func(t *testing.T) { url, ak, sk, token, err := parseEnvURLStr(testCase.hostURL) if testCase.accessKey != ak { t.Fatalf("Expected %s, got %s", testCase.accessKey, ak) } if testCase.secretKey != sk { t.Fatalf("Expected %s, got %s", testCase.secretKey, sk) } if testCase.sessionToken != token { t.Fatalf("Expected %s, got %s", testCase.sessionToken, token) } if testCase.hostname != url.Hostname() { t.Fatalf("Expected %s, got %s", testCase.hostname, url.Hostname()) } if testCase.port != url.Port() { t.Fatalf("Expected %s, got %s", testCase.port, url.Port()) } if err != nil { t.Fatalf("Expected test to pass. Failed with err %s", err) } }) } } func TestParseEnvURLStrInvalid(t *testing.T) { _, _, _, _, err := parseEnvURLStr("") if err == nil { t.Fatalf("Expected failure") } } minio-client-0.0~20250403/cmd/cors-get.go000066400000000000000000000044021477450377600176170ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/pkg/v3/console" ) var corsGetCmd = cli.Command{ Name: "get", Usage: "get a bucket CORS configuration", Action: mainCorsGet, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS/BUCKET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get the CORS configuration for the bucket 'mybucket': {{.Prompt}} {{.HelpName}} myminio/mybucket `, } // checkCorsGetSyntax - validate all the passed arguments func checkCorsGetSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainCorsGet is the handle for "mc cors get" command. func mainCorsGet(ctx *cli.Context) error { checkCorsGetSyntax(ctx) console.SetColor("CorsMessage", color.New(color.FgGreen)) console.SetColor("CorsNotFound", color.New(color.FgYellow)) // args[0] is the ALIAS/BUCKET argument. args := ctx.Args() urlStr := args.Get(0) client, err := newClient(urlStr) fatalIf(err.Trace(urlStr), "Unable to initialize client for "+urlStr) corsCfg, err := client.GetBucketCors(globalContext) fatalIf(err.Trace(urlStr), "Unable to get bucket CORS configuration for "+urlStr) status := "success" if corsCfg == nil { status = "not found" } printMsg(corsMessage{ op: "get", Status: status, CorsCfg: corsCfg, }) return nil } minio-client-0.0~20250403/cmd/cors-main.go000066400000000000000000000022631477450377600177670ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var corsSubcommands = []cli.Command{ corsSetCmd, corsGetCmd, corsRemoveCmd, } var corsCmd = cli.Command{ Name: "cors", Usage: "manage bucket CORS configuration", Action: mainCors, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: corsSubcommands, } func mainCors(ctx *cli.Context) error { commandNotFound(ctx, corsSubcommands) return nil } minio-client-0.0~20250403/cmd/cors-remove.go000066400000000000000000000042151477450377600203370ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/pkg/v3/console" ) var corsRemoveCmd = cli.Command{ Name: "remove", Usage: "remove a bucket CORS configuration", Action: mainCorsRemove, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS/BUCKET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove the CORS configuration for the bucket 'mybucket': {{.Prompt}} {{.HelpName}} myminio/mybucket `, } // checkCorsRemoveSyntax - validate all the passed arguments func checkCorsRemoveSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainCorsRemove is the handle for "mc cors remove" command. func mainCorsRemove(ctx *cli.Context) error { checkCorsRemoveSyntax(ctx) console.SetColor("CorsMessage", color.New(color.FgGreen)) // args[0] is the ALIAS/BUCKET argument. args := ctx.Args() urlStr := args.Get(0) client, err := newClient(urlStr) fatalIf(err.Trace(urlStr), "Unable to initialize client for "+urlStr) err = client.DeleteBucketCors(globalContext) fatalIf(err.Trace(urlStr), "Unable to remove bucket CORS configuration for "+urlStr) printMsg(corsMessage{ op: "remove", Status: "success", }) return nil } minio-client-0.0~20250403/cmd/cors-set.go000066400000000000000000000073021477450377600176350ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "io" "os" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/cors" "github.com/minio/pkg/v3/console" ) var corsSetCmd = cli.Command{ Name: "set", Usage: "set a bucket CORS configuration", Action: mainCorsSet, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS/BUCKET CORSFILE CORSFILE: Path to the XML file containing the CORS configuration. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Set the CORS configuration for the bucket 'mybucket': {{.Prompt}} {{.HelpName}} myminio/mybucket /path/to/cors.xml 2. Set the CORS configuration for the bucket 'mybucket' using stdin: {{.Prompt}} {{.HelpName}} myminio/mybucket - `, } // corsMessage container for output message. type corsMessage struct { op string Status string `json:"status"` Message string `json:"message,omitempty"` CorsCfg *cors.Config `json:"cors,omitempty"` } func (c corsMessage) String() string { switch c.op { case "get": if c.CorsCfg == nil { return console.Colorize("CorsNotFound", "No bucket CORS configuration found.") } corsXML, e := c.CorsCfg.ToXML() fatalIf(probe.NewError(e), "Unable to marshal to XML.") return string(corsXML) case "set": return console.Colorize("CorsMessage", "Set bucket CORS config successfully.") case "remove": return console.Colorize("CorsMessage", "Removed bucket CORS config successfully.") } return "" } func (c corsMessage) JSON() string { jsonBytes, e := json.MarshalIndent(&c, "", " ") fatalIf(probe.NewError(e), "Unable to marshal to JSON.") return string(jsonBytes) } // checkCorsSetSyntax - validate all the passed arguments func checkCorsSetSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainCorsSet is the handle for "mc cors set" command. func mainCorsSet(ctx *cli.Context) error { checkCorsSetSyntax(ctx) console.SetColor("CorsMessage", color.New(color.FgGreen)) // args[0] is the ALIAS/BUCKET argument. args := ctx.Args() urlStr := args.Get(0) // args[1] is the CORSFILE which is a local file, or in the case of "-", stdin. var e error in := os.Stdin if f := args.Get(1); f != "-" { in, e = os.Open(f) fatalIf(probe.NewError(e).Trace(args...), "Unable to open bucket CORS configuration file.") defer in.Close() } corsXML, e := io.ReadAll(in) fatalIf(probe.NewError(e).Trace(args...), "Unable to read bucket CORS configuration file.") client, err := newClient(urlStr) fatalIf(err.Trace(urlStr), "Unable to initialize client for "+urlStr) fatalIf(client.SetBucketCors(globalContext, corsXML), "Unable to set bucket CORS configuration for "+urlStr) printMsg(corsMessage{ op: "set", Status: "success", }) return nil } minio-client-0.0~20250403/cmd/cp-main.go000066400000000000000000000422151477450377600174240ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "fmt" "io" "path/filepath" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) // cp command flags. var ( cpFlags = []cli.Flag{ cli.StringFlag{ Name: "rewind", Usage: "roll back object(s) to current version at specified time", }, cli.StringFlag{ Name: "version-id, vid", Usage: "select an object version to copy", }, cli.BoolFlag{ Name: "recursive, r", Usage: "copy recursively", }, cli.StringFlag{ Name: "older-than", Usage: "copy objects older than value in duration string (e.g. 7d10h31s)", }, cli.StringFlag{ Name: "newer-than", Usage: "copy objects newer than value in duration string (e.g. 7d10h31s)", }, cli.StringFlag{ Name: "storage-class, sc", Usage: "set storage class for new object(s) on target", }, cli.StringFlag{ Name: "attr", Usage: "add custom metadata for the object", }, cli.BoolFlag{ Name: "preserve, a", Usage: "preserve filesystem attributes (mode, ownership, timestamps)", }, cli.BoolFlag{ Name: "disable-multipart", Usage: "disable multipart upload feature", }, cli.BoolFlag{ Name: "md5", Usage: "force all upload(s) to calculate md5sum checksum", Hidden: true, }, cli.StringFlag{ Name: "tags", Usage: "apply one or more tags to the uploaded objects", }, cli.StringFlag{ Name: rmFlag, Usage: "retention mode to be applied on the object (governance, compliance)", }, cli.StringFlag{ Name: rdFlag, Usage: "retention duration for the object in d days or y years", }, cli.StringFlag{ Name: lhFlag, Usage: "apply legal hold to the copied object (on, off)", }, cli.BoolFlag{ Name: "zip", Usage: "Extract from remote zip file (MinIO server source only)", }, checksumFlag, } ) var ( rmFlag = "retention-mode" rdFlag = "retention-duration" lhFlag = "legal-hold" ) // ErrInvalidMetadata reflects invalid metadata format var ErrInvalidMetadata = errors.New("specified metadata should be of form key1=value1;key2=value2;... and so on") // Copy command. var cpCmd = cli.Command{ Name: "cp", Usage: "copy objects", Action: mainCopy, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(append(cpFlags, encFlags...), globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] SOURCE [SOURCE...] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} ENVIRONMENT VARIABLES: MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key). MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key). EXAMPLES: 01. Copy a list of objects from local file system to Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} Music/*.ogg s3/jukebox/ 02. Copy a folder recursively from MinIO cloud storage to Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} --recursive play/mybucket/myfolder/ s3/mybucket/ 03. Copy multiple local folders recursively to MinIO cloud storage. {{.Prompt}} {{.HelpName}} --recursive backup/2014/ backup/2015/ play/archive/ 04. Copy a bucket recursively from aliased Amazon S3 cloud storage to local filesystem on Windows. {{.Prompt}} {{.HelpName}} --recursive s3\documents\2014\ C:\Backups\2014 05. Copy files older than 7 days and 10 hours from MinIO cloud storage to Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} --older-than 7d10h play/mybucket/myfolder/ s3/mybucket/ 06. Copy files newer than 7 days and 10 hours from MinIO cloud storage to a local path. {{.Prompt}} {{.HelpName}} --newer-than 7d10h play/mybucket/myfolder/ ~/latest/ 07. Copy an object with name containing unicode characters to Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} 本語 s3/andoria/ 08. Copy a local folder with space separated characters to Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} --recursive 'workdir/documents/May 2014/' s3/miniocloud 09. Copy a folder with encrypted objects recursively from Amazon S3 to MinIO cloud storage using s3 encryption. {{.Prompt}} {{.HelpName}} --recursive --enc-s3 "s3/documents" --enc-s3 "myminio/documents" s3/documents/ myminio/documents/ 10. Copy a folder with encrypted objects recursively from Amazon S3 to MinIO cloud storage. {{.Prompt}} {{.HelpName}} --recursive --enc-c "s3/documents/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" --enc-c "myminio/documents/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5BBB" s3/documents/ myminio/documents/ 11. Copy a list of objects from local file system to MinIO cloud storage with specified metadata, separated by ";" {{.Prompt}} {{.HelpName}} --attr "key1=value1;key2=value2" Music/*.mp4 play/mybucket/ 12. Copy a folder recursively from MinIO cloud storage to Amazon S3 cloud storage with Cache-Control and custom metadata, separated by ";". {{.Prompt}} {{.HelpName}} --attr "Cache-Control=max-age=90000,min-fresh=9000;key1=value1;key2=value2" --recursive play/mybucket/myfolder/ s3/mybucket/ 13. Copy a text file to an object storage and assign REDUCED_REDUNDANCY storage-class to the uploaded object. {{.Prompt}} {{.HelpName}} --storage-class REDUCED_REDUNDANCY myobject.txt play/mybucket 14. Copy a text file to an object storage and preserve the file system attribute as metadata. {{.Prompt}} {{.HelpName}} -a myobject.txt play/mybucket 15. Copy a text file to an object storage with object lock mode set to 'GOVERNANCE' with retention duration 1 day. {{.Prompt}} {{.HelpName}} --retention-mode governance --retention-duration 1d locked.txt play/locked-bucket/ 16. Copy a text file to an object storage with legal-hold enabled. {{.Prompt}} {{.HelpName}} --legal-hold on locked.txt play/locked-bucket/ 17. Copy a text file to an object storage and disable multipart upload feature. {{.Prompt}} {{.HelpName}} --disable-multipart myobject.txt play/mybucket 18. Roll back 10 days in the past to copy the content of 'mybucket' {{.Prompt}} {{.HelpName}} --rewind 10d -r play/mybucket/ /tmp/dest/ 19. Set tags to the uploaded objects {{.Prompt}} {{.HelpName}} -r --tags "category=prod&type=backup" ./data/ play/another-bucket/ `, } // copyMessage container for file copy messages type copyMessage struct { Status string `json:"status"` Source string `json:"source"` Target string `json:"target"` Size int64 `json:"size"` TotalCount int64 `json:"totalCount"` TotalSize int64 `json:"totalSize"` } // String colorized copy message func (c copyMessage) String() string { return console.Colorize("Copy", fmt.Sprintf("`%s` -> `%s`", c.Source, c.Target)) } // JSON jsonified copy message func (c copyMessage) JSON() string { c.Status = "success" copyMessageBytes, e := json.MarshalIndent(c, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(copyMessageBytes) } // Progress - an interface which describes current amount // of data written. type Progress interface { Get() int64 SetTotal(int64) } // ProgressReader can be used to update the progress of // an on-going transfer progress. type ProgressReader interface { io.Reader Progress } // doCopy - Copy a single file from source to destination func doCopy(ctx context.Context, copyOpts doCopyOpts) URLs { if copyOpts.cpURLs.Error != nil { copyOpts.cpURLs.Error = copyOpts.cpURLs.Error.Trace() return copyOpts.cpURLs } sourceAlias := copyOpts.cpURLs.SourceAlias sourceURL := copyOpts.cpURLs.SourceContent.URL targetAlias := copyOpts.cpURLs.TargetAlias targetURL := copyOpts.cpURLs.TargetContent.URL length := copyOpts.cpURLs.SourceContent.Size sourcePath := filepath.ToSlash(filepath.Join(sourceAlias, sourceURL.Path)) if progressReader, ok := copyOpts.pg.(*progressBar); ok { progressReader.SetCaption(copyOpts.cpURLs.SourceContent.URL.String() + ":") } else { targetPath := filepath.ToSlash(filepath.Join(targetAlias, targetURL.Path)) printMsg(copyMessage{ Source: sourcePath, Target: targetPath, Size: length, TotalCount: copyOpts.cpURLs.TotalCount, TotalSize: copyOpts.cpURLs.TotalSize, }) } urls := uploadSourceToTargetURL(ctx, uploadSourceToTargetURLOpts{ urls: copyOpts.cpURLs, progress: copyOpts.pg, encKeyDB: copyOpts.encryptionKeys, preserve: copyOpts.preserve, isZip: copyOpts.isZip, multipartSize: copyOpts.multipartSize, multipartThreads: copyOpts.multipartThreads, updateProgressTotal: copyOpts.updateProgressTotal, ifNotExists: copyOpts.ifNotExists, }) if copyOpts.isMvCmd && urls.Error == nil { rmManager.add(ctx, sourceAlias, sourceURL.String()) } return urls } // doCopyFake - Perform a fake copy to update the progress bar appropriately. func doCopyFake(cpURLs URLs, pg Progress) URLs { if progressReader, ok := pg.(*progressBar); ok { progressReader.Add64(cpURLs.SourceContent.Size) } return cpURLs } func printCopyURLsError(cpURLs *URLs) { // Print in new line and adjust to top so that we // don't print over the ongoing scan bar if !globalQuiet && !globalJSON { console.Eraseline() } if strings.Contains(cpURLs.Error.ToGoError().Error(), " is a folder.") { errorIf(cpURLs.Error.Trace(), "Folder cannot be copied. Please use `...` suffix.") } else { errorIf(cpURLs.Error.Trace(), "Unable to prepare URL for copying.") } } func doCopySession(ctx context.Context, cancelCopy context.CancelFunc, cli *cli.Context, encryptionKeys map[string][]prefixSSEPair, isMvCmd bool) error { var isCopied func(string) bool var totalObjects, totalBytes int64 cpURLsCh := make(chan URLs, 10000) errSeen := false // Store a progress bar or an accounter var pg ProgressReader // Enable progress bar reader only during default mode. if !globalQuiet && !globalJSON { // set up progress bar pg = newProgressBar(totalBytes) } else { pg = newAccounter(totalBytes) } sourceURLs := cli.Args()[:len(cli.Args())-1] targetURL := cli.Args()[len(cli.Args())-1] // Last one is target // Check if the target path has object locking enabled withLock, _ := isBucketLockEnabled(ctx, targetURL) isRecursive := cli.Bool("recursive") olderThan := cli.String("older-than") newerThan := cli.String("newer-than") rewind := cli.String("rewind") versionID := cli.String("version-id") md5, checksum := parseChecksum(cli) if withLock { // The Content-MD5 header is required for any request to upload an object with a retention period configured using Amazon S3 Object Lock. md5, checksum = true, minio.ChecksumNone } go func() { totalBytes := int64(0) opts := prepareCopyURLsOpts{ sourceURLs: sourceURLs, targetURL: targetURL, isRecursive: isRecursive, encKeyDB: encryptionKeys, olderThan: olderThan, newerThan: newerThan, timeRef: parseRewindFlag(rewind), versionID: versionID, isZip: cli.Bool("zip"), } for cpURLs := range prepareCopyURLs(ctx, opts) { if cpURLs.Error != nil { errSeen = true printCopyURLsError(&cpURLs) break } totalBytes += cpURLs.SourceContent.Size pg.SetTotal(totalBytes) totalObjects++ cpURLsCh <- cpURLs } close(cpURLsCh) }() quitCh := make(chan struct{}) statusCh := make(chan URLs) parallel := newParallelManager(statusCh) go func() { gracefulStop := func() { parallel.stopAndWait() close(statusCh) } for { select { case <-quitCh: gracefulStop() return case cpURLs, ok := <-cpURLsCh: if !ok { gracefulStop() return } // Save total count. cpURLs.TotalCount = totalObjects // Save totalSize. cpURLs.TotalSize = totalBytes // Initialize target metadata. cpURLs.TargetContent.Metadata = make(map[string]string) // Initialize target user metadata. cpURLs.TargetContent.UserMetadata = make(map[string]string) // Check and handle storage class if passed in command line args if storageClass := cli.String("storage-class"); storageClass != "" { cpURLs.TargetContent.StorageClass = storageClass } if rm := cli.String(rmFlag); rm != "" { cpURLs.TargetContent.RetentionMode = rm cpURLs.TargetContent.RetentionEnabled = true } if rd := cli.String(rdFlag); rd != "" { cpURLs.TargetContent.RetentionDuration = rd } if lh := cli.String(lhFlag); lh != "" { cpURLs.TargetContent.LegalHold = strings.ToUpper(lh) cpURLs.TargetContent.LegalHoldEnabled = true } if tags := cli.String("tags"); tags != "" { cpURLs.TargetContent.Metadata["X-Amz-Tagging"] = tags } preserve := cli.Bool("preserve") isZip := cli.Bool("zip") if cli.String("attr") != "" { userMetaMap, _ := getMetaDataEntry(cli.String("attr")) for metadataKey, metaDataVal := range userMetaMap { cpURLs.TargetContent.UserMetadata[metadataKey] = metaDataVal } } cpURLs.MD5 = md5 cpURLs.checksum = checksum cpURLs.DisableMultipart = cli.Bool("disable-multipart") // Verify if previously copied, notify progress bar. if isCopied != nil && isCopied(cpURLs.SourceContent.URL.String()) { parallel.queueTask(func() URLs { return doCopyFake(cpURLs, pg) }, 0) } else { // Print the copy resume summary once in start parallel.queueTask(func() URLs { return doCopy(ctx, doCopyOpts{ cpURLs: cpURLs, pg: pg, encryptionKeys: encryptionKeys, isMvCmd: isMvCmd, preserve: preserve, isZip: isZip, }) }, cpURLs.SourceContent.Size) } } } }() var retErr error cpAllFilesErr := true loop: for { select { case <-globalContext.Done(): close(quitCh) cancelCopy() // Receive interrupt notification. if !globalQuiet && !globalJSON { console.Eraseline() } break loop case cpURLs, ok := <-statusCh: // Status channel is closed, we should return. if !ok { break loop } if cpURLs.Error == nil { cpAllFilesErr = false } else { // Set exit status for any copy error retErr = exitStatus(globalErrorExitStatus) // Print in new line and adjust to top so that we // don't print over the ongoing progress bar. if !globalQuiet && !globalJSON { console.Eraseline() } errorIf(cpURLs.Error.Trace(cpURLs.SourceContent.URL.String()), "Failed to copy `%s`.", cpURLs.SourceContent.URL) if isErrIgnored(cpURLs.Error) { cpAllFilesErr = false continue loop } errSeen = true if progressReader, pgok := pg.(*progressBar); pgok { if progressReader.Get() > 0 { writeContSize := (int)(cpURLs.SourceContent.Size) totalPGSize := (int)(progressReader.Total) written := (int)(progressReader.Get()) if totalPGSize > writeContSize && written > writeContSize { progressReader.Set((written - writeContSize)) progressReader.Update() } } } } } } if progressReader, ok := pg.(*progressBar); ok { if errSeen || (cpAllFilesErr && totalObjects > 0) { // We only erase a line if we are displaying a progress bar if !globalQuiet && !globalJSON { console.Eraseline() } } else if progressReader.Get() > 0 { progressReader.Finish() } } else { if accntReader, ok := pg.(*accounter); ok { if errSeen || (cpAllFilesErr && totalObjects > 0) { // We only erase a line if we are displaying a progress bar if !globalQuiet && !globalJSON { console.Eraseline() } } else { printMsg(accntReader.Stat()) } } } // Source has error if errSeen && totalObjects == 0 && retErr == nil { retErr = exitStatus(globalErrorExitStatus) } return retErr } // mainCopy is the entry point for cp command. func mainCopy(cliCtx *cli.Context) error { ctx, cancelCopy := context.WithCancel(globalContext) defer cancelCopy() checkCopySyntax(cliCtx) console.SetColor("Copy", color.New(color.FgGreen, color.Bold)) var err *probe.Error // Parse encryption keys per command. encryptionKeyMap, err := validateAndCreateEncryptionKeys(cliCtx) if err != nil { err.Trace(cliCtx.Args()...) } fatalIf(err, "SSE Error") return doCopySession(ctx, cancelCopy, cliCtx, encryptionKeyMap, false) } type doCopyOpts struct { cpURLs URLs pg ProgressReader encryptionKeys map[string][]prefixSSEPair isMvCmd, preserve, isZip bool updateProgressTotal bool multipartSize string multipartThreads string ifNotExists bool } minio-client-0.0~20250403/cmd/cp-main_contrib.go000066400000000000000000000053751477450377600211520ustar00rootroot00000000000000// MinIO Object Storage (c) 2021 MinIO, Inc. // // Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "net/http" "strings" "github.com/minio/mc/pkg/probe" ) // validate the passed metadataString and populate the map func getMetaDataEntry(metadataString string) (map[string]string, *probe.Error) { metaDataMap := make(map[string]string) r := strings.NewReader(metadataString) type pToken int const ( KEY pToken = iota VALUE ) type pState int const ( NORMAL pState = iota QSTRING DQSTRING ) var key, value strings.Builder writeRune := func(ch rune, pt pToken) { switch pt { case KEY: key.WriteRune(ch) case VALUE: value.WriteRune(ch) default: panic("Invalid parser token type") } } ps := NORMAL pt := KEY p := 0 for ; ; p++ { ch, _, e := r.ReadRune() if e != nil { // eof if ps == QSTRING || ps == DQSTRING || pt == KEY { return nil, probe.NewError(ErrInvalidMetadata) } metaDataMap[http.CanonicalHeaderKey(key.String())] = value.String() return metaDataMap, nil } if ch == '"' { switch ps { case DQSTRING: ps = NORMAL case QSTRING: writeRune(ch, pt) case NORMAL: ps = DQSTRING default: break } continue } if ch == '\'' { switch ps { case QSTRING: ps = NORMAL case DQSTRING: writeRune(ch, pt) case NORMAL: ps = QSTRING default: break } continue } if ch == '=' { if ps == QSTRING || ps == DQSTRING { writeRune(ch, pt) } else if pt == KEY { pt = VALUE } else if pt == VALUE { writeRune(ch, pt) } else { break } continue } if ch == ';' { if ps == QSTRING || ps == DQSTRING { writeRune(ch, pt) } else if pt == KEY { return nil, probe.NewError(ErrInvalidMetadata) } else if pt == VALUE { metaDataMap[http.CanonicalHeaderKey(key.String())] = value.String() key.Reset() value.Reset() pt = KEY } else { break } continue } writeRune(ch, pt) } fatalErr := fmt.Sprintf("Invalid parser state at index: %d", p) panic(fatalErr) } minio-client-0.0~20250403/cmd/cp-main_test.go000066400000000000000000000070711477450377600204640ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "reflect" "testing" ) func TestParseMetaData(t *testing.T) { metaDataCases := []struct { input string output map[string]string err error status bool }{ // success scenario using ; as delimiter {"key1=value1;key2=value2", map[string]string{"Key1": "value1", "Key2": "value2"}, nil, true}, // success scenario using ; as delimiter {"key1=m1=m2,m3=m4;key2=value2", map[string]string{"Key1": "m1=m2,m3=m4", "Key2": "value2"}, nil, true}, // success scenario using = more than once {"Cache-Control=max-age=90000,min-fresh=9000;key1=value1;key2=value2", map[string]string{"Cache-Control": "max-age=90000,min-fresh=9000", "Key1": "value1", "Key2": "value2"}, nil, true}, // using different delimiter, other than '=' between key value {"key1:value1;key2:value2", nil, ErrInvalidMetadata, false}, // using no delimiter {"key1:value1:key2:value2", nil, ErrInvalidMetadata, false}, // success: use value in quotes {"Content-Disposition='form-data; name=\"description\"'", map[string]string{"Content-Disposition": "form-data; name=\"description\""}, nil, true}, // success: use value in double quotes {"Content-Disposition=\"form-data; name='description'\"", map[string]string{"Content-Disposition": "form-data; name='description'"}, nil, true}, // fail: unterminated quote {"Content-Disposition='form-data; name=\"description\"", nil, ErrInvalidMetadata, false}, // fail: unterminated double quote {"Content-Disposition=\"form-data; name='description'", nil, ErrInvalidMetadata, false}, // success: use value and key in quotes {"\"Content-Disposition\"='form-data; name=\"description\"'", map[string]string{"Content-Disposition": "form-data; name=\"description\""}, nil, true}, // success: use value and key in quotes {"\"Content=Disposition;Other key part=this is also key data\"='form-data; name=\"description\"'", map[string]string{"Content=Disposition;Other key part=this is also key data": "form-data; name=\"description\""}, nil, true}, } for idx, testCase := range metaDataCases { metaDatamap, errMeta := getMetaDataEntry(testCase.input) if testCase.status == true { if errMeta != nil { t.Fatalf("Test %d: generated error not matching, expected = `%s`, found = `%s`", idx+1, testCase.err, errMeta) } if !reflect.DeepEqual(metaDatamap, testCase.output) { t.Fatalf("Test %d: generated Map not matching, expected = `%s`, found = `%s`", idx+1, testCase.input, metaDatamap) } } if testCase.status == false { if !reflect.DeepEqual(metaDatamap, testCase.output) { t.Fatalf("Test %d: generated Map not matching, expected = `%s`, found = `%s`", idx+1, testCase.input, metaDatamap) } if errMeta.Cause.Error() != testCase.err.Error() { t.Fatalf("Test %d: generated error not matching, expected = `%s`, found = `%s`", idx+1, testCase.err, errMeta) } } } } minio-client-0.0~20250403/cmd/cp-url-syntax.go000066400000000000000000000046531477450377600206320ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "runtime" "github.com/minio/cli" ) func checkCopySyntax(cliCtx *cli.Context) { if len(cliCtx.Args()) < 2 { showCommandHelpAndExit(cliCtx, 1) // last argument is exit code. } parseChecksum(cliCtx) // extract URLs. URLs := cliCtx.Args() if len(URLs) < 2 { fatalIf(errDummy().Trace(cliCtx.Args()...), "Unable to parse source and target arguments.") } srcURLs := URLs[:len(URLs)-1] tgtURL := URLs[len(URLs)-1] isZip := cliCtx.Bool("zip") versionID := cliCtx.String("version-id") if versionID != "" && len(srcURLs) > 1 { fatalIf(errDummy().Trace(cliCtx.Args()...), "Unable to pass --version flag with multiple copy sources arguments.") } if isZip && cliCtx.String("rewind") != "" { fatalIf(errDummy().Trace(cliCtx.Args()...), "--zip and --rewind cannot be used together") } // Check if bucket name is passed for URL type arguments. url := newClientURL(tgtURL) if url.Host != "" { if url.Path == string(url.Separator) { fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("Target `%s` does not contain bucket name.", tgtURL)) } } if cliCtx.String(rdFlag) != "" && cliCtx.String(rmFlag) == "" { fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("Both object retention flags `--%s` and `--%s` are required.\n", rdFlag, rmFlag)) } if cliCtx.String(rdFlag) == "" && cliCtx.String(rmFlag) != "" { fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("Both object retention flags `--%s` and `--%s` are required.\n", rdFlag, rmFlag)) } // Preserve functionality not supported for windows if cliCtx.Bool("preserve") && runtime.GOOS == "windows" { fatalIf(errInvalidArgument().Trace(), "Permissions are not preserved on windows platform.") } } minio-client-0.0~20250403/cmd/cp-url.go000066400000000000000000000306661477450377600173110ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "path/filepath" "strings" "time" "github.com/minio/mc/pkg/probe" ) type copyURLsType uint8 // NOTE: All the parse rules should reduced to A: Copy(Source, Target). // // * VALID RULES // ======================= // A: copy(f, f) -> copy(f, f) // B: copy(f, d) -> copy(f, d/f) -> []A // C: copy(d1..., d2) -> []copy(f, d2/d1/f) -> []A // D: copy([]f, d) -> []B // * INVALID RULES // ========================= // copy(d, f) // copy(d..., f) // copy([](f|d)..., f) const ( copyURLsTypeInvalid copyURLsType = iota copyURLsTypeA copyURLsTypeB copyURLsTypeC copyURLsTypeD ) // guessCopyURLType guesses the type of clientURL. This approach all allows prepareURL // functions to accurately report failure causes. func guessCopyURLType(ctx context.Context, o prepareCopyURLsOpts) (*copyURLsContent, *probe.Error) { cc := new(copyURLsContent) // Extract alias before fiddling with the clientURL. cc.sourceURL = o.sourceURLs[0] cc.sourceAlias, _, _ = mustExpandAlias(cc.sourceURL) // Find alias and expanded clientURL. cc.targetAlias, cc.targetURL, _ = mustExpandAlias(o.targetURL) if len(o.sourceURLs) == 1 { // 1 Source, 1 Target var err *probe.Error if !o.isRecursive { _, cc.sourceContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.sourceURL, versionID: o.versionID, fileAttr: false, encKeyDB: o.encKeyDB, timeRef: o.timeRef, isZip: o.isZip, ignoreBucketExistsCheck: false}) } else { _, cc.sourceContent, err = firstURL2Stat(ctx, cc.sourceURL, o.timeRef, o.isZip) } if err != nil { cc.copyType = copyURLsTypeInvalid return cc, err } // If recursion is ON, it is type C. // If source is a folder, it is Type C. if cc.sourceContent.Type.IsDir() || o.isRecursive { cc.copyType = copyURLsTypeC return cc, nil } // If target is a folder, it is Type B. var isDir bool isDir, cc.targetContent = isAliasURLDir(ctx, o.targetURL, o.encKeyDB, o.timeRef, o.ignoreBucketExistsCheck) if isDir { cc.copyType = copyURLsTypeB cc.sourceVersionID = cc.sourceContent.VersionID return cc, nil } // else Type A. cc.copyType = copyURLsTypeA cc.sourceVersionID = cc.sourceContent.VersionID return cc, nil } var isDir bool // Multiple source args and target is a folder. It is Type D. isDir, cc.targetContent = isAliasURLDir(ctx, o.targetURL, o.encKeyDB, o.timeRef, o.ignoreBucketExistsCheck) if isDir { cc.copyType = copyURLsTypeD return cc, nil } cc.copyType = copyURLsTypeInvalid return cc, errInvalidArgument().Trace() } // SINGLE SOURCE - Type A: copy(f, f) -> copy(f, f) // prepareCopyURLsTypeA - prepares target and source clientURLs for copying. func prepareCopyURLsTypeA(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) URLs { var err *probe.Error if cc.sourceContent == nil { _, cc.sourceContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.sourceURL, versionID: cc.sourceVersionID, fileAttr: false, encKeyDB: o.encKeyDB, timeRef: time.Time{}, isZip: o.isZip, ignoreBucketExistsCheck: false}) if err != nil { // Source does not exist or insufficient privileges. return URLs{Error: err.Trace(cc.sourceURL)} } } if !cc.sourceContent.Type.IsRegular() { // Source is not a regular file return URLs{Error: errInvalidSource(cc.sourceURL).Trace(cc.sourceURL)} } // All OK.. We can proceed. Type A return makeCopyContentTypeA(cc) } // prepareCopyContentTypeA - makes CopyURLs content for copying. func makeCopyContentTypeA(cc copyURLsContent) URLs { targetContent := ClientContent{URL: *newClientURL(cc.targetURL)} return URLs{ SourceAlias: cc.sourceAlias, SourceContent: cc.sourceContent, TargetAlias: cc.targetAlias, TargetContent: &targetContent, } } // SINGLE SOURCE - Type B: copy(f, d) -> copy(f, d/f) -> A // prepareCopyURLsTypeB - prepares target and source clientURLs for copying. func prepareCopyURLsTypeB(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) URLs { var err *probe.Error if cc.sourceContent == nil { _, cc.sourceContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.sourceURL, versionID: cc.sourceVersionID, fileAttr: false, encKeyDB: o.encKeyDB, timeRef: time.Time{}, isZip: o.isZip, ignoreBucketExistsCheck: o.ignoreBucketExistsCheck}) if err != nil { // Source does not exist or insufficient privileges. return URLs{Error: err.Trace(cc.sourceURL)} } } if !cc.sourceContent.Type.IsRegular() { if cc.sourceContent.Type.IsDir() { return URLs{Error: errSourceIsDir(cc.sourceURL).Trace(cc.sourceURL)} } // Source is not a regular file. return URLs{Error: errInvalidSource(cc.sourceURL).Trace(cc.sourceURL)} } if cc.targetContent == nil { _, cc.targetContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.targetURL, versionID: "", fileAttr: false, encKeyDB: o.encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: o.ignoreBucketExistsCheck}) if err == nil { if !cc.targetContent.Type.IsDir() { return URLs{Error: errInvalidTarget(cc.targetURL).Trace(cc.targetURL)} } } } // All OK.. We can proceed. Type B: source is a file, target is a folder and exists. return makeCopyContentTypeB(cc) } // makeCopyContentTypeB - CopyURLs content for copying. func makeCopyContentTypeB(cc copyURLsContent) URLs { // All OK.. We can proceed. Type B: source is a file, target is a folder and exists. targetURLParse := newClientURL(cc.targetURL) targetURLParse.Path = filepath.ToSlash(filepath.Join(targetURLParse.Path, filepath.Base(cc.sourceContent.URL.Path))) cc.targetURL = targetURLParse.String() return makeCopyContentTypeA(cc) } // SINGLE SOURCE - Type C: copy(d1..., d2) -> []copy(d1/f, d1/d2/f) -> []A // prepareCopyRecursiveURLTypeC - prepares target and source clientURLs for copying. func prepareCopyURLsTypeC(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) <-chan URLs { copyURLsCh := make(chan URLs, 1) returnErrorAndCloseChannel := func(err *probe.Error) chan URLs { copyURLsCh <- URLs{Error: err} close(copyURLsCh) return copyURLsCh } c, err := newClient(cc.sourceURL) if err != nil { return returnErrorAndCloseChannel(err.Trace(cc.sourceURL)) } if cc.targetContent == nil { _, cc.targetContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.targetURL, versionID: "", fileAttr: false, encKeyDB: o.encKeyDB, timeRef: time.Time{}, isZip: o.isZip, ignoreBucketExistsCheck: false}) if err == nil { if !cc.targetContent.Type.IsDir() { return returnErrorAndCloseChannel(errTargetIsNotDir(cc.targetURL).Trace(cc.targetURL)) } } } if cc.sourceContent == nil { _, cc.sourceContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.sourceURL, versionID: "", fileAttr: false, encKeyDB: o.encKeyDB, timeRef: time.Time{}, isZip: o.isZip, ignoreBucketExistsCheck: false}) if err != nil { return returnErrorAndCloseChannel(err.Trace(cc.sourceURL)) } } if cc.sourceContent.Type.IsDir() { // Require --recursive flag if we are copying a directory if !o.isRecursive { return returnErrorAndCloseChannel(errRequiresRecursive(cc.sourceURL).Trace(cc.sourceURL)) } // Check if we are going to copy a directory into itself if isURLContains(cc.sourceURL, cc.targetURL, string(c.GetURL().Separator)) { return returnErrorAndCloseChannel(errCopyIntoSelf(cc.sourceURL).Trace(cc.targetURL)) } } go func(sourceClient Client, cc copyURLsContent, o prepareCopyURLsOpts, copyURLsCh chan URLs) { defer close(copyURLsCh) for sourceContent := range sourceClient.List(ctx, ListOptions{Recursive: o.isRecursive, TimeRef: o.timeRef, ShowDir: DirNone, ListZip: o.isZip}) { if sourceContent.Err != nil { // Listing failed. copyURLsCh <- URLs{Error: sourceContent.Err.Trace(sourceClient.GetURL().String())} continue } if !sourceContent.Type.IsRegular() { // Source is not a regular file. Skip it for copy. continue } // Clone cc newCC := cc newCC.sourceContent = sourceContent // All OK.. We can proceed. Type B: source is a file, target is a folder and exists. copyURLsCh <- makeCopyContentTypeC(newCC, sourceClient.GetURL()) } }(c, cc, o, copyURLsCh) return copyURLsCh } // makeCopyContentTypeC - CopyURLs content for copying. func makeCopyContentTypeC(cc copyURLsContent, sourceClientURL ClientURL) URLs { newSourceURL := cc.sourceContent.URL pathSeparatorIndex := strings.LastIndex(sourceClientURL.Path, string(sourceClientURL.Separator)) newSourceSuffix := filepath.ToSlash(newSourceURL.Path) if pathSeparatorIndex > 1 { sourcePrefix := filepath.ToSlash(sourceClientURL.Path[:pathSeparatorIndex]) newSourceSuffix = strings.TrimPrefix(newSourceSuffix, sourcePrefix) } newTargetURL := urlJoinPath(cc.targetURL, newSourceSuffix) cc.targetURL = newTargetURL return makeCopyContentTypeA(cc) } // MULTI-SOURCE - Type D: copy([](f|d...), d) -> []B // prepareCopyURLsTypeE - prepares target and source clientURLs for copying. func prepareCopyURLsTypeD(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) <-chan URLs { copyURLsCh := make(chan URLs, 1) copyURLsFilterCh := make(chan URLs, 1) go func(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) { defer close(copyURLsFilterCh) for _, sourceURL := range o.sourceURLs { // Clone CC newCC := cc newCC.sourceURL = sourceURL for cpURLs := range prepareCopyURLsTypeC(ctx, newCC, o) { copyURLsFilterCh <- cpURLs } } }(ctx, cc, o) go func() { defer close(copyURLsCh) filter := make(map[string]struct{}) for cpURLs := range copyURLsFilterCh { if cpURLs.Error != nil || cpURLs.TargetContent == nil { copyURLsCh <- cpURLs continue } url := cpURLs.TargetContent.URL.String() _, ok := filter[url] if !ok { filter[url] = struct{}{} copyURLsCh <- cpURLs } } }() return copyURLsCh } type prepareCopyURLsOpts struct { sourceURLs []string targetURL string isRecursive bool encKeyDB map[string][]prefixSSEPair olderThan, newerThan string timeRef time.Time versionID string isZip bool ignoreBucketExistsCheck bool } type copyURLsContent struct { targetContent *ClientContent targetAlias string targetURL string sourceContent *ClientContent sourceAlias string sourceURL string copyType copyURLsType sourceVersionID string } // prepareCopyURLs - prepares target and source clientURLs for copying. func prepareCopyURLs(ctx context.Context, o prepareCopyURLsOpts) chan URLs { copyURLsCh := make(chan URLs) go func(o prepareCopyURLsOpts) { defer close(copyURLsCh) copyURLsContent, err := guessCopyURLType(ctx, o) if err != nil { copyURLsCh <- URLs{Error: errUnableToGuess().Trace(o.sourceURLs...)} return } switch copyURLsContent.copyType { case copyURLsTypeA: copyURLsCh <- prepareCopyURLsTypeA(ctx, *copyURLsContent, o) case copyURLsTypeB: copyURLsCh <- prepareCopyURLsTypeB(ctx, *copyURLsContent, o) case copyURLsTypeC: for cURLs := range prepareCopyURLsTypeC(ctx, *copyURLsContent, o) { copyURLsCh <- cURLs } case copyURLsTypeD: for cURLs := range prepareCopyURLsTypeD(ctx, *copyURLsContent, o) { copyURLsCh <- cURLs } default: copyURLsCh <- URLs{Error: errInvalidArgument().Trace(o.sourceURLs...)} } }(o) finalCopyURLsCh := make(chan URLs) go func() { defer close(finalCopyURLsCh) for cpURLs := range copyURLsCh { if cpURLs.Error != nil { finalCopyURLsCh <- cpURLs continue } // Skip objects older than --older-than parameter if specified if o.olderThan != "" && isOlder(cpURLs.SourceContent.Time, o.olderThan) { continue } // Skip objects newer than --newer-than parameter if specified if o.newerThan != "" && isNewer(cpURLs.SourceContent.Time, o.newerThan) { continue } finalCopyURLsCh <- cpURLs } }() return finalCopyURLsCh } minio-client-0.0~20250403/cmd/diff-main.go000066400000000000000000000165321477450377600177350ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) // diff specific flags. var ( diffFlags = []cli.Flag{} ) // Compute differences in object name, size, and date between two buckets. var diffCmd = cli.Command{ Name: "diff", Usage: "list differences in object name, size, and date between two buckets", Action: mainDiff, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(diffFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] SOURCE TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} DESCRIPTION: Diff only calculates differences in object name, size and time. It *DOES NOT* compare objects' contents. LEGEND: < - object is only in source. > - object is only in destination. ! - newer object is in source. EXAMPLES: 1. Compare a local folder with a folder on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} ~/Photos s3/mybucket/Photos 2. Compare two folders on a local filesystem. {{.Prompt}} {{.HelpName}} ~/Photos /Media/Backup/Photos `, } // diffMessage json container for diff messages type diffMessage struct { Status string `json:"status"` FirstURL string `json:"first"` SecondURL string `json:"second"` Diff differType `json:"diff"` Error *probe.Error `json:"error,omitempty"` firstContent *ClientContent secondContent *ClientContent } // String colorized diff message func (d diffMessage) String() string { msg := "" switch d.Diff { case differInFirst: msg = console.Colorize("DiffOnlyInFirst", "< "+d.FirstURL) case differInSecond: msg = console.Colorize("DiffOnlyInSecond", "> "+d.SecondURL) case differInType: msg = console.Colorize("DiffType", "! "+d.SecondURL) case differInSize: msg = console.Colorize("DiffSize", "! "+d.SecondURL) case differInMetadata: msg = console.Colorize("DiffMetadata", "! "+d.SecondURL) case differInAASourceMTime: msg = console.Colorize("DiffMMSourceMTime", "! "+d.SecondURL) case differInNone: msg = console.Colorize("DiffInNone", "= "+d.FirstURL) default: fatalIf(errDummy().Trace(d.FirstURL, d.SecondURL), "Unhandled difference between `"+d.FirstURL+"` and `"+d.SecondURL+"`.") } return msg } // JSON jsonified diff message func (d diffMessage) JSON() string { d.Status = "success" diffJSONBytes, e := json.MarshalIndent(d, "", " ") fatalIf(probe.NewError(e), "Unable to marshal diff message `"+d.FirstURL+"`, `"+d.SecondURL+"` and `"+fmt.Sprint(d.Diff)+"`.") return string(diffJSONBytes) } func checkDiffSyntax(ctx context.Context, cliCtx *cli.Context, encKeyDB map[string][]prefixSSEPair) { if len(cliCtx.Args()) != 2 { showCommandHelpAndExit(cliCtx, 1) // last argument is exit code } for _, arg := range cliCtx.Args() { if strings.TrimSpace(arg) == "" { fatalIf(errInvalidArgument().Trace(cliCtx.Args()...), "Unable to validate empty argument.") } } URLs := cliCtx.Args() firstURL := URLs[0] secondURL := URLs[1] // Diff only works between two directories, verify them below. // Verify if firstURL is accessible. _, firstContent, err := url2Stat(ctx, url2StatOptions{urlStr: firstURL, versionID: "", fileAttr: false, encKeyDB: encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false}) if err != nil { fatalIf(err.Trace(firstURL), fmt.Sprintf("Unable to stat '%s'.", firstURL)) } // Verify if its a directory. if !firstContent.Type.IsDir() { fatalIf(errInvalidArgument().Trace(firstURL), fmt.Sprintf("`%s` is not a folder.", firstURL)) } // Verify if secondURL is accessible. _, secondContent, err := url2Stat(ctx, url2StatOptions{urlStr: secondURL, versionID: "", fileAttr: false, encKeyDB: encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false}) if err != nil { // Destination doesn't exist is okay. if _, ok := err.ToGoError().(ObjectMissing); !ok { fatalIf(err.Trace(secondURL), fmt.Sprintf("Unable to stat '%s'.", secondURL)) } } // Verify if its a directory. if err == nil && !secondContent.Type.IsDir() { fatalIf(errInvalidArgument().Trace(secondURL), fmt.Sprintf("`%s` is not a folder.", secondURL)) } } // doDiffMain runs the diff. func doDiffMain(ctx context.Context, firstURL, secondURL string) error { // Source and targets are always directories sourceSeparator := string(newClientURL(firstURL).Separator) if !strings.HasSuffix(firstURL, sourceSeparator) { firstURL = firstURL + sourceSeparator } targetSeparator := string(newClientURL(secondURL).Separator) if !strings.HasSuffix(secondURL, targetSeparator) { secondURL = secondURL + targetSeparator } // Expand aliased urls. firstAlias, firstURL, _ := mustExpandAlias(firstURL) secondAlias, secondURL, _ := mustExpandAlias(secondURL) firstClient, err := newClientFromAlias(firstAlias, firstURL) if err != nil { fatalIf(err.Trace(firstAlias, firstURL, secondAlias, secondURL), fmt.Sprintf("Failed to diff '%s' and '%s'", firstURL, secondURL)) } secondClient, err := newClientFromAlias(secondAlias, secondURL) if err != nil { fatalIf(err.Trace(firstAlias, firstURL, secondAlias, secondURL), fmt.Sprintf("Failed to diff '%s' and '%s'", firstURL, secondURL)) } // Diff first and second urls. for diffMsg := range bucketObjectDifference(ctx, firstClient, secondClient) { if diffMsg.Error != nil { errorIf(diffMsg.Error, "Unable to calculate objects difference.") // Ignore error and proceed to next object. continue } printMsg(diffMsg) } return nil } // mainDiff main for 'diff'. func mainDiff(cliCtx *cli.Context) error { ctx, cancelDiff := context.WithCancel(globalContext) defer cancelDiff() // Parse encryption keys per command. encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) fatalIf(err, "Unable to parse encryption keys.") // check 'diff' cli arguments. checkDiffSyntax(ctx, cliCtx, encKeyDB) // Additional command specific theme customization. console.SetColor("DiffMessage", color.New(color.FgGreen, color.Bold)) console.SetColor("DiffOnlyInFirst", color.New(color.FgRed)) console.SetColor("DiffOnlyInSecond", color.New(color.FgGreen)) console.SetColor("DiffType", color.New(color.FgMagenta)) console.SetColor("DiffSize", color.New(color.FgYellow, color.Bold)) console.SetColor("DiffMetadata", color.New(color.FgYellow, color.Bold)) console.SetColor("DiffMMSourceMTime", color.New(color.FgYellow, color.Bold)) URLs := cliCtx.Args() firstURL := URLs.Get(0) secondURL := URLs.Get(1) return doDiffMain(ctx, firstURL, secondURL) } minio-client-0.0~20250403/cmd/difference.go000066400000000000000000000266721477450377600202030ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "strings" "time" "unicode/utf8" // golang does not support flat keys for path matching, find does "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "golang.org/x/text/unicode/norm" ) // differType difference in type. type differType int const ( differInUnknown differType = iota differInNone // does not differ differInSize // differs in size differInMetadata // differs in metadata differInType // differs in type, exfile/directory differInFirst // only in source (FIRST) differInSecond // only in target (SECOND) differInAASourceMTime // differs in active-active source modtime ) func (d differType) String() string { switch d { case differInNone: return "" case differInSize: return "size" case differInMetadata: return "metadata" case differInAASourceMTime: return "mm-source-mtime" case differInType: return "type" case differInFirst: return "only-in-first" case differInSecond: return "only-in-second" } return "unknown" } const activeActiveSourceModTimeKey = "X-Amz-Meta-Mm-Source-Mtime" func getSourceModTimeKey(metadata map[string]string) string { if metadata[activeActiveSourceModTimeKey] != "" { return metadata[activeActiveSourceModTimeKey] } if metadata[strings.ToLower(activeActiveSourceModTimeKey)] != "" { return metadata[strings.ToLower(activeActiveSourceModTimeKey)] } if metadata[strings.ToLower("Mm-Source-Mtime")] != "" { return metadata[strings.ToLower("Mm-Source-Mtime")] } if metadata["Mm-Source-Mtime"] != "" { return metadata["Mm-Source-Mtime"] } return "" } // activeActiveModTimeUpdated tries to calculate if the object copy in the target // is older than the one in the source by comparing the modtime of the data. func activeActiveModTimeUpdated(src, dst *ClientContent) bool { if src == nil || dst == nil { return false } if src.Time.IsZero() || dst.Time.IsZero() { // This should only happen in a messy environment // but we are returning false anyway so the caller // function won't take any action. return false } srcActualModTime := src.Time dstActualModTime := dst.Time srcModTime := getSourceModTimeKey(src.UserMetadata) dstModTime := getSourceModTimeKey(dst.UserMetadata) if srcModTime == "" && dstModTime == "" { // No active-active mirror context found, fallback to modTimes presented // by the client content return srcActualModTime.After(dstActualModTime) } var srcOriginLastModified, dstOriginLastModified time.Time var err error if srcModTime != "" { srcOriginLastModified, err = time.Parse(time.RFC3339Nano, srcModTime) if err != nil { // failure to parse source modTime, modTime tampered ignore the file return false } } if dstModTime != "" { dstOriginLastModified, err = time.Parse(time.RFC3339Nano, dstModTime) if err != nil { // failure to parse source modTime, modTime tampered ignore the file return false } } if !srcOriginLastModified.IsZero() && srcOriginLastModified.After(src.Time) { srcActualModTime = srcOriginLastModified } if !dstOriginLastModified.IsZero() && dstOriginLastModified.After(dst.Time) { dstActualModTime = dstOriginLastModified } return srcActualModTime.After(dstActualModTime) } func metadataEqual(m1, m2 map[string]string) bool { for k, v := range m1 { if k == activeActiveSourceModTimeKey { continue } if k == strings.ToLower(activeActiveSourceModTimeKey) { continue } if m2[k] != v { return false } } for k, v := range m2 { if k == activeActiveSourceModTimeKey { continue } if k == strings.ToLower(activeActiveSourceModTimeKey) { continue } if m1[k] != v { return false } } return true } func bucketObjectDifference(ctx context.Context, sourceClnt, targetClnt Client) (diffCh chan diffMessage) { return objectDifference(ctx, sourceClnt, targetClnt, mirrorOptions{ isMetadata: false, }) } func objectDifference(ctx context.Context, sourceClnt, targetClnt Client, opts mirrorOptions) (diffCh chan diffMessage) { sourceURL := sourceClnt.GetURL().String() sourceCh := sourceClnt.List(ctx, ListOptions{Recursive: true, WithMetadata: opts.isMetadata, ShowDir: DirNone}) targetURL := targetClnt.GetURL().String() targetCh := targetClnt.List(ctx, ListOptions{Recursive: true, WithMetadata: opts.isMetadata, ShowDir: DirNone}) return difference(sourceURL, sourceCh, targetURL, targetCh, opts, false) } func bucketDifference(ctx context.Context, sourceClnt, targetClnt Client, opts mirrorOptions) (diffCh chan diffMessage) { sourceURL := sourceClnt.GetURL().String() sourceCh := make(chan *ClientContent) go func() { defer close(sourceCh) buckets, err := sourceClnt.ListBuckets(ctx) if err != nil { select { case <-ctx.Done(): case sourceCh <- &ClientContent{Err: err}: } return } for _, b := range buckets { select { case <-ctx.Done(): return case sourceCh <- b: } } }() targetURL := targetClnt.GetURL().String() targetCh := make(chan *ClientContent) go func() { defer close(targetCh) buckets, err := targetClnt.ListBuckets(ctx) if err != nil { select { case <-ctx.Done(): case targetCh <- &ClientContent{Err: err}: } return } for _, b := range buckets { select { case <-ctx.Done(): return case targetCh <- b: } } }() return difference(sourceURL, sourceCh, targetURL, targetCh, opts, false) } func differenceInternal(sourceURL string, srcCh <-chan *ClientContent, targetURL string, tgtCh <-chan *ClientContent, opts mirrorOptions, returnSimilar bool, diffCh chan<- diffMessage, ) *probe.Error { // Pop first entries from the source and targets srcCtnt, srcOk := <-srcCh tgtCtnt, tgtOk := <-tgtCh var srcEOF, tgtEOF bool for { srcEOF = !srcOk tgtEOF = !tgtOk // No objects from source AND target: Finish if opts.sourceListingOnly { if srcEOF { break } } else { if srcEOF && tgtEOF { break } } if !srcEOF && srcCtnt.Err != nil { return srcCtnt.Err.Trace(sourceURL, targetURL) } if !tgtEOF && tgtCtnt.Err != nil { return tgtCtnt.Err.Trace(sourceURL, targetURL) } // If source doesn't have objects anymore, comparison becomes obvious if srcEOF { diffCh <- diffMessage{ SecondURL: tgtCtnt.URL.String(), Diff: differInSecond, secondContent: tgtCtnt, } tgtCtnt, tgtOk = <-tgtCh continue } // The same for target if tgtEOF { diffCh <- diffMessage{ FirstURL: srcCtnt.URL.String(), Diff: differInFirst, firstContent: srcCtnt, } srcCtnt, srcOk = <-srcCh continue } srcSuffix := strings.TrimPrefix(srcCtnt.URL.String(), sourceURL) tgtSuffix := strings.TrimPrefix(tgtCtnt.URL.String(), targetURL) current := urlJoinPath(targetURL, srcSuffix) expected := urlJoinPath(targetURL, tgtSuffix) if !utf8.ValidString(srcSuffix) { // Error. Keys must be valid UTF-8. diffCh <- diffMessage{Error: errInvalidSource(current).Trace()} srcCtnt, srcOk = <-srcCh continue } if !utf8.ValidString(tgtSuffix) { // Error. Keys must be valid UTF-8. diffCh <- diffMessage{Error: errInvalidTarget(expected).Trace()} tgtCtnt, tgtOk = <-tgtCh continue } // Normalize to avoid situations where multiple byte representations are possible. // e.g. 'ä' can be represented as precomposed U+00E4 (UTF-8 0xc3a4) or decomposed // U+0061 U+0308 (UTF-8 0x61cc88). normalizedCurrent := norm.NFC.String(current) normalizedExpected := norm.NFC.String(expected) if normalizedExpected > normalizedCurrent { diffCh <- diffMessage{ FirstURL: srcCtnt.URL.String(), Diff: differInFirst, firstContent: srcCtnt, } srcCtnt, srcOk = <-srcCh continue } if normalizedExpected == normalizedCurrent { srcType, tgtType := srcCtnt.Type, tgtCtnt.Type srcSize, tgtSize := srcCtnt.Size, tgtCtnt.Size if srcType.IsRegular() && !tgtType.IsRegular() || !srcType.IsRegular() && tgtType.IsRegular() { // Type differs. Source is never a directory. diffCh <- diffMessage{ FirstURL: srcCtnt.URL.String(), SecondURL: tgtCtnt.URL.String(), Diff: differInType, firstContent: srcCtnt, secondContent: tgtCtnt, } continue } if srcSize != tgtSize { // Regular files differing in size. diffCh <- diffMessage{ FirstURL: srcCtnt.URL.String(), SecondURL: tgtCtnt.URL.String(), Diff: differInSize, firstContent: srcCtnt, secondContent: tgtCtnt, } } else if activeActiveModTimeUpdated(srcCtnt, tgtCtnt) { diffCh <- diffMessage{ FirstURL: srcCtnt.URL.String(), SecondURL: tgtCtnt.URL.String(), Diff: differInAASourceMTime, firstContent: srcCtnt, secondContent: tgtCtnt, } } else if opts.isMetadata && !metadataEqual(srcCtnt.UserMetadata, tgtCtnt.UserMetadata) && !metadataEqual(srcCtnt.Metadata, tgtCtnt.Metadata) { // Regular files user requesting additional metadata to same file. diffCh <- diffMessage{ FirstURL: srcCtnt.URL.String(), SecondURL: tgtCtnt.URL.String(), Diff: differInMetadata, firstContent: srcCtnt, secondContent: tgtCtnt, } } // No differ if returnSimilar { diffCh <- diffMessage{ FirstURL: srcCtnt.URL.String(), SecondURL: tgtCtnt.URL.String(), Diff: differInNone, firstContent: srcCtnt, secondContent: tgtCtnt, } } srcCtnt, srcOk = <-srcCh tgtCtnt, tgtOk = <-tgtCh continue } // Differ in second diffCh <- diffMessage{ SecondURL: tgtCtnt.URL.String(), Diff: differInSecond, secondContent: tgtCtnt, } tgtCtnt, tgtOk = <-tgtCh continue } return nil } // objectDifference function finds the difference between all objects // recursively in sorted order from source and target. func difference(sourceURL string, sourceCh <-chan *ClientContent, targetURL string, targetCh <-chan *ClientContent, opts mirrorOptions, returnSimilar bool) (diffCh chan diffMessage) { diffCh = make(chan diffMessage, 10000) go func() { defer close(diffCh) err := differenceInternal(sourceURL, sourceCh, targetURL, targetCh, opts, returnSimilar, diffCh) if err != nil { // handle this specifically for filesystem related errors. switch v := err.ToGoError().(type) { case PathNotFound, PathInsufficientPermission, PathNotADirectory: diffCh <- diffMessage{ Error: err, } return case minio.ErrorResponse: switch v.Code { case "NoSuchBucket", "NoSuchKey", "SignatureDoesNotMatch": diffCh <- diffMessage{ Error: err, } return } } errorIf(err, "Unable to list comparison retrying..") } }() return diffCh } minio-client-0.0~20250403/cmd/difference_test.go000066400000000000000000000046111477450377600212270ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "testing" ) var testCases = []struct { pattern []string srcSuffix string match bool typ ClientURLType }{ {nil, "testfile", false, objectStorage}, {[]string{"test*"}, "testfile", true, objectStorage}, {[]string{"file*"}, "file/abc/bcd/def", true, objectStorage}, {[]string{"*"}, "file/abc/bcd/def", true, objectStorage}, {[]string{""}, "file/abc/bcd/def", false, objectStorage}, {[]string{"abc*"}, "file/abc/bcd/def", false, objectStorage}, {[]string{"abc*", "*abc/*"}, "file/abc/bcd/def", true, objectStorage}, {[]string{"*.txt"}, "file/abc/bcd/def.txt", true, objectStorage}, {[]string{".*"}, ".sys", true, objectStorage}, {[]string{"*."}, ".sys.", true, objectStorage}, {nil, "testfile", false, fileSystem}, {[]string{"test*"}, "testfile", true, fileSystem}, {[]string{"file*"}, "file/abc/bcd/def", true, fileSystem}, {[]string{"*"}, "file/abc/bcd/def", true, fileSystem}, {[]string{""}, "file/abc/bcd/def", false, fileSystem}, {[]string{"abc*"}, "file/abc/bcd/def", false, fileSystem}, {[]string{"abc*", "*abc/*"}, "file/abc/bcd/def", true, fileSystem}, {[]string{"abc*", "*abc/*"}, "/file/abc/bcd/def", true, fileSystem}, {[]string{"*.txt"}, "file/abc/bcd/def.txt", true, fileSystem}, {[]string{"*.txt"}, "/file/abc/bcd/def.txt", true, fileSystem}, {[]string{".*"}, ".sys", true, fileSystem}, {[]string{"*."}, ".sys.", true, fileSystem}, } func TestExcludeOptions(t *testing.T) { for _, test := range testCases { if matchExcludeOptions(test.pattern, test.srcSuffix, test.typ) != test.match { t.Fatalf("Unexpected result %t, with pattern %s and srcSuffix %s \n", !test.match, test.pattern, test.srcSuffix) } } } minio-client-0.0~20250403/cmd/du-main.go000066400000000000000000000152331477450377600174320ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "net/url" "path" "strings" "time" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) // du specific flags. var ( duFlags = []cli.Flag{ cli.IntFlag{ Name: "depth, d", Usage: "print the total for a folder prefix only if it is N or fewer levels below the command line argument", }, cli.BoolFlag{ Name: "recursive, r", Usage: "recursively print the total for a folder prefix", }, cli.StringFlag{ Name: "rewind", Usage: "include all object versions no later than specified date", }, cli.BoolFlag{ Name: "versions", Usage: "include all object versions", }, } ) // Summarize disk usage. var duCmd = cli.Command{ Name: "du", Usage: "summarize disk usage recursively", Action: mainDu, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(duFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Summarize disk usage of 'jazz-songs' bucket recursively. {{.Prompt}} {{.HelpName}} s3/jazz-songs 2. Summarize disk usage of 'louis' prefix in 'jazz-songs' bucket upto two levels. {{.Prompt}} {{.HelpName}} --depth=2 s3/jazz-songs/louis/ 3. Summarize disk usage of 'jazz-songs' bucket at a fixed date/time {{.Prompt}} {{.HelpName}} --rewind "2020.01.01" s3/jazz-songs/ 4. Summarize disk usage of 'jazz-songs' bucket with all objects versions {{.Prompt}} {{.HelpName}} --versions s3/jazz-songs/ `, } // Structured message depending on the type of console. type duMessage struct { Prefix string `json:"prefix"` Size int64 `json:"size"` Objects int64 `json:"objects"` Status string `json:"status"` IsVersions bool `json:"isVersions"` } // Colorized message for console printing. func (r duMessage) String() string { humanSize := strings.Join(strings.Fields(humanize.IBytes(uint64(r.Size))), "") cnt := fmt.Sprintf("%d object", r.Objects) if r.IsVersions { cnt = fmt.Sprintf("%d version", r.Objects) } if r.Objects != 1 { cnt += "s" // pluralize } return fmt.Sprintf("%s\t%s\t%s", console.Colorize("Size", humanSize), console.Colorize("Objects", cnt), console.Colorize("Prefix", r.Prefix)) } // JSON'ified message for scripting. func (r duMessage) JSON() string { msgBytes, e := json.MarshalIndent(r, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } func du(ctx context.Context, urlStr string, timeRef time.Time, withVersions bool, depth int) (sz, objs int64, err error) { targetAlias, targetURL, _ := mustExpandAlias(urlStr) if !strings.HasSuffix(targetURL, "/") { targetURL += "/" } clnt, pErr := newClientFromAlias(targetAlias, targetURL) if pErr != nil { errorIf(pErr.Trace(urlStr), "Failed to summarize disk usage `%s`.", urlStr) return 0, 0, exitStatus(globalErrorExitStatus) // End of journey. } // No disk usage details below this level, // just do a recursive listing recursive := depth == 1 targetAbsolutePath := path.Clean(clnt.GetURL().String()) contentCh := clnt.List(ctx, ListOptions{ TimeRef: timeRef, WithOlderVersions: withVersions, Recursive: recursive, ShowDir: DirFirst, }) size := int64(0) objects := int64(0) for content := range contentCh { if content.Err != nil { switch content.Err.ToGoError().(type) { // handle this specifically for filesystem related errors. case BrokenSymlink, TooManyLevelsSymlink, PathNotFound, ObjectOnGlacier: continue case PathInsufficientPermission: errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") continue } errorIf(content.Err.Trace(urlStr), "Failed to find disk usage of `%s` recursively.", urlStr) return 0, 0, exitStatus(globalErrorExitStatus) } if content.URL.Path == targetAbsolutePath { continue } if content.Type.IsDir() && !recursive { depth := depth if depth > 0 { depth-- } subDirAlias := content.URL.Path if targetAlias != "" { subDirAlias = targetAlias + "/" + content.URL.Path } used, n, err := du(ctx, subDirAlias, timeRef, withVersions, depth) if err != nil { return 0, 0, err } size += used objects += n } else { if !content.IsDeleteMarker && !content.Type.IsDir() { size += content.Size objects++ } } } if depth != 0 { u, e := url.Parse(targetURL) if e != nil { panic(e) } printMsg(duMessage{ Prefix: strings.Trim(u.Path, "/"), Size: size, Objects: objects, Status: "success", IsVersions: withVersions, }) } return size, objects, nil } // main for du command. func mainDu(cliCtx *cli.Context) error { if !cliCtx.Args().Present() { showCommandHelpAndExit(cliCtx, 1) } // Set colors. console.SetColor("Remove", color.New(color.FgGreen, color.Bold)) console.SetColor("Prefix", color.New(color.FgCyan, color.Bold)) console.SetColor("Objects", color.New(color.FgGreen)) console.SetColor("Size", color.New(color.FgYellow)) ctx, cancelRm := context.WithCancel(globalContext) defer cancelRm() // du specific flags. depth := cliCtx.Int("depth") if depth == 0 { if cliCtx.Bool("recursive") { if !cliCtx.IsSet("depth") { depth = -1 } } else { depth = 1 } } withVersions := cliCtx.Bool("versions") timeRef := parseRewindFlag(cliCtx.String("rewind")) var duErr error var isDir bool for _, urlStr := range cliCtx.Args() { isDir, _ = isAliasURLDir(ctx, urlStr, nil, time.Time{}, false) if !isDir { fatalIf(errInvalidArgument().Trace(urlStr), fmt.Sprintf("Source `%s` is not a folder. Only folders are supported by 'du' command.", urlStr)) } if _, _, err := du(ctx, urlStr, timeRef, withVersions, depth); duErr == nil { duErr = err } } return duErr } minio-client-0.0~20250403/cmd/duration.go000066400000000000000000000116101477450377600177200ustar00rootroot00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the https://go.dev/LICENSE file. // // Code is borrowed directly from pkg/time.ParseDuration under // Go's BSD-style license, extended with changes to support // days, weeks and year as time Duration units. // Package cmd provides extended time.Duration implementation package cmd import ( "errors" "strings" "time" ) // Duration is a standard unit of time. type Duration time.Duration // Days returns the duration as a floating point number of days. func (d Duration) Days() float64 { hour := d / Hour nsec := d % Hour return float64(hour) + float64(nsec)*(1e-9/60/60/24) } // Standard unit of time. var ( Nanosecond = Duration(time.Nanosecond) Microsecond = Duration(time.Microsecond) Millisecond = Duration(time.Millisecond) Second = Duration(time.Second) Minute = Duration(time.Minute) Hour = Duration(time.Hour) Day = Hour * 24 Week = Day * 7 Fortnight = Week * 2 Month = Day * 30 // Approximation Year = Day * 365 // Approximation Decade = Year * 10 // Approximation Century = Year * 100 // Approximation Millennium = Year * 1000 // Approximation ) // leadingInt consumes the leading [0-9]* from s. // this function is directly copied from // https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/time/format.go;drc=55590f3a2b89f001bcadf0df6eb2dde62618302b;l=1455 // only changes the "error string that's being returned" func leadingInt(s string) (x int64, rem string, err error) { errLeadingInt := errors.New("bad [0-9]*") i := 0 for ; i < len(s); i++ { c := s[i] if c < '0' || c > '9' { break } if x > (1<<63-1)/10 { // overflow return 0, "", errLeadingInt } x = x*10 + int64(c) - '0' if x < 0 { // overflow return 0, "", errLeadingInt } } return x, s[i:], nil } // unitMap defines 'unit' to 'actual-value' // translates for calculating duration. var unitMap = map[string]int64{ "ns": int64(Nanosecond), "us": int64(Microsecond), "µs": int64(Microsecond), // U+00B5 = micro symbol "μs": int64(Microsecond), // U+03BC = Greek letter mu "ms": int64(Millisecond), "s": int64(Second), "m": int64(Minute), "h": int64(Hour), "d": int64(Day), "w": int64(Week), "y": int64(Year), // Approximation } // ParseDuration parses a duration string. // This implementation is similar to Go's // https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/time/format.go;l=1546;bpv=1;bpt=1 // extends it to parse days, weeks and years.. // // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h", "d", "w", "y". func ParseDuration(s string) (Duration, error) { if strings.TrimSpace(s) == "" { return 0, errors.New("invalid empty duration") } // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ orig := s var d int64 neg := false // Consume [-+]? if s != "" { c := s[0] if c == '-' || c == '+' { neg = c == '-' s = s[1:] } } // Special case: if all that is left is "0", this is zero. if s == "0" { return 0, nil } if s == "" { return 0, errors.New("invalid duration " + orig) } for s != "" { var ( v, f int64 // integers before, after decimal point scale float64 = 1 // value = v + f/scale ) var err error // The next character must be [0-9.] if s[0] != '.' && ('0' > s[0] || s[0] > '9') { return 0, errors.New("invalid duration " + orig) } // Consume [0-9]* pl := len(s) v, s, err = leadingInt(s) if err != nil { return 0, errors.New("invalid duration " + orig) } pre := pl != len(s) // whether we consumed anything before a period // Consume (\.[0-9]*)? post := false if s != "" && s[0] == '.' { s = s[1:] pl := len(s) f, s, err = leadingInt(s) if err != nil { return 0, errors.New("invalid duration " + orig) } for n := pl - len(s); n > 0; n-- { scale *= 10 } post = pl != len(s) } if !pre && !post { // no digits (e.g. ".s" or "-.s") return 0, errors.New("invalid duration " + orig) } // Consume unit. i := 0 for ; i < len(s); i++ { c := s[i] if c == '.' || '0' <= c && c <= '9' { break } } if i == 0 { return 0, errors.New("missing unit in duration " + orig) } u := s[:i] s = s[i:] unit, ok := unitMap[u] if !ok { return 0, errors.New("unknown unit " + u + " in duration " + orig) } if v > (1<<63-1)/unit { // overflow return 0, errors.New("invalid duration " + orig) } v *= unit if f > 0 { // float64 is needed to be nanosecond accurate for fractions of hours. // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) v += int64(float64(f) * (float64(unit) / scale)) if v < 0 { // overflow return 0, errors.New("invalid duration " + orig) } } d += v if d < 0 { // overflow return 0, errors.New("invalid duration " + orig) } } if neg { d = -d } return Duration(d), nil } minio-client-0.0~20250403/cmd/encrypt-clear.go000066400000000000000000000054601477450377600206510ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var encryptClearCmd = cli.Command{ Name: "clear", Usage: "clear encryption config", Action: mainEncryptClear, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove auto encryption config on bucket "mybucket" for alias "myminio". {{.Prompt}} {{.HelpName}} myminio/mybucket `, } // checkEncryptClearSyntax - validate all the passed arguments func checkEncryptClearSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } type encryptClearMessage struct { Op string `json:"op"` Status string `json:"status"` URL string `json:"url"` } func (v encryptClearMessage) JSON() string { v.Status = "success" jsonMessageBytes, e := json.MarshalIndent(v, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (v encryptClearMessage) String() string { return console.Colorize("encryptClearMessage", fmt.Sprintf("Auto encryption configuration has been cleared successfully for %s", v.URL)) } func mainEncryptClear(cliCtx *cli.Context) error { ctx, cancelencryptClear := context.WithCancel(globalContext) defer cancelencryptClear() console.SetColor("encryptClearMessage", color.New(color.FgGreen)) checkEncryptClearSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") fatalIf(client.DeleteEncryption(ctx), "Unable to clear auto encryption configuration") printMsg(encryptClearMessage{ Op: cliCtx.Command.Name, Status: "success", URL: aliasedURL, }) return nil } minio-client-0.0~20250403/cmd/encrypt-info.go000066400000000000000000000063501477450377600205150ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var encryptInfoCmd = cli.Command{ Name: "info", Usage: "show bucket encryption status", Action: mainEncryptInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display bucket encryption status for bucket "mybucket". {{.Prompt}} {{.HelpName}} myminio/mybucket `, } // checkversionInfoSyntax - validate all the passed arguments func checkEncryptInfoSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } type encryptInfoMessage struct { Op string `json:"op"` Status string `json:"status"` URL string `json:"url"` Encryption struct { Algorithm string `json:"algorithm,omitempty"` KeyID string `json:"keyId,omitempty"` } `json:"encryption,omitempty"` } func (v encryptInfoMessage) JSON() string { v.Status = "success" jsonMessageBytes, e := json.MarshalIndent(v, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (v encryptInfoMessage) String() string { msg := "" switch v.Encryption.Algorithm { case "": msg = fmt.Sprintf("Auto encryption is not enabled for %s ", v.URL) default: msg = "Auto encryption 'sse-s3' is enabled" } if v.Encryption.KeyID != "" { msg = fmt.Sprintf("Auto encryption 'sse-kms' is enabled with KeyID: %s", v.Encryption.KeyID) } return console.Colorize("encryptInfoMessage", msg) } func mainEncryptInfo(cliCtx *cli.Context) error { ctx, cancelEncryptInfo := context.WithCancel(globalContext) defer cancelEncryptInfo() console.SetColor("encryptInfoMessage", color.New(color.FgGreen)) checkEncryptInfoSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") algorithm, keyID, e := client.GetEncryption(ctx) fatalIf(e, "Unable to get encryption info") msg := encryptInfoMessage{ Op: cliCtx.Command.Name, Status: "success", URL: aliasedURL, } msg.Encryption.Algorithm = algorithm msg.Encryption.KeyID = keyID printMsg(msg) return nil } minio-client-0.0~20250403/cmd/encrypt-main.go000066400000000000000000000025711477450377600205070ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var encryptSubcommands = []cli.Command{ encryptSetCmd, encryptClearCmd, encryptInfoCmd, } var encryptCmd = cli.Command{ Name: "encrypt", Usage: "manage bucket encryption config", HideHelpCommand: true, Action: mainEncrypt, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: encryptSubcommands, } // mainEncrypt is the handle for "mc encrypt" command. func mainEncrypt(ctx *cli.Context) error { commandNotFound(ctx, encryptSubcommands) return nil // Sub-commands like "info", "set", "clear" have their own main. } minio-client-0.0~20250403/cmd/encrypt-set.go000066400000000000000000000071021477450377600203510ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var encryptSetCmd = cli.Command{ Name: "set", Usage: "set encryption config", Action: mainEncryptSet, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable SSE-KMS auto encryption with KMS key on bucket "mybucket" for alias "myminio". {{.Prompt}} {{.HelpName}} sse-kms my-minio-key myminio/mybucket 2. Enable SSE-KMS auto encryption with KMS key on bucket "mybucket" for alias "s3". {{.Prompt}} {{.HelpName}} sse-kms arn:aws:kms:us-east-1:xxx:key/xxx s3/mybucket `, } // checkEncryptSetSyntax - validate all the passed arguments func checkEncryptSetSyntax(ctx *cli.Context) { if len(ctx.Args()) < 2 || len(ctx.Args()) > 3 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } type encryptSetMessage struct { Op string `json:"op"` Status string `json:"status"` URL string `json:"url"` Encryption struct { Algorithm string `json:"algorithm,omitempty"` KeyID string `json:"keyId,omitempty"` } `json:"encryption,omitempty"` } func (v encryptSetMessage) JSON() string { v.Status = "success" jsonMessageBytes, e := json.MarshalIndent(v, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (v encryptSetMessage) String() string { return console.Colorize("encryptSetMessage", fmt.Sprintf("Auto encryption configuration has been set successfully for %s", v.URL)) } func mainEncryptSet(cliCtx *cli.Context) error { ctx, cancelencryptSet := context.WithCancel(globalContext) defer cancelencryptSet() console.SetColor("encryptSetMessage", color.New(color.FgGreen)) checkEncryptSetSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(len(args) - 1) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") var algorithm, keyID string switch len(args) { case 3: algorithm = strings.ToLower(args[0]) keyID = args[1] case 2: algorithm = strings.ToLower(args[0]) } if algorithm != "sse-s3" && algorithm != "sse-kms" { fatalIf(probe.NewError(fmt.Errorf("Unknown argument `%s` passed", algorithm)), "Invalid encryption algorithm") } fatalIf(client.SetEncryption(ctx, algorithm, keyID), "Unable to enable auto encryption") msg := encryptSetMessage{ Op: cliCtx.Command.Name, Status: "success", URL: aliasedURL, } msg.Encryption.Algorithm = algorithm printMsg(msg) return nil } minio-client-0.0~20250403/cmd/encryption-methods.go000066400000000000000000000143421477450377600217330ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // # This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "encoding/base64" "encoding/hex" "fmt" "sort" "strings" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/encrypt" ) type sseKeyType int const ( sseNone sseKeyType = iota sseC sseKMS sseS3 ) // struct representing object prefix and sse keys association. type prefixSSEPair struct { Prefix string SSE encrypt.ServerSide } // byPrefixLength implements sort.Interface. type byPrefixLength []prefixSSEPair func (p byPrefixLength) Len() int { return len(p) } func (p byPrefixLength) Less(i, j int) bool { if len(p[i].Prefix) != len(p[j].Prefix) { return len(p[i].Prefix) > len(p[j].Prefix) } return p[i].Prefix < p[j].Prefix } func (p byPrefixLength) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // get SSE Key if object prefix matches with given resource. func getSSE(resource string, encKeys []prefixSSEPair) encrypt.ServerSide { for _, k := range encKeys { if strings.HasPrefix(resource, k.Prefix) { return k.SSE } } return nil } func validateAndCreateEncryptionKeys(ctx *cli.Context) (encMap map[string][]prefixSSEPair, err *probe.Error) { encMap = make(map[string][]prefixSSEPair, 0) for _, v := range ctx.StringSlice("enc-kms") { prefixPair, alias, err := validateAndParseKey(ctx, v, sseKMS) if err != nil { return nil, err } encMap[alias] = append(encMap[alias], *prefixPair) } for _, v := range ctx.StringSlice("enc-s3") { prefixPair, alias, err := validateAndParseKey(ctx, v, sseS3) if err != nil { return nil, err } encMap[alias] = append(encMap[alias], *prefixPair) } for _, v := range ctx.StringSlice("enc-c") { prefixPair, alias, err := validateAndParseKey(ctx, v, sseC) if err != nil { return nil, err } encMap[alias] = append(encMap[alias], *prefixPair) } for i := range encMap { err = validateOverLappingSSEKeys(encMap[i]) if err != nil { return nil, err } } for alias, ps := range encMap { if hostCfg := mustGetHostConfig(alias); hostCfg == nil { for _, p := range ps { return nil, errSSEInvalidAlias(p.Prefix) } } } for _, encKeys := range encMap { sort.Sort(byPrefixLength(encKeys)) } return encMap, nil } func validateAndParseKey(ctx *cli.Context, key string, keyType sseKeyType) (SSEPair *prefixSSEPair, alias string, perr *probe.Error) { matchedCount := 0 alias, prefix, encKey, keyErr := parseSSEKey(key, keyType) if keyErr != nil { return nil, "", keyErr } if alias == "" { return nil, "", errSSEInvalidAlias(prefix).Trace(key) } if (keyType == sseKMS || keyType == sseC) && encKey == "" { return nil, "", errSSEClientKeyFormat("SSE-C/KMS key should be of the form alias/prefix=key,... ").Trace(key) } ssePairPrefix := alias + "/" + prefix for _, arg := range ctx.Args() { if strings.HasPrefix(arg, ssePairPrefix) { matchedCount++ } else if strings.HasPrefix(ssePairPrefix, arg) { matchedCount++ } } if matchedCount == 0 { return nil, "", errSSEPrefixMatch() } var sse encrypt.ServerSide var err error switch keyType { case sseC: sse, err = encrypt.NewSSEC([]byte(encKey)) case sseKMS: sse, err = encrypt.NewSSEKMS(encKey, nil) case sseS3: sse = encrypt.NewSSE() } if err != nil { return nil, "", probe.NewError(err).Trace(key) } return &prefixSSEPair{ Prefix: ssePairPrefix, SSE: sse, }, alias, nil } func validateOverLappingSSEKeys(keyMap []prefixSSEPair) (err *probe.Error) { for i := 0; i < len(keyMap); i++ { for j := i + 1; j < len(keyMap); j++ { if strings.HasPrefix(keyMap[i].Prefix, keyMap[j].Prefix) || strings.HasPrefix(keyMap[j].Prefix, keyMap[i].Prefix) { return errSSEOverlappingAlias(keyMap[i].Prefix, keyMap[j].Prefix) } } } return } func splitKey(sseKey string) (alias, prefix string) { x := strings.SplitN(sseKey, "/", 2) switch len(x) { case 2: return x[0], x[1] case 1: return x[0], "" } return "", "" } func parseSSEKey(sseKey string, keyType sseKeyType) ( alias string, prefix string, key string, err *probe.Error, ) { sseKeyBytes := []byte(sseKey) separatorIndex := bytes.LastIndex(sseKeyBytes, []byte("=")) if separatorIndex < 0 { if keyType == sseS3 { alias, prefix = splitKey(sseKey) return } err = errSSEKeyMissing().Trace(sseKey) return } if separatorIndex == len(sseKeyBytes)-1 { err = errSSEKeyMissing().Trace(sseKey) return } encodedKey := string(sseKeyBytes[separatorIndex+1:]) alias, prefix = splitKey(string(sseKeyBytes[:separatorIndex])) if keyType == sseKMS { if !validKMSKeyName(encodedKey) { err = errSSEKMSKeyFormat(fmt.Sprintf("Key (%s) is badly formatted.", encodedKey)).Trace(sseKey) } key = encodedKey return } var keyB []byte var decodeError error if len(encodedKey) == 64 { keyB, decodeError = hex.DecodeString(encodedKey) } else { keyB, decodeError = base64.RawStdEncoding.DecodeString(encodedKey) } if decodeError != nil { err = errSSEClientKeyFormat(fmt.Sprintf("Key (%s) was neither base64 raw encoded nor hex encoded.", encodedKey)).Trace(sseKey) } key = string(keyB) if len(key) != 32 { err = errSSEClientKeyFormat(fmt.Sprintf("Plain text from key (%s) is only %d bytes, but should be 32 bytes.", encodedKey, len(key))).Trace(sseKey) return } return } func validKMSKeyName(s string) bool { if s == "" || s == "_" { return false } n := len(s) - 1 for i, r := range s { switch { case r >= '0' && r <= '9': case r >= 'A' && r <= 'Z': case r >= 'a' && r <= 'z': case r == '-' && i > 0 && i < n: case r == '_': default: return false } } return true } minio-client-0.0~20250403/cmd/encryption-methods_test.go000066400000000000000000000132361477450377600227730ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // # This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "testing" ) func TestParseEncryptionKeys(t *testing.T) { baseAlias := "mintest" basePrefix := "two/layer/prefix" baseObject := "object_name" sseKeyKMS := "my-default-key" sseKeyKMSInvalid := "my@default@key" sseKey := "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" sseKeyPlain := "01234567890123456789012345678900" sseHexKey := "3031323334353637383930313233343536373839303132333435363738393030" // INVALID KEYS sseKeyInvalidShort := "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2" sseKeyInvalidSymbols := "MDEyMzQ1Njc4O____jM0N!!!ODkwMTIzNDU2Nzg5MDA" sseKeyInvalidSpaces := "MDE yMzQ1Njc4OTAxM jM0NTY3ODkwMTIzNDU2Nzg5MDA" sseKeyInvalidPrefixSpace := " MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" sseKeyInvalidOneShort := "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MD" testCases := []struct { encryptionKey string keyPlain string alias string prefix string object string sseType sseKeyType success bool }{ { encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseHexKey), keyPlain: sseKeyPlain, alias: baseAlias, prefix: basePrefix, object: baseObject, sseType: sseC, success: true, }, { encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseKey), keyPlain: sseKeyPlain, alias: baseAlias, prefix: basePrefix, object: baseObject, sseType: sseC, success: true, }, { encryptionKey: fmt.Sprintf("%s=/%s=/%s==%s", baseAlias, basePrefix, baseObject, sseKey), keyPlain: sseKeyPlain, alias: baseAlias + "=", prefix: basePrefix + "=", object: baseObject + "=", sseType: sseC, success: true, }, { encryptionKey: fmt.Sprintf("%s//%s//%s/=%s", baseAlias, basePrefix, baseObject, sseKey), keyPlain: sseKeyPlain, alias: baseAlias + "/", prefix: basePrefix + "/", object: baseObject + "/", sseType: sseC, success: true, }, { encryptionKey: fmt.Sprintf("%s/%s/%s==%s", baseAlias, basePrefix, baseObject, sseKey), keyPlain: sseKeyPlain, alias: baseAlias, prefix: basePrefix, object: baseObject + "=", sseType: sseC, success: true, }, { encryptionKey: fmt.Sprintf("%s/%s/%s!@_==_$^&*=%s", baseAlias, basePrefix, baseObject, sseKey), keyPlain: sseKeyPlain, alias: baseAlias, prefix: basePrefix, object: baseObject + "!@_==_$^&*", sseType: sseC, success: true, }, { encryptionKey: fmt.Sprintf("%s/%s/%s=%sXXXXX", baseAlias, basePrefix, baseObject, sseKey), sseType: sseC, success: false, }, { encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseKeyInvalidShort), sseType: sseC, success: false, }, { encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseKeyInvalidSymbols), sseType: sseC, success: false, }, { encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseKeyInvalidSpaces), sseType: sseC, success: false, }, { encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseKeyInvalidPrefixSpace), sseType: sseC, success: false, }, { encryptionKey: fmt.Sprintf("%s/%s/%s==%s", baseAlias, basePrefix, baseObject, sseKeyInvalidOneShort), sseType: sseC, success: false, }, // sse-type KMS { encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseKeyKMS), keyPlain: sseKeyKMS, alias: baseAlias, prefix: basePrefix, object: baseObject, sseType: sseKMS, success: true, }, { encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseKeyKMSInvalid), sseType: sseKMS, success: false, }, // sse-type S3 { encryptionKey: fmt.Sprintf("%s/%s/%s", baseAlias, basePrefix, baseObject), alias: baseAlias, prefix: basePrefix, object: baseObject, sseType: sseS3, success: true, }, } for i, tc := range testCases { alias, prefix, key, err := parseSSEKey(tc.encryptionKey, tc.sseType) if tc.success { if err != nil { t.Fatalf("Test %d: Expected success, got %s", i+1, err) } if fmt.Sprintf("%s/%s", alias, prefix) != fmt.Sprintf("%s/%s/%s", tc.alias, tc.prefix, tc.object) { t.Fatalf("Test %d: alias and prefix parsing was invalid, expected %s/%s/%s, got %s/%s", i, tc.alias, tc.prefix, tc.object, alias, prefix) } if key != tc.keyPlain { t.Fatalf("Test %d: sse key parsing is invalid, expected %s, got %s", i, tc.keyPlain, key) } } if !tc.success { if err == nil { t.Fatalf("Test %d: Expected error, got success", i+1) } } } } minio-client-0.0~20250403/cmd/error.go000066400000000000000000000123701477450377600172300ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "fmt" "strings" "unicode" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) // causeMessage container for golang error messages type causeMessage struct { Message string `json:"message"` Error error `json:"error"` } // errorMessage container for error messages type errorMessage struct { Message string `json:"message"` Cause causeMessage `json:"cause"` Type string `json:"type"` CallTrace []probe.TracePoint `json:"trace,omitempty"` SysInfo map[string]string `json:"sysinfo,omitempty"` } // fatalIf wrapper function which takes error and selectively prints stack frames if available on debug func fatalIf(err *probe.Error, msg string, data ...interface{}) { if err == nil { return } fatal(err, msg, data...) } func fatal(err *probe.Error, msg string, data ...interface{}) { if globalJSON { errorMsg := errorMessage{ Message: msg, Type: "fatal", Cause: causeMessage{ Message: err.ToGoError().Error(), Error: err.ToGoError(), }, } if globalDebug { errorMsg.CallTrace = err.CallTrace errorMsg.SysInfo = err.SysInfo } json, e := json.MarshalIndent(struct { Status string `json:"status"` Error errorMessage `json:"error"` }{ Status: "error", Error: errorMsg, }, "", " ") if e != nil { console.Fatalln(probe.NewError(e)) } console.Println(string(json)) console.Fatalln() } msg = fmt.Sprintf(msg, data...) errmsg := err.String() if !globalDebug { var e error if errors.Is(globalContext.Err(), context.Canceled) { // mc is getting killed e = errors.New("Canceling upon user request") } else { e = err.ToGoError() } errmsg = e.Error() } // Remove unnecessary leading spaces in generic/detailed error messages msg = strings.TrimSpace(msg) errmsg = strings.TrimSpace(errmsg) // Add punctuations when needed if len(errmsg) > 0 && len(msg) > 0 { if msg[len(msg)-1] != ':' && msg[len(msg)-1] != '.' { // The detailed error message starts with a capital letter, // we should then add '.', otherwise add ':'. if unicode.IsUpper(rune(errmsg[0])) { msg += "." } else { msg += ":" } } // Add '.' to the detail error if not found if errmsg[len(errmsg)-1] != '.' { errmsg += "." } } console.Fatalln(fmt.Sprintf("%s %s", msg, errmsg)) } // Exit coder wraps cli new exit error with a // custom exitStatus number. cli package requires // an error with `cli.ExitCoder` compatibility // after an action. Which woud allow cli package to // exit with the specified `exitStatus`. func exitStatus(status int) error { return cli.NewExitError("", status) } // errorIf synonymous with fatalIf but doesn't exit on error != nil func errorIf(err *probe.Error, msg string, data ...interface{}) { if err == nil { return } if globalJSON { errorMsg := errorMessage{ Message: fmt.Sprintf(msg, data...), Type: "error", Cause: causeMessage{ Message: err.ToGoError().Error(), Error: err.ToGoError(), }, } if globalDebug { errorMsg.CallTrace = err.CallTrace errorMsg.SysInfo = err.SysInfo } json, e := json.MarshalIndent(struct { Status string `json:"status"` Error errorMessage `json:"error"` }{ Status: "error", Error: errorMsg, }, "", " ") if e != nil { console.Fatalln(probe.NewError(e)) } console.Println(string(json)) return } msg = fmt.Sprintf(msg, data...) if !globalDebug { var e error if errors.Is(globalContext.Err(), context.Canceled) { // mc is getting killed e = errors.New("Canceling upon user request") } else { e = err.ToGoError() } console.Errorln(fmt.Sprintf("%s %s", msg, e)) return } console.Errorln(fmt.Sprintf("%s %s", msg, err)) } // deprecatedError function for deprecated commands func deprecatedError(newCommandName string) { err := probe.NewError(fmt.Errorf("Please use '%s' instead", newCommandName)) fatal(err, "Deprecated command") } // deprecatedError function for deprecated flags func deprecatedFlagError(oldFlag, newFlag string) { err := probe.NewError(fmt.Errorf("'%s' has been deprecated, please use %s instead", oldFlag, newFlag)) fatal(err, "a deprecated Flag") } func deprecatedFlagsWarning(cliCtx *cli.Context) { for _, v := range cliCtx.Args() { switch v { case "--encrypt", "-encrypt": deprecatedFlagError("--encrypt", "--enc-s3 or --enc-kms") case "--encrypt-key", "-encrypt-key": deprecatedFlagError("--encrypt-key", "--enc-c") } } } minio-client-0.0~20250403/cmd/event-add.go000066400000000000000000000104131477450377600177420ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var eventAddFlags = []cli.Flag{ cli.StringFlag{ Name: "event", Value: "put,delete,get", Usage: "filter specific type of event. Defaults to all event", }, cli.StringFlag{ Name: "prefix", Usage: "filter event associated to the specified prefix", }, cli.StringFlag{ Name: "suffix", Usage: "filter event associated to the specified suffix", }, cli.BoolFlag{ Name: "ignore-existing, p", Usage: "ignore if event already exists", }, } var eventAddCmd = cli.Command{ Name: "add", Usage: "add a new bucket notification", Action: mainEventAdd, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(eventAddFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET ARN [FLAGS] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable bucket notification with a specific ARN {{.Prompt}} {{.HelpName}} myminio/mybucket arn:aws:sqs:us-west-2:444455556666:your-queue 2. Enable bucket notification with filters parameters {{.Prompt}} {{.HelpName}} s3/mybucket arn:aws:sqs:us-west-2:444455556666:your-queue --event put,delete,get --prefix photos/ --suffix .jpg 3. Ignore duplicate bucket notification with -p flag {{.Prompt}} {{.HelpName}} s3/mybucket arn:aws:sqs:us-west-2:444455556666:your-queue -p --event put,delete,get --prefix photos/ --suffix .jpg 4. Enable bucket notification for Replication and ILM transition events to a specific ARN {{.Prompt}} {{.HelpName}} myminio/mysourcebucket arn:aws:sqs:us-west-2:444455556666:your-queue --event replica,ilm `, } // checkEventAddSyntax - validate all the passed arguments func checkEventAddSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // eventAddMessage container type eventAddMessage struct { ARN string `json:"arn"` Event []string `json:"event"` Prefix string `json:"prefix"` Suffix string `json:"suffix"` Status string `json:"status"` } // JSON jsonified update message. func (u eventAddMessage) JSON() string { u.Status = "success" eventAddMessageJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(eventAddMessageJSONBytes) } func (u eventAddMessage) String() string { msg := console.Colorize("Event", "Successfully added "+u.ARN) return msg } func mainEventAdd(cliCtx *cli.Context) error { ctx, cancelEventAdd := context.WithCancel(globalContext) defer cancelEventAdd() console.SetColor("Event", color.New(color.FgGreen, color.Bold)) checkEventAddSyntax(cliCtx) args := cliCtx.Args() path := args[0] arn := args[1] ignoreExisting := cliCtx.Bool("p") event := strings.Split(cliCtx.String("event"), ",") prefix := cliCtx.String("prefix") suffix := cliCtx.String("suffix") client, err := newClient(path) if err != nil { fatalIf(err.Trace(), "Unable to parse the provided url.") } s3Client, ok := client.(*S3Client) if !ok { fatalIf(errDummy().Trace(), "The provided url doesn't point to a S3 server.") } err = s3Client.AddNotificationConfig(ctx, arn, event, prefix, suffix, ignoreExisting) fatalIf(err, "Unable to enable notification on the specified bucket.") printMsg(eventAddMessage{ ARN: arn, Event: event, Prefix: prefix, Suffix: suffix, }) return nil } minio-client-0.0~20250403/cmd/event-list.go000066400000000000000000000074751477450377600202030ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var eventListFlags = []cli.Flag{} var eventListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list bucket notifications", Action: mainEventList, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(eventListFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET ARN [FLAGS] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List notification configurations associated to a specific arn {{.Prompt}} {{.HelpName}} myminio/mybucket arn:aws:sqs:us-west-2:444455556666:your-queue 2. List all notification configurations {{.Prompt}} {{.HelpName}} s3/mybucket `, } // checkEventListSyntax - validate all the passed arguments func checkEventListSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 && len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // eventListMessage container type eventListMessage struct { Status string `json:"status"` ID string `json:"id"` Event []string `json:"event"` Prefix string `json:"prefix"` Suffix string `json:"suffix"` Arn string `json:"arn"` } func (u eventListMessage) JSON() string { u.Status = "success" eventListMessageJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(eventListMessageJSONBytes) } func (u eventListMessage) String() string { msg := console.Colorize("ARN", fmt.Sprintf("%s ", u.Arn)) for i, event := range u.Event { msg += console.Colorize("Event", event) if i != len(u.Event)-1 { msg += "," } } msg += console.Colorize("Filter", " Filter: ") if u.Prefix != "" { msg += console.Colorize("Filter", fmt.Sprintf("prefix=\"%s\"", u.Prefix)) } if u.Suffix != "" { msg += console.Colorize("Filter", fmt.Sprintf("suffix=\"%s\"", u.Suffix)) } return msg } func mainEventList(cliCtx *cli.Context) error { ctx, cancelEventList := context.WithCancel(globalContext) defer cancelEventList() console.SetColor("ARN", color.New(color.FgGreen, color.Bold)) console.SetColor("Event", color.New(color.FgCyan, color.Bold)) console.SetColor("Filter", color.New(color.Bold)) checkEventListSyntax(cliCtx) args := cliCtx.Args() path := args[0] arn := "" if len(args) > 1 { arn = args[1] } client, err := newClient(path) if err != nil { fatalIf(err.Trace(), "Unable to parse the provided url.") } s3Client, ok := client.(*S3Client) if !ok { fatalIf(errDummy().Trace(), "The provided url doesn't point to a S3 server.") } configs, err := s3Client.ListNotificationConfigs(ctx, arn) fatalIf(err, "Unable to list notifications on the specified bucket.") for _, config := range configs { printMsg(eventListMessage{ Event: config.Events, Prefix: config.Prefix, Suffix: config.Suffix, Arn: config.Arn, ID: config.ID, }) } return nil } minio-client-0.0~20250403/cmd/event-main.go000066400000000000000000000026251477450377600201440ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var eventFlags = []cli.Flag{} var eventSubcommands = []cli.Command{ eventAddCmd, eventRemoveCmd, eventListCmd, } var eventCmd = cli.Command{ Name: "event", Usage: "manage object notifications", HideHelpCommand: true, Action: mainEvent, Before: setGlobalsFromContext, Flags: append(eventFlags, globalFlags...), Subcommands: eventSubcommands, } // mainEvent is the handle for "mc event" command. func mainEvent(ctx *cli.Context) error { commandNotFound(ctx, eventSubcommands) return nil // Sub-commands like "add", "remove", "list" have their own main. } minio-client-0.0~20250403/cmd/event-remove.go000066400000000000000000000077411477450377600205210ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var eventRemoveFlags = []cli.Flag{ cli.BoolFlag{ Name: "force", Usage: "force removing all bucket notifications", }, cli.StringFlag{ Name: "event", Value: "put,delete,get", Usage: "filter specific type of event. Defaults to all event", }, cli.StringFlag{ Name: "prefix", Usage: "filter event associated to the specified prefix", }, cli.StringFlag{ Name: "suffix", Usage: "filter event associated to the specified suffix", }, } var eventRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove a bucket notification; '--force' removes all bucket notifications", Action: mainEventRemove, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(eventRemoveFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [ARN] [FLAGS] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove bucket notification associated to a specific arn {{.Prompt}} {{.HelpName}} myminio/mybucket arn:aws:sqs:us-west-2:444455556666:your-queue 2. Remove all bucket notifications. --force flag is mandatory here {{.Prompt}} {{.HelpName}} myminio/mybucket --force `, } // checkEventRemoveSyntax - validate all the passed arguments func checkEventRemoveSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } if len(ctx.Args()) == 1 && !ctx.Bool("force") { fatalIf(probe.NewError(errors.New("")), "--force flag needs to be passed to remove all bucket notifications.") } } // eventRemoveMessage container type eventRemoveMessage struct { ARN string `json:"arn"` Status string `json:"status"` } // JSON jsonified remove message. func (u eventRemoveMessage) JSON() string { u.Status = "success" eventRemoveMessageJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(eventRemoveMessageJSONBytes) } func (u eventRemoveMessage) String() string { msg := console.Colorize("Event", "Successfully removed "+u.ARN) return msg } func mainEventRemove(cliCtx *cli.Context) error { ctx, cancelEventRemove := context.WithCancel(globalContext) defer cancelEventRemove() console.SetColor("Event", color.New(color.FgGreen, color.Bold)) checkEventRemoveSyntax(cliCtx) args := cliCtx.Args() path := args.Get(0) arn := "" if len(args) == 2 { arn = args.Get(1) } client, err := newClient(path) if err != nil { fatalIf(err.Trace(), "Unable to parse the provided url.") } s3Client, ok := client.(*S3Client) if !ok { fatalIf(errDummy().Trace(), "The provided url doesn't point to a S3 server.") } // flags for the attributes of the even event := cliCtx.String("event") prefix := cliCtx.String("prefix") suffix := cliCtx.String("suffix") err = s3Client.RemoveNotificationConfig(ctx, arn, event, prefix, suffix) if err != nil { fatalIf(err, "Unable to disable notification on the specified bucket.") } printMsg(eventRemoveMessage{ARN: arn}) return nil } minio-client-0.0~20250403/cmd/find-main.go000066400000000000000000000245101477450377600177400ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "regexp" "strings" "time" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) // List of all flags supported by find command. var ( findFlags = []cli.Flag{ cli.StringFlag{ Name: "exec", Usage: "spawn an external process for each matching object (see FORMAT)", }, cli.StringFlag{ Name: "ignore", Usage: "exclude objects matching the wildcard pattern", }, cli.BoolFlag{ Name: "versions", Usage: "include all objects versions", }, cli.StringFlag{ Name: "name", Usage: "find object names matching wildcard pattern", }, cli.StringFlag{ Name: "newer-than", Usage: "match all objects newer than value in duration string (e.g. 7d10h31s)", }, cli.StringFlag{ Name: "older-than", Usage: "match all objects older than value in duration string (e.g. 7d10h31s)", }, cli.StringFlag{ Name: "path", Usage: "match directory names matching wildcard pattern", }, cli.StringFlag{ Name: "print", Usage: "print in custom format to STDOUT (see FORMAT)", }, cli.StringFlag{ Name: "regex", Usage: "match directory and object name with RE2 regex pattern", }, cli.StringFlag{ Name: "larger", Usage: "match all objects larger than specified size in units (see UNITS)", }, cli.StringFlag{ Name: "smaller", Usage: "match all objects smaller than specified size in units (see UNITS)", }, cli.UintFlag{ Name: "maxdepth", Usage: "limit directory navigation to specified depth", }, cli.BoolFlag{ Name: "watch", Usage: "monitor a specified path for newly created object(s)", }, cli.StringSliceFlag{ Name: "metadata", Usage: "match metadata with RE2 regex pattern. Specify each with key=regex. MinIO server only.", }, cli.StringSliceFlag{ Name: "tags", Usage: "match tags with RE2 regex pattern. Specify each with key=regex. MinIO server only.", }, } ) var findCmd = cli.Command{ Name: "find", Usage: "search for objects", Action: mainFind, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(findFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} UNITS --smaller, --larger flags accept human-readable case-insensitive number suffixes such as "k", "m", "g" and "t" referring to the metric units KB, MB, GB and TB respectively. Adding an "i" to these prefixes, uses the IEC units, so that "gi" refers to "gibibyte" or "GiB". A "b" at the end is also accepted. Without suffixes the unit is bytes. --older-than, --newer-than flags accept the string for days, hours and minutes i.e. 1d2h30m states 1 day, 2 hours and 30 minutes. FORMAT Support string substitutions with special interpretations for following keywords. Keywords supported if target is filesystem or object storage: {} --> Substitutes to full path. {base} --> Substitutes to basename of path. {dir} --> Substitutes to dirname of the path. {size} --> Substitutes to object size of the path. {time} --> Substitutes to object modified time of the path. {version} --> Substitutes to object version identifier. Keywords supported if target is object storage: {url} --> Substitutes to a shareable URL of the path. EXAMPLES: 01. Find all "foo.jpg" in all buckets under "s3" account. {{.Prompt}} {{.HelpName}} s3 --name "foo.jpg" 02. Find all objects with ".txt" extension under "s3/mybucket". {{.Prompt}} {{.HelpName}} s3/mybucket --name "*.txt" 03. Find only the object names without the directory component under "s3/mybucket". {{.Prompt}} {{.HelpName}} s3/mybucket --name "*" -print {base} 04. Find all images with ".jpg" extension under "s3/photos", prefixed with "album". {{.Prompt}} {{.HelpName}} s3/photos --name "*.jpg" --path "*/album*/*" 05. Find all images with ".jpg", ".png", and ".gif" extensions, using regex under "s3/photos". {{.Prompt}} {{.HelpName}} s3/photos --regex "(?i)\.(jpg|png|gif)$" 06. Find all images with ".jpg" extension under "s3/bucket" and copy to "play/bucket" *continuously*. {{.Prompt}} {{.HelpName}} s3/bucket --name "*.jpg" --watch --exec "mc cp {} play/bucket" 07. Find and generate public URLs valid for 7 days, for all objects between 64 MB, and 1 GB in size under "s3" account. {{.Prompt}} {{.HelpName}} s3 --larger 64MB --smaller 1GB --print {url} 08. Find all objects created in the last week under "s3/bucket". {{.Prompt}} {{.HelpName}} s3/bucket --newer-than 7d 09. Find all objects which were created are older than 2 days, 5 hours and 10 minutes and exclude the ones with ".jpg" extension under "s3". {{.Prompt}} {{.HelpName}} s3 --older-than 2d5h10m --ignore "*.jpg" 10. List all objects up to 3 levels sub-directory deep under "s3/bucket". {{.Prompt}} {{.HelpName}} s3/bucket --maxdepth 3 11. Copy all versions of all objects in bucket in the local machine {{.Prompt}} {{.HelpName}} s3/bucket --versions --exec "mc cp --version-id {version} {} /tmp/dir/{}.{version}" `, } // checkFindSyntax - validate the passed arguments func checkFindSyntax(ctx context.Context, cliCtx *cli.Context, encKeyDB map[string][]prefixSSEPair) { args := cliCtx.Args() if !args.Present() { args = []string{"./"} // No args just default to present directory. } else if args.Get(0) == "." { args[0] = "./" // If the arg is '.' treat it as './'. } for _, arg := range args { if strings.TrimSpace(arg) == "" { fatalIf(errInvalidArgument().Trace(args...), "Unable to validate empty argument.") } } // Extract input URLs and validate. for _, url := range args { _, _, err := url2Stat(ctx, url2StatOptions{urlStr: url, versionID: "", fileAttr: false, encKeyDB: encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false}) if err != nil { // Bucket name empty is a valid error for 'find myminio' unless we are using watch, treat it as such. if _, ok := err.ToGoError().(BucketNameEmpty); ok && !cliCtx.Bool("watch") { continue } fatalIf(err.Trace(url), "Unable to stat `"+url+"`.") } } } // Find context is container to hold all parsed input arguments, // each parsed input is stored in its native typed form for // ease of repurposing. type findContext struct { *cli.Context execCmd string ignorePattern string namePattern string pathPattern string regexPattern *regexp.Regexp maxDepth uint printFmt string olderThan string newerThan string largerSize uint64 smallerSize uint64 watch bool withVersions bool matchMeta map[string]*regexp.Regexp matchTags map[string]*regexp.Regexp // Internal values targetAlias string targetURL string targetFullURL string clnt Client } // mainFind - handler for mc find commands func mainFind(cliCtx *cli.Context) error { ctx, cancelFind := context.WithCancel(globalContext) defer cancelFind() // Additional command specific theme customization. console.SetColor("Find", color.New(color.FgGreen, color.Bold)) console.SetColor("FindExecErr", color.New(color.FgRed, color.Italic, color.Bold)) // Parse encryption keys per command. encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) fatalIf(err, "Unable to parse encryption keys.") checkFindSyntax(ctx, cliCtx, encKeyDB) args := cliCtx.Args() if !args.Present() { args = []string{"./"} // Not args present default to present directory. } else if args.Get(0) == "." { args[0] = "./" // If the arg is '.' treat it as './'. } clnt, err := newClient(args[0]) fatalIf(err.Trace(args...), "Unable to initialize `"+args[0]+"`.") var olderThan, newerThan string if cliCtx.String("older-than") != "" { olderThan = cliCtx.String("older-than") } if cliCtx.String("newer-than") != "" { newerThan = cliCtx.String("newer-than") } // Use 'e' to indicate Go error, this is a convention followed in `mc`. For probe.Error we call it // 'err' and regular Go error is called as 'e'. var e error var largerSize, smallerSize uint64 if cliCtx.String("larger") != "" { largerSize, e = humanize.ParseBytes(cliCtx.String("larger")) fatalIf(probe.NewError(e).Trace(cliCtx.String("larger")), "Unable to parse input bytes.") } if cliCtx.String("smaller") != "" { smallerSize, e = humanize.ParseBytes(cliCtx.String("smaller")) fatalIf(probe.NewError(e).Trace(cliCtx.String("smaller")), "Unable to parse input bytes.") } // Get --versions flag withVersions := cliCtx.Bool("versions") targetAlias, _, hostCfg, err := expandAlias(args[0]) fatalIf(err.Trace(args[0]), "Unable to expand alias.") var targetFullURL string if hostCfg != nil { targetFullURL = hostCfg.URL } var regMatch *regexp.Regexp if cliCtx.String("regex") != "" { regMatch = regexp.MustCompile(cliCtx.String("regex")) } return doFind(ctx, &findContext{ Context: cliCtx, maxDepth: cliCtx.Uint("maxdepth"), execCmd: cliCtx.String("exec"), printFmt: cliCtx.String("print"), namePattern: cliCtx.String("name"), pathPattern: cliCtx.String("path"), regexPattern: regMatch, ignorePattern: cliCtx.String("ignore"), withVersions: withVersions, olderThan: olderThan, newerThan: newerThan, largerSize: largerSize, smallerSize: smallerSize, watch: cliCtx.Bool("watch"), targetAlias: targetAlias, targetURL: args[0], targetFullURL: targetFullURL, clnt: clnt, matchMeta: getRegexMap(cliCtx, "metadata"), matchTags: getRegexMap(cliCtx, "tags"), }) } minio-client-0.0~20250403/cmd/find.go000066400000000000000000000414401477450377600170170ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "fmt" "net/http" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "syscall" "time" "github.com/dustin/go-humanize" "github.com/google/shlex" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" "golang.org/x/text/unicode/norm" // golang does not support flat keys for path matching, find does "github.com/minio/pkg/v3/wildcard" ) // findMessage holds JSON and string values for printing find command output. type findMessage struct { contentMessage } // String calls tells the console what to print and how to print it. func (f findMessage) String() string { var msg string msg += f.Key if f.VersionID != "" { msg += " (" + f.VersionID + ")" } return console.Colorize("Find", msg) } // JSON formats output to be JSON output. func (f findMessage) JSON() string { return f.contentMessage.JSON() } // nameMatch is similar to filepath.Match but only matches the // base path of the input, if we couldn't find a match we // also proceed to look for similar strings alone and print it. // // pattern: // // { term } // // term: // // '*' matches any sequence of non-Separator characters // '?' matches any single non-Separator character // '[' [ '^' ] { character-range } ']' // character class (must be non-empty) // c matches character c (c != '*', '?', '\\', '[') // '\\' c matches character c // // character-range: // // c matches character c (c != '\\', '-', ']') // '\\' c matches character c // lo '-' hi matches character c for lo <= c <= hi func nameMatch(pattern, path string) bool { matched, e := filepath.Match(pattern, filepath.Base(path)) errorIf(probe.NewError(e).Trace(pattern, path), "Unable to match with input pattern.") if !matched { for _, pathComponent := range strings.Split(path, "/") { matched = pathComponent == pattern if matched { break } } } return matched } func patternMatch(pattern, match string) bool { pattern = strings.ToLower(pattern) match = strings.ToLower(match) return wildcard.Match(pattern, match) } // pathMatch reports whether path matches the wildcard pattern. // supports '*' and '?' wildcards in the pattern string. // unlike path.Match(), considers a path as a flat name space // while matching the pattern. The difference is illustrated in // the example here https://play.golang.org/p/Ega9qgD4Qz . func pathMatch(pattern, path string) bool { return wildcard.Match(pattern, path) } func getExitStatus(err error) int { if err == nil { return 0 } if pe, ok := err.(*exec.ExitError); ok { if es, ok := pe.Sys().(syscall.WaitStatus); ok { return es.ExitStatus() } } return 1 } // execFind executes the input command line, additionally formats input // for the command line in accordance with subsititution arguments. func execFind(ctx context.Context, args string, fileContent contentMessage) { split, err := shlex.Split(args) if err != nil { console.Println(console.Colorize("FindExecErr", "Unable to parse --exec: "+err.Error())) os.Exit(getExitStatus(err)) } if len(split) == 0 { return } for i, arg := range split { split[i] = stringsReplace(ctx, arg, fileContent) } cmd := exec.Command(split[0], split[1:]...) var out bytes.Buffer var stderr bytes.Buffer cmd.Stdout = &out cmd.Stderr = &stderr if err := cmd.Run(); err != nil { if stderr.Len() > 0 { console.Println(console.Colorize("FindExecErr", strings.TrimSpace(stderr.String()))) } console.Println(console.Colorize("FindExecErr", err.Error())) // Return exit status of the command run os.Exit(getExitStatus(err)) } console.PrintC(out.String()) } // watchFind - enables listening on the input path, listens for all file/object // created actions. Asynchronously executes the input command line, also allows // formatting for the command line in accordance with subsititution arguments. func watchFind(ctxCtx context.Context, ctx *findContext) { // Watch is not enabled, return quickly. if !ctx.watch { return } options := WatchOptions{ Recursive: true, Events: []string{"put"}, } watchObj, err := ctx.clnt.Watch(ctxCtx, options) fatalIf(err.Trace(ctx.targetAlias), "Unable to watch with given options.") // Loop until user CTRL-C the command line. for { select { case <-globalContext.Done(): console.Println() close(watchObj.DoneChan) return case events, ok := <-watchObj.Events(): if !ok { return } for _, event := range events { time, e := time.Parse(time.RFC3339, event.Time) if e != nil { errorIf(probe.NewError(e).Trace(event.Time), "Unable to parse event time.") continue } find(ctxCtx, ctx, contentMessage{ Key: getAliasedPath(ctx, event.Path), Time: time, Size: event.Size, }) } case err, ok := <-watchObj.Errors(): if !ok { return } errorIf(err, "Unable to watch for events.") return } } } // Descend at most (a non-negative integer) levels of files // below the starting-prefix and trims the suffix. This function // returns path as is without manipulation if the maxDepth is 0 // i.e (not set). func trimSuffixAtMaxDepth(startPrefix, path, separator string, maxDepth uint) string { if maxDepth == 0 { return path } // Remove the requested prefix from consideration, maxDepth is // only considered for all other levels excluding the starting prefix. path = strings.TrimPrefix(path, startPrefix) pathComponents := strings.SplitAfter(path, separator) if len(pathComponents) >= int(maxDepth) { pathComponents = pathComponents[:maxDepth] } pathComponents = append([]string{startPrefix}, pathComponents...) return strings.Join(pathComponents, "") } // Get aliased path used finally in printing, trim paths to ensure // that we have removed the fully qualified paths and original // start prefix (targetAlias) is retained. This function also honors // maxDepth if set then the resultant path will be trimmed at requested // maxDepth. func getAliasedPath(ctx *findContext, path string) string { separator := string(ctx.clnt.GetURL().Separator) prefixPath := ctx.clnt.GetURL().String() var aliasedPath string if ctx.targetAlias != "" { aliasedPath = ctx.targetAlias + strings.TrimPrefix(path, strings.TrimSuffix(ctx.targetFullURL, separator)) } else { aliasedPath = path // look for prefix path, if found filter at that, Watch calls // for example always provide absolute path. So for relative // prefixes we need to employ this kind of code. if i := strings.Index(path, prefixPath); i > 0 { aliasedPath = path[i:] } } return trimSuffixAtMaxDepth(ctx.targetURL, aliasedPath, separator, ctx.maxDepth) } func find(ctxCtx context.Context, ctx *findContext, fileContent contentMessage) { // Match the incoming content, didn't match return. if !matchFind(ctx, fileContent) { return } // For all matching content // proceed to either exec, format the output string. if ctx.execCmd != "" { execFind(ctxCtx, ctx.execCmd, fileContent) return } if ctx.printFmt != "" { fileContent.Key = stringsReplace(ctxCtx, ctx.printFmt, fileContent) } printMsg(findMessage{fileContent}) } // doFind - find is main function body which interprets and executes // all the input parameters. func doFind(ctxCtx context.Context, ctx *findContext) error { // If watch is enabled we will wait on the prefix perpetually // for all I/O events until canceled by user, if watch is not enabled // following defer is a no-op. defer watchFind(ctxCtx, ctx) lstOptions := ListOptions{ WithOlderVersions: ctx.withVersions, WithDeleteMarkers: ctx.withVersions, Recursive: true, ShowDir: DirFirst, WithMetadata: len(ctx.matchMeta) > 0 || len(ctx.matchTags) > 0, } // iterate over all content which is within the given directory for content := range ctx.clnt.List(globalContext, lstOptions) { if content.Err != nil { switch content.Err.ToGoError().(type) { // handle this specifically for filesystem related errors. case BrokenSymlink: errorIf(content.Err.Trace(ctx.clnt.GetURL().String()), "Unable to list broken link.") continue case TooManyLevelsSymlink: errorIf(content.Err.Trace(ctx.clnt.GetURL().String()), "Unable to list too many levels link.") continue case PathNotFound: errorIf(content.Err.Trace(ctx.clnt.GetURL().String()), "Unable to list folder.") continue case PathInsufficientPermission: errorIf(content.Err.Trace(ctx.clnt.GetURL().String()), "Unable to list folder.") continue } fatalIf(content.Err.Trace(ctx.clnt.GetURL().String()), "Unable to list folder.") continue } if content.StorageClass == s3StorageClassGlacier { continue } fileKeyName := getAliasedPath(ctx, content.URL.String()) fileContent := contentMessage{ Key: fileKeyName, VersionID: content.VersionID, Time: content.Time.Local(), Size: content.Size, Metadata: content.UserMetadata, Tags: content.Tags, } // Match the incoming content, didn't match return. if !matchFind(ctx, fileContent) { continue } // For all matching content // proceed to either exec, format the output string. if ctx.execCmd != "" { execFind(ctxCtx, ctx.execCmd, fileContent) continue } if ctx.printFmt != "" { fileContent.Key = stringsReplace(ctxCtx, ctx.printFmt, fileContent) } printMsg(findMessage{fileContent}) } // Success, notice watch will execute in defer only if enabled and this call // will return after watch is canceled. return nil } // stringsReplace - formats the string to remove {} and replace each // with the appropriate argument func stringsReplace(ctx context.Context, args string, fileContent contentMessage) string { // replace all instances of {} str := args str = strings.ReplaceAll(str, "{}", fileContent.Key) // replace all instances of {""} str = strings.ReplaceAll(str, `{""}`, strconv.Quote(fileContent.Key)) // replace all instances of {base} str = strings.ReplaceAll(str, "{base}", filepath.Base(fileContent.Key)) // replace all instances of {"base"} str = strings.ReplaceAll(str, `{"base"}`, strconv.Quote(filepath.Base(fileContent.Key))) // replace all instances of {dir} str = strings.ReplaceAll(str, "{dir}", filepath.Dir(fileContent.Key)) // replace all instances of {"dir"} str = strings.ReplaceAll(str, `{"dir"}`, strconv.Quote(filepath.Dir(fileContent.Key))) // replace all instances of {size} str = strings.ReplaceAll(str, "{size}", humanize.IBytes(uint64(fileContent.Size))) // replace all instances of {"size"} str = strings.ReplaceAll(str, `{"size"}`, strconv.Quote(humanize.IBytes(uint64(fileContent.Size)))) // replace all instances of {time} str = strings.ReplaceAll(str, "{time}", fileContent.Time.Format(printDate)) // replace all instances of {"time"} str = strings.ReplaceAll(str, `{"time"}`, strconv.Quote(fileContent.Time.Format(printDate))) // replace all instances of {url} if strings.Contains(str, "{url}") { str = strings.ReplaceAll(str, "{url}", getShareURL(ctx, fileContent.Key)) } // replace all instances of {"url"} if strings.Contains(str, `{"url"}`) { str = strings.ReplaceAll(str, `{"url"}`, strconv.Quote(getShareURL(ctx, fileContent.Key))) } // replace all instances of {version} str = strings.ReplaceAll(str, `{version}`, fileContent.VersionID) // replace all instances of {"version"} str = strings.ReplaceAll(str, `{"version"}`, strconv.Quote(fileContent.VersionID)) return str } // matchFind matches whether fileContent matches appropriately with standard // "pattern matching" flags requested by the user, such as "name", "path", "regex" ..etc. func matchFind(ctx *findContext, fileContent contentMessage) (match bool) { match = true prefixPath := ctx.targetURL // Add separator only if targetURL doesn't already have separator. if !strings.HasPrefix(prefixPath, string(ctx.clnt.GetURL().Separator)) { prefixPath = ctx.targetURL + string(ctx.clnt.GetURL().Separator) } // Trim the prefix such that we will apply file path matching techniques // on path excluding the starting prefix. path := strings.TrimPrefix(fileContent.Key, prefixPath) if match && ctx.ignorePattern != "" { match = !pathMatch(ctx.ignorePattern, path) } if match && ctx.namePattern != "" { match = nameMatch(ctx.namePattern, path) } if match && ctx.pathPattern != "" { match = pathMatch(ctx.pathPattern, path) } if match && ctx.regexPattern != nil { match = ctx.regexPattern.MatchString(path) } if match && ctx.olderThan != "" { match = !isOlder(fileContent.Time, ctx.olderThan) } if match && ctx.newerThan != "" { match = !isNewer(fileContent.Time, ctx.newerThan) } if match && ctx.largerSize > 0 { match = int64(ctx.largerSize) < fileContent.Size } if match && ctx.smallerSize > 0 { match = int64(ctx.smallerSize) > fileContent.Size } if match && len(ctx.matchMeta) > 0 { match = matchMetadataRegexMaps(ctx.matchMeta, fileContent.Metadata) } if match && len(ctx.matchTags) > 0 { match = matchRegexMaps(ctx.matchTags, fileContent.Tags) } return match } // 7 days in seconds. var defaultSevenDays = time.Duration(604800) * time.Second // getShareURL is used in conjunction with the {url} substitution // argument to generate and return presigned URLs, returns error if any. func getShareURL(ctx context.Context, path string) string { targetAlias, targetURLFull, _, err := expandAlias(path) fatalIf(err.Trace(path), "Unable to expand alias.") clnt, err := newClientFromAlias(targetAlias, targetURLFull) fatalIf(err.Trace(targetAlias, targetURLFull), "Unable to initialize client instance from alias.") content, err := clnt.Stat(ctx, StatOptions{}) fatalIf(err.Trace(targetURLFull, targetAlias), "Unable to lookup file/object.") // Skip if it is a directory. if content.Type.IsDir() { return "" } objectURL := content.URL.String() newClnt, err := newClientFromAlias(targetAlias, objectURL) fatalIf(err.Trace(targetAlias, objectURL), "Unable to initialize new client from alias.") // Set default expiry for each url (point of no longer valid), to be 7 days shareURL, err := newClnt.ShareDownload(ctx, "", defaultSevenDays) fatalIf(err.Trace(targetAlias, objectURL), "Unable to generate share url.") return shareURL } // getRegexMap returns a map from the StringSlice key. // Each entry must be key=regex. // Will exit with error if an un-parsable entry is found. func getRegexMap(cliCtx *cli.Context, key string) map[string]*regexp.Regexp { sl := cliCtx.StringSlice(key) if len(sl) == 0 { return nil } reMap := make(map[string]*regexp.Regexp, len(sl)) for _, v := range sl { split := strings.SplitN(v, "=", 2) if len(split) < 2 { err := probe.NewError(fmt.Errorf("want one = separator, got none")) fatalIf(err.Trace(v), "Unable to split key+value. Must be key=regex") } // No value means it should not exist or be empty. if len(split[1]) == 0 { reMap[split[0]] = nil continue } // Normalize character encoding. norm := norm.NFC.String(split[1]) var err error reMap[split[0]], err = regexp.Compile(norm) if err != nil { fatalIf(probe.NewError(err), fmt.Sprintf("Unable to compile metadata regex for %s=%s", split[0], split[1])) } } return reMap } // matchRegexMaps will check if all regexes in 'm' match values in 'v' with the same key. // If a regex is nil, it must either not exist in v or have a 0 length value. func matchRegexMaps(m map[string]*regexp.Regexp, v map[string]string) bool { for k, reg := range m { if reg == nil { if v[k] != "" { return false } // Does not exist or empty, that is fine. continue } val, ok := v[k] if !ok || !reg.MatchString(norm.NFC.String(val)) { return false } } return true } // matchMetadataRegexMaps will check if all regexes in 'm' match values in 'v' with the same key. // If a regex is nil, it must either not exist in v or have a 0 length value. func matchMetadataRegexMaps(m map[string]*regexp.Regexp, v map[string]string) bool { for k, reg := range m { if reg == nil { if v[k] != "" { return false } // Does not exist or empty, that is fine. continue } val, ok := v[k] if !ok { val, ok = v[http.CanonicalHeaderKey(fmt.Sprintf("X-Amz-Meta-%s", k))] } if !ok || !reg.MatchString(norm.NFC.String(val)) { return false } } return true } minio-client-0.0~20250403/cmd/find_test.go000066400000000000000000000275251477450377600200660ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "os/exec" "regexp" "runtime" "strings" "testing" "time" ) // Tests match find function with all supported inputs on // file pattern, size and time. func TestMatchFind(t *testing.T) { // List of various contexts used in each tests, // tests are run in the same order as this list. listFindContexts := []*findContext{ { clnt: &S3Client{ targetURL: &ClientURL{}, }, ignorePattern: "*.go", }, { clnt: &S3Client{ targetURL: &ClientURL{}, }, namePattern: "console", }, { clnt: &S3Client{ targetURL: &ClientURL{}, }, pathPattern: "*console*", }, { clnt: &S3Client{ targetURL: &ClientURL{}, }, regexPattern: regexp.MustCompile(`^(\d+\.){3}\d+$`), }, { clnt: &S3Client{ targetURL: &ClientURL{}, }, olderThan: "1d", }, { clnt: &S3Client{ targetURL: &ClientURL{}, }, newerThan: "32000d", }, { clnt: &S3Client{ targetURL: &ClientURL{}, }, largerSize: 1024 * 1024, }, { clnt: &S3Client{ targetURL: &ClientURL{}, }, smallerSize: 1024, }, { clnt: &S3Client{ targetURL: &ClientURL{}, }, ignorePattern: "*.txt", }, { clnt: &S3Client{ targetURL: &ClientURL{}, }, }, } testCases := []struct { content contentMessage expectedMatch bool }{ // Matches ignore pattern, so match will be false - Test 1. { content: contentMessage{ Key: "pkg/console/console.go", }, expectedMatch: false, }, // Matches name pattern - Test 2. { content: contentMessage{ Key: "pkg/console/console.go", }, expectedMatch: true, }, // Matches path pattern - Test 3. { content: contentMessage{ Key: "pkg/console/console.go", }, expectedMatch: true, }, // Matches regex pattern - Test 4. { content: contentMessage{ Key: "192.168.1.1", }, expectedMatch: true, }, // Matches older than time - Test 5. { content: contentMessage{ Time: time.Unix(11999, 0).UTC(), }, expectedMatch: true, }, // Matches newer than time - Test 6. { content: contentMessage{ Time: time.Unix(12001, 0).UTC(), }, expectedMatch: true, }, // Matches size larger - Test 7. { content: contentMessage{ Size: 1024 * 1024 * 2, }, expectedMatch: true, }, // Matches size smaller - Test 8. { content: contentMessage{ Size: 1023, }, expectedMatch: true, }, // Does not match ignore pattern, so match will be true - Test 9. { content: contentMessage{ Key: "pkg/console/console.go", }, expectedMatch: true, }, // No matching inputs were provided, so nothing to match return value is true - Test 10. { content: contentMessage{}, expectedMatch: true, }, } // Runs all the test cases and validate the expected conditions. for i, testCase := range testCases { gotMatch := matchFind(listFindContexts[i], testCase.content) if testCase.expectedMatch != gotMatch { t.Errorf("Test: %d, expected match %t, got %t", i+1, testCase.expectedMatch, gotMatch) } } } // Tests suffix strings trimmed off correctly at maxdepth. func TestSuffixTrimmingAtMaxDepth(t *testing.T) { testCases := []struct { startPrefix string path string separator string maxDepth uint expectedNewPath string }{ // Tests at max depth 0. { startPrefix: "./", path: ".git/refs/remotes", separator: "/", maxDepth: 0, expectedNewPath: ".git/refs/remotes", }, // Tests at max depth 1. { startPrefix: "./", path: ".git/refs/remotes", separator: "/", maxDepth: 1, expectedNewPath: "./.git/", }, // Tests at max depth 2. { startPrefix: "./", path: ".git/refs/remotes", separator: "/", maxDepth: 2, expectedNewPath: "./.git/refs/", }, // Tests at max depth 3. { startPrefix: "./", path: ".git/refs/remotes", separator: "/", maxDepth: 3, expectedNewPath: "./.git/refs/remotes", }, // Tests with startPrefix empty. { startPrefix: "", path: ".git/refs/remotes", separator: "/", maxDepth: 2, expectedNewPath: ".git/refs/", }, // Tests with separator empty. { startPrefix: "", path: ".git/refs/remotes", separator: "", maxDepth: 2, expectedNewPath: ".g", }, // Tests with nested startPrefix paths - 1. { startPrefix: ".git/refs/", path: ".git/refs/remotes", separator: "/", maxDepth: 1, expectedNewPath: ".git/refs/remotes", }, // Tests with nested startPrefix paths - 2. { startPrefix: ".git/refs", path: ".git/refs/remotes", separator: "/", maxDepth: 1, expectedNewPath: ".git/refs/", }, } // Run all the test cases and validate for returned new path. for i, testCase := range testCases { gotNewPath := trimSuffixAtMaxDepth(testCase.startPrefix, testCase.path, testCase.separator, testCase.maxDepth) if testCase.expectedNewPath != gotNewPath { t.Errorf("Test: %d, expected path %s, got %s", i+1, testCase.expectedNewPath, gotNewPath) } } } // Tests matching functions for name, path and regex. func TestFindMatch(t *testing.T) { // testFind is the structure used to contain params pertinent to find related tests type testFind struct { pattern, filePath, flagName string match bool } basicTests := []testFind{ // Name match tests - success cases. {"*.jpg", "carter.jpg", "name", true}, {"console", "pkg/console/console.go", "name", true}, {"console.go", "pkg/console/console.go", "name", true}, {"*XA==", "I/enjoy/morning/walks/XA==", "name ", true}, {"*parser", "/This/might/mess up./the/parser", "name", true}, {"*LTIxNDc0ODM2NDgvLTE=", "What/A/Naughty/String/LTIxNDc0ODM2NDgvLTE=", "name", true}, {"*", "/bla/bla/bla/ ", "name", true}, // Name match tests - failure cases. {"*.jpg", "carter.jpeg", "name", false}, {"*/test/*", "/test/bob/likes/cake", "name", false}, {"*test/*", "bob/test/likes/cake", "name", false}, {"*test/*", "bob/likes/test/cake", "name", false}, {"*/test/*", "bob/likes/cake/test", "name", false}, {"*.jpg", ".jpg/elves/are/evil", "name", false}, { "wq3YgNiB2ILYg9iE2IXYnNud3I/hoI7igIvigIzigI3igI7igI/igKrigKvigKzigK3igK7igaDi", "An/Even/Bigger/String/wq3YgNiB2ILYg9iE2IXYnNud3I/hoI7igIvigIzigI3igI7igI/igKrigKvigKzigK3igK7igaDi", "name", false, }, {"𝕿𝖍𝖊", "well/this/isAN/odd/font/THE", "name", false}, {"𝕿𝖍𝖊", "well/this/isAN/odd/font/The", "name", false}, {"𝕿𝖍𝖊", "well/this/isAN/odd/font/𝓣𝓱𝓮", "name", false}, {"𝕿𝖍𝖊", "what/a/strange/turn/of/events/𝓣he", "name", false}, {"𝕿𝖍𝖊", "well/this/isAN/odd/font/𝕿𝖍𝖊", "name", true}, // Path match tests - success cases. {"*/test/*", "bob/test/likes/cake", "path", true}, {"*/test/*", "/test/bob/likes/cake", "path", true}, // Path match tests - failure cases. {"*.jpg", ".jpg/elves/are/evil", "path", false}, {"*/test/*", "test1/test2/test3/test", "path", false}, {"*/ test /*", "test/test1/test2/test3/test", "path", false}, {"*/test/*", " test /I/have/Really/Long/hair", "path", false}, {"*XA==", "XA==/Height/is/a/social/construct", "path", false}, {"*W", "/Word//this/is a/trickyTest", "path", false}, {"LTIxNDc0ODM2NDgvLTE=", "LTIxNDc0ODM2NDgvLTE=/I/Am/One/Baaaaad/String", "path", false}, {"/", "funky/path/name", "path", false}, } for _, test := range basicTests { switch test.flagName { case "name": testMatch := nameMatch(test.pattern, test.filePath) if testMatch != test.match { t.Fatalf("Unexpected result %t, with pattern %s, flag %s and filepath %s \n", !test.match, test.pattern, test.flagName, test.filePath) } case "path": testMatch := pathMatch(test.pattern, test.filePath) if testMatch != test.match { t.Fatalf("Unexpected result %t, with pattern %s, flag %s and filepath %s \n", !test.match, test.pattern, test.flagName, test.filePath) } } } } // Tests string substitution function. func TestStringReplace(t *testing.T) { testCases := []struct { str string expectedStr string content contentMessage }{ // Tests string replace {} without quotes. { str: "{}", expectedStr: "path/1", content: contentMessage{Key: "path/1"}, }, // Tests string replace {} with quotes. { str: `{""}`, expectedStr: `"path/1"`, content: contentMessage{Key: "path/1"}, }, // Tests string replace {base} { str: "{base}", expectedStr: "1", content: contentMessage{Key: "path/1"}, }, // Tests string replace {"base"} with quotes. { str: `{"base"}`, expectedStr: `"1"`, content: contentMessage{Key: "path/1"}, }, // Tests string replace {dir} { str: `{dir}`, expectedStr: `path`, content: contentMessage{Key: "path/1"}, }, // Tests string replace {"dir"} with quotes. { str: `{"dir"}`, expectedStr: `"path"`, content: contentMessage{Key: "path/1"}, }, // Tests string replace {"size"} with quotes. { str: `{"size"}`, expectedStr: `"0 B"`, content: contentMessage{Size: 0}, }, // Tests string replace {"time"} with quotes. { str: `{"time"}`, expectedStr: `"2038-01-19 03:14:07 UTC"`, content: contentMessage{ Time: time.Unix(2147483647, 0).UTC(), }, }, // Tests string replace {size} { str: `{size}`, expectedStr: `1.0 MiB`, content: contentMessage{Size: 1024 * 1024}, }, // Tests string replace {time} { str: `{time}`, expectedStr: `2038-01-19 03:14:07 UTC`, content: contentMessage{ Time: time.Unix(2147483647, 0).UTC(), }, }, } for i, testCase := range testCases { gotStr := stringsReplace(context.Background(), testCase.str, testCase.content) if gotStr != testCase.expectedStr { t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.expectedStr, gotStr) } } } // Tests exit status, getExitStatus() function func TestGetExitStatus(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("Skipping on non-linux") return } testCases := []struct { command string expectedExitStatus int }{ // Tests "No such file or directory", exit status code 2 { command: "ls asdf", expectedExitStatus: 2, }, { command: "cp x x", expectedExitStatus: 1, }, // expectedExitStatus for "command not found" case is 127, // but exec command cannot capture anything since a process // for the command could not be started at all, // so the expectedExitStatus is 1 { command: "asdf", expectedExitStatus: 1, }, } for i, testCase := range testCases { commandArgs := strings.Split(testCase.command, " ") cmd := exec.Command(commandArgs[0], commandArgs[1:]...) // Return exit status of the command run exitStatus := getExitStatus(cmd.Run()) if exitStatus != testCase.expectedExitStatus { t.Errorf("Test %d: Expected error status code for command \"%v\" is %v, got %v", i+1, testCase.command, testCase.expectedExitStatus, exitStatus) } } } minio-client-0.0~20250403/cmd/flags.go000066400000000000000000000111001477450377600171610ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "errors" "fmt" "strings" "time" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" ) const envPrefix = "MC_" // Collection of mc flags currently supported var globalFlags = []cli.Flag{ cli.StringFlag{ Name: "config-dir, C", Value: mustGetMcConfigDir(), Usage: "path to configuration folder", EnvVar: envPrefix + "CONFIG_DIR", }, cli.BoolFlag{ Name: "quiet, q", Usage: "disable progress bar display", EnvVar: envPrefix + "QUIET", }, cli.BoolFlag{ Name: "disable-pager, dp", Usage: "disable mc internal pager and print to raw stdout", EnvVar: envPrefix + globalDisablePagerEnv, Hidden: false, }, cli.BoolFlag{ Name: "no-color", Usage: "disable color theme", EnvVar: envPrefix + "NO_COLOR", }, cli.BoolFlag{ Name: "json", Usage: "enable JSON lines formatted output", EnvVar: envPrefix + "JSON", }, cli.BoolFlag{ Name: "debug", Usage: "enable debug output", EnvVar: envPrefix + "DEBUG", }, cli.StringSliceFlag{ Name: "resolve", Usage: "resolves HOST[:PORT] to an IP address. Example: minio.local:9000=10.10.75.1", EnvVar: envPrefix + "RESOLVE", }, cli.BoolFlag{ Name: "insecure", Usage: "disable SSL certificate verification", EnvVar: envPrefix + "INSECURE", }, cli.StringFlag{ Name: "limit-upload", Usage: "limits uploads to a maximum rate in KiB/s, MiB/s, GiB/s. (default: unlimited)", EnvVar: envPrefix + "LIMIT_UPLOAD", }, cli.StringFlag{ Name: "limit-download", Usage: "limits downloads to a maximum rate in KiB/s, MiB/s, GiB/s. (default: unlimited)", EnvVar: envPrefix + "LIMIT_DOWNLOAD", }, cli.DurationFlag{ Name: "conn-read-deadline", Usage: "custom connection READ deadline", Hidden: true, Value: 10 * time.Minute, }, cli.DurationFlag{ Name: "conn-write-deadline", Usage: "custom connection WRITE deadline", Hidden: true, Value: 10 * time.Minute, }, cli.StringSliceFlag{ Name: "custom-header,H", Usage: "add custom HTTP header to the request. 'key:value' format.", }, } // bundled encryption flags var encFlags = []cli.Flag{ encCFlag, encKSMFlag, encS3Flag, } var encCFlag = cli.StringSliceFlag{ Name: "enc-c", Usage: "encrypt/decrypt objects using client provided keys. (multiple keys can be provided) Formats: RawBase64 or Hex.", } var encKSMFlag = cli.StringSliceFlag{ Name: "enc-kms", Usage: "encrypt/decrypt objects using specific server-side encryption keys. (multiple keys can be provided)", EnvVar: envPrefix + "ENC_KMS", } var encS3Flag = cli.StringSliceFlag{ Name: "enc-s3", Usage: "encrypt/decrypt objects using server-side default keys and configurations. (multiple keys can be provided).", EnvVar: envPrefix + "ENC_S3", } var checksumFlag = cli.StringFlag{ Name: "checksum", Usage: "Add checksum to uploaded object. Values: CRC64NVME, CRC32, CRC32C, SHA1 or SHA256. Requires server trailing headers (AWS, MinIO)", Value: "", } func parseChecksum(ctx *cli.Context) (useMD5 bool, ct minio.ChecksumType) { useMD5 = ctx.Bool("md5") if cs := ctx.String("checksum"); cs != "" { switch strings.ToUpper(cs) { case "CRC32": ct = minio.ChecksumCRC32 case "CRC32C": ct = minio.ChecksumCRC32C case "CRC32-FO": ct = minio.ChecksumFullObjectCRC32 case "CRC32C-FO": ct = minio.ChecksumFullObjectCRC32C case "SHA1": ct = minio.ChecksumSHA1 case "SHA256": ct = minio.ChecksumSHA256 case "CRC64N", "CRC64NVME": ct = minio.ChecksumCRC64NVME case "MD5": useMD5 = true default: err := fmt.Errorf("unknown checksum type: %s. Should be one of CRC64NVME, MD5, CRC32, CRC32C, SHA1 or SHA256", cs) fatalIf(probe.NewError(err), "") } if ct.IsSet() { useTrailingHeaders.Store(true) if useMD5 { err := errors.New("cannot combine MD5 with checksum") fatalIf(probe.NewError(err), "") } } } return } minio-client-0.0~20250403/cmd/fs-pathutils_nix.go000066400000000000000000000015501477450377600213760ustar00rootroot00000000000000//go:build !windows // +build !windows // Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd func normalizePath(path string) string { return path } minio-client-0.0~20250403/cmd/fs-pathutils_windows.go000066400000000000000000000020741477450377600222740ustar00rootroot00000000000000//go:build windows // +build windows // Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "path/filepath" "strings" "syscall" ) func normalizePath(path string) string { if filepath.VolumeName(path) == "" && strings.HasPrefix(path, "\\") { var err error path, err = syscall.FullPath(path) if err != nil { panic(err) } } return path } minio-client-0.0~20250403/cmd/get-main.go000066400000000000000000000100471477450377600175770ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "strings" "github.com/minio/cli" "github.com/minio/pkg/v3/console" ) // get command flags. var ( getFlags = []cli.Flag{ cli.StringFlag{ Name: "version-id, vid", Usage: "get a specific version of an object", }, } ) // Get command. var getCmd = cli.Command{ Name: "get", Usage: "get s3 object to local", Action: mainGet, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(append(globalFlags, encCFlag), getFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] SOURCE TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get an object from MinIO storage to local file system {{.Prompt}} {{.HelpName}} play/mybucket/object path-to/object 2. Get an object from MinIO storage using encryption {{.Prompt}} {{.HelpName}} --enc-c "play/mybucket/object=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" play/mybucket/object path-to/object `, } // mainGet is the entry point for get command. func mainGet(cliCtx *cli.Context) (e error) { args := cliCtx.Args() if len(args) != 2 { showCommandHelpAndExit(cliCtx, 1) // last argument is exit code. } ctx, cancelGet := context.WithCancel(globalContext) defer cancelGet() encryptionKeys, err := validateAndCreateEncryptionKeys(cliCtx) if err != nil { err.Trace(cliCtx.Args()...) } fatalIf(err, "unable to parse encryption keys") // get source and target sourceURLs := args[:len(args)-1] targetURL := args[len(args)-1] getURLsCh := make(chan URLs, 10000) var totalObjects, totalBytes int64 // Store a progress bar or an accounter var pg ProgressReader // Enable progress bar reader only during default mode. if !globalQuiet && !globalJSON { // set up progress bar pg = newProgressBar(totalBytes) } else { pg = newAccounter(totalBytes) } go func() { opts := prepareCopyURLsOpts{ sourceURLs: sourceURLs, targetURL: targetURL, encKeyDB: encryptionKeys, ignoreBucketExistsCheck: true, versionID: cliCtx.String("version-id"), } for getURLs := range prepareGetURLs(ctx, opts) { if getURLs.Error != nil { getURLsCh <- getURLs break } totalObjects++ getURLsCh <- getURLs } close(getURLsCh) }() for { select { case <-ctx.Done(): showLastProgressBar(pg, nil) return case getURLs, ok := <-getURLsCh: if !ok { showLastProgressBar(pg, nil) return } if getURLs.Error != nil { printGetURLsError(&getURLs) showLastProgressBar(pg, getURLs.Error.ToGoError()) return } urls := doCopy(ctx, doCopyOpts{ cpURLs: getURLs, pg: pg, encryptionKeys: encryptionKeys, updateProgressTotal: true, }) if urls.Error != nil { e = urls.Error.ToGoError() showLastProgressBar(pg, e) return } } } } func printGetURLsError(cpURLs *URLs) { // Print in new line and adjust to top so that we // don't print over the ongoing scan bar if !globalQuiet && !globalJSON { console.Eraseline() } if strings.Contains(cpURLs.Error.ToGoError().Error(), " is a folder.") { errorIf(cpURLs.Error.Trace(), "Folder cannot be copied. Please use `...` suffix.") } else { errorIf(cpURLs.Error.Trace(), "Unable to download.") } } minio-client-0.0~20250403/cmd/get-url.go000066400000000000000000000071661477450377600174650ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" ) // prepareGetURLs - prepares target and source clientURLs for copying. func prepareGetURLs(ctx context.Context, o prepareCopyURLsOpts) chan URLs { copyURLsCh := make(chan URLs) go func(o prepareCopyURLsOpts) { defer close(copyURLsCh) copyURLsContent, err := guessGetURLType(ctx, o) if err != nil { copyURLsCh <- URLs{Error: err} return } switch copyURLsContent.copyType { case copyURLsTypeA: copyURLsCh <- prepareCopyURLsTypeA(ctx, *copyURLsContent, o) case copyURLsTypeB: copyURLsCh <- prepareCopyURLsTypeB(ctx, *copyURLsContent, o) default: copyURLsCh <- URLs{Error: errInvalidArgument().Trace(o.sourceURLs...)} } }(o) finalCopyURLsCh := make(chan URLs) go func() { defer close(finalCopyURLsCh) for cpURLs := range copyURLsCh { if cpURLs.Error != nil { finalCopyURLsCh <- cpURLs continue } finalCopyURLsCh <- cpURLs } }() return finalCopyURLsCh } // guessGetURLType guesses the type of clientURL. This approach all allows prepareURL // functions to accurately report failure causes. func guessGetURLType(ctx context.Context, o prepareCopyURLsOpts) (*copyURLsContent, *probe.Error) { cc := new(copyURLsContent) // Extract alias before fiddling with the clientURL. cc.sourceURL = o.sourceURLs[0] cc.sourceAlias, _, _ = mustExpandAlias(cc.sourceURL) // Find alias and expanded clientURL. cc.targetAlias, cc.targetURL, _ = mustExpandAlias(o.targetURL) if len(o.sourceURLs) == 1 { // 1 Source, 1 Target var err *probe.Error client, err := newClient(cc.sourceURL) if err != nil { cc.copyType = copyURLsTypeInvalid return cc, err } s3clnt, ok := client.(*S3Client) if !ok { return cc, probe.NewError(fmt.Errorf("Source is not s3.")) } bucket, path := s3clnt.url2BucketAndObject() if bucket == "" { return cc, probe.NewError(fmt.Errorf("Please set bucket for s3 resource.")) } if path == "" { return cc, probe.NewError(fmt.Errorf("Please set a full path for s3 resource.")) } cc.sourceContent = s3clnt.objectInfo2ClientContent(bucket, minio.ObjectInfo{ Key: path, VersionID: o.versionID, }) client, err = newClient(o.targetURL) if err != nil { cc.copyType = copyURLsTypeInvalid return cc, err } _, ok = client.(*fsClient) if !ok { return cc, probe.NewError(fmt.Errorf("Target is not local filesystem.")) } // If target is a folder, it is Type B. var isDir bool isDir, cc.targetContent = isAliasURLDir(ctx, o.targetURL, o.encKeyDB, o.timeRef, o.ignoreBucketExistsCheck) if isDir { cc.copyType = copyURLsTypeB cc.sourceVersionID = cc.sourceContent.VersionID return cc, nil } // else Type A. cc.copyType = copyURLsTypeA cc.sourceVersionID = cc.sourceContent.VersionID return cc, nil } cc.copyType = copyURLsTypeInvalid return cc, errInvalidArgument().Trace() } minio-client-0.0~20250403/cmd/globals.go000066400000000000000000000154571477450377600175330ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package cmd contains all the global variables and constants. ONLY TO BE ACCESSED VIA GET/SET FUNCTIONS. package cmd import ( "context" "crypto/x509" "fmt" "net" "net/http" "net/netip" "net/url" "os" "strconv" "strings" "time" "github.com/charmbracelet/lipgloss" "github.com/dustin/go-humanize" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/pkg/v3/console" "github.com/muesli/termenv" "golang.org/x/net/http/httpguts" ) const ( globalMCConfigVersion = "10" globalMCConfigFile = "config.json" globalMCCertsDir = "certs" globalMCCAsDir = "CAs" // session config and shared urls related constants globalSessionDir = "session" globalSharedURLsDataDir = "share" globalSessionConfigVersion = "8" // Profile directory for dumping profiler outputs. globalProfileDir = "profile" // Global error exit status. globalErrorExitStatus = 1 // Global CTRL-C (SIGINT, #2) exit status. globalCancelExitStatus = 130 // Global SIGKILL (#9) exit status. globalKillExitStatus = 137 // Global SIGTERM (#15) exit status globalTerminatExitStatus = 143 ) var ( globalQuiet = false // Quiet flag set via command line globalJSON = false // Json flag set via command line globalJSONLine = false // Print json as single line. globalDebug = false // Debug flag set via command line globalNoColor = false // No Color flag set via command line globalInsecure = false // Insecure flag set via command line globalResolvers map[string]netip.Addr // Custom mappings from HOST[:PORT] to IP globalAirgapped = false // Airgapped flag set via command line globalSubnetConfig []madmin.SubsysConfig // Subnet config // GlobalDevMode is set to true if the program is running in development mode GlobalDevMode = false // GlobalSubnetProxyURL is the proxy to be used for communication with subnet GlobalSubnetProxyURL *url.URL globalConnReadDeadline time.Duration globalConnWriteDeadline time.Duration globalLimitUpload uint64 globalLimitDownload uint64 globalContext, globalCancel = context.WithCancel(context.Background()) globalCustomHeader http.Header ) var ( // Terminal height/width, zero if not found globalTermWidth, globalTermHeight int globalDisablePagerEnv = "DISABLE_PAGER" globalDisablePagerFlag = "--disable-pager" globalDisablePagerFlagShort = "--dp" globalPagerDisabled = false globalHelpPager *termPager // CA root certificates, a nil value means system certs pool will be used globalRootCAs *x509.CertPool ) func parsePagerDisableFlag(args []string) { globalPagerDisabled, _ = strconv.ParseBool(os.Getenv(envPrefix + globalDisablePagerEnv)) for _, arg := range args { if arg == globalDisablePagerFlag || arg == globalDisablePagerFlagShort { globalPagerDisabled = true } } } // Set global states. NOTE: It is deliberately kept monolithic to ensure we dont miss out any flags. func setGlobalsFromContext(ctx *cli.Context) error { quiet := ctx.Bool("quiet") || ctx.GlobalBool("quiet") debug := ctx.Bool("debug") || ctx.GlobalBool("debug") json := ctx.Bool("json") || ctx.GlobalBool("json") noColor := ctx.Bool("no-color") || ctx.GlobalBool("no-color") insecure := ctx.Bool("insecure") || ctx.GlobalBool("insecure") devMode := ctx.Bool("dev") || ctx.GlobalBool("dev") airgapped := ctx.Bool("airgap") || ctx.GlobalBool("airgap") globalQuiet = globalQuiet || quiet globalDebug = globalDebug || debug globalJSONLine = !isTerminal() && json globalJSON = globalJSON || json globalNoColor = globalNoColor || noColor || globalJSONLine globalInsecure = globalInsecure || insecure GlobalDevMode = GlobalDevMode || devMode globalAirgapped = globalAirgapped || airgapped // Disable colorified messages if requested. if globalNoColor || globalQuiet { console.SetColorOff() lipgloss.SetColorProfile(termenv.Ascii) } globalConnReadDeadline = ctx.Duration("conn-read-deadline") if globalConnReadDeadline <= 0 { globalConnReadDeadline = ctx.GlobalDuration("conn-read-deadline") } globalConnWriteDeadline = ctx.Duration("conn-write-deadline") if globalConnWriteDeadline <= 0 { globalConnWriteDeadline = ctx.GlobalDuration("conn-write-deadline") } limitUploadStr := ctx.String("limit-upload") if limitUploadStr == "" { limitUploadStr = ctx.GlobalString("limit-upload") } if limitUploadStr != "" { var e error globalLimitUpload, e = humanize.ParseBytes(limitUploadStr) if e != nil { return e } } limitDownloadStr := ctx.String("limit-download") if limitDownloadStr == "" { limitDownloadStr = ctx.GlobalString("limit-download") } if limitDownloadStr != "" { var e error globalLimitDownload, e = humanize.ParseBytes(limitDownloadStr) if e != nil { return e } } dnsEntries := ctx.StringSlice("resolve") if len(dnsEntries) > 0 { globalResolvers = make(map[string]netip.Addr, len(dnsEntries)) // Each entry is a HOST[:PORT]=IP pair. This is very similar to cURL's syntax. for _, e := range dnsEntries { i := strings.IndexByte(e, '=') if i < 0 { return fmt.Errorf("invalid DNS resolve entry %s", e) } if strings.ContainsRune(e[:i], ':') { if _, _, err := net.SplitHostPort(e[:i]); err != nil { return fmt.Errorf("invalid DNS resolve entry %s: %v", e, err) } } host := e[:i] addr, err := netip.ParseAddr(e[i+1:]) if err != nil { return fmt.Errorf("invalid DNS resolve entry %s: %v", e, err) } globalResolvers[host] = addr } } customHeaders := ctx.StringSlice("custom-header") if len(customHeaders) > 0 { globalCustomHeader = make(http.Header) for _, header := range customHeaders { i := strings.IndexByte(header, ':') if i <= 0 { return fmt.Errorf("invalid custom header entry %s", header) } h := strings.TrimSpace(header[:i]) hv := strings.TrimSpace(header[i+1:]) if !httpguts.ValidHeaderFieldName(h) || !httpguts.ValidHeaderFieldValue(hv) { return fmt.Errorf("invalid custom header entry %s", header) } globalCustomHeader.Add(h, hv) } } return nil } minio-client-0.0~20250403/cmd/head-main.go000066400000000000000000000137401477450377600177240ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "compress/bzip2" "compress/gzip" "context" "errors" "io" "os" "strings" "syscall" "time" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" ) var headFlags = []cli.Flag{ cli.Int64Flag{ Name: "n,lines", Usage: "print the first 'n' lines", Value: 10, }, cli.StringFlag{ Name: "rewind", Usage: "select an object version at specified time", }, cli.StringFlag{ Name: "version-id, vid", Usage: "select an object version to display", }, cli.BoolFlag{ Name: "zip", Usage: "extract from remote zip file (MinIO server source only)", }, } // Display contents of a file. var headCmd = cli.Command{ Name: "head", Usage: "display first 'n' lines of an object", Action: mainHead, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(append(headFlags, encCFlag), globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} NOTE: '{{.HelpName}}' automatically decompresses 'gzip', 'bzip2' compressed objects. EXAMPLES: 1. Display only first line from a 'gzip' compressed object on Amazon S3. {{.Prompt}} {{.HelpName}} -n 1 s3/csv-data/population.csv.gz 2. Display only first line from server encrypted object on Amazon S3. {{.Prompt}} {{.HelpName}} -n 1 --enc-c 's3/csv-data=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA' s3/csv-data/population.csv 3. Display only first line from server encrypted object on Amazon S3. {{.Prompt}} {{.HelpName}} --enc-c "s3/json-data=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" s3/json-data/population.json 4. Display the first lines of a specific object version. {{.Prompt}} {{.HelpName}} --version-id "3ddac055-89a7-40fa-8cd3-530a5581b6b8" s3/json-data/population.json `, } // headURL displays contents of a URL to stdout. func headURL(sourceURL, sourceVersion string, timeRef time.Time, encKeyDB map[string][]prefixSSEPair, nlines int64, zip bool) *probe.Error { var reader io.ReadCloser switch sourceURL { case "-": reader = os.Stdin default: var err *probe.Error var content *ClientContent if reader, content, err = getSourceStreamMetadataFromURL(context.Background(), sourceURL, sourceVersion, timeRef, encKeyDB, zip); err != nil { return err.Trace(sourceURL) } ctype := content.Metadata["Content-Type"] if strings.Contains(ctype, "gzip") { var e error reader, e = gzip.NewReader(reader) if e != nil { return probe.NewError(e) } defer reader.Close() } else if strings.Contains(ctype, "bzip") { defer reader.Close() reader = io.NopCloser(bzip2.NewReader(reader)) } else { defer reader.Close() } } return headOut(reader, nlines).Trace(sourceURL) } // headOut reads from reader stream and writes to stdout. Also check the length of the // read bytes against size parameter (if not -1) and return the appropriate error func headOut(r io.Reader, nlines int64) *probe.Error { var stdout io.Writer // In case of a user showing the object content in a terminal, // avoid printing control and other bad characters to avoid // terminal session corruption if isTerminal() { stdout = newPrettyStdout(os.Stdout) } else { stdout = os.Stdout } // Initialize a new scanner. br := bufio.NewReader(r) // Negative number of lines means default number of lines. if nlines < 0 { nlines = 10 } for nlines > 0 { line, _, err := br.ReadLine() if err != nil && !errors.Is(err, io.EOF) { return probe.NewError(err) } if errors.Is(err, io.EOF) { break } if _, e := stdout.Write(line); e != nil { switch e := e.(type) { case *os.PathError: if e.Err == syscall.EPIPE { // stdout closed by the user. Gracefully exit. return nil } return probe.NewError(e) default: return probe.NewError(e) } } stdout.Write([]byte("\n")) nlines-- } return nil } // parseHeadSyntax performs command-line input validation for head command. func parseHeadSyntax(ctx *cli.Context) (args []string, versionID string, timeRef time.Time) { args = ctx.Args() versionID = ctx.String("version-id") rewind := ctx.String("rewind") if versionID != "" && rewind != "" { fatalIf(errInvalidArgument().Trace(), "You cannot specify --version-id and --rewind at the same time") } if versionID != "" && len(args) != 1 { fatalIf(errInvalidArgument().Trace(), "You need to pass at least one argument if --version-id is specified") } timeRef = parseRewindFlag(rewind) return } // mainHead is the main entry point for head command. func mainHead(ctx *cli.Context) error { // Parse encryption keys per command. encryptionKeys, err := validateAndCreateEncryptionKeys(ctx) fatalIf(err, "Unable to parse encryption keys.") args, versionID, timeRef := parseHeadSyntax(ctx) stdinMode := len(args) == 0 // handle std input data. if stdinMode { fatalIf(headOut(os.Stdin, ctx.Int64("lines")).Trace(), "Unable to read from standard input.") return nil } // Convert arguments to URLs: expand alias, fix format. for _, url := range ctx.Args() { err = headURL( url, versionID, timeRef, encryptionKeys, ctx.Int64("lines"), ctx.Bool("zip"), ) fatalIf(err.Trace(url), "Unable to read from `"+url+"`.") } return nil } minio-client-0.0~20250403/cmd/health.go000066400000000000000000000027171477450377600173500ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "time" // HealthReportInfo - interface to be implemented by health report schema struct type HealthReportInfo interface { GetTimestamp() time.Time GetStatus() string GetError() string message } // HealthReportHeader - Header of the subnet health report // expected to generate JSON output of the form // {"subnet":{"health":{"version":"v1"}}} type HealthReportHeader struct { Subnet Health `json:"subnet"` } // Health - intermediate struct for subnet health header // Just used to achieve the JSON structure we want type Health struct { Health SchemaVersion `json:"health"` } // SchemaVersion - version of the health report schema type SchemaVersion struct { Version string `json:"version"` } minio-client-0.0~20250403/cmd/health_v1.go000066400000000000000000000212541477450377600177530ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "encoding/json" "reflect" "sync" "time" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/set" "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/disk" "github.com/shirou/gopsutil/v3/mem" ) // HwServersV1 - hardware health Info type HwServersV1 struct { Servers []HwServerV1 `json:"servers,omitempty"` } // HwServerV1 - server health Info type HwServerV1 struct { Addr string `json:"addr"` CPUs []HwCPUV1 `json:"cpus,omitempty"` Drives []HwDriveV1 `json:"drives,omitempty"` MemInfo HwMemV1 `json:"meminfo,omitempty"` Perf HwPerfV1 `json:"perf,omitempty"` } // HwCPUV1 - CPU Info type HwCPUV1 struct { CPUStat []cpu.InfoStat `json:"cpu,omitempty"` TimesStat []cpu.TimesStat `json:"time,omitempty"` Error string `json:"error,omitempty"` } // HwDriveV1 - Drive info type HwDriveV1 struct { Counters map[string]disk.IOCountersStat `json:"counters,omitempty"` Partitions []madmin.PartitionStat `json:"partitions,omitempty"` Usage []*disk.UsageStat `json:"usage,omitempty"` Error string `json:"error,omitempty"` } // HwMemV1 - Includes host virtual and swap mem information type HwMemV1 struct { SwapMem *mem.SwapMemoryStat `json:"swap,omitempty"` VirtualMem *mem.VirtualMemoryStat `json:"virtualmem,omitempty"` Error string `json:"error,omitempty"` } // HwPerfV1 - hardware performance type HwPerfV1 struct { Net HwNetPerfV1 `json:"net,omitempty"` Drive HwDrivePerfV1 `json:"drives,omitempty"` } // HwNetPerfV1 - Network performance info type HwNetPerfV1 struct { Serial []madmin.NetPerfInfoV0 `json:"serial,omitempty"` Parallel []madmin.NetPerfInfoV0 `json:"parallel,omitempty"` } // HwDrivePerfV1 - Network performance info type HwDrivePerfV1 struct { Serial []madmin.DrivePerfInfoV0 `json:"serial,omitempty"` Parallel []madmin.DrivePerfInfoV0 `json:"parallel,omitempty"` Error string `json:"error,omitempty"` } // SwInfoV1 - software health Info type SwInfoV1 struct { Minio MinioHealthInfoV1 `json:"minio,omitempty"` OsInfo []madmin.ServerOsInfo `json:"osinfos,omitempty"` } // MinioHealthInfoV1 - Health info of the MinIO cluster type MinioHealthInfoV1 struct { Info madmin.InfoMessage `json:"info,omitempty"` Config interface{} `json:"config,omitempty"` ProcInfo []madmin.ServerProcInfo `json:"procinfos,omitempty"` Error string `json:"error,omitempty"` } // ClusterHealthV1 - main struct of the health report type ClusterHealthV1 struct { TimeStamp time.Time `json:"timestamp,omitempty"` Status string `json:"status"` Error string `json:"error,omitempty"` Hardware HwServersV1 `json:"hardware,omitempty"` Software SwInfoV1 `json:"software,omitempty"` } func (ch ClusterHealthV1) String() string { return ch.JSON() } // JSON jsonifies service status message. func (ch ClusterHealthV1) JSON() string { statusJSONBytes, e := json.MarshalIndent(ch, " ", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(statusJSONBytes) } // GetStatus - return status of the health info func (ch ClusterHealthV1) GetStatus() string { return ch.Status } // GetError - return error from the health info func (ch ClusterHealthV1) GetError() string { return ch.Error } // GetTimestamp - return timestamp from the health info func (ch ClusterHealthV1) GetTimestamp() time.Time { return ch.TimeStamp } // MapHealthInfoToV1 - maps the health info returned by minio server to V1 format func MapHealthInfoToV1(healthInfo madmin.HealthInfoV0, err error) ClusterHealthV1 { ch := ClusterHealthV1{} ch.TimeStamp = healthInfo.TimeStamp if err != nil { ch.Status = "Error" ch.Error = err.Error() return ch } ch.Status = "Success" serverAddrs := set.NewStringSet() var serverCPUs map[string][]HwCPUV1 var serverDrives map[string][]HwDriveV1 var serverMems map[string]HwMemV1 var serverNetPerfSerial, serverNetPerfParallel map[string][]madmin.NetPerfInfoV0 var serverDrivePerf map[string]HwDrivePerfV1 mapCPUs := func() { serverCPUs = mapServerCPUs(healthInfo) } mapDrives := func() { serverDrives = mapServerDrives(healthInfo) } mapMems := func() { serverMems = mapServerMems(healthInfo) } mapNetPerf := func() { serverNetPerfSerial, serverNetPerfParallel = mapServerNetPerf(healthInfo) } mapDrivePerf := func() { serverDrivePerf = mapServerDrivePerf(healthInfo) } parallelize(mapCPUs, mapDrives, mapMems, mapNetPerf, mapDrivePerf) addKeysToSet(reflect.ValueOf(serverCPUs).MapKeys(), &serverAddrs) addKeysToSet(reflect.ValueOf(serverDrives).MapKeys(), &serverAddrs) addKeysToSet(reflect.ValueOf(serverMems).MapKeys(), &serverAddrs) addKeysToSet(reflect.ValueOf(serverNetPerfSerial).MapKeys(), &serverAddrs) if len(healthInfo.Perf.NetParallel.Addr) > 0 { serverAddrs.Add(healthInfo.Perf.NetParallel.Addr) } addKeysToSet(reflect.ValueOf(serverDrivePerf).MapKeys(), &serverAddrs) // Merge hardware info hw := HwServersV1{Servers: []HwServerV1{}} for addr := range serverAddrs { perf := HwPerfV1{ Net: HwNetPerfV1{ Serial: serverNetPerfSerial[addr], Parallel: serverNetPerfParallel[addr], }, Drive: serverDrivePerf[addr], } hw.Servers = append(hw.Servers, HwServerV1{ Addr: addr, CPUs: serverCPUs[addr], Drives: serverDrives[addr], MemInfo: serverMems[addr], Perf: perf, }) } ch.Hardware = hw ch.Software = SwInfoV1{ Minio: MinioHealthInfoV1{ Info: healthInfo.Minio.Info, Config: healthInfo.Minio.Config, Error: healthInfo.Minio.Error, ProcInfo: healthInfo.Sys.ProcInfo, }, OsInfo: healthInfo.Sys.OsInfo, } return ch } func parallelize(functions ...func()) { var waitGroup sync.WaitGroup waitGroup.Add(len(functions)) defer waitGroup.Wait() for _, function := range functions { go func(fn func()) { defer waitGroup.Done() fn() }(function) } } func addKeysToSet(input []reflect.Value, output *set.StringSet) { for _, key := range input { output.Add(key.String()) } } func mapServerCPUs(healthInfo madmin.HealthInfoV0) map[string][]HwCPUV1 { serverCPUs := map[string][]HwCPUV1{} for _, ci := range healthInfo.Sys.CPUInfo { cpus, ok := serverCPUs[ci.Addr] if !ok { cpus = []HwCPUV1{} } cpus = append(cpus, HwCPUV1{ CPUStat: ci.CPUStat, TimesStat: ci.TimeStat, Error: ci.Error, }) serverCPUs[ci.Addr] = cpus } return serverCPUs } func mapServerDrives(healthInfo madmin.HealthInfoV0) map[string][]HwDriveV1 { serverDrives := map[string][]HwDriveV1{} for _, di := range healthInfo.Sys.DiskHwInfo { drives, ok := serverDrives[di.Addr] if !ok { drives = []HwDriveV1{} } drives = append(drives, HwDriveV1{ Counters: di.Counters, Partitions: di.Partitions, Usage: di.Usage, Error: di.Error, }) serverDrives[di.Addr] = drives } return serverDrives } func mapServerMems(healthInfo madmin.HealthInfoV0) map[string]HwMemV1 { serverMems := map[string]HwMemV1{} for _, mi := range healthInfo.Sys.MemInfo { serverMems[mi.Addr] = HwMemV1{ SwapMem: mi.SwapMem, VirtualMem: mi.VirtualMem, Error: mi.Error, } } return serverMems } func mapServerNetPerf(healthInfo madmin.HealthInfoV0) (map[string][]madmin.NetPerfInfoV0, map[string][]madmin.NetPerfInfoV0) { snpSerial := map[string][]madmin.NetPerfInfoV0{} for _, serverPerf := range healthInfo.Perf.Net { snpSerial[serverPerf.Addr] = serverPerf.Net } snpParallel := map[string][]madmin.NetPerfInfoV0{} snpParallel[healthInfo.Perf.NetParallel.Addr] = healthInfo.Perf.NetParallel.Net return snpSerial, snpParallel } func mapServerDrivePerf(healthInfo madmin.HealthInfoV0) map[string]HwDrivePerfV1 { sdp := map[string]HwDrivePerfV1{} for _, drivePerf := range healthInfo.Perf.DriveInfo { sdp[drivePerf.Addr] = HwDrivePerfV1{ Serial: drivePerf.Serial, Parallel: drivePerf.Parallel, Error: drivePerf.Error, } } return sdp } minio-client-0.0~20250403/cmd/humanized-duration.go000066400000000000000000000071021477450377600217030ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "math" "time" ) // humanizedDuration container to capture humanized time. type humanizedDuration struct { Days int64 `json:"days,omitempty"` Hours int64 `json:"hours,omitempty"` Minutes int64 `json:"minutes,omitempty"` Seconds int64 `json:"seconds,omitempty"` MilliSeconds int64 `json:"milliSeconds,omitempty"` } // StringShort() humanizes humanizedDuration to human readable short format. // This does not print at seconds. func (r humanizedDuration) StringShort() string { switch { case r.Days == 0 && r.Hours == 0 && r.Minutes == 0 && r.Seconds == 0: return fmt.Sprintf("%d milliseconds", r.MilliSeconds) case r.Days == 0 && r.Hours == 0 && r.Minutes == 0: return fmt.Sprintf("%d seconds", r.Seconds) case r.Days == 0 && r.Hours == 0: return fmt.Sprintf("%d minutes", r.Minutes) case r.Days == 0: return fmt.Sprintf("%d hours %d minutes", r.Hours, r.Minutes) case r.Days <= 2: return fmt.Sprintf("%d days, %d hours", r.Days, r.Hours) default: return fmt.Sprintf("%d days", r.Days) } } // String() humanizes humanizedDuration to human readable, func (r humanizedDuration) String() string { if r.Days == 0 && r.Hours == 0 && r.Minutes == 0 && r.Seconds == 0 { return fmt.Sprintf("%d milliseconds", r.MilliSeconds) } if r.Days == 0 && r.Hours == 0 && r.Minutes == 0 { return fmt.Sprintf("%d seconds", r.Seconds) } if r.Days == 0 && r.Hours == 0 { return fmt.Sprintf("%d minutes %d seconds", r.Minutes, r.Seconds) } if r.Days == 0 { return fmt.Sprintf("%d hours %d minutes %d seconds", r.Hours, r.Minutes, r.Seconds) } return fmt.Sprintf("%d days %d hours %d minutes %d seconds", r.Days, r.Hours, r.Minutes, r.Seconds) } // timeDurationToHumanizedDuration convert golang time.Duration to a custom more readable humanizedDuration. func timeDurationToHumanizedDuration(duration time.Duration) humanizedDuration { r := humanizedDuration{} if duration.Milliseconds() < 1000 { r.MilliSeconds = int64(duration.Milliseconds()) return r } if duration.Seconds() < 60.0 { r.Seconds = int64(duration.Seconds()) return r } if duration.Minutes() < 60.0 { remainingSeconds := math.Mod(duration.Seconds(), 60) r.Seconds = int64(remainingSeconds) r.Minutes = int64(duration.Minutes()) return r } if duration.Hours() < 24.0 { remainingMinutes := math.Mod(duration.Minutes(), 60) remainingSeconds := math.Mod(duration.Seconds(), 60) r.Seconds = int64(remainingSeconds) r.Minutes = int64(remainingMinutes) r.Hours = int64(duration.Hours()) return r } remainingHours := math.Mod(duration.Hours(), 24) remainingMinutes := math.Mod(duration.Minutes(), 60) remainingSeconds := math.Mod(duration.Seconds(), 60) r.Hours = int64(remainingHours) r.Minutes = int64(remainingMinutes) r.Seconds = int64(remainingSeconds) r.Days = int64(duration.Hours() / 24) return r } minio-client-0.0~20250403/cmd/idp-ldap-accesskey-create-with-login.go000066400000000000000000000077151477450377600250700ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "fmt" "net/http" "net/url" "os" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/pkg/v3/console" "golang.org/x/term" ) var idpLdapAccesskeyCreateWithLoginCmd = cli.Command{ Name: "create-with-login", Usage: "login using LDAP credentials to generate access key pair", Action: mainIDPLdapAccesskeyCreateWithLogin, Before: setGlobalsFromContext, Flags: append(idpLdapAccesskeyCreateFlags, globalFlags...), OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] URL FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Create a new access key pair for https://minio.example.com by logging in with LDAP credentials {{.Prompt}} {{.HelpName}} https://minio.example.com 2. Create a new access key pair for http://localhost:9000 via login with custom access key and secret key {{.Prompt}} {{.HelpName}} http://localhost:9000 --access-key myaccesskey --secret-key mysecretkey `, } func mainIDPLdapAccesskeyCreateWithLogin(ctx *cli.Context) error { if !ctx.Args().Present() { showCommandHelpAndExit(ctx, 1) // last argument is exit code } isTerminal := term.IsTerminal(int(os.Stdin.Fd())) if !isTerminal { e := fmt.Errorf("login flag cannot be used with a non-interactive terminal") fatalIf(probe.NewError(e), "unable to read from STDIN") } client, opts := loginLDAPAccesskey(ctx) res, e := client.AddServiceAccountLDAP(globalContext, opts) fatalIf(probe.NewError(e), "unable to add service account") m := ldapAccesskeyMessage{ op: "create", Status: "success", AccessKey: res.AccessKey, SecretKey: res.SecretKey, Expiration: &res.Expiration, Name: opts.Name, Description: opts.Description, } printMsg(m) return nil } func loginLDAPAccesskey(ctx *cli.Context) (*madmin.AdminClient, madmin.AddServiceAccountReq) { urlStr := ctx.Args().First() u, e := url.Parse(urlStr) fatalIf(probe.NewError(e), "unable to parse server URL") console.SetColor(cred, color.New(color.FgYellow, color.Italic)) reader := bufio.NewReader(os.Stdin) fmt.Printf("%s", console.Colorize(cred, "Enter LDAP Username: ")) value, _, e := reader.ReadLine() fatalIf(probe.NewError(e), "unable to read username") username := string(value) fmt.Printf("%s", console.Colorize(cred, "Enter LDAP Password: ")) bytePassword, e := term.ReadPassword(int(os.Stdin.Fd())) fatalIf(probe.NewError(e), "unable to read password") fmt.Printf("\n") password := string(bytePassword) stsCreds, e := credentials.NewLDAPIdentity(urlStr, username, password) fatalIf(probe.NewError(e), "unable to initialize LDAP identity") tempCreds, e := stsCreds.GetWithContext(&credentials.CredContext{ Client: http.DefaultClient, }) fatalIf(probe.NewError(e), "unable to create a temporary account from LDAP identity") client, e := madmin.NewWithOptions(u.Host, &madmin.Options{ Creds: stsCreds, Secure: u.Scheme == "https", }) fatalIf(probe.NewError(e), "unable to initialize admin connection") return client, accessKeyCreateOpts(ctx, tempCreds.AccessKeyID) } minio-client-0.0~20250403/cmd/idp-ldap-accesskey-create.go000066400000000000000000000135711477450377600230060ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "errors" "fmt" "os" "time" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/policy" ) var idpLdapAccesskeyCreateFlags = []cli.Flag{ cli.StringFlag{ Name: "access-key", Usage: "set an access key for the account", }, cli.StringFlag{ Name: "secret-key", Usage: "set a secret key for the account", }, cli.StringFlag{ Name: "policy", Usage: "path to a JSON policy file", }, cli.StringFlag{ Name: "name", Usage: "friendly name for the account", }, cli.StringFlag{ Name: "description", Usage: "description for the account", }, cli.StringFlag{ Name: "expiry-duration", Usage: "duration before the access key expires", }, cli.StringFlag{ Name: "expiry", Usage: "expiry date for the access key", }, cli.BoolFlag{ Name: "login", Usage: "log in using ldap credentials to generate access key pair for future use", Hidden: true, }, } var idpLdapAccesskeyCreateCmd = cli.Command{ Name: "create", Usage: "create access key pairs for LDAP", Action: mainIDPLdapAccesskeyCreate, Before: setGlobalsFromContext, Flags: append(idpLdapAccesskeyCreateFlags, globalFlags...), OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] [TARGET] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Create a new access key pair with the same policy as the authenticated user {{.Prompt}} {{.HelpName}} local/ 2. Create a new access key pair with custom access key and secret key {{.Prompt}} {{.HelpName}} local/ --access-key myaccesskey --secret-key mysecretkey 4. Create a new access key pair for user with username "james" that expires in 1 day {{.Prompt}} {{.HelpName}} local/ james --expiry-duration 24h 5. Create a new access key pair for authenticated user that expires on 2021-01-01 {{.Prompt}} {{.HelpName}} --expiry 2021-01-01 `, } func mainIDPLdapAccesskeyCreate(ctx *cli.Context) error { return commonAccesskeyCreate(ctx, true) } func commonAccesskeyCreate(ctx *cli.Context, ldap bool) error { if len(ctx.Args()) == 0 || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } args := ctx.Args() aliasedURL := args.Get(0) targetUser := args.Get(1) if ctx.Bool("login") { deprecatedError("mc idp ldap accesskey create-with-login") } opts := accessKeyCreateOpts(ctx, targetUser) client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") var res madmin.Credentials var e error if ldap { res, e = client.AddServiceAccountLDAP(globalContext, opts) } else { res, e = client.AddServiceAccount(globalContext, opts) } fatalIf(probe.NewError(e), "Unable to add service account.") m := ldapAccesskeyMessage{ op: "create", Status: "success", AccessKey: res.AccessKey, SecretKey: res.SecretKey, Expiration: &res.Expiration, Name: opts.Name, Description: opts.Description, } printMsg(m) return nil } func accessKeyCreateOpts(ctx *cli.Context, targetUser string) madmin.AddServiceAccountReq { name := ctx.String("name") expVal := ctx.String("expiry") policyPath := ctx.String("policy") accessKey := ctx.String("access-key") secretKey := ctx.String("secret-key") description := ctx.String("description") expDurVal := ctx.Duration("expiry-duration") // generate access key and secret key if len(accessKey) <= 0 || len(secretKey) <= 0 { randomAccessKey, randomSecretKey, err := generateCredentials() if err != nil { fatalIf(err, "unable to generate randomized access credentials") } if len(accessKey) <= 0 { accessKey = randomAccessKey } if len(secretKey) <= 0 { secretKey = randomSecretKey } } if expVal != "" && expDurVal != 0 { fatalIf(probe.NewError(errors.New("Only one of --expiry or --expiry-duration can be specified")), "invalid flags") } opts := madmin.AddServiceAccountReq{ TargetUser: targetUser, AccessKey: accessKey, SecretKey: secretKey, Name: name, Description: description, } if policyPath != "" { // Validate the policy document and ensure it has at least one statement policyBytes, e := os.ReadFile(policyPath) fatalIf(probe.NewError(e), "unable to read the policy document") p, e := policy.ParseConfig(bytes.NewReader(policyBytes)) fatalIf(probe.NewError(e), "unable to parse the policy document") if p.IsEmpty() { fatalIf(errInvalidArgument(), "empty policies are not allowed") } opts.Policy = policyBytes } switch { case expVal != "": location, e := time.LoadLocation("Local") fatalIf(probe.NewError(e), "unable to load local location. verify your local TZ= settings") var found bool for _, format := range supportedTimeFormats { t, e := time.ParseInLocation(format, expVal, location) if e == nil { found = true opts.Expiration = &t break } } if !found { fatalIf(probe.NewError(fmt.Errorf("invalid expiry date format '%s'", expVal)), "unable to parse the expiry argument") } case expDurVal != 0: t := time.Now().Add(expDurVal) opts.Expiration = &t } return opts } minio-client-0.0~20250403/cmd/idp-ldap-accesskey-disable.go000066400000000000000000000026001477450377600231350ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var idpLdapAccesskeyDisableCmd = cli.Command{ Name: "disable", Usage: "disable an access key", Action: mainIDPLdapAccesskeyDisable, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] [TARGET] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Disable LDAP access key {{.Prompt}} {{.HelpName}} myminio myaccesskey `, } func mainIDPLdapAccesskeyDisable(ctx *cli.Context) error { return enableDisableAccesskey(ctx, false) } minio-client-0.0~20250403/cmd/idp-ldap-accesskey-edit.go000066400000000000000000000113421477450377600224620ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "errors" "fmt" "os" "time" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/policy" ) var idpLdapAccesskeyEditFlags = []cli.Flag{ cli.StringFlag{ Name: "secret-key", Usage: "set a secret key for the account", }, cli.StringFlag{ Name: "policy", Usage: "path to a JSON policy file", }, cli.StringFlag{ Name: "name", Usage: "friendly name for the account", }, cli.StringFlag{ Name: "description", Usage: "description for the account", }, cli.StringFlag{ Name: "expiry-duration", Usage: "duration before the access key expires", }, cli.StringFlag{ Name: "expiry", Usage: "expiry date for the access key", }, } var idpLdapAccesskeyEditCmd = cli.Command{ Name: "edit", Usage: "edit existing access keys for LDAP", Action: mainIDPLdapAccesskeyEdit, Before: setGlobalsFromContext, Flags: append(idpLdapAccesskeyEditFlags, globalFlags...), OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] [TARGET] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Change the secret key for the access key "testkey" {{.Prompt}} {{.HelpName}} myminio/ testkey --secret-key 'xxxxxxx' 2. Change the expiry duration for the access key "testkey" {{.Prompt}} {{.HelpName}} myminio/ testkey ---expiry-duration 24h `, } func mainIDPLdapAccesskeyEdit(ctx *cli.Context) error { return commonAccesskeyEdit(ctx) } func commonAccesskeyEdit(ctx *cli.Context) error { if len(ctx.Args()) == 0 || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } args := ctx.Args() aliasedURL := args.Get(0) accessKey := args.Get(1) opts := accessKeyEditOpts(ctx) client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") e := client.UpdateServiceAccount(globalContext, accessKey, opts) fatalIf(probe.NewError(e), "Unable to edit service account.") m := ldapAccesskeyMessage{ op: "edit", Status: "success", AccessKey: accessKey, } printMsg(m) return nil } func accessKeyEditOpts(ctx *cli.Context) madmin.UpdateServiceAccountReq { name := ctx.String("name") expVal := ctx.String("expiry") policyPath := ctx.String("policy") secretKey := ctx.String("secret-key") description := ctx.String("description") expDurVal := ctx.Duration("expiry-duration") if name == "" && expVal == "" && expDurVal == 0 && policyPath == "" && secretKey == "" && description == "" { fatalIf(probe.NewError(errors.New("At least one property must be edited")), "invalid flags") } if expVal != "" && expDurVal != 0 { fatalIf(probe.NewError(errors.New("Only one of --expiry or --expiry-duration can be specified")), "invalid flags") } opts := madmin.UpdateServiceAccountReq{ NewName: name, NewSecretKey: secretKey, NewDescription: description, } if policyPath != "" { // Validate the policy document and ensure it has at least one statement policyBytes, e := os.ReadFile(policyPath) fatalIf(probe.NewError(e), "unable to read the policy document") p, e := policy.ParseConfig(bytes.NewReader(policyBytes)) fatalIf(probe.NewError(e), "unable to parse the policy document") if p.IsEmpty() { fatalIf(errInvalidArgument(), "empty policies are not allowed") } opts.NewPolicy = policyBytes } switch { case expVal != "": location, e := time.LoadLocation("Local") fatalIf(probe.NewError(e), "unable to load local location. verify your local TZ= settings") var found bool for _, format := range supportedTimeFormats { t, e := time.ParseInLocation(format, expVal, location) if e == nil { found = true opts.NewExpiration = &t break } } if !found { fatalIf(probe.NewError(fmt.Errorf("invalid expiry date format '%s'", expVal)), "unable to parse the expiry argument") } case expDurVal != 0: t := time.Now().Add(expDurVal) opts.NewExpiration = &t } return opts } minio-client-0.0~20250403/cmd/idp-ldap-accesskey-enable.go000066400000000000000000000042441477450377600227660ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var idpLdapAccesskeyEnableCmd = cli.Command{ Name: "enable", Usage: "enable an access key", Action: mainIDPLdapAccesskeyEnable, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] [TARGET] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable LDAP access key {{.Prompt}} {{.HelpName}} myminio myaccesskey `, } func mainIDPLdapAccesskeyEnable(ctx *cli.Context) error { return enableDisableAccesskey(ctx, true) } func enableDisableAccesskey(ctx *cli.Context, enable bool) error { if len(ctx.Args()) == 0 || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } args := ctx.Args() aliasedURL := args.Get(0) accessKey := args.Get(1) client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") op := "disable" status := "off" if enable { op = "enable" status = "on" } e := client.UpdateServiceAccount(globalContext, accessKey, madmin.UpdateServiceAccountReq{ NewStatus: status, }) fatalIf(probe.NewError(e), "Unable to add service account.") m := ldapAccesskeyMessage{ op: op, Status: "success", AccessKey: accessKey, } printMsg(m) return nil } minio-client-0.0~20250403/cmd/idp-ldap-accesskey-info.go000066400000000000000000000146101477450377600224710ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "strings" "time" "github.com/charmbracelet/lipgloss" humanize "github.com/dustin/go-humanize" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" ) var idpLdapAccesskeyInfoCmd = cli.Command{ Name: "info", Usage: "info about given access key pairs for LDAP", Action: mainIDPLdapAccesskeyInfo, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET ACCESSKEY [ACCESSKEY...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get info for the access key "testkey" {{.Prompt}} {{.HelpName}} local/ testkey 2. Get info for the access keys "testkey" and "testkey2" {{.Prompt}} {{.HelpName}} local/ testkey testkey2 `, } type ldapAccesskeyMessage struct { op string Status string `json:"status"` AccessKey string `json:"accessKey"` SecretKey string `json:"secretKey,omitempty"` ParentUser string `json:"parentUser,omitempty"` AccountStatus string `json:"accountStatus,omitempty"` ImpliedPolicy bool `json:"impliedPolicy,omitempty"` Policy json.RawMessage `json:"policy,omitempty"` Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` Expiration *time.Time `json:"expiration,omitempty"` } func (m ldapAccesskeyMessage) String() string { labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) // green o := strings.Builder{} switch m.op { case "info": expirationStr := "NONE" if m.Expiration != nil && !m.Expiration.IsZero() && !m.Expiration.Equal(timeSentinel) { expirationStr = humanize.Time(*m.Expiration) } policyStr := "embedded" if m.ImpliedPolicy { policyStr = "implied" } statusStr := "enabled" if m.AccountStatus == "off" { statusStr = "disabled" } o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Access Key:"), m.AccessKey)) o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Parent User:"), m.ParentUser)) o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Status:"), statusStr)) o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Policy:"), policyStr)) o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Name:"), m.Name)) o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Description:"), m.Description)) o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Expiration:"), expirationStr)) case "create": expirationStr := "NONE" if m.Expiration != nil && !m.Expiration.IsZero() && !m.Expiration.Equal(timeSentinel) { expirationStr = m.Expiration.String() } o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Access Key:"), m.AccessKey)) o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Secret Key:"), m.SecretKey)) o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Expiration:"), expirationStr)) o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Name:"), m.Name)) o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Description:"), m.Description)) case "remove": o.WriteString(labelStyle.Render(iFmt(0, "Successfully removed access key `%s`.", m.AccessKey))) case "edit": o.WriteString(labelStyle.Render(iFmt(0, "Successfully edited access key `%s`.", m.AccessKey))) case "enable": o.WriteString(labelStyle.Render(iFmt(0, "Successfully enabled access key `%s`.", m.AccessKey))) case "disable": o.WriteString(labelStyle.Render(iFmt(0, "Successfully disabled access key `%s`.", m.AccessKey))) } return o.String() } func (m ldapAccesskeyMessage) JSON() string { jsonMessageBytes, e := json.MarshalIndent(m, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func mainIDPLdapAccesskeyInfo(ctx *cli.Context) error { return commonAccesskeyInfo(ctx) } // currently no difference between ldap and builtin accesskey info func commonAccesskeyInfo(ctx *cli.Context) error { if len(ctx.Args()) < 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } args := ctx.Args() aliasedURL := args.Get(0) accessKeys := args.Tail() // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") for _, accessKey := range accessKeys { // Assume service account by default res, e := client.InfoServiceAccount(globalContext, accessKey) if e != nil { // If not a service account must be sts tempRes, e := client.TemporaryAccountInfo(globalContext, accessKey) if e != nil { errorIf(probe.NewError(e), "Unable to retrieve access key %s info.", accessKey) } else { m := ldapAccesskeyMessage{ op: "info", AccessKey: accessKey, Status: "success", ParentUser: tempRes.ParentUser, AccountStatus: tempRes.AccountStatus, ImpliedPolicy: tempRes.ImpliedPolicy, Policy: json.RawMessage(tempRes.Policy), Name: tempRes.Name, Description: tempRes.Description, Expiration: nilExpiry(tempRes.Expiration), } printMsg(m) } } else { m := ldapAccesskeyMessage{ op: "info", AccessKey: accessKey, Status: "success", ParentUser: res.ParentUser, AccountStatus: res.AccountStatus, ImpliedPolicy: res.ImpliedPolicy, Policy: json.RawMessage(res.Policy), Name: res.Name, Description: res.Description, Expiration: nilExpiry(res.Expiration), } printMsg(m) } } return nil } func nilExpiry(expiry *time.Time) *time.Time { if expiry != nil && expiry.Equal(timeSentinel) { return nil } return expiry } minio-client-0.0~20250403/cmd/idp-ldap-accesskey-list.go000066400000000000000000000120271477450377600225110ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "errors" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var idpLdapAccesskeyListFlags = []cli.Flag{ cli.BoolFlag{ Name: "users-only", Usage: "only list user DNs", }, cli.BoolFlag{ Name: "temp-only", Usage: "only list temporary access keys", }, cli.BoolFlag{ Name: "svcacc-only", Usage: "only list service account access keys", }, cli.BoolFlag{ Name: "self", Usage: "list access keys for the authenticated user", }, cli.BoolFlag{ Name: "all", Usage: "list all access keys for all LDAP users", }, } var idpLdapAccesskeyListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list access key pairs for LDAP", Action: mainIDPLdapAccesskeyList, Before: setGlobalsFromContext, Flags: append(idpLdapAccesskeyListFlags, globalFlags...), OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [DN...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get list of all LDAP users and associated access keys in local server (if admin) {{.Prompt}} {{.HelpName}} local/ 2. Get list of LDAP users in local server (if admin) {{.Prompt}} {{.HelpName}} local/ --users-only 3. Get list of all users and associated temporary access keys in play server (if admin) {{.Prompt}} {{.HelpName}} play/ --temp-only 4. Get list of access keys associated with user 'bobfisher' {{.Prompt}} {{.HelpName}} play/ uid=bobfisher,dc=min,dc=io 5. Get list of access keys associated with user 'bobfisher' (alt) {{.Prompt}} {{.HelpName}} play/ bobfisher 6. Get list of access keys associated with users 'bobfisher' and 'cody3' {{.Prompt}} {{.HelpName}} play/ uid=bobfisher,dc=min,dc=io uid=cody3,dc=min,dc=io 7. Get authenticated user and associated access keys in local server (if not admin) {{.Prompt}} {{.HelpName}} local/ `, } func mainIDPLdapAccesskeyList(ctx *cli.Context) error { aliasedURL, tentativeAll, users, opts := commonAccesskeyList(ctx) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") accessKeysMap, e := client.ListAccessKeysLDAPBulkWithOpts(globalContext, users, opts) if e != nil { if e.Error() == "Access Denied." && tentativeAll { // retry with self opts.All = false accessKeysMap, e = client.ListAccessKeysLDAPBulkWithOpts(globalContext, users, opts) } fatalIf(probe.NewError(e), "Unable to list access keys.") } for dn, accessKeys := range accessKeysMap { m := userAccesskeyList{ Status: "success", User: dn, ServiceAccounts: accessKeys.ServiceAccounts, STSKeys: accessKeys.STSKeys, LDAP: true, } printMsg(m) } return nil } func commonAccesskeyList(ctx *cli.Context) (aliasedURL string, tentativeAll bool, users []string, opts madmin.ListAccessKeysOpts) { if len(ctx.Args()) == 0 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } usersOnly := ctx.Bool("users-only") stsOnly := ctx.Bool("temp-only") svcaccOnly := ctx.Bool("svcacc-only") selfFlag := ctx.Bool("self") opts.All = ctx.Bool("all") args := ctx.Args() aliasedURL = args.Get(0) users = args.Tail() var e error if (usersOnly && svcaccOnly) || (usersOnly && stsOnly) || (svcaccOnly && stsOnly) { e = errors.New("only one of --users-only, --temp-only, or --permanent-only can be specified") } else if selfFlag && opts.All { e = errors.New("only one of --self or --all can be specified") } else if (selfFlag || opts.All) && len(users) > 0 { e = errors.New("user DNs cannot be specified with --self or --all") } fatalIf(probe.NewError(e), "Invalid flags.") // If no users/self/all flags are specified, tentatively assume --all // If access is denied on tentativeAll, retry with self // This is to maintain compatibility with the previous behavior if !selfFlag && !opts.All && len(users) == 0 { tentativeAll = true opts.All = true } switch { case usersOnly: opts.ListType = madmin.AccessKeyListUsersOnly case stsOnly: opts.ListType = madmin.AccessKeyListSTSOnly case svcaccOnly: opts.ListType = madmin.AccessKeyListSvcaccOnly default: opts.ListType = madmin.AccessKeyListAll } return aliasedURL, tentativeAll, users, opts } minio-client-0.0~20250403/cmd/idp-ldap-accesskey-remove.go000066400000000000000000000041741477450377600230370ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" "github.com/minio/mc/pkg/probe" ) var idpLdapAccesskeyRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "delete access key pairs for LDAP", Action: mainIDPLdapAccesskeyRemove, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET ACCESSKEY FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove the access key "testkey" from local server {{.Prompt}} {{.HelpName}} local/ testkey `, } func mainIDPLdapAccesskeyRemove(ctx *cli.Context) error { return commonAccesskeyRemove(ctx) } // No difference between ldap and builtin accesskey remove for now func commonAccesskeyRemove(ctx *cli.Context) error { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } args := ctx.Args() aliasedURL := args.Get(0) accessKey := args.Get(1) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") e := client.DeleteServiceAccount(globalContext, accessKey) fatalIf(probe.NewError(e), "Unable to remove service account.") m := ldapAccesskeyMessage{ op: "remove", Status: "success", AccessKey: accessKey, } printMsg(m) return nil } minio-client-0.0~20250403/cmd/idp-ldap-accesskey-sts-revoke.go000066400000000000000000000057551477450377600236520ustar00rootroot00000000000000// Copyright (c) 2015-2025 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var idpLdapAccesskeySTSRevokeCmd = cli.Command{ Name: "sts-revoke", Usage: "revokes all STS accounts or specified types for the specified user", Action: mainIdpLdapAccesskeySTSRevoke, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(adminAccesskeySTSRevokeFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS USER [--all | --token-type TOKEN_TYPE] Exactly one of --all or --token-type must be specified. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Revoke all STS accounts for LDAP user 'bobfisher' {{.Prompt}} {{.HelpName}} myminio uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io --all 2. Revoke all STS accounts for LDAP user 'bobfisher' (alt) {{.Prompt}} {{.HelpName}} myminio bobfisher --all 3. Revoke STS accounts of a token type 'app-1' for user 'user1' {{.Prompt}} {{.HelpName}} myminio user1 --token-type app-1 4. Revoke all STS accounts for the authenticated user (must be LDAP service account) {{.Prompt}} {{.HelpName}} myminio --self 5. Revoke STS accounts of a token type 'app-1' for the authenticated user (must be LDAP service account) {{.Prompt}} {{.HelpName}} myminio --self --token-type app-1 `, } // mainIdpLdapAccesskeySTSRevoke is the handle for "mc idp ldap accesskey sts-revoke" command. func mainIdpLdapAccesskeySTSRevoke(ctx *cli.Context) error { checkSTSRevokeSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) user := args.Get(1) // will be empty if --self flag is set tokenRevokeType := ctx.String("token-type") fullRevoke := ctx.Bool("all") // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") e := client.RevokeTokens(globalContext, madmin.RevokeTokensReq{ User: user, TokenRevokeType: tokenRevokeType, FullRevoke: fullRevoke, }) fatalIf(probe.NewError(e).Trace(args...), "Unable to revoke tokens for %s", user) printMsg(stsRevokeMessage{ User: user, TokenRevokeType: tokenRevokeType, }) return nil } minio-client-0.0~20250403/cmd/idp-ldap-accesskey.go000066400000000000000000000030051477450377600215340ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var idpLdapAccesskeySubcommands = []cli.Command{ idpLdapAccesskeyListCmd, idpLdapAccesskeyRemoveCmd, idpLdapAccesskeyInfoCmd, idpLdapAccesskeyCreateCmd, idpLdapAccesskeyCreateWithLoginCmd, idpLdapAccesskeyEditCmd, idpLdapAccesskeyEnableCmd, idpLdapAccesskeyDisableCmd, idpLdapAccesskeySTSRevokeCmd, } var idpLdapAccesskeyCmd = cli.Command{ Name: "accesskey", Usage: "manage LDAP access key pairs", Action: mainIDPLDAPAccesskey, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: idpLdapAccesskeySubcommands, HideHelpCommand: true, } func mainIDPLDAPAccesskey(ctx *cli.Context) error { commandNotFound(ctx, idpLdapAccesskeySubcommands) return nil } minio-client-0.0~20250403/cmd/idp-ldap-policy-subcommands.go000066400000000000000000000320001477450377600233670ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "errors" "fmt" "strings" "time" "github.com/charmbracelet/lipgloss" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/set" ) var idpLdapPolicyAttachFlags = []cli.Flag{ cli.StringFlag{ Name: "user, u", Usage: "attach policy to user by DN or by login name", }, cli.StringFlag{ Name: "group, g", Usage: "attach policy to LDAP Group DN", }, } var idpLdapPolicyAttachCmd = cli.Command{ Name: "attach", Usage: "attach a policy to an entity", Action: mainIDPLdapPolicyAttach, Before: setGlobalsFromContext, Flags: append(idpLdapPolicyAttachFlags, globalFlags...), OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET POLICY [POLICY...] [ --user=USER | --group=GROUP ] Exactly one "--user" or "--group" flag is required. POLICY: Name of a policy on the MinIO server. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Attach policy "mypolicy" to a user {{.Prompt}} {{.HelpName}} play/ mypolicy --user='uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io' 2. Attach policies "policy1" and "policy2" to a group {{.Prompt}} {{.HelpName}} play/ policy1 policy2 --group='cn=projectb,ou=groups,ou=swengg,dc=min,dc=io' `, } // Quote from AWS policy naming requirement (ref: // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html): // // Names of users, groups, roles, policies, instance profiles, and server // certificates must be alphanumeric, including the following common characters: // plus (+), equal (=), comma (,), period (.), at (@), underscore (_), and // hyphen (-). func mainIDPLdapPolicyAttach(ctx *cli.Context) error { // We need exactly one alias, and at least one policy. if len(ctx.Args()) < 2 { showCommandHelpAndExit(ctx, 1) } user := ctx.String("user") group := ctx.String("group") args := ctx.Args() aliasedURL := args.Get(0) policies := args[1:] req := madmin.PolicyAssociationReq{ Policies: policies, User: user, Group: group, } fatalIf(probe.NewError(req.IsValid()), "Invalid policy attach arguments.") // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") res, e := client.AttachPolicyLDAP(globalContext, req) fatalIf(probe.NewError(e), "Unable to make LDAP policy association") m := policyAssociationMessage{ attach: true, Status: "success", PoliciesAttached: res.PoliciesAttached, User: user, Group: group, } printMsg(m) return nil } type policyAssociationMessage struct { attach bool Status string `json:"status"` PoliciesAttached []string `json:"policiesAttached,omitempty"` PoliciesDetached []string `json:"policiesDetached,omitempty"` User string `json:"user,omitempty"` Group string `json:"group,omitempty"` } func (m policyAssociationMessage) String() string { style := lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) // green policiesS := style.Render("Attached Policies:") entityS := style.Render("To User:") policies := m.PoliciesAttached entity := m.User switch { case m.User != "" && m.attach: case m.User != "" && !m.attach: policiesS = style.Render("Detached Policies:") policies = m.PoliciesDetached entityS = style.Render("From User:") case m.Group != "" && m.attach: entityS = style.Render("To Group:") entity = m.Group case m.Group != "" && !m.attach: policiesS = style.Render("Detached Policies:") policies = m.PoliciesDetached entityS = style.Render("From Group:") entity = m.Group } return fmt.Sprintf("%s %v\n%s %s\n", policiesS, policies, entityS, entity) } func (m policyAssociationMessage) JSON() string { jsonMessageBytes, e := json.MarshalIndent(m, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } var idpLdapPolicyDetachFlags = []cli.Flag{ cli.StringFlag{ Name: "user, u", Usage: "attach policy to user by DN or by login name", }, cli.StringFlag{ Name: "group, g", Usage: "attach policy to LDAP Group DN", }, } var idpLdapPolicyDetachCmd = cli.Command{ Name: "detach", Usage: "detach a policy from an entity", Action: mainIDPLdapPolicyDetach, Before: setGlobalsFromContext, Flags: append(idpLdapPolicyDetachFlags, globalFlags...), OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET POLICY [POLICY...] [ --user=USER | --group=GROUP ] Exactly one of "--user" or "--group" is required. POLICY: Name of a policy on the MinIO server. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Detach policy "mypolicy" from a user {{.Prompt}} {{.HelpName}} play/ mypolicy --user='uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io' 2. Detach policies "policy1" and "policy2" from a group {{.Prompt}} {{.HelpName}} play/ policy1 policy2 --group='cn=projectb,ou=groups,ou=swengg,dc=min,dc=io' `, } // Quote from AWS policy naming requirement (ref: // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html): // // Names of users, groups, roles, policies, instance profiles, and server // certificates must be alphanumeric, including the following common characters: // plus (+), equal (=), comma (,), period (.), at (@), underscore (_), and // hyphen (-). func mainIDPLdapPolicyDetach(ctx *cli.Context) error { // We need exactly one alias, and at least one policy. if len(ctx.Args()) < 2 { showCommandHelpAndExit(ctx, 1) } user := ctx.String("user") group := ctx.String("group") if user == "" && group == "" { e := errors.New("at least one of --user or --group is required.") fatalIf(probe.NewError(e), "Missing flag in command") } args := ctx.Args() aliasedURL := args.Get(0) policies := args[1:] // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") res, e := client.DetachPolicyLDAP(globalContext, madmin.PolicyAssociationReq{ Policies: policies, User: user, Group: group, }) fatalIf(probe.NewError(e), "Unable to make LDAP policy association") m := policyAssociationMessage{ attach: false, Status: "success", PoliciesDetached: res.PoliciesDetached, User: user, Group: group, } printMsg(m) return nil } var idpLdapPolicyEntitiesFlags = []cli.Flag{ cli.StringSliceFlag{ Name: "user, u", Usage: "list policies associated with user(s)", }, cli.StringSliceFlag{ Name: "group, g", Usage: "list policies associated with group(s)", }, cli.StringSliceFlag{ Name: "policy, p", Usage: "list users or groups associated with policy", }, } var idpLdapPolicyEntitiesCmd = cli.Command{ Name: "entities", Usage: "list policy association entities", Action: mainIDPLdapPolicyEntities, Before: setGlobalsFromContext, Flags: append(idpLdapPolicyEntitiesFlags, globalFlags...), OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List all LDAP entities associated with all policies {{.Prompt}} {{.HelpName}} play/ 2. List all LDAP entities associated with the policies 'finteam-policy' and 'mlteam-policy' {{.Prompt}} {{.HelpName}} play/ --policy finteam-policy --policy mlteam-policy 3. List all policies associated with a pair of User LDAP entities {{.Prompt}} {{.HelpName}} play/ \ --user 'uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io' \ --user 'uid=fahim,ou=people,ou=swengg,dc=min,dc=io' 4. List all policies associated with a pair of Group LDAP entities {{.Prompt}} {{.HelpName}} play/ \ --group 'cn=projecta,ou=groups,ou=swengg,dc=min,dc=io' \ --group 'cn=projectb,ou=groups,ou=swengg,dc=min,dc=io' 5. List all entities associated with a policy, group and user {{.Prompt}} {{.HelpName}} play/ \ --policy finteam-policy --user 'uid=bobfisher,ou=people,ou=hwengg,dc=min,dc=io' \ --group 'cn=projectb,ou=groups,ou=swengg,dc=min,dc=io' `, } func mainIDPLdapPolicyEntities(ctx *cli.Context) error { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) } usersToQuery := ctx.StringSlice("user") groupsToQuery := ctx.StringSlice("group") policiesToQuery := ctx.StringSlice("policy") args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") res, e := client.GetLDAPPolicyEntities(globalContext, madmin.PolicyEntitiesQuery{ Users: usersToQuery, Groups: groupsToQuery, Policy: policiesToQuery, }) fatalIf(probe.NewError(e), "Unable to fetch LDAP policy entities") printMsg(policyEntitiesFrom(res)) return nil } type policyEntities struct { Status string `json:"status"` Result madmin.PolicyEntitiesResult `json:"result"` } func policyEntitiesFrom(r madmin.PolicyEntitiesResult) policyEntities { return policyEntities{ Status: "success", Result: r, } } func (p policyEntities) JSON() string { bs, e := json.MarshalIndent(p, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(bs) } func iFmt(n int, fmtStr string, a ...any) string { indentStr := "" if n > 0 { s := make([]rune, n) for i := range s { s[i] = ' ' } indentStr = string(s) } return fmt.Sprintf(indentStr+fmtStr, a...) } func builderWrapper(strList []string, o *strings.Builder, indent, maxLen int) { currLen := 0 for _, s := range strList { if currLen+len(s) > maxLen && currLen > 0 { o.WriteString("\n") currLen = 0 } if currLen == 0 { o.WriteString(iFmt(indent, "")) currLen = indent } else { o.WriteString(", ") currLen += 2 } if strings.Contains(s, ",") { s = fmt.Sprintf("\"%s\"", s) } o.WriteString(s) currLen += len(s) } o.WriteString("\n") } func (p policyEntities) String() string { labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) // green o := strings.Builder{} o.WriteString(iFmt(0, "%s %s\n", labelStyle.Render("Query time:"), p.Result.Timestamp.Format(time.RFC3339))) if len(p.Result.UserMappings) > 0 { o.WriteString(iFmt(0, "%s\n", labelStyle.Render("User -> Policy Mappings:"))) for _, u := range p.Result.UserMappings { o.WriteString(iFmt(2, "%s %s\n", labelStyle.Render("User:"), u.User)) o.WriteString(iFmt(4, "%s\n", labelStyle.Render("Policies:"))) builderWrapper(u.Policies, &o, 6, 80) if len(u.MemberOfMappings) > 0 { effectivePolicies := set.CreateStringSet(u.Policies...) o.WriteString(iFmt(4, "%s\n", labelStyle.Render("Group Memberships:"))) groups := make([]string, 0, len(u.MemberOfMappings)) for _, g := range u.MemberOfMappings { groups = append(groups, g.Group) for _, p := range g.Policies { effectivePolicies.Add(p) } } builderWrapper(groups, &o, 6, 80) o.WriteString(iFmt(4, "%s\n", labelStyle.Render("Effective Policies:"))) builderWrapper(effectivePolicies.ToSlice(), &o, 6, 80) } } } if len(p.Result.GroupMappings) > 0 { o.WriteString(iFmt(0, "%s\n", labelStyle.Render("Group -> Policy Mappings:"))) for _, u := range p.Result.GroupMappings { o.WriteString(iFmt(2, "%s %s\n", labelStyle.Render("Group:"), u.Group)) o.WriteString(iFmt(4, "%s\n", labelStyle.Render("Policies:"))) for _, p := range u.Policies { o.WriteString(iFmt(6, "%s\n", p)) } } } if len(p.Result.PolicyMappings) > 0 { o.WriteString(iFmt(0, "%s\n", labelStyle.Render("Policy -> Entity Mappings:"))) for _, u := range p.Result.PolicyMappings { o.WriteString(iFmt(2, "%s %s\n", labelStyle.Render("Policy:"), u.Policy)) if len(u.Users) > 0 { o.WriteString(iFmt(4, "%s\n", labelStyle.Render("User Mappings:"))) for _, p := range u.Users { o.WriteString(iFmt(6, "%s\n", p)) } } if len(u.Groups) > 0 { o.WriteString(iFmt(4, "%s\n", labelStyle.Render("Group Mappings:"))) for _, p := range u.Groups { o.WriteString(iFmt(6, "%s\n", p)) } } } } return o.String() } minio-client-0.0~20250403/cmd/idp-ldap-policy.go000066400000000000000000000025001477450377600210600ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var idpLdapPolicySubcommands = []cli.Command{ idpLdapPolicyAttachCmd, idpLdapPolicyDetachCmd, idpLdapPolicyEntitiesCmd, } var idpLdapPolicyCmd = cli.Command{ Name: "policy", Usage: "manage policy assignments for LDAP", Action: mainIDPLDAPPolicy, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: idpLdapPolicySubcommands, HideHelpCommand: true, } func mainIDPLDAPPolicy(ctx *cli.Context) error { commandNotFound(ctx, idpLdapPolicySubcommands) return nil } minio-client-0.0~20250403/cmd/idp-ldap-subcommands.go000066400000000000000000000166131477450377600221060ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "errors" "strings" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var idpLdapAddCmd = cli.Command{ Name: "add", Usage: "Create an LDAP IDP server configuration", Action: mainIDPLDAPAdd, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [CFG_PARAMS...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Create LDAP IDentity Provider configuration. {{.Prompt}} {{.HelpName}} myminio/ \ server_addr=myldapserver:636 \ lookup_bind_dn=cn=admin,dc=min,dc=io \ lookup_bind_password=somesecret \ user_dn_search_base_dn=dc=min,dc=io \ user_dn_search_filter="(uid=%s)" \ group_search_base_dn=ou=swengg,dc=min,dc=io \ group_search_filter="(&(objectclass=groupofnames)(member=%d))" `, } func mainIDPLDAPAdd(ctx *cli.Context) error { if len(ctx.Args()) < 2 { showCommandHelpAndExit(ctx, 1) } args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") cfgName := madmin.Default input := args[1:] if !strings.Contains(args.Get(1), "=") { cfgName = args.Get(1) input = args[2:] } if cfgName != madmin.Default { fatalIf(probe.NewError(errors.New("all config parameters must be of the form \"key=value\"")), "Bad LDAP IDP configuration") } inputCfg := strings.Join(input, " ") restart, e := client.AddOrUpdateIDPConfig(globalContext, madmin.LDAPIDPCfg, cfgName, inputCfg, false) fatalIf(probe.NewError(e), "Unable to add LDAP IDP config to server") // Print set config result printMsg(configSetMessage{ targetAlias: aliasedURL, restart: restart, }) return nil } var idpLdapUpdateCmd = cli.Command{ Name: "update", Usage: "Update an LDAP IDP configuration", Action: mainIDPLDAPUpdate, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [CFG_PARAMS...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Update the LDAP IDP configuration. {{.Prompt}} {{.HelpName}} play/ \ lookup_bind_dn=cn=admin,dc=min,dc=io \ lookup_bind_password=somesecret `, } func mainIDPLDAPUpdate(ctx *cli.Context) error { if len(ctx.Args()) < 2 { showCommandHelpAndExit(ctx, 1) } args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") cfgName := madmin.Default input := args[1:] if !strings.Contains(args.Get(1), "=") { cfgName = args.Get(1) input = args[2:] } if cfgName != madmin.Default { fatalIf(probe.NewError(errors.New("all config parameters must be of the form \"key=value\"")), "Bad LDAP IDP configuration") } inputCfg := strings.Join(input, " ") restart, e := client.AddOrUpdateIDPConfig(globalContext, madmin.LDAPIDPCfg, cfgName, inputCfg, true) fatalIf(probe.NewError(e), "Unable to update LDAP IDP configuration") // Print set config result printMsg(configSetMessage{ targetAlias: aliasedURL, restart: restart, }) return nil } var idpLdapRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove LDAP IDP server configuration", Action: mainIDPLDAPRemove, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove the default LDAP IDP configuration. {{.Prompt}} {{.HelpName}} play/ `, } func mainIDPLDAPRemove(ctx *cli.Context) error { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) } cfgName := madmin.Default return idpRemove(ctx, false, cfgName) } var idpLdapListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list LDAP IDP server configuration(s)", Action: mainIDPLDAPList, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List configurations for LDAP IDP. {{.Prompt}} {{.HelpName}} play/ `, } func mainIDPLDAPList(ctx *cli.Context) error { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) } return idpListCommon(ctx, false) } var idpLdapInfoCmd = cli.Command{ Name: "info", Usage: "get LDAP IDP server configuration info", Action: mainIDPLDAPInfo, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get configuration info on the LDAP IDP configuration. {{.Prompt}} {{.HelpName}} play/ `, } func mainIDPLDAPInfo(ctx *cli.Context) error { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) } cfgName := madmin.Default return idpInfo(ctx, false, cfgName) } var idpLdapEnableCmd = cli.Command{ Name: "enable", Usage: "manage LDAP IDP server configuration", Action: mainIDPLDAPEnable, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable the LDAP IDP configuration. {{.Prompt}} {{.HelpName}} play/ `, } func mainIDPLDAPEnable(ctx *cli.Context) error { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) } isOpenID, enable := false, true return idpEnableDisable(ctx, isOpenID, enable) } var idpLdapDisableCmd = cli.Command{ Name: "disable", Usage: "Disable an LDAP IDP server configuration", Action: mainIDPLDAPDisable, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Disable the default LDAP IDP configuration. {{.Prompt}} {{.HelpName}} play/ `, } func mainIDPLDAPDisable(ctx *cli.Context) error { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) } isOpenID, enable := false, false return idpEnableDisable(ctx, isOpenID, enable) } minio-client-0.0~20250403/cmd/idp-ldap.go000066400000000000000000000026121477450377600175670ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var ( idpLdapSubcommands = []cli.Command{ idpLdapAddCmd, idpLdapUpdateCmd, idpLdapRemoveCmd, idpLdapListCmd, idpLdapInfoCmd, idpLdapEnableCmd, idpLdapDisableCmd, idpLdapPolicyCmd, idpLdapAccesskeyCmd, } idpLdapCmd = cli.Command{ Name: "ldap", Usage: "manage Ldap IDP server configuration", Action: mainIDPLdap, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: idpLdapSubcommands, HideHelpCommand: true, } ) func mainIDPLdap(ctx *cli.Context) error { commandNotFound(ctx, idpLdapSubcommands) return nil } minio-client-0.0~20250403/cmd/idp-openid-subcommands.go000066400000000000000000000336471477450377600224520ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strings" "github.com/charmbracelet/lipgloss" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var idpOpenidAddCmd = cli.Command{ Name: "add", Usage: "Create an OpenID IDP server configuration", Action: mainIDPOpenIDAdd, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [CFG_NAME] [CFG_PARAMS...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Create a default OpenID IDP configuration (CFG_NAME is omitted). {{.Prompt}} {{.HelpName}} play/ \ client_id=minio-client-app \ client_secret=minio-client-app-secret \ config_url="http://localhost:5556/dex/.well-known/openid-configuration" \ scopes="openid,groups" \ redirect_uri="http://127.0.0.1:10000/oauth_callback" \ role_policy="consoleAdmin" 2. Create OpenID IDP configuration named "dex_test". {{.Prompt}} {{.HelpName}} play/ dex_test \ client_id=minio-client-app \ client_secret=minio-client-app-secret \ config_url="http://localhost:5556/dex/.well-known/openid-configuration" \ scopes="openid,groups" \ redirect_uri="http://127.0.0.1:10000/oauth_callback" \ role_policy="consoleAdmin" `, } func mainIDPOpenIDAdd(ctx *cli.Context) error { return mainIDPOpenIDAddOrUpdate(ctx, false) } func mainIDPOpenIDAddOrUpdate(ctx *cli.Context, update bool) error { if len(ctx.Args()) < 2 { showCommandHelpAndExit(ctx, 1) } args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") cfgName := madmin.Default input := args[1:] if !strings.Contains(args.Get(1), "=") { cfgName = args.Get(1) input = args[2:] } inputCfg := strings.Join(input, " ") restart, e := client.AddOrUpdateIDPConfig(globalContext, madmin.OpenidIDPCfg, cfgName, inputCfg, update) fatalIf(probe.NewError(e), "Unable to add OpenID IDP config to server") // Print set config result printMsg(configSetMessage{ targetAlias: aliasedURL, restart: restart, }) return nil } var idpOpenidUpdateCmd = cli.Command{ Name: "update", Usage: "Update an OpenID IDP configuration", Action: mainIDPOpenIDUpdate, Before: setGlobalsFromContext, OnUsageError: onUsageError, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [CFG_NAME] [CFG_PARAMS...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Update the default OpenID IDP configuration (CFG_NAME is omitted). {{.Prompt}} {{.HelpName}} play/ scopes="openid,groups" \ role_policy="consoleAdmin" 2. Update configuration for OpenID IDP configuration named "dex_test". {{.Prompt}} {{.HelpName}} play/ dex_test \ scopes="openid,groups" \ role_policy="consoleAdmin" `, } func mainIDPOpenIDUpdate(ctx *cli.Context) error { return mainIDPOpenIDAddOrUpdate(ctx, true) } var idpOpenidRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove OpenID IDP server configuration", Action: mainIDPOpenIDRemove, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [CFG_NAME] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove the default OpenID IDP configuration (CFG_NAME is omitted). {{.Prompt}} {{.HelpName}} play/ 2. Remove OpenID IDP configuration named "dex_test". {{.Prompt}} {{.HelpName}} play/ dex_test `, } func mainIDPOpenIDRemove(ctx *cli.Context) error { if len(ctx.Args()) < 1 || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) } args := ctx.Args() var cfgName string if len(args) == 2 { cfgName = args.Get(1) } return idpRemove(ctx, true, cfgName) } func idpRemove(ctx *cli.Context, isOpenID bool, cfgName string) error { args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") idpType := madmin.LDAPIDPCfg if isOpenID { idpType = madmin.OpenidIDPCfg } restart, e := client.DeleteIDPConfig(globalContext, idpType, cfgName) fatalIf(probe.NewError(e), "Unable to remove %s IDP config '%s'", idpType, cfgName) printMsg(configSetMessage{ targetAlias: aliasedURL, restart: restart, }) return nil } var idpOpenidListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list OpenID IDP server configuration(s)", Action: mainIDPOpenIDList, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List configurations for OpenID IDP. {{.Prompt}} {{.HelpName}} play/ `, } func mainIDPOpenIDList(ctx *cli.Context) error { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) } return idpListCommon(ctx, true) } func idpListCommon(ctx *cli.Context, isOpenID bool) error { args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") idpType := madmin.LDAPIDPCfg if isOpenID { idpType = madmin.OpenidIDPCfg } result, e := client.ListIDPConfig(globalContext, idpType) fatalIf(probe.NewError(e), "Unable to list IDP config for '%s'", idpType) printMsg(idpCfgList(result)) return nil } type idpCfgList []madmin.IDPListItem func (i idpCfgList) JSON() string { bs, e := json.MarshalIndent(i, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(bs) } func (i idpCfgList) String() string { maxNameWidth := len("Name") maxRoleARNWidth := len("RoleArn") for _, item := range i { name := item.Name if name == "_" { name = "(default)" // for the un-named config, don't show `_` } if maxNameWidth < len(name) { maxNameWidth = len(name) } if maxRoleARNWidth < len(item.RoleARN) { maxRoleARNWidth = len(item.RoleARN) } } enabledWidth := 5 // Add 2 for padding maxNameWidth += 2 maxRoleARNWidth += 2 enabledColStyle := lipgloss.NewStyle(). Align(lipgloss.Center). PaddingLeft(1). PaddingRight(1). Width(enabledWidth) nameColStyle := lipgloss.NewStyle(). Align(lipgloss.Right). PaddingLeft(1). PaddingRight(1). Width(maxNameWidth) arnColStyle := lipgloss.NewStyle(). Align(lipgloss.Left). PaddingLeft(1). PaddingRight(1). Foreground(lipgloss.Color("#04B575")). // green Width(maxRoleARNWidth) styles := []lipgloss.Style{enabledColStyle, nameColStyle, arnColStyle} headers := []string{"On?", "Name", "RoleARN"} headerRow := []string{} // Override some style settings for the header for ii, hdr := range headers { styl := styles[ii] headerRow = append(headerRow, styl.Bold(true). Foreground(lipgloss.Color("#6495ed")). // green Align(lipgloss.Center). Render(hdr), ) } lines := []string{strings.Join(headerRow, "")} enabledOff := "🔴" enabledOn := "🟢" for _, item := range i { enabled := enabledOff if item.Enabled { enabled = enabledOn } line := []string{ styles[0].Render(enabled), styles[1].Render(item.Name), styles[2].Render(item.RoleARN), } if item.Name == "_" { // For default config, don't display `_` and make it look faint. styl := styles[1] line[1] = styl.Faint(true). Render("(default)") } lines = append(lines, strings.Join(line, "")) } boxContent := strings.Join(lines, "\n") boxStyle := lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()) return boxStyle.Render(boxContent) } var idpOpenidInfoCmd = cli.Command{ Name: "info", Usage: "get OpenID IDP server configuration info", Action: mainIDPOpenIDInfo, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [CFG_NAME] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get configuration info on the default OpenID IDP configuration (CFG_NAME is omitted). {{.Prompt}} {{.HelpName}} play/ 2. Get configuration info on OpenID IDP configuration named "dex_test". {{.Prompt}} {{.HelpName}} play/ dex_test `, } func mainIDPOpenIDInfo(ctx *cli.Context) error { if len(ctx.Args()) < 1 || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) } args := ctx.Args() var cfgName string if len(args) == 2 { cfgName = args.Get(1) } return idpInfo(ctx, true, cfgName) } func idpInfo(ctx *cli.Context, isOpenID bool, cfgName string) error { args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") idpType := madmin.LDAPIDPCfg if isOpenID { idpType = madmin.OpenidIDPCfg } result, e := client.GetIDPConfig(globalContext, idpType, cfgName) fatalIf(probe.NewError(e), "Unable to get %s IDP config from server", idpType) // Print set config result printMsg(idpConfig(result)) return nil } type idpConfig madmin.IDPConfig func (i idpConfig) JSON() string { bs, e := json.MarshalIndent(i, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(bs) } func (i idpConfig) String() string { if len(i.Info) == 0 { return "Not configured." } enableStr := "on" // Determine required width for key column. fieldColWidth := 0 for _, kv := range i.Info { if fieldColWidth < len(kv.Key) { fieldColWidth = len(kv.Key) } if kv.Key == "enable" { enableStr = kv.Value } } // Add 1 for the colon-suffix in each entry. fieldColWidth++ fieldColStyle := lipgloss.NewStyle(). Width(fieldColWidth). Foreground(lipgloss.Color("#04B575")). // green Bold(true). Align(lipgloss.Right) valueColStyle := lipgloss.NewStyle(). PaddingLeft(1). Align(lipgloss.Left) envMarkStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("201")). // pinkish-red PaddingLeft(1) var lines []string lines = append(lines, fmt.Sprintf("%s%s", fieldColStyle.Render("enable:"), valueColStyle.Render(enableStr), )) for _, kv := range i.Info { if kv.Key == "enable" { continue } envStr := "" if kv.IsCfg && kv.IsEnv { envStr = " (environment)" } lines = append(lines, fmt.Sprintf("%s%s%s", fieldColStyle.Render(kv.Key+":"), valueColStyle.Render(kv.Value), envMarkStyle.Render(envStr), )) } boxContent := strings.Join(lines, "\n") boxStyle := lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()) return boxStyle.Render(boxContent) } var idpOpenidEnableCmd = cli.Command{ Name: "enable", Usage: "enable an OpenID IDP server configuration", Action: mainIDPOpenIDEnable, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [CFG_NAME] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable the default OpenID IDP configuration (CFG_NAME is omitted). {{.Prompt}} {{.HelpName}} play/ 2. Enable OpenID IDP configuration named "dex_test". {{.Prompt}} {{.HelpName}} play/ dex_test `, } func mainIDPOpenIDEnable(ctx *cli.Context) error { isOpenID, enable := true, true return idpEnableDisable(ctx, isOpenID, enable) } func idpEnableDisable(ctx *cli.Context, isOpenID, enable bool) error { if len(ctx.Args()) < 1 || len(ctx.Args()) > 2 { showCommandHelpAndExit(ctx, 1) } args := ctx.Args() cfgName := madmin.Default if len(args) == 2 { cfgName = args.Get(1) } aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") idpType := madmin.LDAPIDPCfg if isOpenID { idpType = madmin.OpenidIDPCfg } configBody := "enable=" if !enable { configBody = "enable=off" } restart, e := client.AddOrUpdateIDPConfig(globalContext, idpType, cfgName, configBody, true) fatalIf(probe.NewError(e), "Unable to remove %s IDP config '%s'", idpType, cfgName) printMsg(configSetMessage{ targetAlias: aliasedURL, restart: restart, }) return nil } var idpOpenidDisableCmd = cli.Command{ Name: "disable", Usage: "Disable an OpenID IDP server configuration", Action: mainIDPOpenIDDisable, Before: setGlobalsFromContext, Flags: globalFlags, OnUsageError: onUsageError, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [CFG_NAME] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Disable the default OpenID IDP configuration (CFG_NAME is omitted). {{.Prompt}} {{.HelpName}} play/ 2. Disable OpenID IDP configuration named "dex_test". {{.Prompt}} {{.HelpName}} play/ dex_test `, } func mainIDPOpenIDDisable(ctx *cli.Context) error { isOpenID, enable := true, false return idpEnableDisable(ctx, isOpenID, enable) } minio-client-0.0~20250403/cmd/idp-openid.go000066400000000000000000000026341477450377600201310ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var ( idpOpenidSubcommands = []cli.Command{ idpOpenidAddCmd, idpOpenidUpdateCmd, idpOpenidRemoveCmd, idpOpenidListCmd, idpOpenidInfoCmd, idpOpenidEnableCmd, idpOpenidDisableCmd, // TODO: idpOpenidPolicyCmd, } idpOpenidCmd = cli.Command{ Name: "openid", Usage: "manage OpenID IDP server configuration", Action: mainIDPOpenID, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: idpOpenidSubcommands, HideHelpCommand: true, } ) func mainIDPOpenID(ctx *cli.Context) error { commandNotFound(ctx, idpOpenidSubcommands) return nil } minio-client-0.0~20250403/cmd/idp.go000066400000000000000000000023411477450377600166500ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var idpSubcommands = []cli.Command{ idpOpenidCmd, idpLdapCmd, } var idpCmd = cli.Command{ Name: "idp", Usage: "manage MinIO IDentity Provider server configuration", Action: mainIDP, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: idpSubcommands, HideHelpCommand: true, } func mainIDP(ctx *cli.Context) error { commandNotFound(ctx, idpSubcommands) return nil } minio-client-0.0~20250403/cmd/ilm-deprecated-cmds.go000066400000000000000000000170111477450377600216770ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var ilmDepCmds = []cli.Command{ ilmDepAddCmd, ilmDepEditCmd, ilmDepLsCmd, ilmDepRmCmd, ilmDepExportCmd, ilmDepImportCmd, } var ( ilmDepAddCmd = cli.Command{ Name: "add", Usage: "add a lifecycle configuration rule for a bucket", Action: mainILMAdd, Hidden: true, // to avoid being listed in `mc ilm` OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(ilmAddFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [COMMAND FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} DESCRIPTION: Add a lifecycle configuration rule. EXAMPLES: 1. Add a lifecycle rule with an expiration action for all objects in mybucket. {{.Prompt}} {{.HelpName}} --expire-days "200" myminio/mybucket 2. Add a lifecycle rule with a transition and a noncurrent version transition action for objects with prefix doc/ in mybucket. Tiers must exist in MinIO. Use existing tiers or add new tiers. {{.Prompt}} mc tier add minio myminio MINIOTIER-1 --endpoint https://warm-minio-1.com \ --access-key ACCESSKEY --secret-key SECRETKEY --bucket bucket1 --prefix prefix1 {{.Prompt}} mc tier add minio myminio MINIOTIER-2 --endpoint https://warm-minio-2.com \ --access-key ACCESSKEY --secret-key SECRETKEY --bucket bucket2 --prefix prefix2 {{.Prompt}} {{.HelpName}} --prefix "doc/" --transition-days "90" --transition-tier "MINIOTIER-1" \ --noncurrent-transition-days "45" --noncurrent-transition-tier "MINIOTIER-2" \ myminio/mybucket/ 3. Add a lifecycle rule with an expiration and a noncurrent version expiration action for all objects with prefix doc/ in mybucket. {{.Prompt}} {{.HelpName}} --prefix "doc/" --expire-days "300" --noncurrent-expire-days "100" \ myminio/mybucket/ `, } ilmDepRmCmd = cli.Command{ Name: "rm", Usage: "remove (if any) existing lifecycle configuration rule", Action: mainILMRemove, Hidden: true, // to avoid being listed in `mc ilm` OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(ilmRemoveFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} DESCRIPTION: Remove a lifecycle configuration rule for the bucket by ID, optionally you can remove all the lifecycle rules on a bucket with '--all --force' option. EXAMPLES: 1. Remove the lifecycle management configuration rule given by ID "bgrt1ghju" for mybucket on alias 'myminio'. ID is case sensitive. {{.Prompt}} {{.HelpName}} --id "bgrt1ghju" myminio/mybucket 2. Remove ALL the lifecycle management configuration rules for mybucket on alias 'myminio'. Because the result is complete removal, the use of --force flag is enforced. {{.Prompt}} {{.HelpName}} --all --force myminio/mybucket `, } ilmDepEditCmd = cli.Command{ Name: "edit", Usage: "modify a lifecycle configuration rule with given id", Action: mainILMEdit, Hidden: true, // to avoid being listed in `mc ilm` OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(ilmEditFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [COMMAND FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} DESCRIPTION: Modify a lifecycle configuration rule with given id. EXAMPLES: 1. Modify the expiration date for an existing rule with id "rHTY.a123". {{.Prompt}} {{.HelpName}} --id "rHTY.a123" --expiry-date "2020-09-17" s3/mybucket 2. Modify the expiration and transition days for an existing rule with id "hGHKijqpo123". {{.Prompt}} {{.HelpName}} --id "hGHKijqpo123" --expiry-days "300" \ --transition-days "200" --storage-class "GLACIER" s3/mybucket 3. Disable the rule with id "rHTY.a123". {{.Prompt}} {{.HelpName}} --id "rHTY.a123" --disable s3/mybucket `, } ilmDepLsCmd = cli.Command{ Name: "ls", Usage: "lists lifecycle configuration rules set on a bucket", Action: mainILMList, Hidden: true, // to avoid being listed in `mc ilm` OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(ilmListFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} DESCRIPTION: List lifecycle configuration rules set on a bucket. EXAMPLES: 1. List the lifecycle management rules (all fields) for mybucket on alias 'myminio'. {{.Prompt}} {{.HelpName}} myminio/mybucket 2. List the lifecycle management rules (expration date/days fields) for mybucket on alias 'myminio'. {{.Prompt}} {{.HelpName}} --expiry myminio/mybucket 3. List the lifecycle management rules (transition date/days, storage class fields) for mybucket on alias 'myminio'. {{.Prompt}} {{.HelpName}} --transition myminio/mybucket 4. List the lifecycle management rules in JSON format for mybucket on alias 'myminio'. {{.Prompt}} {{.HelpName}} --json myminio/mybucket `, } ilmDepExportCmd = cli.Command{ Name: "export", Usage: "export lifecycle configuration in JSON format", Action: mainILMExport, Hidden: true, // to avoid being listed in `mc ilm` OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET DESCRIPTION: Exports lifecycle configuration in JSON format to STDOUT. EXAMPLES: 1. Export lifecycle configuration for 'mybucket' to 'lifecycle.json' file. {{.Prompt}} {{.HelpName}} myminio/mybucket > lifecycle.json 2. Print lifecycle configuration for 'mybucket' to STDOUT. {{.Prompt}} {{.HelpName}} play/mybucket `, } ilmDepImportCmd = cli.Command{ Name: "import", Usage: "import lifecycle configuration in JSON format", Action: mainILMImport, Hidden: true, // to avoid being listed in `mc ilm` OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET DESCRIPTION: Import entire lifecycle configuration from STDIN, input file is expected to be in JSON format. EXAMPLES: 1. Set lifecycle configuration for the mybucket on alias 'myminio' to the rules imported from lifecycle.json {{.Prompt}} {{.HelpName}} myminio/mybucket < lifecycle.json 2. Set lifecycle configuration for the mybucket on alias 'myminio'. User is expected to enter the JSON contents on STDIN {{.Prompt}} {{.HelpName}} myminio/mybucket `, } ) minio-client-0.0~20250403/cmd/ilm-main.go000066400000000000000000000042501477450377600176000ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/pkg/v3/console" ) var ilmSubcommands = []cli.Command{ ilmRuleCmd, ilmTierCmd, ilmRestoreCmd, } var ilmCmd = cli.Command{ Name: "ilm", Usage: "manage bucket lifecycle", Action: mainILM, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, Subcommands: append(ilmSubcommands, ilmDepCmds...), } const ( ilmMainHeader string = "Main-Heading" ilmThemeHeader string = "Row-Header" ilmThemeRow string = "Row-Normal" ilmThemeTick string = "Row-Tick" ilmThemeExpiry string = "Row-Expiry" ilmThemeResultSuccess string = "SuccessOp" ilmThemeResultFailure string = "FailureOp" ) func mainILM(ctx *cli.Context) error { commandNotFound(ctx, ilmSubcommands) return nil } // Color scheme for the table func setILMDisplayColorScheme() { console.SetColor(ilmMainHeader, color.New(color.Bold, color.FgHiRed)) console.SetColor(ilmThemeRow, color.New(color.FgHiWhite)) console.SetColor(ilmThemeHeader, color.New(color.Bold, color.FgHiGreen)) console.SetColor(ilmThemeTick, color.New(color.FgGreen)) console.SetColor(ilmThemeExpiry, color.New(color.BlinkRapid, color.FgGreen)) console.SetColor(ilmThemeResultSuccess, color.New(color.FgGreen, color.Bold)) console.SetColor(ilmThemeResultFailure, color.New(color.FgHiYellow, color.Bold)) } minio-client-0.0~20250403/cmd/ilm-remove.go000066400000000000000000000105671477450377600201610ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/cmd/ilm" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var ilmRemoveFlags = []cli.Flag{ cli.StringFlag{ Name: "id", Usage: "id of the lifecycle rule", }, cli.BoolFlag{ Name: "force", Usage: "force flag is to be used when deleting all lifecycle configuration rules for the bucket", }, cli.BoolFlag{ Name: "all", Usage: "delete all lifecycle configuration rules of the bucket, force flag enforced", }, } var ilmRmCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove (if any) existing lifecycle configuration rule", Action: mainILMRemove, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(ilmRemoveFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} DESCRIPTION: Remove a lifecycle configuration rule for the bucket by ID, optionally you can remove all the lifecycle rules on a bucket with '--all --force' option. EXAMPLES: 1. Remove the lifecycle management configuration rule given by ID "bgrt1ghju" for mybucket on alias 'myminio'. ID is case sensitive. {{.Prompt}} {{.HelpName}} --id "bgrt1ghju" myminio/mybucket 2. Remove ALL the lifecycle management configuration rules for mybucket on alias 'myminio'. Because the result is complete removal, the use of --force flag is enforced. {{.Prompt}} {{.HelpName}} --all --force myminio/mybucket `, } type ilmRmMessage struct { Status string `json:"status"` ID string `json:"id"` Target string `json:"target"` All bool `json:"all"` } func (i ilmRmMessage) String() string { msg := "Rule ID `" + i.ID + "` from target " + i.Target + " removed." if i.All { msg = "Rules for `" + i.Target + "` removed." } return console.Colorize(ilmThemeResultSuccess, msg) } func (i ilmRmMessage) JSON() string { msgBytes, e := json.MarshalIndent(i, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } func checkILMRemoveSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, globalErrorExitStatus) } ilmAll := ctx.Bool("all") ilmForce := ctx.Bool("force") forceChk := (ilmAll && ilmForce) || (!ilmAll && !ilmForce) if !forceChk { fatalIf(errInvalidArgument(), "It is mandatory to specify --all and --force flag together for mc "+ctx.Command.FullName()+".") } if ilmAll && ilmForce { return } ilmID := ctx.String("id") if ilmID == "" { fatalIf(errInvalidArgument().Trace(ilmID), "ilm ID cannot be empty") } } func mainILMRemove(cliCtx *cli.Context) error { ctx, cancelILMImport := context.WithCancel(globalContext) defer cancelILMImport() checkILMRemoveSyntax(cliCtx) setILMDisplayColorScheme() args := cliCtx.Args() urlStr := args.Get(0) client, err := newClient(urlStr) fatalIf(err.Trace(args...), "Unable to initialize client for "+urlStr+".") ilmCfg, _, err := client.GetLifecycle(ctx) fatalIf(err.Trace(urlStr), "Unable to fetch lifecycle rules") ilmAll := cliCtx.Bool("all") ilmForce := cliCtx.Bool("force") if ilmAll && ilmForce { ilmCfg.Rules = nil // Remove all rules } else { ilmCfg, err = ilm.RemoveILMRule(ilmCfg, cliCtx.String("id")) fatalIf(err.Trace(urlStr, cliCtx.String("id")), "Unable to remove rule by id") } fatalIf(client.SetLifecycle(ctx, ilmCfg).Trace(urlStr), "Unable to set lifecycle rules") printMsg(ilmRmMessage{ Status: "success", ID: cliCtx.String("id"), All: ilmAll, Target: urlStr, }) return nil } minio-client-0.0~20250403/cmd/ilm-restore.go000066400000000000000000000214261477450377600203430ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "fmt" "time" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" ) // ilm restore specific flags. var ( ilmRestoreFlags = []cli.Flag{ cli.IntFlag{ Name: "days", Value: 1, Usage: "keep the restored copy for N days", }, cli.BoolFlag{ Name: "recursive, r", Usage: "apply recursively", }, cli.BoolFlag{ Name: "versions", Usage: "apply on versions", }, cli.StringFlag{ Name: "version-id, vid", Usage: "select a specific version id", }, } ) var ilmRestoreCmd = cli.Command{ Name: "restore", Usage: "restore archived objects", Action: mainILMRestore, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(append(ilmRestoreFlags, encCFlag), globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET DESCRIPTION: Restore a copy of one or more objects from its remote tier. This copy automatically expires after the specified number of days (Default 1 day). FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Restore one specific object {{.Prompt}} {{.HelpName}} myminio/mybucket/path/to/object 2. Restore a specific object version {{.Prompt}} {{.HelpName}} --vid "CL3sWgdSN2pNntSf6UnZAuh2kcu8E8si" myminio/mybucket/path/to/object 3. Restore all objects under a specific prefix {{.Prompt}} {{.HelpName}} --recursive myminio/mybucket/dir/ 4. Restore all objects with all versions under a specific prefix {{.Prompt}} {{.HelpName}} --recursive --versions myminio/mybucket/dir/ 5. Restore an SSE-C encrypted object. {{.Prompt}} {{.HelpName}} --enc-c "myminio/mybucket/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" myminio/mybucket/myobject.txt `, } // checkILMRestoreSyntax - validate arguments passed by user func checkILMRestoreSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, globalErrorExitStatus) } if ctx.Int("days") <= 0 { fatalIf(errDummy().Trace(), "--days should be equal or greater than 1") } if ctx.Bool("version-id") && (ctx.Bool("recursive") || ctx.Bool("versions")) { fatalIf(errDummy().Trace(), "You cannot combine --version-id with --recursive or --versions flags.") } } // Send Restore S3 API func restoreObject(ctx context.Context, targetAlias, targetURL, versionID string, days int) *probe.Error { clnt, err := newClientFromAlias(targetAlias, targetURL) if err != nil { return err } return clnt.Restore(ctx, versionID, days) } // Send restore S3 API request to one or more objects depending on the arguments func sendRestoreRequests(ctx context.Context, targetAlias, targetURL, targetVersionID string, recursive, applyOnVersions bool, days int, restoreSentReq chan *probe.Error) { defer close(restoreSentReq) client, err := newClientFromAlias(targetAlias, targetURL) if err != nil { restoreSentReq <- err return } if !recursive { err := restoreObject(ctx, targetAlias, targetURL, targetVersionID, days) restoreSentReq <- err return } prev := "" for content := range client.List(ctx, ListOptions{ Recursive: true, WithOlderVersions: applyOnVersions, ShowDir: DirNone, }) { if content.Err != nil { errorIf(content.Err.Trace(client.GetURL().String()), "Unable to list folder.") continue } err := restoreObject(ctx, targetAlias, content.URL.String(), content.VersionID, days) if err != nil { restoreSentReq <- err continue } // Avoid sending the status of each separate version // of the same object name. if prev != content.URL.String() { prev = content.URL.String() restoreSentReq <- nil } } } // Wait until an object which receives restore request is completely restored in the fast tier func waitRestoreObject(ctx context.Context, targetAlias, targetURL, versionID string, encKeyDB map[string][]prefixSSEPair) *probe.Error { clnt, err := newClientFromAlias(targetAlias, targetURL) if err != nil { return err } for { opts := StatOptions{ versionID: versionID, sse: getSSE(targetAlias+clnt.GetURL().Path, encKeyDB[targetAlias]), } st, err := clnt.Stat(ctx, opts) if err != nil { return err } if st.Restore == nil { return probe.NewError(fmt.Errorf("`%s` did not receive restore request", targetURL)) } if st.Restore != nil && !st.Restore.OngoingRestore { return nil } // Restore still going on, wait for 5 seconds before checking again time.Sleep(5 * time.Second) } } // Check and wait the restore status of one or more objects one by one. func checkRestoreStatus(ctx context.Context, targetAlias, targetURL, targetVersionID string, recursive, applyOnVersions bool, encKeyDB map[string][]prefixSSEPair, restoreStatus chan *probe.Error) { defer close(restoreStatus) client, err := newClientFromAlias(targetAlias, targetURL) if err != nil { restoreStatus <- err return } if !recursive { restoreStatus <- waitRestoreObject(ctx, targetAlias, targetURL, targetVersionID, encKeyDB) return } prev := "" for content := range client.List(ctx, ListOptions{ Recursive: true, WithOlderVersions: applyOnVersions, ShowDir: DirNone, }) { if content.Err != nil { restoreStatus <- content.Err continue } err := waitRestoreObject(ctx, targetAlias, content.URL.String(), content.VersionID, encKeyDB) if err != nil { restoreStatus <- err continue } if prev != content.URL.String() { prev = content.URL.String() restoreStatus <- nil } } } var dotCycle = 0 // Clear and print text in the same line func printStatus(msg string, args ...interface{}) { if globalJSON { return } dotCycle++ dots := bytes.Repeat([]byte{'.'}, dotCycle%3+1) fmt.Print("\n\033[1A\033[K") fmt.Printf(msg+string(dots), args...) } // Receive restore request & restore finished status and print in the console func showRestoreStatus(restoreReqStatus, restoreFinishedStatus chan *probe.Error, doneCh chan struct{}) { var sent, finished int var done bool ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for !done { select { case err, ok := <-restoreReqStatus: if !ok { done = true break } errorIf(err.Trace(), "Unable to send restore request.") if err == nil { sent++ } case <-ticker.C: } printStatus("Sent restore requests to %d object(s)", sent) } if !globalJSON { fmt.Println("") } done = false for !done { select { case err, ok := <-restoreFinishedStatus: if !ok { done = true break } errorIf(err.Trace(), "Unable to check for restore status") if err == nil { finished++ } case <-ticker.C: } printStatus("%d/%d object(s) successfully restored", finished, sent) } if !globalJSON { fmt.Println("") } else { type ilmRestore struct { Status string `json:"status"` Restored int `json:"restored"` } msgBytes, _ := json.Marshal(ilmRestore{Status: "success", Restored: sent}) fmt.Println(string(msgBytes)) } close(doneCh) } func mainILMRestore(cliCtx *cli.Context) (cErr error) { ctx, cancelILMRestore := context.WithCancel(globalContext) defer cancelILMRestore() checkILMRestoreSyntax(cliCtx) args := cliCtx.Args() aliasedURL := args.Get(0) versionID := cliCtx.String("version-id") recursive := cliCtx.Bool("recursive") includeVersions := cliCtx.Bool("versions") days := cliCtx.Int("days") encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) fatalIf(err, "Unable to parse encryption keys.") targetAlias, targetURL, _ := mustExpandAlias(aliasedURL) if targetAlias == "" { fatalIf(errDummy().Trace(), "Unable to restore the given URL") } restoreReqStatus := make(chan *probe.Error) restoreStatus := make(chan *probe.Error) done := make(chan struct{}) go func() { showRestoreStatus(restoreReqStatus, restoreStatus, done) }() sendRestoreRequests(ctx, targetAlias, targetURL, versionID, recursive, includeVersions, days, restoreReqStatus) checkRestoreStatus(ctx, targetAlias, targetURL, versionID, recursive, includeVersions, encKeyDB, restoreStatus) // Wait until the UI printed all the status <-done return nil } minio-client-0.0~20250403/cmd/ilm-rule-add.go000066400000000000000000000170711477450377600203560ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/cmd/ilm" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/lifecycle" "github.com/minio/pkg/v3/console" ) var ilmAddCmd = cli.Command{ Name: "add", Usage: "add a lifecycle configuration rule for a bucket", Action: mainILMAdd, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(ilmAddFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [COMMAND FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} DESCRIPTION: Add a lifecycle configuration rule. EXAMPLES: 1. Add a lifecycle rule with a transition and a noncurrent version transition action for objects with prefix doc/ whose size is greater than 1MiB in mybucket. Tiers must exist in MinIO. Use existing tiers or add new tiers. {{.Prompt}} mc ilm tier add minio myminio MINIOTIER-1 --endpoint https://warm-minio-1.com \ --access-key ACCESSKEY --secret-key SECRETKEY --bucket bucket1 --prefix prefix1 {{.Prompt}} {{.HelpName}} --prefix "doc/" --size-gt 1MiB --transition-days "90" --transition-tier "MINIOTIER-1" \ --noncurrent-transition-days "45" --noncurrent-transition-tier "MINIOTIER-1" \ myminio/mybucket/ 2. Add a lifecycle rule with an expiration action for all objects in mybucket. {{.Prompt}} {{.HelpName}} --expire-days "200" myminio/mybucket 3. Add a lifecycle rule with an expiration and a noncurrent version expiration action for all objects with prefix doc/ in mybucket. {{.Prompt}} {{.HelpName}} --prefix "doc/" --expire-days "300" --noncurrent-expire-days "100" \ myminio/mybucket/ `, } var ilmAddFlags = []cli.Flag{ cli.StringFlag{ Name: "prefix", Usage: "object prefix", }, cli.StringFlag{ Name: "tags", Usage: "key value pairs of the form '=&=&='", }, cli.StringFlag{ Name: "size-lt", Usage: "objects with size less than this value will be selected for the lifecycle action", }, cli.StringFlag{ Name: "size-gt", Usage: "objects with size greater than this value will be selected for the lifecycle action", }, cli.StringFlag{ Name: "expiry-date", Usage: "format 'YYYY-MM-DD' the date of expiration", Hidden: true, }, cli.StringFlag{ Name: "expiry-days", Usage: "the number of days to expiration", Hidden: true, }, cli.StringFlag{ Name: "expire-days", Usage: "number of days to expire", }, cli.BoolFlag{ Name: "expired-object-delete-marker", Usage: "remove delete markers with no parallel versions", Hidden: true, }, cli.BoolFlag{ Name: "expire-delete-marker", Usage: "expire zombie delete markers", }, cli.StringFlag{ Name: "transition-date", Usage: "format 'YYYY-MM-DD' for the date to transition", Hidden: true, }, cli.StringFlag{ Name: "transition-days", Usage: "number of days to transition", }, cli.StringFlag{ Name: "storage-class", Usage: "storage class for current version to transition into. MinIO supports tiers configured via `mc-admin-tier-add`.", Hidden: true, }, cli.StringFlag{ Name: "tier", Usage: "remote tier where current versions transition to", Hidden: true, }, cli.StringFlag{ Name: "transition-tier", Usage: "remote tier name to transition", }, cli.IntFlag{ Name: "noncurrentversion-expiration-days", Usage: "the number of days to remove noncurrent versions", Hidden: true, }, cli.StringFlag{ Name: "noncurrent-expire-days", Usage: "number of days to expire noncurrent versions", }, cli.IntFlag{ Name: "newer-noncurrentversions-expiration", Usage: "the number of noncurrent versions to retain", Hidden: true, }, cli.IntFlag{ Name: "noncurrent-expire-newer", Usage: "number of newer noncurrent versions to retain", }, cli.IntFlag{ Name: "noncurrentversion-transition-days", Usage: "the number of days to transition noncurrent versions", Hidden: true, }, cli.IntFlag{ Name: "noncurrent-transition-days", Usage: "number of days to transition noncurrent versions", }, cli.IntFlag{ Name: "newer-noncurrentversions-transition", Usage: "the number of noncurrent versions to retain. If there are this many more recent noncurrent versions they will be transitioned", Hidden: true, }, cli.IntFlag{ Name: "noncurrent-transition-newer", Usage: "number of noncurrent versions to retain in hot tier", Hidden: true, }, cli.StringFlag{ Name: "noncurrentversion-transition-storage-class", Usage: "storage class for noncurrent versions to transition into", Hidden: true, }, cli.StringFlag{ Name: "noncurrentversion-tier", Usage: "remote tier where noncurrent versions transition to", Hidden: true, }, cli.StringFlag{ Name: "noncurrent-transition-tier", Usage: "remote tier name to transition", }, cli.BoolFlag{ Name: "expire-all-object-versions", Usage: "expire all object versions", }, } type ilmAddMessage struct { Status string `json:"status"` Target string `json:"target"` ID string `json:"id"` } func (i ilmAddMessage) String() string { return console.Colorize(ilmThemeResultSuccess, "Lifecycle configuration rule added with ID `"+i.ID+"` to "+i.Target+".") } func (i ilmAddMessage) JSON() string { msgBytes, e := json.MarshalIndent(i, "", " ") fatalIf(probe.NewError(e), "Unable to encode as JSON.") return string(msgBytes) } // Validate user given arguments func checkILMAddSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, globalErrorExitStatus) } } // Calls SetBucketLifecycle with the XML representation of lifecycleConfiguration type. func mainILMAdd(cliCtx *cli.Context) error { ctx, cancelILMAdd := context.WithCancel(globalContext) defer cancelILMAdd() checkILMAddSyntax(cliCtx) setILMDisplayColorScheme() args := cliCtx.Args() urlStr := args.Get(0) client, err := newClient(urlStr) fatalIf(err.Trace(urlStr), "Unable to initialize client for "+urlStr) // Configuration that is already set. lfcCfg, _, err := client.GetLifecycle(ctx) if err != nil { if e := err.ToGoError(); minio.ToErrorResponse(e).Code == "NoSuchLifecycleConfiguration" { lfcCfg = lifecycle.NewConfiguration() } else { fatalIf(err.Trace(args...), "Unable to fetch lifecycle rules for "+urlStr) } } opts, err := ilm.GetLifecycleOptions(cliCtx) fatalIf(err.Trace(args...), "Unable to generate new lifecycle rules for the input") newRule, err := opts.ToILMRule() fatalIf(err.Trace(args...), "Unable to generate new lifecycle rules for the input") lfcCfg.Rules = append(lfcCfg.Rules, newRule) fatalIf(client.SetLifecycle(ctx, lfcCfg).Trace(urlStr), "Unable to add this lifecycle rule") printMsg(ilmAddMessage{ Status: "success", Target: urlStr, ID: opts.ID, }) return nil } minio-client-0.0~20250403/cmd/ilm-rule-edit.go000066400000000000000000000112471477450377600205520ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/cmd/ilm" "github.com/minio/mc/pkg/probe" minio "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/lifecycle" "github.com/minio/pkg/v3/console" ) var ilmEditCmd = cli.Command{ Name: "edit", Usage: "modify a lifecycle configuration rule with given id", Action: mainILMEdit, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(ilmEditFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [COMMAND FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} DESCRIPTION: Modify a lifecycle configuration rule with given id. EXAMPLES: 1. Modify the expiration date for an existing rule with id "rHTY.a123". {{.Prompt}} {{.HelpName}} --id "rHTY.a123" --expiry-date "2020-09-17" s3/mybucket 2. Modify the expiration and transition days for an existing rule with id "hGHKijqpo123". {{.Prompt}} {{.HelpName}} --id "hGHKijqpo123" --expiry-days "300" \ --transition-days "200" --storage-class "GLACIER" s3/mybucket 3. Disable the rule with id "rHTY.a123". {{.Prompt}} {{.HelpName}} --id "rHTY.a123" --disable s3/mybucket `, } var ilmEditFlags = append( // Start by showing --id in edit command []cli.Flag{ cli.StringFlag{ Name: "id", Usage: "id of the rule to be modified", }, cli.BoolFlag{ Name: "disable", Usage: "disable the rule", }, cli.BoolFlag{ Name: "enable", Usage: "enable the rule", }, }, ilmAddFlags..., ) type ilmEditMessage struct { Status string `json:"status"` Target string `json:"target"` ID string `json:"id"` } func (i ilmEditMessage) String() string { return console.Colorize(ilmThemeResultSuccess, "Lifecycle configuration rule with ID `"+i.ID+"` modified to "+i.Target+".") } func (i ilmEditMessage) JSON() string { msgBytes, e := json.MarshalIndent(i, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } // Validate user given arguments func checkILMEditSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, globalErrorExitStatus) } id := ctx.String("id") if id == "" { fatalIf(errInvalidArgument(), "ID for lifecycle rule cannot be empty, please refer mc "+ctx.Command.FullName()+" --help for more details") } } // Calls SetBucketLifecycle with the XML representation of lifecycleConfiguration type. func mainILMEdit(cliCtx *cli.Context) error { ctx, cancelILMEdit := context.WithCancel(globalContext) defer cancelILMEdit() checkILMEditSyntax(cliCtx) setILMDisplayColorScheme() args := cliCtx.Args() urlStr := args.Get(0) client, err := newClient(urlStr) fatalIf(err.Trace(urlStr), "Unable to initialize client for "+urlStr) // Configuration that is already set. lfcCfg, _, err := client.GetLifecycle(ctx) if err != nil { if e := err.ToGoError(); minio.ToErrorResponse(e).Code == "NoSuchLifecycleConfiguration" { lfcCfg = lifecycle.NewConfiguration() } else { fatalIf(err.Trace(args...), "Unable to fetch lifecycle rules for "+urlStr) } } // Configuration that needs to be set is returned by ilm.GetILMConfigToSet. // A new rule is added or the rule (if existing) is replaced opts, err := ilm.GetLifecycleOptions(cliCtx) fatalIf(err.Trace(args...), "Unable to generate new lifecycle rules for the input") var rule *lifecycle.Rule for i := range lfcCfg.Rules { if lfcCfg.Rules[i].ID != opts.ID { continue } rule = &lfcCfg.Rules[i] break } if rule == nil { fatalIf(errDummy(), "Unable to find rule id") } err = ilm.ApplyRuleFields(rule, opts) fatalIf(err.Trace(args...), "Unable to generate new lifecycle rules for the input") fatalIf(client.SetLifecycle(ctx, lfcCfg).Trace(urlStr), "Unable to set new lifecycle rules") printMsg(ilmEditMessage{ Status: "success", Target: urlStr, ID: opts.ID, }) return nil } minio-client-0.0~20250403/cmd/ilm-rule-export.go000066400000000000000000000062311477450377600211430ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "time" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/lifecycle" ) var ilmExportCmd = cli.Command{ Name: "export", Usage: "export lifecycle configuration in JSON format", Action: mainILMExport, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET DESCRIPTION: Exports lifecycle configuration in JSON format to STDOUT. EXAMPLES: 1. Export lifecycle configuration for 'mybucket' to 'lifecycle.json' file. {{.Prompt}} {{.HelpName}} myminio/mybucket > lifecycle.json 2. Print lifecycle configuration for 'mybucket' to STDOUT. {{.Prompt}} {{.HelpName}} play/mybucket `, } type ilmExportMessage struct { Status string `json:"status"` Target string `json:"target"` Config *lifecycle.Configuration `json:"config"` UpdatedAt time.Time `json:"updatedAt,omitempty"` } func (i ilmExportMessage) String() string { msgBytes, e := json.MarshalIndent(i.Config, "", " ") fatalIf(probe.NewError(e), "Unable to export ILM configuration") return string(msgBytes) } func (i ilmExportMessage) JSON() string { msgBytes, e := json.MarshalIndent(i, "", " ") fatalIf(probe.NewError(e), "Unable to marshal ILM message") return string(msgBytes) } // checkILMExportSyntax - validate arguments passed by user func checkILMExportSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, globalErrorExitStatus) } } func mainILMExport(cliCtx *cli.Context) error { ctx, cancelILMExport := context.WithCancel(globalContext) defer cancelILMExport() checkILMExportSyntax(cliCtx) setILMDisplayColorScheme() args := cliCtx.Args() urlStr := args.Get(0) client, err := newClient(urlStr) fatalIf(err.Trace(args...), "Unable to initialize client for "+urlStr+".") ilmCfg, updatedAt, err := client.GetLifecycle(ctx) fatalIf(err.Trace(args...), "Unable to get lifecycle configuration") if len(ilmCfg.Rules) == 0 { fatalIf(probe.NewError(errors.New("lifecycle configuration not set")).Trace(urlStr), "Unable to export lifecycle configuration") } printMsg(ilmExportMessage{ Status: "success", Target: urlStr, Config: ilmCfg, UpdatedAt: updatedAt, }) return nil } minio-client-0.0~20250403/cmd/ilm-rule-import.go000066400000000000000000000072041477450377600211350ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "os" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/lifecycle" "github.com/minio/pkg/v3/console" ) var ilmImportCmd = cli.Command{ Name: "import", Usage: "import lifecycle configuration in JSON format", Action: mainILMImport, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET DESCRIPTION: Import entire lifecycle configuration from STDIN, input file is expected to be in JSON format. EXAMPLES: 1. Set lifecycle configuration for the mybucket on alias 'myminio' to the rules imported from lifecycle.json {{.Prompt}} {{.HelpName}} myminio/mybucket < lifecycle.json 2. Set lifecycle configuration for the mybucket on alias 'myminio'. User is expected to enter the JSON contents on STDIN {{.Prompt}} {{.HelpName}} myminio/mybucket `, } type ilmImportMessage struct { Status string `json:"status"` Target string `json:"target"` } func (i ilmImportMessage) String() string { return console.Colorize(ilmThemeResultSuccess, "Lifecycle configuration imported successfully to `"+i.Target+"`.") } func (i ilmImportMessage) JSON() string { msgBytes, e := json.MarshalIndent(i, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } // readILMConfig read from stdin, returns XML. func readILMConfig() (*lifecycle.Configuration, *probe.Error) { // User is expected to enter the lifecycleConfiguration instance contents in JSON format cfg := lifecycle.NewConfiguration() // Consume json from STDIN dec := json.NewDecoder(os.Stdin) if e := dec.Decode(cfg); e != nil { return cfg, probe.NewError(e) } return cfg, nil } // checkILMImportSyntax - validate arguments passed by user func checkILMImportSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, globalErrorExitStatus) } } func mainILMImport(cliCtx *cli.Context) error { ctx, cancelILMImport := context.WithCancel(globalContext) defer cancelILMImport() checkILMImportSyntax(cliCtx) setILMDisplayColorScheme() args := cliCtx.Args() urlStr := args.Get(0) client, err := newClient(urlStr) fatalIf(err.Trace(urlStr), "Unable to initialize client for "+urlStr) ilmCfg, err := readILMConfig() fatalIf(err.Trace(args...), "Unable to read ILM configuration") if len(ilmCfg.Rules) == 0 { // Abort here, otherwise client.SetLifecycle will remove the lifecycle configuration // since no rules are provided and we will show a success message. fatalIf(errDummy(), "The provided ILM configuration does not contain any rule, aborting.") } fatalIf(client.SetLifecycle(ctx, ilmCfg).Trace(urlStr), "Unable to set new lifecycle rules") printMsg(ilmImportMessage{ Status: "success", Target: urlStr, }) return nil } minio-client-0.0~20250403/cmd/ilm-rule-list.go000066400000000000000000000123631477450377600206000ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "os" "time" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/cmd/ilm" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/lifecycle" ) var ilmListFlags = []cli.Flag{ cli.BoolFlag{ Name: "expiry", Usage: "display only expiration fields", }, cli.BoolFlag{ Name: "transition", Usage: "display only transition fields", }, } var ilmLsCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "lists lifecycle configuration rules set on a bucket", Action: mainILMList, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(ilmListFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} DESCRIPTION: List lifecycle configuration rules set on a bucket. EXAMPLES: 1. List the lifecycle management rules (all fields) for mybucket on alias 'myminio'. {{.Prompt}} {{.HelpName}} myminio/mybucket 2. List the lifecycle management rules (expration date/days fields) for mybucket on alias 'myminio'. {{.Prompt}} {{.HelpName}} --expiry myminio/mybucket 3. List the lifecycle management rules (transition date/days, storage class fields) for mybucket on alias 'myminio'. {{.Prompt}} {{.HelpName}} --transition myminio/mybucket 4. List the lifecycle management rules in JSON format for mybucket on alias 'myminio'. {{.Prompt}} {{.HelpName}} --json myminio/mybucket `, } type ilmListMessage struct { Status string `json:"status"` Target string `json:"target"` Context *cli.Context `json:"-"` Config *lifecycle.Configuration `json:"config"` UpdatedAt time.Time `json:"updatedAt,omitempty"` } func (i ilmListMessage) String() string { // We don't use this method to display ilm-ls output. This is here to // implement the interface required by printMsg return "" } func (i ilmListMessage) JSON() string { msgBytes, e := json.MarshalIndent(i, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } // validateILMListFlagSet - validates ilm list flags func validateILMListFlagSet(ctx *cli.Context) bool { expiryOnly := ctx.Bool("expiry") transitionOnly := ctx.Bool("transition") // Only one of expiry or transition rules can be filtered if expiryOnly && transitionOnly { return false } return true } // checkILMListSyntax - validate arguments passed by a user func checkILMListSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, globalErrorExitStatus) } if !validateILMListFlagSet(ctx) { fatalIf(errInvalidArgument(), "only one display field flag is allowed per ls command. Refer mc "+ctx.Command.FullName()+" --help.") } } func mainILMList(cliCtx *cli.Context) error { ctx, cancelILMList := context.WithCancel(globalContext) defer cancelILMList() checkILMListSyntax(cliCtx) setILMDisplayColorScheme() args := cliCtx.Args() urlStr := args.Get(0) // Note: validateILMListFlagsSet ensures we deal with only valid // combinations here. var filter ilm.LsFilter if v := cliCtx.Bool("expiry"); v { filter = ilm.ExpiryOnly } if v := cliCtx.Bool("transition"); v { filter = ilm.TransitionOnly } client, err := newClient(urlStr) fatalIf(err.Trace(urlStr), "Unable to initialize client for "+urlStr) ilmCfg, updatedAt, err := client.GetLifecycle(ctx) fatalIf(err.Trace(args...), "Unable to get lifecycle") if len(ilmCfg.Rules) == 0 { fatalIf(probe.NewError(errors.New("lifecycle configuration not set")).Trace(urlStr), "Unable to ls lifecycle configuration") } // applies listing filter on ILM rules ilmCfg.Rules = filter.Apply(ilmCfg.Rules) if globalJSON { printMsg(ilmListMessage{ Status: "success", Target: urlStr, Context: cliCtx, Config: ilmCfg, UpdatedAt: updatedAt, }) return nil } for _, tbl := range ilm.ToTables(ilmCfg) { rows := tbl.Rows() if len(rows) == 0 { continue } t := table.NewWriter() t.SetOutputMirror(os.Stdout) var colCfgs []table.ColumnConfig for i := 0; i < len(rows[0]); i++ { colCfgs = append(colCfgs, table.ColumnConfig{ Align: text.AlignCenter, }) } t.SetColumnConfigs(colCfgs) t.SetTitle(tbl.Title()) t.AppendHeader(tbl.ColumnHeaders()) t.AppendRows(rows) t.SetStyle(table.StyleLight) t.Render() } return nil } minio-client-0.0~20250403/cmd/ilm-rule-main.go000066400000000000000000000024201477450377600205420ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var ilmRuleSubcommands = []cli.Command{ ilmAddCmd, ilmEditCmd, ilmLsCmd, ilmRmCmd, ilmExportCmd, ilmImportCmd, } var ilmRuleCmd = cli.Command{ Name: "rule", Usage: "manage bucket lifecycle rules", Before: setGlobalsFromContext, Action: mainILMRule, Subcommands: ilmRuleSubcommands, Flags: globalFlags, HideHelpCommand: true, } func mainILMRule(ctx *cli.Context) error { commandNotFound(ctx, ilmRuleSubcommands) return nil } minio-client-0.0~20250403/cmd/ilm-tier-add.go000066400000000000000000000345011477450377600203470ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "os" "slices" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminTierAddFlags = []cli.Flag{ cli.StringFlag{ Name: "endpoint", Value: "", Usage: "remote tier endpoint. e.g https://s3.amazonaws.com", }, cli.StringFlag{ Name: "region", Value: "", Usage: "remote tier region. e.g us-west-2", }, cli.StringFlag{ Name: "access-key", Value: "", Usage: "AWS S3 or compatible object storage access-key", }, cli.StringFlag{ Name: "secret-key", Value: "", Usage: "AWS S3 or compatible object storage secret-key", }, cli.BoolFlag{ Name: "use-aws-role", Usage: "use AWS S3 role", }, cli.StringFlag{ Name: "aws-role-arn", Usage: "use AWS S3 role name", }, cli.StringFlag{ Name: "aws-web-identity-file", Usage: "use AWS S3 web identity file", }, cli.StringFlag{ Name: "account-name", Value: "", Usage: "Azure Blob Storage account name", }, cli.StringFlag{ Name: "account-key", Value: "", Usage: "Azure Blob Storage account key", }, cli.StringFlag{ Name: "az-sp-tenant-id", Value: "", Usage: "Directory ID for the Azure service principal account", }, cli.StringFlag{ Name: "az-sp-client-id", Value: "", Usage: "The client ID of the Azure service principal account", }, cli.StringFlag{ Name: "az-sp-client-secret", Value: "", Usage: "The client secret of the Azure service principal account", }, cli.StringFlag{ Name: "credentials-file", Value: "", Usage: "path to Google Cloud Storage credentials file", }, cli.StringFlag{ Name: "bucket", Value: "", Usage: "remote tier bucket", }, cli.StringFlag{ Name: "prefix", Value: "", Usage: "remote tier prefix", }, cli.StringFlag{ Name: "storage-class", Value: "", Usage: "remote tier storage-class", }, cli.BoolFlag{ Name: "force", Hidden: true, Usage: "ignores in-use check for remote tier bucket/prefix", }, } var adminTierAddCmd = cli.Command{ Name: "add", Usage: "add a new remote tier target", Action: mainAdminTierAdd, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, adminTierAddFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TYPE ALIAS NAME [FLAGS] TYPE: Transition objects to supported cloud storage backend tier. Supported values are minio, s3, azure and gcs. NAME: Name of the remote tier target. e.g WARM-TIER FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Configure a new remote tier which transitions objects to a bucket in a MinIO deployment: {{.Prompt}} {{.HelpName}} minio myminio WARM-MINIO-TIER --endpoint https://warm-minio.com \ --access-key ACCESSKEY --secret-key SECRETKEY --bucket mybucket --prefix myprefix/ 2. Configure a new remote tier which transitions objects to a bucket in Azure Blob Storage: {{.Prompt}} {{.HelpName}} azure myminio AZTIER --account-name ACCOUNT-NAME --account-key ACCOUNT-KEY \ --bucket myazurebucket --prefix myazureprefix/ 3. Configure a new remote tier which transitions objects to a bucket in AWS S3 with STANDARD storage class: {{.Prompt}} {{.HelpName}} s3 myminio S3TIER --endpoint https://s3.amazonaws.com \ --access-key ACCESSKEY --secret-key SECRETKEY --bucket mys3bucket --prefix mys3prefix/ \ --storage-class "STANDARD" --region us-west-2 4. Configure a new remote tier which transitions objects to a bucket in Google Cloud Storage: {{.Prompt}} {{.HelpName}} gcs myminio GCSTIER --credentials-file /path/to/credentials.json \ --bucket mygcsbucket --prefix mygcsprefix/ `, } // checkAdminTierAddSyntax validates all the positional arguments func checkAdminTierAddSyntax(ctx *cli.Context) { argsNr := len(ctx.Args()) if argsNr < 3 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } if argsNr > 3 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Incorrect number of arguments for tier add command.") } } // The list of AWS S3 storage classes that can be used with MinIO ILM tiering var supportedAWSTierSC = []string{"STANDARD", "REDUCED_REDUNDANCY", "STANDARD_IA"} // fetchTierConfig returns a TierConfig given a tierName, a tierType and ctx to // lookup command-line flags from. It exits with non-zero error code if any of // the flags contain invalid values. func fetchTierConfig(ctx *cli.Context, tierName string, tierType madmin.TierType) *madmin.TierConfig { switch tierType { case madmin.MinIO: accessKey := ctx.String("access-key") secretKey := ctx.String("secret-key") if accessKey == "" || secretKey == "" { fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s remote tier requires access credentials", tierType)) } bucket := ctx.String("bucket") if bucket == "" { fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s remote tier requires target bucket", tierType)) } endpoint := ctx.String("endpoint") if endpoint == "" { fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s remote tier requires target endpoint", tierType)) } minioOpts := []madmin.MinIOOptions{} prefix := ctx.String("prefix") if prefix != "" { minioOpts = append(minioOpts, madmin.MinIOPrefix(prefix)) } region := ctx.String("region") if region != "" { minioOpts = append(minioOpts, madmin.MinIORegion(region)) } minioCfg, e := madmin.NewTierMinIO(tierName, endpoint, accessKey, secretKey, bucket, minioOpts...) fatalIf(probe.NewError(e), "Invalid configuration for MinIO tier") return minioCfg case madmin.S3: accessKey := ctx.IsSet("access-key") secretKey := ctx.IsSet("secret-key") useAwsRole := ctx.IsSet("use-aws-role") awsRoleArn := ctx.IsSet("aws-role-arn") awsWebIdentity := ctx.IsSet("aws-web-identity-file") // Extensive flag check switch { case !accessKey && !secretKey && !useAwsRole && !awsRoleArn && !awsWebIdentity: fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s: No authentication mechanism was provided", tierType)) case (accessKey || secretKey) && (useAwsRole || awsRoleArn || awsWebIdentity): fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s: Static credentials cannot be combined with AWS role authentication", tierType)) case useAwsRole && (awsRoleArn || awsWebIdentity): fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s: --use-aws-role cannot be combined with --aws-role-arn or --aws-web-identity-file", tierType)) case (awsRoleArn && !awsWebIdentity) || (!awsRoleArn && awsWebIdentity): fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s: Both --use-aws-role and --aws-web-identity-file are required to enable web identity token based authentication", tierType)) case (accessKey && !secretKey) || (!accessKey && secretKey): fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s: Both --access-key and --secret-key are required to enable static credentials authentication", tierType)) } bucket := ctx.String("bucket") if bucket == "" { fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s remote tier requires target bucket", tierType)) } s3Opts := []madmin.S3Options{} prefix := ctx.String("prefix") if prefix != "" { s3Opts = append(s3Opts, madmin.S3Prefix(prefix)) } endpoint := ctx.String("endpoint") if endpoint != "" { s3Opts = append(s3Opts, madmin.S3Endpoint(endpoint)) } region := ctx.String("region") if region != "" { s3Opts = append(s3Opts, madmin.S3Region(region)) } s3SC := ctx.String("storage-class") if s3SC != "" { if !slices.Contains(supportedAWSTierSC, s3SC) { fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("unsupported storage-class type %s", s3SC)) } s3Opts = append(s3Opts, madmin.S3StorageClass(s3SC)) } if ctx.IsSet("use-aws-role") { s3Opts = append(s3Opts, madmin.S3AWSRole()) } if ctx.IsSet("aws-role-arn") { s3Opts = append(s3Opts, madmin.S3AWSRoleARN(ctx.String("aws-role-arn"))) } if ctx.IsSet("aws-web-identity-file") { s3Opts = append(s3Opts, madmin.S3AWSRoleWebIdentityTokenFile(ctx.String("aws-web-identity-file"))) } s3Cfg, e := madmin.NewTierS3(tierName, ctx.String("access-key"), ctx.String("secret-key"), bucket, s3Opts...) fatalIf(probe.NewError(e), "Invalid configuration for AWS S3 compatible remote tier") return s3Cfg case madmin.Azure: accountName := ctx.String("account-name") accountKey := ctx.String("account-key") if accountName == "" { fatalIf(errDummy().Trace(), fmt.Sprintf("%s remote tier requires the storage account name", tierType)) } if accountKey == "" && (ctx.String("az-sp-tenant-id") == "" || ctx.String("az-sp-client-id") == "" || ctx.String("az-sp-client-secret") == "") { fatalIf(errDummy().Trace(), fmt.Sprintf("%s remote tier requires static credentials OR service principal credentials", tierType)) } bucket := ctx.String("bucket") if bucket == "" { fatalIf(errDummy().Trace(), fmt.Sprintf("%s remote tier requires target bucket", tierType)) } azOpts := []madmin.AzureOptions{} endpoint := ctx.String("endpoint") if endpoint != "" { azOpts = append(azOpts, madmin.AzureEndpoint(endpoint)) } region := ctx.String("region") if region != "" { azOpts = append(azOpts, madmin.AzureRegion(region)) } prefix := ctx.String("prefix") if prefix != "" { azOpts = append(azOpts, madmin.AzurePrefix(prefix)) } if ctx.String("az-sp-tenant-id") != "" || ctx.String("az-sp-client-id") != "" || ctx.String("az-sp-client-secret") != "" { azOpts = append(azOpts, madmin.AzureServicePrincipal(ctx.String("az-sp-tenant-id"), ctx.String("az-sp-client-id"), ctx.String("az-sp-client-secret"))) } azCfg, e := madmin.NewTierAzure(tierName, accountName, accountKey, bucket, azOpts...) fatalIf(probe.NewError(e), "Invalid configuration for Azure Blob Storage remote tier") return azCfg case madmin.GCS: bucket := ctx.String("bucket") if bucket == "" { fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s remote requires target bucket", tierType)) } gcsOpts := []madmin.GCSOptions{} prefix := ctx.String("prefix") if prefix != "" { gcsOpts = append(gcsOpts, madmin.GCSPrefix(prefix)) } region := ctx.String("region") if region != "" { gcsOpts = append(gcsOpts, madmin.GCSRegion(region)) } credsPath := ctx.String("credentials-file") credsBytes, e := os.ReadFile(credsPath) fatalIf(probe.NewError(e), "Failed to read credentials file") gcsCfg, e := madmin.NewTierGCS(tierName, credsBytes, bucket, gcsOpts...) fatalIf(probe.NewError(e), "Invalid configuration for Google Cloud Storage remote tier") return gcsCfg } fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("Invalid remote tier type %s", tierType)) return nil } type tierMessage struct { op string Status string `json:"status"` TierName string `json:"tierName"` TierType string `json:"tierType"` Endpoint string `json:"tierEndpoint"` Bucket string `json:"bucket"` Prefix string `json:"prefix,omitempty"` Region string `json:"region,omitempty"` TierParams map[string]string `json:"tierParams,omitempty"` } // String returns string representation of msg func (msg *tierMessage) String() string { switch msg.op { case "add": addMsg := fmt.Sprintf("Added remote tier %s of type %s", msg.TierName, msg.TierType) return console.Colorize("TierMessage", addMsg) case "rm": rmMsg := fmt.Sprintf("Removed remote tier %s", msg.TierName) return console.Colorize("TierMessage", rmMsg) case "verify": verifyMsg := fmt.Sprintf("Verified remote tier %s", msg.TierName) return console.Colorize("TierMessage", verifyMsg) case "check": checkMsg := fmt.Sprintf("Remote tier connectivity check for %s was successful", msg.TierName) return console.Colorize("TierMessage", checkMsg) case "edit": editMsg := fmt.Sprintf("Updated remote tier %s", msg.TierName) return console.Colorize("TierMessage", editMsg) } return "" } // JSON returns json encoded msg func (msg *tierMessage) JSON() string { jsonMessageBytes, e := json.MarshalIndent(msg, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // SetTierConfig sets TierConfig related fields func (msg *tierMessage) SetTierConfig(sCfg *madmin.TierConfig) { msg.TierName = sCfg.Name msg.TierType = sCfg.Type.String() msg.Endpoint = sCfg.Endpoint() msg.Bucket = sCfg.Bucket() msg.Prefix = sCfg.Prefix() msg.Region = sCfg.Region() switch sCfg.Type { case madmin.S3: msg.TierParams = map[string]string{ "storageClass": sCfg.S3.StorageClass, } } } func mainAdminTierAdd(ctx *cli.Context) error { checkAdminTierAddSyntax(ctx) console.SetColor("TierMessage", color.New(color.FgGreen)) args := ctx.Args() tierTypeStr := args.Get(0) tierType, e := madmin.NewTierType(tierTypeStr) fatalIf(probe.NewError(e), "Unsupported tier type") aliasedURL := args.Get(1) tierName := args.Get(2) if tierName == "" { fatalIf(errInvalidArgument(), "Tier name can't be empty") } // Create a new MinIO Admin Client client, cerr := newAdminClient(aliasedURL) fatalIf(cerr, "Unable to initialize admin connection.") tCfg := fetchTierConfig(ctx, strings.ToUpper(tierName), tierType) ignoreInUse := ctx.Bool("force") if ignoreInUse { fatalIf(probe.NewError(client.AddTierIgnoreInUse(globalContext, tCfg)).Trace(args...), "Unable to configure remote tier target") } else { fatalIf(probe.NewError(client.AddTier(globalContext, tCfg)).Trace(args...), "Unable to configure remote tier target") } msg := &tierMessage{ op: ctx.Command.Name, Status: "success", } msg.SetTierConfig(tCfg) printMsg(msg) return nil } minio-client-0.0~20250403/cmd/ilm-tier-check.go000066400000000000000000000025011477450377600206670ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var ilmTierCheckCmd = cli.Command{ Name: "check", Usage: "validate remote tier configuration", Action: mainAdminTierVerify, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET NAME NAME: Name of remote tier target. e.g WARM-TIER FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Validate a tier config given by name. {{.Prompt}} {{.HelpName}} myminio WARM-TIER `, } minio-client-0.0~20250403/cmd/ilm-tier-edit.go000066400000000000000000000120771477450377600205500ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminTierEditFlags = []cli.Flag{ cli.StringFlag{ Name: "access-key", Value: "", Usage: "AWS S3 or compatible object storage access-key", }, cli.StringFlag{ Name: "secret-key", Value: "", Usage: "AWS S3 or compatible object storage secret-key", }, cli.BoolFlag{ Name: "use-aws-role", Usage: "use AWS S3 role", }, cli.StringFlag{ Name: "account-key", Value: "", Usage: "Azure Blob Storage account key", }, cli.StringFlag{ Name: "az-sp-tenant-id", Value: "", Usage: "Directory ID for the Azure service principal account", }, cli.StringFlag{ Name: "az-sp-client-id", Value: "", Usage: "The client ID of the Azure service principal account", }, cli.StringFlag{ Name: "az-sp-client-secret", Value: "", Usage: "The client secret of the Azure service principal account", }, cli.StringFlag{ Name: "credentials-file", Value: "", Usage: "path to Google Cloud Storage credentials file", }, } var adminTierEditCmd = cli.Command{ Name: "edit", Usage: "update an existing remote tier configuration", Action: mainAdminTierEdit, Hidden: true, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, adminTierEditFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS NAME NAME: Name of remote tier. e.g WARM-TIER FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Update credentials for an existing Azure Blob Storage remote tier: {{.Prompt}} {{.HelpName}} myminio AZTIER --account-key ACCOUNT-KEY 2. Update credentials for an existing AWS S3 compatible remote tier: {{.Prompt}} {{.HelpName}} myminio S3TIER --access-key ACCESS-KEY --secret-key SECRET-KEY 3. Update credentials for an existing Google Cloud Storage remote tier: {{.Prompt}} {{.HelpName}} myminio GCSTIER --credentials-file /path/to/credentials.json `, } // checkAdminTierEditSyntax - validate all the postitional arguments func checkAdminTierEditSyntax(ctx *cli.Context) { argsNr := len(ctx.Args()) if argsNr < 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } if argsNr > 2 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Incorrect number of arguments for tier-edit subcommand.") } } func mainAdminTierEdit(ctx *cli.Context) error { checkAdminTierEditSyntax(ctx) console.SetColor("TierMessage", color.New(color.FgGreen)) args := ctx.Args() aliasedURL := args.Get(0) tierName := args.Get(1) // Create a new MinIO Admin Client client, cerr := newAdminClient(aliasedURL) fatalIf(cerr, "Unable to initialize admin connection.") var creds madmin.TierCreds accessKey := ctx.String("access-key") secretKey := ctx.String("secret-key") credsPath := ctx.String("credentials-file") useAwsRole := ctx.IsSet("use-aws-role") // Azure, either account-key or one of the 3 service principal flags are required accountKey := ctx.String("account-key") azSPTenantID := ctx.String("az-sp-tenant-id") azSPClientID := ctx.String("az-sp-client-id") azSPClientSecret := ctx.String("az-sp-client-secret") switch { case accessKey != "" && secretKey != "" && !useAwsRole: // S3 tier creds.AccessKey = accessKey creds.SecretKey = secretKey case useAwsRole: creds.AWSRole = true case accountKey != "": // Azure tier, account key given creds.SecretKey = accountKey case azSPTenantID != "" || azSPClientID != "" || azSPClientSecret != "": // Azure tier, SP creds given creds.AzSP = madmin.ServicePrincipalAuth{ TenantID: azSPTenantID, ClientID: azSPClientID, ClientSecret: azSPClientSecret, } case credsPath != "": // GCS tier credsBytes, e := os.ReadFile(credsPath) fatalIf(probe.NewError(e), "Unable to read credentials file at %s", credsPath) creds.CredsJSON = credsBytes default: fatalIf(errInvalidArgument().Trace(args.Tail()...), "Insufficient credential information supplied to update remote tier target credentials") } e := client.EditTier(globalContext, tierName, creds) fatalIf(probe.NewError(e).Trace(args...), "Unable to edit remote tier") printMsg(&tierMessage{ op: ctx.Command.Name, Status: "success", TierName: tierName, }) return nil } minio-client-0.0~20250403/cmd/ilm-tier-info.go000066400000000000000000000143111477450377600205470ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "strconv" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/table" "github.com/dustin/go-humanize" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminTierInfoCmd = cli.Command{ Name: "info", Usage: "display tier statistics", Action: mainAdminTierInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS [NAME] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Prints per-tier statistics of all remote tier targets configured on 'myminio': {{.Prompt}} {{.HelpName}} myminio 2. Print per-tier statistics of given tier name 'MINIOTIER-1': {{.Prompt}} {{.HelpName}} myminio MINIOTIER-1 `, } // checkAdminTierInfoSyntax - validate all the passed arguments func checkAdminTierInfoSyntax(ctx *cli.Context) { argsNr := len(ctx.Args()) if argsNr < 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } if argsNr == 2 && globalJSON { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Incorrect number of arguments for tier-info subcommand with json output.") } if argsNr > 2 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Incorrect number of arguments for tier-info subcommand.") } } type tierInfos []madmin.TierInfo var _ table.Data = tierInfos(nil) func (t tierInfos) At(row, col int) string { cell := "-" switch col { case 0: cell = t[row].Name case 1: cell = t[row].Type case 2: cell = tierInfoType(t[row].Type) case 3: cell = humanize.IBytes(t[row].Stats.TotalSize) case 4: cell = strconv.Itoa(t[row].Stats.NumObjects) case 5: cell = strconv.Itoa(t[row].Stats.NumVersions) } return cell } func (t tierInfos) Rows() int { return len(t) } func (t tierInfos) Columns() int { return len(t.Headers()) } func (t tierInfos) Headers() []string { return []string{ "Tier Name", "API", "Type", "Usage", "Objects", "Versions", } } func (t tierInfos) MarshalJSON() ([]byte, error) { type tierInfo struct { Name string API string Type string Stats madmin.TierStats DailyStats madmin.DailyTierStats } ts := make([]tierInfo, 0, len(t)) for _, tInfo := range t { ts = append(ts, tierInfo{ Name: tInfo.Name, API: tInfo.Type, Type: tierInfoType(tInfo.Type), Stats: tInfo.Stats, DailyStats: tInfo.DailyStats, }) } return json.Marshal(ts) } func tierInfoType(tierType string) string { if tierType == "internal" { return "hot" } return "warm" } func mainAdminTierInfo(ctx *cli.Context) error { checkAdminTierInfoSyntax(ctx) args := ctx.Args() aliasedURL := args.Get(0) tier := args.Get(1) // Create a new MinIO Admin Client client, cerr := newAdminClient(aliasedURL) fatalIf(cerr, "Unable to initialize admin connection.") var msg tierInfoMessage tInfos, e := client.TierStats(globalContext) if e != nil { msg = tierInfoMessage{ Status: "error", Context: ctx, Error: e.Error(), } } else { msg = tierInfoMessage{ Status: "success", Context: ctx, TierInfos: tierInfos(tInfos), } } if globalJSON { printMsg(&msg) return nil } var ( HeaderStyle = lipgloss.NewStyle().Bold(true).Align(lipgloss.Center) EvenRowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("3")).Align(lipgloss.Center) OddRowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("4")).Align(lipgloss.Center) NumbersStyle = lipgloss.NewStyle().Align(lipgloss.Right) ) tableData := tierInfos(tInfos) var filteredData table.Data filteredData = table.NewFilter(tableData). Filter(func(row int) bool { if tier == "" { return true } return tableData.At(row, 0) == tier }) if filteredData.Rows() == 0 { // check if that tier name is valid // if valid will show that with empty data tiers, e := client.ListTiers(globalContext) fatalIf(probe.NewError(e).Trace(args...), "Unable to list configured remote tier targets") for _, t := range tiers { if t.Name == tier { filteredData = tierInfos([]madmin.TierInfo{ { Name: tier, Type: t.Type.String(), }, }) break } } } tbl := table.New(). Border(lipgloss.NormalBorder()). Headers(tableData.Headers()...). StyleFunc(func(row, col int) lipgloss.Style { var style lipgloss.Style switch { case row == 0: return HeaderStyle case row%2 == 0: style = EvenRowStyle default: style = OddRowStyle } switch col { case 3, 4, 5: style = NumbersStyle.Foreground(style.GetForeground()) } return style }). Data(filteredData) if filteredData.Rows() == 0 { if tier != "" { console.Printf("No remote tiers' name match %s\n", tier) } else { console.Println("No remote tiers configured") } return nil } fmt.Println(tbl) return nil } type tierInfoMessage struct { Status string `json:"status"` Context *cli.Context `json:"-"` TierInfos tierInfos `json:"tiers,omitempty"` Error string `json:"error,omitempty"` } // String method returns a tabular listing of remote tier configurations. func (msg *tierInfoMessage) String() string { return "" // Not used, present to satisfy msg interface } // JSON method returns JSON encoding of msg. func (msg *tierInfoMessage) JSON() string { b, _ := json.Marshal(msg) return string(b) } minio-client-0.0~20250403/cmd/ilm-tier-list.go000066400000000000000000000112041477450377600205650ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "cmp" "fmt" "slices" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/table" "github.com/minio/cli" json "github.com/minio/colorjson" madmin "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var adminTierListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list configured remote tier targets", Action: mainAdminTierList, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List remote tier targets configured on 'myminio': {{.Prompt}} {{.HelpName}} myminio `, } // checkAdminTierListSyntax - validate all the passed arguments func checkAdminTierListSyntax(ctx *cli.Context) { argsNr := len(ctx.Args()) if argsNr < 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } if argsNr > 1 { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "Incorrect number of arguments for tier-ls subcommand.") } } func storageClass(t *madmin.TierConfig) string { switch t.Type { case madmin.S3: return t.S3.StorageClass case madmin.Azure: return t.Azure.StorageClass case madmin.GCS: return t.GCS.StorageClass default: return "" } } type tierListMessage struct { Status string `json:"status"` Context *cli.Context `json:"-"` Tiers []*madmin.TierConfig `json:"tiers"` } // String method returns a tabular listing of remote tier configurations. func (msg *tierListMessage) String() string { return "" // Not used in rendering; only to satisfy msg interface } // JSON method returns JSON encoding of msg. func (msg *tierListMessage) JSON() string { b, _ := json.Marshal(msg) return string(b) } func mainAdminTierList(ctx *cli.Context) error { checkAdminTierListSyntax(ctx) args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, cerr := newAdminClient(aliasedURL) fatalIf(cerr, "Unable to initialize admin connection.") tiers, e := client.ListTiers(globalContext) fatalIf(probe.NewError(e).Trace(args...), "Unable to list configured remote tier targets") if len(tiers) == 0 { console.Infoln("No remote tier targets found for alias '" + aliasedURL + "'. Use `mc ilm tier add` to configure one.") return nil } if globalJSON { printMsg(&tierListMessage{ Status: "success", Context: ctx, Tiers: tiers, }) return nil } tableData := tierTable(tiers) slices.SortFunc(tableData, func(a, b *madmin.TierConfig) int { return cmp.Compare(a.Name, b.Name) }) tbl := table.New(). Border(lipgloss.NormalBorder()). Headers(tableData.Headers()...). StyleFunc(func(row, _ int) lipgloss.Style { switch { case row == 0: return lipgloss.NewStyle().Bold(true).Align(lipgloss.Center) case row%2 == 0: return lipgloss.NewStyle().Foreground(lipgloss.Color("3")).Align(lipgloss.Center) default: return lipgloss.NewStyle().Foreground(lipgloss.Color("4")).Align(lipgloss.Center) } }). Data(tableData) fmt.Println(tbl) return nil } type tierTable []*madmin.TierConfig var _ table.Data = tierTable(nil) func (tt tierTable) Headers() []string { return []string{ "Name", "Type", "Endpoint", "Bucket", "Prefix", "Region", "Storage-Class", } } func (tt tierTable) At(row, col int) string { tc := []*madmin.TierConfig(tt) cell := "" switch col { case 0: cell = tc[row].Name case 1: cell = tc[row].Type.String() case 2: cell = tc[row].Endpoint() case 3: cell = tc[row].Bucket() case 4: cell = tc[row].Prefix() case 5: cell = tc[row].Region() case 6: cell = storageClass(tc[row]) } if cell == "" { return "-" } return cell } func (tt tierTable) Rows() int { return len(tt) } func (tt tierTable) Columns() int { return len(tt.Headers()) } minio-client-0.0~20250403/cmd/ilm-tier-main.go000066400000000000000000000025171477450377600205450ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var ilmTierSubcommands = []cli.Command{ adminTierInfoCmd, adminTierListCmd, adminTierAddCmd, adminTierEditCmd, ilmTierUpdateCmd, adminTierVerifyCmd, ilmTierCheckCmd, adminTierRmCmd, } var ilmTierCmd = cli.Command{ Name: "tier", Usage: "manage remote tiers", Action: mainILMTier, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: ilmTierSubcommands, HideHelpCommand: true, } func mainILMTier(ctx *cli.Context) error { commandNotFound(ctx, ilmTierSubcommands) return nil } minio-client-0.0~20250403/cmd/ilm-tier-remove.go000066400000000000000000000054441477450377600211200ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var adminTierRmFlags = []cli.Flag{ cli.BoolFlag{ Name: "force", Usage: "forcefully remove the specified tier", Hidden: true, }, cli.BoolFlag{ Name: "dangerous", Usage: "additional flag to be required in addition to force flag", Hidden: true, }, } var adminTierRmCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove an empty remote tier", Action: mainAdminTierRm, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, adminTierRmFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS NAME NAME: Name of remote tier target. e.g WARM-TIER FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove an empty tier by name 'WARM-TIER': {{.Prompt}} {{.HelpName}} myminio WARM-TIER `, } func mainAdminTierRm(ctx *cli.Context) error { args := ctx.Args() nArgs := len(args) if nArgs < 2 { showCommandHelpAndExit(ctx, 1) } if nArgs != 2 { fatalIf(errInvalidArgument().Trace(args.Tail()...), "Incorrect number of arguments for tier remove command.") } aliasedURL := args.Get(0) tierName := args.Get(1) if tierName == "" { fatalIf(errInvalidArgument(), "Tier name can't be empty") } if ctx.Bool("force") && !ctx.Bool("dangerous") { fatalIf(errInvalidArgument(), "This operation results in an irreversible disconnection from the specified remote tier. If you are really sure, retry this command with ‘--force’ and ‘--dangerous’ flags.") } // Create a new MinIO Admin Client client, cerr := newAdminClient(aliasedURL) fatalIf(cerr, "Unable to initialize admin connection.") e := client.RemoveTierV2(globalContext, tierName, madmin.RemoveTierOpts{Force: ctx.Bool("force")}) fatalIf(probe.NewError(e).Trace(args...), "Unable to remove remote tier target") printMsg(&tierMessage{ op: ctx.Command.Name, Status: "success", TierName: tierName, }) return nil } minio-client-0.0~20250403/cmd/ilm-tier-update.go000066400000000000000000000033431477450377600211010ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var ilmTierUpdateCmd = cli.Command{ Name: "update", Usage: "update an existing remote tier configuration", Action: mainAdminTierEdit, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, adminTierEditFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS NAME NAME: Name of remote tier. e.g WARM-TIER FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Update credentials for an existing Azure Blob Storage remote tier: {{.Prompt}} {{.HelpName}} myminio AZTIER --account-key ACCOUNT-KEY 2. Update credentials for an existing AWS S3 compatible remote tier: {{.Prompt}} {{.HelpName}} myminio S3TIER --access-key ACCESS-KEY --secret-key SECRET-KEY 3. Update credentials for an existing Google Cloud Storage remote tier: {{.Prompt}} {{.HelpName}} myminio GCSTIER --credentials-file /path/to/credentials.json `, } minio-client-0.0~20250403/cmd/ilm-tier-verify.go000066400000000000000000000042611477450377600211230ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" "github.com/minio/mc/pkg/probe" ) var adminTierVerifyCmd = cli.Command{ Name: "verify", Usage: "verifies if remote tier configuration is valid", Action: mainAdminTierVerify, Hidden: true, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET NAME NAME: Name of remote tier target. e.g WARM-TIER FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Verify if a tier config is valid. {{.Prompt}} {{.HelpName}} myminio WARM-TIER `, } func mainAdminTierVerify(ctx *cli.Context) error { args := ctx.Args() nArgs := len(args) if nArgs < 2 { showCommandHelpAndExit(ctx, 1) } if nArgs != 2 { fatalIf(errInvalidArgument().Trace(args.Tail()...), "Incorrect number of arguments for tier verify command.") } aliasedURL := args.Get(0) tierName := args.Get(1) if tierName == "" { fatalIf(errInvalidArgument(), "Tier name can't be empty") } // Create a new MinIO Admin Client client, cerr := newAdminClient(aliasedURL) fatalIf(cerr, "Unable to initialize admin connection.") e := client.VerifyTier(globalContext, tierName) fatalIf(probe.NewError(e).Trace(args...), "Unable to verify remote tier target") printMsg(&tierMessage{ op: ctx.Command.Name, Status: "success", TierName: tierName, }) return nil } minio-client-0.0~20250403/cmd/ilm/000077500000000000000000000000001477450377600163265ustar00rootroot00000000000000minio-client-0.0~20250403/cmd/ilm/options.go000066400000000000000000000371021477450377600203530ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package ilm import ( "errors" "fmt" "math" "strconv" "strings" "github.com/dustin/go-humanize" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/lifecycle" "github.com/rs/xid" ) const defaultILMDateFormat string = "2006-01-02" // RemoveILMRule - Remove the ILM rule (with ilmID) from the configuration in XML that is provided. func RemoveILMRule(lfcCfg *lifecycle.Configuration, ilmID string) (*lifecycle.Configuration, *probe.Error) { if lfcCfg == nil { return lfcCfg, probe.NewError(fmt.Errorf("lifecycle configuration not set")) } if len(lfcCfg.Rules) == 0 { return lfcCfg, probe.NewError(fmt.Errorf("lifecycle configuration not set")) } n := 0 for _, rule := range lfcCfg.Rules { if rule.ID != ilmID { lfcCfg.Rules[n] = rule n++ } } if n == len(lfcCfg.Rules) && len(lfcCfg.Rules) > 0 { // if there was no filtering then rules will be of same length, means we didn't find // our ilm id return an error here. return lfcCfg, probe.NewError(fmt.Errorf("lifecycle rule for id '%s' not found", ilmID)) } lfcCfg.Rules = lfcCfg.Rules[:n] return lfcCfg, nil } // LifecycleOptions is structure to encapsulate type LifecycleOptions struct { ID string Status *bool Prefix *string Tags *string ObjectSizeLessThan *int64 ObjectSizeGreaterThan *int64 ExpiryDate *string ExpiryDays *string TransitionDate *string TransitionDays *string StorageClass *string ExpiredObjectDeleteMarker *bool NoncurrentVersionExpirationDays *int NewerNoncurrentExpirationVersions *int NoncurrentVersionTransitionDays *int NewerNoncurrentTransitionVersions *int NoncurrentVersionTransitionStorageClass *string ExpiredObjectAllversions *bool } // Filter returns lifecycle.Filter appropriate for opts func (opts LifecycleOptions) Filter() lifecycle.Filter { var f lifecycle.Filter var tags []lifecycle.Tag var predCount int if opts.Tags != nil { tags = extractILMTags(*opts.Tags) predCount += len(tags) } var prefix string if opts.Prefix != nil { prefix = *opts.Prefix predCount++ } var szLt, szGt int64 if opts.ObjectSizeLessThan != nil { szLt = *opts.ObjectSizeLessThan predCount++ } if opts.ObjectSizeGreaterThan != nil { szGt = *opts.ObjectSizeGreaterThan predCount++ } if predCount >= 2 { f.And = lifecycle.And{ Tags: tags, Prefix: prefix, ObjectSizeLessThan: szLt, ObjectSizeGreaterThan: szGt, } } else { // In a valid lifecycle rule filter at most one of the // following will only be set. f.Prefix = prefix f.ObjectSizeGreaterThan = szGt f.ObjectSizeLessThan = szLt if len(tags) >= 1 { f.Tag = tags[0] } } return f } // ToILMRule creates lifecycle.Configuration based on LifecycleOptions func (opts LifecycleOptions) ToILMRule() (lifecycle.Rule, *probe.Error) { var ( id, status string nonCurrentVersionExpirationDays lifecycle.ExpirationDays newerNonCurrentExpirationVersions int nonCurrentVersionTransitionDays lifecycle.ExpirationDays newerNonCurrentTransitionVersions int nonCurrentVersionTransitionStorageClass string ) id = opts.ID status = func() string { if opts.Status != nil && !*opts.Status { return "Disabled" } // Generating a new ILM rule without explicit status is enabled return "Enabled" }() expiry, err := parseExpiry(opts.ExpiryDate, opts.ExpiryDays, opts.ExpiredObjectDeleteMarker, opts.ExpiredObjectAllversions) if err != nil { return lifecycle.Rule{}, err } transition, err := parseTransition(opts.StorageClass, opts.TransitionDate, opts.TransitionDays) if err != nil { return lifecycle.Rule{}, err } if opts.NoncurrentVersionExpirationDays != nil { nonCurrentVersionExpirationDays = lifecycle.ExpirationDays(*opts.NoncurrentVersionExpirationDays) } if opts.NewerNoncurrentExpirationVersions != nil { newerNonCurrentExpirationVersions = *opts.NewerNoncurrentExpirationVersions } if opts.NoncurrentVersionTransitionDays != nil { nonCurrentVersionTransitionDays = lifecycle.ExpirationDays(*opts.NoncurrentVersionTransitionDays) } if opts.NewerNoncurrentTransitionVersions != nil { newerNonCurrentTransitionVersions = *opts.NewerNoncurrentTransitionVersions } if opts.NoncurrentVersionTransitionStorageClass != nil { nonCurrentVersionTransitionStorageClass = *opts.NoncurrentVersionTransitionStorageClass } newRule := lifecycle.Rule{ ID: id, RuleFilter: opts.Filter(), Status: status, Expiration: expiry, Transition: transition, NoncurrentVersionExpiration: lifecycle.NoncurrentVersionExpiration{ NoncurrentDays: nonCurrentVersionExpirationDays, NewerNoncurrentVersions: newerNonCurrentExpirationVersions, }, NoncurrentVersionTransition: lifecycle.NoncurrentVersionTransition{ NoncurrentDays: nonCurrentVersionTransitionDays, NewerNoncurrentVersions: newerNonCurrentTransitionVersions, StorageClass: nonCurrentVersionTransitionStorageClass, }, } if err := validateILMRule(newRule); err != nil { return lifecycle.Rule{}, err } return newRule, nil } func strPtr(s string) *string { ptr := s return &ptr } func intPtr(i int) *int { ptr := i return &ptr } func int64Ptr(i int64) *int64 { return &i } func boolPtr(b bool) *bool { ptr := b return &ptr } // GetLifecycleOptions create LifeCycleOptions based on cli inputs func GetLifecycleOptions(ctx *cli.Context) (LifecycleOptions, *probe.Error) { var ( id string status *bool prefix *string tags *string sizeLt *int64 sizeGt *int64 expiryDate *string expiryDays *string transitionDate *string transitionDays *string tier *string expiredObjectDeleteMarker *bool noncurrentVersionExpirationDays *int newerNoncurrentExpirationVersions *int noncurrentVersionTransitionDays *int newerNoncurrentTransitionVersions *int noncurrentTier *string expiredObjectAllversions *bool ) id = ctx.String("id") if id == "" { id = xid.New().String() } switch { case ctx.IsSet("disable"): status = boolPtr(!ctx.Bool("disable")) case ctx.IsSet("enable"): status = boolPtr(ctx.Bool("enable")) } if ctx.IsSet("prefix") { prefix = strPtr(ctx.String("prefix")) } else { // Calculating the prefix for the aliased URL is deprecated in Aug 2022 // split the first arg i.e. path into alias, bucket and prefix result := strings.SplitN(ctx.Args().First(), "/", 3) // get the prefix from path if len(result) > 2 { p := result[len(result)-1] if len(p) > 0 { prefix = &p } } } expiryRulesCount := 0 if ctx.IsSet("size-lt") { szStr := ctx.String("size-lt") szLt, err := humanize.ParseBytes(szStr) if err != nil || szLt > math.MaxInt64 { return LifecycleOptions{}, probe.NewError(fmt.Errorf("size-lt value %s is invalid", szStr)) } sizeLt = int64Ptr(int64(szLt)) } if ctx.IsSet("size-gt") { szStr := ctx.String("size-gt") szGt, err := humanize.ParseBytes(szStr) if err != nil || szGt > math.MaxInt64 { return LifecycleOptions{}, probe.NewError(fmt.Errorf("size-gt value %s is invalid", szStr)) } sizeGt = int64Ptr(int64(szGt)) } // For backward-compatibility if ctx.IsSet("storage-class") { tier = strPtr(strings.ToUpper(ctx.String("storage-class"))) } if ctx.IsSet("noncurrentversion-transition-storage-class") { noncurrentTier = strPtr(strings.ToUpper(ctx.String("noncurrentversion-transition-storage-class"))) } if ctx.IsSet("tier") { tier = strPtr(strings.ToUpper(ctx.String("tier"))) } if f := "transition-tier"; ctx.IsSet(f) { tier = strPtr(strings.ToUpper(ctx.String(f))) } if ctx.IsSet("noncurrentversion-tier") { noncurrentTier = strPtr(strings.ToUpper(ctx.String("noncurrentversion-tier"))) } if f := "noncurrent-transition-tier"; ctx.IsSet(f) { noncurrentTier = strPtr(strings.ToUpper(ctx.String(f))) } if tier != nil && !ctx.IsSet("transition-days") && !ctx.IsSet("transition-date") { return LifecycleOptions{}, probe.NewError(errors.New("transition-date or transition-days must be set")) } if noncurrentTier != nil && !ctx.IsSet("noncurrentversion-transition-days") && !ctx.IsSet("noncurrent-transition-days") { return LifecycleOptions{}, probe.NewError(errors.New("noncurrentversion-transition-days must be set")) } // for MinIO transition storage-class is same as label defined on // `mc admin bucket remote add --service ilm --label` command if ctx.IsSet("tags") { tags = strPtr(ctx.String("tags")) } if ctx.IsSet("expiry-date") { expiryRulesCount++ expiryDate = strPtr(ctx.String("expiry-date")) } if ctx.IsSet("expiry-days") { expiryRulesCount++ expiryDays = strPtr(ctx.String("expiry-days")) } if f := "expire-days"; ctx.IsSet(f) { expiryDays = strPtr(ctx.String(f)) } if ctx.IsSet("transition-date") { transitionDate = strPtr(ctx.String("transition-date")) } if ctx.IsSet("transition-days") { transitionDays = strPtr(ctx.String("transition-days")) } if ctx.IsSet("expired-object-delete-marker") { expiredObjectDeleteMarker = boolPtr(ctx.Bool("expired-object-delete-marker")) } if f := "expire-delete-marker"; ctx.IsSet(f) { expiryRulesCount++ expiredObjectDeleteMarker = boolPtr(ctx.Bool(f)) } if ctx.IsSet("noncurrentversion-expiration-days") { noncurrentVersionExpirationDays = intPtr(ctx.Int("noncurrentversion-expiration-days")) } if f := "noncurrent-expire-days"; ctx.IsSet(f) { ndaysStr := ctx.String(f) ndays, err := strconv.Atoi(ndaysStr) if err != nil { return LifecycleOptions{}, probe.NewError(fmt.Errorf("failed to parse %s: %v", f, err)) } noncurrentVersionExpirationDays = &ndays } if ctx.IsSet("newer-noncurrentversions-expiration") { newerNoncurrentExpirationVersions = intPtr(ctx.Int("newer-noncurrentversions-expiration")) } if f := "noncurrent-expire-newer"; ctx.IsSet(f) { newerNoncurrentExpirationVersions = intPtr(ctx.Int(f)) } if ctx.IsSet("noncurrentversion-transition-days") { noncurrentVersionTransitionDays = intPtr(ctx.Int("noncurrentversion-transition-days")) } if f := "noncurrent-transition-days"; ctx.IsSet(f) { noncurrentVersionTransitionDays = intPtr(ctx.Int(f)) } if ctx.IsSet("newer-noncurrentversions-transition") { newerNoncurrentTransitionVersions = intPtr(ctx.Int("newer-noncurrentversions-transition")) } if f := "noncurrent-transition-newer"; ctx.IsSet(f) { newerNoncurrentTransitionVersions = intPtr(ctx.Int(f)) } if ctx.IsSet("expire-all-object-versions") { expiredObjectAllversions = boolPtr(ctx.Bool("expire-all-object-versions")) } if expiryRulesCount > 1 { return LifecycleOptions{}, probe.NewError(errors.New("only one of expiry-date, expiry-days and expire-delete-marker can be used in a single rule. Try adding multiple rules to achieve the desired effect")) } return LifecycleOptions{ ID: id, Status: status, Prefix: prefix, Tags: tags, ObjectSizeLessThan: sizeLt, ObjectSizeGreaterThan: sizeGt, ExpiryDate: expiryDate, ExpiryDays: expiryDays, TransitionDate: transitionDate, TransitionDays: transitionDays, StorageClass: tier, ExpiredObjectDeleteMarker: expiredObjectDeleteMarker, NoncurrentVersionExpirationDays: noncurrentVersionExpirationDays, NewerNoncurrentExpirationVersions: newerNoncurrentExpirationVersions, NoncurrentVersionTransitionDays: noncurrentVersionTransitionDays, NewerNoncurrentTransitionVersions: newerNoncurrentTransitionVersions, NoncurrentVersionTransitionStorageClass: noncurrentTier, ExpiredObjectAllversions: expiredObjectAllversions, }, nil } // ApplyRuleFields applies non nil fields of LifcycleOptions to the existing lifecycle rule func ApplyRuleFields(dest *lifecycle.Rule, opts LifecycleOptions) *probe.Error { // If src has tags, it should override the destination if opts.Tags != nil { dest.RuleFilter.And.Tags = extractILMTags(*opts.Tags) } // since prefix is a part of command args, it is always present in the src rule and // it should be always set to the destination. if opts.Prefix != nil { if dest.RuleFilter.And.Tags != nil { dest.RuleFilter.And.Prefix = *opts.Prefix } else { dest.RuleFilter.Prefix = *opts.Prefix } } // only one of expiration day, date or transition day, date is expected if opts.ExpiryDate != nil { date, err := parseExpiryDate(*opts.ExpiryDate) if err != nil { return err } dest.Expiration.Date = date // reset everything else dest.Expiration.Days = 0 dest.Expiration.DeleteMarker = false } else if opts.ExpiryDays != nil { days, err := parseExpiryDays(*opts.ExpiryDays) if err != nil { return err } dest.Expiration.Days = days // reset everything else dest.Expiration.Date = lifecycle.ExpirationDate{} } else if opts.ExpiredObjectDeleteMarker != nil { dest.Expiration.DeleteMarker = lifecycle.ExpireDeleteMarker(*opts.ExpiredObjectDeleteMarker) dest.Expiration.Days = 0 dest.Expiration.Date = lifecycle.ExpirationDate{} } if opts.ExpiredObjectAllversions != nil { dest.Expiration.DeleteAll = lifecycle.ExpirationBoolean(*opts.ExpiredObjectAllversions) } if opts.TransitionDate != nil { date, err := parseTransitionDate(*opts.TransitionDate) if err != nil { return err } dest.Transition.Date = date // reset everything else dest.Transition.Days = 0 } else if opts.TransitionDays != nil { days, err := parseTransitionDays(*opts.TransitionDays) if err != nil { return err } dest.Transition.Days = days // reset everything else dest.Transition.Date = lifecycle.ExpirationDate{} } if opts.NoncurrentVersionExpirationDays != nil { dest.NoncurrentVersionExpiration.NoncurrentDays = lifecycle.ExpirationDays(*opts.NoncurrentVersionExpirationDays) } if opts.NewerNoncurrentExpirationVersions != nil { dest.NoncurrentVersionExpiration.NewerNoncurrentVersions = *opts.NewerNoncurrentExpirationVersions } if opts.NoncurrentVersionTransitionDays != nil { dest.NoncurrentVersionTransition.NoncurrentDays = lifecycle.ExpirationDays(*opts.NoncurrentVersionTransitionDays) } if opts.NewerNoncurrentTransitionVersions != nil { dest.NoncurrentVersionTransition.NewerNoncurrentVersions = *opts.NewerNoncurrentTransitionVersions } if opts.NoncurrentVersionTransitionStorageClass != nil { dest.NoncurrentVersionTransition.StorageClass = *opts.NoncurrentVersionTransitionStorageClass } if opts.StorageClass != nil { dest.Transition.StorageClass = *opts.StorageClass } // Updated the status if opts.Status != nil { dest.Status = func() string { if *opts.Status { return "Enabled" } return "Disabled" }() } return nil } minio-client-0.0~20250403/cmd/ilm/options_test.go000066400000000000000000000066331477450377600214170ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package ilm import ( "fmt" "testing" "github.com/dustin/go-humanize" "github.com/minio/minio-go/v7/pkg/lifecycle" ) func TestOptionFilter(t *testing.T) { emptyFilter := lifecycle.Filter{} emptyOpts := LifecycleOptions{} filterWithPrefix := lifecycle.Filter{ Prefix: "doc/", } optsWithPrefix := LifecycleOptions{ Prefix: strPtr("doc/"), } filterWithTag := lifecycle.Filter{ Tag: lifecycle.Tag{ Key: "key1", Value: "value1", }, } optsWithTag := LifecycleOptions{ Tags: strPtr("key1=value1"), } filterWithSzLt := lifecycle.Filter{ ObjectSizeLessThan: 100 * humanize.MiByte, } optsWithSzLt := LifecycleOptions{ ObjectSizeLessThan: int64Ptr(100 * humanize.MiByte), } filterWithSzGt := lifecycle.Filter{ ObjectSizeGreaterThan: 1 * humanize.MiByte, } optsWithSzGt := LifecycleOptions{ ObjectSizeGreaterThan: int64Ptr(1 * humanize.MiByte), } filterWithAnd := lifecycle.Filter{ And: lifecycle.And{ Prefix: "doc/", Tags: []lifecycle.Tag{ { Key: "key1", Value: "value1", }, }, ObjectSizeLessThan: 100 * humanize.MiByte, ObjectSizeGreaterThan: 1 * humanize.MiByte, }, } optsWithAnd := LifecycleOptions{ Prefix: strPtr("doc/"), Tags: strPtr("key1=value1"), ObjectSizeLessThan: int64Ptr(100 * humanize.MiByte), ObjectSizeGreaterThan: int64Ptr(1 * humanize.MiByte), } tests := []struct { opts LifecycleOptions want lifecycle.Filter }{ { opts: emptyOpts, want: emptyFilter, }, { opts: optsWithPrefix, want: filterWithPrefix, }, { opts: optsWithTag, want: filterWithTag, }, { opts: optsWithSzGt, want: filterWithSzGt, }, { opts: optsWithSzLt, want: filterWithSzLt, }, { opts: optsWithAnd, want: filterWithAnd, }, } filterEq := func(a, b lifecycle.Filter) bool { if a.ObjectSizeGreaterThan != b.ObjectSizeGreaterThan { return false } if a.ObjectSizeLessThan != b.ObjectSizeLessThan { return false } if a.Prefix != b.Prefix { return false } if a.Tag != b.Tag { return false } if a.And.ObjectSizeGreaterThan != b.And.ObjectSizeGreaterThan { return false } if a.And.ObjectSizeLessThan != b.And.ObjectSizeLessThan { return false } if a.And.Prefix != b.And.Prefix { return false } if len(a.And.Tags) != len(b.And.Tags) { return false } for i := range a.And.Tags { if a.And.Tags[i] != b.And.Tags[i] { return false } } return true } for i, test := range tests { t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) { if got := test.opts.Filter(); !filterEq(got, test.want) { t.Fatalf("Expected %#v but got %#v", test.want, got) } }) } } minio-client-0.0~20250403/cmd/ilm/parse.go000066400000000000000000000221031477450377600177650ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package ilm import ( "errors" "strconv" "strings" "time" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/lifecycle" ) // Used in tags. Ex: --tags "key1=value1&key2=value2&key3=value3" const ( tagSeperator string = "&" keyValSeperator string = "=" ) // Extracts the tags provided by user. The tagfilter array will be put in lifecycleRule structure. func extractILMTags(tagLabelVal string) []lifecycle.Tag { var ilmTagKVList []lifecycle.Tag for _, tag := range strings.Split(tagLabelVal, tagSeperator) { if tag == "" { // split returns empty for empty tagLabelVal, skip it. continue } lfcTag := lifecycle.Tag{} kvs := strings.SplitN(tag, keyValSeperator, 2) if len(kvs) == 2 { lfcTag.Key = kvs[0] lfcTag.Value = kvs[1] } else { lfcTag.Key = kvs[0] } ilmTagKVList = append(ilmTagKVList, lfcTag) } return ilmTagKVList } // Some of these rules are enforced by Amazon S3 standards. // For example: Transition has to happen before Expiry. // Storage class must be specified if transition date/days is provided. func validateTranExpDate(rule lifecycle.Rule) error { expiryDateSet := !rule.Expiration.IsDateNull() transitionSet := !rule.Transition.IsNull() transitionDateSet := transitionSet && !rule.Transition.IsDateNull() if transitionDateSet && expiryDateSet { if rule.Expiration.Date.Before(rule.Transition.Date.Time) { return errors.New("transition should apply before expiration") } } if transitionDateSet && rule.Transition.StorageClass == "" { return errors.New("missing transition storage-class") } return nil } func validateTranDays(rule lifecycle.Rule) error { if rule.Transition.Days < 0 { return errors.New("number of days to transition can't be negative") } if rule.Transition.Days < 30 && strings.ToLower(rule.Transition.StorageClass) == "standard_ia" { return errors.New("number of days to transition should be >= 30 with STANDARD_IA storage-class") } return nil } // Amazon S3 requires a minimum of one action for a rule to be added. func validateRuleAction(rule lifecycle.Rule) error { expirySet := !rule.Expiration.IsNull() transitionSet := !rule.Transition.IsNull() noncurrentExpirySet := !rule.NoncurrentVersionExpiration.IsDaysNull() newerNoncurrentVersionsExpiry := rule.NoncurrentVersionExpiration.NewerNoncurrentVersions > 0 noncurrentTransitionSet := rule.NoncurrentVersionTransition.StorageClass != "" newerNoncurrentVersionsTransition := rule.NoncurrentVersionTransition.NewerNoncurrentVersions > 0 if !expirySet && !transitionSet && !noncurrentExpirySet && !noncurrentTransitionSet && !newerNoncurrentVersionsExpiry && !newerNoncurrentVersionsTransition { return errors.New("at least one of Expiry, Transition, NoncurrentExpiry, NoncurrentVersionTransition actions should be specified in a rule") } return nil } func validateExpiration(rule lifecycle.Rule) error { var i int if !rule.Expiration.IsDaysNull() { i++ } if !rule.Expiration.IsDateNull() { i++ } if rule.Expiration.IsDeleteMarkerExpirationEnabled() { i++ } if i > 1 { return errors.New("only one parameter under Expiration can be specified") } return nil } func validateTransition(rule lifecycle.Rule) error { if !rule.Transition.IsDaysNull() && !rule.Transition.IsDateNull() { return errors.New("only one parameter under Transition can be specified") } return nil } func validateNoncurrentExpiration(rule lifecycle.Rule) error { days := rule.NoncurrentVersionExpiration.NoncurrentDays if days < 0 { return errors.New("NoncurrentVersionExpiration.NoncurrentDays is not a positive integer") } return nil } func validateNoncurrentTransition(rule lifecycle.Rule) error { days := rule.NoncurrentVersionTransition.NoncurrentDays storageClass := rule.NoncurrentVersionTransition.StorageClass if days < 0 { return errors.New("NoncurrentVersionTransition.NoncurrentDays is not a positive integer") } if days > 0 && storageClass == "" { return errors.New("both NoncurrentVersionTransition NoncurrentDays and StorageClass need to be specified") } return nil } // Check if any date is before than cur date func validateTranExpCurdate(rule lifecycle.Rule) error { var e error expirySet := !rule.Expiration.IsNull() transitionSet := !rule.Transition.IsNull() transitionDateSet := transitionSet && !rule.Transition.IsDateNull() expiryDateSet := expirySet && !rule.Expiration.IsDateNull() currentTime := time.Now() curTimeStr := currentTime.Format(defaultILMDateFormat) currentTime, e = time.Parse(defaultILMDateFormat, curTimeStr) if e != nil { return e } if expirySet && expiryDateSet && rule.Expiration.Date.Before(currentTime) { e = errors.New("expiry date falls before or on today's date") } else if transitionSet && transitionDateSet && rule.Transition.Date.Before(currentTime) { e = errors.New("transition date falls before or on today's date") } return e } // Check S3 compatibility for the new rule and some other basic checks. func validateILMRule(rule lifecycle.Rule) *probe.Error { if e := validateRuleAction(rule); e != nil { return probe.NewError(e) } if e := validateExpiration(rule); e != nil { return probe.NewError(e) } if e := validateTransition(rule); e != nil { return probe.NewError(e) } if e := validateTranExpCurdate(rule); e != nil { return probe.NewError(e) } if e := validateTranExpDate(rule); e != nil { return probe.NewError(e) } if e := validateTranDays(rule); e != nil { return probe.NewError(e) } if e := validateNoncurrentExpiration(rule); e != nil { return probe.NewError(e) } if e := validateNoncurrentTransition(rule); e != nil { return probe.NewError(e) } return nil } func parseTransitionDate(transitionDateStr string) (lifecycle.ExpirationDate, *probe.Error) { transitionDate, e := time.Parse(defaultILMDateFormat, transitionDateStr) if e != nil { return lifecycle.ExpirationDate{}, probe.NewError(e) } return lifecycle.ExpirationDate{Time: transitionDate}, nil } func parseTransitionDays(transitionDaysStr string) (lifecycle.ExpirationDays, *probe.Error) { transitionDays, e := strconv.Atoi(transitionDaysStr) if e != nil { return lifecycle.ExpirationDays(0), probe.NewError(e) } return lifecycle.ExpirationDays(transitionDays), nil } // Returns valid lifecycleTransition to be included in lifecycleRule func parseTransition(storageClass, transitionDateStr, transitionDaysStr *string) (lifecycle.Transition, *probe.Error) { var transition lifecycle.Transition if transitionDateStr != nil { transitionDate, err := parseTransitionDate(*transitionDateStr) if err != nil { return lifecycle.Transition{}, err } transition.Date = transitionDate } if transitionDaysStr != nil { transitionDays, err := parseTransitionDays(*transitionDaysStr) if err != nil { return lifecycle.Transition{}, err } transition.Days = transitionDays } if storageClass != nil { transition.StorageClass = *storageClass } return transition, nil } func parseExpiryDate(expiryDateStr string) (lifecycle.ExpirationDate, *probe.Error) { date, e := time.Parse(defaultILMDateFormat, expiryDateStr) if e != nil { return lifecycle.ExpirationDate{}, probe.NewError(e) } if date.IsZero() { return lifecycle.ExpirationDate{}, probe.NewError(errors.New("expiration date cannot be set to zero")) } return lifecycle.ExpirationDate{Time: date}, nil } func parseExpiryDays(expiryDayStr string) (lifecycle.ExpirationDays, *probe.Error) { days, e := strconv.Atoi(expiryDayStr) if e != nil { return lifecycle.ExpirationDays(0), probe.NewError(e) } if days == 0 { return lifecycle.ExpirationDays(0), probe.NewError(errors.New("expiration days cannot be set to zero")) } return lifecycle.ExpirationDays(days), nil } // Returns lifecycleExpiration to be included in lifecycleRule func parseExpiry(expiryDate, expiryDays *string, expiredDeleteMarker, expiredObjectAllVersions *bool) (lfcExp lifecycle.Expiration, err *probe.Error) { if expiryDate != nil { date, err := parseExpiryDate(*expiryDate) if err != nil { return lifecycle.Expiration{}, err } lfcExp.Date = date } if expiryDays != nil { days, err := parseExpiryDays(*expiryDays) if err != nil { return lifecycle.Expiration{}, err } lfcExp.Days = days } if expiredDeleteMarker != nil { lfcExp.DeleteMarker = lifecycle.ExpireDeleteMarker(*expiredDeleteMarker) } if expiredObjectAllVersions != nil { lfcExp.DeleteAll = lifecycle.ExpirationBoolean(*expiredObjectAllVersions) } return lfcExp, nil } minio-client-0.0~20250403/cmd/ilm/table.go000066400000000000000000000120331477450377600177430ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package ilm import ( "github.com/jedib0t/go-pretty/v6/table" "github.com/minio/minio-go/v7/pkg/lifecycle" ) // Table interface provides methods when implemented allows a []T to be rendered // as a table. type Table interface { Len() int Title() string Rows() []table.Row ColumnHeaders() table.Row } // LsFilter enumerates the 3 possible ilm-ls filter options. type LsFilter uint8 const ( // None - no filter None LsFilter = iota // ExpiryOnly - filter expiration actions across rules ExpiryOnly // TransitionOnly - filter transition actions across rules TransitionOnly ) // Apply applies f on rules and filters lifecycle rules matching it func (f LsFilter) Apply(rules []lifecycle.Rule) []lifecycle.Rule { check := func(rule lifecycle.Rule) bool { switch f { case ExpiryOnly: return !rule.Expiration.IsNull() || !rule.NoncurrentVersionExpiration.IsDaysNull() || rule.NoncurrentVersionExpiration.NewerNoncurrentVersions > 0 case TransitionOnly: return !rule.Transition.IsNull() || !rule.NoncurrentVersionTransition.IsStorageClassEmpty() } return true } var n int for _, rule := range rules { if check(rule) { rules[n] = rule n++ } } rules = rules[:n] return rules } type expirationCurrentRow struct { ID string Status string Prefix string Tags string Days int ExpireDelMarker bool } type expirationCurrentTable []expirationCurrentRow func (e expirationCurrentTable) Len() int { return len(e) } func (e expirationCurrentTable) Title() string { return "Expiration for latest version (Expiration)" } func (e expirationCurrentTable) Rows() (rows []table.Row) { for _, row := range e { if row.Prefix == "" { row.Prefix = "-" } if row.Tags == "" { row.Tags = "-" } rows = append(rows, table.Row{row.ID, row.Status, row.Prefix, row.Tags, row.Days, row.ExpireDelMarker}) } return rows } func (e expirationCurrentTable) ColumnHeaders() (headers table.Row) { return table.Row{"ID", "Status", "Prefix", "Tags", "Days to Expire", "Expire DeleteMarker"} } type expirationNoncurrentTable []expirationNoncurrentRow type expirationNoncurrentRow struct { ID string Status string Prefix string Tags string Days int KeepVersions int } func (e expirationNoncurrentTable) Len() int { return len(e) } func (e expirationNoncurrentTable) Title() string { return "Expiration for older versions (NoncurrentVersionExpiration)" } func (e expirationNoncurrentTable) Rows() (rows []table.Row) { for _, row := range e { if row.Prefix == "" { row.Prefix = "-" } if row.Tags == "" { row.Tags = "-" } rows = append(rows, table.Row{row.ID, row.Status, row.Prefix, row.Tags, row.Days, row.KeepVersions}) } return rows } func (e expirationNoncurrentTable) ColumnHeaders() (headers table.Row) { return table.Row{"ID", "Status", "Prefix", "Tags", "Days to Expire", "Keep Versions"} } type tierCurrentTable []tierCurrentRow type tierCurrentRow struct { ID string Status string Prefix string Tags string Days int Tier string } func (t tierCurrentTable) Len() int { return len(t) } func (t tierCurrentTable) Title() string { return "Transition for latest version (Transition)" } func (t tierCurrentTable) ColumnHeaders() (headers table.Row) { return table.Row{"ID", "Status", "Prefix", "Tags", "Days to Tier", "Tier"} } func (t tierCurrentTable) Rows() (rows []table.Row) { for _, row := range t { if row.Prefix == "" { row.Prefix = "-" } if row.Tags == "" { row.Tags = "-" } rows = append(rows, table.Row{row.ID, row.Status, row.Prefix, row.Tags, row.Days, row.Tier}) } return rows } type ( tierNoncurrentTable []tierNoncurrentRow tierNoncurrentRow tierCurrentRow ) func (t tierNoncurrentTable) Len() int { return len(t) } func (t tierNoncurrentTable) Title() string { return "Transition for older versions (NoncurrentVersionTransition)" } func (t tierNoncurrentTable) ColumnHeaders() table.Row { return table.Row{"ID", "Status", "Prefix", "Tags", "Days to Tier", "Tier"} } func (t tierNoncurrentTable) Rows() (rows []table.Row) { for _, row := range t { if row.Prefix == "" { row.Prefix = "-" } if row.Tags == "" { row.Tags = "-" } rows = append(rows, table.Row{row.ID, row.Status, row.Prefix, row.Tags, row.Days, row.Tier}) } return rows } minio-client-0.0~20250403/cmd/ilm/utils.go000066400000000000000000000103271477450377600200200ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package ilm import ( "fmt" "strings" "time" "github.com/minio/minio-go/v7/pkg/lifecycle" ) // getPrefix returns the prefix configured func getPrefix(rule lifecycle.Rule) string { // deprecated, but older ILM policies may have them if rule.Prefix != "" { return rule.Prefix } if rule.RuleFilter.Prefix != "" { return rule.RuleFilter.Prefix } if rule.RuleFilter.And.Prefix != "" { return rule.RuleFilter.And.Prefix } return "" } // getTags returns the tags configured as "k1=v1&k2=v2" func getTags(rule lifecycle.Rule) string { if !rule.RuleFilter.Tag.IsEmpty() { return fmt.Sprintf("%s=%s", rule.RuleFilter.Tag.Key, rule.RuleFilter.Tag.Value) } if len(rule.RuleFilter.And.Tags) > 0 { var tags strings.Builder for i, tag := range rule.RuleFilter.And.Tags { fmt.Fprintf(&tags, "%s=%s", tag.Key, tag.Value) if i < len(rule.RuleFilter.And.Tags)-1 { fmt.Fprintf(&tags, "&") } } return tags.String() } return "" } // getExpirationDays returns the number of days to expire relative to // time.Now().UTC() for the given rule. func getExpirationDays(rule lifecycle.Rule) int { if rule.Expiration.Days > 0 { return int(rule.Expiration.Days) } if !rule.Expiration.Date.IsZero() { return int(time.Until(rule.Expiration.Date.Time).Hours() / 24) } return 0 } // getTransitionDays returns the number of days to transition/tier relative to // time.Now().UTC() for the given rule. func getTransitionDays(rule lifecycle.Rule) int { if !rule.Transition.Date.IsZero() { return int(time.Until(rule.Transition.Date.Time).Hours() / 24) } return int(rule.Transition.Days) } // ToTables converts a lifecycle.Configuration into its tabular representation. func ToTables(cfg *lifecycle.Configuration) []Table { var tierCur tierCurrentTable var tierNoncur tierNoncurrentTable var expCur expirationCurrentTable var expNoncur expirationNoncurrentTable for _, rule := range cfg.Rules { if !rule.Expiration.IsNull() { expCur = append(expCur, expirationCurrentRow{ ID: rule.ID, Status: rule.Status, Prefix: getPrefix(rule), Tags: getTags(rule), Days: getExpirationDays(rule), ExpireDelMarker: bool(rule.Expiration.DeleteMarker), }) } if !rule.NoncurrentVersionExpiration.IsDaysNull() || rule.NoncurrentVersionExpiration.NewerNoncurrentVersions > 0 { expNoncur = append(expNoncur, expirationNoncurrentRow{ ID: rule.ID, Status: rule.Status, Prefix: getPrefix(rule), Tags: getTags(rule), Days: int(rule.NoncurrentVersionExpiration.NoncurrentDays), KeepVersions: rule.NoncurrentVersionExpiration.NewerNoncurrentVersions, }) } if !rule.Transition.IsNull() { tierCur = append(tierCur, tierCurrentRow{ ID: rule.ID, Status: rule.Status, Prefix: getPrefix(rule), Tags: getTags(rule), Days: getTransitionDays(rule), Tier: rule.Transition.StorageClass, }) } if !rule.NoncurrentVersionTransition.IsStorageClassEmpty() { tierNoncur = append(tierNoncur, tierNoncurrentRow{ ID: rule.ID, Status: rule.Status, Prefix: getPrefix(rule), Tags: getTags(rule), Days: int(rule.NoncurrentVersionTransition.NoncurrentDays), Tier: rule.NoncurrentVersionTransition.StorageClass, }) } } var table []Table inclTbl := func(tbl Table) { if len(tbl.Rows()) > 0 { table = append(table, tbl) } } inclTbl(expCur) inclTbl(expNoncur) inclTbl(tierCur) inclTbl(tierNoncur) return table } minio-client-0.0~20250403/cmd/ilm/utils_test.go000066400000000000000000000032721477450377600210600ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package ilm import ( "testing" "github.com/minio/minio-go/v7/pkg/lifecycle" ) func TestILMTags(t *testing.T) { tests := []struct { rule lifecycle.Rule expected string }{ { rule: lifecycle.Rule{ ID: "one-tag", RuleFilter: lifecycle.Filter{ Tag: lifecycle.Tag{ Key: "key1", Value: "val1", }, }, }, expected: "key1=val1", }, { rule: lifecycle.Rule{ ID: "many-tags", RuleFilter: lifecycle.Filter{ And: lifecycle.And{ Tags: []lifecycle.Tag{ { Key: "key1", Value: "val1", }, { Key: "key2", Value: "val2", }, { Key: "key3", Value: "val3", }, }, }, }, }, expected: "key1=val1&key2=val2&key3=val3", }, } for i, test := range tests { if got := getTags(test.rule); got != test.expected { t.Fatalf("%d: Expected %s but got %s", i+1, test.expected, got) } } } minio-client-0.0~20250403/cmd/legalhold-clear.go000066400000000000000000000063011477450377600211130ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "time" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) var lhClearFlags = []cli.Flag{ cli.BoolFlag{ Name: "recursive, r", Usage: "clear legal hold recursively", }, cli.StringFlag{ Name: "version-id, vid", Usage: "clear legal hold of a specific object version", }, cli.StringFlag{ Name: "rewind", Usage: "clear legal hold on an object version at specified time", }, cli.BoolFlag{ Name: "versions", Usage: "clear legal hold on multiple versions of object(s)", }, } var legalHoldClearCmd = cli.Command{ Name: "clear", Usage: "clear legal hold for object(s)", Action: mainLegalHoldClear, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(lhClearFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Disable legal hold on a specific object $ {{.HelpName}} myminio/mybucket/prefix/obj.csv 2. Disable legal hold on a specific object version $ {{.HelpName}} myminio/mybucket/prefix/obj.csv --version-id "HiMFUTOowG6ylfNi4LKxD3ieHbgfgrvC" 3. Disable object legal hold recursively for all objects at a prefix $ {{.HelpName}} myminio/mybucket/prefix --recursive 4. Disable object legal hold recursively for all objects versions older than one year $ {{.HelpName}} myminio/mybucket/prefix --recursive --rewind 365d --versions `, } // main for legalhold clear command. func mainLegalHoldClear(cliCtx *cli.Context) error { console.SetColor("LegalHoldSuccess", color.New(color.FgGreen, color.Bold)) console.SetColor("LegalHoldPartialFailure", color.New(color.FgRed, color.Bold)) console.SetColor("LegalHoldMessageFailure", color.New(color.FgYellow)) targetURL, versionID, timeRef, recursive, withVersions := parseLegalHoldArgs(cliCtx) if timeRef.IsZero() && withVersions { timeRef = time.Now().UTC() } ctx, cancelCopy := context.WithCancel(globalContext) defer cancelCopy() enabled, err := isBucketLockEnabled(ctx, targetURL) if err != nil { fatalIf(err, "Unable to clear legalhold of `%s`", targetURL) } if !enabled { fatalIf(errDummy().Trace(), "Bucket locking needs to be enabled in order to use this feature.") } return setLegalHold(ctx, targetURL, versionID, timeRef, withVersions, recursive, minio.LegalHoldDisabled) } minio-client-0.0~20250403/cmd/legalhold-info.go000066400000000000000000000173521477450377600207700ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "path/filepath" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) var lhInfoFlags = []cli.Flag{ cli.BoolFlag{ Name: "recursive, r", Usage: "show legal hold status recursively", }, cli.StringFlag{ Name: "version-id, vid", Usage: "show legal hold status of a specific object version", }, cli.StringFlag{ Name: "rewind", Usage: "show legal hold status of an object version at specified time", }, cli.BoolFlag{ Name: "versions", Usage: "show legal hold status of multiple versions of object(s)", }, } var legalHoldInfoCmd = cli.Command{ Name: "info", Usage: "show legal hold info for object(s)", Action: mainLegalHoldInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(lhInfoFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Show legal hold on a specific object $ {{.HelpName}} myminio/mybucket/prefix/obj.csv 2. Show legal hold on a specific object version $ {{.HelpName}} myminio/mybucket/prefix/obj.csv --version-id "HiMFUTOowG6ylfNi4LKxD3ieHbgfgrvC" 3. Show object legal hold recursively for all objects at a prefix $ {{.HelpName}} myminio/mybucket/prefix --recursive 4. Show object legal hold recursively for all objects versions older than one year $ {{.HelpName}} myminio/mybucket/prefix --recursive --rewind 365d --versions `, } // Structured message depending on the type of console. type legalHoldInfoMessage struct { LegalHold minio.LegalHoldStatus `json:"legalhold"` URLPath string `json:"urlpath"` Key string `json:"key"` VersionID string `json:"versionID"` Status string `json:"status"` Err error `json:"error,omitempty"` } // Colorized message for console printing. func (l legalHoldInfoMessage) String() string { if l.Err != nil { return console.Colorize("LegalHoldMessageFailure", "Unable to get object legal hold status `"+l.Key+"`. "+l.Err.Error()) } var msg string var legalhold string switch l.LegalHold { case "": legalhold = console.Colorize("LegalHoldNotSet", "Not set") case minio.LegalHoldEnabled: legalhold = console.Colorize("LegalHoldOn", l.LegalHold) case minio.LegalHoldDisabled: legalhold = console.Colorize("LegalHoldOff", l.LegalHold) } msg += "[ " + centerText(legalhold, 8) + " ] " if l.VersionID != "" { msg += " " + console.Colorize("LegalHoldVersion", l.VersionID) + " " } msg += " " msg += l.Key return msg } // JSON'ified message for scripting. func (l legalHoldInfoMessage) JSON() string { msgBytes, e := json.MarshalIndent(l, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } // showLegalHoldInfo - show legalhold for one or many objects within a given prefix, with or without versioning func showLegalHoldInfo(ctx context.Context, urlStr, versionID string, timeRef time.Time, withVersions, recursive bool) error { clnt, err := newClient(urlStr) if err != nil { fatalIf(err.Trace(), "Unable to parse the provided url.") } prefixPath := clnt.GetURL().Path prefixPath = filepath.ToSlash(prefixPath) if !strings.HasSuffix(prefixPath, "/") { prefixPath = prefixPath[:strings.LastIndex(prefixPath, "/")+1] } prefixPath = strings.TrimPrefix(prefixPath, "./") if !recursive && !withVersions { lhold, err := clnt.GetObjectLegalHold(ctx, versionID) if err != nil { fatalIf(err.Trace(urlStr), "Failed to show legal hold information of `"+urlStr+"`.") } else { contentURL := filepath.ToSlash(clnt.GetURL().Path) key := strings.TrimPrefix(contentURL, prefixPath) printMsg(legalHoldInfoMessage{ LegalHold: lhold, Status: "success", URLPath: clnt.GetURL().String(), Key: key, VersionID: versionID, }) } return nil } alias, _, _ := mustExpandAlias(urlStr) var cErr error errorsFound := false objectsFound := false lstOptions := ListOptions{Recursive: recursive, ShowDir: DirNone} if !timeRef.IsZero() { lstOptions.WithOlderVersions = withVersions lstOptions.TimeRef = timeRef } for content := range clnt.List(ctx, lstOptions) { if content.Err != nil { errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") cErr = exitStatus(globalErrorExitStatus) // Set the exit status. continue } if !recursive && getStandardizedURL(alias+getKey(content)) != getStandardizedURL(urlStr) { break } objectsFound = true newClnt, perr := newClientFromAlias(alias, content.URL.String()) if perr != nil { errorIf(content.Err.Trace(clnt.GetURL().String()), "Invalid URL") continue } lhold, probeErr := newClnt.GetObjectLegalHold(ctx, content.VersionID) if probeErr != nil { errorsFound = true errorIf(probeErr.Trace(content.URL.Path), "Failed to get legal hold information on `%s`", content.URL.Path) } else { if !globalJSON { contentURL := filepath.ToSlash(content.URL.Path) key := strings.TrimPrefix(contentURL, prefixPath) printMsg(legalHoldInfoMessage{ LegalHold: lhold, Status: "success", URLPath: content.URL.String(), Key: key, VersionID: content.VersionID, }) } } } if cErr == nil && !globalJSON { switch { case errorsFound: console.Print(console.Colorize("LegalHoldPartialFailure", fmt.Sprintf("Errors found while getting legal hold status on objects with prefix `%s`. \n", urlStr))) case !objectsFound: console.Print(console.Colorize("LegalHoldMessageFailure", fmt.Sprintf("No objects/versions found while getting legal hold status with prefix `%s`. \n", urlStr))) } } return cErr } // main for legalhold info command. func mainLegalHoldInfo(cliCtx *cli.Context) error { console.SetColor("LegalHoldSuccess", color.New(color.FgGreen, color.Bold)) console.SetColor("LegalHoldNotSet", color.New(color.FgYellow)) console.SetColor("LegalHoldOn", color.New(color.FgGreen, color.Bold)) console.SetColor("LegalHoldOff", color.New(color.FgRed, color.Bold)) console.SetColor("LegalHoldVersion", color.New(color.FgGreen)) console.SetColor("LegalHoldPartialFailure", color.New(color.FgRed, color.Bold)) console.SetColor("LegalHoldMessageFailure", color.New(color.FgYellow)) targetURL, versionID, timeRef, recursive, withVersions := parseLegalHoldArgs(cliCtx) if timeRef.IsZero() && withVersions { timeRef = time.Now().UTC() } ctx, cancelLegalHold := context.WithCancel(globalContext) defer cancelLegalHold() enabled, err := isBucketLockEnabled(ctx, targetURL) if err != nil { fatalIf(err, "Unable to get legalhold info of `%s`", targetURL) } if !enabled { fatalIf(errDummy().Trace(), "Bucket lock needs to be enabled in order to use this feature.") } return showLegalHoldInfo(ctx, targetURL, versionID, timeRef, withVersions, recursive) } minio-client-0.0~20250403/cmd/legalhold-main.go000066400000000000000000000103271477450377600207540ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "fmt" "net/http" "strings" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" minio "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) var legalHoldSubcommands = []cli.Command{ legalHoldSetCmd, legalHoldClearCmd, legalHoldInfoCmd, } var legalHoldCmd = cli.Command{ Name: "legalhold", Usage: "manage legal hold for object(s)", Action: mainLegalHold, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: legalHoldSubcommands, } // Structured message depending on the type of console. type legalHoldCmdMessage struct { LegalHold minio.LegalHoldStatus `json:"legalhold"` URLPath string `json:"urlpath"` Key string `json:"key"` VersionID string `json:"versionID"` Status string `json:"status"` Err error `json:"error,omitempty"` } // Colorized message for console printing. func (l legalHoldCmdMessage) String() string { if l.Err != nil { return console.Colorize("LegalHoldMessageFailure", "Unable to set object legal hold status `"+l.Key+"`. "+l.Err.Error()) } op := "set" if l.LegalHold == minio.LegalHoldDisabled { op = "cleared" } msg := fmt.Sprintf("Object legal hold successfully %s for `%s`", op, l.Key) if l.VersionID != "" { msg += fmt.Sprintf(" (version-id=%s)", l.VersionID) } msg += "." return console.Colorize("LegalHoldSuccess", msg) } // JSON'ified message for scripting. func (l legalHoldCmdMessage) JSON() string { msgBytes, e := json.MarshalIndent(l, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } var ( errObjectLockConfigNotFound = errors.New("object locking is not configured") errObjectLockNotSupported = errors.New("object locking is not supported") ) // Return true if this an S3 bucket with locking enabled // Return false if this an S3 bucket with no locking enabled // Return false if this is a filesystem URL // Otherwise return unexpected errors func isBucketLockEnabled(ctx context.Context, aliasedURL string) (bool, *probe.Error) { st, err := getBucketLockStatus(ctx, aliasedURL) if err != nil { switch err.ToGoError() { case errObjectLockConfigNotFound, errObjectLockNotSupported: return false, nil } return false, err } return st == "Enabled", nil } // Check if the bucket corresponding to the target url has object locking enabled func getBucketLockStatus(ctx context.Context, aliasedURL string) (status string, err *probe.Error) { clnt, err := newClient(aliasedURL) if err != nil { return "", err } // Remove the prefix/object from the aliased url and reconstruct the client switch c := clnt.(type) { case *S3Client: _, object := c.url2BucketAndObject() if object != "" { clnt, _ = newClient(strings.TrimSuffix(aliasedURL, object)) } default: return "", probe.NewError(errObjectLockNotSupported) } status, _, _, _, err = clnt.GetObjectLockConfig(ctx) if err != nil { errResp := minio.ToErrorResponse(err.ToGoError()) switch { case errResp.Code == "ObjectLockConfigurationNotFoundError": return "", probe.NewError(errObjectLockConfigNotFound) case errResp.StatusCode == http.StatusNotImplemented: return "", probe.NewError(errObjectLockNotSupported) } return "", err } return status, nil } // main for retention command. func mainLegalHold(ctx *cli.Context) error { commandNotFound(ctx, legalHoldSubcommands) return nil } minio-client-0.0~20250403/cmd/legalhold-set.go000066400000000000000000000151551477450377600206270ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "path/filepath" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) var lhSetFlags = []cli.Flag{ cli.BoolFlag{ Name: "recursive, r", Usage: "apply legal hold recursively", }, cli.StringFlag{ Name: "version-id, vid", Usage: "apply legal hold to a specific object version", }, cli.StringFlag{ Name: "rewind", Usage: "apply legal hold on an object version at specified time", }, cli.BoolFlag{ Name: "versions", Usage: "apply legal hold on multiple versions of an object", }, } var legalHoldSetCmd = cli.Command{ Name: "set", Usage: "set legal hold for object(s)", Action: mainLegalHoldSet, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(lhSetFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable legal hold on a specific object $ {{.HelpName}} myminio/mybucket/prefix/obj.csv 2. Enable legal hold on a specific object version $ {{.HelpName}} myminio/mybucket/prefix/obj.csv --version-id "HiMFUTOowG6ylfNi4LKxD3ieHbgfgrvC" 3. Enable object legal hold recursively for all objects at a prefix $ {{.HelpName}} myminio/mybucket/prefix --recursive 4. Enable object legal hold recursively for all objects versions older than one year $ {{.HelpName}} myminio/mybucket/prefix --recursive --rewind 365d --versions `, } // setLegalHold - Set legalhold for all objects within a given prefix. func setLegalHold(ctx context.Context, urlStr, versionID string, timeRef time.Time, withVersions, recursive bool, lhold minio.LegalHoldStatus) error { clnt, err := newClient(urlStr) if err != nil { fatalIf(err.Trace(), "Unable to parse the provided url.") } prefixPath := clnt.GetURL().Path prefixPath = filepath.ToSlash(prefixPath) if !strings.HasSuffix(prefixPath, "/") { prefixPath = prefixPath[:strings.LastIndex(prefixPath, "/")+1] } prefixPath = strings.TrimPrefix(prefixPath, "./") if !recursive && !withVersions { err = clnt.PutObjectLegalHold(ctx, versionID, lhold) if err != nil { errorIf(err.Trace(urlStr), "Failed to set legal hold on `%s` successfully", urlStr) } else { contentURL := filepath.ToSlash(clnt.GetURL().Path) key := strings.TrimPrefix(contentURL, prefixPath) printMsg(legalHoldCmdMessage{ LegalHold: lhold, Status: "success", URLPath: clnt.GetURL().String(), Key: key, VersionID: versionID, }) } return nil } alias, _, _ := mustExpandAlias(urlStr) var cErr error objectsFound := false lstOptions := ListOptions{Recursive: recursive, ShowDir: DirNone} if !timeRef.IsZero() { lstOptions.WithOlderVersions = withVersions lstOptions.TimeRef = timeRef } for content := range clnt.List(ctx, lstOptions) { if content.Err != nil { errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") cErr = exitStatus(globalErrorExitStatus) // Set the exit status. continue } if !recursive && getStandardizedURL(alias+getKey(content)) != getStandardizedURL(urlStr) { break } objectsFound = true newClnt, perr := newClientFromAlias(alias, content.URL.String()) if perr != nil { errorIf(content.Err.Trace(clnt.GetURL().String()), "Invalid URL") continue } probeErr := newClnt.PutObjectLegalHold(ctx, content.VersionID, lhold) if probeErr != nil { errorIf(probeErr.Trace(content.URL.Path), "Failed to set legal hold on `%s` successfully", content.URL.Path) } else { if !globalJSON { contentURL := filepath.ToSlash(content.URL.Path) key := strings.TrimPrefix(contentURL, prefixPath) printMsg(legalHoldCmdMessage{ LegalHold: lhold, Status: "success", URLPath: content.URL.String(), Key: key, VersionID: content.VersionID, }) } } } if cErr == nil && !globalJSON { if !objectsFound { console.Print(console.Colorize("LegalHoldMessageFailure", fmt.Sprintf("No objects/versions found while setting legal hold on `%s`. \n", urlStr))) } } return cErr } // Validate command line arguments. func parseLegalHoldArgs(cliCtx *cli.Context) (targetURL, versionID string, timeRef time.Time, recursive, withVersions bool) { args := cliCtx.Args() if len(args) != 1 { showCommandHelpAndExit(cliCtx, 1) } targetURL = args[0] if targetURL == "" { fatalIf(errInvalidArgument(), "You cannot pass an empty target url.") } versionID = cliCtx.String("version-id") recursive = cliCtx.Bool("recursive") withVersions = cliCtx.Bool("versions") rewind := cliCtx.String("rewind") if versionID != "" && (recursive || withVersions || rewind != "") { fatalIf(errInvalidArgument(), "You cannot pass --version-id with any of --versions, --recursive and --rewind flags.") } timeRef = parseRewindFlag(rewind) return } // main for legalhold set command. func mainLegalHoldSet(cliCtx *cli.Context) error { console.SetColor("LegalHoldSuccess", color.New(color.FgGreen, color.Bold)) console.SetColor("LegalHoldFailure", color.New(color.FgRed, color.Bold)) console.SetColor("LegalHoldPartialFailure", color.New(color.FgRed, color.Bold)) console.SetColor("LegalHoldMessageFailure", color.New(color.FgYellow)) targetURL, versionID, timeRef, recursive, withVersions := parseLegalHoldArgs(cliCtx) if timeRef.IsZero() && withVersions { timeRef = time.Now().UTC() } ctx, cancelLegalHold := context.WithCancel(globalContext) defer cancelLegalHold() enabled, err := isBucketLockEnabled(ctx, targetURL) if err != nil { fatalIf(err, "Unable to set legalhold on `%s`", targetURL) } if !enabled { fatalIf(errDummy().Trace(), "Bucket lock needs to be enabled in order to use this feature.") } return setLegalHold(ctx, targetURL, versionID, timeRef, withVersions, recursive, minio.LegalHoldEnabled) } minio-client-0.0~20250403/cmd/license-info.go000066400000000000000000000146641477450377600204620ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "net/http" "time" "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/lipgloss" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var licenseInfoCmd = cli.Command{ Name: "info", Usage: "display license information", OnUsageError: onUsageError, Action: mainLicenseInfo, Before: setGlobalsFromContext, Flags: subnetCommonFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display license configuration for cluster with alias 'play' {{.Prompt}} {{.HelpName}} play `, } const ( licInfoMsgTag = "licenseInfoMessage" licInfoErrTag = "licenseInfoError" licInfoFieldTag = "licenseInfoField" licInfoValTag = "licenseValueField" ) type licInfoMessage struct { Status string `json:"status"` Info licInfo `json:"info,omitempty"` Error string `json:"error,omitempty"` } type licInfo struct { LicenseID string `json:"license_id,omitempty"` // Unique ID of the license Organization string `json:"org,omitempty"` // Subnet organization name Plan string `json:"plan,omitempty"` // Subnet plan IssuedAt *time.Time `json:"issued_at,omitempty"` // Time of license issue ExpiresAt *time.Time `json:"expires_at,omitempty"` // Time of license expiry DeploymentID string `json:"deployment_id,omitempty"` // Cluster deployment ID Message string `json:"message,omitempty"` // Message to be displayed APIKey string `json:"api_key,omitempty"` // API Key of the org account } func licInfoField(s string) string { return console.Colorize(licInfoFieldTag, s) } func licInfoVal(s string) string { return console.Colorize(licInfoValTag, s) } func licInfoMsg(s string) string { return console.Colorize(licInfoMsgTag, s) } func licInfoErr(s string) string { return console.Colorize(licInfoErrTag, s) } // String colorized license info func (li licInfoMessage) String() string { if len(li.Error) > 0 { return licInfoErr(li.Error) } if len(li.Info.Message) > 0 { return licInfoMsg(li.Info.Message) } return getLicInfoStr(li.Info) } // JSON jsonified license info func (li licInfoMessage) JSON() string { jsonBytes, e := json.MarshalIndent(li, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonBytes) } func getLicInfoStr(li licInfo) string { columns := []table.Column{ {Title: "License", Width: 20}, {Title: "", Width: 45}, } rows := []table.Row{ {licInfoField("Organization"), licInfoVal(li.Organization)}, {licInfoField("Plan"), licInfoVal(li.Plan)}, {licInfoField("Issued"), licInfoVal(li.IssuedAt.Format(http.TimeFormat))}, {licInfoField("Expires"), licInfoVal(li.ExpiresAt.Format(http.TimeFormat))}, } if len(li.LicenseID) > 0 { rows = append(rows, table.Row{licInfoField("License ID"), licInfoVal(li.LicenseID)}) } if len(li.DeploymentID) > 0 { rows = append(rows, table.Row{licInfoField("Deployment ID"), licInfoVal(li.DeploymentID)}) } if len(li.APIKey) > 0 { rows = append(rows, table.Row{licInfoField("API Key"), licInfoVal(li.APIKey)}) } t := table.New( table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), table.WithHeight(len(rows)), ) s := table.DefaultStyles() s.Header = s.Header. BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")). BorderBottom(true). Bold(false) s.Selected = s.Selected.Bold(false) t.SetStyles(s) return lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")).Render(t.View()) } func getAGPLMessage() string { return `License: GNU AGPL v3 If you are distributing or hosting MinIO along with your proprietary application as combined works, you may require a commercial license included in the Standard and Enterprise subscription plans. (https://min.io/signup?ref=mc)` } func initLicInfoColors() { console.SetColor(licInfoMsgTag, color.New(color.FgGreen, color.Bold)) console.SetColor(licInfoErrTag, color.New(color.FgRed, color.Bold)) console.SetColor(licInfoFieldTag, color.New(color.FgCyan)) console.SetColor(licInfoValTag, color.New(color.FgWhite)) } func mainLicenseInfo(ctx *cli.Context) error { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } initLicInfoColors() aliasedURL := ctx.Args().Get(0) alias, _ := initSubnetConnectivity(ctx, aliasedURL, true) apiKey, lic, e := getSubnetCreds(alias) fatalIf(probe.NewError(e), "Error in checking cluster registration status") var lim licInfoMessage if len(lic) > 0 { lim = getLicInfoMsg(lic) } else if len(apiKey) > 0 { lim = licInfoMessage{ Status: "success", Info: licInfo{ Message: fmt.Sprintf("%s is registered with SUBNET. License info not available.", alias), }, } } else { // Not registered. Default to AGPLv3 lim = licInfoMessage{ Status: "success", Info: licInfo{ Plan: "AGPLv3", Message: getAGPLMessage(), }, } } printMsg(lim) return nil } func getLicInfoMsg(lic string) licInfoMessage { li, e := parseLicense(lic) if e != nil { return licErrMsg(e) } return licInfoMessage{ Status: "success", Info: licInfo{ LicenseID: li.LicenseID, Organization: li.Organization, Plan: li.Plan, IssuedAt: &li.IssuedAt, ExpiresAt: &li.ExpiresAt, DeploymentID: li.DeploymentID, APIKey: li.APIKey, }, } } func licErrMsg(e error) licInfoMessage { return licInfoMessage{ Status: "error", Error: e.Error(), } } minio-client-0.0~20250403/cmd/license-register.go000066400000000000000000000207271477450377600213500ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "net" "net/url" "os" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/pkg/v3/console" ) const ( licRegisterMsgTag = "licenseRegisterMessage" licRegisterLinkTag = "licenseRegisterLink" ) var licenseRegisterFlags = append([]cli.Flag{ cli.StringFlag{ Name: "name", Usage: "Specify the name to associate to this MinIO cluster in SUBNET", }, cli.StringFlag{ Name: "license", Usage: "license of the account on SUBNET", }, }, subnetCommonFlags...) var licenseRegisterCmd = cli.Command{ Name: "register", Usage: "register with MinIO Subscription Network", OnUsageError: onUsageError, Action: mainLicenseRegister, Before: setGlobalsFromContext, Flags: licenseRegisterFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Register MinIO cluster at alias 'play' on SUBNET, using api key for auth {{.Prompt}} {{.HelpName}} play --api-key 08efc836-4289-dbd4-ad82-b5e8b6d25577 2. Register MinIO cluster at alias 'play' on SUBNET, using license file ./minio.license {{.Prompt}} {{.HelpName}} play --license ./minio.license 3. Register MinIO cluster at alias 'play' on SUBNET, using api key for auth, and "play-cluster" as the preferred name for the cluster on SUBNET. {{.Prompt}} {{.HelpName}} play --api-key 08efc836-4289-dbd4-ad82-b5e8b6d25577 --name play-cluster 4. Register MinIO cluster at alias 'play' on SUBNET in an airgapped environment {{.Prompt}} {{.HelpName}} play --airgap 5. Register MinIO cluster at alias 'play' on SUBNET, using alias as the cluster name. This asks for SUBNET credentials if the cluster is not already registered. {{.Prompt}} {{.HelpName}} play `, } type licRegisterMessage struct { Status string `json:"status"` Alias string `json:"-"` Action string `json:"action,omitempty"` Type string `json:"type"` URL string `json:"url,omitempty"` } // String colorized license register message func (li licRegisterMessage) String() string { var msg string switch li.Type { case "online": msg = console.Colorize(licRegisterMsgTag, fmt.Sprintf("%s %s successfully.", li.Alias, li.Action)) case "offline": msg = fmt.Sprintln("Open the following URL in the browser to register", li.Alias, "on SUBNET:") msg = console.Colorize(licRegisterMsgTag, msg) + console.Colorize(licRegisterLinkTag, li.URL) } return msg } // JSON jsonified license register message func (li licRegisterMessage) JSON() string { jsonBytes, e := json.MarshalIndent(li, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonBytes) } // checkLicenseRegisterSyntax - validate arguments passed by a user func checkLicenseRegisterSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // ClusterRegistrationReq - JSON payload of the subnet api for cluster registration // Contains a registration token created by base64 encoding of the registration info type ClusterRegistrationReq struct { Token string `json:"token"` } // ClusterRegistrationInfo - Information stored in the cluster registration token type ClusterRegistrationInfo struct { DeploymentID string `json:"deployment_id"` ClusterName string `json:"cluster_name"` UsedCapacity uint64 `json:"used_capacity"` Info ClusterInfo `json:"info"` } // ClusterInfo - The "info" sub-node of the cluster registration information struct // Intended to be extensible i.e. more fields will be added as and when required type ClusterInfo struct { MinioVersion string `json:"minio_version"` NoOfServerPools int `json:"no_of_server_pools"` NoOfServers int `json:"no_of_servers"` NoOfDrives int `json:"no_of_drives"` NoOfBuckets uint64 `json:"no_of_buckets"` NoOfObjects uint64 `json:"no_of_objects"` TotalDriveSpace uint64 `json:"total_drive_space"` UsedDriveSpace uint64 `json:"used_drive_space"` } // SubnetLoginReq - JSON payload of the SUBNET login api type SubnetLoginReq struct { Username string `json:"username"` Password string `json:"password"` } // SubnetMFAReq - JSON payload of the SUBNET mfa api type SubnetMFAReq struct { Username string `json:"username"` OTP string `json:"otp"` Token string `json:"token"` } func isPlay(endpoint url.URL) (bool, error) { playEndpoint := "https://play.min.io" if globalAirgapped { return endpoint.String() == playEndpoint, nil } aliasIPs, e := net.LookupHost(endpoint.Hostname()) if e != nil { return false, e } aliasIPSet := set.CreateStringSet(aliasIPs...) playURL, e := url.Parse(playEndpoint) if e != nil { return false, e } playIPs, e := net.LookupHost(playURL.Hostname()) if e != nil { return false, e } playIPSet := set.CreateStringSet(playIPs...) return !aliasIPSet.Intersection(playIPSet).IsEmpty(), nil } func validateNotPlay(aliasedURL string) { client := getClient(aliasedURL) endpoint := client.GetEndpointURL() if endpoint == nil { fatal(errDummy().Trace(), "invalid endpoint on alias "+aliasedURL) return } isplay, e := isPlay(*endpoint) fatalIf(probe.NewError(e), "error checking if endpoint is play:") if isplay { fatal(errDummy().Trace(), "play is a public demo cluster; cannot be registered") } } func mainLicenseRegister(ctx *cli.Context) error { console.SetColor(licRegisterMsgTag, color.New(color.FgGreen, color.Bold)) console.SetColor(licRegisterLinkTag, color.New(color.FgWhite, color.Bold)) checkLicenseRegisterSyntax(ctx) // Get the alias parameter from cli aliasedURL := ctx.Args().Get(0) validateNotPlay(aliasedURL) licFile := ctx.String("license") var alias, accAPIKey string if len(licFile) > 0 { licBytes, e := os.ReadFile(licFile) fatalIf(probe.NewError(e), fmt.Sprintf("Unable to read license file %s", licFile)) alias, _ = url2Alias(aliasedURL) accAPIKey = validateAndSaveLic(string(licBytes), alias, true) } else { alias, accAPIKey = initSubnetConnectivity(ctx, aliasedURL, false) } clusterName := ctx.String("name") if len(clusterName) == 0 { clusterName = alias } else { if globalAirgapped { fatalIf(errInvalidArgument(), "'--name' is not allowed in airgapped mode") } } regInfo := GetClusterRegInfo(getAdminInfo(aliasedURL), clusterName) lrm := licRegisterMessage{Status: "success", Alias: alias} if !globalAirgapped { alreadyRegistered := false if len(accAPIKey) == 0 { apiKey, _, e := getSubnetCreds(alias) fatalIf(probe.NewError(e), "Error in fetching subnet API Key") if len(apiKey) > 0 { alreadyRegistered = true accAPIKey = apiKey } } else { apiKey := getSubnetAPIKeyFromConfig(alias) if len(apiKey) > 0 { alreadyRegistered = true } } lrm.Type = "online" _, _, e := registerClusterOnSubnet(regInfo, alias, accAPIKey) if e == nil { lrm.Action = "registered" if alreadyRegistered { lrm.Action = "updated" } printMsg(lrm) return nil } console.Println("Could not register cluster with SUBNET: ", e.Error()) } // Airgapped mode OR online mode with registration failure lrm.Type = "offline" regToken, e := generateRegToken(regInfo) fatalIf(probe.NewError(e), "Unable to generate registration token") lrm.URL = subnetOfflineRegisterURL(regToken) printMsg(lrm) return nil } func getAdminInfo(aliasedURL string) madmin.InfoMessage { // Create a new MinIO Admin Client client := getClient(aliasedURL) // Fetch info of all servers (cluster or single server) admInfo, e := client.ServerInfo(globalContext) fatalIf(probe.NewError(e), "Unable to fetch cluster info") return admInfo } minio-client-0.0~20250403/cmd/license-unregister.go000066400000000000000000000057231477450377600217120ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) const licUnregisterMsgTag = "licenseUnregisterMessage" var licenseUnregisterCmd = cli.Command{ Name: "unregister", Usage: "unregister from MinIO Subscription Network", OnUsageError: onUsageError, Action: mainLicenseUnregister, Before: setGlobalsFromContext, Hidden: true, Flags: subnetCommonFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Unregister MinIO cluster at alias 'myminio' from SUBNET {{.Prompt}} {{.HelpName}} myminio `, } type licUnregisterMessage struct { Status string `json:"status"` Alias string `json:"-"` } // String colorized license unregister message func (li licUnregisterMessage) String() string { msg := fmt.Sprintf("%s unregistered successfully.", li.Alias) return console.Colorize(licUnregisterMsgTag, msg) } // JSON jsonified license unregister message func (li licUnregisterMessage) JSON() string { jsonBytes, e := json.MarshalIndent(li, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonBytes) } // checkLicenseUnregisterSyntax - validate arguments passed by a user func checkLicenseUnregisterSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainLicenseUnregister(ctx *cli.Context) error { console.SetColor(licUnregisterMsgTag, color.New(color.FgGreen, color.Bold)) checkLicenseUnregisterSyntax(ctx) aliasedURL := ctx.Args().Get(0) alias, apiKey := initSubnetConnectivity(ctx, aliasedURL, true) if len(apiKey) == 0 { // api key not passed as flag. Check that the cluster is registered. apiKey = validateClusterRegistered(alias, true) } if !globalAirgapped { info := getAdminInfo(aliasedURL) e := unregisterClusterFromSubnet(info.DeploymentID, apiKey) fatalIf(probe.NewError(e), "Could not unregister cluster from SUBNET:") } removeSubnetAuthConfig(alias) printMsg(licUnregisterMessage{Status: "success", Alias: alias}) return nil } minio-client-0.0~20250403/cmd/license-update.go000066400000000000000000000071131477450377600210000ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "os" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var licenseUpdateCmd = cli.Command{ Name: "update", Usage: "update the license", OnUsageError: onUsageError, Action: mainLicenseUpdate, Before: setGlobalsFromContext, Flags: supportGlobalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS LICENSE-FILE-PATH FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Update license for cluster with alias 'play' from the file license.key {{.Prompt}} {{.HelpName}} play license.key 2. Update (renew) license for already registered cluster with alias 'play' {{.Prompt}} {{.HelpName}} play `, } const licUpdateMsgTag = "licenseUpdateMessage" type licUpdateMessage struct { Status string `json:"status"` Alias string `json:"-"` } // String colorized license update message func (li licUpdateMessage) String() string { return console.Colorize(licUpdateMsgTag, "License updated successfully for "+li.Alias) } // JSON jsonified license update message func (li licUpdateMessage) JSON() string { jsonBytes, e := json.MarshalIndent(li, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonBytes) } func mainLicenseUpdate(ctx *cli.Context) error { args := ctx.Args() argsLen := len(args) if argsLen > 2 || argsLen < 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } console.SetColor(licUpdateMsgTag, color.New(color.FgGreen, color.Bold)) aliasedURL := args.Get(0) alias, _ := url2Alias(aliasedURL) if argsLen == 2 { licFile := args.Get(1) printMsg(performLicenseUpdate(licFile, alias)) return nil } // renew the license printMsg(performLicenseRenew(alias)) return nil } func performLicenseRenew(alias string) licUpdateMessage { apiKey, _, e := getSubnetCreds(alias) fatalIf(probe.NewError(e), "Error getting subnet creds") if len(apiKey) == 0 { errMsg := fmt.Sprintf("Please register the cluster first by running 'mc license register %s'", alias) fatal(errDummy().Trace(), errMsg) } renewURL := subnetLicenseRenewURL() headers := SubnetAPIKeyAuthHeaders(apiKey) headers.addDeploymentIDHeader(alias) resp, e := SubnetPostReq(renewURL, nil, headers) fatalIf(probe.NewError(e), "Error renewing license for %s", alias) extractAndSaveSubnetCreds(alias, resp) return licUpdateMessage{ Alias: alias, Status: "success", } } func performLicenseUpdate(licFile, alias string) licUpdateMessage { lum := licUpdateMessage{ Alias: alias, Status: "success", } licBytes, e := os.ReadFile(licFile) fatalIf(probe.NewError(e), fmt.Sprintf("Unable to read license file %s", licFile)) lic := string(licBytes) validateAndSaveLic(lic, alias, true) return lum } minio-client-0.0~20250403/cmd/license.go000066400000000000000000000026201477450377600175160ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var licenseSubcommands = []cli.Command{ licenseRegisterCmd, licenseInfoCmd, licenseUpdateCmd, licenseUnregisterCmd, } var licenseCmd = cli.Command{ Name: "license", Usage: "license related commands", Action: mainlicense, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: licenseSubcommands, HideHelpCommand: true, } // mainlicense is the handle for "mc license" command. func mainlicense(ctx *cli.Context) error { commandNotFound(ctx, licenseSubcommands) return nil // Sub-commands like "register", "info" have their own main. } minio-client-0.0~20250403/cmd/ls-main.go000066400000000000000000000157231477450377600174440ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) // ls specific flags. var ( lsFlags = []cli.Flag{ cli.StringFlag{ Name: "rewind", Usage: "list all object versions no later than specified date", }, cli.BoolFlag{ Name: "versions", Usage: "list all versions", }, cli.BoolFlag{ Name: "recursive, r", Usage: "list recursively", }, cli.BoolFlag{ Name: "incomplete, I", Usage: "list incomplete uploads", }, cli.BoolFlag{ Name: "summarize", Usage: "display summary information (number of objects, total size)", }, cli.StringFlag{ Name: "storage-class, sc", Usage: "filter to specified storage class", }, cli.BoolFlag{ Name: "zip", Usage: "list files inside zip archive (MinIO servers only)", }, } ) // list files and folders. var lsCmd = cli.Command{ Name: "ls", Usage: "list buckets and objects", Action: mainList, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(lsFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET ...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List buckets on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} s3 2. List buckets and all its contents from Amazon S3 cloud storage recursively. {{.Prompt}} {{.HelpName}} --recursive s3 3. List all contents of mybucket on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} s3/mybucket/ 4. List all contents of mybucket on Amazon S3 cloud storage on Microsoft Windows. {{.Prompt}} {{.HelpName}} s3\mybucket\ 5. List files recursively on a local filesystem on Microsoft Windows. {{.Prompt}} {{.HelpName}} --recursive C:\Users\Worf\ 6. List incomplete (previously failed) uploads of objects on Amazon S3. {{.Prompt}} {{.HelpName}} --incomplete s3/mybucket 7. List contents at a specific time in the past if the bucket versioning is enabled. {{.Prompt}} {{.HelpName}} --rewind 2020.01.01 s3/mybucket {{.Prompt}} {{.HelpName}} --rewind 2020.01.01T11:30 s3/mybucket {{.Prompt}} {{.HelpName}} --rewind 7d s3/mybucket 8. List all contents versions if the bucket versioning is enabled. {{.Prompt}} {{.HelpName}} --versions s3/mybucket 9. List all objects on mybucket, summarize the number of objects and total size. {{.Prompt}} {{.HelpName}} --summarize s3/mybucket/ 10. List all objects on mybucket, for the GLACIER storage class {{.Prompt}} {{.HelpName}} --storage-class 'GLACIER' s3/mybucket `, } var rewindSupportedFormat = []string{ "2006.01.02", "2006.01.02T15:04", "2006.01.02T15:04:05", time.RFC3339, printDate, } // Parse rewind flag while considering the system local time zone func parseRewindFlag(rewind string) (timeRef time.Time) { if rewind != "" { location, e := time.LoadLocation("Local") if e != nil { return } for _, format := range rewindSupportedFormat { if t, e := time.ParseInLocation(format, rewind, location); e == nil { timeRef = t break } } if timeRef.IsZero() { // rewind is not parsed, check if it is a duration instead if duration, e := ParseDuration(rewind); e == nil { if duration < 0 { fatalIf(probe.NewError(errors.New("negative duration is not supported")), "Unable to parse --rewind argument") } timeRef = time.Now().Add(-time.Duration(duration)) } } if timeRef.IsZero() { // rewind argument still not parsed, error out fatalIf(probe.NewError(errors.New("unknown format")), "Unable to parse --rewind argument") } } return } // checkListSyntax - validate all the passed arguments func checkListSyntax(cliCtx *cli.Context) ([]string, doListOptions) { args := cliCtx.Args() if !cliCtx.Args().Present() { args = []string{"."} } for _, arg := range args { if strings.TrimSpace(arg) == "" { fatalIf(errInvalidArgument().Trace(args...), "Unable to validate empty argument.") } } isRecursive := cliCtx.Bool("recursive") isIncomplete := cliCtx.Bool("incomplete") withVersions := cliCtx.Bool("versions") isSummary := cliCtx.Bool("summarize") listZip := cliCtx.Bool("zip") timeRef := parseRewindFlag(cliCtx.String("rewind")) if listZip && (withVersions || !timeRef.IsZero()) { fatalIf(errInvalidArgument().Trace(args...), "Zip file listing can only be performed on the latest version") } storageClasss := cliCtx.String("storage-class") opts := doListOptions{ timeRef: timeRef, isRecursive: isRecursive, isIncomplete: isIncomplete, isSummary: isSummary, withVersions: withVersions, listZip: listZip, filter: storageClasss, } return args, opts } // mainList - is a handler for mc ls command func mainList(cliCtx *cli.Context) error { ctx, cancelList := context.WithCancel(globalContext) defer cancelList() // Additional command specific theme customization. console.SetColor("File", color.New(color.Bold)) console.SetColor("DEL", color.New(color.FgRed)) console.SetColor("PUT", color.New(color.FgGreen)) console.SetColor("VersionID", color.New(color.FgHiBlue)) console.SetColor("VersionOrd", color.New(color.FgHiMagenta)) console.SetColor("Dir", color.New(color.FgCyan, color.Bold)) console.SetColor("Size", color.New(color.FgYellow)) console.SetColor("Time", color.New(color.FgGreen)) console.SetColor("Summarize", color.New(color.Bold)) console.SetColor("SC", color.New(color.FgBlue)) // check 'ls' cliCtx arguments. args, opts := checkListSyntax(cliCtx) var cErr error for _, targetURL := range args { clnt, err := newClient(targetURL) fatalIf(err.Trace(targetURL), "Unable to initialize target `"+targetURL+"`.") if !strings.HasSuffix(targetURL, string(clnt.GetURL().Separator)) { var st *ClientContent st, err = clnt.Stat(ctx, StatOptions{incomplete: opts.isIncomplete, includeVersions: opts.withVersions}) if st != nil && err == nil && st.Type.IsDir() { targetURL = targetURL + string(clnt.GetURL().Separator) clnt, err = newClient(targetURL) fatalIf(err.Trace(targetURL), "Unable to initialize target `"+targetURL+"`.") } } if e := doList(ctx, clnt, opts); e != nil { cErr = e } } return cErr } minio-client-0.0~20250403/cmd/ls.go000066400000000000000000000172031477450377600165150ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "path/filepath" "sort" "strings" "time" "github.com/dustin/go-humanize" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) // printDate - human friendly formatted date. const ( printDate = "2006-01-02 15:04:05 MST" ) // contentMessage container for content message structure. type contentMessage struct { Status string `json:"status"` Filetype string `json:"type"` Time time.Time `json:"lastModified"` Size int64 `json:"size"` Key string `json:"key"` ETag string `json:"etag"` URL string `json:"url,omitempty"` VersionID string `json:"versionId,omitempty"` VersionOrd int `json:"versionOrdinal,omitempty"` VersionIndex int `json:"versionIndex,omitempty"` IsDeleteMarker bool `json:"isDeleteMarker,omitempty"` StorageClass string `json:"storageClass,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` Tags map[string]string `json:"tags,omitempty"` } // String colorized string message. func (c contentMessage) String() string { message := console.Colorize("Time", fmt.Sprintf("[%s]", c.Time.Format(printDate))) message += console.Colorize("Size", fmt.Sprintf("%7s", strings.Join(strings.Fields(humanize.IBytes(uint64(c.Size))), ""))) fileDesc := "" if c.StorageClass != "" { message += " " + console.Colorize("SC", c.StorageClass) } if c.VersionID != "" { fileDesc += console.Colorize("VersionID", " "+c.VersionID) + console.Colorize("VersionOrd", fmt.Sprintf(" v%d", c.VersionOrd)) if c.IsDeleteMarker { fileDesc += console.Colorize("DEL", " DEL") } else { fileDesc += console.Colorize("PUT", " PUT") } } fileDesc += " " + c.Key if c.Filetype == "folder" { message += console.Colorize("Dir", fileDesc) } else { message += console.Colorize("File", fileDesc) } return message } // JSON jsonified content message. func (c contentMessage) JSON() string { c.Status = "success" jsonMessageBytes, e := json.MarshalIndent(c, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // Use OS separator and adds a trailing separator if it is a dir func getOSDependantKey(path string, isDir bool) string { sep := "/" if isDir && !strings.HasSuffix(path, sep) { return fmt.Sprintf("%s%s", path, sep) } return path } // get content key func getKey(c *ClientContent) string { return getOSDependantKey(c.URL.Path, c.Type.IsDir()) } // Generate printable listing from a list of sorted client // contents, the latest created content comes first. func generateContentMessages(clntURL ClientURL, ctnts []*ClientContent, printAllVersions bool) (msgs []contentMessage) { prefixPath := clntURL.Path prefixPath = filepath.ToSlash(prefixPath) if !strings.HasSuffix(prefixPath, "/") { prefixPath = prefixPath[:strings.LastIndex(prefixPath, "/")+1] } prefixPath = strings.TrimPrefix(prefixPath, "./") nrVersions := len(ctnts) for i, c := range ctnts { // Convert any os specific delimiters to "/". contentURL := filepath.ToSlash(c.URL.Path) // Trim prefix path from the content path. c.URL.Path = strings.TrimPrefix(contentURL, prefixPath) contentMsg := contentMessage{} contentMsg.Time = c.Time.Local() // guess file type. contentMsg.Filetype = func() string { if c.Type.IsDir() { return "folder" } return "file" }() contentMsg.Size = c.Size contentMsg.StorageClass = c.StorageClass contentMsg.Metadata = c.Metadata contentMsg.Tags = c.Tags md5sum := strings.TrimPrefix(c.ETag, "\"") md5sum = strings.TrimSuffix(md5sum, "\"") contentMsg.ETag = md5sum // Convert OS Type to match console file printing style. contentMsg.Key = getKey(c) contentMsg.VersionID = c.VersionID contentMsg.IsDeleteMarker = c.IsDeleteMarker contentMsg.VersionOrd = nrVersions - i // URL is empty by default // Set it to either relative dir (host) or public url (remote) contentMsg.URL = clntURL.String() msgs = append(msgs, contentMsg) if !printAllVersions { break } } return } func sortObjectVersions(ctntVersions []*ClientContent) { // Sort versions sort.Slice(ctntVersions, func(i, j int) bool { if ctntVersions[i].IsLatest { return true } if ctntVersions[j].IsLatest { return false } return ctntVersions[i].Time.After(ctntVersions[j].Time) }) } // summaryMessage container for summary message structure type summaryMessage struct { TotalObjects int64 `json:"totalObjects"` TotalSize int64 `json:"totalSize"` } // String colorized string message func (s summaryMessage) String() string { msg := console.Colorize("Summarize", fmt.Sprintf("\nTotal Size: %s", humanize.IBytes(uint64(s.TotalSize)))) msg += "\n" + console.Colorize("Summarize", fmt.Sprintf("Total Objects: %d", s.TotalObjects)) return msg } // JSON jsonified summary message func (s summaryMessage) JSON() string { jsonMessageBytes, e := json.MarshalIndent(s, "", "") fatalIf(probe.NewError(e), "Unable to marshal into JSON") return string(jsonMessageBytes) } // Pretty print the list of versions belonging to one object func printObjectVersions(clntURL ClientURL, ctntVersions []*ClientContent, printAllVersions bool) { sortObjectVersions(ctntVersions) msgs := generateContentMessages(clntURL, ctntVersions, printAllVersions) for _, msg := range msgs { printMsg(msg) } } type doListOptions struct { timeRef time.Time isRecursive bool isIncomplete bool isSummary bool withVersions bool listZip bool filter string } // doList - list all entities inside a folder. func doList(ctx context.Context, clnt Client, o doListOptions) error { var ( lastPath string perObjectVersions []*ClientContent cErr error totalSize int64 totalObjects int64 ) for content := range clnt.List(ctx, ListOptions{ Recursive: o.isRecursive, Incomplete: o.isIncomplete, TimeRef: o.timeRef, WithOlderVersions: o.withVersions || !o.timeRef.IsZero(), WithDeleteMarkers: true, ShowDir: DirNone, ListZip: o.listZip, }) { if content.Err != nil { errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") cErr = exitStatus(globalErrorExitStatus) // Set the exit status. continue } if content.StorageClass != "" && o.filter != "" && o.filter != "*" && content.StorageClass != o.filter { continue } if lastPath != content.URL.Path { // Print any object in the current list before reinitializing it printObjectVersions(clnt.GetURL(), perObjectVersions, o.withVersions) lastPath = content.URL.Path perObjectVersions = []*ClientContent{} } perObjectVersions = append(perObjectVersions, content) totalSize += content.Size totalObjects++ } printObjectVersions(clnt.GetURL(), perObjectVersions, o.withVersions) if o.isSummary { printMsg(summaryMessage{ TotalObjects: totalObjects, TotalSize: totalSize, }) } return cErr } minio-client-0.0~20250403/cmd/ls_test.go000066400000000000000000000014071477450377600175530ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd minio-client-0.0~20250403/cmd/main.go000066400000000000000000000347501477450377600170310ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "errors" "fmt" "os" "os/exec" "path/filepath" "reflect" "regexp" "runtime" "sort" "strconv" "strings" "syscall" "time" "github.com/inconshreveable/mousetrap" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/pkg/v3/console" "github.com/minio/pkg/v3/env" "github.com/minio/pkg/v3/trie" "github.com/minio/pkg/v3/words" "golang.org/x/term" completeinstall "github.com/posener/complete/cmd/install" ) // global flags for mc. var mcFlags = []cli.Flag{ cli.BoolFlag{ Name: "autocompletion", Usage: "install auto-completion for your shell", }, } // Help template for mc var mcHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: {{.Name}} {{if .VisibleFlags}}[FLAGS] {{end}}COMMAND{{if .VisibleFlags}} [COMMAND FLAGS | -h]{{end}} [ARGUMENTS...] COMMANDS: {{range .VisibleCommands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} {{end}}{{if .VisibleFlags}} GLOBAL FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} TIP: Use '{{.Name}} --autocompletion' to enable shell autocompletion COPYRIGHT: Copyright (c) 2015-` + CopyrightYear + ` MinIO, Inc. LICENSE: GNU AGPLv3 ` func init() { if env.IsSet(mcEnvConfigFile) { configFile := env.Get(mcEnvConfigFile, "") fatalIf(readAliasesFromFile(configFile).Trace(configFile), "Unable to parse "+configFile) } if runtime.GOOS == "windows" { if mousetrap.StartedByExplorer() { fmt.Printf("Don't double-click %s\n", os.Args[0]) fmt.Println("You need to open cmd.exe/PowerShell and run it from the command line") fmt.Println("Press the Enter Key to Exit") fmt.Scanln() os.Exit(1) } } } // Main starts mc application func Main(args []string) error { if len(args) > 1 { switch args[1] { case "mc", filepath.Base(args[0]): mainComplete() return nil } } // ``MC_PROFILER`` supported options are [cpu, mem, block, goroutine]. if p := os.Getenv("MC_PROFILER"); p != "" { profilers := strings.Split(p, ",") if e := enableProfilers(mustGetProfileDir(), profilers); e != nil { console.Fatal(e) } } probe.Init() // Set project's root source path. probe.SetAppInfo("Release-Tag", ReleaseTag) probe.SetAppInfo("Commit", ShortCommitID) // Fetch terminal size, if not available, automatically // set globalQuiet to true on non-window. if w, h, e := term.GetSize(int(os.Stdout.Fd())); e != nil { globalQuiet = runtime.GOOS != "windows" } else { globalTermWidth, globalTermHeight = w, h } // Set the mc app name. appName := filepath.Base(args[0]) if runtime.GOOS == "windows" && strings.HasSuffix(strings.ToLower(appName), ".exe") { // Trim ".exe" from Windows executable. appName = appName[:strings.LastIndex(appName, ".")] } // Monitor OS exit signals and cancel the global context in such case go trapSignals(os.Interrupt, syscall.SIGTERM, syscall.SIGKILL) globalHelpPager = newTermPager() // Wait until the user quits the pager defer globalHelpPager.WaitForExit() parsePagerDisableFlag(args) // Run the app return registerApp(appName).Run(args) } func flagValue(f cli.Flag) reflect.Value { fv := reflect.ValueOf(f) for fv.Kind() == reflect.Ptr { fv = reflect.Indirect(fv) } return fv } func visibleFlags(fl []cli.Flag) []cli.Flag { visible := []cli.Flag{} for _, flag := range fl { field := flagValue(flag).FieldByName("Hidden") if !field.IsValid() || !field.Bool() { visible = append(visible, flag) } } return visible } // Function invoked when invalid flag is passed func onUsageError(ctx *cli.Context, err error, _ bool) error { type subCommandHelp struct { flagName string usage string } // Calculate the maximum width of the flag name field // for a good looking printing vflags := visibleFlags(ctx.Command.Flags) help := make([]subCommandHelp, len(vflags)) maxWidth := 0 for i, f := range vflags { s := strings.Split(f.String(), "\t") if len(s[0]) > maxWidth { maxWidth = len(s[0]) } help[i] = subCommandHelp{flagName: s[0], usage: s[1]} } maxWidth += 2 var errMsg strings.Builder // Do the good-looking printing now fmt.Fprintln(&errMsg, "Invalid command usage,", err.Error()) if len(help) > 0 { fmt.Fprintln(&errMsg, "\nSUPPORTED FLAGS:") for _, h := range help { spaces := string(bytes.Repeat([]byte{' '}, maxWidth-len(h.flagName))) fmt.Fprintf(&errMsg, " %s%s%s\n", h.flagName, spaces, h.usage) } } console.Fatal(errMsg.String()) return err } // Function invoked when invalid command is passed. func commandNotFound(ctx *cli.Context, cmds []cli.Command) { command := ctx.Args().First() if command == "" { cli.ShowCommandHelp(ctx, command) return } msg := fmt.Sprintf("`%s` is not a recognized command. Get help using `--help` flag.", command) commandsTree := trie.NewTrie() for _, cmd := range cmds { commandsTree.Insert(cmd.Name) } closestCommands := findClosestCommands(commandsTree, command) if len(closestCommands) > 0 { msg += "\n\nDid you mean one of these?\n" if len(closestCommands) == 1 { cmd := closestCommands[0] msg += fmt.Sprintf(" `%s`", cmd) } else { for _, cmd := range closestCommands { msg += fmt.Sprintf(" `%s`\n", cmd) } } } fatalIf(errDummy().Trace(), msg) } // Check for sane config environment early on and gracefully report. func checkConfig() { // Refresh the config once. loadMcConfig = loadMcConfigFactory() // Ensures config file is sane. config, err := loadMcConfig() // Verify if the path is accesible before validating the config fatalIf(err.Trace(mustGetMcConfigPath()), "Unable to access configuration file.") // Validate and print error messges ok, errMsgs := validateConfigFile(config) if !ok { var errorMsg bytes.Buffer for index, errMsg := range errMsgs { // Print atmost 10 errors if index > 10 { break } errorMsg.WriteString(errMsg + "\n") } console.Fatal(errorMsg.String()) } } func migrate() { // Fix broken config files if any. fixConfig() // Migrate config files if any. migrateConfig() // Migrate shared urls if any. migrateShare() } // initMC - initialize 'mc'. func initMC() { // Check if mc config exists. if !isMcConfigExists() { err := saveMcConfig(newMcConfig()) fatalIf(err.Trace(), "Unable to save new mc config.") if !globalQuiet && !globalJSON { console.Infoln("Configuration written to `" + mustGetMcConfigPath() + "`. Please update your access credentials.") } } // Check if mc share directory exists. if !isShareDirExists() { initShareConfig() } // Check if certs dir exists if !isCertsDirExists() { fatalIf(createCertsDir().Trace(), "Unable to create `CAs` directory.") } // Check if CAs dir exists if !isCAsDirExists() { fatalIf(createCAsDir().Trace(), "Unable to create `CAs` directory.") } // Load all authority certificates present in CAs dir loadRootCAs() } func getShellName() (string, bool) { shellName := os.Getenv("SHELL") if shellName != "" || runtime.GOOS == "windows" { return strings.ToLower(filepath.Base(shellName)), true } ppid := os.Getppid() cmd := exec.Command("ps", "-p", strconv.Itoa(ppid), "-o", "comm=") ppName, err := cmd.Output() if err != nil { fatalIf(probe.NewError(err), "Failed to enable autocompletion. Cannot determine shell type and "+ "no SHELL environment variable found") } shellName = strings.TrimSpace(string(ppName)) return strings.ToLower(filepath.Base(shellName)), false } func installAutoCompletion() { if runtime.GOOS == "windows" { console.Infoln("autocompletion feature is not available for this operating system") return } shellName, ok := getShellName() if !ok { console.Infoln("No 'SHELL' env var. Your shell is auto determined as '" + shellName + "'.") } else { console.Infoln("Your shell is set to '" + shellName + "', by env var 'SHELL'.") } supportedShellsSet := set.CreateStringSet("bash", "zsh", "fish") if !supportedShellsSet.Contains(shellName) { fatalIf(probe.NewError(errors.New("")), "'"+shellName+"' is not a supported shell. "+ "Supported shells are: bash, zsh, fish") } e := completeinstall.Install(filepath.Base(os.Args[0])) var printMsg string if e != nil && strings.Contains(e.Error(), "* already installed") { errStr := e.Error()[strings.Index(e.Error(), "\n")+1:] re := regexp.MustCompile(`[::space::]*\*.*` + shellName + `.*`) relatedMsg := re.FindStringSubmatch(errStr) if len(relatedMsg) > 0 { printMsg = "\n" + relatedMsg[0] } else { printMsg = "" } } if printMsg != "" { if completeinstall.IsInstalled(filepath.Base(os.Args[0])) || completeinstall.IsInstalled("mc") { console.Infoln("autocompletion is enabled.", printMsg) } else { fatalIf(probe.NewError(e), "Unable to install auto-completion.") } } else { console.Infoln("enabled autocompletion in your '" + shellName + "' rc file. Please restart your shell.") } } func registerBefore(ctx *cli.Context) error { deprecatedFlagsWarning(ctx) if ctx.IsSet("config-dir") { // Set the config directory. setMcConfigDir(ctx.String("config-dir")) } else if ctx.GlobalIsSet("config-dir") { // Set the config directory. setMcConfigDir(ctx.GlobalString("config-dir")) } // Set global flags. setGlobalsFromContext(ctx) // Migrate any old version of config / state files to newer format. migrate() // Initialize default config files. initMC() // Check if config can be read. checkConfig() return nil } // findClosestCommands to match a given string with commands trie tree. func findClosestCommands(commandsTree *trie.Trie, command string) []string { closestCommands := commandsTree.PrefixMatch(command) sort.Strings(closestCommands) // Suggest other close commands - allow missed, wrongly added and even transposed characters for _, value := range commandsTree.Walk(commandsTree.Root()) { if sort.SearchStrings(closestCommands, value) < len(closestCommands) { continue } // 2 is arbitrary and represents the max allowed number of typed errors if words.DamerauLevenshteinDistance(command, value) < 2 { closestCommands = append(closestCommands, value) } } return closestCommands } // Check for updates and print a notification message func checkUpdate(ctx *cli.Context) { // Do not print update messages, if quiet flag is set. if !ctx.Bool("quiet") && !ctx.GlobalBool("quiet") { // Its OK to ignore any errors during doUpdate() here. if updateMsg, _, currentReleaseTime, latestReleaseTime, _, err := getUpdateInfo("", 2*time.Second); err == nil { printMsg(updateMessage{ Status: "success", Message: updateMsg, }) } else { printMsg(updateMessage{ Status: "success", Message: prepareUpdateMessage("Run `mc update`", latestReleaseTime.Sub(currentReleaseTime)), }) } } } var appCmds = []cli.Command{ aliasCmd, adminCmd, anonymousCmd, batchCmd, cpCmd, catCmd, configCmd, corsCmd, diffCmd, duCmd, encryptCmd, eventCmd, findCmd, getCmd, headCmd, ilmCmd, idpCmd, licenseCmd, legalHoldCmd, lsCmd, mbCmd, mvCmd, mirrorCmd, odCmd, pingCmd, policyCmd, pipeCmd, putCmd, quotaCmd, rmCmd, retentionCmd, rbCmd, replicateCmd, readyCmd, sqlCmd, statCmd, supportCmd, shareCmd, treeCmd, tagCmd, undoCmd, updateCmd, versionCmd, watchCmd, } func printMCVersion(c *cli.Context) { fmt.Fprintf(c.App.Writer, "%s version %s (commit-id=%s)\n", c.App.Name, c.App.Version, CommitID) fmt.Fprintf(c.App.Writer, "Runtime: %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH) fmt.Fprintf(c.App.Writer, "Copyright (c) 2015-%s MinIO, Inc.\n", CopyrightYear) fmt.Fprintf(c.App.Writer, "License GNU AGPLv3 \n") } func registerApp(name string) *cli.App { cli.HelpFlag = cli.BoolFlag{ Name: "help, h", Usage: "show help", } // Override default cli version printer cli.VersionPrinter = printMCVersion app := cli.NewApp() app.Name = name app.Action = func(ctx *cli.Context) error { mcEnable := env.Get("MC_UPDATE", madmin.EnableOn) minioEnable := env.Get("MINIO_UPDATE", madmin.EnableOn) if strings.HasPrefix(ReleaseTag, "RELEASE.") && (mcEnable == madmin.EnableOn || minioEnable == madmin.EnableOn) { // Check for new updates from dl.min.io. checkUpdate(ctx) } if ctx.Bool("autocompletion") || ctx.GlobalBool("autocompletion") { // Install shell completions installAutoCompletion() return nil } if ctx.Args().First() == "" { showAppHelpAndExit(ctx) } commandNotFound(ctx, app.Commands) return exitStatus(globalErrorExitStatus) } app.Before = registerBefore app.HideHelpCommand = true app.Usage = "MinIO Client for object storage and filesystems." app.Commands = appCmds app.Author = "MinIO, Inc." app.Version = ReleaseTag app.Flags = append(mcFlags, globalFlags...) app.CustomAppHelpTemplate = mcHelpTemplate app.EnableBashCompletion = true app.OnUsageError = onUsageError app.After = func(*cli.Context) error { globalExpiringCerts.Range(func(k, v interface{}) bool { host := k.(string) expires := v.(time.Time) fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, "== WARN: `%s` certificate will expire in %s. Renew soon to avoid outage.\n", host, expires) fmt.Fprintf(os.Stderr, "\n") return true }) return nil } if isTerminal() && !globalPagerDisabled { app.HelpWriter = globalHelpPager } else { app.HelpWriter = os.Stdout } return app } // mustGetProfilePath must get location that the profile will be written to. func mustGetProfileDir() string { return filepath.Join(mustGetMcConfigDir(), globalProfileDir) } func showCommandHelpAndExit(cliCtx *cli.Context, code int) { cli.ShowCommandHelp(cliCtx, cliCtx.Command.Name) // Wait until the user quits the pager globalHelpPager.WaitForExit() os.Exit(code) } func showAppHelpAndExit(cliCtx *cli.Context) { cli.ShowAppHelp(cliCtx) // Wait until the user quits the pager globalHelpPager.WaitForExit() os.Exit(globalErrorExitStatus) } minio-client-0.0~20250403/cmd/mb-main.go000066400000000000000000000120561477450377600174200ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var mbFlags = []cli.Flag{ cli.StringFlag{ Name: "region", Value: "us-east-1", Usage: "specify bucket region; defaults to 'us-east-1'", }, cli.BoolFlag{ Name: "ignore-existing, p", Usage: "ignore if bucket/directory already exists", }, cli.BoolFlag{ Name: "with-lock, l", Usage: "enable object lock", }, cli.BoolFlag{ Name: "with-versioning", Usage: "enable versioned bucket", }, } // make a bucket. var mbCmd = cli.Command{ Name: "mb", Usage: "make a bucket", Action: mainMakeBucket, Before: setGlobalsFromContext, OnUsageError: onUsageError, Flags: append(mbFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET...] {{if .VisibleFlags}} FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} EXAMPLES: 1. Create a bucket on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} s3/mynewbucket 2. Create a new bucket on Google Cloud Storage. {{.Prompt}} {{.HelpName}} gcs/miniocloud 3. Create a new bucket on Amazon S3 cloud storage in region 'us-west-2'. {{.Prompt}} {{.HelpName}} --region=us-west-2 s3/myregionbucket 4. Create a new directory including its missing parents (equivalent to 'mkdir -p'). {{.Prompt}} {{.HelpName}} /tmp/this/new/dir1 5. Create multiple directories including its missing parents (behavior similar to 'mkdir -p'). {{.Prompt}} {{.HelpName}} /mnt/sdb/mydisk /mnt/sdc/mydisk /mnt/sdd/mydisk 6. Ignore if bucket/directory already exists. {{.Prompt}} {{.HelpName}} --ignore-existing myminio/mynewbucket 7. Create a new bucket on Amazon S3 cloud storage in region 'us-west-2' with object lock enabled. {{.Prompt}} {{.HelpName}} --with-lock --region=us-west-2 s3/myregionbucket 8. Create a new bucket on MinIO with versioning enabled. {{.Prompt}} {{.HelpName}} --with-versioning myminio/myversionedbucket `, } // makeBucketMessage is container for make bucket success and failure messages. type makeBucketMessage struct { Status string `json:"status"` Bucket string `json:"bucket"` Region string `json:"region"` } // String colorized make bucket message. func (s makeBucketMessage) String() string { return console.Colorize("MakeBucket", "Bucket created successfully `"+s.Bucket+"`.") } // JSON jsonified make bucket message. func (s makeBucketMessage) JSON() string { makeBucketJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(makeBucketJSONBytes) } // Validate command line arguments. func checkMakeBucketSyntax(cliCtx *cli.Context) { if !cliCtx.Args().Present() { showCommandHelpAndExit(cliCtx, 1) // last argument is exit code } } // mainMakeBucket is entry point for mb command. func mainMakeBucket(cliCtx *cli.Context) error { // check 'mb' cli arguments. checkMakeBucketSyntax(cliCtx) // Additional command speific theme customization. console.SetColor("MakeBucket", color.New(color.FgGreen, color.Bold)) // Save region. region := cliCtx.String("region") ignoreExisting := cliCtx.Bool("p") withLock := cliCtx.Bool("l") var cErr error for _, targetURL := range cliCtx.Args() { // Instantiate client for URL. clnt, err := newClient(targetURL) if err != nil { errorIf(err.Trace(targetURL), "Invalid target `%s`.", targetURL) cErr = exitStatus(globalErrorExitStatus) continue } ctx, cancelMakeBucket := context.WithCancel(globalContext) defer cancelMakeBucket() // Make bucket. if err = clnt.MakeBucket(ctx, region, ignoreExisting, withLock); err != nil { switch err.ToGoError().(type) { case BucketNameEmpty: errorIf(err.Trace(targetURL), "Unable to make bucket, please use `mc mb %s`.", urlJoinPath(targetURL, "your-bucket-name")) default: errorIf(err.Trace(targetURL), "Unable to make bucket `%s`.", targetURL) } cErr = exitStatus(globalErrorExitStatus) continue } if cliCtx.Bool("with-versioning") { fatalIf(clnt.SetVersion(ctx, "enable", []string{}, false), "Unable to enable versioning") } // Successfully created a bucket. printMsg(makeBucketMessage{Status: "success", Bucket: targetURL}) } return cErr } minio-client-0.0~20250403/cmd/mc_test.go000066400000000000000000000075731477450377600175460ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "path/filepath" "runtime" "testing" "time" checkv1 "gopkg.in/check.v1" ) func Test(t *testing.T) { checkv1.TestingT(t) } type TestSuite struct{} var _ = checkv1.Suite(&TestSuite{}) func (s *TestSuite) SetUpSuite(_ *checkv1.C) { } func (s *TestSuite) TearDownSuite(_ *checkv1.C) { } func (s *TestSuite) TestValidPERMS(c *checkv1.C) { perms := accessPerms("none") c.Assert(perms.isValidAccessPERM(), checkv1.Equals, true) c.Assert(string(perms), checkv1.Equals, "none") perms = accessPerms("public") c.Assert(perms.isValidAccessPERM(), checkv1.Equals, true) c.Assert(string(perms), checkv1.Equals, "public") perms = accessPerms("private") c.Assert(perms.isValidAccessPERM(), checkv1.Equals, true) c.Assert(string(perms), checkv1.Equals, "private") perms = accessPerms("download") c.Assert(perms.isValidAccessPERM(), checkv1.Equals, true) c.Assert(string(perms), checkv1.Equals, "download") perms = accessPerms("upload") c.Assert(perms.isValidAccessPERM(), checkv1.Equals, true) c.Assert(string(perms), checkv1.Equals, "upload") } func (s *TestSuite) TestInvalidPERMS(c *checkv1.C) { perms := accessPerms("invalid") c.Assert(perms.isValidAccessPERM(), checkv1.Equals, false) } func (s *TestSuite) TestGetMcConfigDir(c *checkv1.C) { dir, err := getMcConfigDir() c.Assert(err, checkv1.IsNil) c.Assert(dir, checkv1.Not(checkv1.Equals), "") c.Assert(mustGetMcConfigDir(), checkv1.Equals, dir) } func (s *TestSuite) TestGetMcConfigPath(c *checkv1.C) { dir, err := getMcConfigPath() c.Assert(err, checkv1.IsNil) switch runtime.GOOS { case "linux", "freebsd", "darwin", "solaris": c.Assert(dir, checkv1.Equals, filepath.Join(mustGetMcConfigDir(), "config.json")) case "windows": c.Assert(dir, checkv1.Equals, filepath.Join(mustGetMcConfigDir(), "config.json")) default: c.Fail() } c.Assert(mustGetMcConfigPath(), checkv1.Equals, dir) } func (s *TestSuite) TestIsvalidAliasName(c *checkv1.C) { c.Check(isValidAlias("helloWorld0"), checkv1.Equals, true) c.Check(isValidAlias("hello_World0"), checkv1.Equals, true) c.Check(isValidAlias("h0SFD2k24Fdsa"), checkv1.Equals, true) c.Check(isValidAlias("fdslka-4"), checkv1.Equals, true) c.Check(isValidAlias("fdslka-"), checkv1.Equals, true) c.Check(isValidAlias("helloWorld$"), checkv1.Equals, false) c.Check(isValidAlias("h0SFD2k2#Fdsa"), checkv1.Equals, false) c.Check(isValidAlias("0dslka-4"), checkv1.Equals, false) c.Check(isValidAlias("-fdslka"), checkv1.Equals, false) } func (s *TestSuite) TestHumanizedTime(c *checkv1.C) { hTime := timeDurationToHumanizedDuration(time.Duration(10) * time.Second) c.Assert(hTime.Minutes, checkv1.Equals, int64(0)) c.Assert(hTime.Hours, checkv1.Equals, int64(0)) c.Assert(hTime.Days, checkv1.Equals, int64(0)) hTime = timeDurationToHumanizedDuration(time.Duration(10) * time.Minute) c.Assert(hTime.Hours, checkv1.Equals, int64(0)) c.Assert(hTime.Days, checkv1.Equals, int64(0)) hTime = timeDurationToHumanizedDuration(time.Duration(10) * time.Hour) c.Assert(hTime.Days, checkv1.Equals, int64(0)) hTime = timeDurationToHumanizedDuration(time.Duration(24) * time.Hour) c.Assert(hTime.Days, checkv1.Not(checkv1.Equals), int64(0)) } minio-client-0.0~20250403/cmd/mirror-main.go000066400000000000000000001110131477450377600203250ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "math/rand" "net/http" "path" "path/filepath" "runtime" "strings" "sync" "time" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/encrypt" "github.com/minio/minio-go/v7/pkg/notification" "github.com/minio/pkg/v3/console" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" ) // mirror specific flags. var ( mirrorFlags = []cli.Flag{ cli.BoolFlag{ Name: "force", Usage: "force allows forced overwrite or removal of object(s) on target", Hidden: true, // Hidden since this option is deprecated. }, cli.BoolFlag{ Name: "overwrite", Usage: "overwrite object(s) on target if it differs from source", }, cli.BoolFlag{ Name: "fake", Usage: "perform a fake mirror operation", Hidden: true, // deprecated 2022 }, cli.BoolFlag{ Name: "dry-run", Usage: "perform a fake mirror operation", }, cli.BoolFlag{ Name: "watch, w", Usage: "watch and synchronize changes", }, cli.BoolFlag{ Name: "remove", Usage: "remove extraneous object(s) on target", }, cli.StringFlag{ Name: "region", Usage: "specify region when creating new bucket(s) on target", Value: "us-east-1", }, cli.BoolFlag{ Name: "preserve, a", Usage: "preserve file(s)/object(s) attributes and bucket(s) policy/locking configuration(s) on target bucket(s)", }, cli.BoolFlag{ Name: "md5", Usage: "force all upload(s) to calculate md5sum checksum", Hidden: true, }, cli.BoolFlag{ Name: "multi-master", Usage: "enable multi-master multi-site setup", Hidden: true, }, cli.BoolFlag{ Name: "active-active", Usage: "enable active-active multi-site setup", }, cli.BoolFlag{ Name: "disable-multipart", Usage: "disable multipart upload feature", }, cli.StringSliceFlag{ Name: "exclude", Usage: "exclude object(s) that match specified object name pattern", }, cli.StringSliceFlag{ Name: "exclude-bucket", Usage: "exclude bucket(s) that match specified bucket name pattern", }, cli.StringSliceFlag{ Name: "exclude-storageclass", Usage: "exclude object(s) that match the specified storage class", }, cli.StringFlag{ Name: "older-than", Usage: "filter object(s) older than value in duration string (e.g. 7d10h31s)", }, cli.StringFlag{ Name: "newer-than", Usage: "filter object(s) newer than value in duration string (e.g. 7d10h31s)", }, cli.StringFlag{ Name: "storage-class, sc", Usage: "specify storage class for new object(s) on target", }, cli.StringFlag{ Name: "attr", Usage: "add custom metadata for all objects", }, cli.StringFlag{ Name: "monitoring-address", Usage: "if specified, a new prometheus endpoint will be created to report mirroring activity. (eg: localhost:8081)", }, cli.BoolFlag{ Name: "retry", Usage: "if specified, will enable retrying on a per object basis if errors occur", }, cli.BoolFlag{ Name: "summary", Usage: "print a summary of the mirror session", }, cli.BoolFlag{ Name: "skip-errors", Usage: "skip any errors when mirroring", }, checksumFlag, } ) // Mirror folders recursively from a single source to many destinations var mirrorCmd = cli.Command{ Name: "mirror", Usage: "synchronize object(s) to a remote site", Action: mainMirror, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(append(mirrorFlags, encFlags...), globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] SOURCE TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} ENVIRONMENT VARIABLES: MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key). MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key). EXAMPLES: 01. Mirror a bucket recursively from MinIO cloud storage to a bucket on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} play/photos/2014 s3/backup-photos 02. Mirror a local folder recursively to Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} backup/ s3/archive 03. Only mirror files that are newer than 7 days, 10 hours and 30 minutes to Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} --newer-than "7d10h30m" backup/ s3/archive 04. Mirror a bucket from aliased Amazon S3 cloud storage to a folder on Windows. {{.Prompt}} {{.HelpName}} s3\documents\2014\ C:\backup\2014 05. Mirror a bucket from aliased Amazon S3 cloud storage to a local folder use '--overwrite' to overwrite destination. {{.Prompt}} {{.HelpName}} --overwrite s3/miniocloud miniocloud-backup 06. Mirror a bucket from MinIO cloud storage to a bucket on Amazon S3 cloud storage and remove any extraneous files on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} --remove play/photos/2014 s3/backup-photos/2014 07. Continuously mirror a local folder recursively to MinIO cloud storage. '--watch' continuously watches for new objects, uploads and removes extraneous files on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} --remove --watch /var/lib/backups play/backups 08. Continuously mirror all buckets and objects from site 1 to site 2, removed buckets and objects will be reflected as well. {{.Prompt}} {{.HelpName}} --remove --watch site1-alias/ site2-alias/ 09. Mirror a bucket from aliased Amazon S3 cloud storage to a local folder. Exclude all .* files and *.temp files when mirroring. {{.Prompt}} {{.HelpName}} --exclude ".*" --exclude "*.temp" s3/test ~/test 10. Mirror all buckets from aliased Amazon S3 cloud storage to a local folder. Exclude test* buckets and backup* buckets when mirroring. {{.Prompt}} {{.HelpName}} --exclude-bucket 'test*' --exclude 'backup*' s3 ~/test 11. Mirror objects newer than 10 days from bucket test to a local folder. {{.Prompt}} {{.HelpName}} --newer-than 10d s3/test ~/localfolder 12. Mirror objects older than 30 days from Amazon S3 bucket test to a local folder. {{.Prompt}} {{.HelpName}} --older-than 30d s3/test ~/test 13. Mirror server encrypted objects from Amazon S3 cloud storage to a bucket on Amazon S3 cloud storage {{.Prompt}} {{.HelpName}} --enc-c "minio/archive=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" --enc-c "s3/archive=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5BBB" s3/archive/ minio/archive/ 14. Update 'Cache-Control' header on all existing objects recursively. {{.Prompt}} {{.HelpName}} --attr "Cache-Control=max-age=90000,min-fresh=9000" myminio/video-files myminio/video-files 15. Mirror a local folder recursively to Amazon S3 cloud storage and preserve all local file attributes. {{.Prompt}} {{.HelpName}} -a backup/ s3/archive 16. Cross mirror between sites in a active-active deployment. Site-A: {{.Prompt}} {{.HelpName}} --active-active siteA siteB Site-B: {{.Prompt}} {{.HelpName}} --active-active siteB siteA `, } var ( mirrorTotalOps = promauto.NewCounter(prometheus.CounterOpts{ Name: "mc_mirror_total_s3ops", Help: "The total number of mirror operations", }) mirrorTotalUploadedBytes = promauto.NewCounter(prometheus.CounterOpts{ Name: "mc_mirror_total_s3uploaded_bytes", Help: "The total number of bytes uploaded", }) mirrorFailedOps = promauto.NewCounter(prometheus.CounterOpts{ Name: "mc_mirror_failed_s3ops", Help: "The total number of failed mirror operations", }) mirrorRestarts = promauto.NewCounter(prometheus.CounterOpts{ Name: "mc_mirror_total_restarts", Help: "The number of mirror restarts", }) mirrorReplicationDurations = promauto.NewHistogramVec( prometheus.HistogramOpts{ Name: "mc_mirror_replication_duration", Help: "Histogram of replication time in ms per object sizes", Buckets: prometheus.ExponentialBuckets(1, 20, 5), }, []string{"object_size"}, ) ) const uaMirrorAppName = "mc-mirror" type mirrorJob struct { stopCh chan struct{} // the global watcher object, which receives notifications of created // and deleted files watcher *Watcher // Hold operation status information status Status parallel *ParallelManager // channel for status messages statusCh chan URLs TotalObjects int64 TotalBytes int64 sourceURL string targetURL string opts mirrorOptions } // mirrorMessage container for file mirror messages type mirrorMessage struct { Status string `json:"status"` Source string `json:"source"` Target string `json:"target"` Size int64 `json:"size"` TotalCount int64 `json:"totalCount"` TotalSize int64 `json:"totalSize"` EventTime string `json:"eventTime"` EventType notification.EventType `json:"eventType"` } // String colorized mirror message func (m mirrorMessage) String() string { var msg string if m.EventTime != "" { msg = console.Colorize("Time", fmt.Sprintf("[%s] ", m.EventTime)) } switch m.EventType { case notification.ObjectRemovedDelete: return msg + "Removed " + console.Colorize("Removed", fmt.Sprintf("`%s`", m.Target)) case notification.ObjectRemovedDeleteMarkerCreated: return msg + "Removed (Delete Marker)" + console.Colorize("Removed", fmt.Sprintf("`%s`", m.Target)) case notification.ILMDelMarkerExpirationDelete: return msg + "Removed (ILM)" + console.Colorize("Removed", fmt.Sprintf("`%s`", m.Target)) } if m.EventTime == "" { return console.Colorize("Mirror", fmt.Sprintf("`%s` -> `%s`", m.Source, m.Target)) } msg += console.Colorize("Size", fmt.Sprintf("%6s ", humanize.IBytes(uint64(m.Size)))) msg += console.Colorize("Mirror", fmt.Sprintf("`%s` -> `%s`", m.Source, m.Target)) return msg } // JSON jsonified mirror message func (m mirrorMessage) JSON() string { m.Status = "success" mirrorMessageBytes, e := json.MarshalIndent(m, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(mirrorMessageBytes) } func (mj *mirrorJob) doCreateBucket(ctx context.Context, sURLs URLs) URLs { if mj.opts.isFake { return sURLs.WithError(nil) } // Construct proper path with alias. aliasedURL := filepath.Join(sURLs.TargetAlias, sURLs.TargetContent.URL.Path) clnt, pErr := newClient(aliasedURL) if pErr != nil { return sURLs.WithError(pErr) } err := clnt.MakeBucket(ctx, "", mj.opts.isOverwrite, false) if err != nil { return sURLs.WithError(err) } return sURLs.WithError(nil) } func (mj *mirrorJob) doDeleteBucket(ctx context.Context, sURLs URLs) URLs { if mj.opts.isFake { return sURLs.WithError(nil) } // Construct proper path with alias. aliasedURL := filepath.Join(sURLs.TargetAlias, sURLs.TargetContent.URL.Path) clnt, pErr := newClient(aliasedURL) if pErr != nil { return sURLs.WithError(pErr) } contentCh := make(chan *ClientContent, 1) contentCh <- &ClientContent{URL: clnt.GetURL()} close(contentCh) for result := range clnt.Remove(ctx, false, true, false, false, contentCh) { if result.Err != nil { return sURLs.WithError(result.Err) } } return sURLs.WithError(nil) } // doRemove - removes files on target. func (mj *mirrorJob) doRemove(ctx context.Context, sURLs URLs, event EventInfo) URLs { if mj.opts.isFake { return sURLs.WithError(nil) } // Construct proper path with alias. targetWithAlias := filepath.Join(sURLs.TargetAlias, sURLs.TargetContent.URL.Path) clnt, pErr := newClient(targetWithAlias) if pErr != nil { return sURLs.WithError(pErr) } if sURLs.SourceAlias != "" { clnt.AddUserAgent(uaMirrorAppName+":"+sURLs.SourceAlias, ReleaseTag) } else { clnt.AddUserAgent(uaMirrorAppName, ReleaseTag) } contentCh := make(chan *ClientContent, 1) contentCh <- &ClientContent{URL: *newClientURL(sURLs.TargetContent.URL.Path)} close(contentCh) isRemoveBucket := false resultCh := clnt.Remove(ctx, false, isRemoveBucket, false, false, contentCh) for result := range resultCh { if result.Err != nil { switch result.Err.ToGoError().(type) { case PathInsufficientPermission: // Ignore Permission error. continue } return sURLs.WithError(result.Err) } targetPath := filepath.ToSlash(filepath.Join(sURLs.TargetAlias, sURLs.TargetContent.URL.Path)) mj.status.PrintMsg(mirrorMessage{ Target: targetPath, TotalCount: sURLs.TotalCount, TotalSize: sURLs.TotalSize, EventTime: event.Time, EventType: event.Type, }) } return sURLs.WithError(nil) } // doMirror - Mirror an object to multiple destination. URLs status contains a copy of sURLs and error if any. func (mj *mirrorJob) doMirrorWatch(ctx context.Context, targetPath string, tgtSSE encrypt.ServerSide, sURLs URLs, event EventInfo) URLs { shouldQueue := false if !mj.opts.isOverwrite && !mj.opts.activeActive { targetClient, err := newClient(targetPath) if err != nil { // cannot create targetclient return sURLs.WithError(err) } _, err = targetClient.Stat(ctx, StatOptions{sse: tgtSSE}) if err == nil { if !sURLs.SourceContent.RetentionEnabled && !sURLs.SourceContent.LegalHoldEnabled { return sURLs.WithError(probe.NewError(ObjectAlreadyExists{})) } } // doesn't exist shouldQueue = true } if shouldQueue || mj.opts.isOverwrite || mj.opts.activeActive { // adjust total, because we want to show progress of // the item still queued to be copied. mj.status.Add(sURLs.SourceContent.Size) mj.status.SetTotal(mj.status.Get()).Update() mj.status.AddCounts(1) sURLs.TotalSize = mj.status.Get() sURLs.TotalCount = mj.status.GetCounts() return mj.doMirror(ctx, sURLs, event) } return sURLs.WithError(probe.NewError(ObjectAlreadyExists{})) } func convertSizeToTag(size int64) string { switch { case size < 1024: return "LESS_THAN_1_KiB" case size < 1024*1024: return "LESS_THAN_1_MiB" case size < 10*1024*1024: return "LESS_THAN_10_MiB" case size < 100*1024*1024: return "LESS_THAN_100_MiB" case size < 1024*1024*1024: return "LESS_THAN_1_GiB" default: return "GREATER_THAN_1_GiB" } } // doMirror - Mirror an object to multiple destination. URLs status contains a copy of sURLs and error if any. func (mj *mirrorJob) doMirror(ctx context.Context, sURLs URLs, event EventInfo) URLs { if sURLs.Error != nil { // Erroneous sURLs passed. return sURLs.WithError(sURLs.Error.Trace()) } // For a fake mirror make sure we update respective progress bars // and accounting readers under relevant conditions. if mj.opts.isFake { if sURLs.SourceContent != nil { mj.status.Add(sURLs.SourceContent.Size) } mj.status.Update() return sURLs.WithError(nil) } sourceAlias := sURLs.SourceAlias sourceURL := sURLs.SourceContent.URL targetAlias := sURLs.TargetAlias targetURL := sURLs.TargetContent.URL length := sURLs.SourceContent.Size mj.status.SetCaption(sourceURL.String() + ":") // Initialize target metadata. sURLs.TargetContent.Metadata = make(map[string]string) if mj.opts.storageClass != "" { sURLs.TargetContent.StorageClass = mj.opts.storageClass } if mj.opts.activeActive { srcModTime := getSourceModTimeKey(sURLs.SourceContent.Metadata) // If the source object already has source modtime attribute set, then // use it in target. Otherwise use the S3 modtime instead. if srcModTime != "" { sURLs.TargetContent.Metadata[activeActiveSourceModTimeKey] = srcModTime } else { sURLs.TargetContent.Metadata[activeActiveSourceModTimeKey] = sURLs.SourceContent.Time.Format(time.RFC3339Nano) } } // Initialize additional target user metadata. sURLs.TargetContent.UserMetadata = mj.opts.userMetadata sourcePath := filepath.ToSlash(filepath.Join(sourceAlias, sourceURL.Path)) targetPath := filepath.ToSlash(filepath.Join(targetAlias, targetURL.Path)) if !mj.opts.isSummary { mj.status.PrintMsg(mirrorMessage{ Source: sourcePath, Target: targetPath, Size: length, TotalCount: sURLs.TotalCount, TotalSize: sURLs.TotalSize, EventTime: event.Time, EventType: event.Type, }) } sURLs.MD5 = mj.opts.md5 sURLs.checksum = mj.opts.checksum sURLs.DisableMultipart = mj.opts.disableMultipart var ret URLs if !mj.opts.isRetriable { now := time.Now() ret = uploadSourceToTargetURL(ctx, uploadSourceToTargetURLOpts{urls: sURLs, progress: mj.status, encKeyDB: mj.opts.encKeyDB, preserve: mj.opts.isMetadata, isZip: false}) if ret.Error == nil { durationMs := time.Since(now).Milliseconds() mirrorReplicationDurations.With(prometheus.Labels{"object_size": convertSizeToTag(sURLs.SourceContent.Size)}).Observe(float64(durationMs)) } return ret } newRetryManager(ctx, time.Second, 3).retry(func(rm *retryManager) *probe.Error { if rm.retries > 0 { printMsg(retryMessage{ SourceURL: sURLs.SourceContent.URL.String(), TargetURL: sURLs.TargetContent.URL.String(), Retries: rm.retries, }) } now := time.Now() ret = uploadSourceToTargetURL(ctx, uploadSourceToTargetURLOpts{urls: sURLs, progress: mj.status, encKeyDB: mj.opts.encKeyDB, preserve: mj.opts.isMetadata, isZip: false}) if ret.Error == nil { durationMs := time.Since(now).Milliseconds() mirrorReplicationDurations.With(prometheus.Labels{"object_size": convertSizeToTag(sURLs.SourceContent.Size)}).Observe(float64(durationMs)) } return ret.Error }) return ret } // Update progress status func (mj *mirrorJob) monitorMirrorStatus(cancel context.CancelFunc) (errDuringMirror bool) { // now we want to start the progress bar mj.status.Start() defer mj.status.Finish() var cancelInProgress bool defer func() { // make sure we always cancel the context if !cancelInProgress { cancel() } }() for sURLs := range mj.statusCh { if cancelInProgress { // Do not need to print any error after // canceling the context, just draining // the status channel here. continue } // Update prometheus fields mirrorTotalOps.Inc() if sURLs.Error != nil { var ignoreErr bool switch { case sURLs.SourceContent != nil: if isErrIgnored(sURLs.Error) { ignoreErr = true } else { switch sURLs.Error.ToGoError().(type) { case PathInsufficientPermission: // Ignore Permission error. ignoreErr = true } if !ignoreErr { errorIf(sURLs.Error.Trace(sURLs.SourceContent.URL.String()), "Failed to copy `%s`.", sURLs.SourceContent.URL) } } case sURLs.TargetContent != nil: // When sURLs.SourceContent is nil, we know that we have an error related to removing errorIf(sURLs.Error.Trace(sURLs.TargetContent.URL.String()), "Failed to remove `%s`.", sURLs.TargetContent.URL.String()) default: if strings.Contains(sURLs.Error.ToGoError().Error(), "Overwrite not allowed") { ignoreErr = true } if sURLs.ErrorCond == differInUnknown { errorIf(sURLs.Error.Trace(), "Failed to perform mirroring") } else { errorIf(sURLs.Error.Trace(), "Failed to perform mirroring, with error condition (%s)", sURLs.ErrorCond) } } if !ignoreErr { mirrorFailedOps.Inc() errDuringMirror = true // Quit mirroring if --skip-errors is not passed if !mj.opts.skipErrors { cancel() cancelInProgress = true } } continue } if sURLs.SourceContent != nil { mirrorTotalUploadedBytes.Add(float64(sURLs.SourceContent.Size)) } } return } func (mj *mirrorJob) watchMirrorEvents(ctx context.Context, events []EventInfo) { for _, event := range events { // It will change the expanded alias back to the alias // again, by replacing the sourceUrlFull with the sourceAlias. // This url will be used to mirror. sourceAlias, sourceURLFull, _ := mustExpandAlias(mj.sourceURL) // If the passed source URL points to fs, fetch the absolute src path // to correctly calculate targetPath if sourceAlias == "" { tmpSrcURL, e := filepath.Abs(sourceURLFull) if e == nil { sourceURLFull = tmpSrcURL } } eventPath := event.Path switch runtime.GOOS { case "darwin": // Strip the prefixes in the event path. Happens in darwin OS only eventPath = eventPath[strings.Index(eventPath, sourceURLFull):] case "windows": // Shared folder as source URL and if event path is an absolute path. eventPath = getEventPathURLWin(mj.sourceURL, eventPath) } sourceURL := newClientURL(eventPath) // build target path, it is the relative of the eventPath with the sourceUrl // joined to the targetURL. sourceSuffix := strings.TrimPrefix(eventPath, sourceURLFull) // Skip the object, if it matches the Exclude options provided if matchExcludeOptions(mj.opts.excludeOptions, sourceSuffix, sourceURL.Type) { continue } // Skip the bucket, if it matches the Exclude options provided if matchExcludeBucketOptions(mj.opts.excludeBuckets, sourceSuffix) { continue } sc, ok := event.UserMetadata["x-amz-storage-class"] if ok { var found bool for _, esc := range mj.opts.excludeStorageClasses { if esc == sc { found = true break } } if found { continue } } targetPath := urlJoinPath(mj.targetURL, sourceSuffix) // newClient needs the unexpanded path, newCLientURL needs the expanded path targetAlias, expandedTargetPath, _ := mustExpandAlias(targetPath) targetURL := newClientURL(expandedTargetPath) tgtSSE := getSSE(targetPath, mj.opts.encKeyDB[targetAlias]) if strings.HasPrefix(string(event.Type), "s3:ObjectCreated:") { sourceModTime, _ := time.Parse(time.RFC3339Nano, event.Time) mirrorURL := URLs{ SourceAlias: sourceAlias, SourceContent: &ClientContent{ URL: *sourceURL, RetentionEnabled: event.Type == notification.EventType("s3:ObjectCreated:PutRetention"), LegalHoldEnabled: event.Type == notification.EventType("s3:ObjectCreated:PutLegalHold"), Size: event.Size, Time: sourceModTime, Metadata: event.UserMetadata, }, TargetAlias: targetAlias, TargetContent: &ClientContent{URL: *targetURL}, MD5: mj.opts.md5, checksum: mj.opts.checksum, DisableMultipart: mj.opts.disableMultipart, encKeyDB: mj.opts.encKeyDB, } if mj.opts.activeActive && event.Type != notification.ObjectCreatedCopy && event.Type != notification.ObjectCreatedCompleteMultipartUpload && (getSourceModTimeKey(mirrorURL.SourceContent.Metadata) != "" || getSourceModTimeKey(mirrorURL.SourceContent.UserMetadata) != "") { // If source has active-active attributes, it means that the // object was uploaded by "mc mirror", hence ignore the event // to avoid copying it. continue } mj.parallel.queueTask(func() URLs { return mj.doMirrorWatch(ctx, targetPath, tgtSSE, mirrorURL, event) }, mirrorURL.SourceContent.Size) } else if event.Type == notification.ObjectRemovedDelete || event.Type == notification.ObjectRemovedDeleteMarkerCreated || event.Type == notification.ILMDelMarkerExpirationDelete { if targetAlias != "" && strings.Contains(event.UserAgent, uaMirrorAppName+":"+targetAlias) { // Ignore delete cascading delete events if cyclical. continue } mirrorURL := URLs{ SourceAlias: sourceAlias, SourceContent: nil, TargetAlias: targetAlias, TargetContent: &ClientContent{URL: *targetURL}, MD5: mj.opts.md5, checksum: mj.opts.checksum, DisableMultipart: mj.opts.disableMultipart, encKeyDB: mj.opts.encKeyDB, } mirrorURL.TotalCount = mj.status.GetCounts() mirrorURL.TotalSize = mj.status.Get() if mirrorURL.TargetContent != nil && (mj.opts.isRemove || mj.opts.activeActive) { mj.parallel.queueTask(func() URLs { return mj.doRemove(ctx, mirrorURL, event) }, 0) } } else if event.Type == notification.BucketCreatedAll { mirrorURL := URLs{ SourceAlias: sourceAlias, SourceContent: &ClientContent{URL: *sourceURL}, TargetAlias: targetAlias, TargetContent: &ClientContent{URL: *targetURL}, } mj.parallel.queueTaskWithBarrier(func() URLs { return mj.doCreateBucket(ctx, mirrorURL) }, 0) } else if event.Type == notification.BucketRemovedAll && mj.opts.isRemove { mirrorURL := URLs{ TargetAlias: targetAlias, TargetContent: &ClientContent{URL: *targetURL}, } mj.parallel.queueTaskWithBarrier(func() URLs { return mj.doDeleteBucket(ctx, mirrorURL) }, 0) } } } // this goroutine will watch for notifications, and add modified objects to the queue func (mj *mirrorJob) watchMirror(ctx context.Context) { defer mj.watcher.Stop() for { select { case events, ok := <-mj.watcher.Events(): if !ok { return } mj.watchMirrorEvents(ctx, events) case err, ok := <-mj.watcher.Errors(): if !ok { return } switch err.ToGoError().(type) { case APINotImplemented: errorIf(err.Trace(), "Unable to Watch on source, perhaps source doesn't support Watching for events") return } if err != nil { mj.parallel.queueTask(func() URLs { return URLs{Error: err} }, 0) } case <-ctx.Done(): return } } } func (mj *mirrorJob) watchURL(ctx context.Context, sourceClient Client) *probe.Error { return mj.watcher.Join(ctx, sourceClient, true) } // Fetch urls that need to be mirrored func (mj *mirrorJob) startMirror(ctx context.Context) { URLsCh := prepareMirrorURLs(ctx, mj.sourceURL, mj.targetURL, mj.opts) for { select { case sURLs, ok := <-URLsCh: if !ok { return } if sURLs.Error != nil { mj.statusCh <- sURLs continue } if sURLs.SourceContent != nil { if isOlder(sURLs.SourceContent.Time, mj.opts.olderThan) { continue } if isNewer(sURLs.SourceContent.Time, mj.opts.newerThan) { continue } } if sURLs.SourceContent != nil { mj.status.Add(sURLs.SourceContent.Size) } mj.status.SetTotal(mj.status.Get()).Update() mj.status.AddCounts(1) // Save total count. sURLs.TotalCount = mj.status.GetCounts() // Save totalSize. sURLs.TotalSize = mj.status.Get() if sURLs.SourceContent != nil { mj.parallel.queueTask(func() URLs { return mj.doMirror(ctx, sURLs, EventInfo{}) }, sURLs.SourceContent.Size) } else if sURLs.TargetContent != nil && mj.opts.isRemove { mj.parallel.queueTask(func() URLs { return mj.doRemove(ctx, sURLs, EventInfo{}) }, 0) } case <-ctx.Done(): return case <-mj.stopCh: return } } } // when using a struct for copying, we could save a lot of passing of variables func (mj *mirrorJob) mirror(ctx context.Context) bool { var wg sync.WaitGroup ctx, cancel := context.WithCancel(ctx) // Starts watcher loop for watching for new events. if mj.opts.isWatch { wg.Add(1) go func() { defer wg.Done() mj.watchMirror(ctx) }() } // Start mirroring. wg.Add(1) go func() { defer wg.Done() // startMirror locks and blocks itself. mj.startMirror(ctx) }() // Close statusCh when both watch & mirror quits go func() { wg.Wait() mj.parallel.stopAndWait() close(mj.statusCh) }() return mj.monitorMirrorStatus(cancel) } func newMirrorJob(srcURL, dstURL string, opts mirrorOptions) *mirrorJob { mj := mirrorJob{ stopCh: make(chan struct{}), sourceURL: srcURL, targetURL: dstURL, opts: opts, statusCh: make(chan URLs), watcher: NewWatcher(UTCNow()), } mj.parallel = newParallelManager(mj.statusCh) // we'll define the status to use here, // do we want the quiet status? or the progressbar if globalQuiet || opts.isSummary { mj.status = NewQuietStatus(mj.parallel) } else if globalJSON { mj.status = NewQuietStatus(mj.parallel) } else { mj.status = NewProgressStatus(mj.parallel) } return &mj } // copyBucketPolicies - copy policies from source to dest func copyBucketPolicies(ctx context.Context, srcClt, dstClt Client, isOverwrite bool) *probe.Error { rules, err := srcClt.GetAccessRules(ctx) if err != nil { switch err.ToGoError().(type) { case APINotImplemented: return nil } return err } // Set found rules to target bucket if permitted for _, r := range rules { originalRule, _, err := dstClt.GetAccess(ctx) if err != nil { return err } // Set rule only if it doesn't exist in the target bucket // or force flag is activated if originalRule == "none" || isOverwrite { err = dstClt.SetAccess(ctx, r, false) if err != nil { return err } } } return nil } func getEventPathURLWin(srcURL, eventPath string) string { // A rename or move or sometimes even write event sets eventPath as an absolute filepath. // If the watch folder is a shared folder the write events show the entire event path, // from which we need to deduce the correct path relative to the source URL var eventRelPath, lastPathPrefix string var lastPathPrefixPos int sourceURLpathList := strings.Split(srcURL, slashSeperator) lenSrcURLSlice := len(sourceURLpathList) shdModifyEventPath := filepath.IsAbs(eventPath) && !filepath.IsAbs(srcURL) && lenSrcURLSlice > 1 if shdModifyEventPath { lastPathPrefix = sourceURLpathList[lenSrcURLSlice-1] lastPathPrefixPos = strings.Index(eventPath, lastPathPrefix) } canModifyEventPath := shdModifyEventPath && lastPathPrefix != "" && lastPathPrefixPos > 0 canModifyEventPath = canModifyEventPath && lastPathPrefixPos+len(lastPathPrefix) < len(eventPath) if canModifyEventPath { eventRelPath = filepath.ToSlash(eventPath[lastPathPrefixPos+len(lastPathPrefix):]) eventPath = srcURL + eventRelPath } return eventPath } // runMirror - mirrors all buckets to another S3 server func runMirror(ctx context.Context, srcURL, dstURL string, cli *cli.Context, encKeyDB map[string][]prefixSSEPair) bool { // Parse metadata. userMetadata := make(map[string]string) if cli.String("attr") != "" { var err *probe.Error userMetadata, err = getMetaDataEntry(cli.String("attr")) fatalIf(err, "Unable to parse attribute %v", cli.String("attr")) } srcClt, err := newClient(srcURL) fatalIf(err, "Unable to initialize `"+srcURL+"`.") dstClt, err := newClient(dstURL) fatalIf(err, "Unable to initialize `"+dstURL+"`.") // This is kept for backward compatibility, `--force` means --overwrite. isOverwrite := cli.Bool("force") if !isOverwrite { isOverwrite = cli.Bool("overwrite") } isWatch := cli.Bool("watch") || cli.Bool("multi-master") || cli.Bool("active-active") isRemove := cli.Bool("remove") md5, checksum := parseChecksum(cli) // preserve is also expected to be overwritten if necessary isMetadata := cli.Bool("a") || isWatch || len(userMetadata) > 0 isFake := cli.Bool("fake") || cli.Bool("dry-run") mopts := mirrorOptions{ isFake: isFake, isRemove: isRemove, isOverwrite: isOverwrite, isWatch: isWatch, isMetadata: isMetadata, isSummary: cli.Bool("summary"), isRetriable: cli.Bool("retry"), md5: md5, checksum: checksum, disableMultipart: cli.Bool("disable-multipart"), skipErrors: cli.Bool("skip-errors"), excludeOptions: cli.StringSlice("exclude"), excludeBuckets: cli.StringSlice("exclude-bucket"), excludeStorageClasses: cli.StringSlice("exclude-storageclass"), olderThan: cli.String("older-than"), newerThan: cli.String("newer-than"), storageClass: cli.String("storage-class"), userMetadata: userMetadata, encKeyDB: encKeyDB, activeActive: isWatch, } // If we are not using active/active and we are not removing // files from the remote, then we can exit the listing once // local files have been checked for diff. if !mopts.activeActive && !mopts.isRemove { mopts.sourceListingOnly = true } // Create a new mirror job and execute it mj := newMirrorJob(srcURL, dstURL, mopts) preserve := cli.Bool("preserve") createDstBuckets := dstClt.GetURL().Type == objectStorage && dstClt.GetURL().Path == string(dstClt.GetURL().Separator) mirrorSrcBuckets := srcClt.GetURL().Type == objectStorage && srcClt.GetURL().Path == string(srcClt.GetURL().Separator) mirrorBucketsToBuckets := mirrorSrcBuckets && createDstBuckets if mirrorSrcBuckets || createDstBuckets { // Synchronize buckets using dirDifference function for d := range bucketDifference(ctx, srcClt, dstClt, mj.opts) { if d.Error != nil { if mj.opts.activeActive { errorIf(d.Error, "Failed to start mirroring.. retrying") return true } mj.status.fatalIf(d.Error, "Failed to start mirroring.") } if d.Diff == differInSecond { diffBucket := strings.TrimPrefix(d.SecondURL, dstClt.GetURL().String()) if !isFake && isRemove { aliasedDstBucket := path.Join(dstURL, diffBucket) err := deleteBucket(ctx, aliasedDstBucket, false) mj.status.fatalIf(err, "Failed to start mirroring.") } continue } sourceSuffix := strings.TrimPrefix(d.FirstURL, srcClt.GetURL().String()) newSrcURL := path.Join(srcURL, sourceSuffix) newTgtURL := path.Join(dstURL, sourceSuffix) newSrcClt, _ := newClient(newSrcURL) newDstClt, _ := newClient(newTgtURL) if d.Diff == differInFirst { var ( withLock bool mode minio.RetentionMode validity uint64 unit minio.ValidityUnit err *probe.Error ) if preserve && mirrorBucketsToBuckets { _, mode, validity, unit, err = newSrcClt.GetObjectLockConfig(ctx) if err == nil { withLock = true } } mj.status.PrintMsg(mirrorMessage{ Source: newSrcURL, Target: newTgtURL, }) if mj.opts.isFake { continue } // Skip create bucket, if it matches the Exclude options provided if matchExcludeBucketOptions(mopts.excludeBuckets, sourceSuffix) { continue } // Bucket only exists in the source, create the same bucket in the destination if err := newDstClt.MakeBucket(ctx, cli.String("region"), false, withLock); err != nil { errorIf(err, "Unable to create bucket at `%s`.", newTgtURL) continue } if preserve && mirrorBucketsToBuckets { // object lock configuration set on bucket if mode != "" { err = newDstClt.SetObjectLockConfig(ctx, mode, validity, unit) errorIf(err, "Unable to set object lock config in `%s`.", newTgtURL) if err != nil && mj.opts.activeActive { return true } if err == nil { mj.opts.md5 = true mj.opts.checksum = minio.ChecksumNone } } errorIf(copyBucketPolicies(ctx, newSrcClt, newDstClt, isOverwrite), "Unable to copy bucket policies to `%s`.", newDstClt.GetURL()) } } } } if mj.opts.isWatch { // monitor mode will watch the source folders for changes, // and queue them for copying. if err := mj.watchURL(ctx, srcClt); err != nil { if mj.opts.activeActive { errorIf(err, "Failed to start monitoring.. retrying") return true } mj.status.fatalIf(err, "Failed to start monitoring.") } } return mj.mirror(ctx) } // Main entry point for mirror command. func mainMirror(cliCtx *cli.Context) error { // Additional command specific theme customization. console.SetColor("Mirror", color.New(color.FgGreen, color.Bold)) ctx, cancelMirror := context.WithCancel(globalContext) defer cancelMirror() encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) fatalIf(err, "Unable to parse encryption keys.") // check 'mirror' cli arguments. srcURL, tgtURL := checkMirrorSyntax(ctx, cliCtx, encKeyDB) if prometheusAddress := cliCtx.String("monitoring-address"); prometheusAddress != "" { http.Handle("/metrics", promhttp.Handler()) go func() { if e := http.ListenAndServe(prometheusAddress, nil); e != nil { fatalIf(probe.NewError(e), "Unable to setup monitoring endpoint.") } }() } r := rand.New(rand.NewSource(time.Now().UnixNano())) for { select { case <-ctx.Done(): return exitStatus(globalErrorExitStatus) default: errorDetected := runMirror(ctx, srcURL, tgtURL, cliCtx, encKeyDB) if cliCtx.Bool("watch") || cliCtx.Bool("multi-master") || cliCtx.Bool("active-active") { mirrorRestarts.Inc() time.Sleep(time.Duration(r.Float64() * float64(2*time.Second))) continue } if errorDetected { return exitStatus(globalErrorExitStatus) } return nil } } } minio-client-0.0~20250403/cmd/mirror-url.go000066400000000000000000000227671477450377600202240ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "path/filepath" "runtime" "strings" "time" "github.com/minio/cli" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/wildcard" ) // // * MIRROR ARGS - VALID CASES // ========================= // mirror(d1..., d2) -> []mirror(d1/f, d2/d1/f) // checkMirrorSyntax(URLs []string) func checkMirrorSyntax(ctx context.Context, cliCtx *cli.Context, encKeyDB map[string][]prefixSSEPair) (srcURL, tgtURL string) { if len(cliCtx.Args()) != 2 { showCommandHelpAndExit(cliCtx, 1) // last argument is exit code. } parseChecksum(cliCtx) // extract URLs. URLs := cliCtx.Args() srcURL = URLs[0] tgtURL = URLs[1] if cliCtx.Bool("force") && cliCtx.Bool("remove") { errorIf(errInvalidArgument().Trace(URLs...), "`--force` is deprecated, please use `--overwrite` instead with `--remove` for the same functionality.") } else if cliCtx.Bool("force") { errorIf(errInvalidArgument().Trace(URLs...), "`--force` is deprecated, please use `--overwrite` instead for the same functionality.") } _, expandedSourcePath, _ := mustExpandAlias(srcURL) srcClient := newClientURL(expandedSourcePath) _, expandedTargetPath, _ := mustExpandAlias(tgtURL) destClient := newClientURL(expandedTargetPath) // Mirror with preserve option on windows // only works for object storage to object storage if runtime.GOOS == "windows" && cliCtx.Bool("a") { if srcClient.Type == fileSystem || destClient.Type == fileSystem { errorIf(errInvalidArgument(), "Preserve functionality on windows support object storage to object storage transfer only.") } } /****** Generic rules *******/ if !cliCtx.Bool("watch") && !cliCtx.Bool("active-active") && !cliCtx.Bool("multi-master") { _, srcContent, err := url2Stat(ctx, url2StatOptions{urlStr: srcURL, versionID: "", fileAttr: false, encKeyDB: encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false}) if err != nil { fatalIf(err.Trace(srcURL), "Unable to stat source `"+srcURL+"`.") } if !srcContent.Type.IsDir() { fatalIf(errInvalidArgument().Trace(srcContent.URL.String(), srcContent.Type.String()), fmt.Sprintf("Source `%s` is not a folder. Only folders are supported by mirror command.", srcURL)) } if srcClient.Type == fileSystem && !filepath.IsAbs(srcURL) { origSrcURL := srcURL var e error // Changing relative path to absolute path, if it is a local directory. // Save original in case of error if srcURL, e = filepath.Abs(srcURL); e != nil { srcURL = origSrcURL } } } return } func matchExcludeOptions(excludeOptions []string, srcSuffix string, typ ClientURLType) bool { // if type is file system, remove leading slash if typ == fileSystem { if strings.HasPrefix(srcSuffix, "/") { srcSuffix = srcSuffix[1:] } else if runtime.GOOS == "windows" && strings.HasPrefix(srcSuffix, `\`) { srcSuffix = srcSuffix[1:] } } for _, pattern := range excludeOptions { if wildcard.Match(pattern, srcSuffix) { return true } } return false } func matchExcludeBucketOptions(excludeBuckets []string, srcSuffix string) bool { if strings.HasPrefix(srcSuffix, "/") { srcSuffix = srcSuffix[1:] } else if runtime.GOOS == "windows" && strings.HasPrefix(srcSuffix, `\`) { srcSuffix = srcSuffix[1:] } var bucketName string if runtime.GOOS == "windows" { bucketName = strings.Split(srcSuffix, `\`)[0] } else { bucketName = strings.Split(srcSuffix, "/")[0] } for _, pattern := range excludeBuckets { if wildcard.Match(pattern, bucketName) { return true } } return false } func deltaSourceTarget(ctx context.Context, sourceURL, targetURL string, opts mirrorOptions, URLsCh chan<- URLs) { // source and targets are always directories sourceSeparator := string(newClientURL(sourceURL).Separator) if !strings.HasSuffix(sourceURL, sourceSeparator) { sourceURL = sourceURL + sourceSeparator } targetSeparator := string(newClientURL(targetURL).Separator) if !strings.HasSuffix(targetURL, targetSeparator) { targetURL = targetURL + targetSeparator } // Extract alias and expanded URL sourceAlias, sourceURL, _ := mustExpandAlias(sourceURL) targetAlias, targetURL, _ := mustExpandAlias(targetURL) defer close(URLsCh) sourceClnt, err := newClientFromAlias(sourceAlias, sourceURL) if err != nil { URLsCh <- URLs{Error: err.Trace(sourceAlias, sourceURL)} return } targetClnt, err := newClientFromAlias(targetAlias, targetURL) if err != nil { URLsCh <- URLs{Error: err.Trace(targetAlias, targetURL)} return } // If the passed source URL points to fs, fetch the absolute src path // to correctly calculate targetPath if sourceAlias == "" { tmpSrcURL, e := filepath.Abs(sourceURL) if e == nil { sourceURL = tmpSrcURL } } // List both source and target, compare and return values through channel. for diffMsg := range objectDifference(ctx, sourceClnt, targetClnt, opts) { if diffMsg.Error != nil { // Send all errors through the channel URLsCh <- URLs{Error: diffMsg.Error, ErrorCond: differInUnknown} continue } srcSuffix := strings.TrimPrefix(diffMsg.FirstURL, sourceURL) // Skip the source object if it matches the Exclude options provided if matchExcludeOptions(opts.excludeOptions, srcSuffix, newClientURL(sourceURL).Type) { continue } // Skip the source bucket if it matches the Exclude options provided if matchExcludeBucketOptions(opts.excludeBuckets, srcSuffix) { continue } tgtSuffix := strings.TrimPrefix(diffMsg.SecondURL, targetURL) // Skip the target object if it matches the Exclude options provided if matchExcludeOptions(opts.excludeOptions, tgtSuffix, newClientURL(targetURL).Type) { continue } // Skip the target bucket if it matches the Exclude options provided if matchExcludeBucketOptions(opts.excludeBuckets, tgtSuffix) { continue } if diffMsg.firstContent != nil { var found bool for _, esc := range opts.excludeStorageClasses { if esc == diffMsg.firstContent.StorageClass { found = true break } } if found { continue } } switch diffMsg.Diff { case differInNone: // No difference, continue. case differInType: URLsCh <- URLs{Error: errInvalidTarget(diffMsg.SecondURL)} case differInSize, differInMetadata, differInAASourceMTime: if !opts.isOverwrite && !opts.isFake && !opts.activeActive { // Size or time or etag differs but --overwrite not set. URLsCh <- URLs{ Error: errOverWriteNotAllowed(diffMsg.SecondURL), ErrorCond: diffMsg.Diff, } continue } sourceSuffix := strings.TrimPrefix(diffMsg.FirstURL, sourceURL) // Either available only in source or size differs and force is set targetPath := urlJoinPath(targetURL, sourceSuffix) sourceContent := diffMsg.firstContent targetContent := &ClientContent{URL: *newClientURL(targetPath)} URLsCh <- URLs{ SourceAlias: sourceAlias, SourceContent: sourceContent, TargetAlias: targetAlias, TargetContent: targetContent, } case differInFirst: // Only in first, always copy. sourceSuffix := strings.TrimPrefix(diffMsg.FirstURL, sourceURL) targetPath := urlJoinPath(targetURL, sourceSuffix) sourceContent := diffMsg.firstContent targetContent := &ClientContent{URL: *newClientURL(targetPath)} URLsCh <- URLs{ SourceAlias: sourceAlias, SourceContent: sourceContent, TargetAlias: targetAlias, TargetContent: targetContent, } case differInSecond: if !opts.isRemove && !opts.isFake { continue } URLsCh <- URLs{ TargetAlias: targetAlias, TargetContent: diffMsg.secondContent, } default: URLsCh <- URLs{ Error: errUnrecognizedDiffType(diffMsg.Diff).Trace(diffMsg.FirstURL, diffMsg.SecondURL), ErrorCond: diffMsg.Diff, } } } } type mirrorOptions struct { isFake, isOverwrite, activeActive bool isWatch, isRemove, isMetadata bool isRetriable bool isSummary bool skipErrors bool excludeOptions, excludeStorageClasses, excludeBuckets []string encKeyDB map[string][]prefixSSEPair md5, disableMultipart bool olderThan, newerThan string storageClass string userMetadata map[string]string checksum minio.ChecksumType sourceListingOnly bool } // Prepares urls that need to be copied or removed based on requested options. func prepareMirrorURLs(ctx context.Context, sourceURL, targetURL string, opts mirrorOptions) <-chan URLs { URLsCh := make(chan URLs) go deltaSourceTarget(ctx, sourceURL, targetURL, opts, URLsCh) return URLsCh } minio-client-0.0~20250403/cmd/mv-main.go000066400000000000000000000171451477450377600174500ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "sync" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) // mv command flags. var ( mvFlags = []cli.Flag{ cli.BoolFlag{ Name: "recursive, r", Usage: "move recursively", }, cli.StringFlag{ Name: "older-than", Usage: "move objects older than value in duration string (e.g. 7d10h31s)", }, cli.StringFlag{ Name: "newer-than", Usage: "move objects newer than value in duration string (e.g. 7d10h31s)", }, cli.StringFlag{ Name: "storage-class, sc", Usage: "set storage class for new object(s) on target", }, cli.StringFlag{ Name: "attr", Usage: "add custom metadata for the object", }, cli.BoolFlag{ Name: "preserve, a", Usage: "preserve filesystem attributes (mode, ownership, timestamps)", }, cli.BoolFlag{ Name: "disable-multipart", Usage: "disable multipart upload feature", }, } ) // Move command. var mvCmd = cli.Command{ Name: "mv", Usage: "move objects", Action: mainMove, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(append(mvFlags, encFlags...), globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] SOURCE [SOURCE...] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} ENVIRONMENT VARIABLES: MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key). MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key). EXAMPLES: 01. Move a list of objects from local file system to Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} Music/*.ogg s3/jukebox/ 02. Move a folder recursively from MinIO cloud storage to Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} --recursive play/mybucket/ s3/mybucket/ 03. Move multiple local folders recursively to MinIO cloud storage. {{.Prompt}} {{.HelpName}} --recursive backup/2014/ backup/2015/ play/archive/ 04. Move a bucket recursively from aliased Amazon S3 cloud storage to local filesystem on Windows. {{.Prompt}} {{.HelpName}} --recursive s3\documents\2014\ C:\Backups\2014 05. Move files older than 7 days and 10 hours from MinIO cloud storage to Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} --older-than 7d10h play/mybucket/myfolder/ s3/mybucket/ 06. Move files newer than 7 days and 10 hours from MinIO cloud storage to a local path. {{.Prompt}} {{.HelpName}} --newer-than 7d10h play/mybucket/myfolder/ ~/latest/ 07. Move an object with name containing unicode characters to Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} 本語 s3/andoria/ 08. Move a local folder with space separated characters to Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} --recursive 'workdir/documents/May 2014/' s3/miniocloud 09. Move a list of objects from local file system to MinIO cloud storage with specified metadata, separated by ";" {{.Prompt}} {{.HelpName}} --attr "key1=value1;key2=value2" Music/*.mp4 play/mybucket/ 10. Move a folder recursively from MinIO cloud storage to Amazon S3 cloud storage with Cache-Control and custom metadata, separated by ";". {{.Prompt}} {{.HelpName}} --attr "Cache-Control=max-age=90000,min-fresh=9000;key1=value1;key2=value2" --recursive play/mybucket/myfolder/ s3/mybucket/ 11. Move a text file to an object storage and assign REDUCED_REDUNDANCY storage-class to the uploaded object. {{.Prompt}} {{.HelpName}} --storage-class REDUCED_REDUNDANCY myobject.txt play/mybucket 12. Move a text file to an object storage and preserve the file system attribute as metadata. {{.Prompt}} {{.HelpName}} -a myobject.txt play/mybucket 13. Move a text file to an object storage and disable multipart upload feature. {{.Prompt}} {{.HelpName}} --disable-multipart myobject.txt play/mybucket 14. Move a folder using client provided encryption keys from Amazon S3 to MinIO cloud storage. {{.Prompt}} {{.HelpName}} --r --enc-c "s3/documents/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MBB" --enc-c "myminio/documents/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" s3/documents/ myminio/documents/ 15. Move a folder using specific server managed encryption keys from Amazon S3 to MinIO cloud storage. {{.Prompt}} {{.HelpName}} --r --enc-s3 "s3/documents" --enc-s3 "myminio/documents" s3/documents/ myminio/documents/ `, } type removeClientInfo struct { client Client contentCh chan *ClientContent resultCh <-chan RemoveResult } type removeManager struct { removeMap map[string]*removeClientInfo removeMapMutex sync.RWMutex wg sync.WaitGroup } func (rm *removeManager) readErrors(resultCh <-chan RemoveResult, targetURL string) { rm.wg.Add(1) go func() { defer rm.wg.Done() for result := range resultCh { if result.Err != nil { errorIf(result.Err.Trace(targetURL), "Failed to remove in `%s`.", targetURL) } } }() } // This function should be parallel-safe because it is executed by ParallelManager // If targetAlias is empty, it means we will target local FS contents func (rm *removeManager) add(ctx context.Context, targetAlias, targetURL string) { rm.removeMapMutex.Lock() clientInfo := rm.removeMap[targetAlias] if clientInfo == nil { client, pErr := newClientFromAlias(targetAlias, targetURL) if pErr != nil { errorIf(pErr.Trace(targetURL), "Invalid argument `%s`.", targetURL) return } contentCh := make(chan *ClientContent, 10000) resultCh := client.Remove(ctx, false, false, false, false, contentCh) rm.readErrors(resultCh, targetURL) clientInfo = &removeClientInfo{ client: client, contentCh: contentCh, resultCh: resultCh, } rm.removeMap[targetAlias] = clientInfo } rm.removeMapMutex.Unlock() clientInfo.contentCh <- &ClientContent{URL: *newClientURL(targetURL)} } func (rm *removeManager) close() { for _, clientInfo := range rm.removeMap { close(clientInfo.contentCh) } // Wait until all on-going client.Remove() operations to finish rm.wg.Wait() } var rmManager = &removeManager{ removeMap: make(map[string]*removeClientInfo), } // mainMove is the entry point for mv command. func mainMove(cliCtx *cli.Context) error { ctx, cancelMove := context.WithCancel(globalContext) defer cancelMove() checkCopySyntax(cliCtx) console.SetColor("Copy", color.New(color.FgGreen, color.Bold)) if cliCtx.NArg() == 2 { args := cliCtx.Args() srcURL := args.Get(0) dstURL := args.Get(1) if isURLPrefix(srcURL, dstURL) { fatalIf(errDummy().Trace(), fmt.Sprintf("The source %v and destination %v cannot be subdirectories of each other", srcURL, dstURL)) return nil } } var err *probe.Error encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) fatalIf(err, "Unable to parse encryption keys.") e := doCopySession(ctx, cancelMove, cliCtx, encKeyDB, true) console.Colorize("Copy", "Waiting for move operations to complete") rmManager.close() return e } minio-client-0.0~20250403/cmd/od-main.go000066400000000000000000000120051477450377600174160ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "strings" "time" json "github.com/minio/colorjson" humanize "github.com/dustin/go-humanize" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" ) // make a bucket. var odCmd = cli.Command{ Name: "od", Usage: "measure single stream upload and download", Action: mainOD, Before: setGlobalsFromContext, OnUsageError: onUsageError, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [OPERANDS] OPERANDS: if= source stream to upload of= target path to upload to size= size of each part. If not specified, will be calculated from the source stream size. parts= number of parts to upload. If not specified, will calculated from the source file size. skip= number of parts to skip. {{if .VisibleFlags}} FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} EXAMPLES: 1. Upload 200MiB of a file to a bucket in 5 parts of size 40MiB. {{.HelpName}} if=file.txt of=play/my-bucket/file.txt size=40MiB parts=5 2. Upload a full file to a bucket with 40MiB parts. {{.HelpName}} if=file.txt of=play/my-bucket/file.txt size=40MiB 3. Upload a full file to a bucket in 5 parts. {{.HelpName}} if=file.txt of=play/my-bucket/file.txt parts=5 `, } type odMessage struct { Status string `json:"status"` Type string `json:"type"` Source string `json:"source"` Target string `json:"target"` PartSize uint64 `json:"partSize"` TotalSize int64 `json:"totalSize"` Parts int `json:"parts"` Skip int `json:"skip"` Elapsed int64 `json:"elapsed"` } func (o odMessage) String() string { cleanSize := humanize.IBytes(uint64(o.TotalSize)) elapsed := time.Duration(o.Elapsed) * time.Millisecond speed := humanize.IBytes(uint64(float64(o.TotalSize) / elapsed.Seconds())) if o.Type == "S3toFS" && o.Parts == 0 { return fmt.Sprintf("Transferred: %s, Full file, Time: %s, Speed: %s/s", cleanSize, elapsed, speed) } return fmt.Sprintf("Transferred: %s, Parts: %d, Time: %s, Speed: %s/s", cleanSize, o.Parts, elapsed, speed) } func (o odMessage) JSON() string { odMessageBytes, e := json.MarshalIndent(o, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(odMessageBytes) } // getOdUrls returns the URLs for the object download. func getOdUrls(ctx context.Context, args argKVS) (odURLs URLs, e error) { inFile := args.Get("if") outFile := args.Get("of") // Check if outFile is a folder or a file. opts := prepareCopyURLsOpts{ sourceURLs: []string{inFile}, targetURL: outFile, } copyURLsContent, err := guessCopyURLType(ctx, opts) fatalIf(err, "Unable to guess copy URL type") // Get content of inFile, set up URLs. switch copyURLsContent.copyType { case copyURLsTypeA: odURLs = makeCopyContentTypeA(*copyURLsContent) case copyURLsTypeB: return URLs{}, fmt.Errorf("invalid source path %s, destination cannot be a directory", outFile) default: return URLs{}, fmt.Errorf("invalid source path %s, source cannot be a directory", inFile) } return odURLs, nil } // odCheckType checks if request is a download or upload and calls the appropriate function func odCheckType(ctx context.Context, odURLs URLs, args argKVS) (message, error) { if odURLs.SourceAlias != "" && odURLs.TargetAlias == "" { return odDownload(ctx, odURLs, args) } var odType string if odURLs.SourceAlias == "" && odURLs.TargetAlias != "" { odType = "FStoS3" } else if odURLs.SourceAlias != "" && odURLs.TargetAlias != "" { odType = "S3toS3" } else { odType = "FStoFS" } return odCopy(ctx, odURLs, args, odType) } // mainOd is the entry point for the od command. func mainOD(cliCtx *cli.Context) error { ctx, cancelCopy := context.WithCancel(globalContext) defer cancelCopy() if !cliCtx.Args().Present() { showCommandHelpAndExit(cliCtx, 1) // last argument is exit code } var kvsArgs argKVS for _, arg := range cliCtx.Args() { kv := strings.SplitN(arg, "=", 2) kvsArgs.Set(kv[0], kv[1]) } // Get content from source. odURLs, e := getOdUrls(ctx, kvsArgs) fatalIf(probe.NewError(e), "Unable to get source and target URLs") message, e := odCheckType(ctx, odURLs, kvsArgs) fatalIf(probe.NewError(e), "Unable to transfer object") // Print message. printMsg(message) return nil } minio-client-0.0~20250403/cmd/od-stream.go000066400000000000000000000167351477450377600200030ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "io" "math" "path/filepath" "strconv" "time" humanize "github.com/dustin/go-humanize" ) // odSetSizes sets necessary values for object transfer. func odSetSizes(odURLs URLs, args argKVS) (combinedSize int64, partSize uint64, parts int, skip int64, e error) { // If parts not specified, set to 0, else scan for integer. p := args.Get("parts") if p == "" { parts = 0 } else { parts, e = strconv.Atoi(p) if e != nil { return 0, 0, 0, 0, e } } // Get number of parts to skip, defaults to 0. sk := args.Get("skip") var skipInt int if sk == "" { skipInt = 0 } else { skipInt, e = strconv.Atoi(sk) if e != nil { return 0, 0, 0, 0, e } } sourceSize := odURLs.SourceContent.Size // If neither parts nor size is specified, copy full file in 1 part. s := args.Get("size") if parts <= 1 && s == "" { return sourceSize, uint64(sourceSize), 1, 0, nil } // If size is not specified, calculate the size based on parts and upload full file. if s == "" { partSize = uint64(math.Ceil(float64(sourceSize) / float64(parts))) skip = int64(skipInt) * int64(partSize) parts = parts - skipInt return -1, partSize, parts, skip, nil } partSize, e = humanize.ParseBytes(s) if e != nil { return 0, 0, 0, 0, e } // Convert skipInt to bytes. skip = int64(skipInt) * int64(partSize) // If source has no content, calculate combined size and return. // This is needed for when source is /dev/zero. if sourceSize == 0 { if parts == 0 { parts = 1 } combinedSize = int64(partSize * uint64(parts)) return combinedSize, partSize, parts, skip, nil } // If source is smaller than part size, upload in 1 part. if partSize > uint64(sourceSize) { return sourceSize, uint64(sourceSize), 1, 0, nil } // If parts is not specified, calculate number of parts and upload full file. if parts < 1 { parts = int(math.Ceil(float64(sourceSize)/float64(partSize))) - skipInt return sourceSize, partSize, parts, skip, nil } combinedSize = int64(partSize * uint64(parts)) // If combined size is larger than source after skip, recalculate number of parts. if sourceSize-skip < combinedSize { combinedSize = sourceSize - skip parts = int(math.Ceil(float64(sourceSize)/float64(partSize))) - skipInt } return combinedSize, partSize, parts, skip, nil } // odCopy copies a file/object from local to server, server to server, or local to local. func odCopy(ctx context.Context, odURLs URLs, args argKVS, odType string) (odMessage, error) { // Set sizes. combinedSize, partSize, parts, skip, e := odSetSizes(odURLs, args) if e != nil { return odMessage{}, e } sourceAlias := odURLs.SourceAlias sourceURL := odURLs.SourceContent.URL sourcePath := filepath.ToSlash(filepath.Join(sourceAlias, sourceURL.Path)) targetAlias := odURLs.TargetAlias targetURL := odURLs.TargetContent.URL targetPath := filepath.ToSlash(filepath.Join(targetAlias, targetURL.Path)) getOpts := GetOptions{} // Skip given number of parts. if skip > 0 { getOpts.RangeStart = skip } // Placeholder encryption key database. var encKeyDB map[string][]prefixSSEPair // Create reader from source. reader, err := getSourceStreamFromURL(ctx, sourcePath, encKeyDB, getSourceOpts{GetOptions: getOpts}) fatalIf(err.Trace(sourcePath), "Unable to get source stream") defer reader.Close() putOpts := PutOptions{ storageClass: odURLs.TargetContent.StorageClass, md5: odURLs.MD5, multipartSize: partSize, } // Disable multipart on files too small for multipart upload. if combinedSize < 5242880 && combinedSize > 0 { putOpts.disableMultipart = true } // Used to get transfer time pg := newAccounter(combinedSize) // Write to target. targetClnt, err := newClientFromAlias(targetAlias, targetURL.String()) fatalIf(err.Trace(targetURL.String()), "Unable to initialize target client") // Put object. total, err := targetClnt.PutPart(ctx, reader, combinedSize, pg, putOpts) fatalIf(err.Trace(targetURL.String()), "Unable to upload") // Get upload time. elapsed := time.Since(pg.startTime) message := odMessage{ Status: "success", Type: odType, Source: sourcePath, Target: targetPath, PartSize: partSize, TotalSize: total, Parts: parts, Skip: int(uint64(skip) / partSize), Elapsed: elapsed.Milliseconds(), } return message, nil } // odSetParts sets parts for object download. func odSetParts(args argKVS) (parts, skip int, e error) { if args.Get("size") != "" { return 0, 0, fmt.Errorf("size cannot be specified getting from server") } p := args.Get("parts") if p == "" { return 0, 0, nil } parts, e = strconv.Atoi(p) if e != nil { return 0, 0, e } if parts < 1 { return 0, 0, fmt.Errorf("parts must be at least 1") } sk := args.Get("skip") if sk == "" { skip = 0 } else { skip, e = strconv.Atoi(sk) } if e != nil { return 0, 0, e } return parts, skip, nil } // odDownload copies an object from server to local. func odDownload(ctx context.Context, odURLs URLs, args argKVS) (odMessage, error) { /// Set number of parts to get. parts, skip, e := odSetParts(args) if e != nil { return odMessage{}, e } targetPath := odURLs.TargetContent.URL.Path sourceAlias := odURLs.SourceAlias sourceURL := odURLs.SourceContent.URL sourcePath := filepath.ToSlash(filepath.Join(sourceAlias, sourceURL.Path)) // Get server client. cli, err := newClientFromAlias(sourceAlias, sourceURL.String()) fatalIf(err, "Unable to initialize client") var reader io.Reader if parts == 0 { // Get the full file. reader = singleGet(ctx, cli) } else { // Get the file in parts. reader = multiGet(ctx, cli, parts, skip) } // Accounter to get transfer time. pg := newAccounter(-1) // Upload the file. total, err := putTargetStream(ctx, "", targetPath, "", "", "", reader, -1, pg, PutOptions{}) fatalIf(err.Trace(targetPath), "Unable to upload an object") // Get upload time. elapsed := time.Since(pg.startTime) message := odMessage{ Status: "success", Type: "S3toFS", Source: sourcePath, Target: targetPath, TotalSize: total, Parts: parts, Skip: skip, Elapsed: elapsed.Milliseconds(), } return message, nil } // singleGet helps odDownload download a single part. func singleGet(ctx context.Context, cli Client) io.ReadCloser { reader, err := cli.GetPart(ctx, 0) fatalIf(err, "Unable to download object") return reader } // multiGet helps odDownload download multiple parts. func multiGet(ctx context.Context, cli Client, parts, skip int) io.Reader { var readers []io.Reader // Get reader for each part. for i := 1 + skip; i <= parts; i++ { reader, err := cli.GetPart(ctx, parts) fatalIf(err, "Unable to download part of an object") readers = append(readers, reader) } reader := io.MultiReader(readers...) return reader } minio-client-0.0~20250403/cmd/parallel-manager.go000066400000000000000000000160601477450377600213030ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" "runtime" "strconv" "sync" "sync/atomic" "time" "github.com/minio/minio-go/v7" "github.com/shirou/gopsutil/v3/mem" ) const ( // Maximum number of parallel workers maxParallelWorkers = 128 // Monitor tick to decide to add new workers monitorPeriod = 4 * time.Second ) // Number of workers added per bandwidth monitoring. var defaultWorkerFactor = runtime.GOMAXPROCS(0) // A task is a copy/mirror action that needs to be executed type task struct { // The function to execute in this task fn func() URLs // If set to true, ensure no tasks are // executed in parallel to this one. barrier bool // The total size of the information that we need to upload uploadSize int64 } // ParallelManager - helps manage parallel workers to run tasks type ParallelManager struct { // Calculate sent bytes. // Keep this as first element of struct because it guarantees 64bit // alignment on 32 bit machines. atomic.* functions crash if operand is not // aligned at 64bit. See https://github.com/golang/go/issues/599 sentBytes int64 // Synchronize workers wg *sync.WaitGroup barrierSync sync.RWMutex // Current threads number workersNum uint32 // Channel to receive tasks to run queueCh chan task // Channel to send back results resultCh chan URLs stopMonitorCh chan struct{} // The maximum memory to use maxMem uint64 } // addWorker creates a new worker to process tasks func (p *ParallelManager) addWorker() { if atomic.LoadUint32(&p.workersNum) >= maxParallelWorkers { // Number of maximum workers is reached, no need to // to create a new one. return } // Update number of threads atomic.AddUint32(&p.workersNum, 1) // Start a new worker p.wg.Add(1) go func() { for { // Wait for jobs t, ok := <-p.queueCh if !ok { // No more tasks, quit p.wg.Done() return } // Execute the task and send the result to channel. p.resultCh <- t.fn() if t.barrier { p.barrierSync.Unlock() } else { p.barrierSync.RUnlock() } } }() } func (p *ParallelManager) Read(b []byte) (n int, err error) { atomic.AddInt64(&p.sentBytes, int64(len(b))) return len(b), nil } // monitorProgress monitors realtime transfer speed of data // and increases threads until it reaches a maximum number of // threads or notice there is no apparent enhancement of // transfer speed. func (p *ParallelManager) monitorProgress() { go func() { ticker := time.NewTicker(monitorPeriod) defer ticker.Stop() var prevSentBytes, maxBandwidth int64 var retry int for { select { case <-p.stopMonitorCh: // Ordered to quit immediately return case <-ticker.C: // Compute new bandwidth from counted sent bytes sentBytes := atomic.LoadInt64(&p.sentBytes) bandwidth := sentBytes - prevSentBytes prevSentBytes = sentBytes if bandwidth <= maxBandwidth { retry++ // We still want to add more workers // until we are sure that it is not // useful to add more of them. if retry > 2 { return } } else { retry = 0 maxBandwidth = bandwidth } for i := 0; i < defaultWorkerFactor; i++ { p.addWorker() } } } }() } // Queue task in parallel func (p *ParallelManager) queueTask(fn func() URLs, uploadSize int64) { p.doQueueTask(task{fn: fn, uploadSize: uploadSize}) } // Queue task but ensures that no tasks is running at parallel, // which also means wait until all concurrent tasks finish before // queueing this and execute it solely. func (p *ParallelManager) queueTaskWithBarrier(fn func() URLs, uploadSize int64) { p.doQueueTask(task{fn: fn, barrier: true, uploadSize: uploadSize}) } func (p *ParallelManager) enoughMemForUpload(uploadSize int64) bool { if uploadSize < 0 { panic("unexpected size") } if uploadSize == 0 || p.maxMem == 0 { return true } estimateNeededMemoryForUpload := func(size int64) uint64 { partsCount, partSize, _, e := minio.OptimalPartInfo(size, 0) if e != nil { panic(e) } if partsCount >= 4 { return 4 * uint64(partSize) } return uint64(size) } smem := runtime.MemStats{} if uploadSize > 50<<20 { // GC if upload is bigger than 50MB. runtime.GC() } runtime.ReadMemStats(&smem) return estimateNeededMemoryForUpload(uploadSize)+smem.Alloc < p.maxMem } func (p *ParallelManager) doQueueTask(t task) { // Check if we have enough memory to perform next task, // if not, wait to finish all currents tasks to continue if !p.enoughMemForUpload(t.uploadSize) { t.barrier = true } if t.barrier { p.barrierSync.Lock() } else { p.barrierSync.RLock() } p.queueCh <- t } // Wait for all workers to finish tasks before shutting down Parallel func (p *ParallelManager) stopAndWait() { close(p.queueCh) p.wg.Wait() close(p.stopMonitorCh) } const cgroupLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes" func cgroupLimit(limitFile string) (limit uint64) { buf, e := os.ReadFile(limitFile) if e != nil { return 9223372036854771712 } limit, e = strconv.ParseUint(string(buf), 10, 64) if e != nil { return 9223372036854771712 } return limit } func availableMemory() (available uint64) { available = 4 << 30 // Default to 4 GiB when we can't find the limits. if runtime.GOOS == "linux" { available = cgroupLimit(cgroupLimitFile) // No limit set, It's the highest positive signed 64-bit // integer (2^63-1), rounded down to multiples of 4096 (2^12), // the most common page size on x86 systems - for cgroup_limits. if available != 9223372036854771712 { // This means cgroup memory limit is configured. return } // no-limit set proceed to set the limits based on virtual memory. } // for all other platforms limits are based on virtual memory. memStats, _ := mem.VirtualMemory() if memStats.Available > 0 { available = memStats.Available } // Always use 50% of available memory. available = available / 2 return } // newParallelManager starts new workers waiting for executing tasks func newParallelManager(resultCh chan URLs) *ParallelManager { p := &ParallelManager{ wg: &sync.WaitGroup{}, workersNum: 0, stopMonitorCh: make(chan struct{}), queueCh: make(chan task), resultCh: resultCh, maxMem: availableMemory(), } // Start with runtime.NumCPU(). for i := 0; i < runtime.NumCPU(); i++ { p.addWorker() } // Start monitoring tasks progress p.monitorProgress() return p } minio-client-0.0~20250403/cmd/parse_time_test.go000066400000000000000000000117711477450377600212720ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "testing" ) var parseDurationTests = []struct { in string ok bool want Duration }{ // simple {"0", true, 0}, {"5s", true, 5 * Second}, {"30s", true, 30 * Second}, {"1478s", true, 1478 * Second}, // sign {"-5s", true, -5 * Second}, {"+5s", true, 5 * Second}, {"-0", true, 0}, {"+0", true, 0}, // decimal {"5.0s", true, 5 * Second}, {"5.6s", true, 5*Second + 600*Millisecond}, {"5.s", true, 5 * Second}, {".5s", true, 500 * Millisecond}, {"1.0s", true, 1 * Second}, {"1.00s", true, 1 * Second}, {"1.004s", true, 1*Second + 4*Millisecond}, {"1.0040s", true, 1*Second + 4*Millisecond}, {"100.00100s", true, 100*Second + 1*Millisecond}, // different units {"10ns", true, 10 * Nanosecond}, {"11us", true, 11 * Microsecond}, {"12µs", true, 12 * Microsecond}, // U+00B5 {"12μs", true, 12 * Microsecond}, // U+03BC {"13ms", true, 13 * Millisecond}, {"14s", true, 14 * Second}, {"15m", true, 15 * Minute}, {"16h", true, 16 * Hour}, {"12d", true, 12 * Day}, {"3w", true, 3 * Week}, // composite durations {"3h30m", true, 3*Hour + 30*Minute}, {"10.5s4m", true, 4*Minute + 10*Second + 500*Millisecond}, {"-2m3.4s", true, -(2*Minute + 3*Second + 400*Millisecond)}, {"1h2m3s4ms5us6ns", true, 1*Hour + 2*Minute + 3*Second + 4*Millisecond + 5*Microsecond + 6*Nanosecond}, {"39h9m14.425s", true, 39*Hour + 9*Minute + 14*Second + 425*Millisecond}, {"2w3d12h", true, 2*Week + 3*Day + 12*Hour}, // large value {"52763797000ns", true, 52763797000 * Nanosecond}, // more than 9 digits after decimal point, see https://golang.org/issue/6617 {"0.3333333333333333333h", true, 20 * Minute}, // 9007199254740993 = 1<<53+1 cannot be stored precisely in a float64 {"9007199254740993ns", true, (1<<53 + 1) * Nanosecond}, // largest duration that can be represented by int64 in nanoseconds {"9223372036854775807ns", true, (1<<63 - 1) * Nanosecond}, {"9223372036854775.807us", true, (1<<63 - 1) * Nanosecond}, {"9223372036s854ms775us807ns", true, (1<<63 - 1) * Nanosecond}, // large negative value {"-9223372036854775807ns", true, -1<<63 + 1*Nanosecond}, // errors {"", false, 0}, {"3", false, 0}, {"-", false, 0}, {"s", false, 0}, {".", false, 0}, {"-.", false, 0}, {".s", false, 0}, {"+.s", false, 0}, {"3000000h", false, 0}, // overflow {"9223372036854775808ns", false, 0}, // overflow {"9223372036854775.808us", false, 0}, // overflow {"9223372036854ms775us808ns", false, 0}, // overflow // largest negative value of type int64 in nanoseconds should fail // see https://go-review.googlesource.com/#/c/2461/ {"-9223372036854775808ns", false, 0}, } func TestParseDuration(t *testing.T) { for _, tc := range parseDurationTests { t.Run(tc.in, func(t *testing.T) { d, err := ParseDuration(tc.in) if tc.ok && (err != nil || d != tc.want) { t.Errorf("ParseDuration(%q) = %v, %v, want %v, nil", tc.in, d, err, tc.want) } else if !tc.ok && err == nil { t.Errorf("ParseDuration(%q) = _, nil, want _, non-nil", tc.in) } }) } } // Test for ParseDurationTime. Validates the returned value // for given time value in days, hours and minute format. func TestParseDurationTime(t *testing.T) { testCases := []struct { timeValue string expected Duration err string }{ // Test 1: empty string as input {"", 0, "invalid empty duration"}, // Test 2: Input string contains 4 day, 10 hour and 3 minutes {"4d10h3m", 381780000000000, ""}, // Test 3: Input string contains 10 day and 3 hours {"10d3h", 874800000000000, ""}, // Test 4: Input string contains minutes and days {"3m7d", 604980000000000, ""}, // Test 5: Input string contains unknown unit {"4a3d", 0, "unknown unit a in duration 4a3d"}, // Test 6: Input string contains fractional day {"1.5d", 129600000000000, ""}, // Test 6: Input string contains fractional day and hour {"2.5d1.5h", 221400000000000, ""}, // Test 7: Input string contains fractional day , hour and minute {"2.5d1.5h3.5m", 221610000000000, ""}, } for _, testCase := range testCases { testCase := testCase t.Run("", func(t *testing.T) { myVal, err := ParseDuration(testCase.timeValue) if err != nil && err.Error() != testCase.err { t.Error() } if myVal != testCase.expected { t.Errorf("Expected %v, got %v", testCase.expected, myVal) } }) } } minio-client-0.0~20250403/cmd/ping.go000066400000000000000000000314221477450377600170330ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "math" "net/url" "strconv" "strings" "text/tabwriter" "text/template" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var pingFlags = []cli.Flag{ cli.IntFlag{ Name: "count, c", Usage: "perform liveliness check for count number of times", }, cli.IntFlag{ Name: "error-count, e", Usage: "exit after N consecutive ping errors", }, cli.BoolFlag{ Name: "exit, x", Usage: "exit when server(s) responds and reports being online", }, cli.IntFlag{ Name: "interval, i", Usage: "wait interval between each request in seconds", Value: 1, }, cli.BoolFlag{ Name: "distributed, a", Usage: "ping all the servers in the cluster, use it when you have direct access to nodes/pods", }, } // return latency and liveness probe. var pingCmd = cli.Command{ Name: "ping", Usage: "perform liveness check", Action: mainPing, Before: setGlobalsFromContext, OnUsageError: onUsageError, Flags: append(pingFlags, globalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET...] {{if .VisibleFlags}} FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} EXAMPLES: 1. Return Latency and liveness probe. {{.Prompt}} {{.HelpName}} myminio 2. Return Latency and liveness probe 5 number of times. {{.Prompt}} {{.HelpName}} --count 5 myminio 3. Return Latency and liveness with wait interval set to 30 seconds. {{.Prompt}} {{.HelpName}} --interval 30 myminio 4. Stop pinging when error count > 20. {{.Prompt}} {{.HelpName}} --error-count 20 myminio `, } var stop bool // Validate command line arguments. func checkPingSyntax(cliCtx *cli.Context) { if !cliCtx.Args().Present() { showCommandHelpAndExit(cliCtx, 1) // last argument is exit code } } // JSON jsonified ping result message. func (pr PingResult) JSON() string { statusJSONBytes, e := json.MarshalIndent(pr, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(statusJSONBytes) } var colorMap = template.FuncMap{ "colorWhite": color.New(color.FgWhite).SprintfFunc(), "colorRed": color.New(color.FgRed).SprintfFunc(), } // PingDist is the template for ping result in distributed mode const PingDist = `{{$x := .Counter}}{{range .EndPointsStats}}{{if eq "0 " .CountErr}}{{colorWhite $x}}{{colorWhite ": "}}{{colorWhite .Endpoint.Scheme}}{{colorWhite "://"}}{{colorWhite .Endpoint.Host}}{{if ne "" .Endpoint.Port}}{{colorWhite ":"}}{{colorWhite .Endpoint.Port}}{{end}}{{"\t"}}{{ colorWhite "min="}}{{colorWhite .Min}}{{"\t"}}{{colorWhite "max="}}{{colorWhite .Max}}{{"\t"}}{{colorWhite "average="}}{{colorWhite .Average}}{{"\t"}}{{colorWhite "errors="}}{{colorWhite .CountErr}}{{" "}}{{colorWhite "roundtrip="}}{{colorWhite .Roundtrip}}{{else}}{{colorRed $x}}{{colorRed ": "}}{{colorRed .Endpoint.Scheme}}{{colorRed "://"}}{{colorRed .Endpoint.Host}}{{if ne "" .Endpoint.Port}}{{colorRed ":"}}{{colorRed .Endpoint.Port}}{{end}}{{"\t"}}{{ colorRed "min="}}{{colorRed .Min}}{{"\t"}}{{colorRed "max="}}{{colorRed .Max}}{{"\t"}}{{colorRed "average="}}{{colorRed .Average}}{{"\t"}}{{colorRed "errors="}}{{colorRed .CountErr}}{{" "}}{{colorRed "roundtrip="}}{{colorRed .Roundtrip}}{{end}} {{end}}` // Ping is the template for ping result const Ping = `{{$x := .Counter}}{{range .EndPointsStats}}{{if eq "0 " .CountErr}}{{colorWhite $x}}{{colorWhite ": "}}{{colorWhite .Endpoint.Scheme}}{{colorWhite "://"}}{{colorWhite .Endpoint.Host}}{{if ne "" .Endpoint.Port}}{{colorWhite ":"}}{{colorWhite .Endpoint.Port}}{{end}}{{"\t"}}{{ colorWhite "min="}}{{colorWhite .Min}}{{"\t"}}{{colorWhite "max="}}{{colorWhite .Max}}{{"\t"}}{{colorWhite "average="}}{{colorWhite .Average}}{{"\t"}}{{colorWhite "errors="}}{{colorWhite .CountErr}}{{" "}}{{colorWhite "roundtrip="}}{{colorWhite .Roundtrip}}{{else}}{{colorRed $x}}{{colorRed ": "}}{{colorRed .Endpoint.Scheme}}{{colorRed "://"}}{{colorRed .Endpoint.Host}}{{if ne "" .Endpoint.Port}}{{colorRed ":"}}{{colorRed .Endpoint.Port}}{{end}}{{"\t"}}{{ colorRed "min="}}{{colorRed .Min}}{{"\t"}}{{colorRed "max="}}{{colorRed .Max}}{{"\t"}}{{colorRed "average="}}{{colorRed .Average}}{{"\t"}}{{colorRed "errors="}}{{colorRed .CountErr}}{{" "}}{{colorRed "roundtrip="}}{{colorRed .Roundtrip}}{{end}}{{end}}` // PingTemplateDist - captures ping template var PingTemplateDist = template.Must(template.New("ping-list").Funcs(colorMap).Parse(PingDist)) // PingTemplate - captures ping template var PingTemplate = template.Must(template.New("ping-list").Funcs(colorMap).Parse(Ping)) // String colorized service status message. func (pr PingResult) String() string { var s strings.Builder w := tabwriter.NewWriter(&s, 1, 8, 3, ' ', 0) var e error if len(pr.EndPointsStats) > 1 { e = PingTemplateDist.Execute(w, pr) } else { e = PingTemplate.Execute(w, pr) } fatalIf(probe.NewError(e), "Unable to initialize template writer") w.Flush() return s.String() } // EndPointStats - container to hold server ping stats type EndPointStats struct { Endpoint *url.URL `json:"endpoint"` Min string `json:"min"` Max string `json:"max"` Average string `json:"average"` DNS string `json:"dns"` CountErr string `json:"error-count,omitempty"` Error string `json:"error,omitempty"` Roundtrip string `json:"roundtrip"` } // PingResult contains ping output type PingResult struct { Status string `json:"status"` Counter string `json:"counter"` EndPointsStats []EndPointStats `json:"servers"` } type serverStats struct { min uint64 max uint64 sum uint64 avg uint64 dns uint64 // last DNS resolving time errorCount int // used to keep a track of consecutive errors err string counter int // used to find the average, acts as denominator } func fetchAdminInfo(admClnt *madmin.AdminClient) (madmin.InfoMessage, error) { ctx, cancel := context.WithTimeout(globalContext, 3*time.Second) // Fetch the service status of the specified MinIO server info, e := admClnt.ServerInfo(ctx) cancel() if e == nil { return info, nil } timer := time.NewTimer(time.Second) defer timer.Stop() for { select { case <-globalContext.Done(): return madmin.InfoMessage{}, globalContext.Err() case <-timer.C: ctx, cancel := context.WithTimeout(globalContext, 3*time.Second) info, e := admClnt.ServerInfo(ctx) cancel() if e == nil { return info, nil } timer.Reset(time.Second) } } } func ping(ctx context.Context, cliCtx *cli.Context, anonClient *madmin.AnonymousClient, admInfo madmin.InfoMessage, endPointMap map[string]serverStats, index int) { var endPointStats []EndPointStats var servers []madmin.ServerProperties if cliCtx.Bool("distributed") { servers = admInfo.Servers } allOK := true for result := range anonClient.Alive(ctx, madmin.AliveOpts{}, servers...) { stat := pingStats(cliCtx, result, endPointMap) allOK = allOK && result.Online endPointStat := EndPointStats{ Endpoint: result.Endpoint, Min: trimToTwoDecimal(time.Duration(stat.min)), Max: trimToTwoDecimal(time.Duration(stat.max)), Average: trimToTwoDecimal(time.Duration(stat.avg)), DNS: time.Duration(stat.dns).String(), CountErr: pad(strconv.Itoa(stat.errorCount), " ", 3-len(strconv.Itoa(stat.errorCount)), false), Error: stat.err, Roundtrip: trimToTwoDecimal(result.ResponseTime), } endPointStats = append(endPointStats, endPointStat) endPointMap[result.Endpoint.Host] = stat } stop = stop || cliCtx.Bool("exit") && allOK printMsg(PingResult{ Status: "success", Counter: pad(strconv.Itoa(index), " ", 3-len(strconv.Itoa(index)), true), EndPointsStats: endPointStats, }) if !stop { time.Sleep(time.Duration(cliCtx.Int("interval")) * time.Second) } } func trimToTwoDecimal(d time.Duration) string { var f float64 var unit string switch { case d >= time.Second: f = float64(d) / float64(time.Second) unit = pad("s", " ", 7-len(fmt.Sprintf("%.02f", f)), false) default: f = float64(d) / float64(time.Millisecond) unit = pad("ms", " ", 6-len(fmt.Sprintf("%.02f", f)), false) } return fmt.Sprintf("%.02f%s", f, unit) } // pad adds the `count` number of p string to string s. left true adds to the // left and vice-versa. This is done for proper alignment of ping command // ex:- padding 2 white space to right '90.18s' - > '90.18s ' func pad(s, p string, count int, left bool) string { ret := make([]byte, len(p)*count+len(s)) if left { b := ret[:len(p)*count] bp := copy(b, p) for bp < len(b) { copy(b[bp:], b[:bp]) bp *= 2 } copy(ret[len(b):], s) } else { b := ret[len(s) : len(p)*count+len(s)] bp := copy(b, p) for bp < len(b) { copy(b[bp:], b[:bp]) bp *= 2 } copy(ret[:len(s)], s) } return string(ret) } func pingStats(cliCtx *cli.Context, result madmin.AliveResult, serverMap map[string]serverStats) serverStats { var errorString string var sum, avg, dns uint64 minPing := uint64(math.MaxUint64) var maxPing uint64 var counter, errorCount int if result.Error != nil { errorString = result.Error.Error() if stat, ok := serverMap[result.Endpoint.Host]; ok { minPing = stat.min maxPing = stat.max sum = stat.sum counter = stat.counter avg = stat.avg errorCount = stat.errorCount + 1 } else { minPing = 0 errorCount = 1 } if cliCtx.IsSet("error-count") && errorCount >= cliCtx.Int("error-count") { stop = true } } else { // reset consecutive error count errorCount = 0 if stat, ok := serverMap[result.Endpoint.Host]; ok { var minVal uint64 if stat.min == 0 { minVal = uint64(result.ResponseTime) } else { minVal = stat.min } minPing = uint64(math.Min(float64(minVal), float64(uint64(result.ResponseTime)))) maxPing = uint64(math.Max(float64(stat.max), float64(uint64(result.ResponseTime)))) sum = stat.sum + uint64(result.ResponseTime.Nanoseconds()) counter = stat.counter + 1 } else { minPing = uint64(math.Min(float64(minPing), float64(uint64(result.ResponseTime)))) maxPing = uint64(math.Max(float64(maxPing), float64(uint64(result.ResponseTime)))) sum = uint64(result.ResponseTime) counter = 1 } avg = sum / uint64(counter) dns = uint64(result.DNSResolveTime.Nanoseconds()) } return serverStats{minPing, maxPing, sum, avg, dns, errorCount, errorString, counter} } // mainPing is entry point for ping command. func mainPing(cliCtx *cli.Context) error { // check 'ping' cli arguments. checkPingSyntax(cliCtx) console.SetColor("Info", color.New(color.FgGreen, color.Bold)) console.SetColor("InfoFail", color.New(color.FgRed, color.Bold)) ctx, cancel := context.WithCancel(globalContext) defer cancel() aliasedURL := cliCtx.Args().Get(0) admClient, err := newAdminClient(aliasedURL) fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client for `"+aliasedURL+"`.") anonClient, err := newAnonymousClient(aliasedURL) fatalIf(err.Trace(aliasedURL), "Unable to initialize anonymous client for `"+aliasedURL+"`.") var admInfo madmin.InfoMessage if cliCtx.Bool("distributed") { var e error admInfo, e = fetchAdminInfo(admClient) fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to get server info") } // map to contain server stats for all the servers serverMap := make(map[string]serverStats) index := 1 if cliCtx.IsSet("count") { count := cliCtx.Int("count") if count < 1 { fatalIf(errInvalidArgument().Trace(cliCtx.Args()...), "ping count cannot be less than 1") } for index <= count { // return if consecutive error count more then specified value if stop { return nil } ping(ctx, cliCtx, anonClient, admInfo, serverMap, index) index++ } } else { for { select { case <-globalContext.Done(): return globalContext.Err() default: // return if consecutive error count more then specified value if stop { return nil } ping(ctx, cliCtx, anonClient, admInfo, serverMap, index) index++ } } } return nil } minio-client-0.0~20250403/cmd/pipe-main.go000066400000000000000000000167631477450377600177700ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "io" "os" "runtime/debug" "syscall" "github.com/dustin/go-humanize" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) func defaultPartSize() string { _, partSize, _, _ := minio.OptimalPartInfo(-1, 0) return humanize.IBytes(uint64(partSize)) } var pipeFlags = []cli.Flag{ cli.StringFlag{ Name: "storage-class, sc", Usage: "set storage class for new object(s) on target", }, cli.StringFlag{ Name: "attr", Usage: "add custom metadata for the object", }, cli.StringFlag{ Name: "tags", Usage: "apply one or more tags to the uploaded objects", }, cli.IntFlag{ Name: "concurrent", Value: 1, Usage: "allow N concurrent uploads [WARNING: will use more memory use it with caution]", }, cli.StringFlag{ Name: "part-size", Value: defaultPartSize(), Usage: "customize chunk size for each concurrent upload", }, cli.IntFlag{ Name: "pipe-max-size", Usage: "increase the pipe buffer size to a custom value", Hidden: true, }, checksumFlag, } // Display contents of a file. var pipeCmd = cli.Command{ Name: "pipe", Usage: "stream STDIN to an object", Action: mainPipe, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(append(pipeFlags, encFlags...), globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] [TARGET] {{if .VisibleFlags}} FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} ENVIRONMENT VARIABLES: MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key). MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key). EXAMPLES: 1. Write contents of stdin to a file on local filesystem. {{.Prompt}} {{.HelpName}} /tmp/hello-world.go 2. Write contents of stdin to an object on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} s3/personalbuck/meeting-notes.txt 3. Copy an ISO image to an object on Amazon S3 cloud storage. {{.Prompt}} cat debian-8.2.iso | {{.HelpName}} s3/opensource-isos/gnuos.iso 4. Copy an ISO image to an object on minio storage using KMS encryption. {{.Prompt}} cat debian-8.2.iso | {{.HelpName}} --enc-kms="minio/opensource-isos=my-key-name" minio/opensource-isos/gnuos.iso 5. Stream MySQL database dump to Amazon S3 directly. {{.Prompt}} mysqldump -u root -p ******* accountsdb | {{.HelpName}} s3/sql-backups/backups/accountsdb-oct-9-2015.sql 6. Write contents of stdin to an object on Amazon S3 cloud storage and assign REDUCED_REDUNDANCY storage-class to the uploaded object. {{.Prompt}} {{.HelpName}} --storage-class REDUCED_REDUNDANCY s3/personalbuck/meeting-notes.txt 7. Copy to MinIO cloud storage with specified metadata, separated by ";" {{.Prompt}} cat music.mp3 | {{.HelpName}} --attr "Cache-Control=max-age=90000,min-fresh=9000;Artist=Unknown" play/mybucket/music.mp3 8. Set tags to the uploaded objects {{.Prompt}} tar cvf - . | {{.HelpName}} --tags "category=prod&type=backup" play/mybucket/backup.tar `, } // pipeMessage container for pipe messages type pipeMessage struct { Status string `json:"status"` Target string `json:"target"` Size int64 `json:"size"` } // String colorized pipe message func (p pipeMessage) String() string { return console.Colorize("Pipe", fmt.Sprintf("%d bytes -> `%s`", p.Size, p.Target)) } // JSON jsonified pipe message func (p pipeMessage) JSON() string { p.Status = "success" pipeMessageBytes, e := json.MarshalIndent(p, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(pipeMessageBytes) } func pipe(ctx *cli.Context, targetURL string, encKeyDB map[string][]prefixSSEPair, meta map[string]string, quiet bool, json bool) *probe.Error { // If possible increase the pipe buffer size if e := increasePipeBufferSize(os.Stdin, ctx.Int("pipe-max-size")); e != nil { fatalIf(probe.NewError(e), "Unable to increase custom pipe-max-size") } if targetURL == "" { // When no target is specified, pipe cat's stdin to stdout. return catOut(os.Stdin, -1).Trace() } md5, checksum := parseChecksum(ctx) storageClass := ctx.String("storage-class") alias, _ := url2Alias(targetURL) sseKey := getSSE(targetURL, encKeyDB[alias]) multipartThreads := ctx.Int("concurrent") if multipartThreads > 1 { // We will be allocating large buffers, reduce default GC overhead debug.SetGCPercent(20) } var multipartSize uint64 var e error if partSizeStr := ctx.String("part-size"); partSizeStr != "" { multipartSize, e = humanize.ParseBytes(partSizeStr) if e != nil { return probe.NewError(e) } } // Stream from stdin to multiple objects until EOF. // Ignore size, since os.Stat() would not return proper size all the time // for local filesystem for example /proc files. opts := PutOptions{ sse: sseKey, storageClass: storageClass, metadata: meta, multipartSize: multipartSize, multipartThreads: uint(multipartThreads), concurrentStream: ctx.IsSet("concurrent"), md5: md5, checksum: checksum, } var reader io.Reader if !quiet && !json { pg := newProgressBar(0) reader = io.TeeReader(os.Stdin, pg) } else { reader = os.Stdin } n, err := putTargetStreamWithURL(targetURL, reader, -1, opts) // TODO: See if this check is necessary. switch e := err.ToGoError().(type) { case *os.PathError: if e.Err == syscall.EPIPE { // stdin closed by the user. Gracefully exit. return nil } case nil: printMsg(pipeMessage{ Target: targetURL, Size: n, }) } return err.Trace(targetURL) } // checkPipeSyntax - validate arguments passed by user func checkPipeSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code. } } // mainPipe is the main entry point for pipe command. func mainPipe(ctx *cli.Context) error { // validate pipe input arguments. checkPipeSyntax(ctx) encKeyDB, err := validateAndCreateEncryptionKeys(ctx) fatalIf(err, "Unable to parse encryption keys.") // globalQuiet is true for no window size to get. // We just need --quiet and --json here. quiet := ctx.Bool("quiet") json := ctx.Bool("json") meta := map[string]string{} if attr := ctx.String("attr"); attr != "" { meta, err = getMetaDataEntry(attr) fatalIf(err.Trace(attr), "Unable to parse --attr value") } if tags := ctx.String("tags"); tags != "" { meta["X-Amz-Tagging"] = tags } if len(ctx.Args()) == 0 { err = pipe(ctx, "", nil, meta, quiet, json) fatalIf(err.Trace("stdout"), "Unable to write to one or more targets.") } else { // extract URLs. URLs := ctx.Args() err = pipe(ctx, URLs[0], encKeyDB, meta, quiet, json) fatalIf(err.Trace(URLs[0]), "Unable to write to one or more targets.") } // Done. return nil } minio-client-0.0~20250403/cmd/pipe_supported.go000066400000000000000000000033561477450377600211450ustar00rootroot00000000000000//go:build linux // Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "os" "strconv" "strings" "golang.org/x/sys/unix" ) const pipeMaxSizeProcFile = "/proc/sys/fs/pipe-max-size" func setPipeSize(fd uintptr, size int) error { _, err := unix.FcntlInt(fd, unix.F_SETPIPE_SZ, size) return err } func getConfiguredMaxPipeSize() (int, error) { b, err := os.ReadFile(pipeMaxSizeProcFile) if err != nil { return 0, err } maxSize, err := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64) if err != nil { return 0, fmt.Errorf("error parsing %s content: %v", pipeMaxSizeProcFile, err) } return int(maxSize), nil } // increasePipeBufferSize attempts to increase the pipe size to the the input value // or system-max, if the provided size is 0 or less. func increasePipeBufferSize(f *os.File, desiredPipeSize int) error { fd := f.Fd() if desiredPipeSize <= 0 { maxSize, err := getConfiguredMaxPipeSize() if err == nil { setPipeSize(fd, maxSize) return nil } } return setPipeSize(fd, desiredPipeSize) } minio-client-0.0~20250403/cmd/pipe_unsupported.go000066400000000000000000000016371477450377600215100ustar00rootroot00000000000000//go:build !linux // Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "os" func increasePipeBufferSize(_ *os.File, _ int) error { // this is not supported on non-Linux platforms. return nil } minio-client-0.0~20250403/cmd/pipechan.go000066400000000000000000000061321477450377600176650ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/rjeczalik/notify" // Dynamically sized logical channel: a pipe which never blocks even when // it receives too many elements. Memory consumption is increased and decresed // on the fly following the number of elements. Here is the benchmark which // compares a regular channel which a large capacity (like 1 Million) in // contrast to a PipeChan with an realtime increasing/decreasing capacity. // // BenchmarkRegular1M-4 200 172809399 ns/op // BenchmarkPipeChan1M-4 100 316668338 ns/op // BenchmarkRegular100K-4 2000 16540648 ns/op // BenchmarkPipeChan100K-4 1000 31905966 ns/op // BenchmarkRegular10K-4 20000 1637665 ns/op // BenchmarkPipeChan10K-4 10000 3324329 ns/op // BenchmarkRegular1K-4 200000 168512 ns/op // BenchmarkPipeChan1K-4 100000 550623 ns/op // PipeChan builds a new dynamically sized channel func PipeChan(capacity int) (inputCh, outputCh chan notify.EventInfo) { // A set of channels which store all elements received from input channels := make(chan chan notify.EventInfo, 1000) inputCh = make(chan notify.EventInfo, capacity) // A goroutine which receives elements from inputCh and creates // new channels when needed. go func() { // Create the first channel currCh := make(chan notify.EventInfo, capacity) channels <- currCh for elem := range inputCh { // Prepare next channel with a double capacity when // half of the current channel is already filled. if len(currCh) >= cap(currCh)/2 { close(currCh) currCh = make(chan notify.EventInfo, cap(currCh)*2) channels <- currCh } // Prepare next channel with half capacity when // current channel is 1/4 filled if len(currCh) >= capacity && len(currCh) <= cap(currCh)/4 { close(currCh) currCh = make(chan notify.EventInfo, cap(currCh)/2) channels <- currCh } // Send element to current channel currCh <- elem } close(currCh) close(channels) }() // Copy elements from infinite channel set to the output outputCh = make(chan notify.EventInfo, capacity) go func() { for { currCh, ok := <-channels if !ok { break } for v := range currCh { outputCh <- v } } close(outputCh) }() return inputCh, outputCh } minio-client-0.0~20250403/cmd/pipechan_test.go000066400000000000000000000057711477450377600207340ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "sync" "testing" "github.com/rjeczalik/notify" ) func testPipeChan(inputCh, outputCh chan notify.EventInfo, totalMsgs int) error { var wg sync.WaitGroup msgCtnt := notify.EventInfo(nil) // Send messages up to totalMsgs wg.Add(1) go func() { defer wg.Done() for i := 0; i < totalMsgs; i++ { inputCh <- msgCtnt } close(inputCh) }() // Goroutine to receive and check messages var recvMsgs int var recvErr error wg.Add(1) go func() { defer wg.Done() for msg := range outputCh { recvMsgs++ if msg != msgCtnt { recvErr = fmt.Errorf("Corrupted message, expected = `%s`, found = `%s`", msgCtnt, msg) return } } }() // Wait until we finish sending and receiving messages wg.Wait() if recvErr != nil { return recvErr } // Check if all messages are received if recvMsgs != totalMsgs { return fmt.Errorf("unable to receive all messages, expected: %d, lost: %d", totalMsgs, totalMsgs-recvMsgs) } return nil } // Testing code func TestPipeChannel(t *testing.T) { fastCh, slowCh := PipeChan(1000) err := testPipeChan(fastCh, slowCh, 10*1000) if err != nil { t.Errorf("ERR: %v\n", err) } } func TestRegularChannel(t *testing.T) { ch := make(chan notify.EventInfo, 1000) err := testPipeChan(ch, ch, 10*1000) if err != nil { t.Errorf("ERR: %v\n", err) } } // Benchmark code func benchmarkRegular(b *testing.B, msgsNum int) { for n := 0; n < b.N; n++ { ch := make(chan notify.EventInfo, msgsNum) testPipeChan(ch, ch, msgsNum) } } func benchmarkPipeChan(b *testing.B, msgsNum int) { for n := 0; n < b.N; n++ { fastCh, slowCh := PipeChan(1000) testPipeChan(fastCh, slowCh, msgsNum) } } func BenchmarkRegular1M(b *testing.B) { benchmarkRegular(b, 1*1000*1000) } func BenchmarkPipeChan1M(b *testing.B) { benchmarkPipeChan(b, 1*1000*1000) } func BenchmarkRegular100K(b *testing.B) { benchmarkRegular(b, 100*1000) } func BenchmarkPipeChan100K(b *testing.B) { benchmarkPipeChan(b, 100*1000) } func BenchmarkRegular10K(b *testing.B) { benchmarkRegular(b, 10*1000) } func BenchmarkPipeChan10K(b *testing.B) { benchmarkPipeChan(b, 10*1000) } func BenchmarkRegular1K(b *testing.B) { benchmarkRegular(b, 1*1000) } func BenchmarkPipeChan1K(b *testing.B) { benchmarkPipeChan(b, 1*1000) } minio-client-0.0~20250403/cmd/policy-main.go000066400000000000000000000026431477450377600203220ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" "github.com/minio/pkg/v3/console" ) var policyFlags = []cli.Flag{ cli.BoolFlag{ Name: "recursive, r", Usage: "list recursively", }, } // Manage anonymous access to buckets and objects. var policyCmd = cli.Command{ Name: "policy", Usage: "manage anonymous access to buckets and objects", Action: mainPolicy, Hidden: true, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(policyFlags, globalFlags...), CustomHelpTemplate: `Please use 'mc anonymous' `, } func mainPolicy(_ *cli.Context) error { console.Infoln("Please use 'mc anonymous'") return nil } minio-client-0.0~20250403/cmd/pretty-record.go000066400000000000000000000043371477450377600207060ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "github.com/minio/pkg/v3/console" ) // Row specifies row description and theme type Row struct { desc string descTheme string } // PrettyRecord - an easy struct to format a set of key-value // pairs into a record type PrettyRecord struct { rows []Row indent int maxLen int } // newPrettyRecord - creates a new pretty record func newPrettyRecord(indent int, rows ...Row) PrettyRecord { maxDescLen := 0 for _, r := range rows { if len(r.desc) > maxDescLen { maxDescLen = len(r.desc) } } return PrettyRecord{ rows: rows, indent: indent, maxLen: maxDescLen, } } // buildRecord - creates a string which represents a record table given // some fields contents. func (t PrettyRecord) buildRecord(contents ...string) (line string) { // totalRows is the minimum of the number of fields config // and the number of contents elements. totalRows := len(contents) if len(t.rows) < totalRows { totalRows = len(t.rows) } var format, separator string // Format fields and construct message for i := 0; i < totalRows; i++ { // default heading indent := 0 format = "%s\n" // optionally indented rows with key value pairs if i > 0 { indent = t.indent format = fmt.Sprintf("%%%ds%%-%ds : %%s\n", indent, t.maxLen) line += console.Colorize(t.rows[i].descTheme, fmt.Sprintf(format, separator, t.rows[i].desc, contents[i])) } else { line += console.Colorize(t.rows[i].descTheme, fmt.Sprintf(format, contents[i])) } } return } minio-client-0.0~20250403/cmd/pretty-table.go000066400000000000000000000046371477450377600205220ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "github.com/minio/pkg/v3/console" ) // Field configuration: color theme and max content length type Field struct { colorTheme string maxLen int } // PrettyTable - an easy struct to format a set of line type PrettyTable struct { cols []Field separator string } // newPrettyTable - creates a new pretty table func newPrettyTable(separator string, cols ...Field) PrettyTable { return PrettyTable{ cols: cols, separator: separator, } } // buildRow - creates a string which represents a line table given // some fields contents. func (t PrettyTable) buildRow(contents ...string) (line string) { dots := "..." // totalColumns is the minimum of the number of fields config // and the number of contents elements. totalColumns := len(contents) if len(t.cols) < totalColumns { totalColumns = len(t.cols) } // Format fields and construct message for i := 0; i < totalColumns; i++ { // Default field format without pretty effect fieldContent := "" fieldFormat := "%s" if t.cols[i].maxLen >= 0 { // Override field format fieldFormat = fmt.Sprintf("%%-%d.%ds", t.cols[i].maxLen, t.cols[i].maxLen) // Cut field string and add '...' if length is greater than maxLen if len(contents[i]) > t.cols[i].maxLen { fieldContent = contents[i][:t.cols[i].maxLen-len(dots)] + dots } else { fieldContent = contents[i] } } else { fieldContent = contents[i] } // Add separator if this is not the last column if i < totalColumns-1 { fieldFormat += t.separator } // Add the field to the resulted message line += console.Colorize(t.cols[i].colorTheme, fmt.Sprintf(fieldFormat, fieldContent)) } return } minio-client-0.0~20250403/cmd/pretty-table_test.go000066400000000000000000000040131477450377600215450ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "testing" // TestPrettyTable - testing the behavior of the pretty table module func TestPrettyTable(t *testing.T) { testCases := []struct { sep string tf []Field contents []string expectedRow string }{ // Test 1: empty parameter, empty table {"", []Field{}, []string{}, ""}, // Test 2: one field, without any specific customization {"", []Field{{"", -1}}, []string{"abcd"}, "abcd"}, // Test 3: one field, without 5 chars len {"", []Field{{"", 5}}, []string{"my-long-field"}, "my..."}, // Test 4: one separator, one content {" | ", []Field{{"", -1}}, []string{"abcd"}, "abcd"}, // Test 5: one separtor, multiple contents {" | ", []Field{{"", -1}, {"", -1}, {"", -1}}, []string{"column1", "column2", "column3"}, "column1 | column2 | column3"}, // Test 6: multiple fields {" | ", []Field{{"", 5}, {"", -1}}, []string{"144550032", "my long content that should not be cut"}, "14... | my long content that should not be cut"}, } for idx, testCase := range testCases { tb := newPrettyTable(testCase.sep, testCase.tf...) row := tb.buildRow(testCase.contents...) if row != testCase.expectedRow { t.Fatalf("Test %d: generated row not matching, expected = `%s`, found = `%s`", idx+1, testCase.expectedRow, row) } } } minio-client-0.0~20250403/cmd/print.go000066400000000000000000000027261477450377600172370ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "encoding/json" "strings" "github.com/minio/pkg/v3/console" ) // message interface for all structured messages implementing JSON(), String() methods. type message interface { JSON() string String() string } // printMsg prints message string or JSON structure depending on the type of output console. func printMsg(msg message) { var msgStr string if !globalJSON { msgStr = msg.String() } else { msgStr = msg.JSON() if globalJSONLine && strings.ContainsRune(msgStr, '\n') { // Reformat. var dst bytes.Buffer if err := json.Compact(&dst, []byte(msgStr)); err == nil { msgStr = dst.String() } } } msgStr = strings.TrimSuffix(msgStr, "\n") console.Println(msgStr) } minio-client-0.0~20250403/cmd/profiling.go000066400000000000000000000063651477450377600200770ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "errors" "os" "path" "runtime" "runtime/pprof" "time" ) type profiler interface { Start() error Stop() error } type cpuProfiler struct { *os.File } func newCPUProfiler(f *os.File) *cpuProfiler { return &cpuProfiler{File: f} } func (p *cpuProfiler) Start() error { return pprof.StartCPUProfile(p.File) } func (p *cpuProfiler) Stop() error { pprof.StopCPUProfile() return p.Close() } type memProfiler struct { *os.File } func newMemProfiler(f *os.File) *memProfiler { return &memProfiler{File: f} } func (p *memProfiler) Start() error { return nil } func (p *memProfiler) Stop() error { runtime.GC() if e := pprof.Lookup("heap").WriteTo(p.File, 0); e != nil { return e } return p.Close() } type blockProfiler struct { *os.File } func newBlockProfiler(f *os.File) *blockProfiler { return &blockProfiler{File: f} } func (p *blockProfiler) Start() error { runtime.SetBlockProfileRate(100) return nil } func (p *blockProfiler) Stop() error { if e := pprof.Lookup("block").WriteTo(p.File, 0); e != nil { return e } runtime.SetBlockProfileRate(0) return p.Close() } type goroutineProfiler struct { *os.File } func newGoroutineProfiler(f *os.File) *goroutineProfiler { return &goroutineProfiler{File: f} } func (p *goroutineProfiler) Start() error { return nil } func (p *goroutineProfiler) Stop() error { if e := pprof.Lookup("goroutine").WriteTo(p.File, 1); e != nil { return e } return p.Close() } var globalProfilers []profiler // Enable profiling supported modes are [cpu, mem, block, goroutine]. func enableProfilers(outputFolder string, profilers []string) error { now := time.Now().Format("2006-01-02T15-04-05") if _, e := os.Stat(outputFolder); e != nil { if e := os.MkdirAll(outputFolder, 0o700); e != nil { return e } } for _, profilerName := range profilers { outputFile := path.Join(outputFolder, profilerName+"."+now) f, e := os.Create(outputFile) if e != nil { return e } var p profiler switch profilerName { case "cpu": p = newCPUProfiler(f) case "mem": p = newMemProfiler(f) case "block": p = newBlockProfiler(f) case "goroutine": p = newGoroutineProfiler(f) default: return errors.New("unknown profiler name") } if e := p.Start(); e != nil { return e } // Keep the profiler in a list to stop it later globalProfilers = append(globalProfilers, p) } return nil } func stopProfiling() error { for _, p := range globalProfilers { if e := p.Stop(); e != nil { return e } } return nil } minio-client-0.0~20250403/cmd/progress-bar.go000066400000000000000000000105651477450377600205110ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "io" "runtime" "strings" "time" "github.com/cheggaaa/pb" "github.com/fatih/color" "github.com/minio/pkg/v3/console" ) // progress extender. type progressBar struct { *pb.ProgressBar } func newPB(total int64) *pb.ProgressBar { // Progress bar specific theme customization. console.SetColor("Bar", color.New(color.FgGreen, color.Bold)) // get the new original progress bar. bar := pb.New64(total) // Set new human friendly print units. bar.SetUnits(pb.U_BYTES) // Refresh rate for progress bar is set to 125 milliseconds. bar.SetRefreshRate(time.Millisecond * 125) // Do not print a newline by default handled, it is handled manually. bar.NotPrint = true // Show current speed is true. bar.ShowSpeed = true // Custom callback with colorized bar. bar.Callback = func(s string) { console.Print(console.Colorize("Bar", "\r"+s)) } // Use different unicodes for Linux, OS X and Windows. switch runtime.GOOS { case "linux": // Need to add '\x00' as delimiter for unicode characters. bar.Format("┃\x00▓\x00█\x00░\x00┃") case "darwin": // Need to add '\x00' as delimiter for unicode characters. bar.Format(" \x00▓\x00 \x00░\x00 ") default: // Default to non unicode characters. bar.Format("[=> ]") } // Start the progress bar. return bar.Start() } func newProgressReader(r io.Reader, caption string, total int64) *pb.Reader { bar := newPB(total) if caption != "" { bar.Prefix(caption) } return bar.NewProxyReader(r) } // newProgressBar - instantiate a progress bar. func newProgressBar(total int64) *progressBar { bar := newPB(total) // Return new progress bar here. return &progressBar{ProgressBar: bar} } // Set caption. func (p *progressBar) SetCaption(caption string) *progressBar { caption = fixateBarCaption(caption, getFixedWidth(p.GetWidth(), 18)) p.Prefix(caption) return p } func (p *progressBar) Finish() { p.ProgressBar.Finish() } func (p *progressBar) Set64(length int64) *progressBar { p.ProgressBar = p.ProgressBar.Set64(length) return p } func (p *progressBar) Read(buf []byte) (n int, err error) { defer func() { // Upload retry can read one object twice; Avoid read to be greater than Total if n, t := p.Get(), p.Total; t > 0 && n > t { p.ProgressBar.Set64(t) } }() return p.ProgressBar.Read(buf) } func (p *progressBar) SetTotal(total int64) { p.Total = total } // cursorAnimate - returns a animated rune through read channel for every read. func cursorAnimate() <-chan string { cursorCh := make(chan string) var cursors []string switch runtime.GOOS { case "linux": // cursors = "➩➪➫➬➭➮➯➱" // cursors = "▁▃▄▅▆▇█▇▆▅▄▃" cursors = []string{"◐", "◓", "◑", "◒"} // cursors = "←↖↑↗→↘↓↙" // cursors = "◴◷◶◵" // cursors = "◰◳◲◱" // cursors = "⣾⣽⣻⢿⡿⣟⣯⣷" case "darwin": cursors = []string{"◐", "◓", "◑", "◒"} default: cursors = []string{"|", "/", "-", "\\"} } go func() { for { for _, cursor := range cursors { cursorCh <- cursor } } }() return cursorCh } // fixateBarCaption - fancify bar caption based on the terminal width. func fixateBarCaption(caption string, width int) string { switch { case len(caption) > width: // Trim caption to fit within the screen trimSize := len(caption) - width + 3 if trimSize < len(caption) { caption = "..." + caption[trimSize:] } case len(caption) < width: caption += strings.Repeat(" ", width-len(caption)) } return caption } // getFixedWidth - get a fixed width based for a given percentage. func getFixedWidth(width, percent int) int { return width * percent / 100 } minio-client-0.0~20250403/cmd/put-main.go000066400000000000000000000142501477450377600176300ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "strconv" "strings" "github.com/dustin/go-humanize" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) // put command flags. var ( putFlags = []cli.Flag{ checksumFlag, cli.IntFlag{ Name: "parallel, P", Usage: "upload number of parts in parallel", Value: 4, }, cli.StringFlag{ Name: "part-size, s", Usage: "each part size", Value: "16MiB", }, cli.BoolFlag{ Name: "if-not-exists", Usage: "upload only if object does not exist", Hidden: true, }, cli.BoolFlag{ Name: "disable-multipart", Usage: "disable multipart upload feature", }, } ) // Put command. var putCmd = cli.Command{ Name: "put", Usage: "upload an object to a bucket", Action: mainPut, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(append(encFlags, globalFlags...), putFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] SOURCE TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} ENVIRONMENT VARIABLES: MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key). MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key). EXAMPLES: 1. Put an object from local file system to S3 storage {{.Prompt}} {{.HelpName}} path-to/object play/mybucket 2. Put an object from local file system to S3 bucket with name {{.Prompt}} {{.HelpName}} path-to/object play/mybucket/object 3. Put an object from local file system to S3 bucket under a prefix {{.Prompt}} {{.HelpName}} path-to/object play/mybucket/object-prefix/ 4. Put an object to MinIO storage using sse-c encryption {{.Prompt}} {{.HelpName}} --enc-c "play/mybucket/object=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" path-to/object play/mybucket/object 5. Put an object to MinIO storage using sse-kms encryption {{.Prompt}} {{.HelpName}} --enc-kms path-to/object play/mybucket/object `, } // mainPut is the entry point for put command. func mainPut(cliCtx *cli.Context) (e error) { args := cliCtx.Args() if len(args) < 2 { showCommandHelpAndExit(cliCtx, 1) // last argument is exit code. } ctx, cancelPut := context.WithCancel(globalContext) defer cancelPut() // part size size := cliCtx.String("s") if size == "" { size = "32MiB" } _, perr := humanize.ParseBytes(size) if perr != nil { fatalIf(probe.NewError(perr), "Unable to parse part size") } // threads threads := cliCtx.Int("P") if threads < 1 { fatalIf(errInvalidArgument().Trace(strconv.Itoa(threads)), "Invalid number of threads") } disableMultipart := cliCtx.Bool("disable-multipart") // Parse encryption keys per command. encryptionKeys, err := validateAndCreateEncryptionKeys(cliCtx) if err != nil { err.Trace(cliCtx.Args()...) } fatalIf(err, "SSE Error") md5, checksum := parseChecksum(cliCtx) if len(args) < 2 { fatalIf(errInvalidArgument().Trace(args...), "Invalid number of arguments.") } // get source and target sourceURLs := args[:len(args)-1] targetURL := args[len(args)-1] putURLsCh := make(chan URLs, 10000) var totalObjects, totalBytes int64 // Store a progress bar or an accounter var pg ProgressReader // Enable progress bar reader only during default mode. if !globalQuiet && !globalJSON { // set up progress bar pg = newProgressBar(totalBytes) } else { pg = newAccounter(totalBytes) } go func() { opts := prepareCopyURLsOpts{ sourceURLs: sourceURLs, targetURL: targetURL, encKeyDB: encryptionKeys, ignoreBucketExistsCheck: true, } for putURLs := range preparePutURLs(ctx, opts) { if putURLs.Error != nil { putURLsCh <- putURLs break } putURLs.checksum = checksum putURLs.MD5 = md5 totalBytes += putURLs.SourceContent.Size pg.SetTotal(totalBytes) totalObjects++ putURLs.DisableMultipart = disableMultipart putURLsCh <- putURLs } close(putURLsCh) }() for { select { case <-ctx.Done(): showLastProgressBar(pg, nil) return case putURLs, ok := <-putURLsCh: if !ok { showLastProgressBar(pg, nil) return } if putURLs.Error != nil { printPutURLsError(&putURLs) showLastProgressBar(pg, putURLs.Error.ToGoError()) return } urls := doCopy(ctx, doCopyOpts{ cpURLs: putURLs, pg: pg, encryptionKeys: encryptionKeys, multipartSize: size, multipartThreads: strconv.Itoa(threads), ifNotExists: cliCtx.Bool("if-not-exists"), }) if urls.Error != nil { showLastProgressBar(pg, urls.Error.ToGoError()) fatalIf(urls.Error.Trace(), "unable to upload") return } } } } func printPutURLsError(putURLs *URLs) { // Print in new line and adjust to top so that we // don't print over the ongoing scan bar if !globalQuiet && !globalJSON { console.Eraseline() } if strings.Contains(putURLs.Error.ToGoError().Error(), " is a folder.") { errorIf(putURLs.Error.Trace(), "Folder cannot be copied. Please use `...` suffix.") } else { errorIf(putURLs.Error.Trace(), "Unable to upload.") } } func showLastProgressBar(pg ProgressReader, e error) { if e != nil { // We only erase a line if we are displaying a progress bar if !globalQuiet && !globalJSON { console.Eraseline() } return } if progressReader, ok := pg.(*progressBar); ok { progressReader.Finish() } else { if accntReader, ok := pg.(*accounter); ok { printMsg(accntReader.Stat()) } } } minio-client-0.0~20250403/cmd/put-url.go000066400000000000000000000077441477450377600175200ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "strings" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" ) // preparePutURLs - prepares target and source clientURLs for copying. func preparePutURLs(ctx context.Context, o prepareCopyURLsOpts) chan URLs { copyURLsCh := make(chan URLs) go func(o prepareCopyURLsOpts) { defer close(copyURLsCh) copyURLsContent, err := guessPutURLType(ctx, o) if err != nil { copyURLsCh <- URLs{Error: err} return } switch copyURLsContent.copyType { case copyURLsTypeA: copyURLsCh <- prepareCopyURLsTypeA(ctx, *copyURLsContent, o) case copyURLsTypeB: copyURLsCh <- prepareCopyURLsTypeB(ctx, *copyURLsContent, o) default: copyURLsCh <- URLs{Error: errInvalidArgument().Trace(o.sourceURLs...)} } }(o) finalCopyURLsCh := make(chan URLs) go func() { defer close(finalCopyURLsCh) for cpURLs := range copyURLsCh { if cpURLs.Error != nil { finalCopyURLsCh <- cpURLs continue } finalCopyURLsCh <- cpURLs } }() return finalCopyURLsCh } // guessPutURLType guesses the type of clientURL. This approach all allows prepareURL // functions to accurately report failure causes. func guessPutURLType(ctx context.Context, o prepareCopyURLsOpts) (*copyURLsContent, *probe.Error) { cc := new(copyURLsContent) // Extract alias before fiddling with the clientURL. cc.sourceURL = o.sourceURLs[0] cc.sourceAlias, _, _ = mustExpandAlias(cc.sourceURL) // Find alias and expanded clientURL. cc.targetAlias, cc.targetURL, _ = mustExpandAlias(o.targetURL) if len(o.sourceURLs) == 1 { // 1 Source, 1 Target var err *probe.Error var client Client client, cc.sourceContent, err = url2Stat(ctx, url2StatOptions{urlStr: cc.sourceURL, versionID: o.versionID, fileAttr: false, encKeyDB: o.encKeyDB, timeRef: o.timeRef, isZip: o.isZip, ignoreBucketExistsCheck: false}) if err != nil { cc.copyType = copyURLsTypeInvalid return cc, err } _, ok := client.(*fsClient) if !ok { cc.copyType = copyURLsTypeInvalid return cc, probe.NewError(fmt.Errorf("Source is not local filepath.")) } // If recursion is ON, it is type C. // If source is a folder, it is Type C. if cc.sourceContent.Type.IsDir() { cc.copyType = copyURLsTypeC return cc, nil } client, err = newClient(o.targetURL) if err != nil { cc.copyType = copyURLsTypeInvalid return cc, err } s3clnt, ok := client.(*S3Client) if !ok { cc.copyType = copyURLsTypeInvalid return cc, probe.NewError(fmt.Errorf("Target is not s3.")) } bucket, path := s3clnt.url2BucketAndObject() if bucket == "" { cc.copyType = copyURLsTypeInvalid return cc, probe.NewError(fmt.Errorf("Bucket should not be empty.")) } cc.targetContent = s3clnt.objectInfo2ClientContent(bucket, minio.ObjectInfo{ Key: bucket, }) // If target is a folder, it is Type B. var isDir bool if path == "" { isDir = true } else { isDir = strings.HasSuffix(path, string(cc.targetContent.URL.Separator)) } if isDir { cc.copyType = copyURLsTypeB cc.sourceVersionID = cc.sourceContent.VersionID return cc, nil } // else Type A. cc.copyType = copyURLsTypeA cc.sourceVersionID = cc.sourceContent.VersionID return cc, nil } cc.copyType = copyURLsTypeInvalid return cc, errInvalidArgument().Trace() } minio-client-0.0~20250403/cmd/quota-clear.go000066400000000000000000000046311477450377600203150ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var quotaClearCmd = cli.Command{ Name: "clear", Usage: "clear bucket quota", Action: mainQuotaClear, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Clear bucket quota configured for bucket "mybucket" on MinIO. {{.Prompt}} {{.HelpName}} myminio/mybucket `, } // checkQuotaClearSyntax - validate all the passed arguments func checkQuotaClearSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainQuotaClear is the handler for "mc quota clear" command. func mainQuotaClear(ctx *cli.Context) error { checkQuotaClearSyntax(ctx) console.SetColor("QuotaMessage", color.New(color.FgGreen)) console.SetColor("QuotaInfo", color.New(color.FgCyan)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") _, targetURL := url2Alias(args[0]) if e := client.SetBucketQuota(globalContext, targetURL, &madmin.BucketQuota{}); e != nil { fatalIf(probe.NewError(e).Trace(args...), "Unable to clear bucket quota config") } printMsg(quotaMessage{ op: ctx.Command.Name, Bucket: targetURL, Status: "success", }) return nil } minio-client-0.0~20250403/cmd/quota-info.go000066400000000000000000000046601477450377600201640ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var quotaInfoCmd = cli.Command{ Name: "info", Usage: "show bucket quota", Action: mainQuotaInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display bucket quota configured for "mybucket" on MinIO. {{.Prompt}} {{.HelpName}} myminio/mybucket `, } // checkQuotaInfoSyntax - validate all the passed arguments func checkQuotaInfoSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainQuotaInfo is the handler for "mc quota info" command. func mainQuotaInfo(ctx *cli.Context) error { checkQuotaInfoSyntax(ctx) console.SetColor("QuotaMessage", color.New(color.FgGreen)) console.SetColor("QuotaInfo", color.New(color.FgCyan)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") _, targetURL := url2Alias(args[0]) qCfg, e := client.GetBucketQuota(globalContext, targetURL) fatalIf(probe.NewError(e).Trace(args...), "Unable to get bucket quota") sz := qCfg.Quota if qCfg.Size > 0 { sz = qCfg.Size } printMsg(quotaMessage{ op: ctx.Command.Name, Bucket: targetURL, Quota: sz, QuotaType: string(qCfg.Type), Status: "success", }) return nil } minio-client-0.0~20250403/cmd/quota-main.go000066400000000000000000000025251477450377600201530ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var quotaSubcommands = []cli.Command{ quotaSetCmd, quotaInfoCmd, quotaClearCmd, } var quotaCmd = cli.Command{ Name: "quota", Usage: "manage bucket quota", Action: mainQuota, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: quotaSubcommands, HideHelpCommand: true, } // mainQuota is the handle for "mc quota" command. func mainQuota(ctx *cli.Context) error { commandNotFound(ctx, quotaSubcommands) return nil // Sub-commands like "set", "clear", "info" have their own main. } minio-client-0.0~20250403/cmd/quota-set.go000066400000000000000000000110011477450377600200070ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var quotaSetFlags = []cli.Flag{ cli.StringFlag{ Name: "size", Usage: "set a hard quota, disallowing writes after quota is reached", }, } var quotaSetCmd = cli.Command{ Name: "set", Usage: "set bucket quota", Action: mainQuotaSet, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(quotaSetFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET [--size QUOTA] QUOTA Quota accepts human-readable case-insensitive number. Suffixes such as "k", "m", "g" and "t" referring to the metric units KB, MB, GB and TB respectively. Adding an "i" to these prefixes, uses the IEC units, so that "gi" refers to "gibibyte" or "GiB". A "b" at the end is also accepted. Without suffixes the unit is bytes. The MinIO object scanner checks a bucket's quota each time it is scanned. If the scanner determines a bucket has met or exceeded its quota, MinIO rejects subsequent object write requests until the scanner determines the bucket no longer exceeds its quota. FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Set hard quota of 1gb for a bucket "mybucket" on MinIO. {{.Prompt}} {{.HelpName}} myminio/mybucket --size 1GB `, } // quotaMessage container for content message structure type quotaMessage struct { op string Status string `json:"status"` Bucket string `json:"bucket"` Quota uint64 `json:"quota,omitempty"` QuotaType string `json:"type,omitempty"` } func (q quotaMessage) String() string { switch q.op { case "set": return console.Colorize("QuotaMessage", fmt.Sprintf("Successfully set bucket quota of %s on `%s`", humanize.IBytes(q.Quota), q.Bucket)) case "clear": return console.Colorize("QuotaMessage", fmt.Sprintf("Successfully cleared bucket quota configured on `%s`", q.Bucket)) default: return console.Colorize("QuotaInfo", fmt.Sprintf("Bucket `%s` has %s quota of %s", q.Bucket, q.QuotaType, humanize.IBytes(q.Quota))) } } func (q quotaMessage) JSON() string { jsonMessageBytes, e := json.MarshalIndent(q, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // checkQuotaSetSyntax - validate all the passed arguments func checkQuotaSetSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainQuotaSet is the handler for "mc quota set" command. func mainQuotaSet(ctx *cli.Context) error { checkQuotaSetSyntax(ctx) console.SetColor("QuotaMessage", color.New(color.FgGreen)) console.SetColor("QuotaInfo", color.New(color.FgBlue)) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") _, targetURL := url2Alias(args[0]) if !ctx.IsSet("size") { fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), "--size flag needs to be set.") } qType := madmin.HardQuota quotaStr := ctx.String("size") quota, e := humanize.ParseBytes(quotaStr) fatalIf(probe.NewError(e).Trace(quotaStr), "Unable to parse quota") fatalIf(probe.NewError(client.SetBucketQuota(globalContext, targetURL, &madmin.BucketQuota{ Quota: quota, Type: qType, })).Trace(args...), "Unable to set bucket quota") printMsg(quotaMessage{ op: ctx.Command.Name, Bucket: targetURL, Quota: quota, QuotaType: string(qType), Status: "success", }) return nil } minio-client-0.0~20250403/cmd/rb-main.go000066400000000000000000000211571477450377600174270ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "path" "path/filepath" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) var rbFlags = []cli.Flag{ cli.BoolFlag{ Name: "force", Usage: "force a recursive remove operation on all object versions", }, cli.BoolFlag{ Name: "dangerous", Usage: "allow site-wide removal of objects", }, } // remove a bucket. var rbCmd = cli.Command{ Name: "rb", Usage: "remove a bucket", Action: mainRemoveBucket, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(rbFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET...] {{if .VisibleFlags}} FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} EXAMPLES: 1. Remove an empty bucket on Amazon S3 cloud storage {{.Prompt}} {{.HelpName}} s3/mybucket 2. Remove a directory hierarchy. {{.Prompt}} {{.HelpName}} /tmp/this/new/dir1 3. Remove bucket 'jazz-songs' and all its contents {{.Prompt}} {{.HelpName}} --force s3/jazz-songs 4. Remove all buckets and objects recursively from S3 host {{.Prompt}} {{.HelpName}} --force --dangerous s3 `, } // removeBucketMessage is container for delete bucket success and failure messages. type removeBucketMessage struct { Status string `json:"status"` Bucket string `json:"bucket"` } // String colorized delete bucket message. func (s removeBucketMessage) String() string { return console.Colorize("RemoveBucket", fmt.Sprintf("Removed `%s` successfully.", s.Bucket)) } // JSON jsonified remove bucket message. func (s removeBucketMessage) JSON() string { removeBucketJSONBytes, e := json.Marshal(s) fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(removeBucketJSONBytes) } // Validate command line arguments. func checkRbSyntax(cliCtx *cli.Context) { if !cliCtx.Args().Present() { exitCode := 1 showCommandHelpAndExit(cliCtx, exitCode) } // Set command flags from context. isForce := cliCtx.Bool("force") isDangerous := cliCtx.Bool("dangerous") for _, url := range cliCtx.Args() { if isS3NamespaceRemoval(url) { if isForce && isDangerous { continue } fatalIf(errDummy().Trace(), "This operation results in **site-wide** removal of buckets. If you are really sure, retry this command with ‘--force’ and ‘--dangerous’ flags.") } } } // Return a list of aliased urls of buckets under the passed url func listBucketsURLs(ctx context.Context, url string) ([]string, *probe.Error) { var buckets []string targetAlias, targetURL, _ := mustExpandAlias(url) clnt, err := newClientFromAlias(targetAlias, targetURL) if err != nil { return nil, err } opts := ListOptions{ ShowDir: DirLast, } for content := range clnt.List(ctx, opts) { if content.Err != nil { errorIf(content.Err, "") continue } select { case <-ctx.Done(): return nil, probe.NewError(ctx.Err()) default: } bucketName := strings.TrimPrefix(content.URL.Path, clnt.GetURL().Path) bucketPath := path.Join(url, bucketName) buckets = append(buckets, bucketPath) } return buckets, nil } // Delete a bucket and all its objects and versions will be removed as well. func deleteBucket(ctx context.Context, url string, isForce bool) *probe.Error { targetAlias, targetURL, _ := mustExpandAlias(url) clnt, pErr := newClientFromAlias(targetAlias, targetURL) if pErr != nil { return pErr } contentCh := make(chan *ClientContent) resultCh := clnt.Remove(ctx, false, false, false, false, contentCh) go func() { defer close(contentCh) opts := ListOptions{ Recursive: true, WithOlderVersions: true, WithDeleteMarkers: true, ShowDir: DirLast, } for content := range clnt.List(ctx, opts) { if content.Err != nil { contentCh <- content continue } urlString := content.URL.Path select { case contentCh <- content: case <-ctx.Done(): return } // list internally mimics recursive directory listing of object prefixes for s3 similar to FS. // The rmMessage needs to be printed only for actual buckets being deleted and not objects. tgt := strings.TrimPrefix(urlString, string(filepath.Separator)) if !strings.Contains(tgt, string(filepath.Separator)) && tgt != targetAlias { printMsg(removeBucketMessage{ Bucket: targetAlias + urlString, Status: "success", }) } } }() // Give up on the first error. for result := range resultCh { if result.Err != nil { return result.Err.Trace(url) } } // Return early if prefix delete switch c := clnt.(type) { case *S3Client: _, object := c.url2BucketAndObject() if object != "" && isForce { return nil } default: } // Remove a bucket without force flag first because force // won't work if a bucket has some locking rules, that's // why we start with regular bucket removal first. err := clnt.RemoveBucket(ctx, false) if err != nil { if isForce && minio.ToErrorResponse(err.ToGoError()).Code == "BucketNotEmpty" { return clnt.RemoveBucket(ctx, true) } } return err } // isS3NamespaceRemoval returns true if alias // is not qualified by bucket func isS3NamespaceRemoval(url string) bool { // clean path for aliases like s3/. // Note: UNC path using / works properly in go 1.9.2 even though it breaks the UNC specification. url = filepath.ToSlash(filepath.Clean(url)) // namespace removal applies only for non FS. So filter out if passed url represents a directory _, path := url2Alias(url) return (path == "") } // mainRemoveBucket is entry point for rb command. func mainRemoveBucket(cliCtx *cli.Context) error { ctx, cancelRemoveBucket := context.WithCancel(globalContext) defer cancelRemoveBucket() // check 'rb' cli arguments. checkRbSyntax(cliCtx) isForce := cliCtx.Bool("force") // Additional command specific theme customization. console.SetColor("RemoveBucket", color.New(color.FgGreen, color.Bold)) var cErr error for _, targetURL := range cliCtx.Args() { // Instantiate client for URL. clnt, err := newClient(targetURL) if err != nil { errorIf(err.Trace(targetURL), "Invalid target `%s`.", targetURL) cErr = exitStatus(globalErrorExitStatus) continue } _, err = clnt.Stat(ctx, StatOptions{}) if err != nil { switch err.ToGoError().(type) { case BucketNameEmpty: case BucketDoesNotExist: if isForce { continue } errorIf(err.Trace(targetURL), "Unable to validate target `%s`.", targetURL) cErr = exitStatus(globalErrorExitStatus) continue default: errorIf(err.Trace(targetURL), "Unable to validate target `%s`.", targetURL) cErr = exitStatus(globalErrorExitStatus) continue } } // Check if the bucket contains any object, version or delete marker. isEmpty := true opts := ListOptions{ Recursive: true, ShowDir: DirNone, WithOlderVersions: true, WithDeleteMarkers: true, } listCtx, listCancel := context.WithCancel(ctx) for obj := range clnt.List(listCtx, opts) { if obj.Err != nil { continue } isEmpty = false break } listCancel() // For all recursive operations make sure to check for 'force' flag. if !isForce && !isEmpty { fatalIf(errDummy().Trace(), "`"+targetURL+"` is not empty. Retry this command with ‘--force’ flag if you want to remove `"+targetURL+"` and all its contents") } var bucketsURL []string if isS3NamespaceRemoval(targetURL) { bucketsURL, err = listBucketsURLs(ctx, targetURL) fatalIf(err.Trace(targetURL), "Failed to remove `"+targetURL+"`.") } else { bucketsURL = []string{targetURL} } for _, bucketURL := range bucketsURL { e := deleteBucket(ctx, bucketURL, isForce) fatalIf(e.Trace(bucketURL), "Failed to remove `"+bucketURL+"`.") printMsg(removeBucketMessage{ Bucket: bucketURL, Status: "success", }) } } return cErr } minio-client-0.0~20250403/cmd/ready-main.go000066400000000000000000000101441477450377600201220ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) const ( healthCheckInterval = 5 * time.Second ) var readyFlags = []cli.Flag{ cli.BoolFlag{ Name: "cluster-read", Usage: "check if the cluster has enough read quorum", }, cli.BoolFlag{ Name: "maintenance", Usage: "check if the cluster is taken down for maintenance", }, } // Checks if the cluster is ready or not var readyCmd = cli.Command{ Name: "ready", Usage: "checks if the cluster is ready or not", Action: mainReady, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(readyFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET {{if .VisibleFlags}} FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} EXAMPLES: 1. Check if the cluster is ready or not {{.Prompt}} {{.HelpName}} myminio 2. Check if the cluster has enough read quorum {{.Prompt}} {{.HelpName}} myminio --cluster-read 3. Check if the cluster is taken down for maintenance {{.Prompt}} {{.HelpName}} myminio --maintenance `, } type readyMessage struct { Status string `json:"status"` Alias string `json:"alias"` Healthy bool `json:"healthy"` MaintenanceMode bool `json:"maintenanceMode"` WriteQuorum int `json:"writeQuorum"` HealingDrives int `json:"healingDrives"` Err error `json:"error"` } func (r readyMessage) String() string { switch { case r.Healthy: return color.GreenString(fmt.Sprintf("The cluster '%s' is ready", r.Alias)) case r.Err != nil: return color.RedString(fmt.Sprintf("The cluster '%s' is unreachable: %s", r.Alias, r.Err.Error())) default: return color.RedString(fmt.Sprintf("The cluster '%s' is not ready", r.Alias)) } } // JSON jsonified ready result func (r readyMessage) JSON() string { jsonMessageBytes, e := json.MarshalIndent(r, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // mainReady - main handler for mc ready command. func mainReady(cliCtx *cli.Context) error { if !cliCtx.Args().Present() { exitCode := 1 showCommandHelpAndExit(cliCtx, exitCode) } // Set command flags from context. clusterRead := cliCtx.Bool("cluster-read") maintenance := cliCtx.Bool("maintenance") ctx, cancelClusterReady := context.WithCancel(globalContext) defer cancelClusterReady() aliasedURL := cliCtx.Args().Get(0) anonClient, err := newAnonymousClient(aliasedURL) fatalIf(err.Trace(aliasedURL), "Couldn't construct anonymous client for `"+aliasedURL+"`.") healthOpts := madmin.HealthOpts{ ClusterRead: clusterRead, Maintenance: maintenance, } timer := time.NewTimer(0) defer timer.Stop() for { select { case <-ctx.Done(): return nil case <-timer.C: healthResult, hErr := anonClient.Healthy(ctx, healthOpts) printMsg(readyMessage{ Alias: aliasedURL, Status: "success", Healthy: healthResult.Healthy, MaintenanceMode: healthResult.MaintenanceMode, WriteQuorum: healthResult.WriteQuorum, HealingDrives: healthResult.HealingDrives, Err: hErr, }) if healthResult.Healthy { return nil } timer.Reset(healthCheckInterval) } } } minio-client-0.0~20250403/cmd/replicate-add.go000066400000000000000000000276611477450377600206060ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "net/url" "path" "strconv" "strings" "time" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/minio-go/v7/pkg/s3utils" "github.com/minio/pkg/v3/console" ) var replicateAddFlags = []cli.Flag{ cli.StringFlag{ Name: "arn", Usage: "unique role ARN", Hidden: true, }, cli.StringFlag{ Name: "id", Usage: "id for the rule, should be a unique value", }, cli.StringFlag{ Name: "tags", Usage: "format '=&=&=', multiple values allowed for multiple key/value pairs", }, cli.StringFlag{ Name: "storage-class", Usage: `storage class for destination, valid values are either "STANDARD" or "REDUCED_REDUNDANCY"`, }, cli.BoolFlag{ Name: "disable", Usage: "disable the rule", }, cli.IntFlag{ Name: "priority", Usage: "priority of the rule, should be unique and is a required field", }, cli.StringFlag{ Name: "remote-bucket", Usage: "remote bucket, should be a unique value for the configuration", }, cli.StringFlag{ Name: "replicate", Value: `delete-marker,delete,existing-objects,metadata-sync`, Usage: `comma separated list to enable replication of soft deletes, permanent deletes, existing objects and metadata sync`, }, cli.StringFlag{ Name: "path", Value: "auto", Usage: "bucket path lookup supported by the server. Valid options are ['auto', 'on', 'off']'", }, cli.StringFlag{ Name: "region", Usage: "region of the destination bucket (optional)", }, cli.StringFlag{ Name: "bandwidth", Usage: "set bandwidth limit in bytes per second (K,B,G,T for metric and Ki,Bi,Gi,Ti for IEC units)", }, cli.BoolFlag{ Name: "sync", Usage: "enable synchronous replication for this target. default is async", }, cli.UintFlag{ Name: "healthcheck-seconds", Usage: "health check interval in seconds", Value: 60, }, cli.BoolFlag{ Name: "disable-proxy", Usage: "disable proxying in active-active replication. If unset, default behavior is to proxy", }, } var replicateAddCmd = cli.Command{ Name: "add", Usage: "add a server side replication configuration rule", Action: mainReplicateAdd, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, replicateAddFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Add replication configuration rule on bucket "sourcebucket" for alias "sourceminio" with alias "targetminio" to replicate all operations in an active-active replication setup. {{.Prompt}} {{.HelpName}} sourceminio/sourcebucket --remote-bucket targetminio/targetbucket \ --priority 1 2. Add replication configuration rule on bucket "mybucket" for alias "myminio" to replicate all operations in an active-active replication setup. {{.Prompt}} {{.HelpName}} myminio/mybucket --remote-bucket https://foobar:foo12345@minio.siteb.example.com/targetbucket \ --priority 1 3. Add replication configuration rule on bucket "mybucket" for alias "myminio" to replicate all objects with tags "key1=value1, key2=value2" to targetbucket synchronously with bandwidth set to 2 gigabits per second. {{.Prompt}} {{.HelpName}} myminio/mybucket --remote-bucket https://foobar:foo12345@minio.siteb.example.com/targetbucket \ --tags "key1=value1&key2=value2" --bandwidth "2G" --sync \ --priority 1 4. Disable a replication configuration rule on bucket "mybucket" for alias "myminio". {{.Prompt}} {{.HelpName}} myminio/mybucket --remote-bucket https://foobar:foo12345@minio.siteb.example.com/targetbucket \ --tags "key1=value1&key2=value2" \ --priority 1 --disable 5. Add replication configuration rule with existing object replication, delete marker replication and versioned deletes enabled on bucket "mybucket" for alias "myminio". {{.Prompt}} {{.HelpName}} myminio/mybucket --remote-bucket https://foobar:foo12345@minio.siteb.example.com/targetbucket \ --replicate "existing-objects,delete,delete-marker" \ --priority 1 `, } // checkReplicateAddSyntax - validate all the passed arguments func checkReplicateAddSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } if ctx.String("remote-bucket") == "" { fatal(errDummy().Trace(), "--remote-bucket flag needs to be specified.") } } type replicateAddMessage struct { Op string `json:"op"` Status string `json:"status"` URL string `json:"url"` ID string `json:"id"` } const ( enableStatus = "enable" disableStatus = "disable" ) func (l replicateAddMessage) JSON() string { l.Status = "success" jsonMessageBytes, e := json.MarshalIndent(l, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (l replicateAddMessage) String() string { if l.ID != "" { return console.Colorize("replicateAddMessage", "Replication configuration rule with ID `"+l.ID+"` applied to "+l.URL+".") } return console.Colorize("replicateAddMessage", "Replication configuration rule applied to "+l.URL+" successfully.") } func extractCredentialURL(argURL string) (accessKey, secretKey string, u *url.URL) { var parsedURL string if strings.HasPrefix(argURL, "http://") || strings.HasPrefix(argURL, "https://") { if hostKeyTokens.MatchString(argURL) { fatalIf(errInvalidArgument().Trace(argURL), "temporary tokens are not allowed for remote targets") } if hostKeys.MatchString(argURL) { parts := hostKeys.FindStringSubmatch(argURL) if len(parts) != 5 { fatalIf(errInvalidArgument().Trace(argURL), "unsupported remote target format, please check --help") } accessKey = parts[2] secretKey = parts[3] parsedURL = fmt.Sprintf("%s%s", parts[1], parts[4]) } } else { var alias string var aliasCfg *aliasConfigV10 // get alias config by alias url alias, parsedURL, aliasCfg = mustExpandAlias(argURL) if aliasCfg == nil { fatalIf(errInvalidAliasedURL(alias).Trace(argURL), "No such alias `"+alias+"` found.") return } accessKey, secretKey = aliasCfg.AccessKey, aliasCfg.SecretKey } var e error if parsedURL == "" { fatalIf(errInvalidArgument().Trace(argURL), "no valid credentials were detected") } u, e = url.Parse(parsedURL) if e != nil { fatalIf(errInvalidArgument().Trace(parsedURL), "unsupported URL format %v", e) } return accessKey, secretKey, u } // fetchRemoteTarget - returns the dest bucket, dest endpoint, access and secret key func fetchRemoteTarget(cli *cli.Context) (bktTarget *madmin.BucketTarget) { if !cli.IsSet("remote-bucket") { fatalIf(probe.NewError(fmt.Errorf("missing Remote target configuration")), "unable to parse remote target") } p := cli.String("path") if !isValidPath(p) { fatalIf(errInvalidArgument().Trace(p), "unrecognized bucket path style. Valid options are `[on, off, auto]`.") } tgtURL := cli.String("remote-bucket") accessKey, secretKey, u := extractCredentialURL(tgtURL) var tgtBucket string if u.Path != "" { tgtBucket = path.Clean(u.Path[1:]) } fatalIf(probe.NewError(s3utils.CheckValidBucketName(tgtBucket)).Trace(tgtURL), "invalid target bucket") bandwidthStr := cli.String("bandwidth") bandwidth, e := getBandwidthInBytes(bandwidthStr) fatalIf(probe.NewError(e).Trace(bandwidthStr), "invalid bandwidth value") console.SetColor(cred, color.New(color.FgYellow, color.Italic)) creds := &madmin.Credentials{AccessKey: accessKey, SecretKey: secretKey} disableproxy := cli.Bool("disable-proxy") bktTarget = &madmin.BucketTarget{ TargetBucket: tgtBucket, Secure: u.Scheme == "https", Credentials: creds, Endpoint: u.Host, Path: p, API: "s3v4", Type: madmin.ServiceType("replication"), Region: cli.String("region"), BandwidthLimit: int64(bandwidth), ReplicationSync: cli.Bool("sync"), DisableProxy: disableproxy, HealthCheckDuration: time.Duration(cli.Uint("healthcheck-seconds")) * time.Second, } return bktTarget } func getBandwidthInBytes(bandwidthStr string) (bandwidth uint64, err error) { if bandwidthStr != "" { bandwidth, err = humanize.ParseBytes(bandwidthStr) if err != nil { return } } return } func mainReplicateAdd(cliCtx *cli.Context) error { ctx, cancelReplicateAdd := context.WithCancel(globalContext) defer cancelReplicateAdd() console.SetColor("replicateAddMessage", color.New(color.FgGreen)) checkReplicateAddSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "unable to initialize connection.") var sourceBucket string switch c := client.(type) { case *S3Client: sourceBucket, _ = c.url2BucketAndObject() default: fatalIf(err.Trace(args...), "replication is not supported for filesystem") } // Create a new MinIO Admin Client admclient, cerr := newAdminClient(aliasedURL) fatalIf(cerr, "unable to initialize admin connection.") bktTarget := fetchRemoteTarget(cliCtx) arn, e := admclient.SetRemoteTarget(globalContext, sourceBucket, bktTarget) fatalIf(probe.NewError(e).Trace(args...), "unable to configure remote target") rcfg, err := client.GetReplication(ctx) fatalIf(err.Trace(args...), "unable to fetch replication configuration") ruleStatus := enableStatus if cliCtx.Bool(disableStatus) { ruleStatus = disableStatus } dmReplicateStatus := disableStatus deleteReplicationStatus := disableStatus replicaSync := enableStatus existingReplicationStatus := disableStatus replSlice := strings.Split(cliCtx.String("replicate"), ",") for _, opt := range replSlice { switch strings.TrimSpace(strings.ToLower(opt)) { case "delete-marker": dmReplicateStatus = enableStatus case "delete": deleteReplicationStatus = enableStatus case "metadata-sync", "replica-metadata-sync": replicaSync = enableStatus case "existing-objects": existingReplicationStatus = enableStatus default: fatalIf(probe.NewError(fmt.Errorf("invalid value for --replicate flag %s", cliCtx.String("replicate"))), `--replicate flag takes one or more comma separated string with values "delete", "delete-marker", "metadata-sync", "existing-objects" or "" to disable these settings`) } } opts := replication.Options{ TagString: cliCtx.String("tags"), StorageClass: cliCtx.String("storage-class"), Priority: strconv.Itoa(cliCtx.Int("priority")), RuleStatus: ruleStatus, ID: cliCtx.String("id"), DestBucket: arn, Op: replication.AddOption, ReplicateDeleteMarkers: dmReplicateStatus, ReplicateDeletes: deleteReplicationStatus, ReplicaSync: replicaSync, ExistingObjectReplicate: existingReplicationStatus, } fatalIf(client.SetReplication(ctx, &rcfg, opts), "unable to add replication rule") printMsg(replicateAddMessage{ Op: cliCtx.Command.Name, URL: aliasedURL, ID: opts.ID, }) return nil } minio-client-0.0~20250403/cmd/replicate-backlog.go000066400000000000000000000334001477450377600214440ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "path" "path/filepath" "strconv" "strings" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/table" "github.com/fatih/color" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var replicateBacklogFlags = []cli.Flag{ cli.StringFlag{ Name: "arn", Usage: "unique role ARN", }, cli.BoolFlag{ Name: "verbose,v", Usage: "include replicated versions", }, cli.StringFlag{ Name: "nodes,n", Usage: "show most recent failures for one or more nodes. Valid values are 'all', or node name", Value: "all", }, cli.BoolFlag{ Name: "full,a", Usage: "list and show all replication failures for bucket", }, } var replicateBacklogCmd = cli.Command{ Name: "backlog", Aliases: []string{"diff"}, HiddenAliases: true, Usage: "show unreplicated object versions", Action: mainReplicateBacklog, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, replicateBacklogFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Show most recent replication failures on "myminio" alias for objects in bucket "mybucket" {{.Prompt}} {{.HelpName}} myminio/mybucket 2. Show all unreplicated objects on "myminio" alias for objects in prefix "path/to/prefix" of "mybucket" for all targets. This will perform full listing of all objects in the prefix to find unreplicated objects. {{.Prompt}} {{.HelpName}} myminio/mybucket/path/to/prefix --full `, } // checkReplicateBacklogSyntax - validate all the passed arguments func checkReplicateBacklogSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } type replicateMRFMessage struct { Op string `json:"op"` Status string `json:"status"` madmin.ReplicationMRF } func (m replicateMRFMessage) JSON() string { m.Status = "success" jsonMessageBytes, e := json.MarshalIndent(m, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (m replicateMRFMessage) String() string { return console.Colorize("", newPrettyTable(" | ", Field{getNodeTheme(m.ReplicationMRF.NodeName), len(m.ReplicationMRF.NodeName) + 3}, Field{"Count", 7}, Field{"Object", -1}, ).buildRow(m.NodeName, fmt.Sprintf("Retry=%d", m.RetryCount), fmt.Sprintf("%s (%s)", m.Object, m.VersionID))) } type replicateBacklogMessage struct { Op string `json:"op"` Diff madmin.DiffInfo `json:"diff,omitempty"` MRF madmin.ReplicationMRF `json:"mrf,omitempty"` OpStatus string `json:"opStatus"` arn string `json:"-"` verbose bool `json:"-"` } func (r replicateBacklogMessage) JSON() string { var e error var jsonMessageBytes []byte switch r.Op { case "diff": jsonMessageBytes, e = json.MarshalIndent(r.Diff, "", " ") case "mrf": jsonMessageBytes, e = json.MarshalIndent(r.MRF, "", " ") } fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (r replicateBacklogMessage) toRow() (row table.Row) { switch r.Op { case "diff": return r.toDiffRow() case "mrf": return r.toMRFRow() } return } func (r replicateBacklogMessage) toDiffRow() (row table.Row) { d := r.Diff if d.Object == "" { return } op := "" if d.VersionID != "" { switch d.IsDeleteMarker { case true: op = "DEL" default: op = "PUT" } } st := r.replStatus() replTimeStamp := d.ReplicationTimestamp.Format(printDate) switch { case st == "PENDING": replTimeStamp = "" case op == "DEL": replTimeStamp = "" } return table.Row{ replTimeStamp, d.LastModified.Format(printDate), st, d.VersionID, op, d.Object, } } func (r replicateBacklogMessage) toMRFRow() (row table.Row) { d := r.MRF if d.Object == "" { return } return table.Row{ d.NodeName, d.VersionID, strconv.Itoa(d.RetryCount), path.Join(d.Bucket, d.Object), } } func (r *replicateBacklogMessage) replStatus() string { var st string d := r.Diff if r.arn == "" { // report overall replication status if d.DeleteReplicationStatus != "" { st = d.DeleteReplicationStatus } else { st = d.ReplicationStatus } } else { // report target replication diff for arn, t := range d.Targets { if arn != r.arn { continue } if t.DeleteReplicationStatus != "" { st = t.DeleteReplicationStatus } else { st = t.ReplicationStatus } } if len(d.Targets) == 0 { st = "" } } return st } type replicateBacklogUI struct { spinner spinner.Model sub interface{} diffCh chan madmin.DiffInfo mrfCh chan madmin.ReplicationMRF arn string op string quitting bool table table.Model rows []table.Row help help.Model keymap keyMap count int } type keyMap struct { quit key.Binding up key.Binding down key.Binding enter key.Binding } func newKeyMap() keyMap { return keyMap{ up: key.NewBinding( key.WithKeys("k", "up", "left", "shift+tab"), key.WithHelp("↑/k", "Move up"), ), down: key.NewBinding( key.WithKeys("j", "down", "right", "tab"), key.WithHelp("↓/j", "Move down"), ), enter: key.NewBinding( key.WithKeys("enter", " "), key.WithHelp("enter/spacebar", ""), ), quit: key.NewBinding( key.WithKeys("ctrl+c", "q"), key.WithHelp("q", "quit"), ), } } func initReplicateBacklogUI(arn, op string, diffCh interface{}) *replicateBacklogUI { s := spinner.New() s.Spinner = spinner.Points s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) columns := getBacklogHeader(op) t := table.New( table.WithColumns(columns), table.WithFocused(true), table.WithHeight(7), ) ts := getBacklogStyles() t.SetStyles(ts) ui := &replicateBacklogUI{ spinner: s, sub: diffCh, op: op, arn: arn, table: t, help: help.New(), keymap: newKeyMap(), } if ch, ok := diffCh.(chan madmin.DiffInfo); ok { ui.diffCh = ch } if ch, ok := diffCh.(chan madmin.ReplicationMRF); ok { ui.mrfCh = ch } return ui } func (m *replicateBacklogUI) Init() tea.Cmd { return tea.Batch( m.spinner.Tick, waitForActivity(m.sub, m.op), // wait for activity ) } const rowLimit = 10000 // A command that waits for the activity on a channel. func waitForActivity(sub interface{}, op string) tea.Cmd { return func() tea.Msg { switch op { case "diff": msg := <-sub.(<-chan madmin.DiffInfo) return msg case "mrf": msg := <-sub.(<-chan madmin.ReplicationMRF) return msg } return "unexpected message" } } func getBacklogHeader(op string) []table.Column { switch op { case "diff": return getBacklogDiffHeader() case "mrf": return getBacklogMRFHeader() } return nil } func getBacklogDiffHeader() []table.Column { return []table.Column{ {Title: "Attempted At", Width: 23}, {Title: "Created", Width: 23}, {Title: "Status", Width: 9}, {Title: "VersionID", Width: 36}, {Title: "Op", Width: 3}, {Title: "Object", Width: 60}, } } func getBacklogMRFHeader() []table.Column { return []table.Column{ {Title: "Node", Width: 40}, {Title: "VersionID", Width: 36}, {Title: "Retry", Width: 5}, {Title: "Object", Width: 60}, } } func getBacklogStyles() table.Styles { ts := table.DefaultStyles() ts.Header = ts.Header. BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")). BorderBottom(true). Bold(false) ts.Selected = ts.Selected. Foreground(lipgloss.Color("229")). Background(lipgloss.Color("300")). Bold(false) return ts } func (m *replicateBacklogUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "esc": if m.table.Focused() { m.table.Blur() } else { m.table.Focus() } case "ctrl+c", "q": m.quitting = true return m, tea.Quit case "enter": columns := getBacklogHeader(m.op) ts := getBacklogStyles() m.table = table.New( table.WithColumns(columns), table.WithRows(m.rows), table.WithFocused(true), table.WithHeight(10), ) m.table.SetStyles(ts) default: } case madmin.DiffInfo: if msg.Object != "" { m.count++ if m.count <= rowLimit { // don't buffer more than 10k entries rdif := replicateBacklogMessage{ Op: "diff", Diff: msg, arn: m.arn, } m.rows = append(m.rows, rdif.toRow()) } return m, waitForActivity(m.sub, m.op) } m.quitting = true columns := getBacklogDiffHeader() ts := getBacklogStyles() m.table = table.New( table.WithColumns(columns), table.WithRows(m.rows), table.WithFocused(true), table.WithHeight(10), ) m.table.SetStyles(ts) return m, nil case madmin.ReplicationMRF: if msg.Object != "" { m.count++ if m.count <= rowLimit { // don't buffer more than 10k entries rdif := replicateBacklogMessage{ Op: "mrf", MRF: msg, arn: m.arn, } m.rows = append(m.rows, rdif.toRow()) } return m, waitForActivity(m.sub, m.op) } m.quitting = true columns := getBacklogMRFHeader() ts := getBacklogStyles() m.table = table.New( table.WithColumns(columns), table.WithRows(m.rows), table.WithFocused(true), table.WithHeight(10), ) m.table.SetStyles(ts) return m, nil case spinner.TickMsg: var cmd tea.Cmd if !m.quitting { m.spinner, cmd = m.spinner.Update(msg) return m, cmd } } m.table, cmd = m.table.Update(msg) return m, cmd } var baseStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")) var descStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ Light: "#B2B2B2", Dark: "#4A4A4A", }) var ( subtle = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} special = lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"} divider = lipgloss.NewStyle(). SetString("•"). Padding(0, 1). Foreground(subtle). String() advisory = lipgloss.NewStyle().Foreground(special).Render infoStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderTop(true). BorderForeground(subtle) ) func (m *replicateBacklogUI) helpView() string { return "\n" + m.help.ShortHelpView([]key.Binding{ m.keymap.enter, m.keymap.down, m.keymap.up, m.keymap.quit, }) } func (m *replicateBacklogUI) View() string { var sb strings.Builder if !m.quitting { sb.WriteString(fmt.Sprintf("%s\n", m.spinner.View())) } if m.count > 0 { advisoryStr := "" if m.count > rowLimit { advisoryStr = "[ use --json flag for full listing]" } desc := lipgloss.JoinVertical(lipgloss.Left, descStyle.Render("Unreplicated versions summary"), infoStyle.Render(fmt.Sprintf("Total Unreplicated: %d", m.count)+divider+advisory(advisoryStr+"\n"))) row := lipgloss.JoinHorizontal(lipgloss.Top, desc) sb.WriteString(row + "\n\n") sb.WriteString(baseStyle.Render(m.table.View())) } sb.WriteString(m.helpView()) return sb.String() } func mainReplicateBacklog(cliCtx *cli.Context) error { checkReplicateBacklogSyntax(cliCtx) console.SetColor("diff-msg", color.New(color.FgHiCyan, color.Bold)) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) aliasedURL = filepath.ToSlash(aliasedURL) splits := splitStr(aliasedURL, "/", 3) bucket, prefix := splits[1], splits[2] if bucket == "" { fatalIf(errInvalidArgument(), "bucket not specified in `"+aliasedURL+"`.") } ctx, cancel := context.WithCancel(globalContext) defer cancel() // Create a new MinIO Admin Client client, cerr := newAdminClient(aliasedURL) fatalIf(cerr, "Unable to initialize admin connection.") if !cliCtx.Bool("full") { mrfCh := client.BucketReplicationMRF(ctx, bucket, cliCtx.String("nodes")) if globalJSON { for mrf := range mrfCh { if mrf.Err != "" { fatalIf(probe.NewError(fmt.Errorf("%s", mrf.Err)), "Unable to fetch replication backlog.") } printMsg(replicateMRFMessage{ Op: "mrf", Status: "success", ReplicationMRF: mrf, }) } return nil } ui := tea.NewProgram(initReplicateBacklogUI("", "mrf", mrfCh)) if _, e := ui.Run(); e != nil { cancel() fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch replication backlog") } return nil } verbose := cliCtx.Bool("verbose") arn := cliCtx.String("arn") diffCh := client.BucketReplicationDiff(ctx, bucket, madmin.ReplDiffOpts{ Verbose: verbose, ARN: arn, Prefix: prefix, }) if globalJSON { for di := range diffCh { console.Println(replicateBacklogMessage{ Op: "diff", Diff: di, arn: arn, verbose: verbose, }.JSON()) } return nil } ui := tea.NewProgram(initReplicateBacklogUI(arn, "diff", diffCh)) if _, e := ui.Run(); e != nil { cancel() fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch replication backlog") } return nil } minio-client-0.0~20250403/cmd/replicate-export.go000066400000000000000000000070121477450377600213630ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/pkg/v3/console" ) var replicateExportCmd = cli.Command{ Name: "export", Usage: "export server side replication configuration", Action: mainReplicateExport, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Print replication configuration on bucket "mybucket" for alias "myminio" to STDOUT. {{.Prompt}} {{.HelpName}} myminio/mybucket 2. Export replication configuration on bucket "mybucket" for alias "myminio" to '/data/replicate/config'. {{.Prompt}} {{.HelpName}} myminio/mybucket > /data/replicate/config `, } // checkReplicateExportSyntax - validate all the passed arguments func checkReplicateExportSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } type replicateExportMessage struct { Op string `json:"op"` Status string `json:"status"` URL string `json:"url"` ReplicationConfig replication.Config `json:"config"` } func (r replicateExportMessage) JSON() string { r.Status = "success" jsonMessageBytes, e := json.MarshalIndent(r, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (r replicateExportMessage) String() string { if r.ReplicationConfig.Empty() { return console.Colorize("ReplicateNMessage", "No replication configuration found for "+r.URL+".") } msgBytes, e := json.MarshalIndent(r.ReplicationConfig, "", " ") fatalIf(probe.NewError(e), "Unable to marshal replication configuration") return string(msgBytes) } func mainReplicateExport(cliCtx *cli.Context) error { ctx, cancelReplicateExport := context.WithCancel(globalContext) defer cancelReplicateExport() console.SetColor("replicateExportMessage", color.New(color.FgGreen)) console.SetColor("replicateExportFailure", color.New(color.FgRed)) checkReplicateExportSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") rCfg, err := client.GetReplication(ctx) fatalIf(err.Trace(args...), "Unable to get replication configuration") printMsg(replicateExportMessage{ Op: cliCtx.Command.Name, Status: "success", URL: aliasedURL, ReplicationConfig: rCfg, }) return nil } minio-client-0.0~20250403/cmd/replicate-import.go000066400000000000000000000074031477450377600213600ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "os" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/pkg/v3/console" ) var replicateImportCmd = cli.Command{ Name: "import", Usage: "import server side replication configuration in JSON format", Action: mainReplicateImport, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Set replication configuration from '/data/replication/config' on bucket "mybucket" for alias "myminio". {{.Prompt}} {{.HelpName}} myminio/mybucket < '/data/replication/config' 2. Import replication configuration for bucket "mybucket" on alias "myminio" from STDIN. {{.Prompt}} {{.HelpName}} myminio/mybucket `, } // checkReplicateImportSyntax - validate all the passed arguments func checkReplicateImportSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } type replicateImportMessage struct { Op string `json:"op"` Status string `json:"status"` URL string `json:"url"` ReplicationConfig replication.Config `json:"config"` } func (r replicateImportMessage) JSON() string { r.Status = "success" jsonMessageBytes, e := json.MarshalIndent(r, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (r replicateImportMessage) String() string { return console.Colorize("replicateImportMessage", "Replication configuration successfully set on `"+r.URL+"`.") } // readReplicationConfig read from stdin, returns XML. func readReplicationConfig() (*replication.Config, *probe.Error) { // User is expected to enter the replication configuration in JSON format cfg := replication.Config{} // Consume json from STDIN dec := json.NewDecoder(os.Stdin) if e := dec.Decode(&cfg); e != nil { return &cfg, probe.NewError(e) } return &cfg, nil } func mainReplicateImport(cliCtx *cli.Context) error { ctx, cancelReplicateImport := context.WithCancel(globalContext) defer cancelReplicateImport() console.SetColor("replicateImportMessage", color.New(color.FgGreen)) checkReplicateImportSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") rCfg, err := readReplicationConfig() fatalIf(err.Trace(args...), "Unable to read replication configuration") fatalIf(client.SetReplication(ctx, rCfg, replication.Options{Op: replication.ImportOption}).Trace(aliasedURL), "Unable to set replication configuration") printMsg(replicateImportMessage{ Op: cliCtx.Command.Name, Status: "success", URL: aliasedURL, }) return nil } minio-client-0.0~20250403/cmd/replicate-list.go000066400000000000000000000122171477450377600210200ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "fmt" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/pkg/v3/console" ) var replicateListFlags = []cli.Flag{ cli.StringFlag{ Name: "status", Usage: "show rules by status. Valid options are [enabled,disabled]", }, } var replicateListCmd = cli.Command{ Name: "list", ShortName: "ls", Usage: "list server side replication configuration rules", Action: mainReplicateList, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, replicateListFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List server side replication configuration rules on bucket "mybucket" for alias "myminio". {{.Prompt}} {{.HelpName}} myminio/mybucket `, } // checkReplicateListSyntax - validate all the passed arguments func checkReplicateListSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func printReplicateListHeader() { if globalJSON { return } console.Println(console.Colorize("Headers", "Rules:")) } type replicateListMessage struct { Op string `json:"op"` Status string `json:"status"` URL string `json:"url"` Rule replication.Rule `json:"rule"` targets []madmin.BucketTarget } func (l replicateListMessage) JSON() string { l.Status = "success" jsonMessageBytes, e := json.MarshalIndent(l, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (l replicateListMessage) String() string { r := l.Rule destBucket := r.Destination.Bucket if arn, err := madmin.ParseARN(r.Destination.Bucket); err == nil { destBucket = arn.Bucket } endpoint := r.Destination.Bucket for _, t := range l.targets { if t.Arn == r.Destination.Bucket { endpoint = t.Endpoint break } } var sb strings.Builder sb.WriteString(console.Colorize("Key", "Remote Bucket: ")) sb.WriteString(console.Colorize("EpVal", fmt.Sprintf("%s/%s\n", endpoint, destBucket))) sb.WriteString(fmt.Sprintf(" Rule ID: %s\n", console.Colorize("Val", r.ID))) sb.WriteString(fmt.Sprintf(" Priority: %s\n", console.Colorize("Val", r.Priority))) if r.Filter.And.Prefix != "" { sb.WriteString(fmt.Sprintf(" Prefix: %s\n", console.Colorize("Val", r.Filter.And.Prefix))) } if r.Tags() != "" { sb.WriteString(fmt.Sprintf(" Tags: %s\n", console.Colorize("Val", r.Tags()))) } if r.Destination.StorageClass != "" && r.Destination.StorageClass != "STANDARD" { sb.WriteString(fmt.Sprintf(" StorageClass: %s\n", console.Colorize("Val", r.Destination.StorageClass))) } return sb.String() + "\n" } func mainReplicateList(cliCtx *cli.Context) error { ctx, cancelReplicateList := context.WithCancel(globalContext) defer cancelReplicateList() console.SetColor("Headers", color.New(color.Bold, color.FgHiGreen)) console.SetColor("Key", color.New(color.Bold, color.FgWhite)) console.SetColor("Val", color.New(color.Bold, color.FgCyan)) console.SetColor("EpVal", color.New(color.Bold, color.FgYellow)) checkReplicateListSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") rCfg, err := client.GetReplication(ctx) fatalIf(err.Trace(args...), "Unable to get replication configuration") if rCfg.Empty() { fatalIf(probe.NewError(errors.New("replication configuration not set")).Trace(aliasedURL), "Unable to list replication configuration") } printReplicateListHeader() // Create a new MinIO Admin Client admClient, cerr := newAdminClient(aliasedURL) fatalIf(cerr, "Unable to initialize admin connection.") _, sourceBucket := url2Alias(args[0]) targets, e := admClient.ListRemoteTargets(globalContext, sourceBucket, "") fatalIf(probe.NewError(e).Trace(args...), "Unable to fetch remote target.") statusFlag := cliCtx.String("status") for _, rule := range rCfg.Rules { if statusFlag == "" || strings.EqualFold(statusFlag, string(rule.Status)) { printMsg(replicateListMessage{ Rule: rule, targets: targets, }) } } return nil } minio-client-0.0~20250403/cmd/replicate-main.go000066400000000000000000000030321477450377600207640ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var replicateSubcommands = []cli.Command{ replicateAddCmd, replicateUpdateCmd, replicateListCmd, replicateStatusCmd, replicateResyncCmd, replicateExportCmd, replicateImportCmd, replicateRemoveCmd, replicateBacklogCmd, } var replicateCmd = cli.Command{ Name: "replicate", Usage: "configure server side bucket replication", HideHelpCommand: true, Action: mainReplicate, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: replicateSubcommands, } // mainReplicate is the handle for "mc replicate" command. func mainReplicate(ctx *cli.Context) error { commandNotFound(ctx, replicateSubcommands) return nil // Sub-commands like "list", "clear", "add" have their own main. } minio-client-0.0~20250403/cmd/replicate-remove.go000066400000000000000000000122011477450377600213330ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/pkg/v3/console" ) var replicateRemoveFlags = []cli.Flag{ cli.StringFlag{ Name: "id", Usage: "id for the rule, should be a unique value", }, cli.BoolFlag{ Name: "force", Usage: "force remove all the replication configuration rules on the bucket", }, cli.BoolFlag{ Name: "all", Usage: "remove all replication configuration rules of the bucket, force flag enforced", }, } var replicateRemoveCmd = cli.Command{ Name: "remove", ShortName: "rm", Usage: "remove a server side replication configuration rule", Action: mainReplicateRemove, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, replicateRemoveFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove replication configuration rule on bucket "mybucket" for alias "myminio" with rule id "bsib5mgt874bi56l0fmg". {{.Prompt}} {{.HelpName}} --id "bsib5mgt874bi56l0fmg" myminio/mybucket 2. Remove all the replication configuration rules on bucket "mybucket" for alias "myminio". --force flag is required. {{.Prompt}} {{.HelpName}} --all --force myminio/mybucket `, } // checkReplicateRemoveSyntax - validate all the passed arguments func checkReplicateRemoveSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } rmAll := ctx.Bool("all") rmForce := ctx.Bool("force") rID := ctx.String("id") rmChk := (rmAll && rmForce) || (!rmAll && !rmForce) if !rmChk { fatalIf(errInvalidArgument(), "It is mandatory to specify --all and --force flag together for mc "+ctx.Command.FullName()+".") } if rmAll && rmForce { return } if rID == "" { fatalIf(errInvalidArgument().Trace(rID), "rule ID cannot be empty") } } type replicateRemoveMessage struct { Op string `json:"op"` Status string `json:"status"` URL string `json:"url"` ID string `json:"id"` } func (l replicateRemoveMessage) JSON() string { l.Status = "success" jsonMessageBytes, e := json.MarshalIndent(l, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (l replicateRemoveMessage) String() string { if l.ID != "" { return console.Colorize("replicateRemoveMessage", "Replication configuration rule with ID `"+l.ID+"`removed from "+l.URL+".") } return console.Colorize("replicateRemoveMessage", "Replication configuration removed from "+l.URL+" successfully.") } func mainReplicateRemove(cliCtx *cli.Context) error { ctx, cancelReplicateRemove := context.WithCancel(globalContext) defer cancelReplicateRemove() console.SetColor("replicateRemoveMessage", color.New(color.FgGreen)) checkReplicateRemoveSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") rcfg, err := client.GetReplication(ctx) fatalIf(err.Trace(args...), "Unable to get replication configuration") rmAll := cliCtx.Bool("all") rmForce := cliCtx.Bool("force") ruleID := cliCtx.String("id") if rcfg.Empty() && !rmAll { printMsg(replicateRemoveMessage{ Op: cliCtx.Command.Name, Status: "success", URL: aliasedURL, }) return nil } if rmAll && rmForce { fatalIf(client.RemoveReplication(ctx), "Unable to remove replication configuration") } else { var removeArn string for _, rule := range rcfg.Rules { if rule.ID == ruleID { removeArn = rule.Destination.Bucket } } opts := replication.Options{ ID: ruleID, Op: replication.RemoveOption, } fatalIf(client.SetReplication(ctx, &rcfg, opts), "Could not remove replication rule") admclient, cerr := newAdminClient(aliasedURL) fatalIf(cerr.Trace(aliasedURL), "Unable to initialize admin connection.") _, sourceBucket := url2Alias(args[0]) fatalIf(probe.NewError(admclient.RemoveRemoteTarget(globalContext, sourceBucket, removeArn)).Trace(args...), "Unable to remove remote target") } printMsg(replicateRemoveMessage{ Op: cliCtx.Command.Name, Status: "success", URL: aliasedURL, ID: ruleID, }) return nil } minio-client-0.0~20250403/cmd/replicate-reset-main.go000066400000000000000000000030031477450377600221020ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var replicateResyncSubcommands = []cli.Command{ replicateResyncStartCmd, replicateResyncStatusCmd, } var replicateResyncCmd = cli.Command{ Name: "resync", Usage: "re-replicate all previously replicated objects", HideHelpCommand: true, Action: mainReplicateResync, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: replicateResyncSubcommands, Aliases: []string{"reset"}, HiddenAliases: true, } // mainReplicateResync is the handle for "mc replicate resync" command. func mainReplicateResync(ctx *cli.Context) error { commandNotFound(ctx, replicateResyncSubcommands) return nil // Sub-commands like "status", "start", have their own main. } minio-client-0.0~20250403/cmd/replicate-reset-start.go000066400000000000000000000112031477450377600223140ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/pkg/v3/console" ) var replicateResyncStartFlags = []cli.Flag{ cli.StringFlag{ Name: "older-than", Usage: "replicate back objects older than value in duration string (e.g. 7d10h31s)", }, cli.StringFlag{ Name: "remote-bucket", Usage: "remote bucket ARN", }, } var replicateResyncStartCmd = cli.Command{ Name: "start", Usage: "start replicating back all previously replicated objects", Action: mainReplicateResyncStart, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, replicateResyncStartFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Re-replicate previously replicated objects in bucket "mybucket" for alias "myminio" for remote target. {{.Prompt}} {{.HelpName}} myminio/mybucket --remote-bucket "arn:minio:replication::xxx:mybucket" 2. Re-replicate all objects older than 60 days in bucket "mybucket" for remote bucket target. {{.Prompt}} {{.HelpName}} myminio/mybucket --older-than 60d --remote-bucket "arn:minio:replication::xxx:mybucket" `, } // checkReplicateResyncStartSyntax - validate all the passed arguments func checkReplicateResyncStartSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } if ctx.String("remote-bucket") == "" { fatal(errDummy().Trace(), "--remote-bucket flag needs to be specified.") } } type replicateResyncMessage struct { Op string `json:"op"` URL string `json:"url"` ResyncTargetsInfo replication.ResyncTargetsInfo `json:"resyncInfo"` Status string `json:"status"` TargetArn string `json:"targetArn"` } func (r replicateResyncMessage) JSON() string { r.Status = "success" jsonMessageBytes, e := json.MarshalIndent(r, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (r replicateResyncMessage) String() string { if len(r.ResyncTargetsInfo.Targets) == 1 { return console.Colorize("replicateResyncMessage", fmt.Sprintf("Replication reset started for %s with ID %s", r.URL, r.ResyncTargetsInfo.Targets[0].ResetID)) } return console.Colorize("replicateResyncMessage", fmt.Sprintf("Replication reset started for %s", r.URL)) } func mainReplicateResyncStart(cliCtx *cli.Context) error { ctx, cancelReplicateResyncStart := context.WithCancel(globalContext) defer cancelReplicateResyncStart() console.SetColor("replicateResyncMessage", color.New(color.FgGreen)) checkReplicateResyncStartSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") var olderThanStr string var olderThan time.Duration if cliCtx.IsSet("older-than") { olderThanStr = cliCtx.String("older-than") if olderThanStr != "" { days, e := ParseDuration(olderThanStr) if e != nil || !strings.ContainsAny(olderThanStr, "dwy") { fatalIf(probe.NewError(e), "Unable to parse older-than=`"+olderThanStr+"`.") } if days == 0 { fatalIf(probe.NewError(e), "older-than cannot be set to zero") } olderThan = time.Duration(days.Days()) } } rinfo, err := client.ResetReplication(ctx, olderThan, cliCtx.String("remote-bucket")) fatalIf(err.Trace(args...), "Unable to reset replication") printMsg(replicateResyncMessage{ Op: cliCtx.Command.Name, URL: aliasedURL, ResyncTargetsInfo: rinfo, }) return nil } minio-client-0.0~20250403/cmd/replicate-reset-status.go000066400000000000000000000130001477450377600224770ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" humanize "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/pkg/v3/console" ) var replicateResyncStatusFlags = []cli.Flag{ cli.StringFlag{ Name: "remote-bucket", Usage: "remote bucket ARN", }, } var replicateResyncStatusCmd = cli.Command{ Name: "status", Usage: "status of replication recovery", Action: mainreplicateResyncStatus, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, replicateResyncStatusFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Status of replication resync in bucket "mybucket" under alias "myminio" for all targets. {{.Prompt}} {{.HelpName}} myminio/mybucket 2. Status of replication resync in bucket "mybucket" under specific remote bucket target. {{.Prompt}} {{.HelpName}} myminio/mybucket --remote-bucket "arn:minio:replication::xxx:mybucket" `, } // checkreplicateResyncStatusSyntax - validate all the passed arguments func checkreplicateResyncStatusSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } type replicateResyncStatusMessage struct { Op string `json:"op"` URL string `json:"url"` ResyncTargetsInfo replication.ResyncTargetsInfo `json:"resyncInfo"` Status string `json:"status"` TargetArn string `json:"targetArn"` } func (r replicateResyncStatusMessage) JSON() string { r.Status = "success" jsonMessageBytes, e := json.MarshalIndent(r, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (r replicateResyncStatusMessage) String() string { if len(r.ResyncTargetsInfo.Targets) == 0 { return console.Colorize("replicateResyncStatusWarn", "No replication resync status available.") } coloredDot := console.Colorize("Headers", dot) var rows string rows += console.Colorize("TDetail", "Resync status summary:") for _, st := range r.ResyncTargetsInfo.Targets { rows += "\n" rows += console.Colorize("replicateResyncStatusMsg", newPrettyTable(" | ", Field{"ARN", 120}, ).buildRow(fmt.Sprintf("%s %s", coloredDot, st.Arn))) rows += "\n" rows += console.Colorize("TDetail", " Status: ") rows += console.Colorize(st.ResyncStatus, st.ResyncStatus) rows += "\n" maxLen := 15 theme := []string{"Replicated", "Failed"} rows += console.Colorize("THeaders", newPrettyTable(" | ", Field{"Status", 21}, Field{"Size", maxLen}, Field{"Count", maxLen}, ).buildRow(" Replication Status", "Size (Bytes)", "Count")) rows += "\n" rows += console.Colorize(theme[0], newPrettyTable(" | ", Field{"Status", 21}, Field{"Size", maxLen}, Field{"Count", maxLen}, ).buildRow(" Replicated", humanize.IBytes(uint64(st.ReplicatedSize)), humanize.Comma(int64(st.ReplicatedCount)))) rows += "\n" rows += console.Colorize(theme[0], newPrettyTable(" | ", Field{"Status", 21}, Field{"Size", maxLen}, Field{"Count", maxLen}, ).buildRow(" Failed", humanize.IBytes(uint64(st.FailedSize)), humanize.Comma(int64(st.FailedCount)))) rows += "\n" } return rows } func mainreplicateResyncStatus(cliCtx *cli.Context) error { ctx, cancelreplicateResyncStatus := context.WithCancel(globalContext) defer cancelreplicateResyncStatus() console.SetColor("replicateResyncStatusWarn", color.New(color.FgHiYellow)) console.SetColor("replicateResyncStatusMsg", color.New(color.FgGreen)) console.SetColor("Headers", color.New(color.FgGreen, color.Bold)) console.SetColor("THeaders", color.New(color.Bold, color.FgCyan)) console.SetColor("TDetail", color.New(color.FgWhite, color.Bold)) console.SetColor("Ongoing", color.New(color.Bold, color.FgYellow)) console.SetColor("Failed", color.New(color.Bold, color.FgRed)) console.SetColor("Completed", color.New(color.Bold, color.FgGreen)) checkreplicateResyncStatusSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") rinfo, err := client.ReplicationResyncStatus(ctx, cliCtx.String("remote-bucket")) fatalIf(err.Trace(args...), "Unable to get replication resync status") printMsg(replicateResyncStatusMessage{ Op: cliCtx.Command.Name, URL: aliasedURL, ResyncTargetsInfo: rinfo, TargetArn: cliCtx.String("remote-bucket"), }) return nil } minio-client-0.0~20250403/cmd/replicate-status.go000066400000000000000000000372501477450377600213740ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "hash/fnv" "math" "sort" "strings" "time" humanize "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/pkg/v3/console" "github.com/olekukonko/tablewriter" ) var replicateStatusFlags = []cli.Flag{ cli.StringFlag{ Name: "backlog,b", Usage: "show most recent failures for one or more nodes. Valid values are 'all', or node name", Value: "all", }, cli.BoolFlag{ Name: "nodes,n", Usage: "show replication speed for all nodes", }, } var replicateStatusCmd = cli.Command{ Name: "status", Usage: "show server side replication status", Action: mainReplicateStatus, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, replicateStatusFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET/BUCKET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Get server side replication metrics for bucket "mybucket" for alias "myminio". {{.Prompt}} {{.HelpName}} myminio/mybucket 2. Get replication speed across nodes for bucket "mybucket" for alias "myminio". {{.Prompt}} {{.HelpName}} --nodes myminio/mybucket `, } // checkReplicateStatusSyntax - validate all the passed arguments func checkReplicateStatusSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } type replicateStatusMessage struct { Op string `json:"op"` URL string `json:"url"` Status string `json:"status"` Metrics replication.MetricsV2 `json:"replicationstats"` Targets []madmin.BucketTarget `json:"remoteTargets"` cfg replication.Config `json:"-"` } func (s replicateStatusMessage) JSON() string { s.Status = "success" jsonMessageBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (s replicateStatusMessage) String() string { q := s.Metrics.QueueStats rs := s.Metrics.CurrentStats if s.cfg.Empty() { return "Replication is not configured." } var ( replSz = rs.ReplicatedSize replCount = rs.ReplicatedCount replicaCount = rs.ReplicaCount replicaSz = rs.ReplicaSize failed = rs.Errors qs = q.QStats() ) for arn, st := range rs.Stats { // Remove stale ARNs from stats staleARN := true for _, r := range s.cfg.Rules { if r.Destination.Bucket == arn || s.cfg.Role == arn { staleARN = false break } } if staleARN { replSz -= st.ReplicatedSize replCount -= int64(st.ReplicatedCount) } } // normalize stats, avoid negative values replSz = uint64(math.Max(float64(replSz), 0)) if replCount < 0 { replCount = 0 } // for queue stats qtots := rs.QStats coloredDot := console.Colorize("qStatusOK", dot) if qtots.Curr.Count > qtots.Avg.Count { coloredDot = console.Colorize("qStatusWarn", dot) } var sb strings.Builder // Set table header table := tablewriter.NewWriter(&sb) table.SetAutoWrapText(false) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetRowLine(false) table.SetBorder(false) table.SetTablePadding("\t") // pad with tabs uiFn := func(theme string) func(string) string { return func(s string) string { return console.Colorize(theme, s) } } titleui := uiFn("title") valueui := uiFn("value") hdrui := uiFn("THeaderBold") keyui := uiFn("key") maxui := uiFn("Peak") avgui := uiFn("Avg") addRowF := func(format string, vals ...interface{}) { s := fmt.Sprintf(format, vals...) table.Append([]string{s}) } var arns []string for arn := range rs.Stats { arns = append(arns, arn) } sort.Strings(arns) addRowF(titleui("Replication status since %s"), humanize.RelTime(time.Now(), time.Now().Add(time.Duration(s.Metrics.Uptime)*time.Second), "", "ago")) singleTgt := len(arns) == 1 staleARN := false for i, arn := range arns { if i > 0 && !staleARN { addRowF("\n") } staleARN = true for _, r := range s.cfg.Rules { if r.Destination.Bucket == arn || s.cfg.Role == arn { staleARN = false break } } if staleARN { continue // skip historic metrics for deleted targets } var ep string var tgt madmin.BucketTarget for _, t := range s.Targets { if t.Arn == arn { ep = t.Endpoint tgt = t break } } nodeName := ep if nodeName == "" { nodeName = arn } nodeui := uiFn(getNodeTheme(nodeName)) currDowntime := time.Duration(0) if !tgt.Online && !tgt.LastOnline.IsZero() { currDowntime = UTCNow().Sub(tgt.LastOnline) } // normalize because total downtime is calculated at server side at heartbeat interval, may be slightly behind totalDowntime := tgt.TotalDowntime if currDowntime > totalDowntime { totalDowntime = currDowntime } nodeStr := nodeui(nodeName) addRowF(nodeui(nodeStr)) stat, ok := rs.Stats[arn] if ok { addRowF(titleui("Replicated: ")+humanize.Comma(int64(stat.ReplicatedCount))+keyui(" objects")+" (%s", valueui(humanize.IBytes(stat.ReplicatedSize))+")") } healthDot := console.Colorize("online", dot) if !tgt.Online { healthDot = console.Colorize("offline", dot) } var linkStatus string if tgt.Online { linkStatus = healthDot + fmt.Sprintf(" online (total downtime: %s)", valueui(timeDurationToHumanizedDuration(totalDowntime).String())) } else { linkStatus = healthDot + fmt.Sprintf(" offline %s (total downtime: %s)", valueui(timeDurationToHumanizedDuration(currDowntime).String()), valueui(timeDurationToHumanizedDuration(totalDowntime).String())) } if singleTgt { // for single target - combine summary section into the target section addRowF(titleui("Queued: ") + coloredDot + " " + humanize.Comma(int64(qtots.Curr.Count)) + keyui(" objects, ") + valueui(humanize.IBytes(uint64(qtots.Curr.Bytes))) + " (" + avgui("avg") + ": " + humanize.Comma(int64(qtots.Avg.Count)) + keyui(" objects, ") + valueui(humanize.IBytes(uint64(qtots.Avg.Bytes))) + " ; " + maxui("max:") + " " + humanize.Comma(int64(qtots.Max.Count)) + keyui(" objects, ") + valueui(humanize.IBytes(uint64(qtots.Max.Bytes))) + ")") addRowF(titleui("Workers: ") + valueui(humanize.Comma(int64(qs.Workers.Curr))) + avgui(" (avg: ") + humanize.Comma(int64(qs.Workers.Avg)) + maxui("; max: ") + humanize.Comma(int64(qs.Workers.Max)) + ")") } tgtXfer := qs.TgtXferStats[arn][replication.Total] addRowF(titleui("Transfer Rate: ")+"%s/s ("+keyui("avg: ")+"%s/s"+keyui("; max: ")+"%s/s", valueui(humanize.Bytes(uint64(tgtXfer.CurrRate))), valueui(humanize.Bytes(uint64(tgtXfer.AvgRate))), valueui(humanize.Bytes(uint64(tgtXfer.PeakRate)))) addRowF(titleui("Latency: ")+"%s ("+keyui("avg: ")+"%s"+keyui("; max: ")+"%s)", valueui(tgt.Latency.Curr.Round(time.Millisecond).String()), valueui(tgt.Latency.Avg.Round(time.Millisecond).String()), valueui(tgt.Latency.Max.Round(time.Millisecond).String())) addRowF(titleui("Link: %s"), linkStatus) addRowF(titleui("Errors: ")+"%s in last 1 minute; %s in last 1hr; %s since uptime", valueui(humanize.Comma(int64(stat.Failed.LastMinute.Count))), valueui(humanize.Comma(int64(stat.Failed.LastHour.Count))), valueui(humanize.Comma(int64(stat.Failed.Totals.Count)))) bwStat, ok := rs.Stats[arn] if ok && bwStat.BandWidthLimitInBytesPerSecond > 0 { limit := "N/A" // N/A means cluster bandwidth is not configured current := "N/A" // N/A means cluster bandwidth is not configured if bwStat.CurrentBandwidthInBytesPerSecond > 0 { current = humanize.Bytes(uint64(bwStat.CurrentBandwidthInBytesPerSecond)) current = fmt.Sprintf("%s/s", current) } if bwStat.BandWidthLimitInBytesPerSecond > 0 { limit = humanize.Bytes(uint64(bwStat.BandWidthLimitInBytesPerSecond)) limit = fmt.Sprintf("%s/s", limit) } addRowF(titleui("Configured Max Bandwidth (Bps): ")+"%s"+titleui(" Current Bandwidth (Bps): ")+"%s", valueui(limit), valueui(current)) } } if !singleTgt { xfer := qs.XferStats[replication.Total] addRowF(hdrui("\nSummary:")) addRowF(titleui("Replicated: ")+humanize.Comma(int64(replCount))+keyui(" objects")+" (%s", valueui(humanize.IBytes(replSz))+")") addRowF(titleui("Queued: ") + coloredDot + " " + humanize.Comma(int64(qtots.Curr.Count)) + keyui(" objects, ") + valueui(humanize.IBytes(uint64(qtots.Curr.Bytes))) + " (" + avgui("avg") + ": " + humanize.Comma(int64(qtots.Avg.Count)) + keyui(" objects, ") + valueui(humanize.IBytes(uint64(qtots.Avg.Bytes))) + " ; " + maxui("max:") + " " + humanize.Comma(int64(qtots.Max.Count)) + keyui(" objects, ") + valueui(humanize.IBytes(uint64(qtots.Max.Bytes))) + ")") addRowF(titleui("Workers: ") + valueui(humanize.Comma(int64(qs.Workers.Curr))) + avgui(" (avg: ") + humanize.Comma(int64(qs.Workers.Avg)) + maxui("; max: ") + humanize.Comma(int64(qs.Workers.Max)) + ")") addRowF(titleui("Received: ")+"%s"+keyui(" objects")+" (%s)", humanize.Comma(int64(replicaCount)), valueui(humanize.IBytes(uint64(replicaSz)))) addRowF(titleui("Transfer Rate: ")+"%s/s"+avgui(" (avg: ")+"%s/s"+maxui("; max: ")+"%s/s)", valueui(humanize.Bytes(uint64(xfer.CurrRate))), valueui(humanize.Bytes(uint64(xfer.AvgRate))), valueui(humanize.Bytes(uint64(xfer.PeakRate)))) addRowF(titleui("Errors: ")+"%s in last 1 minute; %s in last 1hr; %s since uptime", valueui(humanize.Comma(int64(failed.LastMinute.Count))), valueui(humanize.Comma(int64(failed.LastHour.Count))), valueui(humanize.Comma(int64(failed.Totals.Count)))) } table.Render() return sb.String() } func mainReplicateStatus(cliCtx *cli.Context) error { ctx, cancelReplicateStatus := context.WithCancel(globalContext) defer cancelReplicateStatus() console.SetColor("title", color.New(color.FgCyan)) console.SetColor("value", color.New(color.FgWhite, color.Bold)) console.SetColor("key", color.New(color.FgWhite)) console.SetColor("THeaderBold", color.New(color.Bold, color.FgWhite)) console.SetColor("Replica", color.New(color.FgCyan)) console.SetColor("Failed", color.New(color.Bold, color.FgRed)) for _, c := range colors { console.SetColor(fmt.Sprintf("Node%d", c), color.New(c)) } console.SetColor("Replicated", color.New(color.FgCyan)) console.SetColor("In-Queue", color.New(color.Bold, color.FgYellow)) console.SetColor("Avg", color.New(color.FgCyan)) console.SetColor("Peak", color.New(color.FgYellow)) console.SetColor("Current", color.New(color.FgCyan)) console.SetColor("Uptime", color.New(color.FgWhite)) console.SetColor("qStatusWarn", color.New(color.FgYellow, color.Bold)) console.SetColor("qStatusOK", color.New(color.FgGreen, color.Bold)) console.SetColor("online", color.New(color.FgGreen, color.Bold)) console.SetColor("offline", color.New(color.FgRed, color.Bold)) for _, c := range colors { console.SetColor(fmt.Sprintf("Node%d", c), color.New(color.Bold, c)) } checkReplicateStatusSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") // Create a new MinIO Admin Client admClient, cerr := newAdminClient(aliasedURL) fatalIf(cerr, "Unable to initialize admin connection.") _, sourceBucket := url2Alias(args[0]) replicateStatus, err := client.GetReplicationMetrics(ctx) fatalIf(err.Trace(args...), "Unable to get replication status") targets, e := admClient.ListRemoteTargets(globalContext, sourceBucket, "") fatalIf(probe.NewError(e).Trace(args...), "Unable to fetch remote target.") cfg, err := client.GetReplication(ctx) fatalIf(err.Trace(args...), "Unable to fetch replication configuration.") if cliCtx.IsSet("nodes") { printMsg(replicateXferMessage{ Op: cliCtx.Command.Name, Status: "success", ReplQueueStats: replicateStatus.QueueStats, }) return nil } printMsg(replicateStatusMessage{ Op: cliCtx.Command.Name, URL: aliasedURL, Metrics: replicateStatus, Targets: targets, cfg: cfg, }) return nil } type replicateXferMessage struct { Op string `json:"op"` Status string `json:"status"` replication.ReplQueueStats } func (m replicateXferMessage) JSON() string { m.Status = "success" jsonMessageBytes, e := json.MarshalIndent(m, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (m replicateXferMessage) String() string { var rows []string maxLen := 0 for _, rqs := range m.Nodes { if len(rqs.NodeName) > maxLen { maxLen = len(rqs.NodeName) } lrgX := rqs.XferStats[replication.Large] smlX := rqs.XferStats[replication.Small] rows = append(rows, console.Colorize("", newPrettyTable(" | ", Field{getNodeTheme(rqs.NodeName), len(rqs.NodeName) + 3}, Field{"Uptime:", 15}, Field{"Lbl", 25}, Field{"Avg", 12}, Field{"Peak", 12}, Field{"Current", 12}, Field{"Workers", 10}, ).buildRow(rqs.NodeName, humanize.RelTime(time.Now(), time.Now().Add(time.Duration(rqs.Uptime)*time.Second), "", ""), "Large Objects (>=128 MiB)", fmt.Sprintf("%s/s", humanize.Bytes(uint64(lrgX.AvgRate))), fmt.Sprintf("%s/s", humanize.Bytes(uint64(lrgX.PeakRate))), fmt.Sprintf("%s/s", humanize.Bytes(uint64(lrgX.CurrRate))), fmt.Sprintf("%d", int(rqs.Workers.Avg))))) rows = append(rows, console.Colorize("", newPrettyTable(" | ", Field{getNodeTheme(rqs.NodeName), len(rqs.NodeName) + 3}, Field{"Uptime:", 15}, Field{"Lbl", 25}, Field{"Avg", 12}, Field{"Peak", 12}, Field{"Current", 12}, Field{"Workers", 10}, ).buildRow(rqs.NodeName, humanize.RelTime(time.Now(), time.Now().Add(time.Duration(rqs.Uptime)*time.Second), "", ""), "Small Objects (<128 MiB)", fmt.Sprintf("%s/s", humanize.Bytes(uint64(smlX.AvgRate))), fmt.Sprintf("%s/s", humanize.Bytes(uint64(smlX.PeakRate))), fmt.Sprintf("%s/s", humanize.Bytes(uint64(smlX.CurrRate))), fmt.Sprintf("%d", int(rqs.Workers.Avg))))) } hdrSlc := []string{ console.Colorize("THeaderBold", newPrettyTable(" | ", Field{"", maxLen + 3}, Field{"Uptime:", 15}, Field{"Lbl", 25}, Field{"XferRate", 42}, Field{"Workers", 12}).buildRow("Node Name", "Uptime", "Label", " Transfer Rate ", "Workers")), console.Colorize("THeaderBold", newPrettyTable(" | ", Field{"", maxLen + 3}, Field{"Uptime:", 15}, Field{"Lbl", 25}, Field{"Avg", 12}, Field{"Peak", 12}, Field{"Current", 12}, Field{"Workers", 10}).buildRow("", "", "", "Avg", "Peak", "Current", "")), } return strings.Join(append(hdrSlc, rows...), "\n") } // colorize node name func getNodeTheme(nodeName string) string { nodeHash := fnv.New32a() nodeHash.Write([]byte(nodeName)) nHashSum := nodeHash.Sum32() idx := nHashSum % uint32(len(colors)) return fmt.Sprintf("Node%d", colors[idx]) } minio-client-0.0~20250403/cmd/replicate-update.go000066400000000000000000000334701477450377600213330ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "path" "strconv" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/minio-go/v7/pkg/s3utils" "github.com/minio/pkg/v3/console" ) var replicateUpdateFlags = []cli.Flag{ cli.StringFlag{ Name: "id", Usage: "id for the rule, should be a unique value", }, cli.StringFlag{ Name: "tags", Usage: "format '=&=&=', multiple values allowed for multiple key/value pairs", }, cli.StringFlag{ Name: "storage-class", Usage: `storage class for destination, valid values are ['STANDARD', 'REDUCED_REDUNDANCY']`, }, cli.StringFlag{ Name: "state", Usage: "change rule status, valid values are ['enable', 'disable']", }, cli.IntFlag{ Name: "priority", Usage: "priority of the rule, should be unique and is a required field", }, cli.StringFlag{ Name: "remote-bucket", Usage: "destination bucket, should be a unique value for the configuration", }, cli.StringFlag{ Name: "replicate", Usage: `comma separated list to enable replication of soft deletes, permanent deletes, existing objects and metadata sync. Valid options are "delete-marker","delete","existing-objects","metadata-sync" and ""'`, }, cli.StringFlag{ Name: "sync", Usage: "enable synchronous replication for this target, valid values are ['enable', 'disable'].", Value: "disable", }, cli.StringFlag{ Name: "proxy", Usage: "enable proxying in active-active replication, valid values are ['enable', 'disable']", Value: "enable", }, cli.StringFlag{ Name: "bandwidth", Usage: "Set bandwidth limit in bytes per second (K,B,G,T for metric and Ki,Bi,Gi,Ti for IEC units)", }, cli.UintFlag{ Name: "healthcheck-seconds", Usage: "health check duration in seconds", Value: 60, }, cli.StringFlag{ Name: "path", Value: "auto", Usage: "bucket path lookup supported by the server, valid options are ['on', 'off', 'auto']", }, } var replicateUpdateCmd = cli.Command{ Name: "update", Aliases: []string{"edit"}, HiddenAliases: true, Usage: "modify an existing server side replication configuration rule", Action: mainReplicateUpdate, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, replicateUpdateFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET --id=RULE-ID [FLAGS] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Change priority of rule with rule ID "bsibgh8t874dnjst8hkg" on bucket "mybucket" for alias "myminio". {{.Prompt}} {{.HelpName}} myminio/mybucket --id "bsibgh8t874dnjst8hkg" --priority 3 2. Disable a replication configuration rule with rule ID "bsibgh8t874dnjst8hkg" on target myminio/bucket {{.Prompt}} {{.HelpName}} myminio/mybucket --id "bsibgh8t874dnjst8hkg" --state disable 3. Set tags and storage class on a replication configuration with rule ID "kMYD.491" on target myminio/bucket/prefix. {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kMYD.491" --tags "key1=value1&key2=value2" \ --storage-class "STANDARD" --priority 2 4. Clear tags for replication configuration rule with ID "kMYD.491" on a target myminio/bucket. {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kMYD.491" --tags "" 5. Enable delete marker replication on a replication configuration rule with ID "kxYD.491" on a target myminio/bucket. {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kxYD.491" --replicate "delete-marker" 6. Disable delete marker and versioned delete replication on a replication configuration rule with ID "kxYD.491" on a target myminio/bucket. {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kxYD.491" --replicate "" 7. Enable existing object replication on a configuration rule with ID "kxYD.491" on a target myminio/bucket. Rule previously had enabled delete marker and versioned delete replication. {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kxYD.491" --replicate "existing-objects,delete-marker,delete" 8. Edit credentials for remote target with replication rule ID kxYD.491 {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kxYD.491" --remote-bucket https://foobar:newpassword@minio.siteb.example.com/targetbucket 9. Edit credentials with alias "targetminio" for remote target with replication rule ID kxYD.491 {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kxYD.491" --remote-bucket targetminio/targetbucket 10. Disable proxying and enable synchronous replication for remote target of bucket mybucket with rule ID kxYD.492 {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kxYD.492" --remote-bucket https://foobar:newpassword@minio.siteb.example.com/targetbucket \ --sync "enable" --proxy "disable" `, } // checkReplicateUpdateSyntax - validate all the passed arguments func checkReplicateUpdateSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // modifyRemoteTarget - modifies the dest credentials or updates sync , disable-proxy settings func modifyRemoteTarget(cli *cli.Context, targets []madmin.BucketTarget, arnStr string) (*madmin.BucketTarget, []madmin.TargetUpdateType) { args := cli.Args() foundIdx := -1 for i, t := range targets { if t.Arn == arnStr { arn, e := madmin.ParseARN(arnStr) if e != nil { fatalIf(errInvalidArgument().Trace(args...), "Malformed ARN `"+arnStr+"` in replication config") } if arn.Bucket != t.TargetBucket { fatalIf(errInvalidArgument().Trace(args...), "Expected remote bucket %s, got %s for rule id %s", t.TargetBucket, arn.Bucket, cli.String("id")) } foundIdx = i break } } if foundIdx < 0 { fatalIf(errInvalidArgument().Trace(args...), "`"+arnStr+"` not found in replication config") } var ops []madmin.TargetUpdateType bktTarget := targets[foundIdx].Clone() if cli.IsSet("sync") { syncState := strings.ToLower(cli.String("sync")) switch syncState { case "enable", "disable": bktTarget.ReplicationSync = syncState == "enable" ops = append(ops, madmin.SyncUpdateType) default: fatalIf(errInvalidArgument().Trace(args...), "--sync can be either [enable|disable]") } } if cli.IsSet("proxy") { proxyState := strings.ToLower(cli.String("proxy")) switch proxyState { case "enable", "disable": bktTarget.DisableProxy = proxyState == "disable" ops = append(ops, madmin.ProxyUpdateType) default: fatalIf(errInvalidArgument().Trace(args...), "--proxy can be either [enable|disable]") } } if len(args) == 1 { _, sourceBucket := url2Alias(args[0]) tgtURL := cli.String("remote-bucket") accessKey, secretKey, u := extractCredentialURL(tgtURL) var tgtBucket string if u.Path != "" { tgtBucket = path.Clean(u.Path[1:]) } fatalIf(probe.NewError(s3utils.CheckValidBucketName(tgtBucket)).Trace(tgtURL), "invalid target bucket") secure := u.Scheme == "https" host := u.Host if u.Port() == "" { port := 80 if secure { port = 443 } host = host + ":" + strconv.Itoa(port) } console.SetColor(cred, color.New(color.FgYellow, color.Italic)) creds := &madmin.Credentials{AccessKey: accessKey, SecretKey: secretKey} if tgtBucket != bktTarget.TargetBucket { fatalIf(errInvalidArgument().Trace(args...), "configured remote target bucket `"+tgtBucket+"` does not match "+bktTarget.TargetBucket+"` for this ARN `"+bktTarget.Arn+"`") } if sourceBucket != bktTarget.SourceBucket { fatalIf(errInvalidArgument().Trace(args...), "configured source bucket `"+sourceBucket+"` does not match "+bktTarget.SourceBucket+"` for this ARN `"+bktTarget.Arn+"`") } bktTarget.TargetBucket = tgtBucket bktTarget.Secure = secure bktTarget.Credentials = creds bktTarget.Endpoint = host ops = append(ops, madmin.CredentialsUpdateType) } if cli.IsSet("bandwidth") { bandwidthStr := cli.String("bandwidth") bandwidth, e := getBandwidthInBytes(bandwidthStr) fatalIf(probe.NewError(e).Trace(bandwidthStr), "invalid bandwidth value") bktTarget.BandwidthLimit = int64(bandwidth) ops = append(ops, madmin.BandwidthLimitUpdateType) } if cli.IsSet("healthcheck-seconds") { bktTarget.HealthCheckDuration = time.Duration(cli.Uint("healthcheck-seconds")) * time.Second ops = append(ops, madmin.HealthCheckDurationUpdateType) } if cli.IsSet("path") { bktTarget.Path = cli.String("path") ops = append(ops, madmin.PathUpdateType) } return &bktTarget, ops } type replicateUpdateMessage struct { Op string `json:"op"` Status string `json:"status"` URL string `json:"url"` ID string `json:"id"` } func (l replicateUpdateMessage) JSON() string { l.Status = "success" jsonMessageBytes, e := json.MarshalIndent(l, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (l replicateUpdateMessage) String() string { if l.ID != "" { return console.Colorize("replicateUpdateMessage", "Replication configuration rule with ID `"+l.ID+"` applied to "+l.URL+".") } return console.Colorize("replicateUpdateMessage", "Replication configuration rule applied to "+l.URL+" successfully.") } func mainReplicateUpdate(cliCtx *cli.Context) error { ctx, cancelReplicateUpdate := context.WithCancel(globalContext) defer cancelReplicateUpdate() console.SetColor("replicateUpdateMessage", color.New(color.FgGreen)) checkReplicateUpdateSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "unable to initialize connection.") rcfg, err := client.GetReplication(ctx) fatalIf(err.Trace(args...), "unable to get replication configuration") if !cliCtx.IsSet("id") { fatalIf(errInvalidArgument(), "--id is a required flag") } var state string if cliCtx.IsSet("state") { state = strings.ToLower(cliCtx.String("state")) if state != "enable" && state != "disable" { fatalIf(err.Trace(args...), "--state can be either `enable` or `disable`") } } var sourceBucket string switch c := client.(type) { case *S3Client: sourceBucket, _ = c.url2BucketAndObject() default: fatalIf(err.Trace(args...), "replication is not supported for filesystem") } // Create a new MinIO Admin Client admClient, err := newAdminClient(aliasedURL) fatalIf(err, "unable to initialize admin connection.") targets, e := admClient.ListRemoteTargets(globalContext, sourceBucket, "") fatalIf(probe.NewError(e).Trace(args...), "unable to fetch remote target.") var arn string for _, rule := range rcfg.Rules { if rule.ID == cliCtx.String("id") { arn = rule.Destination.Bucket if rcfg.Role != "" { arn = rcfg.Role } break } } if cliCtx.IsSet("remote-bucket") { bktTarget, ops := modifyRemoteTarget(cliCtx, targets, arn) _, e = admClient.UpdateRemoteTarget(globalContext, bktTarget, ops...) if e != nil { fatalIf(probe.NewError(e).Trace(args...), "Unable to update remote target `"+bktTarget.Endpoint+"` from `"+bktTarget.SourceBucket+"` -> `"+bktTarget.TargetBucket+"`") } } else { if cliCtx.IsSet("sync") || cliCtx.IsSet("bandwidth") || cliCtx.IsSet("proxy") || cliCtx.IsSet("healthcheck-seconds") || cliCtx.IsSet("path") { fatalIf(errInvalidArgument().Trace(args...), "--remote-bucket is a required flag`") } } var vDeleteReplicate, dmReplicate, replicasync, existingReplState string if cliCtx.IsSet("replicate") { replSlice := strings.Split(cliCtx.String("replicate"), ",") vDeleteReplicate = disableStatus dmReplicate = disableStatus replicasync = disableStatus existingReplState = disableStatus for _, opt := range replSlice { switch strings.TrimSpace(strings.ToLower(opt)) { case "delete-marker": dmReplicate = enableStatus case "delete": vDeleteReplicate = enableStatus case "metadata-sync", "replica-metadata-sync": replicasync = enableStatus case "existing-objects": existingReplState = enableStatus default: if opt != "" { fatalIf(probe.NewError(fmt.Errorf("invalid value for --replicate flag %s", cliCtx.String("replicate"))), `--replicate flag takes one or more comma separated string with values "delete", "delete-marker", "metadata-sync", "existing-objects" or "" to disable these settings`) } } } } opts := replication.Options{ TagString: cliCtx.String("tags"), RoleArn: cliCtx.String("arn"), StorageClass: cliCtx.String("storage-class"), RuleStatus: state, ID: cliCtx.String("id"), Op: replication.SetOption, DestBucket: arn, IsSCSet: cliCtx.IsSet("storage-class"), IsTagSet: cliCtx.IsSet("tags"), } if cliCtx.IsSet("priority") { opts.Priority = strconv.Itoa(cliCtx.Int("priority")) } if cliCtx.IsSet("replicate") { opts.ReplicateDeletes = vDeleteReplicate opts.ReplicateDeleteMarkers = dmReplicate opts.ReplicaSync = replicasync opts.ExistingObjectReplicate = existingReplState } fatalIf(client.SetReplication(ctx, &rcfg, opts), "unable to modify replication rule") printMsg(replicateUpdateMessage{ Op: cliCtx.Command.Name, URL: aliasedURL, ID: opts.ID, }) return nil } minio-client-0.0~20250403/cmd/retention-clear.go000066400000000000000000000110161477450377600211660ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "time" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) var retentionClearFlags = []cli.Flag{ cli.BoolFlag{ Name: "recursive, r", Usage: "clear retention recursively", }, cli.StringFlag{ Name: "version-id, vid", Usage: "clear retention of a specific object version", }, cli.StringFlag{ Name: "rewind", Usage: "roll back object(s) to current version at specified time", }, cli.BoolFlag{ Name: "versions", Usage: "clear retention of object(s) and all its versions", }, cli.BoolFlag{ Name: "default", Usage: "set default bucket locking", }, } var retentionClearCmd = cli.Command{ Name: "clear", Usage: "clear all retention settings on object(s)", Action: mainRetentionClear, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(retentionClearFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Clear object retention for a specific object $ {{.HelpName}} myminio/mybucket/prefix/obj.csv 2. Clear object retention for recursively for all objects at a given prefix $ {{.HelpName}} myminio/mybucket/prefix --recursive 3. Clear object retention for a specific version of a specific object $ {{.HelpName}} myminio/mybucket/prefix/obj.csv --version-id "3Jr2x6fqlBUsVzbvPihBO3HgNpgZgAnp" 4. Clear object retention for recursively for all versions of all objects $ {{.HelpName}} myminio/mybucket/prefix --recursive --versions 5. Clear object retention for recursively for all versions created one year ago $ {{.HelpName}} myminio/mybucket/prefix --recursive --versions --rewind 365d 6. Clear a bucket retention configuration $ {{.HelpName}} --default myminio/mybucket/ `, } func parseClearRetentionArgs(cliCtx *cli.Context) (target, versionID string, timeRef time.Time, withVersions, recursive, bucketMode bool) { args := cliCtx.Args() if len(args) != 1 { showCommandHelpAndExit(cliCtx, 1) } target = args[0] if target == "" { fatalIf(errInvalidArgument().Trace(), "invalid target url '%v'", target) } versionID = cliCtx.String("version-id") timeRef = parseRewindFlag(cliCtx.String("rewind")) withVersions = cliCtx.Bool("versions") recursive = cliCtx.Bool("recursive") bucketMode = cliCtx.Bool("default") if bucketMode && (versionID != "" || !timeRef.IsZero() || withVersions || recursive) { fatalIf(errDummy(), "--default cannot be specified with any of --version-id, --rewind, --versions or --recursive.") } return } // Clear Retention for one object/version or many objects within a given prefix, bypass governance is always enabled func clearRetention(ctx context.Context, target, versionID string, timeRef time.Time, withVersions, isRecursive bool) error { return applyRetention(ctx, lockOpClear, target, versionID, timeRef, withVersions, isRecursive, "", 0, minio.Days, true) } func clearBucketLock(urlStr string) error { return applyBucketLock(lockOpClear, urlStr, "", 0, "") } // main for retention clear command. func mainRetentionClear(cliCtx *cli.Context) error { ctx, cancelSetRetention := context.WithCancel(globalContext) defer cancelSetRetention() console.SetColor("RetentionSuccess", color.New(color.FgGreen, color.Bold)) console.SetColor("RetentionFailure", color.New(color.FgYellow)) target, versionID, rewind, withVersions, recursive, bucketMode := parseClearRetentionArgs(cliCtx) fatalIfBucketLockNotSupported(ctx, target) if bucketMode { return clearBucketLock(target) } if withVersions && rewind.IsZero() { rewind = time.Now().UTC() } return clearRetention(ctx, target, versionID, rewind, withVersions, recursive) } minio-client-0.0~20250403/cmd/retention-common.go000066400000000000000000000213621477450377600213750ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "strconv" "time" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) // Structured message depending on the type of console. type retentionCmdMessage struct { Op lockOpType `json:"op"` Mode minio.RetentionMode `json:"mode"` Validity string `json:"validity"` URLPath string `json:"urlpath"` VersionID string `json:"versionID"` Status string `json:"status"` Err error `json:"error"` } // Colorized message for console printing. func (m retentionCmdMessage) String() string { var color, msg string ed := "" if m.Op == lockOpClear { ed = "ed" } if m.Err != nil { color = "RetentionFailure" msg = fmt.Sprintf("Unable to %s object retention on `%s`: %s", m.Op, m.URLPath, m.Err) } else { color = "RetentionSuccess" msg = fmt.Sprintf("Object retention successfully %s%s for `%s`", m.Op, ed, m.URLPath) } if m.VersionID != "" { msg += fmt.Sprintf(" (version-id=%s)", m.VersionID) } msg += "." return console.Colorize(color, msg) } // JSON'ified message for scripting. func (m retentionCmdMessage) JSON() string { if m.Err != nil { m.Status = "failure" } msgBytes, e := json.MarshalIndent(m, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } type lockOpType string const ( lockOpInfo = "info" lockOpClear = "clear" lockOpSet = "set" ) // Structured message depending on the type of console. type retentionBucketMessage struct { Op lockOpType `json:"op"` Enabled string `json:"enabled"` Mode minio.RetentionMode `json:"mode"` Validity string `json:"validity"` Status string `json:"status"` } // Colorized message for console printing. func (m retentionBucketMessage) String() string { if m.Op == lockOpClear { return console.Colorize("RetentionSuccess", "Object lock configuration cleared successfully.") } // info/set command if !m.Mode.IsValid() { return console.Colorize("RetentionNotFound", "Object locking is not enabled.") } return console.Colorize("RetentionSuccess", fmt.Sprintf("Object locking '%s' is configured for %s.", console.Colorize("Mode", m.Mode), console.Colorize("Validity", m.Validity))) } // JSON'ified message for scripting. func (m retentionBucketMessage) JSON() string { msgBytes, e := json.MarshalIndent(m, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } func getRetainUntilDate(validity uint64, unit minio.ValidityUnit) (string, *probe.Error) { if validity == 0 { return "", probe.NewError(fmt.Errorf("invalid validity '%v'", validity)) } t := UTCNow() if unit == minio.Years { t = t.AddDate(int(validity), 0, 0) } else { t = t.AddDate(0, 0, int(validity)) } timeStr := t.Format(time.RFC3339) return timeStr, nil } func setRetentionSingle(ctx context.Context, op lockOpType, alias, url, versionID string, mode minio.RetentionMode, retainUntil time.Time, bypassGovernance bool) *probe.Error { newClnt, err := newClientFromAlias(alias, url) if err != nil { return err } msg := retentionCmdMessage{ Op: op, Mode: mode, URLPath: urlJoinPath(alias, url), VersionID: versionID, } err = newClnt.PutObjectRetention(ctx, versionID, mode, retainUntil, bypassGovernance) if err != nil { msg.Err = err.ToGoError() msg.Status = "failure" } else { msg.Status = "success" } printMsg(msg) return err } func parseRetentionValidity(validityStr string) (uint64, minio.ValidityUnit, *probe.Error) { unitStr := string(validityStr[len(validityStr)-1]) validityStr = validityStr[:len(validityStr)-1] validity, e := strconv.ParseUint(validityStr, 10, 64) if e != nil { return 0, "", probe.NewError(e).Trace(validityStr) } var unit minio.ValidityUnit switch unitStr { case "d", "D": unit = minio.Days case "y", "Y": unit = minio.Years default: return 0, "", errInvalidArgument().Trace(unitStr) } return validity, unit, nil } func fatalIfBucketLockNotSupported(ctx context.Context, aliasedURL string) { _, err := getBucketLockStatus(ctx, aliasedURL) if err != nil { fatalIf(errDummy().Trace(), "Remote bucket `%s` does not support locking", aliasedURL) } } // Apply Retention for one object/version or many objects within a given prefix. func applyRetention(ctx context.Context, op lockOpType, target, versionID string, timeRef time.Time, withVersions, isRecursive bool, mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit, bypassGovernance bool, ) error { clnt, err := newClient(target) if err != nil { fatalIf(err.Trace(), "Unable to parse the provided url.") } // Quit early if urlStr does not point to an S3 server switch clnt.(type) { case *S3Client: default: fatal(errDummy().Trace(), "Retention is supported only for S3 servers.") } var until time.Time if mode != "" { timeStr, err := getRetainUntilDate(validity, unit) if err != nil { return err.ToGoError() } var e error until, e = time.Parse(time.RFC3339, timeStr) if e != nil { return e } } alias, urlStr, _ := mustExpandAlias(target) if versionID != "" || !isRecursive && !withVersions { err := setRetentionSingle(ctx, op, alias, urlStr, versionID, mode, until, bypassGovernance) fatalIf(err.Trace(), "Unable to set retention on `%s`", target) return nil } lstOptions := ListOptions{Recursive: isRecursive, ShowDir: DirNone} if !timeRef.IsZero() { lstOptions.WithOlderVersions = withVersions lstOptions.WithDeleteMarkers = true lstOptions.TimeRef = timeRef } var cErr error var atLeastOneRetentionApplied bool for content := range clnt.List(ctx, lstOptions) { if content.Err != nil { errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") cErr = exitStatus(globalErrorExitStatus) // Set the exit status. continue } // The spec does not allow setting retention on delete marker if content.IsDeleteMarker { continue } if !isRecursive && getStandardizedURL(alias+getKey(content)) != getStandardizedURL(target) { break } err := setRetentionSingle(ctx, op, alias, content.URL.String(), content.VersionID, mode, until, bypassGovernance) if err != nil { errorIf(err.Trace(clnt.GetURL().String()), "Invalid URL") continue } atLeastOneRetentionApplied = true } if !atLeastOneRetentionApplied { errorIf(errDummy().Trace(clnt.GetURL().String()), "Unable to find any object/version to %s its retention.", op) cErr = exitStatus(globalErrorExitStatus) // Set the exit status. } return cErr } // applyBucketLock - set object lock configuration. func applyBucketLock(op lockOpType, urlStr string, mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit) error { client, err := newClient(urlStr) if err != nil { fatalIf(err.Trace(), "Unable to parse the provided url.") } ctx, cancelLock := context.WithCancel(globalContext) defer cancelLock() if op == lockOpClear || mode != "" { err = client.SetObjectLockConfig(ctx, mode, validity, unit) fatalIf(err, "Unable to apply bucket lock configuration.") } else { _, mode, validity, unit, err = client.GetObjectLockConfig(ctx) fatalIf(err, "Unable to apply bucket lock configuration.") } printMsg(retentionBucketMessage{ Op: op, Enabled: "Enabled", Mode: mode, Validity: fmt.Sprintf("%d%s", validity, unit), Status: "success", }) return nil } // showBucketLock - show object lock configuration. func showBucketLock(urlStr string) error { client, err := newClient(urlStr) if err != nil { fatalIf(err.Trace(), "Unable to parse the provided url.") } ctx, cancelLock := context.WithCancel(globalContext) defer cancelLock() status, mode, validity, unit, err := client.GetObjectLockConfig(ctx) fatalIf(err, "Unable to get bucket lock configuration.") printMsg(retentionBucketMessage{ Op: lockOpInfo, Enabled: status, Mode: mode, Validity: fmt.Sprintf("%d%s", validity, unit), Status: "success", }) return nil } minio-client-0.0~20250403/cmd/retention-info.go000066400000000000000000000257521477450377600210470ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) var retentionInfoFlags = []cli.Flag{ cli.BoolFlag{ Name: "recursive, r", Usage: "show retention info recursively", }, cli.StringFlag{ Name: "version-id, vid", Usage: "show retention info of specific object version", }, cli.StringFlag{ Name: "rewind", Usage: "roll back object(s) to current version at specified time", }, cli.BoolFlag{ Name: "versions", Usage: "show retention info on object(s) and all its versions", }, cli.BoolFlag{ Name: "default", Usage: "show bucket default retention mode", }, } var retentionInfoCmd = cli.Command{ Name: "info", Usage: "show retention settings on object(s)", Action: mainRetentionInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(retentionInfoFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] [governance | compliance] VALIDITY TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Show object retention for a specific object $ {{.HelpName}} myminio/mybucket/prefix/obj.csv 2. Show object retention for recursively for all objects at a given prefix $ {{.HelpName}} myminio/mybucket/prefix --recursive 3. Show object retention to a specific version of a specific object $ {{.HelpName}} myminio/mybucket/prefix/obj.csv --version-id "3Jr2x6fqlBUsVzbvPihBO3HgNpgZgAnp" 4. Show object retention for recursively for all versions of all objects under prefix $ {{.HelpName}} myminio/mybucket/prefix --recursive --versions 5. Show default lock retention configuration for a bucket $ {{.HelpName}} myminio/mybucket/ --default `, } func parseInfoRetentionArgs(cliCtx *cli.Context) (target, versionID string, recursive bool, timeRef time.Time, withVersions, defaultMode bool) { args := cliCtx.Args() if len(args) != 1 { showCommandHelpAndExit(cliCtx, 1) } target = args[0] if target == "" { fatalIf(errInvalidArgument().Trace(), "invalid target url '%v'", target) } versionID = cliCtx.String("version-id") timeRef = parseRewindFlag(cliCtx.String("rewind")) withVersions = cliCtx.Bool("versions") recursive = cliCtx.Bool("recursive") defaultMode = cliCtx.Bool("default") if defaultMode && (versionID != "" || !timeRef.IsZero() || withVersions || recursive) { fatalIf(errDummy(), "--default flag cannot be specified with any of --version-id, --rewind, --versions, --recursive.") } return } // Structured message depending on the type of console. type retentionInfoMessage struct { Mode minio.RetentionMode `json:"mode"` Until time.Time `json:"until"` URLPath string `json:"urlpath"` VersionID string `json:"versionID"` Status string `json:"status"` Err error `json:"error"` } type retentionInfoMessageList retentionInfoMessage func (m *retentionInfoMessageList) SetErr(e error) { m.Err = e } func (m *retentionInfoMessageList) SetStatus(status string) { m.Status = status } func (m *retentionInfoMessageList) SetMode(mode minio.RetentionMode) { m.Mode = mode } func (m *retentionInfoMessageList) SetUntil(until time.Time) { m.Until = until } // Colorized message for console printing. func (m retentionInfoMessageList) String() string { if m.Err != nil { return console.Colorize("RetentionFailure", fmt.Sprintf("Unable to get get object retention on `%s`: %s", m.URLPath, m.Err)) } var msg string var retentionField string if m.Mode == "" { retentionField += console.Colorize("RetentionNotFound", "NO RETENTION") } else { exp := "" if m.Mode == minio.Governance { now := time.Now() if now.After(m.Until) { exp = "EXPIRED" } } retentionField += console.Colorize("RetentionSuccess", m.Mode.String()) + " " + console.Colorize("RetentionExpired", exp) } msg += "[ " + centerText(retentionField, 18) + " ] " if m.VersionID != "" { msg += console.Colorize("RetentionVersionID", m.VersionID+" ") } msg += m.URLPath return msg } // JSON'ified message for scripting. func (m retentionInfoMessageList) JSON() string { if m.Err != nil { m.Status = "failure" } msgBytes, e := json.MarshalIndent(m, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } type retentionInfoMessageRecord retentionInfoMessage func (m *retentionInfoMessageRecord) SetErr(e error) { m.Err = e } func (m *retentionInfoMessageRecord) SetStatus(status string) { m.Status = status } func (m *retentionInfoMessageRecord) SetMode(mode minio.RetentionMode) { m.Mode = mode } func (m *retentionInfoMessageRecord) SetUntil(until time.Time) { m.Until = until } // Colorized message for console printing. func (m retentionInfoMessageRecord) String() string { if m.Err != nil { return console.Colorize("RetentionFailure", fmt.Sprintf("Unable to get object retention on `%s`: %s", m.URLPath, m.Err)) } var msg strings.Builder fmt.Fprintf(&msg, "Name : %s\n", console.Colorize("RetentionSuccess", m.URLPath)) if m.VersionID != "" { fmt.Fprintf(&msg, "Version : %s\n", console.Colorize("RetentionSuccess", m.VersionID)) } fmt.Fprintf(&msg, "Mode : ") if m.Mode == "" { fmt.Fprint(&msg, console.Colorize("RetentionNotFound", "NO RETENTION")) } else { fmt.Fprint(&msg, console.Colorize("RetentionSuccess", m.Mode)) if !m.Until.IsZero() { msg.WriteString(", ") exp := "" now := time.Now() if now.After(m.Until) { prettyDuration := timeDurationToHumanizedDuration(now.Sub(m.Until)).StringShort() exp = console.Colorize("RetentionExpired", "expired "+prettyDuration+" ago") } else { prettyDuration := timeDurationToHumanizedDuration(m.Until.Sub(now)).StringShort() exp = console.Colorize("RetentionSuccess", "expiring in "+prettyDuration) } fmt.Fprint(&msg, exp) } } fmt.Fprint(&msg, "\n") return msg.String() } // JSON'ified message for scripting. func (m retentionInfoMessageRecord) JSON() string { if m.Err != nil { m.Status = "failure" } msgBytes, e := json.MarshalIndent(m, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } type retentionInfoMsg interface { message SetErr(error) SetStatus(string) SetMode(minio.RetentionMode) SetUntil(time.Time) } // Show retention info for a single object or version func infoRetentionSingle(ctx context.Context, alias, url, versionID string, listStyle bool) *probe.Error { newClnt, err := newClientFromAlias(alias, url) if err != nil { return err } var msg retentionInfoMsg if listStyle { msg = &retentionInfoMessageList{ URLPath: urlJoinPath(alias, url), VersionID: versionID, } } else { msg = &retentionInfoMessageRecord{ URLPath: urlJoinPath(alias, url), VersionID: versionID, } } mode, until, err := newClnt.GetObjectRetention(ctx, versionID) if err != nil { errResp := minio.ToErrorResponse(err.ToGoError()) if errResp.Code != "NoSuchObjectLockConfiguration" { if _, ok := err.ToGoError().(ObjectNameEmpty); !ok { msg.SetErr(err.ToGoError()) msg.SetStatus("failure") printMsg(msg) } return err } err = nil } msg.SetStatus("success") msg.SetMode(mode) msg.SetUntil(until) printMsg(msg) return err } // Get Retention for one object/version or many objects within a given prefix. func getRetention(ctx context.Context, target, versionID string, timeRef time.Time, withVersions, isRecursive bool) error { clnt, err := newClient(target) if err != nil { fatalIf(err.Trace(), "Unable to parse the provided url.") } // Quit early if urlStr does not point to an S3 server switch clnt.(type) { case *S3Client: default: fatal(errDummy().Trace(), "Retention is supported only for S3 servers.") } alias, urlStr, _ := mustExpandAlias(target) if versionID != "" || !isRecursive && !withVersions { err := infoRetentionSingle(ctx, alias, urlStr, versionID, false) if err != nil { if _, ok := err.ToGoError().(ObjectNameEmpty); ok { return showBucketLock(target) } return exitStatus(globalErrorExitStatus) } return nil } lstOptions := ListOptions{Recursive: isRecursive, ShowDir: DirNone} if !timeRef.IsZero() { lstOptions.WithOlderVersions = withVersions lstOptions.WithDeleteMarkers = true lstOptions.TimeRef = timeRef } var cErr error var atLeastOneObjectOrVersionFound bool for content := range clnt.List(ctx, lstOptions) { if content.Err != nil { errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") cErr = exitStatus(globalErrorExitStatus) // Set the exit status. continue } // The spec does not allow setting retention on delete marker if content.IsDeleteMarker { continue } if !isRecursive && getStandardizedURL(alias+getKey(content)) != getStandardizedURL(target) { break } err := infoRetentionSingle(ctx, alias, content.URL.String(), content.VersionID, true) if err != nil { errorIf(err.Trace(clnt.GetURL().String()), "Invalid URL") cErr = exitStatus(globalErrorExitStatus) continue } atLeastOneObjectOrVersionFound = true } if !atLeastOneObjectOrVersionFound { errorIf(errDummy().Trace(clnt.GetURL().String()), "Unable to find any object/version to show its retention.") cErr = exitStatus(globalErrorExitStatus) // Set the exit status. } return cErr } // main for retention info command. func mainRetentionInfo(cliCtx *cli.Context) error { ctx, cancelSetRetention := context.WithCancel(globalContext) defer cancelSetRetention() console.SetColor("RetentionSuccess", color.New(color.FgGreen, color.Bold)) console.SetColor("RetentionNotFound", color.New(color.FgYellow)) console.SetColor("RetentionVersionID", color.New(color.FgGreen)) console.SetColor("RetentionExpired", color.New(color.FgRed, color.Bold)) console.SetColor("RetentionFailure", color.New(color.FgYellow)) target, versionID, recursive, rewind, withVersions, bucketMode := parseInfoRetentionArgs(cliCtx) fatalIfBucketLockNotSupported(ctx, target) if bucketMode { return showBucketLock(target) } if withVersions && rewind.IsZero() { rewind = time.Now().UTC() } return getRetention(ctx, target, versionID, rewind, withVersions, recursive) } minio-client-0.0~20250403/cmd/retention-main.go000066400000000000000000000024041477450377600210250ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var retentionSubcommands = []cli.Command{ retentionSetCmd, retentionClearCmd, retentionInfoCmd, } var retentionCmd = cli.Command{ Name: "retention", Usage: "set retention for object(s)", Action: mainRetention, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: retentionSubcommands, } // main for retention command. func mainRetention(ctx *cli.Context) error { commandNotFound(ctx, retentionSubcommands) return nil } minio-client-0.0~20250403/cmd/retention-set.go000066400000000000000000000125051477450377600206770ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" minio "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) var retentionSetFlags = []cli.Flag{ cli.BoolFlag{ Name: "recursive, r", Usage: "apply retention recursively", }, cli.BoolFlag{ Name: "bypass", Usage: "bypass governance", }, cli.StringFlag{ Name: "version-id, vid", Usage: "apply retention to a specific object version", }, cli.StringFlag{ Name: "rewind", Usage: "roll back object(s) to current version at specified time", }, cli.BoolFlag{ Name: "versions", Usage: "apply retention object(s) and all its versions", }, cli.BoolFlag{ Name: "default", Usage: "set bucket default retention mode", }, } var retentionSetCmd = cli.Command{ Name: "set", Usage: "apply retention settings on object(s)", Action: mainRetentionSet, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(retentionSetFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] [governance | compliance] VALIDITY TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} VALIDITY: This argument must be formatted like Nd or Ny where 'd' denotes days and 'y' denotes years e.g. 10d, 3y. EXAMPLES: 1. Set object retention for a specific object $ {{.HelpName}} compliance 30d myminio/mybucket/prefix/obj.csv 2. Set object retention for recursively for all objects at a given prefix $ {{.HelpName}} governance 30d myminio/mybucket/prefix --recursive 3. Set object retention to a specific version of a specific object $ {{.HelpName}} governance 30d myminio/mybucket/prefix/obj.csv --version-id "3Jr2x6fqlBUsVzbvPihBO3HgNpgZgAnp" 4. Set object retention for recursively for all versions of all objects $ {{.HelpName}} governance 30d myminio/mybucket/prefix --recursive --versions 5. Set default lock retention configuration for a bucket $ {{.HelpName}} --default governance 30d myminio/mybucket/ `, } func parseSetRetentionArgs(cliCtx *cli.Context) (target, versionID string, recursive bool, timeRef time.Time, withVersions bool, mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit, bypass, bucketMode bool) { args := cliCtx.Args() if len(args) != 3 { showCommandHelpAndExit(cliCtx, 1) } mode = minio.RetentionMode(strings.ToUpper(args[0])) if !mode.IsValid() { fatalIf(errInvalidArgument().Trace(args...), "invalid retention mode '%v'", mode) } var err *probe.Error validity, unit, err = parseRetentionValidity(args[1]) fatalIf(err.Trace(args[1]), "invalid validity argument") target = args[2] if target == "" { fatalIf(errInvalidArgument().Trace(), "invalid target url '%v'", target) } versionID = cliCtx.String("version-id") timeRef = parseRewindFlag(cliCtx.String("rewind")) withVersions = cliCtx.Bool("versions") recursive = cliCtx.Bool("recursive") bypass = cliCtx.Bool("bypass") bucketMode = cliCtx.Bool("default") if bucketMode && (versionID != "" || !timeRef.IsZero() || withVersions || recursive || bypass) { fatalIf(errDummy(), "--default cannot be specified with any of --version-id, --rewind, --versions, --recursive, --bypass.") } return } // Set Retention for one object/version or many objects within a given prefix. func setRetention(ctx context.Context, target, versionID string, timeRef time.Time, withVersions, isRecursive bool, mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit, bypassGovernance bool, ) error { return applyRetention(ctx, lockOpSet, target, versionID, timeRef, withVersions, isRecursive, mode, validity, unit, bypassGovernance) } func setBucketLock(urlStr string, mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit) error { return applyBucketLock(lockOpSet, urlStr, mode, validity, unit) } // main for retention set command. func mainRetentionSet(cliCtx *cli.Context) error { ctx, cancelSetRetention := context.WithCancel(globalContext) defer cancelSetRetention() console.SetColor("RetentionSuccess", color.New(color.FgGreen, color.Bold)) console.SetColor("RetentionFailure", color.New(color.FgYellow)) target, versionID, recursive, rewind, withVersions, mode, validity, unit, bypass, bucketMode := parseSetRetentionArgs(cliCtx) fatalIfBucketLockNotSupported(ctx, target) if bucketMode { return setBucketLock(target, mode, validity, unit) } if withVersions && rewind.IsZero() { rewind = time.Now().UTC() } return setRetention(ctx, target, versionID, rewind, withVersions, recursive, mode, validity, unit, bypass) } minio-client-0.0~20250403/cmd/retry.go000066400000000000000000000027731477450377600172520ustar00rootroot00000000000000package cmd import ( "context" "fmt" "math/rand" "time" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" ) type retryManager struct { retries int maxRetries int retryInterval time.Duration commandCtx context.Context retryCtx context.Context cancelRetry context.CancelFunc } func newRetryManager(ctx context.Context, retryInterval time.Duration, maxRetries int) *retryManager { retryCtx, cancelFunc := context.WithCancel(context.Background()) return &retryManager{ retryInterval: retryInterval, maxRetries: maxRetries, commandCtx: ctx, retryCtx: retryCtx, cancelRetry: cancelFunc, } } type retryMessage struct { SourceURL string `json:"sourceURL"` TargetURL string `json:"targetURL"` Retries int `json:"retries"` } func (r retryMessage) String() string { return fmt.Sprintf(" Retries %d: source `%s` >> target `%s`", r.Retries, r.SourceURL, r.TargetURL) } func (r retryMessage) JSON() string { jsonMessageBytes, e := json.MarshalIndent(r, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (r *retryManager) retry(action func(rm *retryManager) *probe.Error) { defer r.cancelRetry() for r.retries <= r.maxRetries { err := action(r) if err == nil { return } select { case <-r.retryCtx.Done(): return case <-r.commandCtx.Done(): return case <-time.After(r.retryInterval/2 + time.Duration(rand.Int63n(int64(r.retryInterval)))): r.retries++ } } } minio-client-0.0~20250403/cmd/rm-main.go000066400000000000000000000555041477450377600174450ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "context" "fmt" "net/http" "os" "path" "path/filepath" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) // rm specific flags. var ( rmFlags = []cli.Flag{ cli.BoolFlag{ Name: "versions", Usage: "remove object(s) and all its versions", }, cli.BoolFlag{ Name: "recursive, r", Usage: "remove recursively", }, cli.BoolFlag{ Name: "force", Usage: "allow a recursive remove operation", }, cli.BoolFlag{ Name: "dangerous", Usage: "allow site-wide removal of objects", }, cli.StringFlag{ Name: "rewind", Usage: "roll back object(s) to current version at specified time", }, cli.StringFlag{ Name: "version-id, vid", Usage: "delete a specific version of an object", }, cli.BoolFlag{ Name: "incomplete, I", Usage: "remove incomplete uploads", }, cli.BoolFlag{ Name: "dry-run", Usage: "perform a fake remove operation", }, cli.BoolFlag{ Name: "fake", Usage: "perform a fake remove operation", Hidden: true, // deprecated 2022 }, cli.BoolFlag{ Name: "stdin", Usage: "read object names from STDIN", }, cli.StringFlag{ Name: "older-than", Usage: "remove objects older than value in duration string (e.g. 7d10h31s)", }, cli.StringFlag{ Name: "newer-than", Usage: "remove objects newer than value in duration string (e.g. 7d10h31s)", }, cli.BoolFlag{ Name: "bypass", Usage: "bypass governance", }, cli.BoolFlag{ Name: "non-current", Usage: "remove object(s) versions that are non-current", }, cli.BoolFlag{ Name: "purge", Usage: "attempt a prefix purge, requires confirmation please use with caution - only works with '--force'", Hidden: true, }, } ) // remove a file or folder. var rmCmd = cli.Command{ Name: "rm", Usage: "remove object(s)", Action: mainRm, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(rmFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET ...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 01. Remove a file. {{.Prompt}} {{.HelpName}} 1999/old-backup.tgz 02. Perform a fake remove operation. {{.Prompt}} {{.HelpName}} --dry-run 1999/old-backup.tgz 03. Remove all objects recursively from bucket 'jazz-songs' matching the prefix 'louis'. {{.Prompt}} {{.HelpName}} --recursive --force s3/jazz-songs/louis/ 04. Remove all objects older than '90' days recursively from bucket 'jazz-songs' matching the prefix 'louis'. {{.Prompt}} {{.HelpName}} --recursive --force --older-than 90d s3/jazz-songs/louis/ 05. Remove all objects newer than 7 days and 10 hours recursively from bucket 'pop-songs' {{.Prompt}} {{.HelpName}} --recursive --force --newer-than 7d10h s3/pop-songs/ 06. Remove all objects read from STDIN. {{.Prompt}} {{.HelpName}} --force --stdin 07. Remove all objects recursively from Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} --recursive --force --dangerous s3 08. Remove all objects older than '90' days recursively under all buckets. {{.Prompt}} {{.HelpName}} --recursive --dangerous --force --older-than 90d s3 09. Drop all incomplete uploads on the bucket 'jazz-songs'. {{.Prompt}} {{.HelpName}} --incomplete --recursive --force s3/jazz-songs/ 10. Bypass object retention in governance mode and delete the object. {{.Prompt}} {{.HelpName}} --bypass s3/pop-songs/ 11. Remove a particular version ID. {{.Prompt}} {{.HelpName}} s3/docs/money.xls --version-id "f20f3792-4bd4-4288-8d3c-b9d05b3b62f6" 12. Remove all object versions older than one year. {{.Prompt}} {{.HelpName}} s3/docs/ --recursive --versions --rewind 365d 14. Perform a fake removal of object(s) versions that are non-current and older than 10 days. If top-level version is a delete marker, this will also be deleted when --non-current flag is specified. {{.Prompt}} {{.HelpName}} s3/docs/ --recursive --force --versions --non-current --older-than 10d --dry-run `, } // Structured message depending on the type of console. type rmMessage struct { Status string `json:"status"` Key string `json:"key"` DeleteMarker bool `json:"deleteMarker"` VersionID string `json:"versionID"` ModTime *time.Time `json:"modTime"` DryRun bool `json:"dryRun"` } // Colorized message for console printing. func (r rmMessage) String() string { msg := "Removed " if r.DryRun { msg = "DRYRUN: Removing " } if r.DeleteMarker { msg = "Created delete marker " } msg += console.Colorize("Removed", fmt.Sprintf("`%s`", r.Key)) if r.VersionID != "" { msg += fmt.Sprintf(" (versionId=%s)", r.VersionID) if r.ModTime != nil { msg += fmt.Sprintf(" (modTime=%s)", r.ModTime.Format(printDate)) } } msg += "." return msg } // JSON'ified message for scripting. func (r rmMessage) JSON() string { r.Status = "success" msgBytes, e := json.MarshalIndent(r, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } // Validate command line arguments. func checkRmSyntax(ctx context.Context, cliCtx *cli.Context) { // Set command flags from context. isForce := cliCtx.Bool("force") isRecursive := cliCtx.Bool("recursive") isStdin := cliCtx.Bool("stdin") isDangerous := cliCtx.Bool("dangerous") isVersions := cliCtx.Bool("versions") isNoncurrentVersion := cliCtx.Bool("non-current") isForceDel := cliCtx.Bool("purge") versionID := cliCtx.String("version-id") rewind := cliCtx.String("rewind") isNamespaceRemoval := false if versionID != "" && (isRecursive || isVersions || rewind != "") { fatalIf(errDummy().Trace(), "You cannot specify --version-id with any of --versions, --rewind and --recursive flags.") } if isNoncurrentVersion && (!isVersions || !isRecursive) { fatalIf(errDummy().Trace(), "You cannot specify --non-current without --versions --recursive, please use --non-current --versions --recursive.") } if isForceDel && !isForce { fatalIf(errDummy().Trace(), "You cannot specify --purge without --force.") } if isForceDel && isRecursive { fatalIf(errDummy().Trace(), "You cannot specify --purge with --recursive.") } if isForceDel && (isNoncurrentVersion || isVersions || cliCtx.IsSet("older-than") || cliCtx.IsSet("newer-than") || versionID != "") { fatalIf(errDummy().Trace(), "You cannot specify --purge flag with any flag(s) other than --force.") } if !isForceDel { for _, url := range cliCtx.Args() { // clean path for aliases like s3/. // Note: UNC path using / works properly in go 1.9.2 even though it breaks the UNC specification. url = filepath.ToSlash(filepath.Clean(url)) // namespace removal applies only for non FS. So filter out if passed url represents a directory dir, _ := isAliasURLDir(ctx, url, nil, time.Time{}, false) if dir { _, path := url2Alias(url) isNamespaceRemoval = (path == "") break } if dir && isRecursive && !isForce { fatalIf(errDummy().Trace(), "Removal requires --force flag. This operation is *IRREVERSIBLE*. Please review carefully before performing this *DANGEROUS* operation.") } if dir && !isRecursive { fatalIf(errDummy().Trace(), "Removal requires --recursive flag. This operation is *IRREVERSIBLE*. Please review carefully before performing this *DANGEROUS* operation.") } } } if !cliCtx.Args().Present() && !isStdin { exitCode := 1 showCommandHelpAndExit(cliCtx, exitCode) } // For all recursive or versions bulk deletion operations make sure to check for 'force' flag. if (isVersions || isRecursive || isStdin) && !isForce { fatalIf(errDummy().Trace(), "Removal requires --force flag. This operation is *IRREVERSIBLE*. Please review carefully before performing this *DANGEROUS* operation.") } if isNamespaceRemoval && (!isDangerous || !isForce) { fatalIf(errDummy().Trace(), "This operation results in site-wide removal of objects. If you are really sure, retry this command with ‘--dangerous’ and ‘--force’ flags.") } } // Remove a single object or a single version in a versioned bucket func removeSingle(url, versionID string, opts removeOpts) error { ctx, cancel := context.WithCancel(globalContext) defer cancel() var ( // A HEAD request can fail with: // - 400 Bad Request when the object SSE-C // - 405 Method Not Allowed when this is a delete marker // In those cases, we still want t remove the target object/version // so we simply ignore them. ignoreStatError bool isDir bool modTime time.Time ) targetAlias, targetURL, _ := mustExpandAlias(url) if !opts.isForceDel { _, content, pErr := url2Stat(ctx, url2StatOptions{ urlStr: url, versionID: versionID, fileAttr: false, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false, }) if pErr != nil { switch st := minio.ToErrorResponse(pErr.ToGoError()).StatusCode; st { case http.StatusBadRequest, http.StatusMethodNotAllowed: ignoreStatError = true default: _, ok := pErr.ToGoError().(ObjectMissing) ignoreStatError = (st == http.StatusServiceUnavailable || ok || st == http.StatusNotFound) && (opts.isForce && opts.isForceDel) if !ignoreStatError { errorIf(pErr.Trace(url), "Failed to remove `%s`.", url) return exitStatus(globalErrorExitStatus) } } } else { isDir = content.Type.IsDir() modTime = content.Time } // We should not proceed if ignoreStatError && (opts.olderThan != "" || opts.newerThan != "") { errorIf(pErr.Trace(url), "Unable to stat `%s`.", url) return exitStatus(globalErrorExitStatus) } // Skip objects older than older--than parameter if specified if opts.olderThan != "" && isOlder(modTime, opts.olderThan) { return nil } // Skip objects older than older--than parameter if specified if opts.newerThan != "" && isNewer(modTime, opts.newerThan) { return nil } if opts.isFake { printDryRunMsg(targetAlias, content, opts.withVersions) return nil } } clnt, pErr := newClientFromAlias(targetAlias, targetURL) if pErr != nil { errorIf(pErr.Trace(url), "Invalid argument `%s`.", url) return exitStatus(globalErrorExitStatus) // End of journey. } if !strings.HasSuffix(targetURL, string(clnt.GetURL().Separator)) && isDir { targetURL = targetURL + string(clnt.GetURL().Separator) } contentCh := make(chan *ClientContent, 1) contentURL := *newClientURL(targetURL) contentCh <- &ClientContent{URL: contentURL, VersionID: versionID} close(contentCh) isRemoveBucket := false resultCh := clnt.Remove(ctx, opts.isIncomplete, isRemoveBucket, opts.isBypass, opts.isForce && opts.isForceDel, contentCh) for result := range resultCh { if result.Err != nil { errorIf(result.Err.Trace(url), "Failed to remove `%s`.", url) switch result.Err.ToGoError().(type) { case PathInsufficientPermission: // Ignore Permission error. continue } return exitStatus(globalErrorExitStatus) } msg := rmMessage{ Key: path.Join(targetAlias, result.BucketName, result.ObjectName), VersionID: result.ObjectVersionID, } if result.DeleteMarker { msg.DeleteMarker = true msg.VersionID = result.DeleteMarkerVersionID } printMsg(msg) } return nil } type removeOpts struct { timeRef time.Time withVersions bool nonCurrentVersion bool isForce bool isRecursive bool isIncomplete bool isFake bool isBypass bool isForceDel bool olderThan string newerThan string } func printDryRunMsg(targetAlias string, content *ClientContent, printModTime bool) { if content == nil { return } msg := rmMessage{ Status: "success", DryRun: true, Key: targetAlias + getKey(content), VersionID: content.VersionID, } if printModTime { msg.ModTime = &content.Time } printMsg(msg) } // listAndRemove uses listing before removal, it can list recursively or not, with versions or not. // // Use cases: // * Remove objects recursively // * Remove all versions of a single object func listAndRemove(url string, opts removeOpts) error { ctx, cancelRemove := context.WithCancel(globalContext) defer cancelRemove() targetAlias, targetURL, _ := mustExpandAlias(url) clnt, pErr := newClientFromAlias(targetAlias, targetURL) if pErr != nil { errorIf(pErr.Trace(url), "Failed to remove `%s` recursively.", url) return exitStatus(globalErrorExitStatus) // End of journey. } contentCh := make(chan *ClientContent) isRemoveBucket := false listOpts := ListOptions{Recursive: opts.isRecursive, Incomplete: opts.isIncomplete, ShowDir: DirLast} if !opts.timeRef.IsZero() { listOpts.WithOlderVersions = opts.withVersions listOpts.WithDeleteMarkers = true listOpts.TimeRef = opts.timeRef } atLeastOneObjectFound := false resultCh := clnt.Remove(ctx, opts.isIncomplete, isRemoveBucket, opts.isBypass, false, contentCh) var lastPath string var perObjectVersions []*ClientContent for content := range clnt.List(ctx, listOpts) { if content.Err != nil { errorIf(content.Err.Trace(url), "Failed to remove `%s` recursively.", url) switch content.Err.ToGoError().(type) { case PathInsufficientPermission: // Ignore Permission error. continue } close(contentCh) return exitStatus(globalErrorExitStatus) } urlString := content.URL.Path // rm command is not supposed to remove buckets, ignore if this is a bucket name if content.URL.Type == objectStorage && strings.LastIndex(urlString, string(content.URL.Separator)) == 0 { continue } if !opts.isRecursive { currentObjectURL := getStandardizedURL(targetAlias + getKey(content)) standardizedURL := getStandardizedURL(currentObjectURL) if !strings.HasPrefix(url, standardizedURL) { break } } if opts.nonCurrentVersion && opts.isRecursive && opts.withVersions { if lastPath != content.URL.Path { lastPath = content.URL.Path for _, content := range perObjectVersions { if content.IsLatest && !content.IsDeleteMarker { continue } if !content.Time.IsZero() { // Skip objects older than --older-than parameter, if specified if opts.olderThan != "" && isOlder(content.Time, opts.olderThan) { continue } // Skip objects newer than --newer-than parameter if specified if opts.newerThan != "" && isNewer(content.Time, opts.newerThan) { continue } } else { // Skip prefix levels. continue } if opts.isFake { printDryRunMsg(targetAlias, content, true) continue } sent := false for !sent { select { case contentCh <- content: sent = true case result := <-resultCh: path := path.Join(targetAlias, result.BucketName, result.ObjectName) if result.Err != nil { errorIf(result.Err.Trace(path), "Failed to remove `%s`.", path) switch result.Err.ToGoError().(type) { case PathInsufficientPermission: // Ignore Permission error. continue } close(contentCh) return exitStatus(globalErrorExitStatus) } msg := rmMessage{ Key: path, VersionID: result.ObjectVersionID, } if result.DeleteMarker { msg.DeleteMarker = true msg.VersionID = result.DeleteMarkerVersionID } printMsg(msg) } } } perObjectVersions = []*ClientContent{} } atLeastOneObjectFound = true perObjectVersions = append(perObjectVersions, content) continue } // This will mark that we found at least one target object // even that it could be ineligible for deletion. So we can // inform the user that he was searching in an empty area atLeastOneObjectFound = true if !content.Time.IsZero() { // Skip objects older than --older-than parameter, if specified if opts.olderThan != "" && isOlder(content.Time, opts.olderThan) { continue } // Skip objects newer than --newer-than parameter if specified if opts.newerThan != "" && isNewer(content.Time, opts.newerThan) { continue } } else { // Skip prefix levels. continue } if !opts.isFake { sent := false for !sent { select { case contentCh <- content: sent = true case result := <-resultCh: path := path.Join(targetAlias, result.BucketName, result.ObjectName) if result.Err != nil { errorIf(result.Err.Trace(path), "Failed to remove `%s`.", path) switch e := result.Err.ToGoError().(type) { case PathInsufficientPermission: // Ignore Permission error. continue case minio.ErrorResponse: if strings.Contains(e.Message, "Object is WORM protected and cannot be overwritten") { continue } } close(contentCh) return exitStatus(globalErrorExitStatus) } msg := rmMessage{ Key: path, VersionID: result.ObjectVersionID, } if result.DeleteMarker { msg.DeleteMarker = true msg.VersionID = result.DeleteMarkerVersionID } printMsg(msg) } } } else { printDryRunMsg(targetAlias, content, opts.withVersions) } } if opts.nonCurrentVersion && opts.isRecursive && opts.withVersions { for _, content := range perObjectVersions { if content.IsLatest && !content.IsDeleteMarker { continue } if !content.Time.IsZero() { // Skip objects older than --older-than parameter, if specified if opts.olderThan != "" && isOlder(content.Time, opts.olderThan) { continue } // Skip objects newer than --newer-than parameter if specified if opts.newerThan != "" && isNewer(content.Time, opts.newerThan) { continue } } else { // Skip prefix levels. continue } if opts.isFake { printDryRunMsg(targetAlias, content, true) continue } sent := false for !sent { select { case contentCh <- content: sent = true case result := <-resultCh: path := path.Join(targetAlias, result.BucketName, result.ObjectName) if result.Err != nil { errorIf(result.Err.Trace(path), "Failed to remove `%s`.", path) switch result.Err.ToGoError().(type) { case PathInsufficientPermission: // Ignore Permission error. continue } close(contentCh) return exitStatus(globalErrorExitStatus) } msg := rmMessage{ Key: path, VersionID: result.ObjectVersionID, } if result.DeleteMarker { msg.DeleteMarker = true msg.VersionID = result.DeleteMarkerVersionID } printMsg(msg) } } } } close(contentCh) if opts.isFake { return nil } for result := range resultCh { path := path.Join(targetAlias, result.BucketName, result.ObjectName) if result.Err != nil { errorIf(result.Err.Trace(path), "Failed to remove `%s` recursively.", path) switch result.Err.ToGoError().(type) { case PathInsufficientPermission: // Ignore Permission error. continue } return exitStatus(globalErrorExitStatus) } msg := rmMessage{ Key: path, VersionID: result.ObjectVersionID, } if result.DeleteMarker { msg.DeleteMarker = true msg.VersionID = result.DeleteMarkerVersionID } printMsg(msg) } if !atLeastOneObjectFound { if opts.isForce { // Do not throw an exit code with --force check unix `rm -f` // behavior and do not print an error as well. return nil } errorIf(errDummy().Trace(url), "No object/version found to be removed in `%s`.", url) return exitStatus(globalErrorExitStatus) } return nil } // main for rm command. func mainRm(cliCtx *cli.Context) error { ctx, cancelRm := context.WithCancel(globalContext) defer cancelRm() checkRmSyntax(ctx, cliCtx) isIncomplete := cliCtx.Bool("incomplete") isRecursive := cliCtx.Bool("recursive") isFake := cliCtx.Bool("dry-run") || cliCtx.Bool("fake") isStdin := cliCtx.Bool("stdin") isBypass := cliCtx.Bool("bypass") olderThan := cliCtx.String("older-than") newerThan := cliCtx.String("newer-than") isForce := cliCtx.Bool("force") isForceDel := cliCtx.Bool("purge") withNoncurrentVersion := cliCtx.Bool("non-current") withVersions := cliCtx.Bool("versions") versionID := cliCtx.String("version-id") rewind := parseRewindFlag(cliCtx.String("rewind")) if withVersions && rewind.IsZero() { rewind = time.Now().UTC() } // Set color. console.SetColor("Removed", color.New(color.FgGreen, color.Bold)) var rerr error var e error // Support multiple targets. for _, url := range cliCtx.Args() { if isRecursive || withVersions { e = listAndRemove(url, removeOpts{ timeRef: rewind, withVersions: withVersions, nonCurrentVersion: withNoncurrentVersion, isForce: isForce, isRecursive: isRecursive, isIncomplete: isIncomplete, isFake: isFake, isBypass: isBypass, olderThan: olderThan, newerThan: newerThan, }) } else { e = removeSingle(url, versionID, removeOpts{ isIncomplete: isIncomplete, isFake: isFake, isForce: isForce, isForceDel: isForceDel, isBypass: isBypass, olderThan: olderThan, newerThan: newerThan, }) } if rerr == nil { rerr = e } } if !isStdin { return rerr } scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { url := scanner.Text() if isRecursive || withVersions { e = listAndRemove(url, removeOpts{ timeRef: rewind, withVersions: withVersions, nonCurrentVersion: withNoncurrentVersion, isForce: isForce, isRecursive: isRecursive, isIncomplete: isIncomplete, isFake: isFake, isBypass: isBypass, olderThan: olderThan, newerThan: newerThan, }) } else { e = removeSingle(url, versionID, removeOpts{ isIncomplete: isIncomplete, isFake: isFake, isForce: isForce, isForceDel: isForceDel, isBypass: isBypass, olderThan: olderThan, newerThan: newerThan, }) } if rerr == nil { rerr = e } } return rerr } minio-client-0.0~20250403/cmd/share-db-v1.go000066400000000000000000000067621477450377600201200ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" "sync" "time" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/quick" ) // shareEntryV1 - container for each download/upload entries. type shareEntryV1 struct { URL string `json:"share"` // Object URL. VersionID string `json:"versionID"` Date time.Time `json:"date"` Expiry time.Duration `json:"expiry"` ContentType string `json:"contentType,omitempty"` // Only used by upload cmd. } // JSON file to persist previously shared uploads. type shareDBV1 struct { Version string `json:"version"` mutex *sync.Mutex // key is unique share URL. Shares map[string]shareEntryV1 `json:"shares"` } // Instantiate a new uploads structure for persistence. func newShareDBV1() *shareDBV1 { s := &shareDBV1{ Version: "1", } s.Shares = make(map[string]shareEntryV1) s.mutex = &sync.Mutex{} return s } // Set upload info for each share. func (s *shareDBV1) Set(objectURL, shareURL string, expiry time.Duration, contentType string) { s.mutex.Lock() defer s.mutex.Unlock() s.Shares[shareURL] = shareEntryV1{ URL: objectURL, Date: UTCNow(), Expiry: expiry, ContentType: contentType, } } // Delete upload info if it exists. func (s *shareDBV1) Delete(objectURL string) { s.mutex.Lock() defer s.mutex.Unlock() delete(s.Shares, objectURL) } // Delete all expired uploads. func (s *shareDBV1) deleteAllExpired() { for shareURL, share := range s.Shares { if (share.Expiry - time.Since(share.Date)) <= 0 { // Expired entry. Safe to drop. delete(s.Shares, shareURL) } } } // Load shareDB entries from disk. Any entries held in memory are reset. func (s *shareDBV1) Load(filename string) *probe.Error { s.mutex.Lock() defer s.mutex.Unlock() // Check if the db file exist. if _, e := os.Stat(filename); e != nil { return probe.NewError(e) } // Initialize and load using quick package. qs, e := quick.NewConfig(newShareDBV1(), nil) if e != nil { return probe.NewError(e).Trace(filename) } e = qs.Load(filename) if e != nil { return probe.NewError(e).Trace(filename) } // Copy map over. for k, v := range qs.Data().(*shareDBV1).Shares { s.Shares[k] = v } // Filter out expired entries and save changes back to disk. s.deleteAllExpired() s.save(filename) return nil } // Persist share uploads to disk. func (s shareDBV1) save(filename string) *probe.Error { // Initialize a new quick file. qs, e := quick.NewConfig(s, nil) if e != nil { return probe.NewError(e).Trace(filename) } if e := qs.Save(filename); e != nil { return probe.NewError(e).Trace(filename) } return nil } // Persist share uploads to disk. func (s shareDBV1) Save(filename string) *probe.Error { s.mutex.Lock() defer s.mutex.Unlock() return s.save(filename) } minio-client-0.0~20250403/cmd/share-download-main.go000066400000000000000000000162401477450377600217300ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "strings" "time" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" ) var shareDownloadFlags = []cli.Flag{ cli.BoolFlag{ Name: "recursive, r", Usage: "share all objects recursively", }, cli.StringFlag{ Name: "version-id, vid", Usage: "share a particular object version", }, shareFlagExpire, } // Share documents via URL. var shareDownload = cli.Command{ Name: "download", Usage: "generate URLs for download access", Action: mainShareDownload, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(shareDownloadFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Share this object with 7 days default expiry. {{.Prompt}} {{.HelpName}} s3/backup/2006-Mar-1/backup.tar.gz 2. Share this object with 10 minutes expiry. {{.Prompt}} {{.HelpName}} --expire=10m s3/backup/2006-Mar-1/backup.tar.gz 3. Share all objects under this folder with 5 days expiry. {{.Prompt}} {{.HelpName}} --expire=120h s3/backup/2006-Mar-1/ 4. Share all objects under this bucket and all its folders and sub-folders with 5 days expiry. {{.Prompt}} {{.HelpName}} --recursive --expire=120h s3/backup/ `, } // checkShareDownloadSyntax - validate command-line args. func checkShareDownloadSyntax(ctx context.Context, cliCtx *cli.Context, encKeyDB map[string][]prefixSSEPair) { args := cliCtx.Args() if !args.Present() { showCommandHelpAndExit(cliCtx, 1) // last argument is exit code. } // Parse expiry. expiry := shareDefaultExpiry expireArg := cliCtx.String("expire") if expireArg != "" { var e error expiry, e = time.ParseDuration(expireArg) fatalIf(probe.NewError(e), "Unable to parse expire=`"+expireArg+"`.") } // Validate expiry. if expiry.Seconds() < 1 { fatalIf(errDummy().Trace(expiry.String()), "Expiry cannot be lesser than 1 second.") } if expiry.Seconds() > 604800 { fatalIf(errDummy().Trace(expiry.String()), "Expiry cannot be larger than 7 days.") } isRecursive := cliCtx.Bool("recursive") versionID := cliCtx.String("version-id") if versionID != "" && isRecursive { fatalIf(errDummy().Trace(), "--version-id cannot be specified with --recursive flag.") } // Validate if object exists only if the `--recursive` flag was NOT specified if !isRecursive { for _, url := range cliCtx.Args() { _, _, err := url2Stat(ctx, url2StatOptions{urlStr: url, versionID: "", fileAttr: false, encKeyDB: encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false}) if err != nil { fatalIf(err.Trace(url), "Unable to stat `"+url+"`.") } } } } // doShareURL share files from target. func doShareDownloadURL(ctx context.Context, targetURL, versionID string, isRecursive bool, expiry time.Duration) *probe.Error { targetAlias, targetURLFull, _, err := expandAlias(targetURL) if err != nil { return err.Trace(targetURL) } clnt, err := newClientFromAlias(targetAlias, targetURLFull) if err != nil { return err.Trace(targetURL) } // Load previously saved upload-shares. Add new entries and write it back. shareDB := newShareDBV1() shareDownloadsFile := getShareDownloadsFile() err = shareDB.Load(shareDownloadsFile) if err != nil { return err.Trace(shareDownloadsFile) } // Channel which will receive objects whose URLs need to be shared objectsCh := make(chan *ClientContent) content, err := clnt.Stat(ctx, StatOptions{versionID: versionID}) if err != nil { return err.Trace(clnt.GetURL().String()) } if !content.Type.IsDir() { go func() { defer close(objectsCh) objectsCh <- content }() } else { if !strings.HasSuffix(targetURLFull, string(clnt.GetURL().Separator)) { targetURLFull = targetURLFull + string(clnt.GetURL().Separator) } clnt, err = newClientFromAlias(targetAlias, targetURLFull) if err != nil { return err.Trace(targetURLFull) } // Recursive mode: Share list of objects go func() { defer close(objectsCh) for content := range clnt.List(ctx, ListOptions{Recursive: isRecursive, ShowDir: DirNone}) { objectsCh <- content } }() } // Iterate over all objects to generate share URL for content := range objectsCh { if content.Err != nil { return content.Err.Trace(clnt.GetURL().String()) } // if any incoming directories, we don't need to calculate. if content.Type.IsDir() { continue } objectURL := content.URL.String() objectVersionID := content.VersionID newClnt, err := newClientFromAlias(targetAlias, objectURL) if err != nil { return err.Trace(objectURL) } // Generate share URL. shareURL, err := newClnt.ShareDownload(ctx, objectVersionID, expiry) if err != nil { // add objectURL and expiry as part of the trace arguments. return err.Trace(objectURL, "expiry="+expiry.String()) } // Make new entries to shareDB. contentType := "" // Not useful for download shares. shareDB.Set(objectURL, shareURL, expiry, contentType) printMsg(shareMessage{ ObjectURL: objectURL, ShareURL: shareURL, TimeLeft: expiry, ContentType: contentType, }) } // Save downloads and return. return shareDB.Save(shareDownloadsFile) } // main for share download. func mainShareDownload(cliCtx *cli.Context) error { ctx, cancelShareDownload := context.WithCancel(globalContext) defer cancelShareDownload() // Parse encryption keys per command. encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) fatalIf(err, "Unable to parse encryption keys.") // check input arguments. checkShareDownloadSyntax(ctx, cliCtx, encKeyDB) // Initialize share config folder. initShareConfig() // Additional command speific theme customization. shareSetColor() // Set command flags from context. isRecursive := cliCtx.Bool("recursive") versionID := cliCtx.String("version-id") expiry := shareDefaultExpiry if cliCtx.String("expire") != "" { var e error expiry, e = time.ParseDuration(cliCtx.String("expire")) fatalIf(probe.NewError(e), "Unable to parse expire=`"+cliCtx.String("expire")+"`.") } for _, targetURL := range cliCtx.Args() { err := doShareDownloadURL(ctx, targetURL, versionID, isRecursive, expiry) if err != nil { switch err.ToGoError().(type) { case APINotImplemented: fatalIf(err.Trace(), "Unable to share a non S3 url `"+targetURL+"`.") default: fatalIf(err.Trace(targetURL), "Unable to share target `"+targetURL+"`.") } } } return nil } minio-client-0.0~20250403/cmd/share-list-main.go000066400000000000000000000062411477450377600210740ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "time" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" ) var shareListFlags = []cli.Flag{} // Share documents via URL. var shareList = cli.Command{ Name: "list", ShortName: "ls", Usage: "list previously shared objects", Action: mainShareList, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(shareListFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} COMMAND - {{.Usage}} USAGE: {{.HelpName}} COMMAND COMMAND: upload: list previously shared access to uploads. download: list previously shared access to downloads. EXAMPLES: 1. List previously shared downloads, that haven't expired yet. {{.Prompt}} {{.HelpName}} download 2. List previously shared uploads, that haven't expired yet. {{.Prompt}} {{.HelpName}} upload `, } // validate command-line args. func checkShareListSyntax(ctx *cli.Context) { args := ctx.Args() if !args.Present() || (args.First() != "upload" && args.First() != "download") { showCommandHelpAndExit(ctx, 1) // last argument is exit code. } } // doShareList list shared url's. func doShareList(cmd string) *probe.Error { if cmd != "upload" && cmd != "download" { return probe.NewError(fmt.Errorf("Unknown argument `%s` passed", cmd)) } // Fetch defaults. uploadsFile := getShareUploadsFile() downloadsFile := getShareDownloadsFile() // Load previously saved upload-shares. shareDB := newShareDBV1() // if upload - read uploads file. if cmd == "upload" { if err := shareDB.Load(uploadsFile); err != nil { return err.Trace(uploadsFile) } } // if download - read downloads file. if cmd == "download" { if err := shareDB.Load(downloadsFile); err != nil { return err.Trace(downloadsFile) } } // Print previously shared entries. for shareURL, share := range shareDB.Shares { printMsg(shareMessage{ ObjectURL: share.URL, ShareURL: shareURL, TimeLeft: share.Expiry - time.Since(share.Date), ContentType: share.ContentType, }) } return nil } // main entry point for share list. func mainShareList(ctx *cli.Context) error { // validate command-line args. checkShareListSyntax(ctx) // Additional command speific theme customization. shareSetColor() // Initialize share config folder. initShareConfig() // List shares. fatalIf(doShareList(ctx.Args().First()).Trace(), "Unable to list previously shared URLs.") return nil } minio-client-0.0~20250403/cmd/share-main.go000066400000000000000000000040511477450377600201200ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" "path/filepath" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var shareFlags = []cli.Flag{} var shareSubcommands = []cli.Command{ shareDownload, shareUpload, shareList, } // Share documents via URL. var shareCmd = cli.Command{ Name: "share", Usage: "generate URL for temporary access to an object", Action: mainShare, Before: setGlobalsFromContext, Flags: append(shareFlags, globalFlags...), HideHelpCommand: true, Subcommands: shareSubcommands, } // migrateShare migrate to newest version sequentially. func migrateShare() { if !isShareDirExists() { return } // Shared URLs are now managed by sub-commands. So delete any old URLs file if found. oldShareFile := filepath.Join(mustGetShareDir(), "urls.json") if _, e := os.Stat(oldShareFile); e == nil { // Old file exits. e := os.Remove(oldShareFile) fatalIf(probe.NewError(e), "Unable to delete old `"+oldShareFile+"`.") console.Infof("Removed older version of share `%s` file.\n", oldShareFile) } } // mainShare - main handler for mc share command. func mainShare(ctx *cli.Context) error { commandNotFound(ctx, shareSubcommands) return nil // Sub-commands like "upload" and "download" have their own main. } minio-client-0.0~20250403/cmd/share-upload-main.go000066400000000000000000000151401477450377600214030ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "regexp" "strings" "time" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" ) var shareUploadFlags = []cli.Flag{ cli.BoolFlag{ Name: "recursive, r", Usage: "recursively upload any object matching the prefix", }, shareFlagExpire, shareFlagContentType, } // Share documents via URL. var shareUpload = cli.Command{ Name: "upload", Usage: "generate `curl` command to upload objects without requiring access/secret keys", Action: mainShareUpload, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(shareUploadFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Generate a curl command to allow upload access for a single object. Command expires in 7 days (default). {{.Prompt}} {{.HelpName}} s3/backup/2006-Mar-1/backup.tar.gz 2. Generate a curl command to allow upload access to a folder. Command expires in 120 hours. {{.Prompt}} {{.HelpName}} --expire=120h s3/backup/2007-Mar-2/ 3. Generate a curl command to allow upload access of only '.png' images to a folder. Command expires in 2 hours. {{.Prompt}} {{.HelpName}} --expire=2h --content-type=image/png s3/backup/2007-Mar-2/ 4. Generate a curl command to allow upload access to any objects matching the key prefix 'backup/'. Command expires in 2 hours. {{.Prompt}} {{.HelpName}} --recursive --expire=2h s3/backup/2007-Mar-2/backup/ `, } var shellQuoteRegex = regexp.MustCompile("([&;#$` \t\n<>()|'\"])") func shellQuote(s string) string { return shellQuoteRegex.ReplaceAllString(s, "\\$1") } // checkShareUploadSyntax - validate command-line args. func checkShareUploadSyntax(ctx *cli.Context) { args := ctx.Args() if !args.Present() { showCommandHelpAndExit(ctx, 1) // last argument is exit code. } // Set command flags from context. isRecursive := ctx.Bool("recursive") expireArg := ctx.String("expire") // Parse expiry. expiry := shareDefaultExpiry if expireArg != "" { var e error expiry, e = time.ParseDuration(expireArg) fatalIf(probe.NewError(e), "Unable to parse expire=`"+expireArg+"`.") } // Validate expiry. if expiry.Seconds() < 1 { fatalIf(errDummy().Trace(expiry.String()), "Expiry cannot be lesser than 1 second.") } if expiry.Seconds() > 604800 { fatalIf(errDummy().Trace(expiry.String()), "Expiry cannot be larger than 7 days.") } for _, targetURL := range ctx.Args() { url := newClientURL(targetURL) if strings.HasSuffix(targetURL, string(url.Separator)) && !isRecursive { fatalIf(errInvalidArgument().Trace(targetURL), "Use --recursive flag to generate curl command for prefixes.") } } } // makeCurlCmd constructs curl command-line. func makeCurlCmd(key, postURL string, isRecursive bool, uploadInfo map[string]string) (string, *probe.Error) { postURL += " " curlCommand := "curl " + postURL for k, v := range uploadInfo { if k == "key" { key = v continue } curlCommand += fmt.Sprintf("-F %s=%s ", k, v) } // If key starts with is enabled prefix it with the output. if isRecursive { curlCommand += fmt.Sprintf("-F key=%s ", shellQuote(key)) // Object name. } else { curlCommand += fmt.Sprintf("-F key=%s ", shellQuote(key)) // Object name. } curlCommand += "-F file=@" // File to upload. return curlCommand, nil } // save shared URL to disk. func saveSharedURL(objectURL, shareURL string, expiry time.Duration, contentType string) *probe.Error { // Load previously saved upload-shares. shareDB := newShareDBV1() if err := shareDB.Load(getShareUploadsFile()); err != nil { return err.Trace(getShareUploadsFile()) } // Make new entries to uploadsDB. shareDB.Set(objectURL, shareURL, expiry, contentType) shareDB.Save(getShareUploadsFile()) return nil } // doShareUploadURL uploads files to the target. func doShareUploadURL(ctx context.Context, objectURL string, isRecursive bool, expiry time.Duration, contentType string) *probe.Error { clnt, err := newClient(objectURL) if err != nil { return err.Trace(objectURL) } // Generate pre-signed access info. shareURL, uploadInfo, err := clnt.ShareUpload(ctx, isRecursive, expiry, contentType) if err != nil { return err.Trace(objectURL, "expiry="+expiry.String(), "contentType="+contentType) } // Get the new expanded url. objectURL = clnt.GetURL().String() // Generate curl command. curlCmd, err := makeCurlCmd(objectURL, shareURL, isRecursive, uploadInfo) if err != nil { return err.Trace(objectURL) } printMsg(shareMessage{ ObjectURL: objectURL, ShareURL: curlCmd, TimeLeft: expiry, ContentType: contentType, }) // save shared URL to disk. return saveSharedURL(objectURL, curlCmd, expiry, contentType) } // main for share upload command. func mainShareUpload(cliCtx *cli.Context) error { ctx, cancelShareDownload := context.WithCancel(globalContext) defer cancelShareDownload() // check input arguments. checkShareUploadSyntax(cliCtx) // Initialize share config folder. initShareConfig() // Additional command speific theme customization. shareSetColor() // Set command flags from context. isRecursive := cliCtx.Bool("recursive") expireArg := cliCtx.String("expire") expiry := shareDefaultExpiry contentType := cliCtx.String("content-type") if expireArg != "" { var e error expiry, e = time.ParseDuration(expireArg) fatalIf(probe.NewError(e), "Unable to parse expire=`"+expireArg+"`.") } for _, targetURL := range cliCtx.Args() { err := doShareUploadURL(ctx, targetURL, isRecursive, expiry, contentType) if err != nil { switch err.ToGoError().(type) { case APINotImplemented: fatalIf(err.Trace(), "Unable to share a non S3 url `"+targetURL+"`.") default: fatalIf(err.Trace(targetURL), "Unable to generate curl command for upload `"+targetURL+"`.") } } } return nil } minio-client-0.0~20250403/cmd/share-upload_test.go000066400000000000000000000032771477450377600215300ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "strings" "testing" ) func TestMakeCurlCmdEscapesSpecialChars(t *testing.T) { testCases := []struct { key string expectedKey string }{ { key: "Robert O'Neil.png", expectedKey: "Robert\\ O\\'Neil.png", }, { key: "A&B-Design|2014.pdf", expectedKey: "A\\&B-Design\\\\|2014.pdf", }, { key: "A&B-Design(Revision 1).pdf", expectedKey: "A\\&B-Design\\(Revision\\ 1\\).pdf", }, { key: "Matt`s\tResume.pdf", expectedKey: "Matt\\`s\\\tResume.pdf", }, { key: "out.pdf;rm -rf $HOME #", expectedKey: "out.pdf\\;rm\\ -rf\\ \\$HOME\\ \\#", }, } for _, testCase := range testCases { cmd, _ := makeCurlCmd(testCase.key, "http://example.com", false, map[string]string{}) if !strings.Contains(cmd, " key="+testCase.expectedKey+" ") { t.Errorf("Did not find key=%s in command %s", testCase.expectedKey, cmd) } } } minio-client-0.0~20250403/cmd/share.go000066400000000000000000000141771477450377600172100ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "fmt" "os" "path/filepath" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) const ( // Default expiry is 7 days (168h). shareDefaultExpiry = time.Duration(604800) * time.Second ) // Upload specific flags. var ( shareFlagContentType = cli.StringFlag{ Name: "content-type, T", Usage: "specify a content-type to allow", } shareFlagExpire = cli.StringFlag{ Name: "expire, E", Value: "168h", Usage: "set expiry in NN[h|m|s]", } ) // Structured share command message. type shareMessage struct { Status string `json:"status"` ObjectURL string `json:"url"` ShareURL string `json:"share"` TimeLeft time.Duration `json:"timeLeft"` ContentType string `json:"contentType,omitempty"` // Only used by upload cmd. } // String - Themefied string message for console printing. func (s shareMessage) String() string { msg := console.Colorize("URL", fmt.Sprintf("URL: %s\n", s.ObjectURL)) msg += console.Colorize("Expire", fmt.Sprintf("Expire: %s\n", timeDurationToHumanizedDuration(s.TimeLeft))) if s.ContentType != "" { msg += console.Colorize("Content-type", fmt.Sprintf("Content-Type: %s\n", s.ContentType)) } // Highlight specifically. "share upload" sub-commands use this identifier. shareURL := strings.Replace(s.ShareURL, "", console.Colorize("File", ""), 1) // Highlight specifically for recursive operation. shareURL = strings.Replace(shareURL, "", console.Colorize("File", ""), 1) msg += console.Colorize("Share", fmt.Sprintf("Share: %s\n", shareURL)) return msg } // JSON - JSONified message for scripting. func (s shareMessage) JSON() string { s.Status = "success" shareMessageBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") // JSON encoding escapes ampersand into its unicode character // which is not usable directly for share and fails with cloud // storage. convert them back so that they are usable. shareMessageBytes = bytes.ReplaceAll(shareMessageBytes, []byte("\\u0026"), []byte("&")) shareMessageBytes = bytes.ReplaceAll(shareMessageBytes, []byte("\\u003c"), []byte("<")) shareMessageBytes = bytes.ReplaceAll(shareMessageBytes, []byte("\\u003e"), []byte(">")) return string(shareMessageBytes) } // shareSetColor sets colors share sub-commands. func shareSetColor() { // Additional command speific theme customization. console.SetColor("URL", color.New(color.Bold)) console.SetColor("Expire", color.New(color.FgCyan)) console.SetColor("Content-type", color.New(color.FgBlue)) console.SetColor("Share", color.New(color.FgGreen)) console.SetColor("File", color.New(color.FgRed, color.Bold)) } // Get share dir name. func getShareDir() (string, *probe.Error) { configDir, err := getMcConfigDir() if err != nil { return "", err.Trace() } sharedURLsDataDir := filepath.Join(configDir, globalSharedURLsDataDir) return sharedURLsDataDir, nil } // Get share dir name or die. (NOTE: This `Die` approach is only OK for mc like tools.). func mustGetShareDir() string { shareDir, err := getShareDir() fatalIf(err.Trace(), "Unable to determine share folder.") return shareDir } // Check if the share dir exists. func isShareDirExists() bool { if _, e := os.Stat(mustGetShareDir()); e != nil { return false } return true } // Create config share dir. func createShareDir() *probe.Error { if e := os.MkdirAll(mustGetShareDir(), 0o700); e != nil { return probe.NewError(e) } return nil } // Get share uploads file. func getShareUploadsFile() string { return filepath.Join(mustGetShareDir(), "uploads.json") } // Get share downloads file. func getShareDownloadsFile() string { return filepath.Join(mustGetShareDir(), "downloads.json") } // Check if share uploads file exists?. func isShareUploadsExists() bool { if _, e := os.Stat(getShareUploadsFile()); e != nil { return false } return true } // Check if share downloads file exists?. func isShareDownloadsExists() bool { if _, e := os.Stat(getShareDownloadsFile()); e != nil { return false } return true } // Initialize share uploads file. func initShareUploadsFile() *probe.Error { return newShareDBV1().Save(getShareUploadsFile()) } // Initialize share downloads file. func initShareDownloadsFile() *probe.Error { return newShareDBV1().Save(getShareDownloadsFile()) } // Initialize share directory, if not done already. func initShareConfig() { // Share directory. if !isShareDirExists() { fatalIf(createShareDir().Trace(mustGetShareDir()), "Failed to create share `"+mustGetShareDir()+"` folder.") if !globalQuiet && !globalJSON { console.Infof("Successfully created `%s`.\n", mustGetShareDir()) } } // Uploads share file. if !isShareUploadsExists() { fatalIf(initShareUploadsFile().Trace(getShareUploadsFile()), "Failed to initialize share uploads `"+getShareUploadsFile()+"` file.") if !globalQuiet && !globalJSON { console.Infof("Initialized share uploads `%s` file.\n", getShareUploadsFile()) } } // Downloads share file. if !isShareDownloadsExists() { fatalIf(initShareDownloadsFile().Trace(getShareDownloadsFile()), "Failed to initialize share downloads `"+getShareDownloadsFile()+"` file.") if !globalQuiet && !globalJSON { console.Infof("Initialized share downloads `%s` file.\n", getShareDownloadsFile()) } } } minio-client-0.0~20250403/cmd/signals.go000066400000000000000000000033111477450377600175320ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" "os/signal" ) // trapSignals traps the registered signals and cancel the global context. func trapSignals(sig ...os.Signal) { // channel to receive signals. sigCh := make(chan os.Signal, 1) defer close(sigCh) // `signal.Notify` registers the given channel to // receive notifications of the specified signals. signal.Notify(sigCh, sig...) // Wait for the signal. s := <-sigCh // Once signal has been received stop signal Notify handler. signal.Stop(sigCh) // Stop profiling if enabled, this needs to be before canceling the // global context to check for any unusual cpu/mem/goroutines usage stopProfiling() // Cancel the global context globalCancel() var exitCode int switch s.String() { case "interrupt": exitCode = globalCancelExitStatus case "killed": exitCode = globalKillExitStatus case "terminated": exitCode = globalTerminatExitStatus default: exitCode = globalErrorExitStatus } os.Exit(exitCode) } minio-client-0.0~20250403/cmd/speedtest-spinner.go000066400000000000000000000223551477450377600215570ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "sort" "strings" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" humanize "github.com/dustin/go-humanize" "github.com/minio/madmin-go/v3" "github.com/olekukonko/tablewriter" ) var whiteStyle = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#ffffff")) type speedTestUI struct { spinner spinner.Model quitting bool result PerfTestResult } // PerfTestType - The type of performance test (net/drive/object) type PerfTestType byte // Constants for performance test type const ( NetPerfTest PerfTestType = 1 << iota DrivePerfTest ObjectPerfTest SiteReplicationPerfTest ClientPerfTest ) // Name - returns name of the performance test func (p PerfTestType) Name() string { switch p { case NetPerfTest: return "NetPerf" case DrivePerfTest: return "DrivePerf" case ObjectPerfTest: return "ObjectPerf" case SiteReplicationPerfTest: return "SiteReplication" case ClientPerfTest: return "Client" } return "" } // PerfTestResult - stores the result of a performance test type PerfTestResult struct { Type PerfTestType `json:"type"` ObjectResult *madmin.SpeedTestResult `json:"object,omitempty"` NetResult *madmin.NetperfResult `json:"network,omitempty"` SiteReplicationResult *madmin.SiteNetPerfResult `json:"siteReplication,omitempty"` ClientResult *madmin.ClientPerfResult `json:"client,omitempty"` DriveResult []madmin.DriveSpeedTestResult `json:"drive,omitempty"` Err string `json:"err,omitempty"` Final bool `json:"final,omitempty"` } func initSpeedTestUI() *speedTestUI { s := spinner.New() s.Spinner = spinner.Points s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) return &speedTestUI{ spinner: s, } } func (m *speedTestUI) Init() tea.Cmd { return m.spinner.Tick } func (m *speedTestUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "esc", "ctrl+c": m.quitting = true return m, tea.Quit default: return m, nil } case PerfTestResult: m.result = msg if msg.Final { m.quitting = true return m, tea.Quit } return m, nil default: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd } } func (m *speedTestUI) View() string { // Quit when there is an error if m.result.Err != "" { return fmt.Sprintf("\n%s: %s (Err: %s)\n", m.result.Type.Name(), crossTickCell, m.result.Err) } var s strings.Builder // Set table header table := tablewriter.NewWriter(&s) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) ores := m.result.ObjectResult nres := m.result.NetResult sres := m.result.SiteReplicationResult dres := m.result.DriveResult cres := m.result.ClientResult trailerIfGreaterThan := func(in string, maxIdx int) string { if len(in) < maxIdx { return in } return in[:maxIdx] + "..." } // Print the spinner if !m.quitting { s.WriteString(fmt.Sprintf("\n%s: %s\n\n", m.result.Type.Name(), m.spinner.View())) } else { s.WriteString(fmt.Sprintf("\n%s: %s\n\n", m.result.Type.Name(), m.spinner.Style.Render(tickCell))) } if ores != nil { table.SetHeader([]string{"", "Throughput", "IOPS"}) data := make([][]string, 2) if ores.Version == "" { data[0] = []string{ "PUT", whiteStyle.Render("-- KiB/sec"), whiteStyle.Render("-- objs/sec"), } data[1] = []string{ "GET", whiteStyle.Render("-- KiB/sec"), whiteStyle.Render("-- objs/sec"), } } else { data[0] = []string{ "PUT", whiteStyle.Render(humanize.IBytes(ores.PUTStats.ThroughputPerSec) + "/s"), whiteStyle.Render(humanize.Comma(int64(ores.PUTStats.ObjectsPerSec)) + " objs/s"), } data[1] = []string{ "GET", whiteStyle.Render(humanize.IBytes(ores.GETStats.ThroughputPerSec) + "/s"), whiteStyle.Render(humanize.Comma(int64(ores.GETStats.ObjectsPerSec)) + " objs/s"), } } table.AppendBulk(data) table.Render() if m.quitting { s.WriteString("\n" + objectTestShortResult(ores)) if globalPerfTestVerbose { s.WriteString("\n\n") s.WriteString(objectTestVerboseResult(ores)) } s.WriteString("\n") } } else if nres != nil { table.SetHeader([]string{"Node", "RX", "TX", ""}) data := make([][]string, 0, len(nres.NodeResults)) if len(nres.NodeResults) == 0 { data = append(data, []string{ "...", whiteStyle.Render("-- MiB/s"), whiteStyle.Render("-- MiB/s"), "", }) } else { for _, nodeResult := range nres.NodeResults { nodeErr := "" if nodeResult.Error != "" { nodeErr = "Err: " + nodeResult.Error } data = append(data, []string{ trailerIfGreaterThan(nodeResult.Endpoint, 64), whiteStyle.Render(humanize.IBytes(uint64(nodeResult.RX))) + "/s", whiteStyle.Render(humanize.IBytes(uint64(nodeResult.TX))) + "/s", nodeErr, }) } } sort.Slice(data, func(i, j int) bool { return data[i][0] < data[j][0] }) table.AppendBulk(data) table.Render() } else if sres != nil { table.SetHeader([]string{"Endpoint", "RX", "TX", ""}) data := make([][]string, 0, len(sres.NodeResults)) if len(sres.NodeResults) == 0 { data = append(data, []string{ "...", whiteStyle.Render("-- MiB"), whiteStyle.Render("-- MiB"), "", }) } else { for _, nodeResult := range sres.NodeResults { if nodeResult.Error != "" { data = append(data, []string{ trailerIfGreaterThan(nodeResult.Endpoint, 64), crossTickCell, crossTickCell, "Err: " + nodeResult.Error, }) } else { dataItem := []string{} dataError := "" // show endpoint dataItem = append(dataItem, trailerIfGreaterThan(nodeResult.Endpoint, 64)) // show RX if uint64(nodeResult.RXTotalDuration.Seconds()) == 0 { dataError += "- RXTotalDuration are zero " dataItem = append(dataItem, crossTickCell) } else { dataItem = append(dataItem, whiteStyle.Render(humanize.IBytes(nodeResult.RX/uint64(nodeResult.RXTotalDuration.Seconds())))+"/s") } // show TX if uint64(nodeResult.TXTotalDuration.Seconds()) == 0 { dataError += "- TXTotalDuration are zero" dataItem = append(dataItem, crossTickCell) } else { dataItem = append(dataItem, whiteStyle.Render(humanize.IBytes(nodeResult.TX/uint64(nodeResult.TXTotalDuration.Seconds())))+"/s") } // show message dataItem = append(dataItem, dataError) data = append(data, dataItem) } } } sort.Slice(data, func(i, j int) bool { return data[i][0] < data[j][0] }) table.AppendBulk(data) table.Render() } else if dres != nil { table.SetHeader([]string{"Node", "Path", "Read", "Write", ""}) data := make([][]string, 0, len(dres)) if len(dres) == 0 { data = append(data, []string{ "...", "...", whiteStyle.Render("-- KiB/s"), whiteStyle.Render("-- KiB/s"), "", }) } else { for _, driveResult := range dres { for _, result := range driveResult.DrivePerf { if result.Error != "" { data = append(data, []string{ trailerIfGreaterThan(driveResult.Endpoint, 64), result.Path, crossTickCell, crossTickCell, "Err: " + result.Error, }) } else { data = append(data, []string{ trailerIfGreaterThan(driveResult.Endpoint, 64), result.Path, whiteStyle.Render(humanize.IBytes(result.ReadThroughput)) + "/s", whiteStyle.Render(humanize.IBytes(result.WriteThroughput)) + "/s", "", }) } } } } table.AppendBulk(data) table.Render() } else if cres != nil { table.SetHeader([]string{"Endpoint", "Tx"}) data := make([][]string, 0, 2) tx := uint64(0) if cres.TimeSpent > 0 { tx = uint64(float64(cres.BytesSend) / time.Duration(cres.TimeSpent).Seconds()) } if tx == 0 { data = append(data, []string{ "...", whiteStyle.Render("-- KiB/s"), "", }) } else { data = append(data, []string{ cres.Endpoint, whiteStyle.Render(humanize.IBytes(tx)) + "/s", cres.Error, }) } table.AppendBulk(data) table.Render() } return s.String() } minio-client-0.0~20250403/cmd/sql-main.go000066400000000000000000000371261477450377600176260ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "compress/bzip2" "compress/gzip" "context" "errors" "fmt" "io" "os" "path/filepath" "regexp" "strings" "time" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/mimedb" ) var sqlFlags = []cli.Flag{ cli.StringFlag{ Name: "query, e", Usage: "sql query expression", Value: "select * from s3object", }, cli.BoolFlag{ Name: "recursive, r", Usage: "sql query recursively", }, cli.StringFlag{ Name: "csv-input", Usage: "csv input serialization option", }, cli.StringFlag{ Name: "json-input", Usage: "json input serialization option", }, cli.StringFlag{ Name: "compression", Usage: "input compression type", }, cli.StringFlag{ Name: "csv-output", Usage: "csv output serialization option", }, cli.StringFlag{ Name: "csv-output-header", Usage: "optional csv output header ", }, cli.StringFlag{ Name: "json-output", Usage: "json output serialization option", }, } // Display contents of a file. var sqlCmd = cli.Command{ Name: "sql", Usage: "run sql queries on objects", Action: mainSQL, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(append(sqlFlags, encCFlag), globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET...] {{if .VisibleFlags}} FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} SERIALIZATION OPTIONS: For query serialization options, refer to https://min.io/docs/minio/linux/reference/minio-mc/mc-sql.html#command-mc.sql EXAMPLES: 1. Run a query on a set of objects recursively on AWS S3. {{.Prompt}} {{.HelpName}} --recursive --query "select * from S3Object" s3/personalbucket/my-large-csvs/ 2. Run a query on an object on MinIO. {{.Prompt}} {{.HelpName}} --query "select count(s.power) from S3Object s" myminio/iot-devices/power-ratio.csv 3. Run a query on an encrypted object with customer provided keys. {{.Prompt}} {{.HelpName}} --enc-c "myminio/iot-devices=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" \ --query "select count(s.power) from S3Object s" myminio/iot-devices/power-ratio-encrypted.csv 4. Run a query on an object on MinIO in gzip format using ; as field delimiter, newline as record delimiter and file header to be used {{.Prompt}} {{.HelpName}} --compression GZIP --csv-input "rd=\n,fh=USE,fd=;" \ --query "select count(s.power) from S3Object s" myminio/iot-devices/power-ratio.csv.gz 5. Run a query on an object on MinIO in gzip format using ; as field delimiter, newline as record delimiter and file header to be used {{.Prompt}} {{.HelpName}} --compression GZIP --csv-input "rd=\n,fh=USE,fd=;" \ --json-output "rd=\n\n" --query "select * from S3Object" myminio/iot-devices/data.csv 6. Run same query as in 5., but specify csv output headers. If --csv-output-headers is specified as "", first row of csv is interpreted as header {{.Prompt}} {{.HelpName}} --compression GZIP --csv-input "rd=\n,fh=USE,fd=;" \ --csv-output "rd=\n" --csv-output-header "device_id,uptime,lat,lon" \ --query "select * from S3Object" myminio/iot-devices/data.csv `, } // valid CSV and JSON keys for input/output serialization var ( validCSVCommonKeys = []string{"FieldDelimiter", "QuoteChar", "QuoteEscChar"} validCSVInputKeys = []string{"Comments", "FileHeader", "QuotedRecordDelimiter", "RecordDelimiter"} validCSVOutputKeys = []string{"QuoteFields"} validJSONInputKeys = []string{"Type"} validJSONCSVCommonOutputKeys = []string{"RecordDelimiter"} // mapping of abbreviation to long form name of CSV and JSON input/output serialization keys validCSVInputAbbrKeys = map[string]string{"cc": "Comments", "fh": "FileHeader", "qrd": "QuotedRecordDelimiter", "rd": "RecordDelimiter", "fd": "FieldDelimiter", "qc": "QuoteChar", "qec": "QuoteEscChar"} validCSVOutputAbbrKeys = map[string]string{"qf": "QuoteFields", "rd": "RecordDelimiter", "fd": "FieldDelimiter", "qc": "QuoteChar", "qec": "QuoteEscChar"} validJSONOutputAbbrKeys = map[string]string{"rd": "RecordDelimiter"} ) // parseKVArgs parses string of the form k=v delimited by "," // into a map of k-v pairs func parseKVArgs(is string) (map[string]string, *probe.Error) { kvmap := make(map[string]string) var key, value string var s, e int // tracking start and end of value var index int // current index in string if is != "" { for index < len(is) { i := strings.Index(is[index:], "=") if i == -1 { return nil, probe.NewError(errors.New("Arguments should be of the form key=value,... ")) } key = is[index : index+i] s = i + index + 1 e = strings.Index(is[s:], ",") delimFound := false for !delimFound { if e == -1 || e+s >= len(is) { delimFound = true break } if string(is[s+e]) != "," { delimFound = true if string(is[s+e-1]) == "," { e-- } } else { e++ } } vEnd := len(is) if e != -1 { vEnd = s + e } value = is[s:vEnd] index = vEnd + 1 if _, ok := kvmap[strings.ToLower(key)]; ok { return nil, probe.NewError(fmt.Errorf("More than one key=value found for %s", strings.TrimSpace(key))) } kvmap[strings.ToLower(key)] = strings.NewReplacer(`\n`, "\n", `\t`, "\t", `\r`, "\r").Replace(value) } } return kvmap, nil } // returns a string with list of serialization options and abbreviation(s) if any func fmtString(validAbbr map[string]string, validKeys []string) string { var sb strings.Builder i := 0 for k, v := range validAbbr { sb.WriteString(fmt.Sprintf("%s(%s) ", v, k)) i++ if i != len(validAbbr) { sb.WriteString(",") } } if len(sb.String()) == 0 { for _, k := range validKeys { sb.WriteString(fmt.Sprintf("%s ", k)) } } return sb.String() } // parses the input string and constructs a k-v map, replacing any abbreviated keys with actual keys func parseSerializationOpts(inp string, validKeys []string, validAbbrKeys map[string]string) (map[string]string, *probe.Error) { if validAbbrKeys == nil { validAbbrKeys = make(map[string]string) } validKeyFn := func(key string, validKeys []string) bool { for _, name := range validKeys { if strings.EqualFold(name, key) { return true } } return false } kv, err := parseKVArgs(inp) if err != nil { return nil, err } ikv := make(map[string]string) for k, v := range kv { fldName, ok := validAbbrKeys[strings.ToLower(k)] if ok { ikv[strings.ToLower(fldName)] = v } else { ikv[strings.ToLower(k)] = v } } for k := range ikv { if !validKeyFn(k, validKeys) { return nil, probe.NewError(errors.New("Options should be key-value pairs in the form key=value,... where valid key(s) are " + fmtString(validAbbrKeys, validKeys))) } } return ikv, nil } // gets the input serialization opts from cli context and constructs a map of csv, json or parquet options func getInputSerializationOpts(ctx *cli.Context) map[string]map[string]string { icsv := ctx.String("csv-input") ijson := ctx.String("json-input") m := make(map[string]map[string]string) csvType := ctx.IsSet("csv-input") jsonType := ctx.IsSet("json-input") if csvType && jsonType { fatalIf(errInvalidArgument(), "Only one of --csv-input or --json-input can be specified as input serialization option") } if icsv != "" { kv, err := parseSerializationOpts(icsv, append(validCSVCommonKeys, validCSVInputKeys...), validCSVInputAbbrKeys) fatalIf(err, "Invalid serialization option(s) specified for --csv-input flag") m["csv"] = kv } if ijson != "" { kv, err := parseSerializationOpts(ijson, validJSONInputKeys, nil) fatalIf(err, "Invalid serialization option(s) specified for --json-input flag") m["json"] = kv } return m } // gets the output serialization opts from cli context and constructs a map of csv or json options func getOutputSerializationOpts(ctx *cli.Context, csvHdrs []string) (opts map[string]map[string]string) { m := make(map[string]map[string]string) ocsv := ctx.String("csv-output") ojson := ctx.String("json-output") csvType := ctx.IsSet("csv-output") jsonType := ctx.IsSet("json-output") if csvType && jsonType { fatalIf(errInvalidArgument(), "Only one of --csv-output, or --json-output can be specified as output serialization option") } if jsonType && len(csvHdrs) > 0 { fatalIf(errInvalidArgument(), "--csv-output-header incompatible with --json-output option") } if csvType { validKeys := append(validCSVCommonKeys, validJSONCSVCommonOutputKeys...) kv, err := parseSerializationOpts(ocsv, append(validKeys, validCSVOutputKeys...), validCSVOutputAbbrKeys) fatalIf(err, "Invalid value(s) specified for --csv-output flag") m["csv"] = kv } if jsonType || globalJSON { kv, err := parseSerializationOpts(ojson, validJSONCSVCommonOutputKeys, validJSONOutputAbbrKeys) fatalIf(err, "Invalid value(s) specified for --json-output flag") m["json"] = kv } return m } // getCSVHeader fetches the first line of csv query object func getCSVHeader(sourceURL string, encKeyDB map[string][]prefixSSEPair) ([]string, *probe.Error) { var r io.ReadCloser switch sourceURL { case "-": r = os.Stdin default: var err *probe.Error var content *ClientContent if r, content, err = getSourceStreamMetadataFromURL(globalContext, sourceURL, "", time.Time{}, encKeyDB, false); err != nil { return nil, err.Trace(sourceURL) } ctype := content.Metadata["Content-Type"] if strings.Contains(ctype, "gzip") { var e error r, e = gzip.NewReader(r) if e != nil { return nil, probe.NewError(e) } defer r.Close() } else if strings.Contains(ctype, "bzip") { defer r.Close() r = io.NopCloser(bzip2.NewReader(r)) } else { defer r.Close() } } br := bufio.NewReader(r) line, _, e := br.ReadLine() if e != nil { return nil, probe.NewError(e) } return strings.Split(string(line), ","), nil } // returns true if query is selectign all columns of the csv object func isSelectAll(query string) bool { match, _ := regexp.MatchString("^\\s*?select\\s+?\\*\\s+?.*?$", query) return match } // if csv-output-header is set to a comma delimited string use it, othjerwise attempt to get the header from // query object func getCSVOutputHeaders(ctx *cli.Context, url string, encKeyDB map[string][]prefixSSEPair, query string) (hdrs []string) { if !ctx.IsSet("csv-output-header") { return } hdrStr := ctx.String("csv-output-header") if hdrStr == "" && isSelectAll(query) { // attempt to get the first line of csv as header if hdrs, err := getCSVHeader(url, encKeyDB); err == nil { return hdrs } } hdrs = strings.Split(hdrStr, ",") return } // get the Select options for sql select API func getSQLOpts(ctx *cli.Context, csvHdrs []string) (s SelectObjectOpts) { is := getInputSerializationOpts(ctx) os := getOutputSerializationOpts(ctx, csvHdrs) return SelectObjectOpts{ InputSerOpts: is, OutputSerOpts: os, CompressionType: minio.SelectCompressionType(ctx.String("compression")), } } func isCSVOrJSON(inOpts map[string]map[string]string) bool { if _, ok := inOpts["csv"]; ok { return true } if _, ok := inOpts["json"]; ok { return true } return false } func sqlSelect(targetURL, expression string, encKeyDB map[string][]prefixSSEPair, selOpts SelectObjectOpts, csvHdrs []string, writeHdr bool) *probe.Error { ctx, cancelSelect := context.WithCancel(globalContext) defer cancelSelect() alias, _, _, err := expandAlias(targetURL) if err != nil { return err.Trace(targetURL) } targetClnt, err := newClient(targetURL) if err != nil { return err.Trace(targetURL) } sseKey := getSSE(targetURL, encKeyDB[alias]) outputer, err := targetClnt.Select(ctx, expression, sseKey, selOpts) if err != nil { return err.Trace(targetURL, expression) } defer outputer.Close() // write csv header to stdout if len(csvHdrs) > 0 && writeHdr { fmt.Println(strings.Join(csvHdrs, ",")) } _, e := io.Copy(os.Stdout, outputer) return probe.NewError(e) } func validateOpts(selOpts SelectObjectOpts, url string) { _, targetURL, _ := mustExpandAlias(url) if strings.HasSuffix(targetURL, ".parquet") && isCSVOrJSON(selOpts.InputSerOpts) { fatalIf(errInvalidArgument(), "Input serialization flags --csv-input and --json-input cannot be used for object in .parquet format") } } // validate args and optionally fetch the csv header of query object func getAndValidateArgs(ctx *cli.Context, encKeyDB map[string][]prefixSSEPair, url string) (query string, csvHdrs []string, selOpts SelectObjectOpts) { query = ctx.String("query") csvHdrs = getCSVOutputHeaders(ctx, url, encKeyDB, query) selOpts = getSQLOpts(ctx, csvHdrs) validateOpts(selOpts, url) return } // check sql input arguments. func checkSQLSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 { showCommandHelpAndExit(ctx, 1) // last argument is exit code. } } // mainSQL is the main entry point for sql command. func mainSQL(cliCtx *cli.Context) error { ctx, cancelSQL := context.WithCancel(globalContext) defer cancelSQL() var ( csvHdrs []string selOpts SelectObjectOpts query string ) // Parse encryption keys per command. encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) fatalIf(err, "Unable to parse encryption keys.") // validate sql input arguments. checkSQLSyntax(cliCtx) // extract URLs. URLs := cliCtx.Args() writeHdr := true for _, url := range URLs { if _, targetContent, err := url2Stat(ctx, url2StatOptions{urlStr: url, versionID: "", fileAttr: false, encKeyDB: encKeyDB, timeRef: time.Time{}, isZip: false, ignoreBucketExistsCheck: false}); err != nil { errorIf(err.Trace(url), "Unable to run sql for %s.", url) continue } else if !targetContent.Type.IsDir() { if writeHdr { query, csvHdrs, selOpts = getAndValidateArgs(cliCtx, encKeyDB, url) } errorIf(sqlSelect(url, query, encKeyDB, selOpts, csvHdrs, writeHdr).Trace(url), "Unable to run sql") writeHdr = false continue } targetAlias, targetURL, _ := mustExpandAlias(url) clnt, err := newClientFromAlias(targetAlias, targetURL) if err != nil { errorIf(err.Trace(url), "Unable to initialize target `%s`.", url) continue } for content := range clnt.List(ctx, ListOptions{Recursive: cliCtx.Bool("recursive"), WithMetadata: true, ShowDir: DirNone}) { if content.Err != nil { errorIf(content.Err.Trace(url), "Unable to list on target `%s`.", url) continue } if writeHdr { query, csvHdrs, selOpts = getAndValidateArgs(cliCtx, encKeyDB, targetAlias+content.URL.Path) } contentType := mimedb.TypeByExtension(filepath.Ext(content.URL.Path)) if len(content.UserMetadata) != 0 && content.UserMetadata["content-type"] != "" { contentType = content.UserMetadata["content-type"] } for _, cTypeSuffix := range supportedContentTypes { if strings.Contains(contentType, cTypeSuffix) { errorIf(sqlSelect(targetAlias+content.URL.Path, query, encKeyDB, selOpts, csvHdrs, writeHdr).Trace(content.URL.String()), "Unable to run sql") } writeHdr = false } } } // Done. return nil } minio-client-0.0~20250403/cmd/sql-main_test.go000066400000000000000000000114721477450377600206610ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "strings" "testing" ) var testParseKVArgsCases = []struct { inp string kvmap map[string]string errMsg string }{ {"fh=use,rd=|,fd=;,qec=\"", map[string]string{"fh": "use", "rd": "|", "fd": ";", "qec": "\""}, ""}, {"", map[string]string{}, ""}, {"not the right format", map[string]string{}, "Arguments should be of the form key=value,... "}, {"k==v", map[string]string{"k": "=v"}, ""}, {"k=v1,k=v2", map[string]string{}, "More than one key=value found for k"}, {"k=v1;k=v2", map[string]string{"k": "v1;k=v2"}, ""}, } func TestParseKVArgs(t *testing.T) { for _, test := range testParseKVArgsCases { kvmap, err := parseKVArgs(test.inp) gerr := err.ToGoError() if gerr != nil && gerr.Error() != test.errMsg { t.Fatalf("Unexpected result for \"%s\", expected: |%s| got: |%s|\n", test.inp, test.errMsg, gerr) } if gerr == nil && test.errMsg != "" { t.Fatalf("Unexpected result for \"%s\", expected: |%s| got: |%s|\n", test.inp, test.errMsg, gerr) } for k, v := range test.kvmap { actual, ok := kvmap[k] if !ok { t.Fatalf("Unexpected result for \"%s,\" expected %s , found %s for key %s\n", test.inp, v, actual, k) } } } } var testParseSerializationCases = []struct { inp string validKeys []string validAbbrKeys map[string]string parsedOpts map[string]string errMsg string }{ { "rd=\n,fd=;,qc=\"", append(validCSVCommonKeys, validCSVInputKeys...), validCSVInputAbbrKeys, map[string]string{"recorddelimiter": "\n", "fielddelimiter": ";", "quotechar": "\""}, "", }, { "rd=\n,fd=;,qc=\"", validCSVInputKeys, validCSVInputAbbrKeys, map[string]string{}, "Options should be key-value pairs in the form key=value,... where valid key(s) are ", }, { "nokey=\n,fd=;,qc=\"", validCSVInputKeys, validCSVInputAbbrKeys, map[string]string{}, "Options should be key-value pairs in the form key=value,... where valid key(s) are ", }, { "rd=\n\n,fd=|,qc=\",qc='", validCSVInputKeys, validCSVInputAbbrKeys, map[string]string{}, "More than one key=value found for ", }, { "recordDelimiter=\n\n,FieldDelimiter=|,QuoteChAR=\"", append(validCSVCommonKeys, validCSVInputKeys...), validCSVInputAbbrKeys, map[string]string{"recorddelimiter": "\n\n", "fielddelimiter": "|", "quotechar": "\""}, "", }, { "recordDelimiter=\n\n,FieldDelimiter=|,QuoteChAR=\",fh=use,qrd=;", append(validCSVCommonKeys, validCSVInputKeys...), validCSVInputAbbrKeys, map[string]string{"recorddelimiter": "\n\n", "fielddelimiter": "|", "quotechar": "\"", "quotedrecorddelimiter": ";", "fileheader": "use"}, "", }, { "recordDelimiter=\n\n,FieldDelimiter=|,QuoteChar=\",qf=;,qec='", append(validCSVCommonKeys, validCSVOutputKeys...), validCSVOutputAbbrKeys, map[string]string{}, "Options should be key-value pairs in the form key=value,... where valid key(s) are ", }, { "FieldDelimiter=|,QuoteChar=\",qf=;,qec='", append(validCSVCommonKeys, validCSVOutputKeys...), validCSVOutputAbbrKeys, map[string]string{"fielddelimiter": "|", "quotechar": "\"", "quotefields": ";", "quoteescchar": "'"}, "", }, { "type=lines", validJSONInputKeys, nil, map[string]string{"type": "lines"}, "", }, } func TestParseSerializationOpts(t *testing.T) { for i, test := range testParseSerializationCases { optsMap, err := parseSerializationOpts(test.inp, test.validKeys, test.validAbbrKeys) gerr := err.ToGoError() if gerr != nil && gerr.Error() != test.errMsg { // match partial error message if !strings.Contains(gerr.Error(), test.errMsg) { t.Fatalf("Test %d: Unexpected result for \"%s\", expected: |%s| got: |%s|\n", i+1, test.inp, test.errMsg, gerr) } } if gerr == nil && test.errMsg != "" { t.Fatalf("Test %d: Unexpected result for \"%s\", expected: |%s| got: |%s|\n", i+1, test.inp, test.errMsg, gerr) } for k, v := range test.parsedOpts { actual, ok := optsMap[strings.ToLower(k)] if !ok { t.Fatalf("Test %d:Unexpected result for \"%s,\" expected %s , found %s for key %s\n", i+1, test.inp, v, actual, k) } } } } minio-client-0.0~20250403/cmd/stat-main.go000066400000000000000000000143321477450377600177740ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "path/filepath" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/pkg/v3/console" ) // stat specific flags. var ( statFlags = []cli.Flag{ cli.StringFlag{ Name: "rewind", Usage: "stat on older version(s)", }, cli.BoolFlag{ Name: "versions", Usage: "stat all versions", }, cli.StringFlag{ Name: "version-id, vid", Usage: "stat a specific object version", }, cli.BoolFlag{ Name: "recursive, r", Usage: "stat all objects recursively", }, cli.BoolFlag{ Name: "verbose, v", Usage: "show extended bucket(s) stat", }, cli.BoolFlag{ Name: "no-list", Usage: "disable all LIST operations for stat", }, } ) // show object metadata var statCmd = cli.Command{ Name: "stat", Usage: "show object metadata", Action: mainStat, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(append(statFlags, encCFlag), globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET ...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Stat all contents of mybucket on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} s3/mybucket/ 2. Stat all contents of all buckets on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} s3 --verbose 3. Stat all contents of mybucket on Amazon S3 cloud storage on Microsoft Windows. {{.Prompt}} {{.HelpName}} s3\mybucket\ 4. Stat files recursively on a local filesystem on Microsoft Windows. {{.Prompt}} {{.HelpName}} --recursive C:\Users\mydocuments\ 5. Stat encrypted files on Amazon S3 cloud storage. In case the encryption key contains non-printable character like tab, pass the base64 encoded string as key. {{.Prompt}} {{.HelpName}} --enc-c "s3/personal-document/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" s3/personal-document/2019-account_report.docx 6. Stat a specific object version. {{.Prompt}} {{.HelpName}} --version-id "CL3sWgdSN2pNntSf6UnZAuh2kcu8E8si" s3/personal-docs/2018-account_report.docx 7. Stat all objects versions recursively created before 1st January 2020. {{.Prompt}} {{.HelpName}} --versions --rewind 2020.01.01T00:00 s3/personal-docs/ `, } // parseAndCheckStatSyntax - parse and validate all the passed arguments func parseAndCheckStatSyntax(ctx context.Context, cliCtx *cli.Context) ([]string, bool, string, time.Time, bool) { if !cliCtx.Args().Present() { showCommandHelpAndExit(cliCtx, 1) // last argument is exit code } args := cliCtx.Args() for _, arg := range args { if strings.TrimSpace(arg) == "" { fatalIf(errInvalidArgument().Trace(args...), "Unable to validate empty argument.") } } recursive := cliCtx.Bool("recursive") versionID := cliCtx.String("version-id") withVersions := cliCtx.Bool("versions") headOnly := cliCtx.Bool("no-list") rewind := parseRewindFlag(cliCtx.String("rewind")) // extract URLs. URLs := cliCtx.Args() if versionID != "" && len(args) > 1 { fatalIf(errInvalidArgument().Trace(args...), "You cannot specify --version-id with multiple arguments.") } if versionID != "" && (recursive || withVersions || !rewind.IsZero()) { fatalIf(errInvalidArgument().Trace(args...), "You cannot specify --version-id with either --rewind, --versions or --recursive.") } if (recursive || withVersions) && headOnly { fatalIf(errInvalidArgument().Trace(args...), "You cannot specify --no-list with either --versions or --recursive.") } var targetUrls []string for _, url := range URLs { _, path := url2Alias(url) if path != "" || !cliCtx.Bool("verbose") { targetUrls = append(targetUrls, url) continue } clnt, err := newClient(url) fatalIf(err.Trace(args...), "Unable to initialize `"+url+"`.") buckets, e := clnt.ListBuckets(ctx) if e != nil || len(buckets) == 0 { targetUrls = append(targetUrls, url) continue } for _, bucket := range buckets { targetUrls = append(targetUrls, filepath.Join(url, bucket.BucketName)) } } return targetUrls, recursive, versionID, rewind, withVersions } // mainStat - is a handler for mc stat command func mainStat(cliCtx *cli.Context) error { ctx, cancelStat := context.WithCancel(globalContext) defer cancelStat() // Additional command specific theme customization. console.SetColor("Name", color.New(color.Bold, color.FgCyan)) console.SetColor("Date", color.New(color.FgWhite)) console.SetColor("Size", color.New(color.FgWhite)) console.SetColor("ETag", color.New(color.FgWhite)) console.SetColor("Metadata", color.New(color.FgWhite)) // theme specific to stat bucket console.SetColor("Key", color.New(color.FgCyan)) console.SetColor("Value", color.New(color.FgYellow)) console.SetColor("Unset", color.New(color.FgRed)) console.SetColor("Set", color.New(color.FgGreen)) console.SetColor("Title", color.New(color.Bold, color.FgBlue)) console.SetColor("Count", color.New(color.FgGreen)) // Parse encryption keys per command. encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) fatalIf(err, "Unable to parse encryption keys.") // check 'stat' cli arguments. args, isRecursive, versionID, rewind, withVersions := parseAndCheckStatSyntax(ctx, cliCtx) // mimic operating system tool behavior. if len(args) == 0 { args = []string{"."} } headOnly := cliCtx.Bool("no-list") for _, targetURL := range args { fatalIf(statURL(ctx, targetURL, versionID, rewind, withVersions, false, isRecursive, headOnly, encKeyDB), "Unable to stat `"+targetURL+"`.") } return nil } minio-client-0.0~20250403/cmd/stat.go000066400000000000000000000457041477450377600170610ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "fmt" "os" "path/filepath" "sort" "strings" "time" "github.com/dustin/go-humanize" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/lifecycle" "github.com/minio/minio-go/v7/pkg/notification" "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/pkg/v3/console" ) // contentMessage container for content message structure. type statMessage struct { Status string `json:"status"` Key string `json:"name"` Date time.Time `json:"lastModified"` Size int64 `json:"size"` ETag string `json:"etag"` Type string `json:"type,omitempty"` Expires *time.Time `json:"expires,omitempty"` Expiration *time.Time `json:"expiration,omitempty"` ExpirationRuleID string `json:"expirationRuleID,omitempty"` ReplicationStatus string `json:"replicationStatus,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` VersionID string `json:"versionID,omitempty"` DeleteMarker bool `json:"deleteMarker,omitempty"` Restore *minio.RestoreInfo `json:"restore,omitempty"` Checksum map[string]string `json:"checksum,omitempty"` } func (stat statMessage) String() (msg string) { var msgBuilder strings.Builder // Format properly for alignment based on maxKey leng stat.Key = fmt.Sprintf("%-10s: %s", "Name", stat.Key) msgBuilder.WriteString(console.Colorize("Name", stat.Key) + "\n") if !stat.Date.IsZero() && !stat.Date.Equal(timeSentinel) { msgBuilder.WriteString(fmt.Sprintf("%-10s: %s ", "Date", stat.Date.Format(printDate)) + "\n") } if stat.Type != "folder" { msgBuilder.WriteString(fmt.Sprintf("%-10s: %-6s ", "Size", humanize.IBytes(uint64(stat.Size))) + "\n") } if stat.ETag != "" { msgBuilder.WriteString(fmt.Sprintf("%-10s: %s ", "ETag", stat.ETag) + "\n") } if stat.VersionID != "" { versionIDField := stat.VersionID if stat.DeleteMarker { versionIDField += " (delete-marker)" } msgBuilder.WriteString(fmt.Sprintf("%-10s: %s ", "VersionID", versionIDField) + "\n") } msgBuilder.WriteString(fmt.Sprintf("%-10s: %s ", "Type", stat.Type) + "\n") if stat.Expires != nil && !stat.Expires.IsZero() && !stat.Expires.Equal(timeSentinel) { msgBuilder.WriteString(fmt.Sprintf("%-10s: %s ", "Expires", stat.Expires.Format(printDate)) + "\n") } if stat.Expiration != nil && !stat.Expiration.IsZero() && !stat.Expiration.Equal(timeSentinel) { msgBuilder.WriteString(fmt.Sprintf("%-10s: %s (lifecycle-rule-id: %s) ", "Expiration", stat.Expiration.Local().Format(printDate), stat.ExpirationRuleID) + "\n") } if len(stat.Checksum) > 0 { cs := strings.TrimSuffix(strings.TrimPrefix(fmt.Sprintf("%v", stat.Checksum), "map["), "]") msgBuilder.WriteString(fmt.Sprintf("%-10s: %v", "Checksum", cs) + "\n") } if stat.Restore != nil { msgBuilder.WriteString(fmt.Sprintf("%-10s:", "Restore") + "\n") if !stat.Restore.ExpiryTime.IsZero() && !stat.Restore.ExpiryTime.Equal(timeSentinel) { msgBuilder.WriteString(fmt.Sprintf(" %-10s: %s", "ExpiryTime", stat.Restore.ExpiryTime.Local().Format(printDate)) + "\n") } msgBuilder.WriteString(fmt.Sprintf(" %-10s: %t", "Ongoing", stat.Restore.OngoingRestore) + "\n") } maxKeyMetadata := 0 maxKeyEncrypted := 0 for k := range stat.Metadata { // Skip encryption headers, we print them later. if !strings.HasPrefix(strings.ToLower(k), serverEncryptionKeyPrefix) { if len(k) > maxKeyMetadata { maxKeyMetadata = len(k) } } else if strings.HasPrefix(strings.ToLower(k), serverEncryptionKeyPrefix) { if len(k) > maxKeyEncrypted { maxKeyEncrypted = len(k) } } } if maxKeyEncrypted > 0 { // Handle various AWS S3 headers, behaviors etc. var found bool if enabled, ok := stat.Metadata["X-Amz-Server-Side-Encryption-Bucket-Key-Enabled"]; ok { if enabled == "true" { msgBuilder.WriteString(fmt.Sprintf("%-10s: SSE-%s\n", "Encryption", "KMS")) } // we still need to treat this as 'true' because X-Amz-Server-Side-Encryption-Bucket-Key-Enabled // can be set to 'false' by the server to indicate there is no SSE enabled on the object // we shouldn't be printing `unknown` in that scenario. found = true } else if keyID, ok := stat.Metadata["X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id"]; ok { msgBuilder.WriteString(fmt.Sprintf("%-10s: SSE-%s (%s)\n", "Encryption", "KMS", keyID)) found = true } else if _, ok := stat.Metadata["X-Amz-Server-Side-Encryption-Customer-Key-Md5"]; ok { msgBuilder.WriteString(fmt.Sprintf("%-10s: SSE-%s\n", "Encryption", "C")) found = true } else if algo, ok := stat.Metadata["X-Amz-Server-Side-Encryption"]; ok && algo == "AES256" { msgBuilder.WriteString(fmt.Sprintf("%-10s: SSE-%s\n", "Encryption", "S3")) found = true } if !found { // encryption headers are present but not something we recognize, check `mc stat --debug` // to obtain more information and understand if we are missing something. msgBuilder.WriteString(fmt.Sprintf("%-10s: SSE-%s\n", "Encryption", "Unknown")) } } if maxKeyMetadata > 0 { msgBuilder.WriteString(fmt.Sprintf("%-10s:", "Metadata") + "\n") for k, v := range stat.Metadata { // Skip encryption headers, we print them later. if !strings.HasPrefix(strings.ToLower(k), serverEncryptionKeyPrefix) { msgBuilder.WriteString(fmt.Sprintf(" %-*.*s: %s ", maxKeyMetadata, maxKeyMetadata, k, v) + "\n") } } } if stat.ReplicationStatus != "" { msgBuilder.WriteString(fmt.Sprintf("%-10s: %s ", "Replication Status", stat.ReplicationStatus)) } msgBuilder.WriteString("\n") return msgBuilder.String() } // JSON jsonified content message. func (stat statMessage) JSON() string { stat.Status = "success" jsonMessageBytes, e := json.MarshalIndent(stat, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // parseStat parses client Content container into statMessage struct. func parseStat(c *ClientContent) statMessage { content := statMessage{} content.Date = c.Time.Local() // guess file type. content.Type = func() string { if c.Type.IsDir() { return "folder" } return "file" }() content.Size = c.Size content.VersionID = c.VersionID content.Key = getKey(c) content.Metadata = c.Metadata content.ETag = strings.TrimPrefix(c.ETag, "\"") content.ETag = strings.TrimSuffix(content.ETag, "\"") if !c.Expires.IsZero() { content.Expires = &c.Expires } if !c.Expiration.IsZero() { content.Expiration = &c.Expiration } content.ExpirationRuleID = c.ExpirationRuleID content.ReplicationStatus = c.ReplicationStatus content.Restore = c.Restore content.Checksum = c.Checksum return content } // Return standardized URL to be used to compare later. func getStandardizedURL(targetURL string) string { return filepath.FromSlash(targetURL) } // statURL - uses combination of GET listing and HEAD to fetch information of one or more objects // HEAD can fail with 400 with an SSE-C encrypted object but we still return information gathered // from GET listing. func statURL(ctx context.Context, targetURL, versionID string, timeRef time.Time, includeOlderVersions, isIncomplete, isRecursive, headOnly bool, encKeyDB map[string][]prefixSSEPair) *probe.Error { clnt, err := newClient(targetURL) if err != nil { return err } targetAlias, _, _ := mustExpandAlias(targetURL) separator := string(clnt.GetURL().Separator) prefixPath := clnt.GetURL().Path hasTrailingSlash := strings.HasSuffix(prefixPath, separator) if !hasTrailingSlash { prefixPath = prefixPath[:strings.LastIndex(prefixPath, separator)+1] } if headOnly || versionID != "" { url := getStandardizedURL(targetURL) _, stat, err := url2Stat(ctx, url2StatOptions{ urlStr: url, versionID: versionID, fileAttr: true, encKeyDB: encKeyDB, timeRef: timeRef, isZip: false, ignoreBucketExistsCheck: false, headOnly: headOnly, }) if err != nil { return err } // Convert any os specific delimiters to "/". contentURL := filepath.ToSlash(stat.URL.Path) // Trim prefix path from the content path. stat.URL.Path = strings.TrimPrefix(contentURL, filepath.ToSlash(prefixPath)) printMsg(parseStat(stat)) return nil } // if stat is on a bucket and non-recursive mode, serve the bucket metadata if !isRecursive && !hasTrailingSlash { bstat, err := clnt.GetBucketInfo(ctx) if err == nil { // Convert any os specific delimiters to "/". contentURL := filepath.ToSlash(bstat.URL.Path) prefixPath = filepath.ToSlash(prefixPath) // Trim prefix path from the content path. contentURL = strings.TrimPrefix(contentURL, prefixPath) bstat.URL.Path = contentURL if bstat.Date.IsZero() || bstat.Date.Equal(timeSentinel) { bstat.Date = time.Now() } var bu madmin.BucketUsageInfo adminClient, _ := newAdminClient(targetURL) if adminClient != nil { // Create a new MinIO Admin Client duinfo, e := adminClient.DataUsageInfo(ctx) if e == nil { bu = duinfo.BucketsUsage[bstat.Key] } } if prefixPath != "/" { bstat.Prefix = true } printMsg(bucketInfoMessage{ Status: "success", BucketInfo: bstat, Usage: bu, }) return nil } } lstOptions := ListOptions{Recursive: isRecursive, Incomplete: isIncomplete, ShowDir: DirNone} switch { case versionID != "": lstOptions.WithOlderVersions = true lstOptions.WithDeleteMarkers = true case !timeRef.IsZero(), includeOlderVersions: lstOptions.WithOlderVersions = includeOlderVersions lstOptions.WithDeleteMarkers = true lstOptions.TimeRef = timeRef } var e error var found int for content := range clnt.List(ctx, lstOptions) { if content.Err != nil { switch content.Err.ToGoError().(type) { // handle this specifically for filesystem related errors. case BrokenSymlink: errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list broken link.") continue case TooManyLevelsSymlink: errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list too many levels link.") continue case PathNotFound: errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") continue case PathInsufficientPermission: errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") continue } errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") e = exitStatus(globalErrorExitStatus) // Set the exit status. continue } found++ if content.StorageClass == s3StorageClassGlacier { continue } url := getStandardizedURL(targetAlias + getKey(content)) standardizedURL := getStandardizedURL(targetURL) if !isRecursive && !strings.HasPrefix(filepath.FromSlash(url), standardizedURL) && !filepath.IsAbs(url) { return errTargetNotFound(targetURL).Trace(url, standardizedURL) } if versionID != "" { if versionID != content.VersionID { continue } } _, stat, err := url2Stat(ctx, url2StatOptions{ urlStr: url, versionID: content.VersionID, fileAttr: true, encKeyDB: encKeyDB, timeRef: timeRef, isZip: false, ignoreBucketExistsCheck: false, }) if err != nil { return err.Trace(url) } // Convert any os specific delimiters to "/". contentURL := filepath.ToSlash(stat.URL.Path) prefixPath = filepath.ToSlash(prefixPath) // Trim prefix path from the content path. contentURL = strings.TrimPrefix(contentURL, prefixPath) stat.URL.Path = contentURL printMsg(parseStat(stat)) } if found <= 0 { return probe.NewError(ObjectMissing{timeRef}) } return probe.NewError(e) } // BucketInfo holds info about a bucket type BucketInfo struct { URL ClientURL `json:"-"` Key string `json:"name"` Date time.Time `json:"lastModified"` Size int64 `json:"size"` Type os.FileMode `json:"-"` Prefix bool `json:"-"` Versioning struct { Status string `json:"status"` MFADelete string `json:"MFADelete"` } `json:"Versioning,omitempty"` Encryption struct { Algorithm string `json:"algorithm,omitempty"` KeyID string `json:"keyId,omitempty"` } `json:"Encryption,omitempty"` Locking struct { Enabled string `json:"enabled"` Mode minio.RetentionMode `json:"mode"` Validity string `json:"validity"` } `json:"ObjectLock,omitempty"` Replication struct { Enabled bool `json:"enabled"` Config replication.Config `json:"config,omitempty"` } `json:"Replication"` Policy struct { Type string `json:"type"` Text string `json:"policy,omitempty"` } `json:"Policy,omitempty"` Location string `json:"location"` Tagging map[string]string `json:"tagging,omitempty"` ILM struct { Config *lifecycle.Configuration `json:"config,omitempty"` } `json:"ilm,omitempty"` Notification struct { Config notification.Configuration `json:"config,omitempty"` } `json:"notification,omitempty"` } // Tags returns stringified tag list. func (i BucketInfo) Tags() string { keys := []string{} for key := range i.Tagging { keys = append(keys, key) } sort.Strings(keys) strs := []string{} for _, key := range keys { strs = append( strs, fmt.Sprintf("%v:%v", console.Colorize("Key", key), console.Colorize("Value", i.Tagging[key])), ) } return strings.Join(strs, ", ") } type bucketInfoMessage struct { Status string `json:"status"` BucketInfo Usage madmin.BucketUsageInfo } func (v bucketInfoMessage) JSON() string { v.Status = "success" v.Key = getKey(&ClientContent{URL: v.URL, Type: v.Type}) var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetIndent("", " ") // Disable escaping special chars to display XML tags correctly enc.SetEscapeHTML(false) fatalIf(probe.NewError(enc.Encode(v)), "Unable to marshal into JSON.") return buf.String() } func countDigits(num uint64) (count uint) { for num > 0 { num /= 10 count++ } return } func (v bucketInfoMessage) String() string { var b strings.Builder keyStr := getKey(&ClientContent{URL: v.URL, Type: v.Type}) keyStr = strings.TrimSuffix(keyStr, slashSeperator) key := fmt.Sprintf("%-10s: %s", "Name", keyStr) b.WriteString(console.Colorize("Title", key) + "\n") if !v.Date.IsZero() && !v.Date.Equal(timeSentinel) { b.WriteString(fmt.Sprintf("%-10s: %s ", "Date", v.Date.Format(printDate)) + "\n") } b.WriteString(fmt.Sprintf("%-10s: %-6s \n", "Size", "N/A")) fType := func() string { if v.Prefix { return "prefix" } if v.Type.IsDir() { return "folder" } return "file" }() b.WriteString(fmt.Sprintf("%-10s: %s \n", "Type", fType)) fmt.Fprintf(&b, "\n") if !v.Prefix { fmt.Fprint(&b, console.Colorize("Title", "Properties:\n")) fmt.Fprint(&b, prettyPrintBucketMetadata(v.BucketInfo)) fmt.Fprintf(&b, "\n") } fmt.Fprint(&b, console.Colorize("Title", "Usage:\n")) fmt.Fprintf(&b, "%16s: %s\n", "Total size", console.Colorize("Count", humanize.IBytes(v.Usage.Size))) fmt.Fprintf(&b, "%16s: %s\n", "Objects count", console.Colorize("Count", humanize.Comma(int64(v.Usage.ObjectsCount)))) fmt.Fprintf(&b, "%16s: %s\n", "Versions count", console.Colorize("Count", humanize.Comma(int64(v.Usage.VersionsCount)))) fmt.Fprintf(&b, "\n") if len(v.Usage.ObjectSizesHistogram) > 0 { fmt.Fprint(&b, console.Colorize("Title", "Object sizes histogram:\n")) var maxDigits uint for _, val := range v.Usage.ObjectSizesHistogram { if d := countDigits(val); d > maxDigits { maxDigits = d } } var sortedTags []string for k := range v.Usage.ObjectSizesHistogram { sortedTags = append(sortedTags, k) } sort.Strings(sortedTags) for _, tagName := range sortedTags { val, ok := v.Usage.ObjectSizesHistogram[tagName] if ok { fmt.Fprintf(&b, " %*d object(s) %s\n", maxDigits, val, tagName) } } } return b.String() } // Pretty print bucket configuration - used by stat and admin bucket info as well func prettyPrintBucketMetadata(info BucketInfo) string { var b strings.Builder placeHolder := "" if info.Encryption.Algorithm != "" { fmt.Fprintf(&b, "%2s%s", placeHolder, "Encryption: ") if info.Encryption.Algorithm == "aws:kms" { fmt.Fprint(&b, console.Colorize("Key", "\n\tKey Type: ")) fmt.Fprint(&b, console.Colorize("Value", "SSE-KMS")) fmt.Fprint(&b, console.Colorize("Key", "\n\tKey ID: ")) fmt.Fprint(&b, console.Colorize("Value", info.Encryption.KeyID)) } else { fmt.Fprint(&b, console.Colorize("Key", "\n\tKey Type: ")) fmt.Fprint(&b, console.Colorize("Value", strings.ToUpper(info.Encryption.Algorithm))) } fmt.Fprintln(&b) } fmt.Fprintf(&b, "%2s%s", placeHolder, "Versioning: ") if info.Versioning.Status == "" { fmt.Fprint(&b, console.Colorize("Unset", "Un-versioned")) } else { fmt.Fprint(&b, console.Colorize("Set", info.Versioning.Status)) } fmt.Fprintln(&b) if info.Locking.Mode != "" { fmt.Fprintf(&b, "%2s%s\n", placeHolder, "LockConfiguration: ") fmt.Fprintf(&b, "%4s%s", placeHolder, "RetentionMode: ") fmt.Fprint(&b, console.Colorize("Value", info.Locking.Mode)) fmt.Fprintln(&b) fmt.Fprintf(&b, "%4s%s", placeHolder, "Retention Until Date: ") fmt.Fprint(&b, console.Colorize("Value", info.Locking.Validity)) fmt.Fprintln(&b) } if len(info.Notification.Config.TopicConfigs) > 0 { fmt.Fprintf(&b, "%2s%s", placeHolder, "Notification: ") fmt.Fprint(&b, console.Colorize("Set", "Set")) fmt.Fprintln(&b) } if info.Replication.Enabled { fmt.Fprintf(&b, "%2s%s", placeHolder, "Replication: ") fmt.Fprint(&b, console.Colorize("Set", "Enabled")) fmt.Fprintln(&b) } fmt.Fprintf(&b, "%2s%s", placeHolder, "Location: ") fmt.Fprint(&b, console.Colorize("Generic", info.Location)) fmt.Fprintln(&b) fmt.Fprintf(&b, "%2s%s", placeHolder, "Anonymous: ") if info.Policy.Type == "none" { fmt.Fprint(&b, console.Colorize("UnSet", "Disabled")) } else { fmt.Fprint(&b, console.Colorize("Set", "Enabled")) } fmt.Fprintln(&b) if info.Tags() != "" { fmt.Fprintf(&b, "%2s%s", placeHolder, "Tagging: ") fmt.Fprint(&b, console.Colorize("Generic", info.Tags())) fmt.Fprintln(&b) } fmt.Fprintf(&b, "%2s%s", placeHolder, "ILM: ") if info.ILM.Config != nil { fmt.Fprint(&b, console.Colorize("Set", "Enabled")) } else { fmt.Fprint(&b, console.Colorize("UnSet", "Disabled")) } fmt.Fprintln(&b) return b.String() } minio-client-0.0~20250403/cmd/stat_test.go000066400000000000000000000054421477450377600201130ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" "reflect" "strings" "testing" "time" ) func TestParseStat(t *testing.T) { localTime := time.Unix(12001, 0).UTC() testCases := []struct { content ClientContent targetAlias string }{ {ClientContent{URL: *newClientURL("https://play.min.io/abc"), Size: 0, Time: localTime, Type: os.ModeDir, ETag: "blahblah", Metadata: map[string]string{"cusom-key": "custom-value"}, Expires: time.Now()}, "play"}, {ClientContent{URL: *newClientURL("https://play.min.io/testbucket"), Size: 500, Time: localTime, Type: os.ModeDir, ETag: "blahblah", Metadata: map[string]string{"cusom-key": "custom-value"}, Expires: time.Unix(0, 0).UTC()}, "play"}, {ClientContent{URL: *newClientURL("https://s3.amazonaws.com/yrdy"), Size: 0, Time: localTime, Type: 0o644, ETag: "abcdefasaas", Metadata: map[string]string{}}, "s3"}, {ClientContent{URL: *newClientURL("https://play.min.io/yrdy"), Size: 10000, Time: localTime, Type: 0o644, ETag: "blahblah", Metadata: map[string]string{"cusom-key": "custom-value"}}, "play"}, } for _, testCase := range testCases { testCase := testCase t.Run("", func(t *testing.T) { statMsg := parseStat(&testCase.content) if !reflect.DeepEqual(testCase.content.Metadata, statMsg.Metadata) { t.Errorf("Expecting %s, got %s", testCase.content.Metadata, statMsg.Metadata) } if testCase.content.Size != statMsg.Size { t.Errorf("Expecting %d, got %d", testCase.content.Size, statMsg.Size) } if statMsg.Expires != nil { if testCase.content.Expires != *statMsg.Expires { t.Errorf("Expecting %s, got %s", testCase.content.Expires, statMsg.Expires) } } if testCase.content.Type.IsRegular() { if statMsg.Type != "file" { t.Errorf("Expecting file, got %s", statMsg.Type) } } else { if statMsg.Type != "folder" { t.Errorf("Expecting folder, got %s", statMsg.Type) } } etag := strings.TrimPrefix(testCase.content.ETag, "\"") etag = strings.TrimSuffix(etag, "\"") if etag != statMsg.ETag { t.Errorf("Expecting %s, got %s", etag, statMsg.ETag) } }) } } minio-client-0.0~20250403/cmd/status.go000066400000000000000000000140261477450377600174220ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "io" "sync/atomic" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) // Status implements a interface that can be used in quit mode or with progressbar. type Status interface { Println(data ...interface{}) AddCounts(int64) SetCounts(int64) GetCounts() int64 Add(int64) Status Get() int64 Start() Finish() PrintMsg(msg message) Update() Total() int64 SetTotal(int64) Status SetCaption(string) Read(p []byte) (n int, err error) errorIf(err *probe.Error, msg string) fatalIf(err *probe.Error, msg string) } // NewQuietStatus returns a quiet status object func NewQuietStatus(hook io.Reader) Status { return &QuietStatus{ accounter: newAccounter(0), hook: hook, } } // QuietStatus will only show the progress and summary type QuietStatus struct { // Keep this as first element of struct because it guarantees 64bit // alignment on 32 bit machines. atomic.* functions crash if operand is not // aligned at 64bit. See https://github.com/golang/go/issues/599 counts int64 *accounter hook io.Reader } // Read implements the io.Reader interface func (qs *QuietStatus) Read(p []byte) (n int, err error) { qs.hook.Read(p) return qs.accounter.Read(p) } // SetCounts sets number of files uploaded func (qs *QuietStatus) SetCounts(v int64) { atomic.StoreInt64(&qs.counts, v) } // GetCounts returns number of files uploaded func (qs *QuietStatus) GetCounts() int64 { return atomic.LoadInt64(&qs.counts) } // AddCounts adds 'v' number of files uploaded. func (qs *QuietStatus) AddCounts(v int64) { atomic.AddInt64(&qs.counts, v) } // SetTotal sets the total of the progressbar, ignored for quietstatus func (qs *QuietStatus) SetTotal(v int64) Status { qs.accounter.SetTotal(v) return qs } // SetCaption sets the caption of the progressbar, ignored for quietstatus func (qs *QuietStatus) SetCaption(_ string) { } // Get returns the current number of bytes func (qs *QuietStatus) Get() int64 { return qs.accounter.Get() } // Total returns the total number of bytes func (qs *QuietStatus) Total() int64 { return qs.accounter.Get() } // Add bytes to current number of bytes func (qs *QuietStatus) Add(v int64) Status { qs.accounter.Add(v) return qs } // Println prints line, ignored for quietstatus func (qs *QuietStatus) Println(_ ...interface{}) { } // PrintMsg prints message func (qs *QuietStatus) PrintMsg(msg message) { printMsg(msg) } // Start is ignored for quietstatus func (qs *QuietStatus) Start() { } // Finish displays the accounting summary func (qs *QuietStatus) Finish() { printMsg(qs.Stat()) } // Update is ignored for quietstatus func (qs *QuietStatus) Update() { } func (qs *QuietStatus) errorIf(err *probe.Error, msg string) { errorIf(err, "%s", msg) } func (qs *QuietStatus) fatalIf(err *probe.Error, msg string) { fatalIf(err, "%s", msg) } // NewProgressStatus returns a progress status object func NewProgressStatus(hook io.Reader) Status { return &ProgressStatus{ progressBar: newProgressBar(0), hook: hook, } } // ProgressStatus shows a progressbar type ProgressStatus struct { // Keep this as first element of struct because it guarantees 64bit // alignment on 32 bit machines. atomic.* functions crash if operand is not // aligned at 64bit. See https://github.com/golang/go/issues/599 counts int64 *progressBar hook io.Reader } // Read implements the io.Reader interface func (ps *ProgressStatus) Read(p []byte) (n int, err error) { ps.hook.Read(p) return ps.progressBar.Read(p) } // SetCaption sets the caption of the progressbar func (ps *ProgressStatus) SetCaption(s string) { ps.progressBar.SetCaption(s) } // SetCounts sets number of files uploaded func (ps *ProgressStatus) SetCounts(v int64) { atomic.StoreInt64(&ps.counts, v) } // GetCounts returns number of files uploaded func (ps *ProgressStatus) GetCounts() int64 { return atomic.LoadInt64(&ps.counts) } // AddCounts adds 'v' number of files uploaded. func (ps *ProgressStatus) AddCounts(v int64) { atomic.AddInt64(&ps.counts, v) } // Get returns the current number of bytes func (ps *ProgressStatus) Get() int64 { return ps.progressBar.Get() } // Total returns the total number of bytes func (ps *ProgressStatus) Total() int64 { return ps.progressBar.Get() } // SetTotal sets the total of the progressbar func (ps *ProgressStatus) SetTotal(v int64) Status { ps.progressBar.SetTotal(v) return ps } // Add bytes to current number of bytes func (ps *ProgressStatus) Add(v int64) Status { ps.Add64(v) return ps } // Println prints line, ignored for quietstatus func (ps *ProgressStatus) Println(data ...interface{}) { console.Eraseline() console.Println(data...) } // PrintMsg prints message func (ps *ProgressStatus) PrintMsg(_ message) { } // Start is ignored for quietstatus func (ps *ProgressStatus) Start() { ps.progressBar.Start() } // Finish displays the accounting summary func (ps *ProgressStatus) Finish() { ps.progressBar.Finish() } // Update is ignored for quietstatus func (ps *ProgressStatus) Update() { ps.progressBar.Update() } func (ps *ProgressStatus) errorIf(err *probe.Error, msg string) { // remove progressbar console.Eraseline() errorIf(err, "%s", msg) ps.progressBar.Update() } func (ps *ProgressStatus) fatalIf(err *probe.Error, msg string) { // remove progressbar console.Eraseline() fatalIf(err, "%s", msg) ps.progressBar.Update() } minio-client-0.0~20250403/cmd/subnet-file-uploader.go000066400000000000000000000111621477450377600221230ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/pem" "io" "mime/multipart" "net/http" "net/url" "os" "path/filepath" "strings" "github.com/klauspost/compress/zstd" "github.com/minio/madmin-go/v3/estream" ) // SubnetFileUploader - struct to upload files to SUBNET type SubnetFileUploader struct { alias string // used for saving api-key and license from response filename string // filename passed in the SUBNET request FilePath string // file to upload ReqURL string // SUBNET upload URL Params url.Values // query params to be sent in the request Headers SubnetHeaders // headers to be sent in the request AutoCompress bool // whether to compress (zst) the file before uploading DeleteAfterUpload bool // whether to delete the file after successful upload AutoEncrypt bool // Encrypt content. PubKey []byte // Custom public encryption key. } // UploadFileToSubnet - uploads the file to SUBNET func (i *SubnetFileUploader) UploadFileToSubnet() (string, error) { req, e := i.subnetUploadReq() if e != nil { return "", e } resp, e := subnetReqDo(req, i.Headers) if e != nil { return "", e } if i.DeleteAfterUpload { os.Remove(i.FilePath) } // ensure that both api-key and license from // SUBNET response are saved in the config if len(i.alias) > 0 { extractAndSaveSubnetCreds(i.alias, resp) } return resp, nil } func (i *SubnetFileUploader) updateParams() { if i.Params == nil { i.Params = url.Values{} } if i.filename == "" { i.filename = filepath.Base(i.FilePath) } i.AutoCompress = i.AutoCompress && !strings.HasSuffix(strings.ToLower(i.FilePath), ".zst") if i.AutoCompress { i.filename += ".zst" i.Params.Add("auto-compression", "zstd") } if i.AutoEncrypt || len(i.PubKey) > 0 { i.Params.Add("encrypted", "true") i.filename += ".enc" } i.Params.Add("filename", i.filename) i.ReqURL += "?" + i.Params.Encode() } func (i *SubnetFileUploader) subnetUploadReq() (*http.Request, error) { i.updateParams() r, w := io.Pipe() mwriter := multipart.NewWriter(w) contentType := mwriter.FormDataContentType() go func() { var ( part io.Writer e error ) var errfn func(string) error defer func() { mwriter.Close() w.CloseWithError(e) if e != nil && errfn != nil { errfn(e.Error()) } }() file, e := os.Open(i.FilePath) if e != nil { return } defer file.Close() part, e = mwriter.CreateFormFile("file", i.filename) if e != nil { return } if i.AutoEncrypt || len(i.PubKey) > 0 { sw := estream.NewWriter(part) defer sw.Close() errfn = sw.AddError key := i.PubKey if key == nil { key, e = base64.StdEncoding.DecodeString(defaultPublicKey) if e != nil { return } } pk, e := bytesToPublicKey(key) if e != nil { sw.AddError(e.Error()) return } e = sw.AddKeyEncrypted(pk) if e != nil { sw.AddError(e.Error()) return } wc, e := sw.AddEncryptedStream(strings.TrimSuffix(i.filename, ".enc"), nil) if e != nil { sw.AddError(e.Error()) return } defer wc.Close() part = wc } if i.AutoCompress { z, _ := zstd.NewWriter(part, zstd.WithEncoderConcurrency(2)) sz, err := file.Stat() if err == nil { // Set file size if we can. z.ResetContentSize(part, sz.Size()) } defer z.Close() _, e = z.ReadFrom(file) } else { _, e = io.Copy(part, file) } }() req, e := http.NewRequest(http.MethodPost, i.ReqURL, r) if e != nil { return nil, e } req.Header.Add("Content-Type", contentType) return req, nil } func bytesToPublicKey(pub []byte) (*rsa.PublicKey, error) { block, _ := pem.Decode(pub) if block != nil { pub = block.Bytes } key, err := x509.ParsePKCS1PublicKey(pub) if err != nil { return nil, err } return key, nil } minio-client-0.0~20250403/cmd/subnet-utils.go000066400000000000000000000476431477450377600205500ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "bytes" "encoding/base64" "encoding/json" "errors" "fmt" "io" "math" "net/http" "net/http/httputil" "net/url" "os" "strings" "time" "github.com/google/uuid" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/licverifier" "github.com/minio/pkg/v3/subnet" "github.com/tidwall/gjson" "golang.org/x/term" ) const ( subnetRespBodyLimit = 1 << 20 // 1 MiB minioSubscriptionURL = "https://min.io/subscription" subnetPublicKeyPath = "/downloads/license-pubkey.pem" minioDeploymentIDHeader = "x-minio-deployment-id" ) var subnetCommonFlags = append(supportGlobalFlags, cli.StringFlag{ Name: "api-key", Usage: "API Key of the account on SUBNET", EnvVar: "_MC_SUBNET_API_KEY", }) // SubnetBaseURL - returns the base URL of SUBNET func SubnetBaseURL() string { return subnet.BaseURL(GlobalDevMode) } func subnetIssueURL(issueNum int) string { return fmt.Sprintf("%s/issues/%d", SubnetBaseURL(), issueNum) } // SubnetUploadURL - returns the upload URL for the given upload type func SubnetUploadURL(uploadType string) string { return fmt.Sprintf("%s/api/%s/upload", SubnetBaseURL(), uploadType) } // SubnetRegisterURL - returns the cluster registration URL func SubnetRegisterURL() string { return SubnetBaseURL() + "/api/cluster/register" } func subnetUnregisterURL(depID string) string { return SubnetBaseURL() + "/api/cluster/unregister?deploymentId=" + depID } func subnetLicenseRenewURL() string { return SubnetBaseURL() + "/api/cluster/renew-license" } func subnetOfflineRegisterURL(regToken string) string { return SubnetBaseURL() + "/cluster/register?token=" + regToken } func subnetLoginURL() string { return SubnetBaseURL() + "/api/auth/login" } func subnetAPIKeyURL() string { return SubnetBaseURL() + "/api/auth/api-key" } func subnetMFAURL() string { return SubnetBaseURL() + "/api/auth/mfa-login" } func checkURLReachable(url string) *probe.Error { _, e := subnetHeadReq(url, nil) if e != nil { return probe.NewError(e).Trace(url) } return nil } func subnetURLWithAuth(reqURL, apiKey string) (string, map[string]string, error) { if len(apiKey) == 0 { // API key not available in minio/mc config. // Ask the user to log in to get auth token token, e := subnetLogin() if e != nil { return "", nil, e } apiKey, e = getSubnetAPIKeyUsingAuthToken(token) if e != nil { return "", nil, e } } return reqURL, SubnetAPIKeyAuthHeaders(apiKey), nil } // SubnetHeaders - type for SUBNET request headers type SubnetHeaders map[string]string func (h SubnetHeaders) addDeploymentIDHeader(alias string) { h[minioDeploymentIDHeader] = getAdminInfo(alias).DeploymentID } func subnetTokenAuthHeaders(authToken string) map[string]string { return map[string]string{"Authorization": "Bearer " + authToken} } // SubnetLicenseAuthHeaders - returns the headers for SUBNET license authentication func SubnetLicenseAuthHeaders(lic string) map[string]string { return map[string]string{"x-subnet-license": lic} } // SubnetAPIKeyAuthHeaders - returns the headers for SUBNET API key authentication func SubnetAPIKeyAuthHeaders(apiKey string) SubnetHeaders { return map[string]string{"x-subnet-api-key": apiKey} } func getSubnetClient() *http.Client { client := httpClient(0) if GlobalSubnetProxyURL != nil { client.Transport.(*http.Transport).Proxy = http.ProxyURL(GlobalSubnetProxyURL) } return client } func subnetHTTPDo(req *http.Request) (resp *http.Response, err error) { resp, err = getSubnetClient().Do(req) if err == nil && globalDebug { dumpHTTPReq(req, resp) } return } // dumpHTTP - dump HTTP request and response. func dumpHTTPReq(req *http.Request, resp *http.Response) error { // Starts http dump. _, err := fmt.Fprintln(os.Stderr, "---------START-HTTP---------") if err != nil { return err } hdrs := req.Header for _, hdr := range []string{"Authorization", "x-subnet-license", "x-subnet-api-key"} { if val := hdrs.Get(hdr); val != "" { req.Header.Set(hdr, strings.Repeat("*", len(val))) } } query := req.URL.Query() for _, q := range []string{"api-key", "api_key"} { if val := query.Get(q); val != "" { query.Add(q, strings.Repeat("*", len(val))) } } req.URL.RawQuery = query.Encode() // Only display request header. reqTrace, err := httputil.DumpRequestOut(req, false) if err != nil { return err } // Write request to trace output. _, err = fmt.Fprint(os.Stderr, string(reqTrace)) if err != nil { return err } respTrace, err := httputil.DumpResponse(resp, true) if err != nil { return err } // Write response to trace output. _, err = fmt.Fprint(os.Stderr, strings.TrimSuffix(string(respTrace), "\r\n")) if err != nil { return err } // Ends the http dump. _, err = fmt.Fprintln(os.Stderr, "---------END-HTTP---------") return err } func subnetReqDo(r *http.Request, headers map[string]string) (string, error) { for k, v := range headers { r.Header.Add(k, v) } ct := r.Header.Get("Content-Type") if len(ct) == 0 { r.Header.Add("Content-Type", "application/json") } resp, e := subnetHTTPDo(r) if e != nil { return "", e } defer resp.Body.Close() respBytes, e := io.ReadAll(io.LimitReader(resp.Body, subnetRespBodyLimit)) if e != nil { return "", e } respStr := string(respBytes) if resp.StatusCode == http.StatusOK { return respStr, nil } return respStr, fmt.Errorf("Request failed with code %d with error: %s", resp.StatusCode, respStr) } func subnetHeadReq(reqURL string, headers map[string]string) (string, error) { r, e := http.NewRequest(http.MethodHead, reqURL, nil) if e != nil { return "", e } return subnetReqDo(r, headers) } func subnetGetReq(reqURL string, headers map[string]string) (string, error) { r, e := http.NewRequest(http.MethodGet, reqURL, nil) if e != nil { return "", e } return subnetReqDo(r, headers) } // SubnetPostReq - makes a POST request to SUBNET func SubnetPostReq(reqURL string, payload interface{}, headers map[string]string) (string, error) { body, e := json.Marshal(payload) if e != nil { return "", e } r, e := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body)) if e != nil { return "", e } return subnetReqDo(r, headers) } func getMinIOSubSysConfig(client *madmin.AdminClient, subSys string) ([]madmin.SubsysConfig, error) { buf, e := client.GetConfigKV(globalContext, subSys) if e != nil { return nil, e } return madmin.ParseServerConfigOutput(string(buf)) } func getMinIOSubnetConfig(alias string) []madmin.SubsysConfig { if globalSubnetConfig != nil { return globalSubnetConfig } client, err := newAdminClient(alias) fatalIf(err, "Unable to initialize admin connection.") var e error globalSubnetConfig, e = getMinIOSubSysConfig(client, madmin.SubnetSubSys) if e != nil && e.Error() != "unknown sub-system subnet" { fatal(probe.NewError(e), "Unable to get server config for subnet") } return globalSubnetConfig } func getKeyFromSubnetConfig(alias, key string) (string, bool) { scfg := getMinIOSubnetConfig(alias) // This function only works for fetch config from single target sub-systems // in the server config and is enough for now. if len(scfg) == 0 { return "", false } return scfg[0].Lookup(key) } func getSubnetAPIKeyFromConfig(alias string) string { // get the subnet api_key config from MinIO if available apiKey, supported := getKeyFromSubnetConfig(alias, "api_key") if supported { return apiKey } // otherwise get it from mc config return mcConfig().Aliases[alias].APIKey } func setGlobalSubnetProxyFromConfig(alias string) error { if GlobalSubnetProxyURL != nil { // proxy already set return nil } var ( proxy string supported bool ) if env, ok := os.LookupEnv("_MC_SUBNET_PROXY_URL"); ok { proxy = env supported = env != "" } else { proxy, supported = getKeyFromSubnetConfig(alias, "proxy") } // get the subnet proxy config from MinIO if available if supported && len(proxy) > 0 { proxyURL, e := url.Parse(proxy) if e != nil { return e } GlobalSubnetProxyURL = proxyURL } return nil } func getSubnetLicenseFromConfig(alias string) string { // get the subnet license config from MinIO if available lic, supported := getKeyFromSubnetConfig(alias, "license") if supported { return lic } // otherwise get it from mc config return mcConfig().Aliases[alias].License } func mcConfig() *configV10 { loadMcConfig = loadMcConfigFactory() config, err := loadMcConfig() fatalIf(err.Trace(mustGetMcConfigPath()), "Unable to access configuration file.") return config } func minioConfigSupportsSubSys(client *madmin.AdminClient, subSys string) bool { help, e := client.HelpConfigKV(globalContext, "", "", false) fatalIf(probe.NewError(e), "Unable to get minio config keys") for _, h := range help.KeysHelp { if h.Key == subSys { return true } } return false } func setSubnetAPIKeyInMcConfig(alias, apiKey string) { aliasCfg := mcConfig().Aliases[alias] if len(apiKey) > 0 { aliasCfg.APIKey = apiKey } setAlias(alias, aliasCfg) } func setSubnetLicenseInMcConfig(alias, lic string) { aliasCfg := mcConfig().Aliases[alias] if len(lic) > 0 { aliasCfg.License = lic } setAlias(alias, aliasCfg) } func setSubnetConfig(alias, subKey, cfgVal string) { client, err := newAdminClient(alias) fatalIf(err, "Unable to initialize admin connection.") cfgKey := "subnet " + subKey _, e := client.SetConfigKV(globalContext, cfgKey+"="+cfgVal) fatalIf(probe.NewError(e), "Unable to set "+cfgKey+" config on MinIO") } func setSubnetAPIKey(alias, apiKey string) { if len(apiKey) == 0 { fatal(errDummy().Trace(), "API Key must not be empty.") } _, apiKeySupported := getKeyFromSubnetConfig(alias, "api_key") if !apiKeySupported { setSubnetAPIKeyInMcConfig(alias, apiKey) return } setSubnetConfig(alias, "api_key", apiKey) } func setSubnetLicense(alias, lic string) { if len(lic) == 0 { fatal(errDummy().Trace(), "License must not be empty.") } _, licSupported := getKeyFromSubnetConfig(alias, "license") if !licSupported { setSubnetLicenseInMcConfig(alias, lic) return } setSubnetConfig(alias, "license", lic) } // GetClusterRegInfo - returns the cluster registration info func GetClusterRegInfo(admInfo madmin.InfoMessage, clusterName string) ClusterRegistrationInfo { noOfPools := 1 noOfDrives := 0 for _, srvr := range admInfo.Servers { for _, poolNumber := range srvr.PoolNumbers { if poolNumber > noOfPools { noOfPools = poolNumber } } if len(srvr.PoolNumbers) == 0 { if srvr.PoolNumber != math.MaxInt && srvr.PoolNumber > noOfPools { noOfPools = srvr.PoolNumber } } noOfDrives += len(srvr.Disks) } totalSpace, usedSpace := getDriveSpaceInfo(admInfo) return ClusterRegistrationInfo{ DeploymentID: admInfo.DeploymentID, ClusterName: clusterName, UsedCapacity: admInfo.Usage.Size, Info: ClusterInfo{ MinioVersion: admInfo.Servers[0].Version, NoOfServerPools: noOfPools, NoOfServers: len(admInfo.Servers), NoOfDrives: noOfDrives, TotalDriveSpace: totalSpace, UsedDriveSpace: usedSpace, NoOfBuckets: admInfo.Buckets.Count, NoOfObjects: admInfo.Objects.Count, }, } } func getDriveSpaceInfo(admInfo madmin.InfoMessage) (uint64, uint64) { total := uint64(0) used := uint64(0) for _, srvr := range admInfo.Servers { for _, d := range srvr.Disks { total += d.TotalSpace used += d.UsedSpace } } return total, used } func generateRegToken(clusterRegInfo ClusterRegistrationInfo) (string, error) { token, e := json.Marshal(clusterRegInfo) if e != nil { return "", e } return base64.StdEncoding.EncodeToString(token), nil } func subnetLogin() (string, error) { reader := bufio.NewReader(os.Stdin) fmt.Print("SUBNET username: ") username, _ := reader.ReadString('\n') username = strings.TrimSpace(username) if len(username) == 0 { return "", errors.New("Username cannot be empty. If you don't have one, please create one from here: " + minioSubscriptionURL) } fmt.Print("Password: ") bytepw, _ := term.ReadPassword(int(os.Stdin.Fd())) fmt.Println() loginReq := map[string]string{ "username": username, "password": string(bytepw), } respStr, e := SubnetPostReq(subnetLoginURL(), loginReq, nil) if e != nil { return "", e } mfaRequired := gjson.Get(respStr, "mfa_required").Bool() if mfaRequired { mfaToken := gjson.Get(respStr, "mfa_token").String() fmt.Print("OTP received in email: ") byteotp, _ := term.ReadPassword(int(os.Stdin.Fd())) fmt.Println() mfaLoginReq := SubnetMFAReq{Username: username, OTP: string(byteotp), Token: mfaToken} respStr, e = SubnetPostReq(subnetMFAURL(), mfaLoginReq, nil) if e != nil { return "", e } } token := gjson.Get(respStr, "token_info.access_token") if token.Exists() { return token.String(), nil } return "", fmt.Errorf("access token not found in response") } // getSubnetCreds - returns the API key and license. // If only one of them is available, and if `--airgap` is not // passed, it will attempt to fetch the other from SUBNET // and save to config func getSubnetCreds(alias string) (string, string, error) { apiKey := getSubnetAPIKeyFromConfig(alias) lic := getSubnetLicenseFromConfig(alias) if (len(apiKey) > 0 && len(lic) > 0) || (len(apiKey) == 0 && len(lic) == 0) || globalAirgapped { return apiKey, lic, nil } var e error // Not airgapped, and only one of api-key or license is available // Try to fetch and save the other. if len(apiKey) > 0 { lic, e = getSubnetLicenseUsingAPIKey(alias, apiKey) } else { apiKey, e = getSubnetAPIKeyUsingLicense(lic) if e == nil { setSubnetAPIKey(alias, apiKey) } } if e != nil { return "", "", e } return apiKey, lic, nil } // getSubnetAPIKey - returns the SUBNET API key. // Returns error if the cluster is not registered with SUBNET. func getSubnetAPIKey(alias string) (string, error) { apiKey, lic, e := getSubnetCreds(alias) if e != nil { return "", e } if len(apiKey) == 0 && len(lic) == 0 { e = fmt.Errorf("Please register the cluster first by running 'mc license register %s'", alias) return "", e } return apiKey, nil } func getSubnetAPIKeyUsingLicense(lic string) (string, error) { return getSubnetAPIKeyUsingAuthHeaders(SubnetLicenseAuthHeaders(lic)) } func getSubnetAPIKeyUsingAuthToken(authToken string) (string, error) { return getSubnetAPIKeyUsingAuthHeaders(subnetTokenAuthHeaders(authToken)) } func getSubnetAPIKeyUsingAuthHeaders(authHeaders map[string]string) (string, error) { resp, e := subnetGetReq(subnetAPIKeyURL(), authHeaders) if e != nil { return "", e } return extractSubnetCred("api_key", gjson.Parse(resp)) } func getSubnetLicenseUsingAPIKey(alias, apiKey string) (string, error) { regInfo := GetClusterRegInfo(getAdminInfo(alias), alias) _, lic, e := registerClusterOnSubnet(regInfo, alias, apiKey) return lic, e } // registerClusterOnSubnet - Registers the given cluster on SUBNET using given API key for auth // If the API key is empty, user will be asked to log in using SUBNET credentials. func registerClusterOnSubnet(clusterRegInfo ClusterRegistrationInfo, alias, apiKey string) (string, string, error) { regURL, headers, e := subnetURLWithAuth(SubnetRegisterURL(), apiKey) if e != nil { return "", "", e } regToken, e := generateRegToken(clusterRegInfo) if e != nil { return "", "", e } reqPayload := ClusterRegistrationReq{Token: regToken} resp, e := SubnetPostReq(regURL, reqPayload, headers) if e != nil { return "", "", e } return extractAndSaveSubnetCreds(alias, resp) } func removeSubnetAuthConfig(alias string) { setSubnetConfig(alias, "api_key", "") setSubnetConfig(alias, "license", "") } // unregisterClusterFromSubnet - Unregisters the given cluster from SUBNET using given API key for auth func unregisterClusterFromSubnet(depID, apiKey string) error { regURL, headers, e := subnetURLWithAuth(subnetUnregisterURL(depID), apiKey) if e != nil { return e } _, e = SubnetPostReq(regURL, nil, headers) return e } // validateAndSaveLic - validates the given license in minio config // If the license contains api key and the saveApiKey arg is true, // api key is also saved in the minio config func validateAndSaveLic(lic, alias string, saveAPIKey bool) string { li, e := parseLicense(lic) fatalIf(probe.NewError(e), "Error parsing license") if li.ExpiresAt.Before(time.Now()) { fatalIf(errDummy().Trace(), fmt.Sprintf("License has expired on %s", li.ExpiresAt)) } if len(li.DeploymentID) > 0 && li.DeploymentID != uuid.Nil.String() && li.DeploymentID != getAdminInfo(alias).DeploymentID { fatalIf(errDummy().Trace(), fmt.Sprintf("License is invalid for the deployment %s", alias)) } setSubnetLicense(alias, lic) if len(li.APIKey) > 0 && saveAPIKey { setSubnetAPIKey(alias, li.APIKey) } return li.APIKey } // extractAndSaveSubnetCreds - extract license from response and set it in minio config func extractAndSaveSubnetCreds(alias, resp string) (string, string, error) { parsedResp := gjson.Parse(resp) lic, e := extractSubnetCred("license_v2", parsedResp) if e != nil { return "", "", e } if len(lic) > 0 { apiKey := validateAndSaveLic(lic, alias, true) if len(apiKey) > 0 { return apiKey, lic, nil } } apiKey, e := extractSubnetCred("api_key", parsedResp) if e != nil { return "", "", e } if len(apiKey) > 0 { setSubnetAPIKey(alias, apiKey) } return apiKey, lic, nil } func extractSubnetCred(key string, resp gjson.Result) (string, error) { result := resp.Get(key) if result.Index == 0 { return "", fmt.Errorf("Couldn't extract %s from SUBNET response: %s", key, resp) } return result.String(), nil } // parseLicense parses the license with the bundle public key and return it's information func parseLicense(license string) (*licverifier.LicenseInfo, error) { client := getSubnetClient() lv := subnet.LicenseValidator{ Client: *client, ExpiryGracePeriod: 0, } lv.Init(GlobalDevMode) return lv.ParseLicense(license) } func prepareSubnetUploadURL(uploadURL, alias, apiKey string) (string, map[string]string) { var e error if len(apiKey) == 0 { // api key not passed as flag. check if it's available in the config apiKey, e = getSubnetAPIKey(alias) fatalIf(probe.NewError(e), "Unable to retrieve SUBNET API key") } reqURL, headers, e := subnetURLWithAuth(uploadURL, apiKey) fatalIf(probe.NewError(e).Trace(uploadURL), "Unable to fetch SUBNET authentication") return reqURL, headers } func getAPIKeyFlag(ctx *cli.Context) (string, error) { apiKey := ctx.String("api-key") if len(apiKey) == 0 { return "", nil } _, e := uuid.Parse(apiKey) if e != nil { return "", e } return apiKey, nil } func initSubnetConnectivity(ctx *cli.Context, aliasedURL string, failOnConnErr bool) (string, string) { if ctx.Bool("airgap") && len(ctx.String("api-key")) > 0 { fatal(errDummy().Trace(), "--api-key is not applicable in airgap mode") } alias, _ := url2Alias(aliasedURL) apiKey, e := getAPIKeyFlag(ctx) fatalIf(probe.NewError(e), "Error in reading --api-key flag:") // if `--airgap` is provided no need to test SUBNET connectivity. if !globalAirgapped { e = setGlobalSubnetProxyFromConfig(alias) fatalIf(probe.NewError(e), "Error in setting SUBNET proxy:") sbu := SubnetBaseURL() err := checkURLReachable(sbu) if err != nil && failOnConnErr { fatal(err.Trace(aliasedURL), "Unable to reach %s, please use --airgap if there is no connectivity to SUBNET", sbu) } } return alias, apiKey } minio-client-0.0~20250403/cmd/subnet-utils_test.go000066400000000000000000000020501477450377600215660ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "net/url" "testing" ) func TestSubnetBaseURL(t *testing.T) { sbu := SubnetBaseURL() u, err := url.ParseRequestURI(sbu) if err != nil { t.Fatal(err) } if u.Scheme != "https" { t.Fatalf("Expected TestSubnetBaseURL() to return an https url, received %s", u.Scheme) } } minio-client-0.0~20250403/cmd/suite_test.go000066400000000000000000002070461477450377600202750ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "bytes" "crypto/md5" "crypto/tls" "encoding/json" "fmt" "io" "log" "math/rand" "net/http" "os" "os/exec" "reflect" "runtime" "runtime/debug" "strconv" "strings" "testing" "time" "github.com/google/uuid" "github.com/minio/mc/pkg/disk" ) // RUN: go test -v ./... -run Test_FullSuite func Test_FullSuite(t *testing.T) { if os.Getenv("MC_TEST_RUN_FULL_SUITE") != "true" { return } defer func() { r := recover() if r != nil { log.Println(r, string(debug.Stack())) } postRunCleanup(t) }() preflightCheck(t) // initializeTestSuite builds the mc client and creates local files which are used for testing initializeTestSuite(t) // Tests within this function depend on one another testsThatDependOnOneAnother(t) // Alias tests AddALIASWithError(t) // Basic admin user tests AdminUserFunctionalTest(t) // Share upload/download ShareURLUploadTest(t) ShareURLDownloadTest(t) // TODO .. for some reason the connection is randomly // reset when running curl. // ShareURLUploadErrorTests(t) // Bucket Error Tests CreateBucketUsingInvalidSymbols(t) RemoveBucketWithNameTooLong(t) RemoveBucketThatDoesNotExist(t) // MC_TEST_ENABLE_HTTPS=true // needs to be set in order to run these tests if protocol == "https://" { PutObjectWithSSECHexKey(t) GetObjectWithSSEC(t) PutObjectWithSSEC(t) PutObjectWithSSECPartialPrefixMatch(t) PutObjectWithSSECMultipart(t) PutObjectWithSSECInvalidKeys(t) GetObjectWithSSEC(t) GetObjectWithSSECWithoutKey(t) CatObjectWithSSEC(t) CatObjectWithSSECWithoutKey(t) CopyObjectWithSSECToNewBucketWithNewKey(t) MirrorTempDirectoryUsingSSEC(t) RemoveObjectWithSSEC(t) } else { PutObjectErrorWithSSECOverHTTP(t) } // MC_TEST_KMS_KEY=[KEY_NAME] // needs to be set in order to run these tests if sseKMSKeyName != "" { VerifyKMSKey(t) PutObjectWithSSEKMS(t) PutObjectWithSSEKMSPartialPrefixMatch(t) PutObjectWithSSEKMSMultipart(t) PutObjectWithSSEKMSInvalidKeys(t) GetObjectWithSSEKMS(t) CatObjectWithSSEKMS(t) CopyObjectWithSSEKMSToNewBucket(t) MirrorTempDirectoryUsingSSEKMS(t) RemoveObjectWithSSEKMS(t) // Error tests CopyObjectWithSSEKMSWithOverLappingKeys(t) } // MC_TEST_ENABLE_SSE_S3=true // needs to be set to in order to run these tests. if sseS3Enabled { PutObjectWithSSES3(t) PutObjectWithSSES3PartialPrefixMatch(t) PutObjectWithSSES3Multipart(t) GetObjectWithSSES3(t) CatObjectWithSSES3(t) CopyObjectWithSSES3ToNewBucket(t) MirrorTempDirectoryUsingSSES3(t) } if protocol == "https://" && sseKMSKeyName != "" { CopyObjectWithSSEKMSToNewBucketWithSSEC(t) } // (DEPRECATED CLI PARAMETERS) if includeDeprecatedMethods { fmt.Println("No deprecated methods implemented") } } func testsThatDependOnOneAnother(t *testing.T) { CreateFileBundle() // uploadAllFiles uploads all files in FileMap to MainTestBucket uploadAllFiles(t) // LSObjects saves the output of LS inside *testFile in FileMap LSObjects(t) // StatObjecsts saves the output of Stat inside *testFile in FileMap StatObjects(t) // ValidateFileMetaDataPostUpload validates the output of LS and Stat ValidateFileMetaData(t) // DU tests DUBucket(t) // Std in/out .. pipe/cat CatObjectToStdIn(t) CatObjectFromStdin(t) // Preserve attributes PutObjectPreserveAttributes(t) // Mirror MirrorTempDirectoryStorageClassReducedRedundancy(t) MirrorTempDirectory(t) MirrorMinio2MinioWithTagsCopy(t) // General object tests FindObjects(t) FindObjectsUsingName(t) FindObjectsUsingNameAndFilteringForTxtType(t) FindObjectsLargerThan64Mebibytes(t) FindObjectsSmallerThan64Mebibytes(t) FindObjectsOlderThan1d(t) FindObjectsNewerThen1d(t) GetObjectsAndCompareMD5(t) } type TestUser struct { Username string Password string } var ( oneMBSlice [1048576]byte // 1x Mebibyte defaultAlias = "mintest" fileMap = make(map[string]*testFile) randomLargeString = "lksdjfljsdklfjklsdjfklksjdf;lsjdk;fjks;djflsdlfkjskldjfklkljsdfljsldkfjklsjdfkljsdklfjklsdjflksjdlfjsdjflsjdflsldfjlsjdflksjdflkjslkdjflksfdj" jsonFlag = "--json" insecureFlag = "--insecure" jsonOutput = true printRawOut = false skipBuild = false mcCmd = ".././mc" preCmdParameters = make([]string, 0) buildPath = "../." metaPrefix = "X-Amz-Meta-" includeDeprecatedMethods = false serverEndpoint = "127.0.0.1:9000" acessKey = "minioadmin" secretKey = "minioadmin" protocol = "http://" skipInsecure = true tempDir = "" mainTestBucket string sseTestBucket string bucketList = make([]string, 0) userList = make(map[string]TestUser, 0) // ENCRYPTION sseHexKey = "8fe4d820587c427d5cc207d75cb76f3c6874808174b04050fa209206bfd08ebb" sseBaseEncodedKey = "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" invalidSSEBaseEncodedKey = "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5" sseBaseEncodedKey2 = "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5YWE" sseKMSKeyName = "" sseInvalidKmsKeyName = "" sseS3Enabled = false curlPath = "/usb/bin/curl" HTTPClient *http.Client failIndicator = "!! FAIL !! _______________________ !! FAIL !! _______________________ !! FAIL !!" ) func openFileAndGetMd5Sum(path string) (md5s string, err error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() fb, err := io.ReadAll(f) if err != nil { return "", err } md5s = GetMD5Sum(fb) return } func GetMBSizeInBytes(MB int) int64 { return int64(MB * len(oneMBSlice)) } func initializeTestSuite(t *testing.T) { shouldSkipBuild := os.Getenv("MC_TEST_SKIP_BUILD") skipBuild, _ = strconv.ParseBool(shouldSkipBuild) fmt.Println("SKIP BUILD:", skipBuild) if !skipBuild { err := BuildCLI() if err != nil { os.Exit(1) } } envBuildPath := os.Getenv("MC_TEST_BUILD_PATH") if envBuildPath != "" { buildPath = envBuildPath } envALIAS := os.Getenv("MC_TEST_ALIAS") if envALIAS != "" { defaultAlias = envALIAS } envSecretKey := os.Getenv("MC_TEST_SECRET_KEY") if envSecretKey != "" { secretKey = envSecretKey } envAccessKey := os.Getenv("MC_TEST_ACCESS_KEY") if envAccessKey != "" { acessKey = envAccessKey } envServerEndpoint := os.Getenv("MC_TEST_SERVER_ENDPOINT") if envServerEndpoint != "" { serverEndpoint = envServerEndpoint } envIncludeDeprecated := os.Getenv("MC_TEST_INCLUDE_DEPRECATED") includeDeprecatedMethods, _ = strconv.ParseBool(envIncludeDeprecated) envKmsKey := os.Getenv("MC_TEST_KMS_KEY") if envKmsKey != "" { sseKMSKeyName = envKmsKey } envSSES3Enabled := os.Getenv("MC_TEST_ENABLE_SSE_S3") if envSSES3Enabled != "" { sseS3Enabled, _ = strconv.ParseBool(envSSES3Enabled) } envSkipInsecure := os.Getenv("MC_TEST_SKIP_INSECURE") if envSkipInsecure != "" { skipInsecure, _ = strconv.ParseBool(envSkipInsecure) } envEnableHTTP := os.Getenv("MC_TEST_ENABLE_HTTPS") EnableHTTPS, _ := strconv.ParseBool(envEnableHTTP) if EnableHTTPS { protocol = "https://" } envCMD := os.Getenv("MC_TEST_BINARY_PATH") if envCMD != "" { mcCmd = envCMD } var err error tempDir, err = os.MkdirTemp("", "test-") if err != nil { log.Println(err) os.Exit(1) } for i := 0; i < len(oneMBSlice); i++ { oneMBSlice[i] = byte(rand.Intn(250)) } for i := 0; i < 10; i++ { tmpNameMap["aaa"+strconv.Itoa(i)] = false } for i := 0; i < 10; i++ { tmpNameMap["bbb"+strconv.Itoa(i)] = false } for i := 0; i < 10; i++ { tmpNameMap["ccc"+strconv.Itoa(i)] = false } for i := 0; i < 10; i++ { tmpNameMap["ddd"+strconv.Itoa(i)] = false } HTTPClient = &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: skipInsecure}, }, } if jsonOutput { preCmdParameters = append(preCmdParameters, jsonFlag) } if skipInsecure { preCmdParameters = append(preCmdParameters, insecureFlag) } CreateTestUsers() _, err = RunMC( "alias", "set", defaultAlias, protocol+serverEndpoint, acessKey, secretKey, ) fatalIfError(err, t) out, err := RunMC("--version") fatalIfError(err, t) fmt.Println(out) preRunCleanup() mainTestBucket = CreateBucket(t) sseTestBucket = CreateBucket(t) } func preflightCheck(t *testing.T) { out, err := exec.Command("which", "curl").Output() fatalIfError(err, t) if len(out) == 0 { fatalMsgOnly("No curl found, output from 'which curl': "+string(out), t) } curlPath = string(out) } func CreateTestUsers() { userList["user1"] = TestUser{ Username: "user1", Password: "user1-password", } userList["user2"] = TestUser{ Username: "user2", Password: "user2-password", } userList["user3"] = TestUser{ Username: "user3", Password: "user3-password", } } func CreateFileBundle() { createFile(newTestFile{ tag: "0M", prefix: "", extension: ".jpg", storageClass: "", sizeInMBS: 0, tags: map[string]string{"name": "0M"}, // uploadShouldFail: false, addToGlobalFileMap: true, }) createFile(newTestFile{ tag: "1M", prefix: "", extension: ".txt", storageClass: "REDUCED_REDUNDANCY", sizeInMBS: 1, metaData: map[string]string{"name": "1M"}, tags: map[string]string{"tag1": "1M-tag"}, // uploadShouldFail: false, addToGlobalFileMap: true, }) createFile(newTestFile{ tag: "2M", prefix: "LVL1", extension: ".jpg", storageClass: "REDUCED_REDUNDANCY", sizeInMBS: 2, metaData: map[string]string{"name": "2M"}, // uploadShouldFail: false, addToGlobalFileMap: true, }) createFile(newTestFile{ tag: "3M", prefix: "LVL1/LVL2", extension: ".png", storageClass: "", sizeInMBS: 3, metaData: map[string]string{"name": "3M"}, // uploadShouldFail: false, addToGlobalFileMap: true, }) createFile(newTestFile{ tag: "65M", prefix: "LVL1/LVL2/LVL3", extension: ".exe", storageClass: "", sizeInMBS: 65, metaData: map[string]string{"name": "65M", "tag1": "value1"}, // uploadShouldFail: false, addToGlobalFileMap: true, }) } var tmpNameMap = make(map[string]bool) func GetRandomName() string { for i := range tmpNameMap { if tmpNameMap[i] == false { tmpNameMap[i] = true return i } } return uuid.NewString() } func CreateBucket(t *testing.T) (bucketPath string) { bucketName := "test-" + GetRandomName() bucketPath = defaultAlias + "/" + bucketName out, err := RunMC("mb", bucketPath) if err != nil { t.Fatalf("Unable to create bucket (%s) err: %s", bucketPath, out) return } bucketList = append(bucketList, bucketPath) out, err = RunMC("stat", defaultAlias+"/"+bucketName) if err != nil { t.Fatalf("Unable to ls stat (%s) err: %s", defaultAlias+"/"+bucketName, out) return } if !strings.Contains(out, bucketName) { t.Fatalf("stat output does not contain bucket name (%s)", bucketName) } return } func AddALIASWithError(t *testing.T) { out, err := RunMC( "alias", "set", defaultAlias, protocol+serverEndpoint, acessKey, "random-invalid-secret-that-will-not-work", ) fatalIfNoErrorWMsg(err, out, t) } func AdminUserFunctionalTest(t *testing.T) { user1Bucket := CreateBucket(t) user1File := createFile(newTestFile{ addToGlobalFileMap: false, tag: "user1", sizeInMBS: 1, }) out, err := RunMC( "admin", "user", "add", defaultAlias, userList["user1"].Username, userList["user1"].Password, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "admin", "user", "list", defaultAlias, ) fatalIfErrorWMsg(err, out, t) userOutput, err := parseUserMessageListOutput(out) fatalIfErrorWMsg(err, out, t) user1found := false for i := range userOutput { if userOutput[i].AccessKey == userList["user1"].Username { user1found = true } } if !user1found { fatalMsgOnly(fmt.Sprintf("did not find user %s when running admin user list --json", userList["user1"].Username), t) } out, err = RunMC( "admin", "policy", "attach", defaultAlias, "readwrite", "--user="+userList["user1"].Username, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "alias", "set", userList["user1"].Username, protocol+serverEndpoint, userList["user1"].Username, userList["user1"].Password, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cp", user1File.diskFile.Name(), user1Bucket+"/"+user1File.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) } func ShareURLUploadErrorTests(t *testing.T) { shareURLErrorBucket := CreateBucket(t) file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "presigned-error", sizeInMBS: 1, }) out, err := RunMC( "share", "upload", shareURLErrorBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) shareMsg, err := parseShareMessageFromJSONOutput(out) fatalIfErrorWMsg(err, out, t) finalURL := strings.ReplaceAll(shareMsg.ShareURL, "", file.diskFile.Name()) splitCommand := strings.Split(finalURL, " ") if skipInsecure { splitCommand = append(splitCommand, "--insecure") } bucketOnly := strings.ReplaceAll(shareURLErrorBucket, defaultAlias+"/", "") // Modify base url bucket path newCmd := make([]string, len(splitCommand)) copy(newCmd, splitCommand) newCmd[1] = strings.ReplaceAll(newCmd[1], bucketOnly, "fake-bucket-name") out, _ = RunCommand(newCmd[0], newCmd[1:]...) curlFatalIfNoErrorTag(out, t) // Modify -F key=X newCmd = make([]string, len(splitCommand)) copy(newCmd, splitCommand) for i := range newCmd { if strings.HasPrefix(newCmd[i], "key=") { newCmd[i] = "key=fake-object-name" break } } out, _ = RunCommand(newCmd[0], newCmd[1:]...) curlFatalIfNoErrorTag(out, t) } func ShareURLUploadTest(t *testing.T) { ShareURLTestBucket := CreateBucket(t) file := createFile(newTestFile{ addToGlobalFileMap: true, tag: "presigned-upload", sizeInMBS: 1, }) out, err := RunMC( "share", "upload", ShareURLTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) shareMsg, err := parseShareMessageFromJSONOutput(out) fatalIfErrorWMsg(err, out, t) finalURL := strings.ReplaceAll(shareMsg.ShareURL, "", file.diskFile.Name()) splitCommand := strings.Split(finalURL, " ") if skipInsecure { splitCommand = append(splitCommand, "--insecure") } _, err = exec.Command(splitCommand[0], splitCommand[1:]...).CombinedOutput() fatalIfErrorWMsg(err, out, t) out, err = RunMC( "stat", ShareURLTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) statMsg, err := parseStatSingleObjectJSONOutput(out) fatalIfError(err, t) if statMsg.ETag != file.md5Sum { fatalMsgOnly(fmt.Sprintf("expecting md5sum (%s) but got md5sum (%s)", file.md5Sum, file.md5Sum), t) } } func ShareURLDownloadTest(t *testing.T) { ShareURLTestBucket := CreateBucket(t) file := createFile(newTestFile{ addToGlobalFileMap: true, tag: "presigned-download", sizeInMBS: 1, }) out, err := RunMC( "cp", file.diskFile.Name(), ShareURLTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "share", "download", ShareURLTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) shareMsg, err := parseShareMessageFromJSONOutput(out) fatalIfErrorWMsg(err, out, t) resp, err := HTTPClient.Get(shareMsg.ShareURL) fatalIfError(err, t) downloadedFile, err := io.ReadAll(resp.Body) fatalIfError(err, t) md5sum := GetMD5Sum(downloadedFile) if md5sum != file.md5Sum { fatalMsgOnly( fmt.Sprintf("expecting md5sum (%s) but got md5sum (%s)", file.md5Sum, md5sum), t, ) } } func PutObjectPreserveAttributes(t *testing.T) { AttrTestBucket := CreateBucket(t) file := fileMap["1M"] out, err := RunMC( "cp", "-a", file.diskFile.Name(), AttrTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "stat", AttrTestBucket+"/"+file.fileNameWithPrefix, ) fatalIfError(err, t) stats, err := parseStatSingleObjectJSONOutput(out) fatalIfError(err, t) attr, err := disk.GetFileSystemAttrs(file.diskFile.Name()) fatalIfError(err, t) if attr != stats.Metadata["X-Amz-Meta-Mc-Attrs"] { fatalMsgOnly(fmt.Sprintf("expecting file attributes (%s) but got file attributes (%s)", attr, stats.Metadata["X-Amz-Meta-Mc-Attrs"]), t) } } func MirrorTempDirectoryStorageClassReducedRedundancy(t *testing.T) { MirrorBucket := CreateBucket(t) out, err := RunMC( "mirror", "--storage-class", "REDUCED_REDUNDANCY", tempDir, MirrorBucket, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC("ls", "-r", MirrorBucket) fatalIfError(err, t) fileList, err := parseLSJSONOutput(out) fatalIfError(err, t) for i, f := range fileMap { fileFound := false for _, o := range fileList { if o.Key == f.fileNameWithoutPath { fileMap[i].MinioLS = o fileFound = true } } if !fileFound { t.Fatalf("File was not uploaded: %s", f.fileNameWithPrefix) } } } func MirrorTempDirectory(t *testing.T) { MirrorBucket := CreateBucket(t) out, err := RunMC( "mirror", tempDir, MirrorBucket, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC("ls", "-r", MirrorBucket) fatalIfError(err, t) fileList, err := parseLSJSONOutput(out) fatalIfError(err, t) for i, f := range fileMap { fileFound := false for _, o := range fileList { if o.Key == f.fileNameWithoutPath { fileMap[i].MinioLS = o fileFound = true } } if !fileFound { t.Fatalf("File was not uploaded: %s", f.fileNameWithPrefix) } } } type tagListResult struct { Tagset map[string]string `json:"tagset"` Status string `json:"status"` URL string `json:"url"` VersionID string `json:"versionID"` } func MirrorMinio2MinioWithTagsCopy(t *testing.T) { TargetBucket := CreateBucket(t) out, err := RunMC( "mirror", mainTestBucket, TargetBucket, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC("tag", "list", TargetBucket, "-r") fatalIfErrorWMsg(err, out, t) var targetTagsList []tagListResult outStrs := bufio.NewScanner(strings.NewReader(out)) for outStrs.Scan() { outStr := outStrs.Text() if outStr == "" { continue } tagList := new(tagListResult) if err := json.Unmarshal([]byte(outStr), tagList); err != nil { fatalIfErrorWMsg(err, out, t) } targetTagsList = append(targetTagsList, *tagList) } for _, f := range fileMap { fileFound := false for _, o := range targetTagsList { if strings.Contains(o.URL, f.fileNameWithPrefix) { fileFound = true if !reflect.DeepEqual(f.tags, o.Tagset) { fatalMsgOnly(fmt.Sprintf("expecting tags (%s) but got tags (%s)", f.tags, o.Tagset), t) } } } if !fileFound { t.Fatalf("File was not mirrored: %s", f.fileNameWithPrefix) } } } func CatObjectFromStdin(t *testing.T) { objectName := "pipe-test-object" CatEchoBucket := CreateBucket(t) file := fileMap["1M"] cmdCAT := exec.Command( "cat", file.diskFile.Name(), ) p := []string{ "pipe", CatEchoBucket + "/" + objectName, } if skipInsecure { p = append(p, "--insecure") } cmdMC := exec.Command(mcCmd, p...) r, w := io.Pipe() defer r.Close() defer w.Close() cmdCAT.Stdout = w cmdMC.Stdin = r err := cmdMC.Start() fatalIfError(err, t) err = cmdCAT.Start() fatalIfError(err, t) err = cmdCAT.Wait() fatalIfError(err, t) w.Close() err = cmdMC.Wait() fatalIfError(err, t) r.Close() outB, err := RunMC( "cat", CatEchoBucket+"/"+objectName, ) fatalIfErrorWMsg(err, outB, t) md5SumCat := GetMD5Sum([]byte(outB)) if file.md5Sum != md5SumCat { fatalMsgOnly( fmt.Sprintf("expecting md5sum (%s) but got md5sum (%s)", file.md5Sum, md5SumCat), t, ) } } func CatObjectToStdIn(t *testing.T) { file := fileMap["1M"] out, err := RunMC( "cat", mainTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) md5Sum := GetMD5Sum([]byte(out)) if md5Sum != file.md5Sum { fatalMsgOnly( fmt.Sprintf("expecting md5sum (%s) but got md5sum (%s)", file.md5Sum, md5Sum), t, ) } } func VerifyKMSKey(t *testing.T) { out, err := RunMC( "admin", "kms", "key", "list", defaultAlias, ) fatalIfError(err, t) keyMsg := new(kmsKeysMsg) err = json.Unmarshal([]byte(out), keyMsg) fatalIfError(err, t) sseInvalidKmsKeyName = uuid.NewString() found := false invalidKeyFound := false for _, v := range keyMsg.Keys { if v == sseKMSKeyName { found = true break } if v == sseInvalidKmsKeyName { invalidKeyFound = true } } if !found { fatalMsgOnly(fmt.Sprintf("expected to find kms key %s but got these keys: %v", sseKMSKeyName, keyMsg.Keys), t) } if invalidKeyFound { fatalMsgOnly("tried to create invalid uuid kms key but for some reason it overlapped with an already existing key", t) } } func PutObjectWithSSEKMSPartialPrefixMatch(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encput-kms-prefix-test", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-kms", sseTestBucket+"/"+file.fileNameWithoutPath+"="+sseKMSKeyName, file.diskFile.Name(), sseTestBucket, ) fatalIfErrorWMsg(err, out, t) } func PutObjectWithSSEKMS(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encput-kms", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-kms", sseTestBucket+"="+sseKMSKeyName, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) } func PutObjectWithSSEKMSMultipart(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encmultiput-kms", sizeInMBS: 68, }) out, err := RunMC( "cp", "--enc-kms", sseTestBucket+"="+sseKMSKeyName, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) } func PutObjectWithSSEKMSInvalidKeys(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encerror-kms", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-kms="+sseTestBucket+"="+sseInvalidKmsKeyName, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfNoErrorWMsg(err, out, t) } func GetObjectWithSSES3(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encget-s3", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-s3="+sseTestBucket, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cp", sseTestBucket+"/"+file.fileNameWithoutPath, file.diskFile.Name()+".download", ) fatalIfErrorWMsg(err, out, t) md5s, err := openFileAndGetMd5Sum(file.diskFile.Name() + ".download") fatalIfError(err, t) if md5s != file.md5Sum { fatalMsgOnly(fmt.Sprintf("expecting md5sum (%s) but got sum (%s)", file.md5Sum, md5s), t) } } func CatObjectWithSSES3(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "enccat-s3", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-s3="+sseTestBucket, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cat", sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) catMD5Sum := GetMD5Sum([]byte(out)) if catMD5Sum != file.md5Sum { fatalMsgOnly(fmt.Sprintf( "expected md5sum %s but we got %s", file.md5Sum, catMD5Sum, ), t) } if int64(len(out)) != file.diskStat.Size() { fatalMsgOnly(fmt.Sprintf( "file size is %d but we got %d", file.diskStat.Size(), len(out), ), t) } fatalIfErrorWMsg( err, "cat length: "+strconv.Itoa(len(out))+" -- file length:"+strconv.Itoa(int(file.diskStat.Size())), t, ) } func CopyObjectWithSSES3ToNewBucket(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encbucketcopy-s3", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-s3="+sseTestBucket, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) TargetSSEBucket := CreateBucket(t) out, err = RunMC( "cp", "--enc-s3="+TargetSSEBucket, sseTestBucket+"/"+file.fileNameWithoutPath, TargetSSEBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cp", TargetSSEBucket+"/"+file.fileNameWithoutPath, file.diskFile.Name()+".download", ) fatalIfErrorWMsg(err, out, t) md5s, err := openFileAndGetMd5Sum(file.diskFile.Name() + ".download") fatalIfError(err, t) if md5s != file.md5Sum { fatalMsgOnly(fmt.Sprintf("expecting md5sum (%s) but got sum (%s)", file.md5Sum, md5s), t) } } func MirrorTempDirectoryUsingSSES3(t *testing.T) { MirrorBucket := CreateBucket(t) subDir := "encmirror-s3" f1 := createFile(newTestFile{ addToGlobalFileMap: false, subDir: subDir, tag: "encmirror1-s3", sizeInMBS: 1, }) f2 := createFile(newTestFile{ addToGlobalFileMap: false, subDir: subDir, tag: "encmirror2-s3", sizeInMBS: 2, }) f3 := createFile(newTestFile{ addToGlobalFileMap: false, subDir: subDir, tag: "encmirror3-s3", sizeInMBS: 4, }) files := append([]*testFile{}, f1, f2, f3) out, err := RunMC( "mirror", "--enc-s3="+MirrorBucket, tempDir+string(os.PathSeparator)+subDir, MirrorBucket, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC("ls", "-r", MirrorBucket) fatalIfError(err, t) fileList, err := parseLSJSONOutput(out) fatalIfError(err, t) for i, f := range files { fileFound := false for _, o := range fileList { if o.Key == f.fileNameWithoutPath { files[i].MinioLS = o fileFound = true } } if !fileFound { fatalMsgOnly(fmt.Sprintf( "File was not uploaded: %s", f.fileNameWithPrefix, ), t) } out, err := RunMC("stat", MirrorBucket+"/"+files[i].MinioLS.Key) fatalIfError(err, t) stat, err := parseStatSingleObjectJSONOutput(out) fatalIfError(err, t) files[i].MinioStat = stat foundKmsTag := false for ii := range stat.Metadata { if ii == amzObjectSSE { foundKmsTag = true break } } if !foundKmsTag { fmt.Println(stat) fatalMsgOnly(amzObjectSSEKMSKeyID+" not found for object "+files[i].MinioLS.Key, t) } } } func PutObjectWithSSES3PartialPrefixMatch(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encput-s3-prefix-test", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-s3="+sseTestBucket+"/"+file.fileNameWithoutPath, file.diskFile.Name(), sseTestBucket, ) fatalIfErrorWMsg(err, out, t) } func PutObjectWithSSES3(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encput-s3", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-s3="+sseTestBucket, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) } func PutObjectWithSSES3Multipart(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encmultiput-s3", sizeInMBS: 68, }) out, err := RunMC( "cp", "--enc-s3="+sseTestBucket, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) } func GetObjectWithSSEKMS(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encget-kms", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-kms="+sseTestBucket+"="+sseKMSKeyName, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cp", sseTestBucket+"/"+file.fileNameWithoutPath, file.diskFile.Name()+".download", ) fatalIfErrorWMsg(err, out, t) md5s, err := openFileAndGetMd5Sum(file.diskFile.Name() + ".download") fatalIfError(err, t) if md5s != file.md5Sum { fatalMsgOnly(fmt.Sprintf("expecting md5sum (%s) but got sum (%s)", file.md5Sum, md5s), t) } } func PutObjectWithSSECMultipart(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encmultiput", sizeInMBS: 68, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) } func CatObjectWithSSEKMS(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "enccat-kms", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-kms="+sseTestBucket+"="+sseKMSKeyName, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cat", sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) catMD5Sum := GetMD5Sum([]byte(out)) if catMD5Sum != file.md5Sum { fatalMsgOnly(fmt.Sprintf( "expected md5sum %s but we got %s", file.md5Sum, catMD5Sum, ), t) } if int64(len(out)) != file.diskStat.Size() { fatalMsgOnly(fmt.Sprintf( "file size is %d but we got %d", file.diskStat.Size(), len(out), ), t) } fatalIfErrorWMsg( err, "cat length: "+strconv.Itoa(len(out))+" -- file length:"+strconv.Itoa(int(file.diskStat.Size())), t, ) } func PutObjectWithSSECPartialPrefixMatch(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encput-prefix-test", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"/"+file.fileNameWithoutPath+"="+sseBaseEncodedKey, file.diskFile.Name(), sseTestBucket, ) fatalIfErrorWMsg(err, out, t) } func PutObjectWithSSECHexKey(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encputhex", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseHexKey, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) } func PutObjectWithSSEC(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encput", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) } func PutObjectErrorWithSSECOverHTTP(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encput-http", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfNoErrorWMsg(err, out, t) } func PutObjectWithSSECInvalidKeys(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encerror-dep", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"="+invalidSSEBaseEncodedKey, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfNoErrorWMsg(err, out, t) } func GetObjectWithSSECHexKey(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encgethex", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseHexKey, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseHexKey, sseTestBucket+"/"+file.fileNameWithoutPath, file.diskFile.Name()+".download", ) fatalIfErrorWMsg(err, out, t) md5s, err := openFileAndGetMd5Sum(file.diskFile.Name() + ".download") fatalIfError(err, t) if md5s != file.md5Sum { fatalMsgOnly(fmt.Sprintf("expecting md5sum (%s) but got sum (%s)", file.md5Sum, md5s), t) } } func GetObjectWithSSEC(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encget", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, sseTestBucket+"/"+file.fileNameWithoutPath, file.diskFile.Name()+".download", ) fatalIfErrorWMsg(err, out, t) md5s, err := openFileAndGetMd5Sum(file.diskFile.Name() + ".download") fatalIfError(err, t) if md5s != file.md5Sum { fatalMsgOnly(fmt.Sprintf("expecting md5sum (%s) but got sum (%s)", file.md5Sum, md5s), t) } } func GetObjectWithSSECWithoutKey(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encerror", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cp", sseTestBucket+"/"+file.fileNameWithoutPath, file.diskFile.Name()+"-get", ) fatalIfNoErrorWMsg(err, out, t) } func CatObjectWithSSEC(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "enccat", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cat", "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) catMD5Sum := GetMD5Sum([]byte(out)) if catMD5Sum != file.md5Sum { fatalMsgOnly(fmt.Sprintf( "expected md5sum %s but we got %s", file.md5Sum, catMD5Sum, ), t) } if int64(len(out)) != file.diskStat.Size() { fatalMsgOnly(fmt.Sprintf( "file size is %d but we got %d", file.diskStat.Size(), len(out), ), t) } fatalIfErrorWMsg( err, "cat length: "+strconv.Itoa(len(out))+" -- file length:"+strconv.Itoa(int(file.diskStat.Size())), t, ) } func CopyObjectWithSSEKMSWithOverLappingKeys(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encbucketcopy-kms", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-kms="+sseTestBucket+"="+sseKMSKeyName, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) TargetSSEBucket := CreateBucket(t) out, err = RunMC( "cp", "--enc-kms="+TargetSSEBucket+"="+sseKMSKeyName, "--enc-kms="+TargetSSEBucket+"="+sseKMSKeyName, sseTestBucket+"/"+file.fileNameWithoutPath, TargetSSEBucket+"/"+file.fileNameWithoutPath, ) fatalIfNoErrorWMsg(err, out, t) } func CopyObjectWithSSEKMSToNewBucket(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encbucketcopy-kms", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-kms="+sseTestBucket+"="+sseKMSKeyName, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) TargetSSEBucket := CreateBucket(t) out, err = RunMC( "cp", "--enc-kms="+TargetSSEBucket+"="+sseKMSKeyName, "--enc-kms="+sseTestBucket+"="+sseKMSKeyName, sseTestBucket+"/"+file.fileNameWithoutPath, TargetSSEBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cp", "--enc-kms="+TargetSSEBucket+"="+sseKMSKeyName, TargetSSEBucket+"/"+file.fileNameWithoutPath, file.diskFile.Name()+".download", ) fatalIfErrorWMsg(err, out, t) md5s, err := openFileAndGetMd5Sum(file.diskFile.Name() + ".download") fatalIfError(err, t) if md5s != file.md5Sum { fatalMsgOnly(fmt.Sprintf("expecting md5sum (%s) but got sum (%s)", file.md5Sum, md5s), t) } } func CopyObjectWithSSEKMSToNewBucketWithSSEC(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encbucketcopy-kms-c", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-kms="+sseTestBucket+"="+sseKMSKeyName, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) TargetSSEBucket := CreateBucket(t) out, err = RunMC( "cp", "--enc-c="+TargetSSEBucket+"="+sseBaseEncodedKey, sseTestBucket+"/"+file.fileNameWithoutPath, TargetSSEBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cp", "--enc-c="+TargetSSEBucket+"="+sseBaseEncodedKey, TargetSSEBucket+"/"+file.fileNameWithoutPath, file.diskFile.Name()+".download", ) fatalIfErrorWMsg(err, out, t) md5s, err := openFileAndGetMd5Sum(file.diskFile.Name() + ".download") fatalIfError(err, t) if md5s != file.md5Sum { fatalMsgOnly(fmt.Sprintf("expecting md5sum (%s) but got sum (%s)", file.md5Sum, md5s), t) } } func MirrorTempDirectoryUsingSSEKMS(t *testing.T) { MirrorBucket := CreateBucket(t) subDir := "encmirror-kms" f1 := createFile(newTestFile{ addToGlobalFileMap: false, subDir: subDir, tag: "encmirror1-kms", sizeInMBS: 1, }) f2 := createFile(newTestFile{ addToGlobalFileMap: false, subDir: subDir, tag: "encmirror2-kms", sizeInMBS: 2, }) f3 := createFile(newTestFile{ addToGlobalFileMap: false, subDir: subDir, tag: "encmirror3-kms", sizeInMBS: 4, }) files := append([]*testFile{}, f1, f2, f3) out, err := RunMC( "mirror", "--enc-kms="+MirrorBucket+"="+sseKMSKeyName, tempDir+string(os.PathSeparator)+subDir, MirrorBucket, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC("ls", "-r", MirrorBucket) fatalIfError(err, t) fileList, err := parseLSJSONOutput(out) fatalIfError(err, t) for i, f := range files { fileFound := false for _, o := range fileList { if o.Key == f.fileNameWithoutPath { files[i].MinioLS = o fileFound = true } } if !fileFound { fatalMsgOnly(fmt.Sprintf( "File was not uploaded: %s", f.fileNameWithPrefix, ), t) } out, err := RunMC("stat", MirrorBucket+"/"+files[i].MinioLS.Key) fatalIfError(err, t) stat, err := parseStatSingleObjectJSONOutput(out) fatalIfError(err, t) files[i].MinioStat = stat foundKmsTag := false for ii, v := range stat.Metadata { if ii == amzObjectSSEKMSKeyID { foundKmsTag = true if !strings.HasSuffix(v, sseKMSKeyName) { fatalMsgOnly("invalid KMS key for object "+files[i].MinioLS.Key, t) break } } } if !foundKmsTag { fatalMsgOnly(amzObjectSSEKMSKeyID+" not found for object "+files[i].MinioLS.Key, t) } } } func RemoveObjectWithSSEKMS(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encrm-kms", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-kms="+sseTestBucket+"="+sseKMSKeyName, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "rm", sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "stat", sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfNoErrorWMsg(err, out, t) } func CatObjectWithSSECWithoutKey(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encerror", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cat", sseTestBucket+"/"+file.fileNameWithoutPath, file.diskFile.Name()+"-cat", ) fatalIfNoErrorWMsg(err, out, t) } func RemoveObjectWithSSEC(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encrm", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "rm", sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "stat", "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfNoErrorWMsg(err, out, t) } func MirrorTempDirectoryUsingSSEC(t *testing.T) { MirrorBucket := CreateBucket(t) subDir := "encmirror" f1 := createFile(newTestFile{ addToGlobalFileMap: false, subDir: subDir, tag: "encmirror1", sizeInMBS: 1, }) f2 := createFile(newTestFile{ addToGlobalFileMap: false, subDir: subDir, tag: "encmirror2", sizeInMBS: 2, }) f3 := createFile(newTestFile{ addToGlobalFileMap: false, subDir: subDir, tag: "encmirror3", sizeInMBS: 4, }) files := append([]*testFile{}, f1, f2, f3) out, err := RunMC( "mirror", "--enc-c="+MirrorBucket+"="+sseBaseEncodedKey, tempDir+string(os.PathSeparator)+subDir, MirrorBucket, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC("ls", "-r", MirrorBucket) fatalIfError(err, t) fileList, err := parseLSJSONOutput(out) fatalIfError(err, t) for i, f := range files { fileFound := false for _, o := range fileList { if o.Key == f.fileNameWithoutPath { files[i].MinioLS = o fileFound = true } } if !fileFound { fatalMsgOnly(fmt.Sprintf( "File was not uploaded: %s", f.fileNameWithPrefix, ), t) } out, err := RunMC( "stat", "--enc-c="+MirrorBucket+"="+sseBaseEncodedKey, MirrorBucket+"/"+files[i].MinioLS.Key, ) fatalIfError(err, t) _, err = parseStatSingleObjectJSONOutput(out) fatalIfError(err, t) } } func CopyObjectWithSSECToNewBucketWithNewKey(t *testing.T) { file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "encbucketcopy", sizeInMBS: 1, }) out, err := RunMC( "cp", "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, file.diskFile.Name(), sseTestBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) TargetSSEBucket := CreateBucket(t) out, err = RunMC( "cp", "--enc-c="+TargetSSEBucket+"="+sseBaseEncodedKey2, "--enc-c="+sseTestBucket+"="+sseBaseEncodedKey, sseTestBucket+"/"+file.fileNameWithoutPath, TargetSSEBucket+"/"+file.fileNameWithoutPath, ) fatalIfErrorWMsg(err, out, t) out, err = RunMC( "cp", "--enc-c="+TargetSSEBucket+"="+sseBaseEncodedKey2, TargetSSEBucket+"/"+file.fileNameWithoutPath, file.diskFile.Name()+".download", ) fatalIfErrorWMsg(err, out, t) md5s, err := openFileAndGetMd5Sum(file.diskFile.Name() + ".download") fatalIfError(err, t) if md5s != file.md5Sum { fatalMsgOnly(fmt.Sprintf("expecting md5sum (%s) but got sum (%s)", file.md5Sum, md5s), t) } } func uploadAllFiles(t *testing.T) { for _, v := range fileMap { parameters := make([]string, 0) parameters = append(parameters, "cp") if v.storageClass != "" { parameters = append(parameters, "--storage-class", v.storageClass) } if len(v.metaData) > 0 { parameters = append(parameters, "--attr") meta := "" for i, v := range v.metaData { meta += i + "=" + v + ";" } meta = strings.TrimSuffix(meta, ";") parameters = append(parameters, meta) } if len(v.tags) > 0 { parameters = append(parameters, "--tags") tags := "" for i, v := range v.tags { tags += i + "=" + v + ";" } tags = strings.TrimSuffix(tags, ";") parameters = append(parameters, tags) } parameters = append(parameters, v.diskFile.Name()) if v.prefix != "" { parameters = append( parameters, mainTestBucket+"/"+v.fileNameWithPrefix, ) } else { parameters = append( parameters, mainTestBucket+"/"+v.fileNameWithoutPath, ) } _, err := RunMC(parameters...) if err != nil { t.Fatal(err) } } } func OD(t *testing.T) { LocalBucketPath := CreateBucket(t) file := fileMap["65M"] out, err := RunMC( "od", "if="+file.diskFile.Name(), "of="+LocalBucketPath+"/od/"+file.fileNameWithoutPath, "parts=10", ) fatalIfError(err, t) odMsg, err := parseSingleODMessageJSONOutput(out) fatalIfError(err, t) if odMsg.TotalSize != file.diskStat.Size() { t.Fatalf( "Expected (%d) bytes to be uploaded but only uploaded (%d) bytes", odMsg.TotalSize, file.diskStat.Size(), ) } if odMsg.Parts != 10 { t.Fatalf( "Expected upload parts to be (10) but they were (%d)", odMsg.Parts, ) } if odMsg.Type != "FStoS3" { t.Fatalf( "Expected type to be (FStoS3) but got (%s)", odMsg.Type, ) } if odMsg.PartSize != uint64(file.diskStat.Size())/10 { t.Fatalf( "Expected part size to be (%d) but got (%d)", file.diskStat.Size()/10, odMsg.PartSize, ) } out, err = RunMC( "od", "of="+file.diskFile.Name(), "if="+LocalBucketPath+"/od/"+file.fileNameWithoutPath, "parts=10", ) fatalIfError(err, t) fmt.Println(out) odMsg, err = parseSingleODMessageJSONOutput(out) fatalIfError(err, t) if odMsg.TotalSize != file.diskStat.Size() { t.Fatalf( "Expected (%d) bytes to be uploaded but only uploaded (%d) bytes", odMsg.TotalSize, file.diskStat.Size(), ) } if odMsg.Parts != 10 { t.Fatalf( "Expected upload parts to be (10) but they were (%d)", odMsg.Parts, ) } if odMsg.Type != "S3toFS" { t.Fatalf( "Expected type to be (FStoS3) but got (%s)", odMsg.Type, ) } if odMsg.PartSize != uint64(file.diskStat.Size())/10 { t.Fatalf( "Expected part size to be (%d) but got (%d)", file.diskStat.Size()/10, odMsg.PartSize, ) } } func MvFromDiskToMinio(t *testing.T) { LocalBucketPath := CreateBucket(t) file := createFile(newTestFile{ addToGlobalFileMap: false, tag: "10Move", prefix: "", extension: ".txt", storageClass: "", sizeInMBS: 1, metaData: map[string]string{"name": "10Move"}, tags: map[string]string{"tag1": "10Move-tag"}, }) out, err := RunMC( "mv", file.diskFile.Name(), LocalBucketPath+"/"+file.fileNameWithoutPath, ) fatalIfError(err, t) splitReturn := bytes.Split([]byte(out), []byte{10}) mvMSG, err := parseSingleCPMessageJSONOutput(string(splitReturn[0])) fatalIfError(err, t) if mvMSG.TotalCount != 1 { t.Fatalf("Expected count to be 1 but got (%d)", mvMSG.TotalCount) } if mvMSG.Size != file.diskStat.Size() { t.Fatalf( "Expected size to be (%d) but got (%d)", file.diskStat.Size(), mvMSG.Size, ) } if mvMSG.Status != "success" { t.Fatalf( "Expected status to be (success) but got (%s)", mvMSG.Status, ) } statMSG, err := parseSingleAccountStatJSONOutput(string(splitReturn[1])) fatalIfError(err, t) if statMSG.Transferred != file.diskStat.Size() { t.Fatalf( "Expected transfeered to be (%d) but got (%d)", file.diskStat.Size(), statMSG.Transferred, ) } if statMSG.Total != file.diskStat.Size() { t.Fatalf( "Expected total to be (%d) but got (%d)", file.diskStat.Size(), statMSG.Total, ) } if statMSG.Status != "success" { t.Fatalf( "Expected status to be (success) but got (%s)", statMSG.Status, ) } } func DUBucket(t *testing.T) { var totalFileSize int64 for _, v := range fileMap { totalFileSize += v.MinioStat.Size } out, err := RunMC("du", mainTestBucket) fatalIfError(err, t) duList, err := parseDUJSONOutput(out) fatalIfError(err, t) if len(duList) != 1 { fatalMsgOnly("Expected 1 result to be returned", t) } if duList[0].Size != totalFileSize { fatalMsgOnly( fmt.Sprintf("total size to be %d but got %d", totalFileSize, duList[0].Size), t, ) } } func LSObjects(t *testing.T) { out, err := RunMC("ls", "-r", mainTestBucket) fatalIfError(err, t) fileList, err := parseLSJSONOutput(out) fatalIfError(err, t) for i, f := range fileMap { fileFound := false for _, o := range fileList { if o.Key == f.fileNameWithPrefix { fileMap[i].MinioLS = o fileFound = true } } if !fileFound { t.Fatalf("File was not uploaded: %s", f.fileNameWithPrefix) } } } func StatObjects(t *testing.T) { for i, v := range fileMap { out, err := RunMC( "stat", mainTestBucket+"/"+v.fileNameWithPrefix, ) fatalIfError(err, t) fileMap[i].MinioStat, err = parseStatSingleObjectJSONOutput(out) fatalIfError(err, t) if fileMap[i].MinioStat.Key == "" { t.Fatalf("Unable to stat Minio object (%s)", v.fileNameWithPrefix) } } } func ValidateFileMetaData(t *testing.T) { for _, f := range fileMap { validateFileLSInfo(t, f) validateObjectMetaData(t, f) // validateContentType(t, f) } } func FindObjects(t *testing.T) { out, err := RunMC("find", mainTestBucket) fatalIfError(err, t) findList, err := parseFindJSONOutput(out) fatalIfError(err, t) for _, v := range fileMap { found := false for _, vv := range findList { if strings.HasSuffix(vv.Key, v.MinioLS.Key) { found = true } } if !found { t.Fatalf("File (%s) not found by 'find' command", v.MinioLS.Key) } } } func FindObjectsUsingName(t *testing.T) { for _, v := range fileMap { out, err := RunMC( "find", mainTestBucket, "--name", v.fileNameWithoutPath, ) fatalIfError(err, t) info, err := parseFindSingleObjectJSONOutput(out) fatalIfError(err, t) if !strings.HasSuffix(info.Key, v.MinioLS.Key) { t.Fatalf("Invalid key (%s) when searching for (%s)", info.Key, v.MinioLS.Key) } } } func FindObjectsUsingNameAndFilteringForTxtType(t *testing.T) { out, err := RunMC( "find", mainTestBucket, "--name", "*.txt", ) fatalIfError(err, t) findList, err := parseFindJSONOutput(out) fatalIfError(err, t) for _, v := range fileMap { if v.extension != ".txt" { continue } found := false for _, vv := range findList { if strings.HasSuffix(vv.Key, v.MinioLS.Key) { found = true } } if !found { t.Fatalf("File (%s) not found by 'find' command", v.MinioLS.Key) } } } func FindObjectsSmallerThan64Mebibytes(t *testing.T) { out, err := RunMC( "find", mainTestBucket, "--smaller", "64MB", ) fatalIfError(err, t) findList, err := parseFindJSONOutput(out) fatalIfError(err, t) for _, v := range fileMap { if v.diskStat.Size() > GetMBSizeInBytes(64) { continue } found := false for _, vv := range findList { if strings.HasSuffix(vv.Key, v.MinioLS.Key) { found = true } } if !found { t.Fatalf("File (%s) not found by 'find' command", v.MinioLS.Key) } } } func FindObjectsLargerThan64Mebibytes(t *testing.T) { out, err := RunMC( "find", mainTestBucket, "--larger", "64MB", ) fatalIfError(err, t) findList, err := parseFindJSONOutput(out) fatalIfError(err, t) for _, v := range fileMap { if v.diskStat.Size() < GetMBSizeInBytes(64) { continue } found := false for _, vv := range findList { if strings.HasSuffix(vv.Key, v.MinioLS.Key) { found = true } } if !found { t.Fatalf("File (%s) not found by 'find' command", v.MinioLS.Key) } } } func FindObjectsOlderThan1d(t *testing.T) { out, err := RunMC( "find", mainTestBucket, "--older-than", "1d", ) fatalIfError(err, t) findList, err := parseFindJSONOutput(out) fatalIfError(err, t) if len(findList) > 0 { t.Fatalf("We should not have found any files which are older then 1 day") } } func FindObjectsNewerThen1d(t *testing.T) { out, err := RunMC( "find", mainTestBucket, "--newer-than", "1d", ) fatalIfError(err, t) findList, err := parseFindJSONOutput(out) fatalIfError(err, t) for _, v := range fileMap { found := false for _, vv := range findList { if strings.HasSuffix(vv.Key, v.MinioLS.Key) { found = true } } if !found { t.Fatalf("File (%s) not found by 'find' command", v.MinioLS.Key) } } } func GetObjectsAndCompareMD5(t *testing.T) { for _, v := range fileMap { // make sure old downloads are not in our way _ = os.Remove(tempDir + "/" + v.fileNameWithoutPath + ".downloaded") _, err := RunMC( "cp", mainTestBucket+"/"+v.fileNameWithPrefix, tempDir+"/"+v.fileNameWithoutPath+".downloaded", ) fatalIfError(err, t) downloadedFile, err := os.Open( tempDir + "/" + v.fileNameWithoutPath + ".downloaded", ) fatalIfError(err, t) fileBytes, err := io.ReadAll(downloadedFile) fatalIfError(err, t) md5sum := GetMD5Sum(fileBytes) if v.md5Sum != md5sum { t.Fatalf( "The downloaded file md5sum is wrong: original-md5(%s) downloaded-md5(%s)", v.md5Sum, md5sum, ) } } } func CreateBucketUsingInvalidSymbols(t *testing.T) { bucketNameMap := make(map[string]string) bucketNameMap["name-too-big"] = randomLargeString bucketNameMap["!"] = "symbol!" bucketNameMap["@"] = "symbol@" bucketNameMap["#"] = "symbol#" bucketNameMap["$"] = "symbol$" bucketNameMap["%"] = "symbol%" bucketNameMap["^"] = "symbol^" bucketNameMap["&"] = "symbol&" bucketNameMap["*"] = "symbol*" bucketNameMap["("] = "symbol(" bucketNameMap[")"] = "symbol)" bucketNameMap["{"] = "symbol{" bucketNameMap["}"] = "symbol}" bucketNameMap["["] = "symbol[" bucketNameMap["]"] = "symbol]" for _, v := range bucketNameMap { _, err := RunMC("mb", defaultAlias+"/"+v) if err == nil { t.Fatalf("We should not have been able to create a bucket with the name: %s", v) } } } func RemoveBucketThatDoesNotExist(t *testing.T) { randomID := uuid.NewString() out, _ := RunMC( "rb", defaultAlias+"/"+randomID, ) errMSG, _ := parseSingleErrorMessageJSONOutput(out) validateErrorMSGValues( t, errMSG, "error", "Unable to validate", "does not exist", ) } func RemoveBucketWithNameTooLong(t *testing.T) { randomID := uuid.NewString() out, _ := RunMC( "rb", defaultAlias+"/"+randomID+randomID, ) errMSG, _ := parseSingleErrorMessageJSONOutput(out) validateErrorMSGValues( t, errMSG, "error", "Unable to validate", "Bucket name cannot be longer than 63 characters", ) } func UploadToUnknownBucket(t *testing.T) { randomBucketID := uuid.NewString() parameters := append( []string{}, "cp", fileMap["1M"].diskFile.Name(), defaultAlias+"/"+randomBucketID+"-test-should-not-exist"+"/"+fileMap["1M"].fileNameWithoutPath, ) _, err := RunMC(parameters...) if err == nil { t.Fatalf("We should not have been able to upload to bucket: %s", randomBucketID) } } func preRunCleanup() { for i := range tmpNameMap { _, _ = RunMC("rb", "--force", "--dangerous", defaultAlias+"/test-"+i) } } func postRunCleanup(t *testing.T) { var err error var berr error var out string err = os.RemoveAll(tempDir) if err != nil { fmt.Println(err) } for _, v := range bucketList { out, berr = RunMC("rb", "--force", "--dangerous", v) if berr != nil { fmt.Printf("Unable to remove bucket (%s) err: %s // out: %s", v, berr, out) } } for _, v := range userList { _, _ = RunMC( "admin", "user", "remove", defaultAlias, v.Username, ) } fatalIfError(berr, t) fatalIfError(err, t) } func validateFileLSInfo(t *testing.T, file *testFile) { if file.diskStat.Size() != int64(file.MinioLS.Size) { t.Fatalf( "File and minio object are not the same size - Object (%d) vs File (%d)", file.MinioLS.Size, file.diskStat.Size(), ) } // if file.md5Sum != file.findOutput.Etag { // t.Fatalf("File and file.findOutput do not have the same md5Sum - Object (%s) vs File (%s)", file.findOutput.Etag, file.md5Sum) // } if file.storageClass != "" { if file.storageClass != file.MinioLS.StorageClass { t.Fatalf( "File and minio object do not have the same storage class - Object (%s) vs File (%s)", file.MinioLS.StorageClass, file.storageClass, ) } } else { if file.MinioLS.StorageClass != "STANDARD" { t.Fatalf( "Minio object was expected to have storage class (STANDARD) but it was (%s)", file.MinioLS.StorageClass, ) } } } func validateObjectMetaData(t *testing.T, file *testFile) { for i, v := range file.metaData { found := false for ii, vv := range file.MinioStat.Metadata { if metaPrefix+http.CanonicalHeaderKey(i) == ii { found = true if v != vv { fmt.Println("------------------------") fmt.Println("META CHECK") fmt.Println(file.MinioStat.Metadata) fmt.Println(file.metaData) fmt.Println("------------------------") t.Fatalf("Meta values are not the same v1(%s) v2(%s)", v, vv) } } } if !found { fmt.Println("------------------------") fmt.Println("META CHECK") fmt.Println(file.MinioStat.Metadata) fmt.Println(file.metaData) fmt.Println("------------------------") t.Fatalf("Meta tag(%s) not found", i) } } } // func validateContentType(t *testing.T, file *testFile) { // value, ok := file.MinioStat.Metadata["Content-Type"] // if !ok { // t.Fatalf("File (%s) did not have a content type", file.fileNameWithPrefix) // return // } // // contentType := mime.TypeByExtension(file.extension) // if contentType != value { // log.Println(file) // log.Println(file.MinioLS) // log.Println(file.extension) // log.Println(file.MinioStat) // t.Fatalf("Content types on file (%s) do not match, extension(%s) File(%s) MinIO object(%s)", file.fileNameWithPrefix, file.extension, contentType, file.MinioStat.Metadata["Content-Type"]) // } // } func GetSource(skip int) (out string) { pc := make([]uintptr, 3) // at least 1 entry needed runtime.Callers(skip, pc) f := runtime.FuncForPC(pc[0]) file, line := f.FileLine(pc[0]) sn := strings.Split(f.Name(), ".") var name string if sn[len(sn)-1] == "func1" { name = sn[len(sn)-2] } else { name = sn[len(sn)-1] } out = file + ":" + fmt.Sprint(line) + ":" + name return } func GetMD5Sum(data []byte) string { md5Writer := md5.New() md5Writer.Write(data) return fmt.Sprintf("%x", md5Writer.Sum(nil)) } func curlFatalIfNoErrorTag(msg string, t *testing.T) { if !strings.Contains(msg, "") { fmt.Println(failIndicator) fmt.Println(msg) t.Fatal(msg) } } func fatalMsgOnly(msg string, t *testing.T) { fmt.Println(failIndicator) t.Fatal(msg) } func fatalIfNoErrorWMsg(err error, msg string, t *testing.T) { if err == nil { fmt.Println(failIndicator) fmt.Println(msg) t.Fatal(err) } } func fatalIfErrorWMsg(err error, msg string, t *testing.T) { if err != nil { fmt.Println(failIndicator) fmt.Println(msg) t.Fatal(err) } } func fatalIfError(err error, t *testing.T) { if err != nil { fmt.Println(failIndicator) t.Fatal(err) } } func parseFindJSONOutput(out string) (findList []*findMessage, err error) { findList = make([]*findMessage, 0) splitList := bytes.Split([]byte(out), []byte{10}) for _, v := range splitList { if len(v) < 1 { continue } line := new(findMessage) err = json.Unmarshal(v, line) if err != nil { return } findList = append(findList, line) } if printRawOut { fmt.Println("FIND LIST ------------------------------") for _, v := range findList { fmt.Println(v) } fmt.Println(" ------------------------------") } return } func parseDUJSONOutput(out string) (duList []duMessage, err error) { duList = make([]duMessage, 0) splitList := bytes.Split([]byte(out), []byte{10}) for _, v := range splitList { if len(v) < 1 { continue } line := duMessage{} err = json.Unmarshal(v, &line) if err != nil { return } duList = append(duList, line) } if printRawOut { fmt.Println("DU LIST ------------------------------") for _, v := range duList { fmt.Println(v) } fmt.Println(" ------------------------------") } return } func parseLSJSONOutput(out string) (lsList []contentMessage, err error) { lsList = make([]contentMessage, 0) splitList := bytes.Split([]byte(out), []byte{10}) for _, v := range splitList { if len(v) < 1 { continue } line := contentMessage{} err = json.Unmarshal(v, &line) if err != nil { return } lsList = append(lsList, line) } if printRawOut { fmt.Println("LS LIST ------------------------------") for _, v := range lsList { fmt.Println(v) } fmt.Println(" ------------------------------") } return } func parseFindSingleObjectJSONOutput(out string) (findInfo contentMessage, err error) { err = json.Unmarshal([]byte(out), &findInfo) if err != nil { return } if printRawOut { fmt.Println("FIND SINGLE OBJECT ------------------------------") fmt.Println(findInfo) fmt.Println(" ------------------------------") } return } func parseStatSingleObjectJSONOutput(out string) (stat statMessage, err error) { err = json.Unmarshal([]byte(out), &stat) if err != nil { return } if printRawOut { fmt.Println("STAT ------------------------------") fmt.Println(stat) fmt.Println(" ------------------------------") } return } // We have to wrap the error output because the console // printing mechanism for json marshals into an anonymous // object before printing, see cmd/error.go line 70 type errorMessageWrapper struct { Error errorMessage `json:"error"` Status string `json:"status"` } func validateErrorMSGValues( t *testing.T, errMSG errorMessageWrapper, TypeToValidate string, MessageToValidate string, CauseToValidate string, ) { if TypeToValidate != "" { if !strings.Contains(errMSG.Error.Type, TypeToValidate) { t.Fatalf( "Expected error.Error.Type to contain (%s) - but got (%s)", TypeToValidate, errMSG.Error.Type, ) } } if MessageToValidate != "" { if !strings.Contains(errMSG.Error.Message, MessageToValidate) { t.Fatalf( "Expected error.Error.Message to contain (%s) - but got (%s)", MessageToValidate, errMSG.Error.Message, ) } } if CauseToValidate != "" { if !strings.Contains(errMSG.Error.Cause.Message, CauseToValidate) { t.Fatalf( "Expected error.Error.Cause.Message to contain (%s) - but got (%s)", CauseToValidate, errMSG.Error.Cause.Message, ) } } } func parseUserMessageListOutput(out string) (users []*userMessage, err error) { users = make([]*userMessage, 0) splitList := bytes.Split([]byte(out), []byte{10}) for _, v := range splitList { if len(v) < 1 { continue } msg := new(userMessage) err = json.Unmarshal(v, msg) if err != nil { return } users = append(users, msg) } if printRawOut { fmt.Println("USER LIST ------------------------------") for _, v := range users { fmt.Println(v) } fmt.Println(" ------------------------------") } return } func parseShareMessageFromJSONOutput(out string) (share *shareMessage, err error) { share = new(shareMessage) err = json.Unmarshal([]byte(out), share) return } func parseSingleErrorMessageJSONOutput(out string) (errMSG errorMessageWrapper, err error) { err = json.Unmarshal([]byte(out), &errMSG) if err != nil { return } fmt.Println("ERROR ------------------------------") fmt.Println(errMSG) fmt.Println(" ------------------------------") return } func parseSingleODMessageJSONOutput(out string) (odMSG odMessage, err error) { err = json.Unmarshal([]byte(out), &odMSG) if err != nil { return } return } func parseSingleAccountStatJSONOutput(out string) (stat accountStat, err error) { err = json.Unmarshal([]byte(out), &stat) if err != nil { return } return } func parseSingleCPMessageJSONOutput(out string) (cpMSG copyMessage, err error) { err = json.Unmarshal([]byte(out), &cpMSG) if err != nil { return } return } type newTestFile struct { tag string // The tag used to identify the file inside the FileMap. This tag is also used in the objects name. prefix string // Prefix for the object name ( not including the object name itself) extension string storageClass string sizeInMBS int // uploadShouldFail bool metaData map[string]string tags map[string]string addToGlobalFileMap bool // sub directory path to place the file in // tempDir+/+subDir subDir string } type testFile struct { newTestFile // File on disk diskFile *os.File // File info on disk diskStat os.FileInfo // md5sum at the time of creation md5Sum string // File name without full path fileNameWithoutPath string // File name with assigned prefix fileNameWithPrefix string // These field are not automatically populated unless // the file is created at the initialization phase of // the test suite: testsThatDependOnOneAnother() // Minio mc stat output MinioStat statMessage // Minio mc ls output MinioLS contentMessage } func (f *testFile) String() (out string) { out = fmt.Sprintf( "Size: %d || Name: %s || md5Sum: %s", f.diskStat.Size(), f.fileNameWithoutPath, f.md5Sum, ) return } func createFile(nf newTestFile) (newTestFile *testFile) { var newFile *os.File var err error if nf.subDir != "" { err = os.MkdirAll( tempDir+string(os.PathSeparator)+nf.subDir, 0o755) if err != nil { log.Println("Could not make additional dir:", err) os.Exit(1) } newFile, err = os.CreateTemp( tempDir+string(os.PathSeparator)+nf.subDir, nf.tag+"-*"+nf.extension, ) } else { newFile, err = os.CreateTemp(tempDir, nf.tag+"-*"+nf.extension) } if err != nil { log.Println("Could not make file:", err) os.Exit(1) } md5Writer := md5.New() for i := 0; i < nf.sizeInMBS; i++ { n, err := newFile.Write(oneMBSlice[:]) mn, merr := md5Writer.Write(oneMBSlice[:]) if err != nil || merr != nil { log.Println(err) log.Println(merr) return nil } if n != len(oneMBSlice) { log.Println("Did not write 1MB to file") return nil } if mn != len(oneMBSlice) { log.Println("Did not write 1MB to md5sum writer") return nil } } splitName := strings.Split(newFile.Name(), string(os.PathSeparator)) fileNameWithoutPath := splitName[len(splitName)-1] md5sum := fmt.Sprintf("%x", md5Writer.Sum(nil)) stats, err := newFile.Stat() if err != nil { return nil } newTestFile = &testFile{ md5Sum: md5sum, fileNameWithoutPath: fileNameWithoutPath, diskFile: newFile, diskStat: stats, } newTestFile.tag = nf.tag newTestFile.metaData = nf.metaData newTestFile.storageClass = nf.storageClass newTestFile.sizeInMBS = nf.sizeInMBS newTestFile.tags = nf.tags newTestFile.prefix = nf.prefix newTestFile.extension = nf.extension if nf.prefix != "" { newTestFile.fileNameWithPrefix = nf.prefix + "/" + fileNameWithoutPath } else { newTestFile.fileNameWithPrefix = fileNameWithoutPath } if nf.addToGlobalFileMap { fileMap[nf.tag] = newTestFile } return newTestFile } func BuildCLI() error { wd, _ := os.Getwd() fmt.Println("WORKING DIR:", wd) fmt.Println("go build -o", mcCmd, buildPath) os.Remove(mcCmd) out, err := exec.Command("go", "build", "-o", mcCmd, buildPath).CombinedOutput() if err != nil { log.Println("BUILD OUT:", string(out)) log.Println(err) panic(err) } err = os.Chmod(mcCmd, 0o777) if err != nil { panic(err) } return nil } func RunMC(parameters ...string) (out string, err error) { var outBytes []byte var outErr error fmt.Println("") fmt.Println(time.Now().Format("2006-01-02T15:04:05.000"), "||", GetSource(3)) fmt.Println(mcCmd, strings.Join(preCmdParameters, " "), strings.Join(parameters, " ")) outBytes, outErr = exec.Command(mcCmd, append(preCmdParameters, parameters...)...).CombinedOutput() if printRawOut { fmt.Println(string(outBytes)) } out = string(outBytes) err = outErr return } func RunCommand(cmd string, parameters ...string) (out string, err error) { fmt.Println("") fmt.Println(time.Now().Format("2006-01-02T15:04:05.000"), "||", GetSource(3)) fmt.Println(cmd, strings.Join(parameters, " ")) var outBytes []byte var outErr error outBytes, outErr = exec.Command(cmd, parameters...).CombinedOutput() if printRawOut { fmt.Println(string(outBytes)) } out = string(outBytes) err = outErr return } minio-client-0.0~20250403/cmd/support-callhome.go000066400000000000000000000070021477450377600213710ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var supportCallhomeCmd = cli.Command{ Name: "callhome", Usage: "configure callhome settings", OnUsageError: onUsageError, Action: mainCallhome, Before: setGlobalsFromContext, Flags: supportGlobalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} enable|disable|status ALIAS OPTIONS: enable - Enable callhome disable - Disable callhome status - Display callhome settings FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable callhome for cluster with alias 'myminio' {{.Prompt}} {{.HelpName}} enable myminio 2. Disable callhome for cluster with alias 'myminio' {{.Prompt}} {{.HelpName}} disable myminio 3. Check callhome status for cluster with alias 'myminio' {{.Prompt}} {{.HelpName}} status myminio `, } type supportCallhomeMessage struct { Status string `json:"status"` } // String colorized callhome command output message. func (s supportCallhomeMessage) String() string { return console.Colorize(supportSuccessMsgTag, "Callhome is "+s.Status) } // JSON jsonified callhome command output message. func (s supportCallhomeMessage) JSON() string { jsonBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonBytes) } func isDiagCallhomeEnabled(alias string) bool { return isFeatureEnabled(alias, "callhome", madmin.Default) } func mainCallhome(ctx *cli.Context) error { initLicInfoColors() setSuccessMessageColor() alias, arg := checkToggleCmdSyntax(ctx) validateClusterRegistered(alias, false) if arg == "status" { printCallhomeStatus(alias) return nil } toggleCallhome(alias, arg) return nil } func printCallhomeStatus(alias string) { resultMsg := supportCallhomeMessage{Status: featureStatusStr(isDiagCallhomeEnabled(alias))} printMsg(resultMsg) } func toggleCallhome(alias, arg string) { enable := arg == "enable" setCallhomeConfig(alias, enable) resultMsg := supportCallhomeMessage{Status: featureStatusStr(enable)} printMsg(resultMsg) } func setCallhomeConfig(alias string, enableCallhome bool) { // Create a new MinIO Admin Client client, err := newAdminClient(alias) fatalIf(err, "Unable to initialize admin connection.") if !minioConfigSupportsSubSys(client, "callhome") { fatal(errDummy().Trace(), "Your version of MinIO doesn't support this configuration") } enableStr := "off" if enableCallhome { enableStr = "on" } configStr := "callhome enable=" + enableStr _, e := client.SetConfigKV(globalContext, configStr) fatalIf(probe.NewError(e), "Unable to set callhome config on minio") } minio-client-0.0~20250403/cmd/support-diag-spinner-v3.go000066400000000000000000000066451477450377600225270ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // # This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "errors" "fmt" "io" "sync" "time" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/vbauerster/mpb/v8" "github.com/vbauerster/mpb/v8/decor" ) func receiveHealthInfo(decoder *json.Decoder) (info madmin.HealthInfo, e error) { var wg sync.WaitGroup pg := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithWidth(16)) spinnerStyle := mpb.SpinnerStyle().Meta(func(s string) string { return infoText(s) }).PositionLeft() type progressSpinner struct { name string spinner *mpb.Bar cond func(madmin.HealthInfo) bool } spinners := []progressSpinner{} createSpinner := func(name string, cond func(madmin.HealthInfo) bool) { caption := fmt.Sprintf("%s %s ...", dot, name) wip := mpb.PrependDecorators(decor.Name(greenText(caption))) done := mpb.BarFillerOnComplete(greenText(check)) spinner := pg.New(1, spinnerStyle, wip, done) spinners = append(spinners, progressSpinner{ name: name, cond: cond, spinner: spinner, }) } createSpinner("CPU Info", func(info madmin.HealthInfo) bool { return len(info.Sys.CPUInfo) > 0 }) createSpinner("Disk Info", func(info madmin.HealthInfo) bool { return len(info.Sys.Partitions) > 0 }) createSpinner("Net Info", func(info madmin.HealthInfo) bool { return len(info.Sys.NetInfo) > 0 }) createSpinner("OS Info", func(info madmin.HealthInfo) bool { return len(info.Sys.OSInfo) > 0 }) createSpinner("Mem Info", func(info madmin.HealthInfo) bool { return len(info.Sys.MemInfo) > 0 }) createSpinner("Process Info", func(info madmin.HealthInfo) bool { return len(info.Sys.ProcInfo) > 0 }) createSpinner("Server Config", func(info madmin.HealthInfo) bool { return info.Minio.Config.Config != nil }) createSpinner("System Errors", func(info madmin.HealthInfo) bool { return len(info.Sys.SysErrs) > 0 }) createSpinner("System Services", func(info madmin.HealthInfo) bool { return len(info.Sys.SysServices) > 0 }) createSpinner("System Config", func(info madmin.HealthInfo) bool { return len(info.Sys.SysConfig) > 0 }) createSpinner("Admin Info", func(info madmin.HealthInfo) bool { return len(info.Minio.Info.Servers) > 0 }) wg.Add(len(spinners)) start := time.Now() markDone := func(bar *mpb.Bar) { if bar.Current() == 0 { bar.EwmaIncrement(time.Since(start)) wg.Done() } } receivedLast := false progress := func(info madmin.HealthInfo) { receivedLast = len(info.Minio.Info.Servers) > 0 for _, bar := range spinners { if receivedLast || bar.cond(info) { markDone(bar.spinner) } } } go func() { for { if e = decoder.Decode(&info); e != nil { if errors.Is(e, io.EOF) { e = nil } break } progress(info) } }() pg.Wait() return } minio-client-0.0~20250403/cmd/support-diag.go000066400000000000000000000346131477450377600205210ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" gojson "encoding/json" "errors" "flag" "fmt" "io" "os" "path/filepath" "strings" "syscall" "time" "github.com/fatih/color" "github.com/klauspost/compress/gzip" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) const ( anonymizeFlag = "anonymize" anonymizeStandard = "standard" anonymizeStrict = "strict" ) var supportDiagFlags = append([]cli.Flag{ HealthDataTypeFlag{ Name: "test", Usage: "choose specific diagnostics to run [" + options.String() + "]", Value: nil, Hidden: true, }, cli.DurationFlag{ Name: "deadline", Usage: "maximum duration diagnostics should be allowed to run", Value: 1 * time.Hour, Hidden: true, }, cli.StringFlag{ Name: anonymizeFlag, Usage: "Data anonymization mode (standard|strict)", Value: anonymizeStandard, }, }, subnetCommonFlags...) var supportDiagCmd = cli.Command{ Name: "diag", Aliases: []string{"diagnostics"}, Usage: "upload health data for diagnostics", OnUsageError: onUsageError, Action: mainSupportDiag, Before: setGlobalsFromContext, Flags: supportDiagFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Upload MinIO diagnostics report for cluster with alias 'myminio' to SUBNET {{.Prompt}} {{.HelpName}} myminio 2. Generate MinIO diagnostics report for cluster with alias 'myminio', save and upload to SUBNET manually {{.Prompt}} {{.HelpName}} myminio --airgap 3. Upload MinIO diagnostics report for cluster with alias 'myminio' to SUBNET, with strict anonymization {{.Prompt}} {{.HelpName}} myminio --anonymize=strict `, } type supportDiagMessage struct { Status string `json:"status"` } // String colorized status message func (s supportDiagMessage) String() string { return console.Colorize(supportSuccessMsgTag, "MinIO diagnostics report was successfully uploaded to SUBNET.") } // JSON jsonified status message func (s supportDiagMessage) JSON() string { s.Status = "success" return toJSON(s) } // checkSupportDiagSyntax - validate arguments passed by a user func checkSupportDiagSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } anon := ctx.String(anonymizeFlag) if anon != anonymizeStandard && anon != anonymizeStrict { fatal(errDummy().Trace(), "Invalid anonymization mode. Valid options are 'standard' or 'strict'.") } } // compress and tar MinIO diagnostics output func tarGZ(healthInfo interface{}, version, filename string) error { data, e := TarGZHealthInfo(healthInfo, version) if e != nil { return e } e = os.WriteFile(filename, data, 0o666) if e != nil { return e } if globalAirgapped { warningMsgBoundary := "*********************************************************************************" warning := warnText(" WARNING!!") warningContents := infoText(` ** THIS FILE MAY CONTAIN SENSITIVE INFORMATION ABOUT YOUR ENVIRONMENT ** ** PLEASE INSPECT CONTENTS BEFORE SHARING IT ON ANY PUBLIC FORUM **`) warningMsgHeader := infoText(warningMsgBoundary) warningMsgTrailer := infoText(warningMsgBoundary) console.Printf("%s\n%s\n%s\n%s\n", warningMsgHeader, warning, warningContents, warningMsgTrailer) console.Infoln("MinIO diagnostics report saved at ", filename) } return nil } // TarGZHealthInfo - compress and tar MinIO diagnostics output func TarGZHealthInfo(healthInfo interface{}, version string) ([]byte, error) { buffer := bytes.NewBuffer(nil) gzWriter := gzip.NewWriter(buffer) enc := gojson.NewEncoder(gzWriter) header := struct { Version string `json:"version"` }{Version: version} if e := enc.Encode(header); e != nil { return nil, e } if e := enc.Encode(healthInfo); e != nil { return nil, e } if e := gzWriter.Close(); e != nil { return nil, e } return buffer.Bytes(), nil } func infoText(s string) string { console.SetColor("INFO", color.New(color.FgGreen, color.Bold)) return console.Colorize("INFO", s) } func greenText(s string) string { console.SetColor("GREEN", color.New(color.FgGreen)) return console.Colorize("GREEN", s) } func warnText(s string) string { console.SetColor("WARN", color.New(color.FgRed, color.Bold)) return console.Colorize("WARN", s) } func mainSupportDiag(ctx *cli.Context) error { checkSupportDiagSyntax(ctx) // Get the alias parameter from cli aliasedURL := ctx.Args().Get(0) alias, apiKey := initSubnetConnectivity(ctx, aliasedURL, true) if len(apiKey) == 0 { // api key not passed as flag. Check that the cluster is registered. apiKey = validateClusterRegistered(alias, true) } // Create a new MinIO Admin Client client := getClient(aliasedURL) // Main execution execSupportDiag(ctx, client, alias, apiKey) return nil } func execSupportDiag(ctx *cli.Context, client *madmin.AdminClient, alias, apiKey string) { var reqURL string var headers map[string]string setSuccessMessageColor() filename := fmt.Sprintf("%s-health_%s.json.gz", filepath.Clean(alias), UTCNow().Format("20060102150405")) if !globalAirgapped { // Retrieve subnet credentials (login/license) beforehand as // it can take a long time to fetch the health information uploadURL := SubnetUploadURL("health") reqURL, headers = prepareSubnetUploadURL(uploadURL, alias, apiKey) } healthInfo, version, e := fetchServerDiagInfo(ctx, client) fatalIf(probe.NewError(e), "Unable to fetch health information.") if globalJSON && globalAirgapped { switch version { case madmin.HealthInfoVersion0: printMsg(healthInfo.(madmin.HealthInfoV0)) case madmin.HealthInfoVersion2: printMsg(healthInfo.(madmin.HealthInfoV2)) case madmin.HealthInfoVersion: printMsg(healthInfo.(madmin.HealthInfo)) } return } e = tarGZ(healthInfo, version, filename) fatalIf(probe.NewError(e), "Unable to save MinIO diagnostics report") if !globalAirgapped { _, e = (&SubnetFileUploader{ alias: alias, FilePath: filename, ReqURL: reqURL, Headers: headers, DeleteAfterUpload: true, }).UploadFileToSubnet() fatalIf(probe.NewError(e), "Unable to upload MinIO diagnostics report to SUBNET portal") printMsg(supportDiagMessage{}) } } func fetchServerDiagInfo(ctx *cli.Context, client *madmin.AdminClient) (interface{}, string, error) { opts := GetHealthDataTypeSlice(ctx, "test") if len(*opts) == 0 { opts = &options } optsMap := make(map[madmin.HealthDataType]struct{}) for _, opt := range *opts { optsMap[opt] = struct{}{} } spinners := []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"} cont, cancel := context.WithCancel(globalContext) defer cancel() startSpinner := func(s string) func() { ctx, cancel := context.WithCancel(cont) printText := func(t, sp string, rewind int) { console.RewindLines(rewind) dot := infoText(dot) t = fmt.Sprintf("%s ...", t) t = greenText(t) sp = infoText(sp) toPrint := fmt.Sprintf("%s %s %s ", dot, t, sp) console.Printf("%s\n", toPrint) } i := 0 sp := func() string { i = i + 1 i = i % len(spinners) return spinners[i] } done := make(chan bool) doneToggle := false go func() { printText(s, sp(), 0) for { time.Sleep(500 * time.Millisecond) // 2 fps if ctx.Err() != nil { printText(s, check, 1) done <- true return } printText(s, sp(), 1) } }() return func() { cancel() if !doneToggle { <-done os.Stdout.Sync() doneToggle = true } } } spinner := func(resource string, opt madmin.HealthDataType) func(bool) bool { var spinStopper func() done := false _, ok := optsMap[opt] // check if option is enabled if globalJSON || !ok { return func(bool) bool { return true } } return func(cond bool) bool { if done { return done } if spinStopper == nil { spinStopper = startSpinner(resource) } if cond { done = true spinStopper() } return done } } admin := spinner("Admin Info", madmin.HealthDataTypeMinioInfo) cpu := spinner("CPU Info", madmin.HealthDataTypeSysCPU) diskHw := spinner("Disk Info", madmin.HealthDataTypeSysDriveHw) osInfo := spinner("OS Info", madmin.HealthDataTypeSysOsInfo) mem := spinner("Mem Info", madmin.HealthDataTypeSysMem) process := spinner("Process Info", madmin.HealthDataTypeSysProcess) config := spinner("Server Config", madmin.HealthDataTypeMinioConfig) syserr := spinner("System Errors", madmin.HealthDataTypeSysErrors) syssrv := spinner("System Services", madmin.HealthDataTypeSysServices) sysconfig := spinner("System Config", madmin.HealthDataTypeSysConfig) progressV0 := func(info madmin.HealthInfoV0) { _ = admin(len(info.Minio.Info.Servers) > 0) && cpu(len(info.Sys.CPUInfo) > 0) && diskHw(len(info.Sys.DiskHwInfo) > 0) && osInfo(len(info.Sys.OsInfo) > 0) && mem(len(info.Sys.MemInfo) > 0) && process(len(info.Sys.ProcInfo) > 0) && config(info.Minio.Config != nil) } progressV2 := func(info madmin.HealthInfoV2) { _ = cpu(len(info.Sys.CPUInfo) > 0) && diskHw(len(info.Sys.Partitions) > 0) && osInfo(len(info.Sys.OSInfo) > 0) && mem(len(info.Sys.MemInfo) > 0) && process(len(info.Sys.ProcInfo) > 0) && config(info.Minio.Config.Config != nil) && syserr(len(info.Sys.SysErrs) > 0) && syssrv(len(info.Sys.SysServices) > 0) && sysconfig(len(info.Sys.SysConfig) > 0) && admin(len(info.Minio.Info.Servers) > 0) } // Fetch info of all servers (cluster or single server) resp, version, e := client.ServerHealthInfo(cont, *opts, ctx.Duration("deadline"), ctx.String(anonymizeFlag)) if e != nil { cancel() return nil, "", e } var healthInfo interface{} decoder := json.NewDecoder(resp.Body) switch version { case madmin.HealthInfoVersion0: info := madmin.HealthInfoV0{} for { if e = decoder.Decode(&info); e != nil { if errors.Is(e, io.EOF) { e = nil } break } progressV0(info) } // Old minio versions don't return the MinIO info in // response of the healthinfo api. So fetch it separately minioInfo, e := client.ServerInfo(globalContext) if e != nil { info.Minio.Error = e.Error() } else { info.Minio.Info = minioInfo } healthInfo = MapHealthInfoToV1(info, nil) version = madmin.HealthInfoVersion1 case madmin.HealthInfoVersion2: info := madmin.HealthInfoV2{} for { if e = decoder.Decode(&info); e != nil { if errors.Is(e, io.EOF) { e = nil } break } progressV2(info) } healthInfo = info case madmin.HealthInfoVersion: healthInfo, e = receiveHealthInfo(decoder) } // cancel the context if supportDiagChan has returned. cancel() return healthInfo, version, e } // HealthDataTypeSlice is a typed list of health tests type HealthDataTypeSlice []madmin.HealthDataType // Set - sets the flag to the given value func (d *HealthDataTypeSlice) Set(value string) error { for _, v := range strings.Split(value, ",") { if supportDiagData, ok := madmin.HealthDataTypesMap[strings.Trim(v, " ")]; ok { *d = append(*d, supportDiagData) } else { return fmt.Errorf("valid options include %s", options.String()) } } return nil } // String - returns the string representation of the health datatypes func (d *HealthDataTypeSlice) String() string { val := "" for _, supportDiagData := range *d { formatStr := "%s" if val != "" { formatStr = fmt.Sprintf("%s,%%s", formatStr) } else { formatStr = fmt.Sprintf("%s%%s", formatStr) } val = fmt.Sprintf(formatStr, val, string(supportDiagData)) } return val } // Value - returns the value func (d *HealthDataTypeSlice) Value() []madmin.HealthDataType { return *d } // Get - returns the value func (d *HealthDataTypeSlice) Get() interface{} { return *d } // HealthDataTypeFlag is a typed flag to represent health datatypes type HealthDataTypeFlag struct { Name string Usage string EnvVar string Hidden bool Value *HealthDataTypeSlice } // String - returns the string to be shown in the help message func (f HealthDataTypeFlag) String() string { return cli.FlagStringer(f) } // GetName - returns the name of the flag func (f HealthDataTypeFlag) GetName() string { return f.Name } // GetHealthDataTypeSlice - returns the list of set health tests func GetHealthDataTypeSlice(c *cli.Context, name string) *HealthDataTypeSlice { generic := c.Generic(name) if generic == nil { return nil } return generic.(*HealthDataTypeSlice) } // GetGlobalHealthDataTypeSlice - returns the list of set health tests set globally func GetGlobalHealthDataTypeSlice(c *cli.Context, name string) *HealthDataTypeSlice { generic := c.GlobalGeneric(name) if generic == nil { return nil } return generic.(*HealthDataTypeSlice) } // Apply - applies the flag func (f HealthDataTypeFlag) Apply(set *flag.FlagSet) { f.ApplyWithError(set) } // ApplyWithError - applies with error func (f HealthDataTypeFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) if envVal, ok := syscall.Getenv(envVar); ok { newVal := &HealthDataTypeSlice{} for _, s := range strings.Split(envVal, ",") { s = strings.TrimSpace(s) if e := newVal.Set(s); e != nil { return fmt.Errorf("could not parse %s as health datatype value for flag %s: %s", envVal, f.Name, e) } } f.Value = newVal break } } } for _, name := range strings.Split(f.Name, ",") { name = strings.Trim(name, " ") if f.Value == nil { f.Value = &HealthDataTypeSlice{} } set.Var(f.Value, name, f.Usage) } return nil } var options = HealthDataTypeSlice(madmin.HealthDataTypesList) minio-client-0.0~20250403/cmd/support-inspect.go000066400000000000000000000220351477450377600212550ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "encoding/base64" "encoding/binary" "encoding/hex" "errors" "fmt" "hash/crc32" "io" "os" "path/filepath" "runtime" "strings" "sync" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3/estream" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) const ( defaultPublicKey = "MIIBCgKCAQEAs/128UFS9A8YSJY1XqYKt06dLVQQCGDee69T+0Tip/1jGAB4z0/3QMpH0MiS8Wjs4BRWV51qvkfAHzwwdU7y6jxU05ctb/H/WzRj3FYdhhHKdzear9TLJftlTs+xwj2XaADjbLXCV1jGLS889A7f7z5DgABlVZMQd9BjVAR8ED3xRJ2/ZCNuQVJ+A8r7TYPGMY3wWvhhPgPk3Lx4WDZxDiDNlFs4GQSaESSsiVTb9vyGe/94CsCTM6Cw9QG6ifHKCa/rFszPYdKCabAfHcS3eTr0GM+TThSsxO7KfuscbmLJkfQev1srfL2Ii2RbnysqIJVWKEwdW05ID8ryPkuTuwIDAQAB" ) var supportInspectFlags = append(subnetCommonFlags, cli.BoolFlag{ Name: "legacy", Usage: "use the older inspect format", }, ) var supportInspectCmd = cli.Command{ Name: "inspect", Usage: "upload raw object contents for analysis", Action: mainSupportInspect, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: supportInspectFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Upload 'xl.meta' of a specific object from all the drives {{.Prompt}} {{.HelpName}} myminio/bucket/test*/xl.meta 2. Upload recursively all objects at a prefix. NOTE: This can be an expensive operation use it with caution. {{.Prompt}} {{.HelpName}} myminio/bucket/test/** 3. Download 'xl.meta' of a specific object from all the drives locally, and upload to SUBNET manually {{.Prompt}} {{.HelpName}} myminio/bucket/test*/xl.meta --airgap `, } type inspectMessage struct { Status string `json:"status"` AliasedURL string `json:"aliasedURL,omitempty"` File string `json:"file,omitempty"` Key string `json:"key,omitempty"` } // Colorized message for console printing. func (t inspectMessage) String() string { var msg string if globalAirgapped || t.File != "" { if t.Key == "" { msg = fmt.Sprintf("File data successfully downloaded as %s", console.Colorize("File", t.File)) } else { msg = fmt.Sprintf("Encrypted file data successfully downloaded as %s\n", console.Colorize("File", t.File)) msg += fmt.Sprintf("Decryption key: %s\n\n", console.Colorize("Key", t.Key)) msg += "The decryption key will ONLY be shown here. It cannot be recovered.\n" msg += "The encrypted file can safely be shared without the decryption key.\n" msg += "Even with the decryption key, data stored with encryption cannot be accessed." } } else { msg = fmt.Sprintf("Object inspection data for '%s' uploaded to SUBNET successfully", t.AliasedURL) } return console.Colorize(supportSuccessMsgTag, msg) } func (t inspectMessage) JSON() string { t.Status = "success" jsonMessageBytes, e := json.MarshalIndent(t, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func checkSupportInspectSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainSupportInspect - the entry function of inspect command func mainSupportInspect(ctx *cli.Context) error { // Check for command syntax checkSupportInspectSyntax(ctx) setSuccessMessageColor() // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) alias, apiKey := initSubnetConnectivity(ctx, aliasedURL, true) if len(apiKey) == 0 { // api key not passed as flag. Check that the cluster is registered. apiKey = validateClusterRegistered(alias, true) } console.SetColor("File", color.New(color.FgWhite, color.Bold)) console.SetColor("Key", color.New(color.FgHiRed, color.Bold)) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } // Compute bucket and object from the aliased URL aliasedURL = filepath.ToSlash(aliasedURL) splits := splitStr(aliasedURL, "/", 3) bucket, prefix := splits[1], splits[2] shellName, _ := getShellName() if runtime.GOOS != "windows" && shellName != "bash" && strings.Contains(prefix, "*") { console.Infoln("Your shell is auto determined as '" + shellName + "', wildcard patterns are only supported with 'bash' SHELL.") } var publicKey []byte if !ctx.Bool("legacy") { var e error publicKey, e = os.ReadFile(filepath.Join(mustGetMcConfigDir(), "support_public.pem")) if e != nil && !os.IsNotExist(e) { fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to inspect file.") } else if len(publicKey) > 0 { if !globalJSON && !globalQuiet { console.Infoln("Using public key from ", filepath.Join(mustGetMcConfigDir(), "support_public.pem")) } } // Fall back to MinIO public key. if len(publicKey) == 0 { // Public key for MinIO confidential information. publicKey, _ = base64.StdEncoding.DecodeString(defaultPublicKey) } } key, r, e := client.Inspect(context.Background(), madmin.InspectOptions{ Volume: bucket, File: prefix, PublicKey: publicKey, }) fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to inspect file.") // Download the inspect data in a temporary file first tmpFile, e := os.CreateTemp("", "mc-inspect-") fatalIf(probe.NewError(e), "Unable to download file data.") copied := false // Validate stream, report errors on stream. if len(key) == 0 { pr, pw := io.Pipe() copied = true var aErr error var wg sync.WaitGroup wg.Add(1) // Validate stream... go func() { defer wg.Done() er, err := estream.NewReader(pr) if err != nil { // Ignore if header is non-parsable... io.Copy(io.Discard, pr) return } // We don't care about decrypting at this point. er.SkipEncrypted(true) for { var st *estream.Stream st, aErr = er.NextStream() if errors.Is(aErr, io.EOF) { aErr = nil pr.CloseWithError(nil) return } if aErr != nil { pr.CloseWithError(aErr) return } aErr = st.Skip() if aErr != nil { fmt.Println("Skip:", aErr) pr.CloseWithError(aErr) return } } }() _, e = io.Copy(io.MultiWriter(tmpFile, pw), r) pw.CloseWithError(e) wg.Wait() if e == nil && aErr != nil { e = aErr } } if !copied { _, e = io.Copy(tmpFile, r) } fatalIf(probe.NewError(e), "Unable to download file data.") r.Close() tmpFile.Close() wantFileName := "inspect-" + conservativeFileName(strings.Join(splits, "_")) + ".enc" if globalAirgapped { saveInspectDataFile(wantFileName, key, tmpFile) return nil } uploadURL := SubnetUploadURL("inspect") reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey) tmpFileName := tmpFile.Name() _, e = (&SubnetFileUploader{ alias: alias, FilePath: tmpFileName, filename: wantFileName, ReqURL: reqURL, Headers: headers, DeleteAfterUpload: true, }).UploadFileToSubnet() if e != nil { console.Errorln("Unable to upload inspect data to SUBNET portal: " + e.Error()) saveInspectDataFile(wantFileName, key, tmpFile) return nil } printMsg(inspectMessage{AliasedURL: aliasedURL}) return nil } func saveInspectDataFile(dstFileName string, key []byte, tmpFile *os.File) { var keyHex string // Choose a name and move the inspect data to its final destination if key != nil { // Create an id that is also crc. var id [4]byte binary.LittleEndian.PutUint32(id[:], crc32.ChecksumIEEE(key[:])) // We use 4 bytes of the 32 bytes to identify they file. dstFileName = fmt.Sprintf("inspect-data.%s.enc", hex.EncodeToString(id[:])) keyHex = hex.EncodeToString(id[:]) + hex.EncodeToString(key[:]) } fi, e := os.Stat(dstFileName) if e == nil && !fi.IsDir() { e = moveFile(dstFileName, dstFileName+"."+time.Now().Format(dateTimeFormatFilename)) fatalIf(probe.NewError(e), "Unable to create a backup of "+dstFileName) } else { if !os.IsNotExist(e) { fatal(probe.NewError(e), "Unable to download file data") } } fatalIf(probe.NewError(moveFile(tmpFile.Name(), dstFileName)), "Unable to rename downloaded data, file exists at %s", tmpFile.Name()) printMsg(inspectMessage{ File: dstFileName, Key: keyHex, }) } minio-client-0.0~20250403/cmd/support-perf-client.go000066400000000000000000000056221477450377600220230ustar00rootroot00000000000000// Copyright (c) 2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "os" "time" tea "github.com/charmbracelet/bubbletea" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) func mainAdminSpeedTestClientPerf(ctx *cli.Context, aliasedURL string, outCh chan<- PerfTestResult) error { client, perr := newAdminClient(aliasedURL) if perr != nil { fatalIf(perr.Trace(aliasedURL), "Unable to initialize admin client.") return nil } ctxt, cancel := context.WithCancel(globalContext) defer cancel() duration, e := time.ParseDuration(ctx.String("duration")) if e != nil { fatalIf(probe.NewError(e), "Unable to parse duration") return nil } if duration <= 0 { fatalIf(errInvalidArgument(), "duration cannot be 0 or negative") return nil } resultCh := make(chan madmin.ClientPerfResult) errorCh := make(chan error) go func() { defer close(resultCh) defer close(errorCh) result, e := client.ClientPerf(ctxt, duration) if e != nil { errorCh <- e } resultCh <- result }() if globalJSON { var rsl PerfTestOutput select { case e := <-errorCh: rsl = convertPerfResult(PerfTestResult{ Type: ClientPerfTest, Err: e.Error(), Final: true, }) case result := <-resultCh: rsl = convertPerfResult(PerfTestResult{ Type: ClientPerfTest, ClientResult: &result, Final: true, }) } printMsg(rsl) return nil } done := make(chan struct{}) p := tea.NewProgram(initSpeedTestUI()) go func() { if _, e := p.Run(); e != nil { os.Exit(1) } close(done) }() go func() { for { select { case e := <-errorCh: r := PerfTestResult{ Type: ClientPerfTest, Err: e.Error(), Final: true, } p.Send(r) if outCh != nil { outCh <- r } return case result := <-resultCh: r := PerfTestResult{ Type: ClientPerfTest, ClientResult: &result, Final: true, } p.Send(r) if outCh != nil { outCh <- r } return default: p.Send(PerfTestResult{ Type: ClientPerfTest, ClientResult: &madmin.ClientPerfResult{}, }) time.Sleep(100 * time.Millisecond) } } }() <-done return nil } minio-client-0.0~20250403/cmd/support-perf-drive.go000066400000000000000000000062551477450377600216610ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "os" tea "github.com/charmbracelet/bubbletea" "github.com/dustin/go-humanize" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) func mainAdminSpeedTestDrive(ctx *cli.Context, aliasedURL string, outCh chan<- PerfTestResult) error { client, perr := newAdminClient(aliasedURL) if perr != nil { fatalIf(perr.Trace(aliasedURL), "Unable to initialize admin client.") return nil } ctxt, cancel := context.WithCancel(globalContext) defer cancel() blocksize, e := humanize.ParseBytes(ctx.String("blocksize")) if e != nil { fatalIf(probe.NewError(e), "Unable to parse blocksize") return nil } if blocksize <= 0 { fatalIf(errInvalidArgument(), "blocksize cannot be <= 0") return nil } filesize, e := humanize.ParseBytes(ctx.String("filesize")) if e != nil { fatalIf(probe.NewError(e), "Unable to parse filesize") return nil } if filesize <= 0 { fatalIf(errInvalidArgument(), "filesize cannot be <= 0") return nil } serial := ctx.Bool("serial") resultCh, e := client.DriveSpeedtest(ctxt, madmin.DriveSpeedTestOpts{ Serial: serial, BlockSize: blocksize, FileSize: filesize, }) if globalJSON { if e != nil { printMsg(convertPerfResult(PerfTestResult{ Type: DrivePerfTest, Err: e.Error(), Final: true, })) return nil } var results []madmin.DriveSpeedTestResult for result := range resultCh { if result.Version != "" { results = append(results, result) } } printMsg(convertPerfResult(PerfTestResult{ Type: DrivePerfTest, DriveResult: results, Final: true, })) return nil } done := make(chan struct{}) p := tea.NewProgram(initSpeedTestUI()) go func() { if _, e := p.Run(); e != nil { os.Exit(1) } close(done) }() go func() { if e != nil { r := PerfTestResult{ Type: DrivePerfTest, Err: e.Error(), Final: true, } p.Send(r) if outCh != nil { outCh <- r } return } var results []madmin.DriveSpeedTestResult for result := range resultCh { if result.Version != "" { results = append(results, result) } else { p.Send(PerfTestResult{ Type: DrivePerfTest, DriveResult: []madmin.DriveSpeedTestResult{}, }) } } r := PerfTestResult{ Type: DrivePerfTest, DriveResult: results, Final: true, } p.Send(r) if outCh != nil { outCh <- r } }() <-done return nil } minio-client-0.0~20250403/cmd/support-perf-net.go000066400000000000000000000055001477450377600213260ustar00rootroot00000000000000// Copyright (c) 2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "os" "time" tea "github.com/charmbracelet/bubbletea" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) func mainAdminSpeedTestNetperf(ctx *cli.Context, aliasedURL string, outCh chan<- PerfTestResult) error { client, perr := newAdminClient(aliasedURL) if perr != nil { fatalIf(perr.Trace(aliasedURL), "Unable to initialize admin client.") return nil } ctxt, cancel := context.WithCancel(globalContext) defer cancel() duration, e := time.ParseDuration(ctx.String("duration")) if e != nil { fatalIf(probe.NewError(e), "Unable to parse duration") return nil } if duration <= 0 { fatalIf(errInvalidArgument(), "duration cannot be 0 or negative") return nil } resultCh := make(chan madmin.NetperfResult) errorCh := make(chan error) go func() { defer close(resultCh) defer close(errorCh) result, e := client.Netperf(ctxt, duration) if e != nil { errorCh <- e } resultCh <- result }() if globalJSON { select { case e := <-errorCh: printMsg(convertPerfResult(PerfTestResult{ Type: NetPerfTest, Err: e.Error(), Final: true, })) case result := <-resultCh: printMsg(convertPerfResult(PerfTestResult{ Type: NetPerfTest, NetResult: &result, Final: true, })) } return nil } done := make(chan struct{}) p := tea.NewProgram(initSpeedTestUI()) go func() { if _, e := p.Run(); e != nil { os.Exit(1) } close(done) }() go func() { for { select { case e := <-errorCh: r := PerfTestResult{ Type: NetPerfTest, Err: e.Error(), Final: true, } p.Send(r) if outCh != nil { outCh <- r } return case result := <-resultCh: r := PerfTestResult{ Type: NetPerfTest, NetResult: &result, Final: true, } p.Send(r) if outCh != nil { outCh <- r } return default: p.Send(PerfTestResult{ Type: NetPerfTest, NetResult: &madmin.NetperfResult{}, }) time.Sleep(100 * time.Millisecond) } } }() <-done return nil } minio-client-0.0~20250403/cmd/support-perf-object.go000066400000000000000000000076461477450377600220230ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "os" "time" tea "github.com/charmbracelet/bubbletea" "github.com/dustin/go-humanize" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var adminSpeedtestCmd = cli.Command{ Name: "speedtest", Usage: "Run server side speed test", Action: mainAdminSpeedtest, OnUsageError: onUsageError, Before: setGlobalsFromContext, HideHelpCommand: true, Hidden: true, CustomHelpTemplate: "Please use 'mc support perf'", } func mainAdminSpeedtest(_ *cli.Context) error { deprecatedError("mc support perf") return nil } func mainAdminSpeedTestObject(ctx *cli.Context, aliasedURL string, outCh chan<- PerfTestResult) error { client, perr := newAdminClient(aliasedURL) if perr != nil { fatalIf(perr.Trace(aliasedURL), "Unable to initialize admin client.") return nil } ctxt, cancel := context.WithCancel(globalContext) defer cancel() duration, e := time.ParseDuration(ctx.String("duration")) if e != nil { fatalIf(probe.NewError(e), "Unable to parse duration") return nil } if duration <= 0 { fatalIf(errInvalidArgument(), "duration cannot be 0 or negative") return nil } size, e := humanize.ParseBytes(ctx.String("size")) if e != nil { fatalIf(probe.NewError(e), "Unable to parse object size") return nil } if size <= 0 { fatalIf(errInvalidArgument(), "size is expected to be more than 0 bytes") return nil } concurrent := ctx.Int("concurrent") if concurrent <= 0 { fatalIf(errInvalidArgument(), "concurrency cannot be '0' or negative") return nil } globalPerfTestVerbose = ctx.Bool("verbose") // Turn-off autotuning only when "concurrent" is specified // in all other scenarios keep auto-tuning on. autotune := !ctx.IsSet("concurrent") resultCh, e := client.Speedtest(ctxt, madmin.SpeedtestOpts{ Size: int(size), Duration: duration, Concurrency: concurrent, Autotune: autotune, Bucket: ctx.String("bucket"), // This is a hidden flag. NoClear: ctx.Bool("noclear"), }) if globalJSON { if e != nil { printMsg(convertPerfResult(PerfTestResult{ Type: ObjectPerfTest, Err: e.Error(), Final: true, })) return nil } var result madmin.SpeedTestResult for result = range resultCh { if result.Version == "" { continue } } printMsg(convertPerfResult(PerfTestResult{ Type: ObjectPerfTest, ObjectResult: &result, Final: true, })) return nil } done := make(chan struct{}) p := tea.NewProgram(initSpeedTestUI()) go func() { if _, e := p.Run(); e != nil { os.Exit(1) } close(done) }() go func() { if e != nil { r := PerfTestResult{ Type: ObjectPerfTest, Err: e.Error(), Final: true, } p.Send(r) if outCh != nil { outCh <- r } return } var result madmin.SpeedTestResult for result = range resultCh { p.Send(PerfTestResult{ Type: ObjectPerfTest, ObjectResult: &result, }) } r := PerfTestResult{ Type: ObjectPerfTest, ObjectResult: &result, Final: true, } p.Send(r) if outCh != nil { outCh <- r } }() <-done return nil } minio-client-0.0~20250403/cmd/support-perf-site-replication.go000066400000000000000000000057701477450377600240240ustar00rootroot00000000000000// Copyright (c) 2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "os" "time" tea "github.com/charmbracelet/bubbletea" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) func mainAdminSpeedTestSiteReplication(ctx *cli.Context, aliasedURL string, outCh chan<- PerfTestResult) error { client, perr := newAdminClient(aliasedURL) if perr != nil { fatalIf(perr.Trace(aliasedURL), "Unable to initialize admin client.") return nil } ctxt, cancel := context.WithCancel(globalContext) defer cancel() duration, e := time.ParseDuration(ctx.String("duration")) if e != nil { fatalIf(probe.NewError(e), "Unable to parse duration") return nil } if duration <= 0 { fatalIf(errInvalidArgument(), "duration cannot be 0 or negative") return nil } resultCh := make(chan madmin.SiteNetPerfResult) errorCh := make(chan error) go func() { defer close(resultCh) defer close(errorCh) result, e := client.SiteReplicationPerf(ctxt, duration) if e != nil { errorCh <- e } resultCh <- result }() if globalJSON { select { case e := <-errorCh: printMsg(convertPerfResult(PerfTestResult{ Type: SiteReplicationPerfTest, Err: e.Error(), Final: true, })) case result := <-resultCh: printMsg(convertPerfResult(PerfTestResult{ Type: SiteReplicationPerfTest, SiteReplicationResult: &result, Final: true, })) } return nil } done := make(chan struct{}) p := tea.NewProgram(initSpeedTestUI()) go func() { if _, e := p.Run(); e != nil { os.Exit(1) } close(done) }() go func() { for { select { case e := <-errorCh: r := PerfTestResult{ Type: SiteReplicationPerfTest, Err: e.Error(), Final: true, } p.Send(r) if outCh != nil { outCh <- r } return case result := <-resultCh: r := PerfTestResult{ Type: SiteReplicationPerfTest, SiteReplicationResult: &result, Final: true, } p.Send(r) if outCh != nil { outCh <- r } return default: p.Send(PerfTestResult{ Type: SiteReplicationPerfTest, SiteReplicationResult: &madmin.SiteNetPerfResult{}, }) time.Sleep(100 * time.Millisecond) } } }() <-done return nil } minio-client-0.0~20250403/cmd/support-perf.go000066400000000000000000000404201477450377600205420ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "archive/zip" gojson "encoding/json" "fmt" "os" "path/filepath" "time" humanize "github.com/dustin/go-humanize" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var supportPerfFlags = append([]cli.Flag{ cli.StringFlag{ Name: "size", Usage: "size of the object used for uploads/downloads", Value: "64MiB", }, cli.BoolFlag{ Name: "verbose, v", Usage: "display per-server stats", }, cli.StringFlag{ Name: "duration", Usage: "maximum duration each perf tests are run", Value: "10s", Hidden: true, }, cli.IntFlag{ Name: "concurrent", Usage: "number of concurrent requests per server", Value: 32, Hidden: true, }, cli.StringFlag{ Name: "bucket", Usage: "provide a custom bucket name to use (NOTE: bucket must be created prior)", Hidden: true, // Hidden for now. }, cli.BoolFlag{ Name: "noclear", Usage: "do not clear bucket after running object perf test", Hidden: true, // Hidden for now. }, // Drive test specific flags. cli.StringFlag{ Name: "filesize", Usage: "total amount of data read/written to each drive", Value: "1GiB", Hidden: true, }, cli.StringFlag{ Name: "blocksize", Usage: "read/write block size", Value: "4MiB", Hidden: true, }, cli.BoolFlag{ Name: "serial", Usage: "run tests on drive(s) one-by-one", Hidden: true, }, }, subnetCommonFlags...) var supportPerfCmd = cli.Command{ Name: "perf", Usage: "upload object, network and drive performance analysis", Action: mainSupportPerf, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: supportPerfFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [COMMAND] [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Upload object storage, network, and drive performance analysis for cluster with alias 'myminio' to SUBNET {{.Prompt}} {{.HelpName}} myminio 2. Run object storage, network, and drive performance tests on cluster with alias 'myminio', save and upload to SUBNET manually {{.Prompt}} {{.HelpName}} myminio --airgap `, } // PerfTestOutput - stores the final output of performance test(s) type PerfTestOutput struct { ObjectResults *ObjTestResults `json:"object,omitempty"` NetResults *NetTestResults `json:"network,omitempty"` SiteReplicationResults *SiteReplicationTestResults `json:"siteReplication,omitempty"` DriveResults *DriveTestResults `json:"drive,omitempty"` ClientResults *ClientResult `json:"client,omitempty"` Error string `json:"error,omitempty"` } // DriveTestResult - result of the drive performance test on a given endpoint type DriveTestResult struct { Endpoint string `json:"endpoint"` Perf []madmin.DrivePerf `json:"perf,omitempty"` Error string `json:"error,omitempty"` } // DriveTestResults - results of drive performance test across all endpoints type DriveTestResults struct { Results []DriveTestResult `json:"servers"` } // ObjTestResults - result of the object performance test type ObjTestResults struct { ObjectSize int `json:"objectSize"` Threads int `json:"threads"` PUTResults ObjPUTPerfResults `json:"PUT"` GETResults ObjGETPerfResults `json:"GET"` } // ObjStats - Object performance stats type ObjStats struct { Throughput uint64 `json:"throughput"` ObjectsPerSec uint64 `json:"objectsPerSec"` } // ObjStatServer - Server level object performance stats type ObjStatServer struct { Endpoint string `json:"endpoint"` Perf ObjStats `json:"perf"` Error string `json:"error,omitempty"` } // ObjPUTPerfResults - Object PUT performance results type ObjPUTPerfResults struct { Perf ObjPUTStats `json:"perf"` Servers []ObjStatServer `json:"servers"` } // ObjPUTStats - PUT stats of all the servers type ObjPUTStats struct { Throughput uint64 `json:"throughput"` ObjectsPerSec uint64 `json:"objectsPerSec"` Response madmin.Timings `json:"responseTime"` } // ObjGETPerfResults - Object GET performance results type ObjGETPerfResults struct { Perf ObjGETStats `json:"perf"` Servers []ObjStatServer `json:"servers"` } // ObjGETStats - GET stats of all the servers type ObjGETStats struct { ObjPUTStats TTFB madmin.Timings `json:"ttfb,omitempty"` } // NetStats - Network performance stats type NetStats struct { TX uint64 `json:"tx"` RX uint64 `json:"rx"` } // NetTestResult - result of the network performance test for given endpoint type NetTestResult struct { Endpoint string `json:"endpoint"` Perf NetStats `json:"perf"` Error string `json:"error,omitempty"` } // NetTestResults - result of the network performance test across all endpoints type NetTestResults struct { Results []NetTestResult `json:"servers"` } // ClientResult - result of the network from client to server type ClientResult struct { BytesSent uint64 `json:"bytesSent"` TimeSpent int64 `json:"timeSpent"` Endpoint string `json:"endpoint"` Error string `json:"error"` } // SiteNetStats - status for siteNet type SiteNetStats struct { TX uint64 `json:"tx"` // transfer rate in bytes TXTotalDuration time.Duration `json:"txTotalDuration"` RX uint64 `json:"rx"` // received rate in bytes RXTotalDuration time.Duration `json:"rxTotalDuration"` TotalConn uint64 `json:"totalConn"` } // SiteReplicationTestNodeResult - result of the network performance test for site-replication type SiteReplicationTestNodeResult struct { Endpoint string `json:"endpoint"` Perf SiteNetStats `json:"perf"` Error string `json:"error,omitempty"` } // SiteReplicationTestResults - result of the network performance test across all site-replication type SiteReplicationTestResults struct { Results []SiteReplicationTestNodeResult `json:"servers"` } func objectTestVerboseResult(result *madmin.SpeedTestResult) (msg string) { msg += "PUT:\n" for _, node := range result.PUTStats.Servers { msg += fmt.Sprintf(" * %s: %s/s %s objs/s", node.Endpoint, humanize.IBytes(node.ThroughputPerSec), humanize.Comma(int64(node.ObjectsPerSec))) if node.Err != "" { msg += " Err: " + node.Err } msg += "\n" } msg += "GET:\n" for _, node := range result.GETStats.Servers { msg += fmt.Sprintf(" * %s: %s/s %s objs/s", node.Endpoint, humanize.IBytes(node.ThroughputPerSec), humanize.Comma(int64(node.ObjectsPerSec))) if node.Err != "" { msg += " Err: " + node.Err } msg += "\n" } return msg } func objectTestShortResult(result *madmin.SpeedTestResult) (msg string) { msg += fmt.Sprintf("MinIO %s, %d servers, %d drives, %s objects, %d threads", result.Version, result.Servers, result.Disks, humanize.IBytes(uint64(result.Size)), result.Concurrent) return msg } // String - dummy function to confirm to the 'message' interface. Not used. func (p PerfTestOutput) String() string { return "" } // JSON - jsonified output of the perf tests func (p PerfTestOutput) JSON() string { JSONBytes, e := json.MarshalIndent(p, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON") return string(JSONBytes) } var globalPerfTestVerbose bool func mainSupportPerf(ctx *cli.Context) error { args := ctx.Args() // the alias parameter from cli aliasedURL := "" perfType := "" switch len(args) { case 1: // cannot use alias by the name 'drive' or 'net' if args[0] == "drive" || args[0] == "net" || args[0] == "object" || args[0] == "site-replication" { showCommandHelpAndExit(ctx, 1) } aliasedURL = args[0] case 2: perfType = args[0] aliasedURL = args[1] default: showCommandHelpAndExit(ctx, 1) // last argument is exit code } // Main execution execSupportPerf(ctx, aliasedURL, perfType) return nil } func convertDriveTestResult(dr madmin.DriveSpeedTestResult) DriveTestResult { return DriveTestResult{ Endpoint: dr.Endpoint, Perf: dr.DrivePerf, Error: dr.Error, } } func convertDriveTestResults(driveResults []madmin.DriveSpeedTestResult) *DriveTestResults { if driveResults == nil { return nil } results := []DriveTestResult{} for _, dr := range driveResults { results = append(results, convertDriveTestResult(dr)) } r := DriveTestResults{ Results: results, } return &r } func convertClientResult(result *madmin.ClientPerfResult) *ClientResult { if result == nil || result.TimeSpent <= 0 { return nil } return &ClientResult{ BytesSent: result.BytesSend, TimeSpent: result.TimeSpent, Endpoint: result.Endpoint, Error: result.Error, } } func convertSiteReplicationTestResults(netResults *madmin.SiteNetPerfResult) *SiteReplicationTestResults { if netResults == nil { return nil } results := []SiteReplicationTestNodeResult{} for _, nr := range netResults.NodeResults { results = append(results, SiteReplicationTestNodeResult{ Endpoint: nr.Endpoint, Error: nr.Error, Perf: SiteNetStats{ TX: nr.TX, TXTotalDuration: nr.TXTotalDuration, RX: nr.RX, RXTotalDuration: nr.RXTotalDuration, TotalConn: nr.TotalConn, }, }) } r := SiteReplicationTestResults{ Results: results, } return &r } func convertNetTestResults(netResults *madmin.NetperfResult) *NetTestResults { if netResults == nil { return nil } results := []NetTestResult{} for _, nr := range netResults.NodeResults { results = append(results, NetTestResult{ Endpoint: nr.Endpoint, Error: nr.Error, Perf: NetStats{ TX: nr.TX, RX: nr.RX, }, }) } r := NetTestResults{ Results: results, } return &r } func convertObjStatServers(ss []madmin.SpeedTestStatServer) []ObjStatServer { out := []ObjStatServer{} for _, s := range ss { out = append(out, ObjStatServer{ Endpoint: s.Endpoint, Perf: ObjStats{ Throughput: s.ThroughputPerSec, ObjectsPerSec: s.ObjectsPerSec, }, Error: s.Err, }) } return out } func convertPUTStats(stats madmin.SpeedTestStats) ObjPUTStats { return ObjPUTStats{ Throughput: stats.ThroughputPerSec, ObjectsPerSec: stats.ObjectsPerSec, Response: stats.Response, } } func convertPUTResults(stats madmin.SpeedTestStats) ObjPUTPerfResults { return ObjPUTPerfResults{ Perf: convertPUTStats(stats), Servers: convertObjStatServers(stats.Servers), } } func convertGETResults(stats madmin.SpeedTestStats) ObjGETPerfResults { return ObjGETPerfResults{ Perf: ObjGETStats{ ObjPUTStats: convertPUTStats(stats), TTFB: stats.TTFB, }, Servers: convertObjStatServers(stats.Servers), } } func convertObjTestResults(objResult *madmin.SpeedTestResult) *ObjTestResults { if objResult == nil { return nil } result := ObjTestResults{ ObjectSize: objResult.Size, Threads: objResult.Concurrent, } result.PUTResults = convertPUTResults(objResult.PUTStats) result.GETResults = convertGETResults(objResult.GETStats) return &result } func updatePerfOutput(r PerfTestResult, out *PerfTestOutput) { switch r.Type { case DrivePerfTest: out.DriveResults = convertDriveTestResults(r.DriveResult) case ObjectPerfTest: out.ObjectResults = convertObjTestResults(r.ObjectResult) case NetPerfTest: out.NetResults = convertNetTestResults(r.NetResult) case SiteReplicationPerfTest: out.SiteReplicationResults = convertSiteReplicationTestResults(r.SiteReplicationResult) case ClientPerfTest: out.ClientResults = convertClientResult(r.ClientResult) default: fatalIf(errDummy().Trace(), fmt.Sprintf("Invalid test type %d", r.Type)) } } func convertPerfResult(r PerfTestResult) PerfTestOutput { out := PerfTestOutput{} updatePerfOutput(r, &out) return out } func convertPerfResults(results []PerfTestResult) PerfTestOutput { out := PerfTestOutput{} for _, r := range results { updatePerfOutput(r, &out) } return out } func execSupportPerf(ctx *cli.Context, aliasedURL, perfType string) { alias, apiKey := initSubnetConnectivity(ctx, aliasedURL, true) if len(apiKey) == 0 { // api key not passed as flag. Check that the cluster is registered. apiKey = validateClusterRegistered(alias, true) } results := runPerfTests(ctx, aliasedURL, perfType) if globalJSON { // No file to be saved or uploaded to SUBNET in case of `--json` return } // If results still not available, don't write anything if len(results) == 0 { console.Fatalln("No performance reports were captured, please report this issue") } else { resultFileNamePfx := fmt.Sprintf("%s-perf_%s", filepath.Clean(alias), UTCNow().Format("20060102150405")) resultFileName := resultFileNamePfx + ".json" regInfo := GetClusterRegInfo(getAdminInfo(aliasedURL), alias) tmpFileName, e := zipPerfResult(convertPerfResults(results), resultFileName, regInfo) fatalIf(probe.NewError(e), "Unable to generate zip file from performance results") if globalAirgapped { console.Infoln() savePerfResultFile(tmpFileName, resultFileNamePfx) return } uploadURL := SubnetUploadURL("perf") reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey) _, e = (&SubnetFileUploader{ alias: alias, FilePath: tmpFileName, ReqURL: reqURL, Headers: headers, DeleteAfterUpload: true, }).UploadFileToSubnet() if e != nil { errorIf(probe.NewError(e), "Unable to upload performance results to SUBNET portal") savePerfResultFile(tmpFileName, resultFileNamePfx) return } console.Infoln("Uploaded performance report to SUBNET successfully") } } func savePerfResultFile(tmpFileName, resultFileNamePfx string) { zipFileName := resultFileNamePfx + ".zip" e := moveFile(tmpFileName, zipFileName) fatalIf(probe.NewError(e), fmt.Sprintf("Unable to move %s -> %s", tmpFileName, zipFileName)) console.Infof("MinIO performance report saved at %s, please upload to SUBNET portal manually\n", zipFileName) } func runPerfTests(ctx *cli.Context, aliasedURL, perfType string) []PerfTestResult { resultCh := make(chan PerfTestResult) results := []PerfTestResult{} defer close(resultCh) tests := []string{perfType} if len(perfType) == 0 { // by default run all tests tests = []string{"net", "drive", "object", "client"} } for _, t := range tests { switch t { case "drive": mainAdminSpeedTestDrive(ctx, aliasedURL, resultCh) case "object": mainAdminSpeedTestObject(ctx, aliasedURL, resultCh) case "net": mainAdminSpeedTestNetperf(ctx, aliasedURL, resultCh) case "site-replication": mainAdminSpeedTestSiteReplication(ctx, aliasedURL, resultCh) case "client": mainAdminSpeedTestClientPerf(ctx, aliasedURL, resultCh) default: showCommandHelpAndExit(ctx, 1) // last argument is exit code } if !globalJSON { results = append(results, <-resultCh) } } return results } func writeJSONObjToZip(zipWriter *zip.Writer, obj interface{}, filename string) error { writer, e := zipWriter.Create(filename) if e != nil { return e } return gojson.NewEncoder(writer).Encode(obj) } // compress MinIO performance output func zipPerfResult(perfOutput PerfTestOutput, resultFilename string, regInfo ClusterRegistrationInfo) (string, error) { // Create perf results zip file tmpArchive, e := os.CreateTemp("", "mc-perf-*.zip") if e != nil { return "", e } defer tmpArchive.Close() zipWriter := zip.NewWriter(tmpArchive) defer zipWriter.Close() e = writeJSONObjToZip(zipWriter, perfOutput, resultFilename) if e != nil { return "", e } e = writeJSONObjToZip(zipWriter, regInfo, "cluster.info") if e != nil { return "", e } return tmpArchive.Name(), nil } minio-client-0.0~20250403/cmd/support-profile.go000066400000000000000000000174011477450377600212510ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "io" "os" "strings" "time" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/pkg/v3/console" ) // profile command flags. var ( profileFlags = append([]cli.Flag{ cli.IntFlag{ Name: "duration", Usage: "profile for the specified duration in seconds", Value: 10, }, cli.StringFlag{ Name: "type", Usage: "profiler type, possible values are 'cpu', 'cpuio', 'mem', 'block', 'mutex', 'trace', 'threads' and 'goroutines'", Value: "cpu,mem,block,mutex,goroutines", }, }, subnetCommonFlags...) ) const profileFile = "profile.zip" type supportProfileMessage struct { Status string `json:"status"` File string `json:"file,omitempty"` Error string `json:"error,omitempty"` } // Colorized message for console printing. func (s supportProfileMessage) String() string { var msg string if s.Error != "" { errMsg := fmt.Sprintln("Unable to upload profile file to SUBNET: ", s.Error) msg := console.Colorize(supportErrorMsgTag, errMsg) infoMsg := fmt.Sprintf("Profiling data saved locally at '%s'", profileFile) msg += console.Colorize(supportSuccessMsgTag, infoMsg) return msg } if globalAirgapped { msg = fmt.Sprintf("Profiling data saved successfully at %s", s.File) } else { msg = "Profiling data uploaded to SUBNET successfully" } return console.Colorize(supportSuccessMsgTag, msg) } // JSON jsonified proxy remove message func (s supportProfileMessage) JSON() string { return toJSON(s) } var supportProfileCmd = cli.Command{ Name: "profile", Usage: "upload profile data for debugging", Action: mainSupportProfile, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: profileFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Profile CPU for 10 seconds on cluster with alias 'myminio' and upload results to SUBNET {{.Prompt}} {{.HelpName}} --type cpu myminio 2. Profile CPU, Memory, Goroutines for 10 seconds on cluster with alias 'myminio' and upload results to SUBNET {{.Prompt}} {{.HelpName}} --type cpu,mem,goroutines myminio 3. Profile CPU, Memory, Goroutines for 10 minutes on cluster with alias 'myminio' and upload results to SUBNET {{.Prompt}} {{.HelpName}} --type cpu,mem,goroutines --duration 600 myminio 4. Profile CPU for 10 seconds on cluster with alias 'myminio', save and upload to SUBNET manually {{.Prompt}} {{.HelpName}} --type cpu --airgap myminio `, } func checkAdminProfileSyntax(ctx *cli.Context) { s := set.CreateStringSet(string(madmin.ProfilerCPU), string(madmin.ProfilerMEM), string(madmin.ProfilerBlock), string(madmin.ProfilerMutex), string(madmin.ProfilerTrace), string(madmin.ProfilerThreads), string(madmin.ProfilerGoroutines), string(madmin.ProfilerCPUIO), string(madmin.ProfilerRuntime), ) // Check if the provided profiler type is known and supported profilers := strings.Split(strings.ToLower(ctx.String("type")), ",") for _, profiler := range profilers { if profiler != "" { if !s.Contains(profiler) { fatalIf(errDummy().Trace(ctx.String("type")), "Profiler type %s unrecognized. Possible values are: %v.", profiler, s) } } } if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } if ctx.Int("duration") < 1 { fatal(errDummy().Trace(), "for any useful profiling one must run it for at least 1 second") } } // moveFile - os.Rename cannot handle cross device renames, in our situation // it is possible that /tmp is mounted from a separate partition and current // working directory is a different partition. To allow all situations to // be handled appropriately use this function instead of os.Rename() func moveFile(sourcePath, destPath string) error { inputFile, e := os.Open(sourcePath) if e != nil { return e } outputFile, e := os.Create(destPath) if e != nil { inputFile.Close() return e } defer outputFile.Close() if _, e = io.Copy(outputFile, inputFile); e != nil { inputFile.Close() return e } // The copy was successful, so now delete the original file inputFile.Close() return os.Remove(sourcePath) } func saveProfileFile(data io.ReadCloser) { // Create profile zip file tmpFile, e := os.CreateTemp("", "mc-profile-") fatalIf(probe.NewError(e), "Unable to download profile data.") // Copy zip content to target download file _, e = io.Copy(tmpFile, data) fatalIf(probe.NewError(e), "Unable to download profile data.") // Close everything data.Close() tmpFile.Close() downloadedFile := profileFile + "." + time.Now().Format(dateTimeFormatFilename) fi, e := os.Stat(profileFile) if e == nil && !fi.IsDir() { e = moveFile(profileFile, downloadedFile) fatalIf(probe.NewError(e), "Unable to create a backup of profile.zip") } else { if !os.IsNotExist(e) { fatal(probe.NewError(e), "Unable to save profile data") } } fatalIf(probe.NewError(moveFile(tmpFile.Name(), profileFile)), "Unable to save profile data") } // mainSupportProfile is the handle for "mc support profile" command. func mainSupportProfile(ctx *cli.Context) error { // Check for command syntax checkAdminProfileSyntax(ctx) setSuccessMessageColor() setErrorMessageColor() // Get the alias parameter from cli aliasedURL := ctx.Args().Get(0) alias, apiKey := initSubnetConnectivity(ctx, aliasedURL, true) if len(apiKey) == 0 { // api key not passed as flag. Check that the cluster is registered. apiKey = validateClusterRegistered(alias, true) } // Create a new MinIO Admin Client client := getClient(aliasedURL) // Main execution execSupportProfile(ctx, client, alias, apiKey) return nil } func execSupportProfile(ctx *cli.Context, client *madmin.AdminClient, alias, apiKey string) { var reqURL string var headers map[string]string profilers := ctx.String("type") duration := ctx.Int("duration") if !globalAirgapped { // Retrieve subnet credentials (login/license) beforehand as // it can take a long time to fetch the profile data uploadURL := SubnetUploadURL("profile") reqURL, headers = prepareSubnetUploadURL(uploadURL, alias, apiKey) } if !globalJSON { console.Infof("Profiling '%s' for %d seconds... \n", alias, duration) } data, e := client.Profile(globalContext, madmin.ProfilerType(profilers), time.Second*time.Duration(duration)) fatalIf(probe.NewError(e), "Unable to save profile data") saveProfileFile(data) if !globalAirgapped { _, e = (&SubnetFileUploader{ alias: alias, FilePath: profileFile, ReqURL: reqURL, Headers: headers, DeleteAfterUpload: true, }).UploadFileToSubnet() if e != nil { printMsg(supportProfileMessage{ Status: "error", Error: e.Error(), File: profileFile, }) return } printMsg(supportProfileMessage{ Status: "success", }) } else { printMsg(supportProfileMessage{ Status: "success", File: profileFile, }) } } minio-client-0.0~20250403/cmd/support-proxy-remove.go000066400000000000000000000050561477450377600222700ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) type supportProxyRemoveMessage struct { Status string `json:"status"` } // String colorized proxy remove message func (s supportProxyRemoveMessage) String() string { return console.Colorize(supportSuccessMsgTag, "Proxy has been removed") } // JSON jsonified proxy remove message func (s supportProxyRemoveMessage) JSON() string { s.Status = "success" return toJSON(s) } var supportProxyRemoveCmd = cli.Command{ Name: "remove", Usage: "Remove proxy configuration", Action: mainSupportProxyRemove, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Remove the proxy configured for cluster with alias 'myminio' {{.Prompt}} {{.HelpName}} myminio `, } func checkSupportProxyRemoveSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainSupportProxyRemove is the handler for "mc support proxy remove" command. func mainSupportProxyRemove(ctx *cli.Context) error { // Check for command syntax checkSupportProxyRemoveSyntax(ctx) setSuccessMessageColor() // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) alias, _ := url2Alias(aliasedURL) validateClusterRegistered(alias, false) // Create a new MinIO Admin Client client := getClient(aliasedURL) // Main execution _, e := client.DelConfigKV(globalContext, "subnet proxy") fatalIf(probe.NewError(e), "Unable to remove proxy:") printMsg(supportProxyRemoveMessage{}) return nil } minio-client-0.0~20250403/cmd/support-proxy-set.go000066400000000000000000000054671477450377600215740ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "net/url" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) type supportProxySetMessage struct { Status string `json:"status"` Proxy string `json:"proxy"` } // String colorized proxy set message func (s supportProxySetMessage) String() string { return console.Colorize(supportSuccessMsgTag, "Proxy is now set to "+s.Proxy) } // JSON jsonified proxy set message func (s supportProxySetMessage) JSON() string { s.Status = "success" return toJSON(s) } var supportProxySetCmd = cli.Command{ Name: "set", Usage: "configure proxy to given URL", Action: mainSupportProxySet, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET PROXY_URL FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Set the proxy to http://my.proxy for cluster with alias 'myminio' {{.Prompt}} {{.HelpName}} myminio http://my.proxy `, } func checkSupportProxySetSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainSupportProxySet is the handle for "mc support proxy set" command. func mainSupportProxySet(ctx *cli.Context) error { // Check for command syntax checkSupportProxySetSyntax(ctx) setSuccessMessageColor() // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) alias, _ := url2Alias(aliasedURL) validateClusterRegistered(alias, false) // Create a new MinIO Admin Client client := getClient(aliasedURL) // Call set config API proxy := args.Get(1) if len(proxy) == 0 { fatal(errDummy().Trace(), "Proxy must not be empty") } _, e := url.Parse(proxy) fatalIf(probe.NewError(e), "Invalid proxy:") // Main execution _, e = client.SetConfigKV(globalContext, "subnet proxy="+proxy) fatalIf(probe.NewError(e), "Unable to set proxy '%s':", proxy) printMsg(supportProxySetMessage{Proxy: proxy}) return nil } minio-client-0.0~20250403/cmd/support-proxy-show.go000066400000000000000000000051671477450377600217560ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" "github.com/minio/pkg/v3/console" ) var supportProxyShowCmd = cli.Command{ Name: "show", Usage: "Show the configured proxy", Action: mainSupportProxyShow, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Show the proxy configured for cluster with alias 'myminio' {{.Prompt}} {{.HelpName}} myminio `, } type supportProxyShowMessage struct { Status string `json:"status"` Proxy string `json:"proxy"` } // String colorized proxy show message func (s supportProxyShowMessage) String() string { msg := s.Proxy if len(msg) == 0 { msg = "Proxy is not configured" } return console.Colorize(supportSuccessMsgTag, msg) } // JSON jsonified proxy show message func (s supportProxyShowMessage) JSON() string { s.Status = "success" return toJSON(s) } func checkSupportProxyShowSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // mainSupportProxyShow is the handler for "mc support proxy show" command. func mainSupportProxyShow(ctx *cli.Context) error { // Check for command syntax checkSupportProxyShowSyntax(ctx) setSuccessMessageColor() // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) alias, _ := url2Alias(aliasedURL) validateClusterRegistered(alias, false) // Main execution // get the subnet proxy config from MinIO if available proxy, supported := getKeyFromSubnetConfig(alias, "proxy") if !supported { fatal(errDummy().Trace(), "Proxy configuration not supported in this version of MinIO.") } printMsg(supportProxyShowMessage{Proxy: proxy}) return nil } minio-client-0.0~20250403/cmd/support-proxy.go000066400000000000000000000027531477450377600207760ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var supportProxySubcommands = []cli.Command{ supportProxySetCmd, supportProxyRemoveCmd, supportProxyShowCmd, } var supportProxyCmd = cli.Command{ Name: "proxy", Usage: "configure proxy", Action: mainSupportProxy, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: supportGlobalFlags, Subcommands: supportProxySubcommands, HideHelpCommand: true, } // mainSupportProxy is the handler for "mc support proxy" command. func mainSupportProxy(ctx *cli.Context) error { commandNotFound(ctx, supportProxySubcommands) return nil // Sub-commands like "set", "remove", "show" have their own main. // Check for command syntax } minio-client-0.0~20250403/cmd/support-register.go000066400000000000000000000026631477450377600214410ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var supportRegisterFlags = append([]cli.Flag{ cli.StringFlag{ Name: "name", Usage: "Specify the name to associate to this MinIO cluster in SUBNET", }, }, subnetCommonFlags...) var supportRegisterCmd = cli.Command{ Name: "register", Usage: "register with MinIO subscription network", OnUsageError: onUsageError, Action: mainSupportRegister, Before: setGlobalsFromContext, Flags: supportRegisterFlags, CustomHelpTemplate: "Please use 'mc license register'", } func mainSupportRegister(_ *cli.Context) error { deprecatedError("mc license register") return nil } minio-client-0.0~20250403/cmd/support-top-api.go000066400000000000000000000066451477450377600211720ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" tea "github.com/charmbracelet/bubbletea" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var supportTopAPIFlags = []cli.Flag{ cli.StringSliceFlag{ Name: "name", Usage: "summarize current calls for matching API name", }, cli.StringSliceFlag{ Name: "path", Usage: "summarize current API calls only on matching path", }, cli.StringSliceFlag{ Name: "node", Usage: "summarize current API calls only on matching servers", }, cli.BoolFlag{ Name: "errors, e", Usage: "summarize current API calls throwing only errors", }, } var supportTopAPICmd = cli.Command{ Name: "api", Usage: "summarize API events on MinIO server in real-time", Action: mainSupportTopAPI, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(supportTopAPIFlags, supportGlobalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display current in-progress all S3 API calls. {{.Prompt}} {{.HelpName}} myminio/ 2. Display current in-progress all 's3.PutObject' API calls. {{.Prompt}} {{.HelpName}} --name s3.PutObject myminio/ `, } // checkSupportTopAPISyntax - validate all the passed arguments func checkSupportTopAPISyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainSupportTopAPI(ctx *cli.Context) error { checkSupportTopAPISyntax(ctx) aliasedURL := ctx.Args().Get(0) alias, _ := url2Alias(aliasedURL) validateClusterRegistered(alias, false) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } ctxt, cancel := context.WithCancel(globalContext) defer cancel() opts, e := tracingOpts(ctx, ctx.StringSlice("call")) fatalIf(probe.NewError(e), "Unable to start tracing") mopts := matchingOpts(ctx) // Start listening on all trace activity. traceCh := client.ServiceTrace(ctxt, opts) filteredTraces := make(chan madmin.ServiceTraceInfo, 1) ui := tea.NewProgram(initTraceStatsUI(false, 30, filteredTraces)) var te error go func() { for t := range traceCh { if t.Err != nil { te = t.Err ui.Kill() return } if mopts.matches(t) { filteredTraces <- t } } }() if _, e := ui.Run(); e != nil { cancel() if te != nil { e = te } fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch http trace statistics") } return nil } minio-client-0.0~20250403/cmd/support-top-drive.go000066400000000000000000000063741477450377600215310ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "time" tea "github.com/charmbracelet/bubbletea" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var supportTopDriveFlags = []cli.Flag{ cli.IntFlag{ Name: "count, c", Usage: "show up to N drives", Value: 10, }, } var supportTopDriveCmd = cli.Command{ Name: "drive", Aliases: []string{"disk"}, HiddenAliases: true, Usage: "show real-time drive metrics", Action: mainSupportTopDrive, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(supportTopDriveFlags, supportGlobalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display drive metrics {{.Prompt}} {{.HelpName}} myminio/ `, } // checkSupportTopDriveSyntax - validate all the passed arguments func checkSupportTopDriveSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainSupportTopDrive(ctx *cli.Context) error { checkSupportTopDriveSyntax(ctx) aliasedURL := ctx.Args().Get(0) alias, _ := url2Alias(aliasedURL) validateClusterRegistered(alias, false) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } ctxt, cancel := context.WithCancel(globalContext) defer cancel() info, e := client.ServerInfo(ctxt) fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to initialize admin client.") var disks []madmin.Disk for _, srv := range info.Servers { disks = append(disks, srv.Disks...) } // MetricsOptions are options provided to Metrics call. opts := madmin.MetricsOptions{ Type: madmin.MetricsDisk, Interval: time.Second, ByDisk: true, N: ctx.Int("count"), } p := tea.NewProgram(initTopDriveUI(disks, ctx.Int("count"))) go func() { out := func(m madmin.RealtimeMetrics) { for name, metric := range m.ByDisk { p.Send(topDriveResult{ diskName: name, stats: metric.IOStats, }) } } e := client.Metrics(ctxt, opts, out) if e != nil { fatalIf(probe.NewError(e), "Unable to fetch top drives events") } p.Quit() }() if _, e := p.Run(); e != nil { cancel() fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch top drive events") } return nil } minio-client-0.0~20250403/cmd/support-top-locks.go000066400000000000000000000132401477450377600215210ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "time" humanize "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var supportTopLocksFlag = []cli.Flag{ cli.BoolFlag{ Name: "stale", Usage: "list all stale locks", Hidden: true, }, cli.IntFlag{ Name: "count", Usage: "list N number of locks", Hidden: true, Value: 10, }, } var supportTopLocksCmd = cli.Command{ Name: "locks", Usage: "list all active locks on a MinIO cluster", Before: setGlobalsFromContext, Action: mainSupportTopLocks, OnUsageError: onUsageError, Flags: append(supportTopLocksFlag, supportGlobalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List oldest locks on a MinIO cluster. {{.Prompt}} {{.HelpName}} myminio/ `, } // lockMessage struct to list lock information. type lockMessage struct { Status string `json:"status"` Lock madmin.LockEntry `json:"locks"` } // String colorized oldest locks message. func (u lockMessage) String() string { elapsed := u.Lock.Elapsed // elapsed can be zero with older MinIO versions, // so this code is deprecated and can be removed later. if elapsed == 0 { elapsed = time.Now().UTC().Sub(u.Lock.Timestamp) } stale := u.Lock.Quorum > len(u.Lock.ServerList) lockState := "Lock" if stale { lockState = "StaleLock" } return console.Colorize(lockState, newPrettyTable(" ", Field{"Since", timeFieldMaxLen}, Field{"Type", typeFieldMaxLen}, Field{"Owner", timeFieldMaxLen}, Field{"Resource", resourceFieldMaxLen}, ).buildRow(humanize.Time(time.Now().UTC().Add(-elapsed)), u.Lock.Type, u.Lock.Owner, u.Lock.Resource)) } // JSON jsonified top oldest locks message. func (u lockMessage) JSON() string { type lockEntry struct { Timestamp time.Time `json:"time"` // When the lock was first granted Elapsed string `json:"elapsed"` // Humanized duration for which lock has been held Resource string `json:"resource"` // Resource contains info like bucket+object Type string `json:"type"` // Type indicates if 'Write' or 'Read' lock Source string `json:"source"` // Source at which lock was granted ServerList []string `json:"serverlist"` // List of servers participating in the lock. Owner string `json:"owner"` // Owner UUID indicates server owns the lock. ID string `json:"id"` // UID to uniquely identify request of client. // Represents quorum number of servers required to hold this lock, used to look for stale locks. Quorum int `json:"quorum"` Stale bool // Represents if the lock is stale. } le := lockEntry{ Timestamp: u.Lock.Timestamp, Elapsed: u.Lock.Elapsed.Round(time.Second).String(), Resource: u.Lock.Resource, Type: u.Lock.Type, Source: u.Lock.Source, ServerList: u.Lock.ServerList, Owner: u.Lock.Owner, ID: u.Lock.ID, Quorum: u.Lock.Quorum, Stale: u.Lock.Quorum > len(u.Lock.ServerList), } statusJSONBytes, e := json.MarshalIndent(le, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(statusJSONBytes) } // checkAdminTopLocksSyntax - validate all the passed arguments func checkSupportTopLocksSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainSupportTopLocks(ctx *cli.Context) error { checkSupportTopLocksSyntax(ctx) // Get the alias parameter from cli args := ctx.Args() aliasedURL := args.Get(0) alias, _ := url2Alias(aliasedURL) validateClusterRegistered(alias, false) console.SetColor("StaleLock", color.New(color.FgRed, color.Bold)) console.SetColor("Lock", color.New(color.FgBlue, color.Bold)) console.SetColor("Headers", color.New(color.FgGreen, color.Bold)) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) fatalIf(err, "Unable to initialize admin connection.") // Call top locks API entries, e := client.TopLocksWithOpts(globalContext, madmin.TopLockOpts{ Count: ctx.Int("count"), Stale: ctx.Bool("stale"), }) fatalIf(probe.NewError(e), "Unable to get server locks list.") // Print printLocks(entries) return nil } const ( timeFieldMaxLen = 20 typeFieldMaxLen = 6 resourceFieldMaxLen = 150 ) func printHeaders() { console.Println(console.Colorize("Headers", newPrettyTable(" ", Field{"Since", timeFieldMaxLen}, Field{"Type", typeFieldMaxLen}, Field{"Owner", timeFieldMaxLen}, Field{"Resource", resourceFieldMaxLen}, ).buildRow("Since", "Type", "Owner", "Resource"))) } // Prints oldest locks. func printLocks(locks madmin.LockEntries) { if !globalJSON { printHeaders() } for _, entry := range locks { printMsg(lockMessage{Lock: entry}) } } minio-client-0.0~20250403/cmd/support-top-net.go000066400000000000000000000071741477450377600212050ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "time" tea "github.com/charmbracelet/bubbletea" "github.com/minio/cli" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" ) var supportTopNetFlags = []cli.Flag{ cli.IntFlag{ Name: "interval", Usage: "interval between requests in seconds", Value: 1, }, cli.IntFlag{ Name: "n", Usage: "number of requests to run before exiting. 0 for endless (default)", Value: 0, }, } var supportTopNetCmd = cli.Command{ Name: "net", Aliases: []string{"network"}, HiddenAliases: true, Usage: "show real-time net metrics", Action: mainSupportTopNet, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(supportTopNetFlags, supportGlobalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display net metrics {{.Prompt}} {{.HelpName}} myminio/ `, } // checkSupportTopNetSyntax - validate all the passed arguments func checkSupportTopNetSyntax(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainSupportTopNet(ctx *cli.Context) error { checkSupportTopNetSyntax(ctx) aliasedURL := ctx.Args().Get(0) alias, _ := url2Alias(aliasedURL) validateClusterRegistered(alias, false) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } ctxt, cancel := context.WithCancel(globalContext) defer cancel() // MetricsOptions are options provided to Metrics call. opts := madmin.MetricsOptions{ Type: madmin.MetricNet, Interval: time.Duration(ctx.Int("interval")) * time.Second, N: ctx.Int("n"), ByHost: true, } if globalJSON { e := client.Metrics(ctxt, opts, func(metrics madmin.RealtimeMetrics) { printMsg(metricsMessage{RealtimeMetrics: metrics}) }) if e != nil && !errors.Is(e, context.Canceled) { fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch net metrics") } return nil } p := tea.NewProgram(initTopNetUI()) go func() { out := func(m madmin.RealtimeMetrics) { for endPoint, metric := range m.ByHost { if metric.Net != nil { p.Send(topNetResult{ endPoint: endPoint, stats: *metric.Net, }) } } if len(m.Errors) != 0 && len(m.Hosts) != 0 { p.Send(topNetResult{ endPoint: m.Hosts[0], error: m.Errors[0], }) } } e := client.Metrics(ctxt, opts, out) if e != nil { fatalIf(probe.NewError(e), "Unable to fetch top net events") } p.Quit() }() if _, e := p.Run(); e != nil { cancel() fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch top net events") } return nil } minio-client-0.0~20250403/cmd/support-top-rpc.go000066400000000000000000000273021477450377600211760ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "context" "errors" "fmt" "io" "os" "sort" "strings" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/klauspost/compress/zstd" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/olekukonko/tablewriter" ) var supportTopRPCFlags = []cli.Flag{ cli.StringFlag{ Name: "nodes", Usage: "collect only metrics from matching servers, comma separate multiple", }, cli.IntFlag{ Name: "interval", Usage: "interval between requests in seconds", Value: 1, }, cli.IntFlag{ Name: "n", Usage: "number of requests to run before exiting. 0 for endless (default)", Value: 0, }, cli.StringFlag{ Name: "in", Usage: "read previously saved json from file and replay", }, } var supportTopRPCCmd = cli.Command{ Name: "rpc", HiddenAliases: true, Usage: "show real-time rpc metrics (grid only)", Action: mainSupportTopRPC, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(supportTopRPCFlags, supportGlobalFlags...), HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display net metrics {{.Prompt}} {{.HelpName}} myminio/ `, } // checkSupportTopNetSyntax - validate all the passed arguments func checkSupportTopRPCSyntax(ctx *cli.Context) { if ctx.String("in") != "" { return } if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } func mainSupportTopRPC(ctx *cli.Context) error { checkSupportTopRPCSyntax(ctx) ui := tea.NewProgram(initTopRPCUI()) ctxt, cancel := context.WithCancel(globalContext) defer cancel() // Replay from file. if inFile := ctx.String("in"); inFile != "" { go func() { defer cancel() if _, e := ui.Run(); e != nil { fatalIf(probe.NewError(e), "Unable to fetch scanner metrics") } }() f, e := os.Open(inFile) fatalIf(probe.NewError(e), "Unable to open input") defer f.Close() in := io.Reader(f) if strings.HasSuffix(inFile, ".zst") { zr, e := zstd.NewReader(in) fatalIf(probe.NewError(e), "Unable to open input") defer zr.Close() in = zr } sc := bufio.NewReader(in) var lastTime time.Time for ctxt.Err() == nil { b, e := sc.ReadBytes('\n') if e == io.EOF { break } var metrics madmin.RealtimeMetrics e = json.Unmarshal(b, &metrics) if e != nil || metrics.Aggregated.RPC == nil { continue } delay := metrics.Aggregated.RPC.CollectedAt.Sub(lastTime) if !lastTime.IsZero() && delay > 0 { if delay > 3*time.Second { delay = 3 * time.Second } time.Sleep(delay) } ui.Send(metrics) lastTime = metrics.Aggregated.RPC.CollectedAt } os.Exit(0) } aliasedURL := ctx.Args().Get(0) alias, _ := url2Alias(aliasedURL) validateClusterRegistered(alias, false) // Create a new MinIO Admin Client client, err := newAdminClient(aliasedURL) if err != nil { fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") return nil } // MetricsOptions are options provided to Metrics call. opts := madmin.MetricsOptions{ Type: madmin.MetricsRPC, Interval: time.Duration(ctx.Int("interval")) * time.Second, N: ctx.Int("n"), Hosts: strings.Split(ctx.String("nodes"), ","), ByHost: true, } if globalJSON { e := client.Metrics(ctxt, opts, func(metrics madmin.RealtimeMetrics) { printMsg(metricsMessage{RealtimeMetrics: metrics}) }) if e != nil && !errors.Is(e, context.Canceled) { fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch net metrics") } return nil } go func() { out := func(m madmin.RealtimeMetrics) { ui.Send(m) } e := client.Metrics(ctxt, opts, out) if e != nil { fatalIf(probe.NewError(e), "Unable to fetch top net events") } ui.Quit() }() if _, e := ui.Run(); e != nil { cancel() fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch top net events") } return nil } const ( rpcSortHostname = iota rpcSortReconnections rpcSortQueue rpcSortPing ) type topRPCUI struct { spinner spinner.Model offset int quitting bool pageSz int showTo bool sortBy uint8 curr madmin.RealtimeMetrics frozen *madmin.RealtimeMetrics } func (m *topRPCUI) Init() tea.Cmd { m.showTo = true return m.spinner.Tick } func (m *topRPCUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": m.quitting = true return m, tea.Quit case "up": m.offset-- case "down": m.offset++ case "pgdown": m.offset += m.pageSz - 1 case "pgup": m.offset -= m.pageSz - 1 case "t": m.showTo = true case "f": m.showTo = false case "r": m.sortBy = rpcSortReconnections case "q": m.sortBy = rpcSortQueue case "p": m.sortBy = rpcSortPing case tea.KeySpace.String(): if m.frozen == nil { freeze := m.curr m.frozen = &freeze } else { m.frozen = nil } case "tab": m.showTo = !m.showTo } return m, nil case madmin.RealtimeMetrics: m.curr = msg if msg.Final { m.quitting = true return m, tea.Quit } return m, nil case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd default: return m, nil } } func (m *topRPCUI) View() string { var s strings.Builder // Set table header table := tablewriter.NewWriter(&s) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(false) table.SetHeaderAlignment(tablewriter.ALIGN_CENTER) table.SetAlignment(tablewriter.ALIGN_CENTER) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) table.SetHeader([]string{"SERVER", "CONCTD", "PING", "PONG", "OUT.Q", "RECONNS", "STR.IN", "STR.OUT", "MSG.IN", "MSG.OUT"}) rpc := m.curr.Aggregated.RPC byhost := m.curr.ByHost if m.frozen != nil { rpc = m.frozen.Aggregated.RPC byhost = m.frozen.ByHost } if rpc == nil || len(rpc.ByDestination) == 0 { table.Render() s.WriteString("\n(no rpc connections)\n") return s.String() } hosts := make([]string, 0, len(rpc.ByDestination)) intoHost := make(map[string]madmin.RPCMetrics, len(rpc.ByDestination)) fromHost := make(map[string]madmin.RPCMetrics, len(rpc.ByDestination)) for k, v := range rpc.ByDestination { k = strings.TrimPrefix(k, "http://") k = strings.TrimPrefix(k, "https://") hosts = append(hosts, k) intoHost[k] = v } if len(byhost) > 0 { for k, v := range byhost { if v.RPC != nil { fromHost[k] = *v.RPC } } } sortBy := "" switch m.sortBy { case rpcSortReconnections: sortBy = " sorted by RECONNS" if m.showTo { sort.Slice(hosts, func(i, j int) bool { if intoHost[hosts[i]].ReconnectCount != intoHost[hosts[j]].ReconnectCount { return intoHost[hosts[i]].ReconnectCount > intoHost[hosts[j]].ReconnectCount } return hosts[i] < hosts[j] }) } else { sort.Slice(hosts, func(i, j int) bool { if fromHost[hosts[i]].ReconnectCount != fromHost[hosts[j]].ReconnectCount { return fromHost[hosts[i]].ReconnectCount > fromHost[hosts[j]].ReconnectCount } return hosts[i] < hosts[j] }) } case rpcSortQueue: sortBy = " sorted by Queue" if m.showTo { sort.Slice(hosts, func(i, j int) bool { if intoHost[hosts[i]].OutQueue != intoHost[hosts[j]].OutQueue { return intoHost[hosts[i]].OutQueue > intoHost[hosts[j]].OutQueue } return hosts[i] < hosts[j] }) } else { sort.Slice(hosts, func(i, j int) bool { if fromHost[hosts[i]].OutQueue != fromHost[hosts[j]].OutQueue { return fromHost[hosts[i]].OutQueue > fromHost[hosts[j]].OutQueue } return hosts[i] < hosts[j] }) } case rpcSortPing: sortBy = " sorted by Ping" if m.showTo { sort.Slice(hosts, func(i, j int) bool { if intoHost[hosts[i]].LastPingMS != intoHost[hosts[j]].LastPingMS { return intoHost[hosts[i]].LastPingMS > intoHost[hosts[j]].LastPingMS } return hosts[i] < hosts[j] }) } else { sort.Slice(hosts, func(i, j int) bool { if fromHost[hosts[i]].LastPingMS != fromHost[hosts[j]].LastPingMS { return fromHost[hosts[i]].LastPingMS > fromHost[hosts[j]].LastPingMS } return hosts[i] < hosts[j] }) } default: sortBy = " sorted by Host" sort.Strings(hosts) } allhosts := hosts maxHosts := max(3, globalTermHeight-4) // at least 3 hosts. m.pageSz = maxHosts truncate := len(hosts) > maxHosts && !m.quitting hostsShown := 0 if m.offset >= len(hosts)-maxHosts { m.offset = len(hosts) - maxHosts } if m.offset < 0 { m.offset = 0 } hosts = hosts[m.offset:] dataRender := make([][]string, 0, maxHosts) for _, host := range hosts { if hostsShown == maxHosts { truncate = true break } if m.showTo { if v, ok := intoHost[host]; ok { if m.sortBy == rpcSortReconnections && v.ReconnectCount == 0 { continue } dataRender = append(dataRender, []string{ fmt.Sprintf("To %s", host), fmt.Sprintf("%d", v.Connected), fmt.Sprintf("%0.1fms", v.LastPingMS), fmt.Sprintf("%ds ago", v.CollectedAt.Sub(v.LastPongTime)/time.Second), fmt.Sprintf("%d", v.OutQueue), fmt.Sprintf("%d", v.ReconnectCount), fmt.Sprintf("->%d", v.IncomingStreams), fmt.Sprintf("%d->", v.OutgoingStreams), fmt.Sprintf("%d", v.IncomingMessages), fmt.Sprintf("%d", v.OutgoingMessages), }) hostsShown++ } continue } if v, ok := fromHost[host]; ok { if m.sortBy == rpcSortReconnections && v.ReconnectCount == 0 { continue } dataRender = append(dataRender, []string{ fmt.Sprintf("From %s", host), fmt.Sprintf("%d", v.Connected), fmt.Sprintf("%0.1fms", v.LastPingMS), fmt.Sprintf("%ds ago", v.CollectedAt.Sub(v.LastPongTime)/time.Second), fmt.Sprintf("%d", v.OutQueue), fmt.Sprintf("%d", v.ReconnectCount), fmt.Sprintf("->%d", v.IncomingStreams), fmt.Sprintf("%d->", v.OutgoingStreams), fmt.Sprintf("%d", v.IncomingMessages), fmt.Sprintf("%d", v.OutgoingMessages), }) hostsShown++ } } dir := "TO" if !m.showTo { dir = "FROM" } table.AppendBulk(dataRender) table.Render() pre := "\n" if m.frozen != nil { if time.Now().UnixMilli()&512 < 256 { pre = "\n[PAUSED] " } else { pre = "\n(PAUSED) " } } s.WriteString(pre) if truncate { s.WriteString(fmt.Sprintf("SHOWING %s Host %d to %d of %d%s. ↑ and ↓ available. =TO/FROM r=RECON q=Q p=PING.", dir, 1+m.offset, m.offset+hostsShown, len(allhosts), sortBy)) } else { s.WriteString(fmt.Sprintf("SHOWING traffic %s hosts%s. =TO/FROM r=RECON q=Q p=PING.", dir, sortBy)) } return s.String() } func initTopRPCUI() *topRPCUI { s := spinner.New() s.Spinner = spinner.Points s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) return &topRPCUI{ spinner: s, } } minio-client-0.0~20250403/cmd/support-top.go000066400000000000000000000026651477450377600204210ustar00rootroot00000000000000// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var supportTopSubcommands = []cli.Command{ supportTopAPICmd, supportTopDriveCmd, supportTopLocksCmd, supportTopNetCmd, supportTopRPCCmd, } var supportTopCmd = cli.Command{ Name: "top", Usage: "provide top like statistics for MinIO", Action: mainSupportTop, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: supportTopSubcommands, HideHelpCommand: true, } // mainSupportTop is the handle for "mc support top" command. func mainSupportTop(ctx *cli.Context) error { commandNotFound(ctx, supportTopSubcommands) return nil // Sub-commands like "locks" have their own main. } minio-client-0.0~20250403/cmd/support-upload.go000066400000000000000000000103141477450377600210710ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "net/url" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) // profile command flags. var ( uploadFlags = append(globalFlags, cli.IntFlag{ Name: "issue", Usage: "SUBNET issue number to which the file is to be uploaded", }, cli.StringFlag{ Name: "comment", Usage: "comment to be posted on the issue along with the file", }, cli.BoolFlag{ Name: "enc", Usage: "encrypt content with key only accessible to minio employees", }, cli.BoolFlag{ Name: "dev", Usage: "Development mode", Hidden: true, }, ) ) type supportUploadMessage struct { Status string `json:"status"` IssueNum int `json:"-"` IssueURL string `json:"issueUrl"` } // String colorized upload message func (s supportUploadMessage) String() string { msg := fmt.Sprintf("File uploaded to SUBNET successfully. Click here to visit the issue: %s", subnetIssueURL(s.IssueNum)) return console.Colorize(supportSuccessMsgTag, msg) } // JSON jsonified upload message func (s supportUploadMessage) JSON() string { return toJSON(s) } var supportUploadCmd = cli.Command{ Name: "upload", Usage: "upload file to a SUBNET issue", Action: mainSupportUpload, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: uploadFlags, HideHelpCommand: true, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] ALIAS FILE FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Upload file './trace.log' for cluster 'myminio' to SUBNET issue number 10 {{.Prompt}} {{.HelpName}} --issue 10 myminio ./trace.log 2. Upload file './trace.log' for cluster 'myminio' to SUBNET issue number 10 with comment 'here is the trace log' {{.Prompt}} {{.HelpName}} --issue 10 --comment "here is the trace log" myminio ./trace.log `, } func checkSupportUploadSyntax(ctx *cli.Context) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } if ctx.Int("issue") <= 0 { fatal(errDummy().Trace(), "Invalid issue number") } } // mainSupportUpload is the handle for "mc support upload" command. func mainSupportUpload(ctx *cli.Context) error { // Check for command syntax checkSupportUploadSyntax(ctx) setSuccessMessageColor() // Get the alias parameter from cli aliasedURL := ctx.Args().Get(0) alias, apiKey := initSubnetConnectivity(ctx, aliasedURL, true) if len(apiKey) == 0 { // api key not passed as flag. Check that the cluster is registered. apiKey = validateClusterRegistered(alias, true) } // Main execution execSupportUpload(ctx, alias, apiKey) return nil } func execSupportUpload(ctx *cli.Context, alias, apiKey string) { filePath := ctx.Args().Get(1) issueNum := ctx.Int("issue") msg := ctx.String("comment") params := url.Values{} params.Add("issueNumber", fmt.Sprintf("%d", issueNum)) if len(msg) > 0 { params.Add("message", msg) } uploadURL := SubnetUploadURL("attachment") reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey) _, e := (&SubnetFileUploader{ alias: alias, FilePath: filePath, ReqURL: reqURL, Headers: headers, AutoCompress: true, AutoEncrypt: ctx.Bool("enc"), Params: params, }).UploadFileToSubnet() if e != nil { fatalIf(probe.NewError(e), "Unable to upload file to SUBNET") } printMsg(supportUploadMessage{IssueNum: issueNum, Status: "success", IssueURL: subnetIssueURL(issueNum)}) } minio-client-0.0~20250403/cmd/support.go000066400000000000000000000122521477450377600176120ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/pkg/v3/console" ) const ( supportSuccessMsgTag = "SupportSuccessMessage" supportErrorMsgTag = "SupportErrorMessage" ) var supportGlobalFlags = append(globalFlags, cli.BoolFlag{ Name: "dev", Usage: "Development mode", Hidden: true, }, cli.BoolFlag{ Name: "airgap", Usage: "use in environments without network access to SUBNET (e.g. airgapped, firewalled, etc.)", }, ) var supportSubcommands = []cli.Command{ supportRegisterCmd, supportCallhomeCmd, supportDiagCmd, supportPerfCmd, supportInspectCmd, supportProfileCmd, supportTopCmd, supportProxyCmd, supportUploadCmd, } var supportCmd = cli.Command{ Name: "support", Usage: "support related commands", Action: mainSupport, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: supportSubcommands, HideHelpCommand: true, } func toggleCmdArgs() set.StringSet { return set.CreateStringSet("enable", "disable", "status") } func validateToggleCmdArg(arg string) error { valid := toggleCmdArgs() if !valid.Contains(arg) { return fmt.Errorf("Invalid argument '%s'. Must be one of %v", arg, valid) } return nil } func checkToggleCmdSyntax(ctx *cli.Context) (string, string) { if len(ctx.Args()) != 2 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } arg := ctx.Args().Get(0) aliasedURL := ctx.Args().Get(1) fatalIf(probe.NewError(validateToggleCmdArg(arg)), "Invalid arguments.") alias, _ := url2Alias(aliasedURL) return alias, arg } func setSuccessMessageColor() { console.SetColor(supportSuccessMsgTag, color.New(color.FgGreen, color.Bold)) } func setErrorMessageColor() { console.SetColor(supportErrorMsgTag, color.New(color.FgYellow, color.Italic)) } func featureStatusStr(enabled bool) string { if enabled { return "enabled" } return "disabled" } func validateClusterRegistered(alias string, cmdTalksToSubnet bool) string { // Non-registered execution allowed only in following scenarios // command doesn't talk to subnet: dev mode (`--dev` passed) // command talks to subnet: dev+airgapped mode (both `--dev` and `--airgap` passed) requireRegistration := !GlobalDevMode if cmdTalksToSubnet { requireRegistration = !GlobalDevMode || !globalAirgapped } apiKey, e := getSubnetAPIKey(alias) if requireRegistration { fatalIf(probe.NewError(e), "") } return apiKey } // isFeatureEnabled - checks if a feature is enabled in MinIO config // To be used with configs that can be switched on/off using the `enable` key // e.g. subSys = logger_webhook, target = logger_webhook:subnet // Returns true if any of the following is true // - `enable` is set to `on` // - `enable` key is not found // Returns false if any of the following is true // - given subsystem is not supported by the version of MinIO // - the given target doesn't exist in the config // - `enable` is set to `off` func isFeatureEnabled(alias, subSys, target string) bool { client, err := newAdminClient(alias) // Create a new MinIO Admin Client fatalIf(err, "Unable to initialize admin connection.") if !minioConfigSupportsSubSys(client, subSys) { return false } scfgs, e := getMinIOSubSysConfig(client, subSys) if e != nil { // Ignore error if the given target doesn't exist // e.g. logger_webhook:subnet doesn't exist when // pushing logs to SUBNET has not been enabled if e.Error() == fmt.Sprintf("sub-system target '%s' doesn't exist", target) { return false } fatalIf(probe.NewError(e), fmt.Sprintf("Unable to get server config for '%s'", subSys)) } if target == madmin.Default { target = "" } for _, scfg := range scfgs { if scfg.Target == target { enable, found := scfg.Lookup(madmin.EnableKey) if !found { // if `enable` key is not found, it means that `enable=on` return true } return enable == madmin.EnableOn } } return false } func toJSON(obj interface{}) string { jsonBytes, e := json.MarshalIndent(obj, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonBytes) } // mainSupport is the handle for "mc support" command. func mainSupport(ctx *cli.Context) error { commandNotFound(ctx, supportSubcommands) return nil // Sub-commands like "register", "callhome", "diagnostics" have their own main. } minio-client-0.0~20250403/cmd/table-ui.go000066400000000000000000000025341477450377600176020ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/fatih/color" // An alias of string to represent the health color code of an object type col string const ( colGrey col = "Grey" colRed col = "Red" colYellow col = "Yellow" colGreen col = "Green" ) // getPrintCol - map color code to color for printing func getPrintCol(c col) *color.Color { switch c { case colGrey: return color.New(color.FgWhite, color.Bold) case colRed: return color.New(color.FgRed, color.Bold) case colYellow: return color.New(color.FgYellow, color.Bold) case colGreen: return color.New(color.FgGreen, color.Bold) } return nil } minio-client-0.0~20250403/cmd/tag-list.go000066400000000000000000000162251477450377600176260ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "fmt" "sort" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" "github.com/minio/pkg/v3/console" ) var tagListFlags = []cli.Flag{ cli.StringFlag{ Name: "version-id, vid", Usage: "list tags of particular object version", }, cli.StringFlag{ Name: "rewind", Usage: "list tags of particular object version at specified time", }, cli.BoolFlag{ Name: "versions", Usage: "list tags on all versions for an object", }, cli.BoolFlag{ Name: "recursive, r", Usage: "recursivley show tags for all objects", }, } var tagListCmd = cli.Command{ Name: "list", Usage: "list tags of a bucket or an object", Action: mainListTag, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(tagListFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [COMMAND FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} DESCRIPTION: List tags assigned to a bucket or an object EXAMPLES: 1. List the tags assigned to an object. {{.Prompt}} {{.HelpName}} myminio/testbucket/testobject 2. List the tags assigned to particular version of an object. {{.Prompt}} {{.HelpName}} --version-id "ieQq7aXsyhlhDt47YURGlrucYY3GxWHa" myminio/testbucket/testobject 3. List the tags assigned to an object versions that are older than one week. {{.Prompt}} {{.HelpName}} --versions --rewind 7d myminio/testbucket/testobject 4. List the tags assigned to an object in JSON format. {{.Prompt}} {{.HelpName}} --json myminio/testbucket/testobject 5. List the tags assigned to a bucket. {{.Prompt}} {{.HelpName}} myminio/testbucket 6. List the tags assigned to a bucket in JSON format. {{.Prompt}} {{.HelpName}} --json s3/testbucket 7. List the tags recursively for all the objects of subdirs of bucket. {{.Prompt}} {{.HelpName}} --recursive myminio/testbucket 8. Show the tags recursively for all versions of all objects of subdirs of bucket. {{.Prompt}} {{.HelpName}} --recursive --versions myminio/testbucket `, } // tagListMessage structure for displaying tag type tagListMessage struct { Tags map[string]string `json:"tagset,omitempty"` Status string `json:"status"` URL string `json:"url"` VersionID string `json:"versionID"` } func (t tagListMessage) JSON() string { tagJSONbytes, e := json.MarshalIndent(t, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON for "+t.URL) return string(tagJSONbytes) } func (t tagListMessage) String() string { keys := []string{} maxKeyLen := 4 // len("Name") for key := range t.Tags { keys = append(keys, key) if len(key) > maxKeyLen { maxKeyLen = len(key) } } sort.Strings(keys) maxKeyLen += 2 // add len(" :") strName := t.URL if strings.TrimSpace(t.VersionID) != "" { strName += " (" + t.VersionID + ")" } strs := []string{ fmt.Sprintf("%v%*v %v", console.Colorize("Name", "Name"), maxKeyLen-4, ":", console.Colorize("Name", strName)), } for _, key := range keys { strs = append( strs, fmt.Sprintf("%v%*v %v", console.Colorize("Key", key), maxKeyLen-len(key), ":", console.Colorize("Value", t.Tags[key])), ) } if len(keys) == 0 { strs = append(strs, console.Colorize("NoTags", "No tags found")) } return strings.Join(strs, "\n") } // parseTagListSyntax performs command-line input validation for tag list command. func parseTagListSyntax(ctx *cli.Context) (targetURL, versionID string, timeRef time.Time, withVersions, recursive bool) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, globalErrorExitStatus) } targetURL = ctx.Args().Get(0) versionID = ctx.String("version-id") withVersions = ctx.Bool("versions") rewind := ctx.String("rewind") recursive = ctx.Bool("recursive") if versionID != "" && rewind != "" { fatalIf(errDummy().Trace(), "You cannot specify both --version-id and --rewind flags at the same time") } timeRef = parseRewindFlag(rewind) return } // showTags pretty prints tags of a bucket or a specified object/version func showTags(ctx context.Context, clnt Client, versionID string) { targetName := clnt.GetURL().String() if versionID != "" { targetName += " (" + versionID + ")" } tagsMap, err := clnt.GetTags(ctx, versionID) if err != nil { if minio.ToErrorResponse(err.ToGoError()).Code == "NoSuchTagSet" { fatalIf(probe.NewError(errors.New("check 'mc tag set --help' on how to set tags")), "No tags found for "+targetName) } fatalIf(err, "Unable to fetch tags for "+targetName) return } printMsg(tagListMessage{ Tags: tagsMap, Status: "success", URL: clnt.GetURL().String(), VersionID: versionID, }) } func showTagsSingle(ctx context.Context, alias, url, versionID string) *probe.Error { newClnt, err := newClientFromAlias(alias, url) if err != nil { return err } showTags(ctx, newClnt, versionID) return nil } func mainListTag(cliCtx *cli.Context) error { ctx, cancelListTag := context.WithCancel(globalContext) defer cancelListTag() console.SetColor("Name", color.New(color.Bold, color.FgCyan)) console.SetColor("Key", color.New(color.FgGreen)) console.SetColor("Value", color.New(color.FgYellow)) console.SetColor("NoTags", color.New(color.FgRed)) targetURL, versionID, timeRef, withVersions, recursive := parseTagListSyntax(cliCtx) if timeRef.IsZero() && withVersions { timeRef = time.Now().UTC() } clnt, err := newClient(targetURL) fatalIf(err, "Unable to initialize target "+targetURL) alias, urlStr, _ := mustExpandAlias(targetURL) if timeRef.IsZero() && !withVersions && !recursive { err := showTagsSingle(ctx, alias, urlStr, versionID) fatalIf(err.Trace(), "Unable to show tags on `%s`", targetURL) return nil } for content := range clnt.List(ctx, ListOptions{TimeRef: timeRef, WithOlderVersions: withVersions, Recursive: recursive}) { if content.Err != nil { fatalIf(content.Err.Trace(), "Unable to list target "+targetURL) continue } // Skip if its delete marker if content.IsDeleteMarker { continue } if !recursive && getStandardizedURL(alias+getKey(content)) != getStandardizedURL(targetURL) { break } err := showTagsSingle(ctx, alias, content.URL.String(), content.VersionID) if err != nil { errorIf(err.Trace(clnt.GetURL().String()), "Invalid URL") continue } } return nil } minio-client-0.0~20250403/cmd/tag-main.go000066400000000000000000000023431477450377600175730ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/cli" ) var tagSubcommands = []cli.Command{ tagListCmd, tagRemoveCmd, tagSetCmd, } var tagCmd = cli.Command{ Name: "tag", Usage: "manage tags for bucket and object(s)", Action: mainTag, Before: setGlobalsFromContext, Flags: globalFlags, HideHelpCommand: true, Subcommands: tagSubcommands, } func mainTag(ctx *cli.Context) error { commandNotFound(ctx, tagSubcommands) return nil } minio-client-0.0~20250403/cmd/tag-remove.go000066400000000000000000000136231477450377600201470ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var tagRemoveFlags = []cli.Flag{ cli.StringFlag{ Name: "version-id, vid", Usage: "remove tags on a specific object version", }, cli.StringFlag{ Name: "rewind", Usage: "remove tags on an object version at specified time", }, cli.BoolFlag{ Name: "versions", Usage: "remove tags on multiple versions of an object", }, cli.BoolFlag{ Name: "recursive, r", Usage: "recursivley remove tags for all objects", }, } var tagRemoveCmd = cli.Command{ Name: "remove", Usage: "remove tags assigned to a bucket or an object", Action: mainRemoveTag, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(tagRemoveFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [COMMAND FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} DESCRIPTION: Remove tags assigned to a bucket or an object. EXAMPLES: 1. Remove the tags assigned to an object. {{.Prompt}} {{.HelpName}} myminio/testbucket/testobject 2. Remove the tags assigned to a particular version of an object. {{.Prompt}} {{.HelpName}} --version-id "ieQq7aXsyhlhDt47YURGlrucYY3GxWHa" myminio/testbucket/testobject 3. Remove the tags assigned to an object versions that are older than one week {{.Prompt}} {{.HelpName}} --versions --rewind 7d myminio/testbucket/testobject 4. Remove the tags assigned to a bucket. {{.Prompt}} {{.HelpName}} play/testbucket 5. Remove the tags recursively for all the objects of subdirs of bucket. {{.Prompt}} {{.HelpName}} --recursive myminio/testbucket 6. Remove the tags recursively for all versions of all objects of subdirs of bucket. {{.Prompt}} {{.HelpName}} --recursive --versions myminio/testbucket `, } // tagSetTagMessage structure will show message depending on the type of console. type tagRemoveMessage struct { Status string `json:"status"` Name string `json:"name"` VersionID string `json:"versionID"` } // tagRemoveMessage console colorized output. func (t tagRemoveMessage) String() string { var msg string msg += "Tags removed for " + t.Name if strings.TrimSpace(t.VersionID) != "" { msg += " (" + t.VersionID + ")" } msg += "." return console.Colorize("Remove", msg) } // JSON tagRemoveMessage. func (t tagRemoveMessage) JSON() string { msgBytes, e := json.MarshalIndent(t, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } func parseRemoveTagSyntax(ctx *cli.Context) (targetURL, versionID string, timeRef time.Time, withVersions, recursive bool) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, globalErrorExitStatus) } targetURL = ctx.Args().Get(0) versionID = ctx.String("version-id") withVersions = ctx.Bool("versions") rewind := ctx.String("rewind") recursive = ctx.Bool("recursive") if versionID != "" && (rewind != "" || withVersions) { fatalIf(errDummy().Trace(), "You cannot specify both --version-id and --rewind or --versions flags at the same time") } timeRef = parseRewindFlag(rewind) return } // Delete tags of a bucket or a specified object/version func deleteTags(ctx context.Context, clnt Client, versionID string) { targetName := clnt.GetURL().String() if versionID != "" { targetName += " (" + versionID + ")" } err := clnt.DeleteTags(ctx, versionID) if err != nil { fatalIf(err, "Unable to remove tags for "+targetName) return } printMsg(tagRemoveMessage{ Status: "success", Name: clnt.GetURL().String(), VersionID: versionID, }) } func deleteTagsSingle(ctx context.Context, alias, url, versionID string) *probe.Error { newClnt, err := newClientFromAlias(alias, url) if err != nil { return err } deleteTags(ctx, newClnt, versionID) return nil } func mainRemoveTag(cliCtx *cli.Context) error { ctx, cancelList := context.WithCancel(globalContext) defer cancelList() console.SetColor("Remove", color.New(color.FgGreen)) targetURL, versionID, timeRef, withVersions, recursive := parseRemoveTagSyntax(cliCtx) if timeRef.IsZero() && withVersions { timeRef = time.Now().UTC() } clnt, pErr := newClient(targetURL) fatalIf(pErr, "Unable to initialize target "+targetURL) alias, urlStr, _ := mustExpandAlias(targetURL) if timeRef.IsZero() && !withVersions && !recursive { err := deleteTagsSingle(ctx, alias, urlStr, versionID) fatalIf(err.Trace(), "Unable to remove tags on `%s`", targetURL) return nil } for content := range clnt.List(ctx, ListOptions{TimeRef: timeRef, WithOlderVersions: withVersions, Recursive: recursive}) { if content.Err != nil { fatalIf(content.Err.Trace(), "Unable to list target "+targetURL) } // Skip if its delete marker if content.IsDeleteMarker { continue } if !recursive && getStandardizedURL(alias+getKey(content)) != getStandardizedURL(targetURL) { break } err := deleteTagsSingle(ctx, alias, content.URL.String(), content.VersionID) if err != nil { errorIf(err.Trace(clnt.GetURL().String()), "Invalid URL") continue } } return nil } minio-client-0.0~20250403/cmd/tag-set.go000066400000000000000000000154531477450377600174500ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var tagSetFlags = []cli.Flag{ cli.StringFlag{ Name: "version-id, vid", Usage: "set tags on a specific object version", }, cli.StringFlag{ Name: "rewind", Usage: "set tags on a specific object version at specific time", }, cli.BoolFlag{ Name: "versions", Usage: "set tags on multiple versions for an object", }, cli.BoolFlag{ Name: "recursive, r", Usage: "recursivley set tags for all objects of subdirs", }, cli.BoolFlag{ Name: "exclude-folders", Usage: "exclude setting tags on folder objects", }, } var tagSetCmd = cli.Command{ Name: "set", Usage: "set tags for a bucket and object(s)", Action: mainSetTag, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(tagSetFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [COMMAND FLAGS] TARGET TAGS FLAGS: {{range .VisibleFlags}}{{.}} {{end}} DESCRIPTION: Assign tags to a bucket or an object. EXAMPLES: 1. Assign tags to an object. {{.Prompt}} {{.HelpName}} play/testbucket/testobject "key1=value1&key2=value2&key3=value3" 2. Assign tags to a particuler version of an object. {{.Prompt}} {{.HelpName}} --version-id "ieQq7aXsyhlhDt47YURGlrucYY3GxWHa" play/testbucket/testobject "key1=value1&key2=value2&key3=value3" 3. Assign tags to a object versions older than one week. {{.Prompt}} {{.HelpName}} --versions --rewind 7d play/testbucket/testobject "key1=value1&key2=value2&key3=value3" 4. Assign tags to a bucket. {{.Prompt}} {{.HelpName}} myminio/testbucket "key1=value1&key2=value2&key3=value3" 5. Assign tags recursively to all the objects of subdirs of bucket. {{.Prompt}} {{.HelpName}} myminio/testbucket --recursive "key1=value1&key2=value2&key3=value3" 6. Assign tags recursively to all versions of all objects of subdirs of bucket. {{.Prompt}} {{.HelpName}} myminio/testbucket --recursive --versions "key1=value1&key2=value2&key3=value3" 7. Assign tags to all the objects on a bucket, excluding folders {{.Prompt}} {{.HelpName}} myminio/testbucket --exclude-folders --recursive "key1=value1&key2=value2&key3=value3" `, } // tagSetTagMessage structure will show message depending on the type of console. type tagSetMessage struct { Status string `json:"status"` Name string `json:"name"` VersionID string `json:"versionID"` } // tagSetMessage console colorized output. func (t tagSetMessage) String() string { var msg string msg += "Tags set for " + t.Name if t.VersionID != "" { msg += " (" + t.VersionID + ")" } msg += "." return console.Colorize("List", msg) } // JSON tagSetMessage. func (t tagSetMessage) JSON() string { msgBytes, e := json.MarshalIndent(t, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(msgBytes) } func parseSetTagSyntax(ctx *cli.Context) (targetURL, versionID string, timeRef time.Time, withVersions bool, tags string, recursive bool, excludeFolders bool) { if len(ctx.Args()) != 2 || ctx.Args().Get(1) == "" { showCommandHelpAndExit(ctx, globalErrorExitStatus) } targetURL = ctx.Args().Get(0) tags = ctx.Args().Get(1) versionID = ctx.String("version-id") withVersions = ctx.Bool("versions") rewind := ctx.String("rewind") recursive = ctx.Bool("recursive") excludeFolders = ctx.Bool("exclude-folders") if versionID != "" && (rewind != "" || withVersions) { fatalIf(errDummy().Trace(), "You cannot specify both --version-id and --rewind or --versions flags at the same time") } if excludeFolders && !recursive { fatalIf(errDummy().Trace(), "'--exclude-folders' must be used with --recursive only") } timeRef = parseRewindFlag(rewind) return } // Set tags to a bucket or to a specified object/version func setTags(ctx context.Context, clnt Client, versionID, tags string) { targetName := clnt.GetURL().String() if versionID != "" { targetName += " (" + versionID + ")" } err := clnt.SetTags(ctx, versionID, tags) if err != nil { fatalIf(err.Trace(tags), "Failed to set tags for "+targetName) return } printMsg(tagSetMessage{ Status: "success", Name: clnt.GetURL().String(), VersionID: versionID, }) } func setTagsSingle(ctx context.Context, alias, url, versionID, tags string) *probe.Error { newClnt, err := newClientFromAlias(alias, url) if err != nil { return err } setTags(ctx, newClnt, versionID, tags) return nil } func mainSetTag(cliCtx *cli.Context) error { ctx, cancelSetTag := context.WithCancel(globalContext) defer cancelSetTag() console.SetColor("List", color.New(color.FgGreen)) targetURL, versionID, timeRef, withVersions, tags, recursive, excludeFolders := parseSetTagSyntax(cliCtx) if timeRef.IsZero() && withVersions { timeRef = time.Now().UTC() } clnt, err := newClient(targetURL) fatalIf(err.Trace(cliCtx.Args()...), "Unable to initialize target "+targetURL) alias, urlStr, _ := mustExpandAlias(targetURL) if timeRef.IsZero() && !withVersions && !recursive && !excludeFolders { err := setTagsSingle(ctx, alias, urlStr, versionID, tags) fatalIf(err.Trace(), "Unable to set tags on `%s`", targetURL) return nil } for content := range clnt.List(ctx, ListOptions{TimeRef: timeRef, WithOlderVersions: withVersions, Recursive: recursive}) { if content.Err != nil { fatalIf(content.Err.Trace(), "Unable to list target "+targetURL) continue } // Dont set tag for the delete marker if content.IsDeleteMarker { continue } // if excludeFolders dont set tags for subdirs _, objName := url2BucketAndObject(&content.URL) if strings.Index(objName, string(content.URL.Separator)) > 0 && excludeFolders { continue } if !recursive && getStandardizedURL(alias+getKey(content)) != getStandardizedURL(targetURL) { break } err := setTagsSingle(ctx, alias, content.URL.String(), content.VersionID, tags) if err != nil { errorIf(err.Trace(clnt.GetURL().String()), "Invalid URL") continue } } return nil } minio-client-0.0~20250403/cmd/term-pager.go000066400000000000000000000110571477450377600201430ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "math" "os" "strings" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/muesli/reflow/wordwrap" ) var percentStyle = lipgloss.NewStyle().Width(4).Align(lipgloss.Left) type model struct { viewport viewport.Model content string ready bool renderedOnce chan struct{} } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( cmd tea.Cmd cmds []tea.Cmd ) switch msg := msg.(type) { case string: m.content += msg m.viewport.SetContent(wordwrap.String(m.content, m.viewport.Width-2)) case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q", "esc": return m, tea.Quit } case tea.WindowSizeMsg: headerHeight := lipgloss.Height(m.headerView()) footerHeight := lipgloss.Height(m.footerView()) verticalMarginHeight := headerHeight + footerHeight if !m.ready { // Since this program is using the full size of the viewport we // need to wait until we've received the window dimensions before // we can initialize the viewport. The initial dimensions come in // quickly, though asynchronously, which is why we wait for them // here. m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) m.viewport.YPosition = headerHeight m.viewport.SetContent(m.content) m.ready = true close(m.renderedOnce) } else { m.viewport.Width = msg.Width m.viewport.Height = msg.Height - verticalMarginHeight } } // Handle keyboard and mouse events in the viewport m.viewport, cmd = m.viewport.Update(msg) cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } func (m model) View() string { if !m.ready { return "\n Initializing..." } return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView()) } func (m model) headerView() string { info := " (q)uit/esc" line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info))) return lipgloss.JoinHorizontal(lipgloss.Center, line, info) } func (m model) footerView() string { // Disables printing if the viewport is not ready if m.viewport.Width == 0 { return "" } if math.IsNaN(m.viewport.ScrollPercent()) { return "" } viewP := int(m.viewport.ScrollPercent() * 100) info := fmt.Sprintf(" %s", percentStyle.Render(fmt.Sprintf("%d%%", viewP))) totalLength := m.viewport.Width - lipgloss.Width(info) finishedCount := int((float64(totalLength) / 100) * float64(viewP)) return lipgloss.JoinHorizontal( lipgloss.Center, info, strings.Repeat("/", finishedCount), strings.Repeat("─", max(0, totalLength-finishedCount)), ) } type termPager struct { initialized bool model *model teaPager *tea.Program buf chan []byte statusCh chan error } func (tp *termPager) init() { tp.statusCh = make(chan error) tp.buf = make(chan []byte) tp.model = &model{renderedOnce: make(chan struct{})} go func() { tp.teaPager = tea.NewProgram( tp.model, ) go func() { _, e := tp.teaPager.Run() tp.statusCh <- e close(tp.statusCh) }() fallback := false select { case <-tp.model.renderedOnce: case err := <-tp.statusCh: if err != nil { fallback = true } } for { select { case s := <-tp.buf: if !fallback { tp.teaPager.Send(string(s)) } else { os.Stdout.Write(s) } case <-tp.statusCh: return } } }() tp.initialized = true } func (tp *termPager) Write(p []byte) (int, error) { if !tp.initialized { tp.init() } tp.buf <- p return len(p), nil } func (tp *termPager) WaitForExit() { if !tp.initialized { return } // Wait until the term pager this is closed // which is trigerred when there is an error // or the user quits for status := range tp.statusCh { _ = status } } func newTermPager() *termPager { return &termPager{} } minio-client-0.0~20250403/cmd/tofu.go000066400000000000000000000127021477450377600170530ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bufio" "bytes" "context" "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" "crypto/sha1" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/asn1" "encoding/hex" "encoding/pem" "fmt" "math/big" "net/http" "os" "path/filepath" "strings" "github.com/fatih/color" "github.com/minio/mc/pkg/probe" ) func marshalPublicKey(pub interface{}) (publicKeyBytes []byte, e error) { // pkcs1PublicKey reflects the ASN.1 structure of a PKCS #1 public key. type pkcs1PublicKey struct { N *big.Int E int } switch pub := pub.(type) { case *rsa.PublicKey: publicKeyBytes, e = asn1.Marshal(pkcs1PublicKey{ N: pub.N, E: pub.E, }) if e != nil { return nil, e } case *ecdsa.PublicKey: pubKey, e := pub.ECDH() if e != nil { return nil, e } publicKeyBytes = pubKey.Bytes() case ed25519.PublicKey: publicKeyBytes = pub default: return nil, fmt.Errorf("x509: unsupported public key type: %T", pub) } return publicKeyBytes, nil } // promptTrustSelfSignedCert connects to the given endpoint and // checks whether the peer certificate can be verified. // If not, it computes a fingerprint of the peer certificate // public key, asks the user to confirm the fingerprint and // adds the peer certificate to the local trust store in the // CAs directory. func promptTrustSelfSignedCert(ctx context.Context, endpoint, alias string) (*x509.Certificate, *probe.Error) { req, e := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if e != nil { return nil, probe.NewError(e) } // no need to probe certs for http endpoints. if req.URL.Scheme == "http" { return nil, nil } client := http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, DialTLSContext: newCustomDialTLSContext(&tls.Config{ RootCAs: globalRootCAs, // make sure to use loaded certs before probing }), }, } _, te := client.Do(req) if te == nil { // certs are already trusted system wide, nothing to do. return nil, nil } if !strings.Contains(te.Error(), "certificate signed by unknown authority") && !strings.Contains(te.Error(), "certificate is not trusted") /* darwin specific error message */ { return nil, probe.NewError(te) } // Now, we fetch the peer certificate, compute the SHA-256 of // public key and let the user confirm the fingerprint. // If the user confirms, we store the peer certificate in the CAs // directory and retry. peerCert, e := fetchPeerCertificate(ctx, endpoint) if e != nil { return nil, probe.NewError(e) } if peerCert.IsCA && len(peerCert.AuthorityKeyId) == 0 { // If peerCert is its own CA then AuthorityKeyId will be empty // which means the SubjeyKeyId is the sha1.Sum(publicKeyBytes) // Refer - SubjectKeyId generated using method 1 in RFC 5280, Section 4.2.1.2: publicKeyBytes, e := marshalPublicKey(peerCert.PublicKey) if e != nil { return nil, probe.NewError(e) } h := sha1.Sum(publicKeyBytes) if !bytes.Equal(h[:], peerCert.SubjectKeyId) { return nil, probe.NewError(te) } } else { // Check that the subject key id is equal to the authority key id. // If true, the certificate is its own issuer, and therefore, a // self-signed certificate. Otherwise, the certificate has been // issued by some other certificate that is just not trusted. if !bytes.Equal(peerCert.SubjectKeyId, peerCert.AuthorityKeyId) { return nil, probe.NewError(te) } } fingerprint := sha256.Sum256(peerCert.RawSubjectPublicKeyInfo) fmt.Printf("Fingerprint of %s public key: %s\nConfirm public key y/N: ", color.GreenString(alias), color.YellowString(hex.EncodeToString(fingerprint[:]))) answer, e := bufio.NewReader(os.Stdin).ReadString('\n') if e != nil { return nil, probe.NewError(e) } if answer = strings.ToLower(answer); answer != "y\n" && answer != "yes\n" { return nil, probe.NewError(te) } certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: peerCert.Raw}) if e = os.WriteFile(filepath.Join(mustGetCAsDir(), alias+".crt"), certPEM, 0o644); e != nil { return nil, probe.NewError(e) } return peerCert, nil } // fetchPeerCertificate uses the given transport to fetch the peer // certificate from the given endpoint. func fetchPeerCertificate(ctx context.Context, endpoint string) (*x509.Certificate, error) { req, e := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if e != nil { return nil, e } client := http.Client{ Transport: &http.Transport{ DialTLSContext: newCustomDialTLSContext(&tls.Config{ InsecureSkipVerify: true, }), }, } resp, e := client.Do(req) if e != nil { return nil, e } if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 { return nil, fmt.Errorf("Unable to read remote TLS certificate") } return resp.TLS.PeerCertificates[0], nil } minio-client-0.0~20250403/cmd/top-drives-spinner.go000066400000000000000000000167501477450377600216550ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "cmp" "fmt" "sort" "strings" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/minio/madmin-go/v3" "github.com/olekukonko/tablewriter" ) type topDriveUI struct { spinner spinner.Model quitting bool sortBy drivesSorter sortAsc bool count int pool, maxPool int drivesInfo map[string]madmin.Disk prevTopMap map[string]madmin.DiskIOStats currTopMap map[string]madmin.DiskIOStats } type topDriveResult struct { final bool diskName string stats madmin.DiskIOStats } func initTopDriveUI(disks []madmin.Disk, count int) *topDriveUI { maxPool := 0 drivesInfo := make(map[string]madmin.Disk) for i := range disks { drivesInfo[disks[i].Endpoint] = disks[i] if disks[i].PoolIndex > maxPool { maxPool = disks[i].PoolIndex } } s := spinner.New() s.Spinner = spinner.Points s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) return &topDriveUI{ count: count, sortBy: sortByName, pool: 0, maxPool: maxPool, drivesInfo: drivesInfo, spinner: s, prevTopMap: make(map[string]madmin.DiskIOStats), currTopMap: make(map[string]madmin.DiskIOStats), } } func (m *topDriveUI) Init() tea.Cmd { return m.spinner.Tick } func (m *topDriveUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q", "esc": m.quitting = true return m, tea.Quit case "right": m.pool++ if m.pool >= m.maxPool { m.pool = m.maxPool } case "left": m.pool-- if m.pool < 0 { m.pool = 0 } case "u": m.sortBy = sortByUsed case "t": m.sortBy = sortByTps case "r": m.sortBy = sortByRead case "w": m.sortBy = sortByWrite case "d": m.sortBy = sortByDiscard case "a": m.sortBy = sortByAwait case "U": m.sortBy = sortByUtil case "o", "O": m.sortAsc = !m.sortAsc } return m, nil case topDriveResult: m.prevTopMap[msg.diskName] = m.currTopMap[msg.diskName] m.currTopMap[msg.diskName] = msg.stats if msg.final { m.quitting = true return m, tea.Quit } return m, nil case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd default: return m, nil } } type driveIOStat struct { endpoint string util float64 await float64 readMBs float64 writeMBs float64 discardMBs float64 tps uint64 used uint64 } func generateDriveStat(disk madmin.Disk, curr, prev madmin.DiskIOStats, interval uint64) (d driveIOStat) { if disk.TotalSpace == 0 { return d } d.endpoint = disk.Endpoint d.used = 100 * disk.UsedSpace / disk.TotalSpace d.util = 100 * float64(curr.TotalTicks-prev.TotalTicks) / float64(interval) currTotalIOs := curr.ReadIOs + curr.WriteIOs + curr.DiscardIOs prevTotalIOs := prev.ReadIOs + prev.WriteIOs + prev.DiscardIOs totalTicksDiff := curr.ReadTicks - prev.ReadTicks + curr.WriteTicks - prev.WriteTicks + curr.DiscardTicks - prev.DiscardTicks if currTotalIOs > prevTotalIOs { d.tps = currTotalIOs - prevTotalIOs d.await = float64(totalTicksDiff) / float64(currTotalIOs-prevTotalIOs) } intervalInSec := float64(interval / 1000) d.readMBs = float64(curr.ReadSectors-prev.ReadSectors) / (2048 * intervalInSec) d.writeMBs = float64(curr.WriteSectors-prev.WriteSectors) / (2048 * intervalInSec) d.discardMBs = float64(curr.DiscardSectors-prev.DiscardSectors) / (2048 * intervalInSec) return d } type drivesSorter int const ( sortByName drivesSorter = iota sortByUsed sortByAwait sortByUtil sortByRead sortByWrite sortByDiscard sortByTps ) func (s drivesSorter) String() string { switch s { case sortByName: return "name" case sortByUsed: return "used" case sortByAwait: return "await" case sortByUtil: return "util" case sortByRead: return "read" case sortByWrite: return "write" case sortByDiscard: return "discard" case sortByTps: return "tps" } return "unknown" } func sortDriveIOStat(sortBy drivesSorter, asc bool, data []driveIOStat) { sort.SliceStable(data, func(i, j int) bool { c := 0 switch sortBy { case sortByName: c = cmp.Compare(data[i].endpoint, data[j].endpoint) case sortByUsed: c = cmp.Compare(data[i].used, data[j].used) case sortByAwait: c = cmp.Compare(data[i].await, data[j].await) case sortByUtil: c = cmp.Compare(data[i].util, data[j].util) case sortByRead: c = cmp.Compare(data[i].readMBs, data[j].readMBs) case sortByWrite: c = cmp.Compare(data[i].writeMBs, data[j].writeMBs) case sortByDiscard: c = cmp.Compare(data[i].discardMBs, data[j].discardMBs) case sortByTps: c = cmp.Compare(data[i].tps, data[j].tps) } less := c < 0 if sortBy != sortByName && !asc { less = !less } return less }) } func (m *topDriveUI) View() string { var s strings.Builder s.WriteString("\n") // Set table header table := tablewriter.NewWriter(&s) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_CENTER) table.SetAlignment(tablewriter.ALIGN_CENTER) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) table.SetHeader([]string{"Drive", "used", "tps", "read", "write", "discard", "await", "util"}) var data []driveIOStat for disk := range m.currTopMap { currDisk, ok := m.drivesInfo[disk] if !ok || currDisk.PoolIndex != m.pool { continue } data = append(data, generateDriveStat(m.drivesInfo[disk], m.currTopMap[disk], m.prevTopMap[disk], 1000)) } sortDriveIOStat(m.sortBy, m.sortAsc, data) if len(data) > m.count { data = data[:m.count] } dataRender := make([][]string, 0, len(data)) for _, d := range data { endpoint := d.endpoint diskInfo := m.drivesInfo[endpoint] if diskInfo.Healing { endpoint += "!" } if diskInfo.Scanning { endpoint += "*" } if diskInfo.TotalSpace == 0 { endpoint += crossTickCell } dataRender = append(dataRender, []string{ endpoint, whiteStyle.Render(fmt.Sprintf("%d%%", d.used)), whiteStyle.Render(fmt.Sprintf("%v", d.tps)), whiteStyle.Render(fmt.Sprintf("%.2f MiB/s", d.readMBs)), whiteStyle.Render(fmt.Sprintf("%.2f MiB/s", d.writeMBs)), whiteStyle.Render(fmt.Sprintf("%.2f MiB/s", d.discardMBs)), whiteStyle.Render(fmt.Sprintf("%.1f ms", d.await)), whiteStyle.Render(fmt.Sprintf("%.1f%%", d.util)), }) } table.AppendBulk(dataRender) table.Render() if !m.quitting { order := "DESC" if m.sortAsc { order = "ASC" } s.WriteString(fmt.Sprintf("\n%s \u25C0 Pool %d \u25B6 | Sort By: %s (u,t,r,w,d,a,U) | (O)rder: %s ", m.spinner.View(), m.pool+1, m.sortBy, order)) } return s.String() + "\n" } minio-client-0.0~20250403/cmd/top-net-spinner.go000066400000000000000000000111271477450377600211400ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "math" "sort" "strings" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/dustin/go-humanize" "github.com/minio/madmin-go/v3" "github.com/olekukonko/tablewriter" "github.com/prometheus/procfs" ) type topNetUI struct { spinner spinner.Model quitting bool sortAsc bool prevTopMap map[string]topNetResult currTopMap map[string]topNetResult } type topNetResult struct { final bool endPoint string error string stats madmin.NetMetrics } func (t topNetResult) GetTotalBytes() uint64 { return t.stats.NetStats.RxBytes + t.stats.NetStats.TxBytes } func (m *topNetUI) Init() tea.Cmd { return m.spinner.Tick } func (m *topNetUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q", "esc": m.quitting = true return m, tea.Quit } return m, nil case topNetResult: m.prevTopMap[msg.endPoint] = m.currTopMap[msg.endPoint] m.currTopMap[msg.endPoint] = msg if msg.final { m.quitting = true return m, tea.Quit } return m, nil case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd default: return m, nil } } func (m *topNetUI) calculationRate(prev, curr uint64, dur time.Duration) uint64 { if curr < prev { return uint64(float64(math.MaxUint64-prev+curr) / dur.Seconds()) } return uint64(float64(curr-prev) / dur.Seconds()) } func (m *topNetUI) View() string { var s strings.Builder // Set table header table := tablewriter.NewWriter(&s) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_CENTER) table.SetAlignment(tablewriter.ALIGN_CENTER) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) table.SetHeader([]string{"SERVER", "INTERFACE", "RECEIVE", "TRANSMIT", ""}) data := make([]topNetResult, 0, len(m.currTopMap)) for endPoint, curr := range m.currTopMap { if prev, ok := m.prevTopMap[endPoint]; ok { data = append(data, topNetResult{ final: curr.final, endPoint: curr.endPoint, error: curr.error, stats: madmin.NetMetrics{ CollectedAt: curr.stats.CollectedAt, InterfaceName: curr.stats.InterfaceName, NetStats: procfs.NetDevLine{ RxBytes: m.calculationRate(prev.stats.NetStats.RxBytes, curr.stats.NetStats.RxBytes, curr.stats.CollectedAt.Sub(prev.stats.CollectedAt)), TxBytes: m.calculationRate(prev.stats.NetStats.TxBytes, curr.stats.NetStats.TxBytes, curr.stats.CollectedAt.Sub(prev.stats.CollectedAt)), }, }, }) } } sort.Slice(data, func(i, j int) bool { if m.sortAsc { return data[i].GetTotalBytes() < data[j].GetTotalBytes() } return data[i].GetTotalBytes() >= data[j].GetTotalBytes() }) dataRender := make([][]string, 0, len(data)) for _, d := range data { if d.error == "" { dataRender = append(dataRender, []string{ d.endPoint, whiteStyle.Render(d.stats.InterfaceName), whiteStyle.Render(fmt.Sprintf("%s/s", humanize.IBytes(d.stats.NetStats.RxBytes))), whiteStyle.Render(fmt.Sprintf("%s/s", humanize.IBytes(d.stats.NetStats.TxBytes))), "", }) } else { dataRender = append(dataRender, []string{ d.endPoint, whiteStyle.Render(d.stats.NetStats.Name), crossTickCell, crossTickCell, d.error, }) } } table.AppendBulk(dataRender) table.Render() return s.String() } func initTopNetUI() *topNetUI { s := spinner.New() s.Spinner = spinner.Points s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) return &topNetUI{ spinner: s, currTopMap: make(map[string]topNetResult), prevTopMap: make(map[string]topNetResult), } } minio-client-0.0~20250403/cmd/trace-stats-ui.go000066400000000000000000000236241477450377600207500ustar00rootroot00000000000000// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "os" "sort" "strconv" "strings" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/madmin-go/v3" "github.com/minio/pkg/v3/console" "github.com/muesli/reflow/truncate" "github.com/olekukonko/tablewriter" "golang.org/x/term" ) type traceStatsUI struct { current *statTrace started time.Time meter spinner.Model quitting bool maxEntries int offset int allFlag bool } func (m *traceStatsUI) Init() tea.Cmd { return m.meter.Tick } func (m *traceStatsUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.quitting { return m, tea.Quit } switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "esc", "ctrl+c": m.quitting = true return m, tea.Quit case "r": // Reset min/max m.current.mu.Lock() defer m.current.mu.Unlock() for k, si := range m.current.Calls { si.MaxDur, si.MinDur = 0, 0 si.MaxTTFB = 0 m.current.Calls[k] = si } return m, nil case "down": m.offset++ case "up": m.offset-- case "home": m.offset = 0 case "end": m.offset = len(m.current.Calls) default: return m, nil } case spinner.TickMsg: var cmd tea.Cmd m.meter, cmd = m.meter.Update(msg) return m, cmd } return m, nil } func (m *traceStatsUI) View() string { var s strings.Builder dur := m.current.Latest.Sub(m.current.Oldest) s.WriteString(fmt.Sprintf("%s %s\n", console.Colorize("metrics-top-title", "Duration: "+dur.Round(time.Second).String()), m.meter.View())) // Set table header - akin to k8s style // https://github.com/olekukonko/tablewriter#example-10---set-nowhitespace-and-tablepadding-option table := tablewriter.NewWriter(&s) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding(" ") // pad with tabs table.SetNoWhiteSpace(true) var entries []statItem m.current.mu.Lock() var ( totalCnt = 0 totalRX = 0 totalTX = 0 ) for _, v := range m.current.Calls { totalCnt += v.Count totalRX += v.CallStats.Rx totalTX += v.CallStats.Tx entries = append(entries, v) } m.current.mu.Unlock() if len(entries) == 0 { s.WriteString("(waiting for data)") return s.String() } sort.Slice(entries, func(i, j int) bool { if entries[i].Count == entries[j].Count { return entries[i].Name < entries[j].Name } return entries[i].Count > entries[j].Count }) frontTrunc := false endTrunc := false m.offset = min(max(0, m.offset), len(entries)) offset := m.offset if m.maxEntries > 0 && len(entries) > m.maxEntries { entLeft := len(entries) - offset if entLeft > m.maxEntries { // Truncate both ends entries = entries[offset : m.maxEntries+offset] frontTrunc = offset > 0 endTrunc = true } else { entries = entries[len(entries)-m.maxEntries:] frontTrunc = true } } hasTTFB := false for _, e := range entries { if e.TTFB > 0 { hasTTFB = true break } } if !m.allFlag { if totalRX > 0 { s.WriteString(console.Colorize("metrics-top-title", fmt.Sprintf("RX Rate:↑ %s/m\n", humanize.IBytes(uint64(float64(totalRX)/dur.Minutes()))))) } if totalTX > 0 { s.WriteString(console.Colorize("metrics-top-title", fmt.Sprintf("TX Rate:↓ %s/m\n", humanize.IBytes(uint64(float64(totalTX)/dur.Minutes()))))) } } s.WriteString(console.Colorize("metrics-top-title", fmt.Sprintf("RPM : %0.1f\n", float64(totalCnt)/dur.Minutes()))) s.WriteString("-------------\n") preCall := "" if frontTrunc { preCall = console.Colorize("metrics-error", "↑ ") } t := []string{ preCall + console.Colorize("metrics-top-title", "Call"), console.Colorize("metrics-top-title", "Count"), console.Colorize("metrics-top-title", "RPM"), console.Colorize("metrics-top-title", "Avg Time"), console.Colorize("metrics-top-title", "Min Time"), console.Colorize("metrics-top-title", "Max Time"), } if hasTTFB { t = append(t, console.Colorize("metrics-top-title", "Avg TTFB"), console.Colorize("metrics-top-title", "Max TTFB"), ) } t = append(t, console.Colorize("metrics-top-title", "Avg Size"), console.Colorize("metrics-top-title", "Rate /min"), console.Colorize("metrics-top-title", "Errors"), ) table.Append(t) for i, v := range entries { if v.Count <= 0 { continue } errs := "0" if v.Errors > 0 { errs = console.Colorize("metrics-error", strconv.Itoa(v.Errors)) } avg := v.Duration / time.Duration(v.Count) avgTTFB := v.TTFB / time.Duration(v.Count) avgColor := "metrics-dur" if avg > 10*time.Second { avgColor = "metrics-dur-high" } else if avg > 2*time.Second { avgColor = "metrics-dur-med" } minColor := "metrics-dur" if v.MinDur > 10*time.Second { minColor = "metrics-dur-high" } else if v.MinDur > 2*time.Second { minColor = "metrics-dur-med" } maxColor := "metrics-dur" if v.MaxDur > 10*time.Second { maxColor = "metrics-dur-high" } else if v.MaxDur > time.Second { maxColor = "metrics-dur-med" } sz := "-" rate := "-" if v.Size > 0 && v.Count > 0 { sz = ibytesShort(uint64(v.Size) / uint64(v.Count)) rate = ibytesShort(uint64(float64(v.Size) / dur.Minutes())) } if v.CallStatsCount > 0 { var s, r []string if v.CallStats.Rx > 0 { s = append(s, fmt.Sprintf("↑%s", ibytesShort(uint64(v.CallStats.Rx/v.CallStatsCount)))) r = append(r, fmt.Sprintf("↑%s", ibytesShort(uint64(float64(v.CallStats.Rx)/dur.Minutes())))) } if v.CallStats.Tx > 0 { s = append(s, fmt.Sprintf("↓%s", ibytesShort(uint64(v.CallStats.Tx/v.CallStatsCount)))) r = append(r, fmt.Sprintf("↓%s", ibytesShort(uint64(float64(v.CallStats.Tx)/dur.Minutes())))) } if len(s)+len(r) > 0 { sz = strings.Join(s, " ") } if len(r) > 0 { rate = strings.Join(r, " ") } } if sz != "-" { sz = console.Colorize("metrics-size", sz) rate = console.Colorize("metrics-size", rate) } preCall = "" if endTrunc && i == len(entries)-1 { preCall = console.Colorize("metrics-error", "↓ ") } t := []string{ preCall + console.Colorize("metrics-title", metricsTitle(v.Name)), console.Colorize("metrics-number", fmt.Sprintf("%d ", v.Count)) + console.Colorize("metrics-number-secondary", fmt.Sprintf("(%0.1f%%)", float64(v.Count)/float64(totalCnt)*100)), console.Colorize("metrics-number", fmt.Sprintf("%0.1f", float64(v.Count)/dur.Minutes())), console.Colorize(avgColor, fmt.Sprintf("%v", roundDur(avg))), console.Colorize(minColor, roundDur(v.MinDur)), console.Colorize(maxColor, roundDur(v.MaxDur)), } if hasTTFB { if v.TTFB > 0 { t = append(t, console.Colorize(avgColor, fmt.Sprintf("%v", roundDur(avgTTFB))), console.Colorize(maxColor, roundDur(v.MaxTTFB))) } else { t = append(t, "-", "-") } } t = append(t, sz, rate, errs) table.Append(t) } table.Render() if globalTermWidth <= 10 { return s.String() } w := globalTermWidth if nw, _, e := term.GetSize(int(os.Stdout.Fd())); e == nil { w = nw } split := strings.Split(s.String(), "\n") for i, line := range split { split[i] = truncate.StringWithTail(line, uint(w), "»") } return strings.Join(split, "\n") } // ibytesShort returns a short un-padded version of the value from humanize.IBytes. func ibytesShort(v uint64) string { return strings.ReplaceAll(strings.TrimSuffix(humanize.IBytes(v), "iB"), " ", "") } // roundDur will round the duration to a nice, printable number, with "reasonable" precision. func roundDur(d time.Duration) time.Duration { if d > time.Minute { return d.Round(time.Second) } if d > time.Second { return d.Round(time.Millisecond) } if d > time.Millisecond { return d.Round(time.Millisecond / 10) } return d.Round(time.Microsecond) } func initTraceStatsUI(allFlag bool, maxEntries int, traces <-chan madmin.ServiceTraceInfo) *traceStatsUI { meter := spinner.New() meter.Spinner = spinner.Meter meter.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) // Use half the default fps to reduce flickering meter.Spinner.FPS = time.Second / 3 console.SetColor("metrics-duration", color.New(color.FgWhite)) console.SetColor("metrics-size", color.New(color.FgGreen)) console.SetColor("metrics-dur", color.New(color.FgGreen)) console.SetColor("metrics-dur-med", color.New(color.FgYellow)) console.SetColor("metrics-dur-high", color.New(color.FgRed)) console.SetColor("metrics-error", color.New(color.FgYellow)) console.SetColor("metrics-title", color.New(color.FgCyan)) console.SetColor("metrics-top-title", color.New(color.FgHiCyan)) console.SetColor("metrics-number", color.New(color.FgWhite)) console.SetColor("metrics-number-secondary", color.New(color.FgBlue)) console.SetColor("metrics-zero", color.New(color.FgWhite)) stats := &statTrace{Calls: make(map[string]statItem, 20)} go func() { for t := range traces { stats.add(t) } }() return &traceStatsUI{ started: time.Now(), meter: meter, maxEntries: maxEntries, current: stats, allFlag: allFlag, } } minio-client-0.0~20250403/cmd/tree-main.go000066400000000000000000000202051477450377600177540ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "fmt" "path/filepath" "strings" "time" "github.com/fatih/color" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) const ( treeEntry = "├─ " treeLastEntry = "└─ " treeNext = "│" treeLevel = " " ) // Structured message depending on the type of console. type treeMessage struct { Entry string IsDir bool BranchString string } // Colorized message for console printing. func (t treeMessage) String() string { entryType := "File" if t.IsDir { entryType = "Dir" } return fmt.Sprintf("%s%s", t.BranchString, console.Colorize(entryType, t.Entry)) } // JSON'ified message for scripting. // Does No-op. JSON requests are redirected to `ls -r --json` func (t treeMessage) JSON() string { fatalIf(probe.NewError(errors.New("JSON() should never be called here")), "Unable to list in tree format. Please report this issue at https://github.com/minio/mc/issues") return "" } var treeFlags = []cli.Flag{ cli.BoolFlag{ Name: "files, f", Usage: "includes files in tree", }, cli.IntFlag{ Name: "depth, d", Usage: "sets the depth threshold", Value: -1, }, cli.StringFlag{ Name: "rewind", Usage: "display tree no later than specified date", }, } // trees files and folders. var treeCmd = cli.Command{ Name: "tree", Usage: "list buckets and objects in a tree format", Action: mainTree, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(treeFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET [TARGET ...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. List all buckets and directories on MinIO object storage server in tree format. {{.Prompt}} {{.HelpName}} myminio 2. List all directories in "mybucket" on MinIO object storage server in tree format. {{.Prompt}} {{.HelpName}} myminio/mybucket/ 3. List all directories in "mybucket" on MinIO object storage server hosted on Microsoft Windows in tree format. {{.Prompt}} {{.HelpName}} myminio\mybucket\ 4. List all directories and objects in "mybucket" on MinIO object storage server in tree format. {{.Prompt}} {{.HelpName}} --files myminio/mybucket/ 5. List all directories upto depth level '2' in tree format. {{.Prompt}} {{.HelpName}} --depth 2 myminio/mybucket/ `, } // parseTreeSyntax - validate all the passed arguments func parseTreeSyntax(ctx context.Context, cliCtx *cli.Context) (args []string, depth int, files bool, timeRef time.Time) { args = cliCtx.Args() depth = cliCtx.Int("depth") files = cliCtx.Bool("files") rewind := cliCtx.String("rewind") timeRef = parseRewindFlag(rewind) if depth < -1 || cliCtx.Int("depth") == 0 { fatalIf(errInvalidArgument().Trace(args...), "please set a proper depth, for example: '--depth 1' to limit the tree output, default (-1) output displays everything") } if len(args) == 0 { return } for _, url := range args { _, _, err := url2Stat(ctx, url2StatOptions{urlStr: url, versionID: "", fileAttr: false, encKeyDB: nil, timeRef: timeRef, isZip: false, ignoreBucketExistsCheck: false}) fatalIf(err.Trace(url), "Unable to tree `"+url+"`.") } return } // doTree - list all entities inside a folder in a tree format. func doTree(ctx context.Context, url string, timeRef time.Time, level int, branchString string, depth int, includeFiles bool) error { targetAlias, targetURL, _ := mustExpandAlias(url) if !strings.HasSuffix(targetURL, "/") { targetURL += "/" } clnt, err := newClientFromAlias(targetAlias, targetURL) fatalIf(err.Trace(targetURL), "Unable to initialize target `"+targetURL+"`.") prefixPath := clnt.GetURL().Path separator := string(clnt.GetURL().Separator) if !strings.HasSuffix(prefixPath, separator) { prefixPath = filepath.Dir(prefixPath) + "/" } bucketNameShowed := false var prev *ClientContent show := func(end bool) error { currbranchString := branchString if level == 1 && !bucketNameShowed { bucketNameShowed = true printMsg(treeMessage{ Entry: url, IsDir: true, BranchString: branchString, }) } isLevelClosed := strings.HasSuffix(currbranchString, treeLastEntry) if isLevelClosed { currbranchString = strings.TrimSuffix(currbranchString, treeLastEntry) } else { currbranchString = strings.TrimSuffix(currbranchString, treeEntry) } if level != 1 { if isLevelClosed { currbranchString += " " + treeLevel } else { currbranchString += treeNext + treeLevel } } if end { currbranchString += treeLastEntry } else { currbranchString += treeEntry } // Convert any os specific delimiters to "/". contentURL := filepath.ToSlash(prev.URL.Path) prefixPath = filepath.ToSlash(prefixPath) // Trim prefix of current working dir prefixPath = strings.TrimPrefix(prefixPath, "."+separator) if prev.Type.IsDir() { nextURL := "" if targetAlias != "" { nextURL = targetAlias + "/" + contentURL } else { nextURL = contentURL } if nextURL == url { return nil } printMsg(treeMessage{ Entry: strings.TrimSuffix(strings.TrimPrefix(contentURL, prefixPath), "/"), IsDir: true, BranchString: currbranchString, }) } else { printMsg(treeMessage{ Entry: strings.TrimPrefix(contentURL, prefixPath), IsDir: false, BranchString: currbranchString, }) } if prev.Type.IsDir() { url := "" if targetAlias != "" { url = targetAlias + "/" + contentURL } else { url = contentURL } if depth == -1 || level <= depth { if err := doTree(ctx, url, timeRef, level+1, currbranchString, depth, includeFiles); err != nil { return err } } } return nil } for content := range clnt.List(ctx, ListOptions{Recursive: false, TimeRef: timeRef, ShowDir: DirFirst}) { if content.Err != nil { errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to tree.") continue } if !includeFiles && !content.Type.IsDir() { continue } if prev != nil { if err := show(false); err != nil { return err } } prev = content } if prev != nil { if err := show(true); err != nil { return err } } return nil } // mainTree - is a handler for mc tree command func mainTree(cliCtx *cli.Context) error { ctx, cancelList := context.WithCancel(globalContext) defer cancelList() console.SetColor("File", color.New(color.Bold)) console.SetColor("Dir", color.New(color.FgCyan, color.Bold)) // parse 'tree' cliCtx arguments. args, depth, includeFiles, timeRef := parseTreeSyntax(ctx, cliCtx) // mimic operating system tool behavior. if len(args) == 0 { args = []string{"."} } var cErr error for _, targetURL := range args { if !globalJSON { if e := doTree(ctx, targetURL, timeRef, 1, "", depth, includeFiles); e != nil { cErr = e } } else { targetAlias, targetURL, _ := mustExpandAlias(targetURL) if !strings.HasSuffix(targetURL, "/") { targetURL += "/" } clnt, err := newClientFromAlias(targetAlias, targetURL) fatalIf(err.Trace(targetURL), "Unable to initialize target `"+targetURL+"`.") opts := doListOptions{ timeRef: timeRef, isRecursive: true, isIncomplete: false, isSummary: false, withVersions: false, listZip: false, filter: "*", } if e := doList(ctx, clnt, opts); e != nil { cErr = e } } } return cErr } minio-client-0.0~20250403/cmd/typed-errors.go000066400000000000000000000136251477450377600205420ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "errors" "fmt" "strings" "github.com/minio/mc/pkg/probe" ) type dummyErr error var errDummy = func() *probe.Error { msg := "" return probe.NewError(dummyErr(errors.New(msg))).Untrace() } type invalidArgumentErr error var errInvalidArgument = func() *probe.Error { msg := "Invalid arguments provided, please refer " + "`mc -h` for relevant documentation." return probe.NewError(invalidArgumentErr(errors.New(msg))).Untrace() } type unableToGuessErr error var errUnableToGuess = func() *probe.Error { msg := "Unable to guess the type of copy operation." return probe.NewError(unableToGuessErr(errors.New(msg))) } type unrecognizedDiffTypeErr error var errUnrecognizedDiffType = func(diff differType) *probe.Error { msg := "Unrecognized diffType: " + diff.String() + " provided." return probe.NewError(unrecognizedDiffTypeErr(errors.New(msg))).Untrace() } type invalidAliasedURLErr error var errInvalidAliasedURL = func(URL string) *probe.Error { msg := "Use `mc alias set mycloud " + URL + " ...` to add an alias. Use the alias for S3 operations." return probe.NewError(invalidAliasedURLErr(errors.New(msg))).Untrace() } type invalidAliasErr error var errInvalidAlias = func(alias string) *probe.Error { msg := "Alias `" + alias + "` should have alphanumeric characters such as [helloWorld0, hello_World0, ...] and begin with a letter" return probe.NewError(invalidAliasErr(errors.New(msg))) } type invalidURLErr error var errInvalidURL = func(URL string) *probe.Error { msg := "URL `" + URL + "` for MinIO Client should be of the form scheme://host[:port]/ without resource component." return probe.NewError(invalidURLErr(errors.New(msg))) } type invalidAPISignatureErr error var errInvalidAPISignature = func(api, url string) *probe.Error { msg := fmt.Sprintf( "Unrecognized API signature %s for host %s. Valid options are `[%s]`", api, url, strings.Join(validAPIs, ", ")) return probe.NewError(invalidAPISignatureErr(errors.New(msg))) } type noMatchingHostErr error var errNoMatchingHost = func(URL string) *probe.Error { msg := "No matching host found for the given URL `" + URL + "`." return probe.NewError(noMatchingHostErr(errors.New(msg))).Untrace() } type invalidSourceErr error var errInvalidSource = func(URL string) *probe.Error { msg := "Invalid source `" + URL + "`." return probe.NewError(invalidSourceErr(errors.New(msg))).Untrace() } type invalidTargetErr error var errInvalidTarget = func(URL string) *probe.Error { msg := "Invalid target `" + URL + "`." return probe.NewError(invalidTargetErr(errors.New(msg))).Untrace() } type requiresRecuriveErr error var errRequiresRecursive = func(URL string) *probe.Error { msg := "To copy or move '" + URL + "' the --recursive flag is required." return probe.NewError(requiresRecuriveErr(errors.New(msg))).Untrace() } type copyIntoSelfErr error var errCopyIntoSelf = func(URL string) *probe.Error { msg := "Copying or moving '" + URL + "' into itself is not allowed." return probe.NewError(copyIntoSelfErr(errors.New(msg))).Untrace() } type targetNotFoundErr error var errTargetNotFound = func(URL string) *probe.Error { msg := "Target `" + URL + "` not found." return probe.NewError(targetNotFoundErr(errors.New(msg))).Untrace() } type overwriteNotAllowedErr struct { error } var errOverWriteNotAllowed = func(URL string) *probe.Error { msg := "Overwrite not allowed for `" + URL + "`. Use `--overwrite` to override this behavior." return probe.NewError(overwriteNotAllowedErr{errors.New(msg)}) } type targetIsNotDirErr error var errTargetIsNotDir = func(URL string) *probe.Error { msg := "Target `" + URL + "` is not a folder." return probe.NewError(targetIsNotDirErr(errors.New(msg))).Untrace() } type sourceIsDirErr error var errSourceIsDir = func(URL string) *probe.Error { msg := "Source `" + URL + "` is a folder." return probe.NewError(sourceIsDirErr(errors.New(msg))).Untrace() } type sseInvalidAliasErr error var errSSEInvalidAlias = func(prefix string) *probe.Error { msg := "SSE prefix " + prefix + " has an invalid alias." return probe.NewError(sseInvalidAliasErr(errors.New(msg))).Untrace() } type sseOverlappingAliasErr error var errSSEOverlappingAlias = func(prefix, overlappingPrefix string) *probe.Error { msg := "SSE prefix " + prefix + " overlaps with " + overlappingPrefix return probe.NewError(sseOverlappingAliasErr(errors.New(msg))).Untrace() } type ssePrefixMatchErr error var errSSEPrefixMatch = func() *probe.Error { msg := "SSE prefixes do not match any object paths." return probe.NewError(ssePrefixMatchErr(errors.New(msg))).Untrace() } type sseKeyMissingError error var errSSEKeyMissing = func() *probe.Error { m := "SSE key is missing" return probe.NewError(sseKeyMissingError(errors.New(m))).Untrace() } type sseKMSKeyFormatErr error var errSSEKMSKeyFormat = func(msg string) *probe.Error { m := "SSE key format error. " m += msg return probe.NewError(sseKMSKeyFormatErr(errors.New(m))).Untrace() } type sseClientKeyFormatErr error var errSSEClientKeyFormat = func(msg string) *probe.Error { m := "Encryption key should be either raw base64 encoded or hex encoded. " m += msg return probe.NewError(sseClientKeyFormatErr(errors.New(m))).Untrace() } minio-client-0.0~20250403/cmd/undo-main.go000066400000000000000000000212601477450377600177640ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "fmt" "path/filepath" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) const ( actionPut = "PUT" actionDelete = "DELETE" ) var undoFlags = []cli.Flag{ cli.IntFlag{ Name: "last", Usage: "undo N last changes", Value: 1, }, cli.BoolFlag{ Name: "recursive, r", Usage: "undo last S3 PUT/DELETE operations recursively", }, cli.BoolFlag{ Name: "force", Usage: "force recursive operation", }, cli.BoolFlag{ Name: "dry-run", Usage: "fake an undo operation", }, cli.StringFlag{ Name: "action", Usage: "undo only if the latest version is of the following type [PUT/DELETE]", }, } var undoCmd = cli.Command{ Name: "undo", Usage: "undo PUT/DELETE operations", Action: mainUndo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(undoFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Undo the last 3 uploads and/or removals of a particular object {{.Prompt}} {{.HelpName}} s3/backups/file.zip --last 3 2. Undo the last upload/removal change of all objects under a prefix {{.Prompt}} {{.HelpName}} s3/backups/prefix/ --recursive --force `, } // undoMessage container for undo message structure. type undoMessage struct { Status string `json:"status"` URL string `json:"url,omitempty"` Key string `json:"key,omitempty"` VersionID string `json:"versionId,omitempty"` IsDeleteMarker bool `json:"isDeleteMarker,omitempty"` } // String colorized string message. func (c undoMessage) String() string { var msg string fmt.Print(color.GreenString("\u2713 ")) yellow := color.New(color.FgYellow).SprintFunc() if c.IsDeleteMarker { msg += "Last " + color.RedString("delete") + " of `" + yellow(c.Key) + "` is reverted" } else { msg += "Last " + color.BlueString("upload") + " of `" + yellow(c.Key) + "` (vid=" + c.VersionID + ") is reverted" } msg += "." return msg } // JSON jsonified content message. func (c undoMessage) JSON() string { c.Status = "success" jsonMessageBytes, e := json.MarshalIndent(c, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } // parseUndoSyntax performs command-line input validation for cat command. func parseUndoSyntax(ctx *cli.Context) (targetAliasedURL string, last int, recursive, dryRun bool, action string) { targetAliasedURL = ctx.Args().Get(0) if targetAliasedURL == "" { fatalIf(errInvalidArgument().Trace(), "The argument should not be empty") } last = ctx.Int("last") if last < 1 { fatalIf(errInvalidArgument().Trace(), "--last value should be a positive integer") } recursive = ctx.Bool("recursive") force := ctx.Bool("force") if recursive && !force { fatalIf(errInvalidArgument().Trace(), "This is a dangerous operation, you need to provide --force flag as well") } dryRun = ctx.Bool("dry-run") action = strings.ToUpper(ctx.String("action")) if action != actionPut && action != actionDelete && action != "" { fatalIf(errInvalidArgument().Trace(), "unsupported action specified, supported actions are PUT, DELETE or empty (default)") } if (action == actionPut || action == actionDelete) && last != 1 { fatalIf(errInvalidArgument().Trace(), "--action if specified requires that you must specify --last=1") } return } func undoLastNOperations(ctx context.Context, clnt Client, objectVersions []*ClientContent, last int, dryRun bool) (exitErr error) { if last == 0 { return } sortObjectVersions(objectVersions) if len(objectVersions) > last { objectVersions = objectVersions[:last] } contentCh := make(chan *ClientContent) resultCh := clnt.Remove(ctx, false, false, false, false, contentCh) prefixPath := clnt.GetURL().Path prefixPath = filepath.ToSlash(prefixPath) if !strings.HasSuffix(prefixPath, "/") { prefixPath = prefixPath[:strings.LastIndex(prefixPath, "/")+1] } prefixPath = strings.TrimPrefix(prefixPath, "./") go func() { for _, objectVersion := range objectVersions { if !dryRun { contentCh <- objectVersion } // Convert any os specific delimiters to "/". contentURL := filepath.ToSlash(objectVersion.URL.Path) // Trim prefix path from the content path. keyName := strings.TrimPrefix(contentURL, prefixPath) printMsg(undoMessage{ Status: "success", Key: getOSDependantKey(keyName, objectVersion.Type.IsDir()), URL: objectVersion.URL.String(), VersionID: objectVersion.VersionID, IsDeleteMarker: objectVersion.IsDeleteMarker, }) } close(contentCh) }() for result := range resultCh { if result.Err != nil { errorIf(result.Err.Trace(), "Unable to undo") exitErr = exitStatus(globalErrorExitStatus) // Set the exit status. } } return } func undoURL(ctx context.Context, aliasedURL string, last int, recursive, dryRun bool, action string) (exitErr error) { clnt, err := newClient(aliasedURL) fatalIf(err.Trace(aliasedURL), "Unable to initialize target `"+aliasedURL+"`.") alias, _, _ := mustExpandAlias(aliasedURL) var ( lastObjectPath string perObjectVersions []*ClientContent atLeastOneUndoApplied bool ) remove := true for content := range clnt.List(ctx, ListOptions{ Recursive: recursive, WithOlderVersions: true, WithDeleteMarkers: true, ShowDir: DirNone, }) { if content.Err != nil { fatalIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") } if content.StorageClass == s3StorageClassGlacier { continue } if !recursive { if getStandardizedURL(alias+getKey(content)) != getStandardizedURL(aliasedURL) { break } } if lastObjectPath != content.URL.Path { // Print any object in the current list before reinitializing it if remove { exitErr = undoLastNOperations(ctx, clnt, perObjectVersions, last, dryRun) } remove = true lastObjectPath = content.URL.Path perObjectVersions = []*ClientContent{} } if !remove { continue } if (content.IsLatest && action == actionDelete && !content.IsDeleteMarker) || (content.IsLatest && action == actionPut && content.IsDeleteMarker) { remove = false continue } perObjectVersions = append(perObjectVersions, content) atLeastOneUndoApplied = true } // Undo the remaining versions found if any if len(perObjectVersions) > 0 && remove { exitErr = undoLastNOperations(ctx, clnt, perObjectVersions, last, dryRun) } if !atLeastOneUndoApplied { errorIf(errDummy().Trace(clnt.GetURL().String()), "Unable to find any object version to undo.") exitErr = exitStatus(globalErrorExitStatus) // Set the exit status. } return } func checkIfBucketIsVersioned(ctx context.Context, aliasedURL string) (versioned bool) { client, err := newClient(aliasedURL) fatalIf(err, "Unable to parse `%s`", aliasedURL) versioningConfig, err := client.GetVersion(ctx) if err != nil { if errors.As(err.ToGoError(), &APINotImplemented{}) { return false } fatalIf(err.Trace(), "Unable to get bucket versioning info") } if versioningConfig.Status == "Enabled" { return true } return false } func checkUndoSyntax(cliCtx *cli.Context) { if !cliCtx.Args().Present() { showCommandHelpAndExit(cliCtx, 1) } } // mainUndo is the main entry point for undo command. func mainUndo(cliCtx *cli.Context) error { checkUndoSyntax(cliCtx) ctx, cancelCat := context.WithCancel(globalContext) defer cancelCat() console.SetColor("Success", color.New(color.FgGreen, color.Bold)) // check 'undo' cli arguments. targetAliasedURL, last, recursive, dryRun, action := parseUndoSyntax(cliCtx) if !checkIfBucketIsVersioned(ctx, targetAliasedURL) { fatalIf(errDummy().Trace(), "Undo command works only with S3 versioned-enabled buckets.") } return undoURL(ctx, targetAliasedURL, last, recursive, dryRun, action) } minio-client-0.0~20250403/cmd/update-main.go000066400000000000000000000377031477450377600203120ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "crypto" "crypto/tls" "encoding/hex" "errors" "fmt" "io" "net" "net/http" "net/url" "os" "path" "path/filepath" "runtime" "strings" "time" _ "crypto/sha256" // needed for selfupdate hashers "github.com/fatih/color" "github.com/mattn/go-isatty" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/env" "github.com/minio/selfupdate" ) // Check for new software updates. var updateCmd = cli.Command{ Name: "update", Usage: "update mc to latest release", Action: mainUpdate, OnUsageError: onUsageError, Flags: []cli.Flag{ cli.BoolFlag{ Name: "json", Usage: "enable JSON lines formatted output", }, }, CustomHelpTemplate: `Name: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}}{{if .VisibleFlags}} [FLAGS]{{end}} {{if .VisibleFlags}} FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} EXIT STATUS: 0 - you are already running the most recent version 1 - new update was applied successfully -1 - error in getting update information EXAMPLES: 1. Check and update mc: {{.Prompt}} {{.HelpName}} `, } const ( mcReleaseTagTimeLayout = "2006-01-02T15-04-05Z" mcOSARCH = runtime.GOOS + "-" + runtime.GOARCH mcReleaseURL = "https://dl.min.io/client/mc/release/" + mcOSARCH + "/" envMinisignPubKey = "MC_UPDATE_MINISIGN_PUBKEY" ) // For windows our files have .exe additionally. var mcReleaseWindowsInfoURL = mcReleaseURL + "mc.exe.sha256sum" // mcVersionToReleaseTime - parses a standard official release // mc --version string. // // An official binary's version string is the release time formatted // with RFC3339 (in UTC) - e.g. `2017-09-29T19:16:56Z` func mcVersionToReleaseTime(version string) (releaseTime time.Time, err *probe.Error) { var e error releaseTime, e = time.Parse(time.RFC3339, version) return releaseTime, probe.NewError(e) } // releaseTagToReleaseTime - releaseTag to releaseTime func releaseTagToReleaseTime(releaseTag string) (releaseTime time.Time, err *probe.Error) { fields := strings.Split(releaseTag, ".") if len(fields) < 2 || len(fields) > 4 { return releaseTime, probe.NewError(fmt.Errorf("%s is not a valid release tag", releaseTag)) } if fields[0] != "RELEASE" { return releaseTime, probe.NewError(fmt.Errorf("%s is not a valid release tag", releaseTag)) } var e error releaseTime, e = time.Parse(mcReleaseTagTimeLayout, fields[1]) return releaseTime, probe.NewError(e) } // getModTime - get the file modification time of `path` func getModTime(path string) (t time.Time, err *probe.Error) { var e error path, e = filepath.EvalSymlinks(path) if e != nil { return t, probe.NewError(fmt.Errorf("Unable to get absolute path of %s. %w", path, e)) } // Version is mc non-standard, we will use mc binary's // ModTime as release time. var fi os.FileInfo fi, e = os.Stat(path) if e != nil { return t, probe.NewError(fmt.Errorf("Unable to get ModTime of %s. %w", path, e)) } // Return the ModTime return fi.ModTime().UTC(), nil } // GetCurrentReleaseTime - returns this process's release time. If it // is official mc --version, parsed version is returned else mc // binary's mod time is returned. func GetCurrentReleaseTime() (releaseTime time.Time, err *probe.Error) { if releaseTime, err = mcVersionToReleaseTime(Version); err == nil { return releaseTime, nil } // Looks like version is mc non-standard, we use mc // binary's ModTime as release time: path, e := os.Executable() if e != nil { return releaseTime, probe.NewError(e) } return getModTime(path) } // IsDocker - returns if the environment mc is running in docker or // not. The check is a simple file existence check. // // https://github.com/moby/moby/blob/master/daemon/initlayer/setup_unix.go#L25 // // "/.dockerenv": "file", func IsDocker() bool { _, e := os.Stat("/.dockerenv") if os.IsNotExist(e) { return false } return e == nil } // IsDCOS returns true if mc is running in DCOS. func IsDCOS() bool { // http://mesos.apache.org/documentation/latest/docker-containerizer/ // Mesos docker containerizer sets this value return os.Getenv("MESOS_CONTAINER_NAME") != "" } // IsKubernetes returns true if MinIO is running in kubernetes. func IsKubernetes() bool { // Kubernetes env used to validate if we are // indeed running inside a kubernetes pod // is KUBERNETES_SERVICE_HOST but in future // we might need to enhance this. return os.Getenv("KUBERNETES_SERVICE_HOST") != "" } // IsSourceBuild - returns if this binary is a non-official build from // source code. func IsSourceBuild() bool { _, err := mcVersionToReleaseTime(Version) return err != nil } // DO NOT CHANGE USER AGENT STYLE. // The style should be // // mc (; [; dcos][; kubernetes][; docker][; source]) mc/ mc/ mc/ // // Any change here should be discussed by opening an issue at // https://github.com/minio/mc/issues. func getUserAgent() string { userAgentParts := []string{} // Helper function to concisely append a pair of strings to a // the user-agent slice. uaAppend := func(p, q string) { userAgentParts = append(userAgentParts, p, q) } uaAppend("mc (", runtime.GOOS) uaAppend("; ", runtime.GOARCH) if IsDCOS() { uaAppend("; ", "dcos") } if IsKubernetes() { uaAppend("; ", "kubernetes") } if IsDocker() { uaAppend("; ", "docker") } if IsSourceBuild() { uaAppend("; ", "source") } uaAppend(") mc/", Version) uaAppend(" mc/", ReleaseTag) uaAppend(" mc/", CommitID) return strings.Join(userAgentParts, "") } func downloadReleaseURL(releaseChecksumURL string, timeout time.Duration) (content string, err *probe.Error) { req, e := http.NewRequest("GET", releaseChecksumURL, nil) if e != nil { return content, probe.NewError(e) } req.Header.Set("User-Agent", getUserAgent()) resp, e := httpClient(timeout).Do(req) if e != nil { return content, probe.NewError(e) } if resp == nil { return content, probe.NewError(fmt.Errorf("No response from server to download URL %s", releaseChecksumURL)) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return content, probe.NewError(fmt.Errorf("Error downloading URL %s. Response: %v", releaseChecksumURL, resp.Status)) } contentBytes, e := io.ReadAll(resp.Body) if e != nil { return content, probe.NewError(fmt.Errorf("Error reading response. %s", err)) } return string(contentBytes), nil } // DownloadReleaseData - downloads release data from mc official server. func DownloadReleaseData(customReleaseURL string, timeout time.Duration) (data string, err *probe.Error) { releaseURL := mcReleaseInfoURL if runtime.GOOS == "windows" { releaseURL = mcReleaseWindowsInfoURL } if customReleaseURL != "" { releaseURL = customReleaseURL } return func() (data string, err *probe.Error) { data, err = downloadReleaseURL(releaseURL, timeout) if err == nil { return data, nil } return data, err.Trace(releaseURL) }() } // parseReleaseData - parses release info file content fetched from // official mc download server. // // The expected format is a single line with two words like: // // fbe246edbd382902db9a4035df7dce8cb441357d mc.RELEASE.2016-10-07T01-16-39Z // // The second word must be `mc.` appended to a standard release tag. func parseReleaseData(data string) (sha256Hex string, releaseTime time.Time, releaseTag string, err *probe.Error) { fields := strings.Fields(data) if len(fields) != 2 { return sha256Hex, releaseTime, "", probe.NewError(fmt.Errorf("Unknown release data `%s`", data)) } sha256Hex = fields[0] releaseInfo := fields[1] fields = strings.SplitN(releaseInfo, ".", 2) if len(fields) != 2 { return sha256Hex, releaseTime, "", probe.NewError(fmt.Errorf("Unknown release information `%s`", releaseInfo)) } if fields[0] != "mc" { return sha256Hex, releaseTime, "", probe.NewError(fmt.Errorf("Unknown release `%s`", releaseInfo)) } releaseTime, err = releaseTagToReleaseTime(fields[1]) if err != nil { return sha256Hex, releaseTime, fields[1], err.Trace(fields...) } return sha256Hex, releaseTime, fields[1], nil } func getLatestReleaseTime(customReleaseURL string, timeout time.Duration) (sha256Hex string, releaseTime time.Time, releaseTag string, err *probe.Error) { data, err := DownloadReleaseData(customReleaseURL, timeout) if err != nil { return sha256Hex, releaseTime, releaseTag, err.Trace() } return parseReleaseData(data) } func getDownloadURL(customReleaseURL, releaseTag string) (downloadURL string) { // Check if we are docker environment, return docker update command if IsDocker() { // Construct release tag name. return fmt.Sprintf("docker pull minio/mc:%s", releaseTag) } if customReleaseURL == "" { return mcReleaseURL + "archive/mc." + releaseTag } u, e := url.Parse(customReleaseURL) if e != nil { return mcReleaseURL + "archive/mc." + releaseTag } u.Path = path.Dir(u.Path) + "/mc." + releaseTag return u.String() } func getUpdateInfo(customReleaseURL string, timeout time.Duration) (updateMsg, sha256Hex string, currentReleaseTime, latestReleaseTime time.Time, releaseTag string, err *probe.Error) { currentReleaseTime, err = GetCurrentReleaseTime() if err != nil { return updateMsg, sha256Hex, currentReleaseTime, latestReleaseTime, releaseTag, err.Trace() } sha256Hex, latestReleaseTime, releaseTag, err = getLatestReleaseTime(customReleaseURL, timeout) if err != nil { return updateMsg, sha256Hex, currentReleaseTime, latestReleaseTime, releaseTag, err.Trace() } var older time.Duration var downloadURL string if latestReleaseTime.After(currentReleaseTime) { older = latestReleaseTime.Sub(currentReleaseTime) downloadURL = getDownloadURL(customReleaseURL, releaseTag) } return prepareUpdateMessage(downloadURL, older), sha256Hex, currentReleaseTime, latestReleaseTime, releaseTag, nil } var ( // Check if we stderr, stdout are dumb terminals, we do not apply // ansi coloring on dumb terminals. isTerminal = func() bool { return isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stderr.Fd()) } colorCyanBold = func() func(a ...interface{}) string { if isTerminal() { color.New(color.FgCyan, color.Bold).SprintFunc() } return fmt.Sprint }() colorYellowBold = func() func(format string, a ...interface{}) string { if isTerminal() { return color.New(color.FgYellow, color.Bold).SprintfFunc() } return fmt.Sprintf }() colorGreenBold = func() func(format string, a ...interface{}) string { if isTerminal() { return color.New(color.FgGreen, color.Bold).SprintfFunc() } return fmt.Sprintf }() ) func getUpdateTransport(timeout time.Duration) http.RoundTripper { var updateTransport http.RoundTripper = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: timeout, KeepAlive: timeout, DualStack: true, }).DialContext, IdleConnTimeout: timeout, TLSHandshakeTimeout: timeout, ExpectContinueTimeout: timeout, TLSClientConfig: &tls.Config{ RootCAs: globalRootCAs, }, DisableCompression: true, } return updateTransport } func getUpdateReaderFromURL(u *url.URL, transport http.RoundTripper) (io.ReadCloser, error) { clnt := &http.Client{ Transport: transport, } req, e := http.NewRequest(http.MethodGet, u.String(), nil) if e != nil { return nil, e } req.Header.Set("User-Agent", getUserAgent()) resp, e := clnt.Do(req) if e != nil { return nil, e } if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } return newProgressReader(resp.Body, "mc", resp.ContentLength), nil } func doUpdate(customReleaseURL, sha256Hex string, latestReleaseTime time.Time, releaseTag string, ok bool) (updateStatusMsg string, err *probe.Error) { fmtReleaseTime := latestReleaseTime.Format(mcReleaseTagTimeLayout) if !ok { updateStatusMsg = colorGreenBold("mc update to version %s canceled.", releaseTag) return updateStatusMsg, nil } sha256Sum, e := hex.DecodeString(sha256Hex) if e != nil { return updateStatusMsg, probe.NewError(e) } u, e := url.Parse(getDownloadURL(customReleaseURL, releaseTag)) if err != nil { return updateStatusMsg, probe.NewError(e) } transport := getUpdateTransport(30 * time.Second) rc, e := getUpdateReaderFromURL(u, transport) if e != nil { return updateStatusMsg, probe.NewError(e) } defer rc.Close() opts := selfupdate.Options{ Hash: crypto.SHA256, Checksum: sha256Sum, } minisignPubkey := env.Get(envMinisignPubKey, "") if minisignPubkey != "" { v := selfupdate.NewVerifier() u.Path = path.Dir(u.Path) + "/mc." + releaseTag + ".minisig" if e = v.LoadFromURL(u.String(), minisignPubkey, transport); e != nil { return updateStatusMsg, probe.NewError(e) } opts.Verifier = v } if e := opts.CheckPermissions(); e != nil { permErrMsg := fmt.Sprintf(" failed with: %s", e) updateStatusMsg = colorYellowBold("mc update to version RELEASE.%s %s.", fmtReleaseTime, permErrMsg) return updateStatusMsg, nil } if e = selfupdate.Apply(rc, opts); e != nil { if re := selfupdate.RollbackError(e); re != nil { rollBackErr := fmt.Sprintf("Failed to rollback from bad update: %v", re) updateStatusMsg = colorYellowBold("mc update to version RELEASE.%s %s.", fmtReleaseTime, rollBackErr) return updateStatusMsg, probe.NewError(e) } var pathErr *os.PathError if errors.As(e, &pathErr) { pathErrMsg := fmt.Sprintf("Unable to update the binary at %s: %v", filepath.Dir(pathErr.Path), pathErr.Err) updateStatusMsg = colorYellowBold("mc update to version RELEASE.%s %s.", fmtReleaseTime, pathErrMsg) return updateStatusMsg, nil } return colorYellowBold(fmt.Sprintf("Error in mc update to version RELEASE.%s %v.", fmtReleaseTime, e)), nil } return colorGreenBold("mc updated to version RELEASE.%s successfully.", fmtReleaseTime), nil } type updateMessage struct { Status string `json:"status"` Message string `json:"message"` } // String colorized make bucket message. func (s updateMessage) String() string { return s.Message } // JSON jsonified make bucket message. func (s updateMessage) JSON() string { s.Status = "success" updateJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(updateJSONBytes) } func mainUpdate(ctx *cli.Context) { if len(ctx.Args()) > 1 { showCommandHelpAndExit(ctx, -1) } globalQuiet = ctx.Bool("quiet") || ctx.GlobalBool("quiet") globalJSON = ctx.Bool("json") || ctx.GlobalBool("json") customReleaseURL := ctx.Args().Get(0) updateMsg, sha256Hex, _, latestReleaseTime, releaseTag, err := getUpdateInfo(customReleaseURL, 10*time.Second) if err != nil { errorIf(err, "Unable to update ‘mc’.") os.Exit(-1) } // Nothing to update running the latest release. color.New(color.FgGreen, color.Bold) if updateMsg == "" { printMsg(updateMessage{ Status: "success", Message: colorGreenBold("You are already running the most recent version of ‘mc’."), }) os.Exit(0) } printMsg(updateMessage{ Status: "success", Message: updateMsg, }) // Avoid updating mc development, source builds. if updateMsg != "" { var updateStatusMsg string var err *probe.Error updateStatusMsg, err = doUpdate(customReleaseURL, sha256Hex, latestReleaseTime, releaseTag, true) if err != nil { errorIf(err, "Unable to update ‘mc’.") os.Exit(-1) } printMsg(updateMessage{Status: "success", Message: updateStatusMsg}) os.Exit(1) } } minio-client-0.0~20250403/cmd/update-notifier.go000066400000000000000000000065531477450377600212040ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "math" "runtime" "strings" "time" "github.com/cheggaaa/pb" humanize "github.com/dustin/go-humanize" ) // prepareUpdateMessage - prepares the update message, only if a // newer version is available. func prepareUpdateMessage(downloadURL string, older time.Duration) string { if downloadURL == "" || older <= 0 { return "" } // Compute friendly duration string to indicate time // difference between newer and current release. t := time.Time{} newerThan := humanize.RelTime(t, t.Add(older), "ago", "") // Return the nicely colored and formatted update message. return colorizeUpdateMessage(downloadURL, newerThan) } // colorizeUpdateMessage - inspired from Yeoman project npm package https://github.com/yeoman/update-notifier func colorizeUpdateMessage(updateString, newerThan string) string { msgLine1Fmt := " You are running an older version of mc released %s " msgLine2Fmt := " Update: %s " // Calculate length *without* color coding: with ANSI terminal // color characters, the result is incorrect. line1Length := len(fmt.Sprintf(msgLine1Fmt, newerThan)) line2Length := len(fmt.Sprintf(msgLine2Fmt, updateString)) // Populate lines with color coding. line1InColor := fmt.Sprintf(msgLine1Fmt, colorYellowBold(newerThan)) line2InColor := fmt.Sprintf(msgLine2Fmt, colorCyanBold(updateString)) // calculate the rectangular box size. maxContentWidth := int(math.Max(float64(line1Length), float64(line2Length))) // termWidth is set to a default one to use when we are // not able to calculate terminal width via OS syscalls termWidth := 25 if width, err := pb.GetTerminalWidth(); err == nil { termWidth = width } // Box cannot be printed if terminal width is small than maxContentWidth if maxContentWidth > termWidth { return "\n" + line1InColor + "\n" + line2InColor + "\n\n" } topLeftChar := "┏" topRightChar := "┓" bottomLeftChar := "┗" bottomRightChar := "┛" horizBarChar := "━" vertBarChar := "┃" // on windows terminal turn off unicode characters. if runtime.GOOS == "windows" { topLeftChar = "+" topRightChar = "+" bottomLeftChar = "+" bottomRightChar = "+" horizBarChar = "-" vertBarChar = "|" } lines := []string{ colorYellowBold(topLeftChar + strings.Repeat(horizBarChar, maxContentWidth) + topRightChar), vertBarChar + line1InColor + strings.Repeat(" ", maxContentWidth-line1Length) + vertBarChar, vertBarChar + line2InColor + strings.Repeat(" ", maxContentWidth-line2Length) + vertBarChar, colorYellowBold(bottomLeftChar + strings.Repeat(horizBarChar, maxContentWidth) + bottomRightChar), } return "\n" + strings.Join(lines, "\n") + "\n" } minio-client-0.0~20250403/cmd/update_fips.go000066400000000000000000000016351477450377600204040ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . //go:build fips // +build fips package cmd // Newer official download info URLs appear earlier below. var mcReleaseInfoURL = mcReleaseURL + "mc.fips.sha256sum" minio-client-0.0~20250403/cmd/update_nofips.go000066400000000000000000000016321477450377600207360ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . //go:build !fips // +build !fips package cmd // Newer official download info URLs appear earlier below. var mcReleaseInfoURL = mcReleaseURL + "mc.sha256sum" minio-client-0.0~20250403/cmd/urls.go000066400000000000000000000040031477450377600170560ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" ) // URLs contains source and target urls type URLs struct { SourceAlias string SourceContent *ClientContent TargetAlias string TargetContent *ClientContent TotalCount int64 TotalSize int64 MD5 bool DisableMultipart bool checksum minio.ChecksumType encKeyDB map[string][]prefixSSEPair Error *probe.Error `json:"-"` ErrorCond differType `json:"-"` } // WithError sets the error and returns object func (m URLs) WithError(err *probe.Error) URLs { m.Error = err return m } // Equal tests if both urls are equal func (m URLs) Equal(n URLs) bool { if m.SourceContent != nil && n.SourceContent == nil { return false } else if m.SourceContent == nil && n.SourceContent != nil { return false } else if m.SourceContent != nil && n.SourceContent != nil && m.SourceContent.URL != n.SourceContent.URL { return false } if m.TargetContent != nil && n.TargetContent == nil { return false } else if m.TargetContent == nil && n.TargetContent != nil { return false } else if m.TargetContent != nil && n.TargetContent != nil && m.TargetContent.URL != n.TargetContent.URL { return false } return true } minio-client-0.0~20250403/cmd/utils.go000066400000000000000000000262701477450377600172430ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "crypto/tls" "errors" "fmt" "math" "math/rand" "net" "net/http" "os" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/mattn/go-ieproxy" "github.com/minio/madmin-go/v3" "github.com/minio/minio-go/v7" jwtgo "github.com/golang-jwt/jwt/v4" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) func isErrIgnored(err *probe.Error) (ignored bool) { // For all non critical errors we can continue for the remaining files. switch e := err.ToGoError().(type) { // Handle these specifically for filesystem related errors. case BrokenSymlink, TooManyLevelsSymlink, PathNotFound: ignored = true // Handle these specifically for object storage related errors. case BucketNameEmpty, ObjectMissing, ObjectAlreadyExists: ignored = true case ObjectAlreadyExistsAsDirectory, BucketDoesNotExist, BucketInvalid: ignored = true case minio.ErrorResponse: ignored = strings.Contains(e.Error(), "The specified key does not exist") default: ignored = false } return ignored } const ( letterBytes = "abcdefghijklmnopqrstuvwxyz01234569" letterIdxBits = 6 // 6 bits to represent a letter index letterIdxMask = 1<= 0; { if remain == 0 { cache, remain = src.Int63(), letterIdxMax } if idx := int(cache & letterIdxMask); idx < len(letterBytes) { b[i] = letterBytes[idx] i-- } cache >>= letterIdxBits remain-- } x := n / 2 if x == 0 { x = 1 } return prefix + string(b[0:x]) } // printTLSCertInfo prints some fields of the certificates received from the server. // Fields will be inspected by the user, so they must be conscise and useful func printTLSCertInfo(t *tls.ConnectionState) { if globalDebug { for _, cert := range t.PeerCertificates { console.Debugln("TLS Certificate found: ") if len(cert.Issuer.Country) > 0 { console.Debugln(" >> Country: " + cert.Issuer.Country[0]) } if len(cert.Issuer.Organization) > 0 { console.Debugln(" >> Organization: " + cert.Issuer.Organization[0]) } console.Debugln(" >> Expires: " + cert.NotAfter.String()) } } } // splitStr splits a string into n parts, empty strings are added // if we are not able to reach n elements func splitStr(path, sep string, n int) []string { splits := strings.SplitN(path, sep, n) // Add empty strings if we found elements less than nr for i := n - len(splits); i > 0; i-- { splits = append(splits, "") } return splits } // NewS3Config simply creates a new Config struct using the passed // parameters. func NewS3Config(alias, urlStr string, aliasCfg *aliasConfigV10) *Config { // We have a valid alias and hostConfig. We populate the // credentials from the match found in the config file. s3Config := new(Config) s3Config.AppName = filepath.Base(os.Args[0]) s3Config.AppVersion = ReleaseTag s3Config.Debug = globalDebug s3Config.Insecure = globalInsecure s3Config.ConnReadDeadline = globalConnReadDeadline s3Config.ConnWriteDeadline = globalConnWriteDeadline s3Config.UploadLimit = int64(globalLimitUpload) s3Config.DownloadLimit = int64(globalLimitDownload) s3Config.HostURL = urlStr s3Config.Alias = alias if aliasCfg != nil { s3Config.AccessKey = aliasCfg.AccessKey s3Config.SecretKey = aliasCfg.SecretKey s3Config.SessionToken = aliasCfg.SessionToken s3Config.Signature = aliasCfg.API s3Config.Lookup = getLookupType(aliasCfg.Path) } return s3Config } // lineTrunc - truncates a string to the given maximum length by // adding ellipsis in the middle func lineTrunc(content string, maxLen int) string { runes := []rune(content) rlen := len(runes) if rlen <= maxLen { return content } halfLen := maxLen / 2 fstPart := string(runes[0:halfLen]) sndPart := string(runes[rlen-halfLen:]) return fstPart + "…" + sndPart } // isOlder returns true if the passed object is older than olderRef func isOlder(ti time.Time, olderRef string) bool { if olderRef == "" { return false } objectAge := time.Since(ti) olderThan, e := ParseDuration(olderRef) if e != nil { for _, format := range rewindSupportedFormat { if t, e2 := time.Parse(format, olderRef); e2 == nil { olderThan = Duration(time.Since(t)) e = nil break } } } fatalIf(probe.NewError(e), "Unable to parse olderThan=`"+olderRef+"`. Supply relative '7d6h2m' or absolute '"+printDate+"'.") return objectAge < time.Duration(olderThan) } // isNewer returns true if the passed object is newer than newerRef func isNewer(ti time.Time, newerRef string) bool { if newerRef == "" { return false } objectAge := time.Since(ti) newerThan, e := ParseDuration(newerRef) if e != nil { for _, format := range rewindSupportedFormat { if t, e2 := time.Parse(format, newerRef); e2 == nil { newerThan = Duration(time.Since(t)) e = nil break } } } fatalIf(probe.NewError(e), "Unable to parse newerThan=`"+newerRef+"`. Supply relative '7d6h2m' or absolute '"+printDate+"'.") return objectAge >= time.Duration(newerThan) } // getLookupType returns the minio.BucketLookupType for lookup // option entered on the command line func getLookupType(l string) minio.BucketLookupType { l = strings.ToLower(l) switch l { case "off": return minio.BucketLookupDNS case "on": return minio.BucketLookupPath } return minio.BucketLookupAuto } // Return true if target url is a part of a source url such as: // alias/bucket/ and alias/bucket/dir/, however func isURLContains(srcURL, tgtURL, sep string) bool { // Add a separator to source url if not found if !strings.HasSuffix(srcURL, sep) { srcURL += sep } if !strings.HasSuffix(tgtURL, sep) { tgtURL += sep } // Check if we are going to copy a directory into itself if strings.HasPrefix(tgtURL, srcURL) { return true } return false } // ErrInvalidFileSystemAttribute reflects invalid fily system attribute var ErrInvalidFileSystemAttribute = errors.New("Error in parsing file system attribute") func parseAtimeMtime(attr map[string]string) (atime, mtime time.Time, err *probe.Error) { if val, ok := attr["atime"]; ok { vals := strings.SplitN(val, "#", 2) atim, e := strconv.ParseInt(vals[0], 10, 64) if e != nil { return atime, mtime, probe.NewError(e) } var atimnsec int64 if len(vals) == 2 { atimnsec, e = strconv.ParseInt(vals[1], 10, 64) if e != nil { return atime, mtime, probe.NewError(e) } } atime = time.Unix(atim, atimnsec) } if val, ok := attr["mtime"]; ok { vals := strings.SplitN(val, "#", 2) mtim, e := strconv.ParseInt(vals[0], 10, 64) if e != nil { return atime, mtime, probe.NewError(e) } var mtimnsec int64 if len(vals) == 2 { mtimnsec, e = strconv.ParseInt(vals[1], 10, 64) if e != nil { return atime, mtime, probe.NewError(e) } } mtime = time.Unix(mtim, mtimnsec) } return atime, mtime, nil } // Returns a map by parsing the value of X-Amz-Meta-Mc-Attrs/X-Amz-Meta-s3Cmd-Attrs func parseAttribute(meta map[string]string) (map[string]string, error) { attribute := make(map[string]string) if meta == nil { return attribute, nil } parseAttrs := func(attrs string) error { var err error param := strings.Split(attrs, "/") for _, val := range param { attr := strings.TrimSpace(val) if attr == "" { err = ErrInvalidFileSystemAttribute } else { attrVal := strings.Split(attr, ":") if len(attrVal) == 2 { attribute[strings.TrimSpace(attrVal[0])] = strings.TrimSpace(attrVal[1]) } else if len(attrVal) == 1 { attribute[attrVal[0]] = "" } else { err = ErrInvalidFileSystemAttribute } } } return err } if attrs, ok := meta[metadataKey]; ok { if err := parseAttrs(attrs); err != nil { return attribute, err } } if attrs, ok := meta[metadataKeyS3Cmd]; ok { if err := parseAttrs(attrs); err != nil { return attribute, err } } return attribute, nil } const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" var reAnsi = regexp.MustCompile(ansi) func centerText(s string, w int) string { var sb strings.Builder textWithoutColor := reAnsi.ReplaceAllString(s, "") length := len(textWithoutColor) padding := float64(w-length) / 2 fmt.Fprintf(&sb, "%s", bytes.Repeat([]byte{' '}, int(math.Ceil(padding)))) fmt.Fprintf(&sb, "%s", s) fmt.Fprintf(&sb, "%s", bytes.Repeat([]byte{' '}, int(math.Floor(padding)))) return sb.String() } func getClient(aliasURL string) *madmin.AdminClient { client, err := newAdminClient(aliasURL) fatalIf(err, "Unable to initialize admin connection.") return client } func httpClient(reqTimeout time.Duration) *http.Client { return &http.Client{ Timeout: reqTimeout, Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 10 * time.Second, }).DialContext, Proxy: ieproxy.GetProxyFunc(), TLSClientConfig: &tls.Config{ RootCAs: globalRootCAs, InsecureSkipVerify: globalInsecure, // Can't use SSLv3 because of POODLE and BEAST // Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher // Can't use TLSv1.1 because of RC4 cipher usage MinVersion: tls.VersionTLS12, }, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 10 * time.Second, }, } } func getPrometheusToken(hostConfig *aliasConfigV10) (string, error) { jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.RegisteredClaims{ ExpiresAt: jwtgo.NewNumericDate(UTCNow().Add(defaultPrometheusJWTExpiry)), Subject: hostConfig.AccessKey, Issuer: "prometheus", }) token, e := jwt.SignedString([]byte(hostConfig.SecretKey)) if e != nil { return "", e } return token, nil } // conservativeFileName returns a conservative file name func conservativeFileName(s string) string { return strings.Trim(strings.Map(func(r rune) rune { switch { case r >= 'a' && r <= 'z': return r case r >= 'A' && r <= 'Z': return r case r >= '0' && r <= '9': return r case strings.ContainsAny(string(r), "+-_%()[]!@"): return r default: return '_' } }, s), "_") } minio-client-0.0~20250403/cmd/utils_test.go000066400000000000000000000051211477450377600202720ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "reflect" "testing" ) func TestParseAttribute(t *testing.T) { metaDataCases := []struct { input string output map[string]string err error status bool }{ // // When blank value is passed. {"", map[string]string{}, ErrInvalidFileSystemAttribute, false}, // When space is passed. {" ", map[string]string{}, ErrInvalidFileSystemAttribute, false}, // When / is passed. {"/", map[string]string{}, ErrInvalidFileSystemAttribute, false}, // When "atime:" is passed. {"atime:/", map[string]string{"atime": ""}, ErrInvalidFileSystemAttribute, false}, // When "atime:" is passed. {"atime", map[string]string{"atime": ""}, nil, true}, // When "atime:" is passed. {"atime:", map[string]string{"atime": ""}, nil, true}, // Passing a valid value { "atime:1/gid:1/gname:a/md:/mode:3/mtime:1/uid:1/uname:a", map[string]string{ "atime": "1", "gid": "1", "gname": "a", "md": "", "mode": "3", "mtime": "1", "uid": "1", "uname": "a", }, nil, true, }, } for idx, testCase := range metaDataCases { meta, err := parseAttribute(map[string]string{ metadataKey: testCase.input, }) if testCase.status == true { if err != nil { t.Fatalf("Test %d: generated error not matching, expected = `%s`, found = `%s`", idx+1, testCase.err, err) } if !reflect.DeepEqual(meta, testCase.output) { t.Fatalf("Test %d: generated Map not matching, expected = `%s`, found = `%s`", idx+1, testCase.input, meta) } } if testCase.status == false { if !reflect.DeepEqual(meta, testCase.output) { t.Fatalf("Test %d: generated Map not matching, expected = `%s`, found = `%s`", idx+1, testCase.input, meta) } if err != testCase.err { t.Fatalf("Test %d: generated error not matching, expected = `%s`, found = `%s`", idx+1, testCase.err, err) } } } } minio-client-0.0~20250403/cmd/version-enable.go000066400000000000000000000100301477450377600207770ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var versionEnableFlags = []cli.Flag{ cli.StringFlag{ Name: "excluded-prefixes", Usage: "exclude versioning on these prefix patterns", }, cli.BoolFlag{ Name: "exclude-folders", Usage: "exclude versioning on folder objects", }, } var versionEnableCmd = cli.Command{ Name: "enable", Usage: "enable bucket versioning", Action: mainVersionEnable, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(globalFlags, versionEnableFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS/BUCKET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Enable versioning on bucket "mybucket" for alias "myminio". {{.Prompt}} {{.HelpName}} myminio/mybucket 2. Enable versioning on bucket "mybucket" while excluding versioning on a few select prefixes. {{.Prompt}} {{.HelpName}} myminio/mybucket --excluded-prefixes "app1/*/_temporary/,app2/*/_staging/" 3. Enable versioning on bucket "mybucket" while excluding versioning on a few select prefixes and all folders. Note: this is useful on buckets used with Spark/Hadoop workloads. {{.Prompt}} {{.HelpName}} myminio/mybucket --excluded-prefixes "app1/*/_temporary/,app2/*/_staging/" --exclude-folders `, } // checkVersionEnableSyntax - validate all the passed arguments func checkVersionEnableSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } type versionEnableMessage struct { Op string Status string `json:"status"` URL string `json:"url"` Versioning struct { Status string `json:"status"` MFADelete string `json:"MFADelete"` ExcludedPrefixes []string `json:"ExcludedPrefixes,omitempty"` ExcludeFolders bool `json:"ExcludeFolders,,omitempty"` } `json:"versioning"` } func (v versionEnableMessage) JSON() string { v.Status = "success" jsonMessageBytes, e := json.MarshalIndent(v, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (v versionEnableMessage) String() string { return console.Colorize("versionEnableMessage", fmt.Sprintf("%s versioning is enabled", v.URL)) } func mainVersionEnable(cliCtx *cli.Context) error { ctx, cancelVersionEnable := context.WithCancel(globalContext) defer cancelVersionEnable() console.SetColor("versionEnableMessage", color.New(color.FgGreen)) checkVersionEnableSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) var excludedPrefixes []string prefixesStr := cliCtx.String("excluded-prefixes") if prefixesStr != "" { excludedPrefixes = strings.Split(prefixesStr, ",") } excludeFolders := cliCtx.Bool("exclude-folders") // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") fatalIf(client.SetVersion(ctx, "enable", excludedPrefixes, excludeFolders), "Unable to enable versioning") printMsg(versionEnableMessage{ Op: cliCtx.Command.Name, Status: "success", URL: aliasedURL, }) return nil } minio-client-0.0~20250403/cmd/version-info.go000066400000000000000000000071201477450377600205120ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var versionInfoCmd = cli.Command{ Name: "info", Usage: "show bucket versioning status", Action: mainVersionInfo, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS/BUCKET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Display bucket versioning status for bucket "mybucket". {{.Prompt}} {{.HelpName}} myminio/mybucket `, } // checkVersionInfoSyntax - validate all the passed arguments func checkVersionInfoSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } type versioningInfoMessage struct { Op string Status string `json:"status"` URL string `json:"url"` Versioning struct { Status string `json:"status"` MFADelete string `json:"MFADelete"` ExcludedPrefixes []string `json:"ExcludedPrefixes,omitempty"` ExcludeFolders bool `json:"ExcludeFolders,omitempty"` } `json:"versioning"` } func (v versioningInfoMessage) JSON() string { v.Status = "success" jsonMessageBytes, e := json.MarshalIndent(v, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (v versioningInfoMessage) String() string { msg := "" switch v.Versioning.Status { case "": msg = fmt.Sprintf("%s is un-versioned", v.URL) default: msg = fmt.Sprintf("%s versioning is %s", v.URL, strings.ToLower(v.Versioning.Status)) } return console.Colorize("versioningInfoMessage", msg) } func mainVersionInfo(cliCtx *cli.Context) error { ctx, cancelVersioningInfo := context.WithCancel(globalContext) defer cancelVersioningInfo() console.SetColor("versioningInfoMessage", color.New(color.FgGreen)) checkVersionInfoSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") vConfig, e := client.GetVersion(ctx) fatalIf(e, "Unable to get versioning info") vMsg := versioningInfoMessage{ Op: cliCtx.Command.Name, Status: "success", URL: aliasedURL, } vMsg.Versioning.Status = vConfig.Status vMsg.Versioning.MFADelete = vConfig.MFADelete vMsg.Versioning.ExcludeFolders = vConfig.ExcludeFolders if len(vConfig.ExcludedPrefixes) > 0 { prefixes := make([]string, 0, len(vConfig.ExcludedPrefixes)) for _, eprefix := range vConfig.ExcludedPrefixes { prefixes = append(prefixes, eprefix.Prefix) } vMsg.Versioning.ExcludedPrefixes = prefixes } printMsg(vMsg) return nil } minio-client-0.0~20250403/cmd/version-main.go000066400000000000000000000025741477450377600205130ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import "github.com/minio/cli" var versionSubcommands = []cli.Command{ versionEnableCmd, versionSuspendCmd, versionInfoCmd, } var versionCmd = cli.Command{ Name: "version", Usage: "manage bucket versioning", HideHelpCommand: true, Action: mainVersion, Before: setGlobalsFromContext, Flags: globalFlags, Subcommands: versionSubcommands, } // mainVersion is the handle for "mc version" command. func mainVersion(ctx *cli.Context) error { commandNotFound(ctx, versionSubcommands) return nil // Sub-commands like "info", "enable", "suspend" have their own main. } minio-client-0.0~20250403/cmd/version-suspend.go000066400000000000000000000056231477450377600212460ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var versionSuspendCmd = cli.Command{ Name: "suspend", Usage: "suspend bucket versioning", Action: mainVersionSuspend, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: globalFlags, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} ALIAS/BUCKET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Suspend versioning on bucket "mybucket" for alias "myminio". {{.Prompt}} {{.HelpName}} myminio/mybucket `, } // checkVersionSuspendSyntax - validate all the passed arguments func checkVersionSuspendSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } type versionSuspendMessage struct { Op string Status string `json:"status"` URL string `json:"url"` Versioning struct { Status string `json:"status"` MFADelete string `json:"MFADelete"` } `json:"versioning"` } func (v versionSuspendMessage) JSON() string { v.Status = "success" jsonMessageBytes, e := json.MarshalIndent(v, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(jsonMessageBytes) } func (v versionSuspendMessage) String() string { return console.Colorize("versionSuspendMessage", fmt.Sprintf("%s versioning is suspended", v.URL)) } func mainVersionSuspend(cliCtx *cli.Context) error { ctx, cancelVersionSuspend := context.WithCancel(globalContext) defer cancelVersionSuspend() console.SetColor("versionSuspendMessage", color.New(color.FgGreen)) checkVersionSuspendSyntax(cliCtx) // Get the alias parameter from cli args := cliCtx.Args() aliasedURL := args.Get(0) // Create a new Client client, err := newClient(aliasedURL) fatalIf(err, "Unable to initialize connection.") fatalIf(client.SetVersion(ctx, "suspend", nil, false), "Unable to suspend versioning") printMsg(versionSuspendMessage{ Op: cliCtx.Command.Name, Status: "success", URL: aliasedURL, }) return nil } minio-client-0.0~20250403/cmd/watch-main.go000066400000000000000000000134771477450377600201400ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "strings" "sync" humanize "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/notification" "github.com/minio/pkg/v3/console" ) var watchFlags = []cli.Flag{ cli.StringFlag{ Name: "events", Value: "put,delete,get", Usage: "filter specific types of events; defaults to all events by default", }, cli.StringFlag{ Name: "prefix", Usage: "filter events for a prefix", }, cli.StringFlag{ Name: "suffix", Usage: "filter events for a suffix", }, cli.BoolFlag{ Name: "recursive", Usage: "recursively watch for events", }, } var watchCmd = cli.Command{ Name: "watch", Usage: "listen for object notification events", Action: mainWatch, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(watchFlags, globalFlags...), CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] TARGET FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Watch new S3 operations on a MinIO server {{.Prompt}} {{.HelpName}} play/testbucket 2. Watch new events for a specific prefix "output/" on MinIO server. {{.Prompt}} {{.HelpName}} --prefix "output/" play/testbucket 3. Watch new events for a specific suffix ".jpg" on MinIO server. {{.Prompt}} {{.HelpName}} --suffix ".jpg" play/testbucket 4. Watch new events on a specific prefix and suffix on MinIO server. {{.Prompt}} {{.HelpName}} --suffix ".jpg" --prefix "photos/" play/testbucket 5. Site level watch (except new buckets created after running this command) {{.Prompt}} {{.HelpName}} play/ 6. Watch for events on local directory. {{.Prompt}} {{.HelpName}} /usr/share `, } // checkWatchSyntax - validate all the passed arguments func checkWatchSyntax(ctx *cli.Context) { if len(ctx.Args()) != 1 { showCommandHelpAndExit(ctx, 1) // last argument is exit code } } // watchMessage container to hold one event notification type watchMessage struct { Status string `json:"status"` Event struct { Time string `json:"time"` Size int64 `json:"size"` Path string `json:"path"` Type notification.EventType `json:"type"` } `json:"events"` Source struct { Host string `json:"host,omitempty"` Port string `json:"port,omitempty"` UserAgent string `json:"userAgent,omitempty"` } `json:"source,omitempty"` } func (u watchMessage) JSON() string { u.Status = "success" watchMessageJSONBytes, e := json.MarshalIndent(u, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(watchMessageJSONBytes) } func (u watchMessage) String() string { msg := console.Colorize("Time", fmt.Sprintf("[%s] ", u.Event.Time)) if strings.HasPrefix(string(u.Event.Type), "s3:ObjectCreated:") { msg += console.Colorize("Size", fmt.Sprintf("%6s ", humanize.IBytes(uint64(u.Event.Size)))) } else { msg += fmt.Sprintf("%6s ", "") } msg += console.Colorize("EventType", fmt.Sprintf("%s ", u.Event.Type)) msg += console.Colorize("ObjectName", u.Event.Path) return msg } func mainWatch(cliCtx *cli.Context) error { console.SetColor("Time", color.New(color.FgGreen)) console.SetColor("Size", color.New(color.FgYellow)) console.SetColor("EventType", color.New(color.FgCyan, color.Bold)) console.SetColor("ObjectName", color.New(color.Bold)) checkWatchSyntax(cliCtx) args := cliCtx.Args() path := args[0] prefix := cliCtx.String("prefix") suffix := cliCtx.String("suffix") events := strings.Split(cliCtx.String("events"), ",") recursive := cliCtx.Bool("recursive") s3Client, pErr := newClient(path) if pErr != nil { fatalIf(pErr.Trace(), "Unable to parse the provided url.") } options := WatchOptions{ Recursive: recursive, Events: events, Prefix: prefix, Suffix: suffix, } ctx, cancelWatch := context.WithCancel(globalContext) defer cancelWatch() // Start watching on events wo, err := s3Client.Watch(ctx, options) fatalIf(err, "Unable to watch on the specified bucket.") // Initialize.. waitgroup to track the go-routine. var wg sync.WaitGroup // Increment wait group to wait subsequent routine. wg.Add(1) // Start routine to watching on events. go func() { defer wg.Done() // Wait for all events. for { select { case <-globalContext.Done(): // Signal received we are done. close(wo.DoneChan) return case events, ok := <-wo.Events(): if !ok { return } for _, event := range events { msg := watchMessage{} msg.Event.Path = event.Path msg.Event.Size = event.Size msg.Event.Time = event.Time msg.Event.Type = event.Type msg.Source.Host = event.Host msg.Source.Port = event.Port msg.Source.UserAgent = event.UserAgent printMsg(msg) } case err, ok := <-wo.Errors(): if !ok { return } if err != nil { errorIf(err, "Unable to watch for events.") return } } } }() // Wait on the routine to be finished or exit. wg.Wait() return nil } minio-client-0.0~20250403/cmd/watch.go000066400000000000000000000072561477450377600172140ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "sync" "time" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/notification" ) // EventInfo contains the information of the event that occurred and the source // IP:PORT of the client which triggerred the event. type EventInfo struct { Time string Size int64 UserMetadata map[string]string Path string Host string Port string UserAgent string Type notification.EventType } // WatchOptions contains watch configuration options type WatchOptions struct { Prefix string Suffix string Events []string Recursive bool } // WatchObject captures watch channels to read and listen on. type WatchObject struct { // eventInfo will be put on this chan EventInfoChan chan []EventInfo // errors will be put on this chan ErrorChan chan *probe.Error // will stop the watcher goroutines DoneChan chan struct{} } // Events returns the chan receiving events func (w *WatchObject) Events() chan []EventInfo { return w.EventInfoChan } // Errors returns the chan receiving errors func (w *WatchObject) Errors() chan *probe.Error { return w.ErrorChan } // Watcher can be used to have one or multiple clients watch for notifications type Watcher struct { sessionStartTime time.Time // all error will be added to this chan ErrorChan chan *probe.Error // all events will be added to this chan EventInfoChan chan []EventInfo // array of watchers joined o []*WatchObject // all watchers joining will enter this waitgroup wg sync.WaitGroup } // NewWatcher creates a new watcher func NewWatcher(sessionStartTime time.Time) *Watcher { return &Watcher{ sessionStartTime: sessionStartTime, ErrorChan: make(chan *probe.Error), EventInfoChan: make(chan []EventInfo), o: []*WatchObject{}, } } // Errors returns a channel which will receive errors func (w *Watcher) Errors() chan *probe.Error { return w.ErrorChan } // Events returns a channel which will receive events func (w *Watcher) Events() chan []EventInfo { return w.EventInfoChan } // Stop all watchers func (w *Watcher) Stop() { for _, w := range w.o { close(w.DoneChan) } w.wg.Wait() } // Join the watcher with client func (w *Watcher) Join(ctx context.Context, client Client, recursive bool) *probe.Error { wo, err := client.Watch(ctx, WatchOptions{ Recursive: recursive, Events: []string{"put", "delete", "bucket-creation", "bucket-removal"}, }) if err != nil { return err } w.o = append(w.o, wo) // join monitoring waitgroup w.wg.Add(1) // wait for events and errors of individual client watchers // and sent then to eventsChan and errorsChan go func() { defer w.wg.Done() for { select { case <-wo.DoneChan: return case events, ok := <-wo.Events(): if !ok { return } w.EventInfoChan <- events case err, ok := <-wo.Errors(): if !ok { return } w.ErrorChan <- err } } }() return nil } minio-client-0.0~20250403/code_of_conduct.md000066400000000000000000000067621477450377600204540ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior, in compliance with the licensing terms applying to the Project developments. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. However, these actions shall respect the licensing terms of the Project Developments that will always supersede such Code of Conduct. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at dev@min.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] This version includes a clarification to ensure that the code of conduct is in compliance with the free software licensing terms of the project. [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ minio-client-0.0~20250403/contributors.sh000077500000000000000000000021551477450377600201010ustar00rootroot00000000000000#!/bin/bash # # Copyright (c) 2015-2021 MinIO, Inc. # # This file is part of MinIO Object Storage stack # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # set -e cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" # see also ".mailmap" for how email addresses and names are deduplicated { cat <<-'EOH' ## Contributors EOH echo git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf | sed 's/^/- /g' } > CONTRIBUTORS.md minio-client-0.0~20250403/docker-buildx.sh000077500000000000000000000026611477450377600201020ustar00rootroot00000000000000#!/bin/bash # # Copyright (c) 2015-2023 MinIO, Inc. # # This file is part of MinIO Object Storage stack # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # sudo sysctl net.ipv6.conf.all.disable_ipv6=1 release=$(git describe --abbrev=0 --tags) docker buildx build --push --no-cache \ --build-arg RELEASE="${release}" \ -t "minio/mc:latest" \ -t "minio/mc:${release}" \ -t "quay.io/minio/mc:${release}" \ -t "quay.io/minio/mc:latest" \ --platform=linux/arm64,linux/amd64,linux/ppc64le \ -f Dockerfile.release . docker buildx prune -f docker buildx build --push --no-cache \ --build-arg RELEASE="${release}" \ -t "minio/mc:${release}-cpuv1" \ -t "quay.io/minio/mc:${release}-cpuv1" \ --platform=linux/arm64,linux/amd64,linux/ppc64le \ -f Dockerfile.release.old_cpu . docker buildx prune -f sudo sysctl net.ipv6.conf.all.disable_ipv6=0 minio-client-0.0~20250403/functional-tests.sh000077500000000000000000001317721477450377600206560ustar00rootroot00000000000000#!/bin/bash # # Copyright (c) 2015-2024 MinIO, Inc. # # This file is part of MinIO Object Storage stack # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # ################################################################################ # # This script is usable by mc functional tests, mint tests and MinIO verification # tests. # # * As mc functional tests, just run this script. It uses mc executable binary # in current working directory or in the path. The tests uses play.min.io # as MinIO server. # # * For other, call this script with environment variables MINT_MODE, # MINT_DATA_DIR, SERVER_ENDPOINT, ACCESS_KEY, SECRET_KEY and ENABLE_HTTPS. It # uses mc executable binary in current working directory and uses given MinIO # server to run tests. MINT_MODE is set by mint to specify what category of # tests to run. # ################################################################################ # Force bytewise sorting for CLI tools LANG=C if [ -n "$DEBUG" ]; then set -x fi if [ -n "$MINT_MODE" ]; then if [ -z "${MINT_DATA_DIR+x}" ]; then echo "MINT_DATA_DIR not defined" exit 1 fi if [ -z "${SERVER_ENDPOINT+x}" ]; then echo "SERVER_ENDPOINT not defined" exit 1 fi if [ -z "${ACCESS_KEY+x}" ]; then echo "ACCESS_KEY not defined" exit 1 fi if [ -z "${SECRET_KEY+x}" ]; then echo "SECRET_KEY not defined" exit 1 fi fi if [ -z "${SERVER_ENDPOINT+x}" ]; then SERVER_ENDPOINT="play.min.io" ACCESS_KEY="Q3AM3UQ867SPQQA43P2F" SECRET_KEY="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" ENABLE_HTTPS=1 fi # If you want to run the complete site cleaning test, set this variable to true COMPLETE_RB_TEST=true WORK_DIR="$PWD" DATA_DIR="$MINT_DATA_DIR" if [ -z "$MINT_MODE" ]; then WORK_DIR="$PWD/.run-$RANDOM" DATA_DIR="$WORK_DIR/data" fi FILE_0_B="$DATA_DIR/datafile-0-b" FILE_1_MB="$DATA_DIR/datafile-1-MB" FILE_65_MB="$DATA_DIR/datafile-65-MB" declare FILE_0_B_MD5SUM declare FILE_1_MB_MD5SUM declare FILE_65_MB_MD5SUM ENDPOINT="https://$SERVER_ENDPOINT" if [ "$ENABLE_HTTPS" != "1" ]; then ENDPOINT="http://$SERVER_ENDPOINT" fi SERVER_ALIAS="myminio" SERVER_ALIAS_TLS="myminio-ssl" BUCKET_NAME="mc-test-bucket-$RANDOM" WATCH_OUT_FILE="$WORK_DIR/watch.out-$RANDOM" MC_CONFIG_DIR="/tmp/.mc-$RANDOM" MC="$PWD/mc" declare -a MC_CMD function get_md5sum() { filename="$1" out=$(md5sum "$filename" 2>/dev/null) rv=$? if [ "$rv" -eq 0 ]; then echo $(awk '{ print $1 }' <<<"$out") fi return "$rv" } function get_time() { date +%s%N } function get_duration() { start_time=$1 end_time=$(get_time) echo $(((end_time - start_time) / 1000000)) } function log_success() { if [ -n "$MINT_MODE" ]; then printf '{"name": "mc", "duration": "%d", "function": "%s", "status": "PASS"}\n' "$(get_duration "$1")" "$2" fi } function show() { if [ -z "$MINT_MODE" ]; then func_name="$1" echo "Running $func_name()" fi } function show_on_success() { rv="$1" shift if [ "$rv" -eq 0 ]; then echo "$@" fi return "$rv" } function show_on_failure() { rv="$1" shift if [ "$rv" -ne 0 ]; then echo "$@" fi return "$rv" } function assert() { expected_rv="$1" shift start_time="$1" shift func_name="$1" shift err=$("$@") rv=$? if [ "$rv" -ne "$expected_rv" ]; then if [ -n "$MINT_MODE" ]; then err=$(printf "$err" | python -c 'import sys,json; print(json.dumps(sys.stdin.read()))') ## err is already JSON string, no need to double quote printf '{"name": "mc", "duration": "%d", "function": "%s", "status": "FAIL", "error": %s}\n' "$(get_duration "$start_time")" "$func_name" "$err" else echo "mc: $func_name: $err" fi if [ "$rv" -eq 0 ]; then exit 1 fi exit "$rv" fi return 0 } function assert_success() { assert 0 "$@" } function assert_failure() { assert 1 "$@" } function mc_cmd() { cmd=("${MC_CMD[@]}" "$@") err_file="$WORK_DIR/cmd.out.$RANDOM" "${cmd[@]}" >"$err_file" 2>&1 rv=$? if [ "$rv" -ne 0 ]; then printf '%q ' "${cmd[@]}" echo " >>> " cat "$err_file" fi rm -f "$err_file" return "$rv" } function check_md5sum() { expected_checksum="$1" shift filename="$@" checksum="$(get_md5sum "$filename")" rv=$? if [ "$rv" -ne 0 ]; then echo "unable to get md5sum for $filename" return "$rv" fi if [ "$checksum" != "$expected_checksum" ]; then echo "$filename: md5sum mismatch" return 1 fi return 0 } function test_make_bucket() { show "${FUNCNAME[0]}" start_time=$(get_time) bucket_name="mc-test-bucket-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${bucket_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rb "${SERVER_ALIAS}/${bucket_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_make_bucket_error() { show "${FUNCNAME[0]}" start_time=$(get_time) bucket_name="MC-test%bucket%$RANDOM" assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${bucket_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_rb() { show "${FUNCNAME[0]}" start_time=$(get_time) bucket1="mc-test-bucket-$RANDOM-1" bucket2="mc-test-bucket-$RANDOM-2" object_name="mc-test-object-$RANDOM" # Tets rb when the bucket is empty assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${bucket1}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rb "${SERVER_ALIAS}/${bucket1}" # Test rb with --force flag when the bucket is not empty assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${bucket1}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${bucket1}/${object_name}" assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd rb "${SERVER_ALIAS}/${bucket1}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rb --force "${SERVER_ALIAS}/${bucket1}" # Test rb with --force and --dangerous to remove a site content if [ "${COMPLETE_RB_TEST}" == "true" ]; then assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${bucket1}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${bucket2}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${bucket1}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${bucket2}/${object_name}" assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd rb --force "${SERVER_ALIAS}/" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rb --force --dangerous "${SERVER_ALIAS}" fi log_success "$start_time" "${FUNCNAME[0]}" } function setup() { start_time=$(get_time) assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${BUCKET_NAME}" } function teardown() { start_time=$(get_time) assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rb --force "${SERVER_ALIAS}/${BUCKET_NAME}" } # Test mc ls on a S3 prefix where a lower similar prefix exists as well e.g. dir-foo/ and dir/ function test_list_dir() { show "${FUNCNAME[0]}" start_time=$(get_time) assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/dir-foo/object1" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/dir/object2" diff -bB <(echo "object2") <("${MC_CMD[@]}" --json ls "${SERVER_ALIAS}/${BUCKET_NAME}/dir" | jq -r '.key') >/dev/null 2>&1 assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unexpected listing dir" # Cleanup assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/dir-foo/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/dir/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_od_object() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd od if="${FILE_1_MB}" of="${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd od of="${FILE_1_MB}" if="${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_put_object() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_put_object_error() { show "${FUNCNAME[0]}" start_time=$(get_time) object_long_name=$(printf "mc-test-object-%01100d" 1) assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_long_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_put_object_multipart() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_65_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_put_object_0byte() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_0_B}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_0_B_MD5SUM" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${object_name}.downloaded" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } ## Test mc cp command with storage-class flag set function test_put_object_with_storage_class() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --storage-class REDUCED_REDUNDANCY "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } ## Test mc cp command with storage-class flag set to incorrect value function test_put_object_with_storage_class_error() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cp --storage-class REDUCED "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } ## Test mc cp command with valid metadata string function test_put_object_with_metadata() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --attr key1=val1\;key2=val2 "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" diff -bB <(echo "val1") <("${MC_CMD[@]}" --json stat "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" | jq -r '.metadata."X-Amz-Meta-Key1"') >/dev/null 2>&1 assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to put object with metadata" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_get_object() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_1_MB_MD5SUM" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${object_name}.downloaded" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_get_object_multipart() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_65_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_65_MB_MD5SUM" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${object_name}.downloaded" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_presigned_post_policy_error() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" out=$("${MC_CMD[@]}" --json share upload "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}") assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to get presigned post policy and put object url" # Support IPv6 address, IPv6 is specifed as [host]:9000 form, we should # replace '['']' with their escaped values as '\[' '\]'. # # Without escaping '['']', 'sed' command interprets them as expressions # which fails our requirement of replacing $endpoint/$bucket URLs in the # subsequent operations. endpoint=$(echo "$ENDPOINT" | sed 's|[][]|\\&|g') # Extract share field of json output, and append object name to the URL upload=$(echo "$out" | jq -r .share | sed "s||$FILE_1_MB|g" | sed "s|curl|curl -sSg|g" | sed "s|${endpoint}/${BUCKET_NAME}/|${endpoint}/${BUCKET_NAME}/${object_name}|g") # In case of virtual host style URL path, the previous replace would have failed. # One of the following two commands will append the object name in that scenario. upload=$(echo "$upload" | sed "s|http://${BUCKET_NAME}.${SERVER_ENDPOINT}/|http://${BUCKET_NAME}.${SERVER_ENDPOINT}/${object_name}|g") upload=$(echo "$upload" | sed "s|https://${BUCKET_NAME}.${SERVER_ENDPOINT}/|https://${BUCKET_NAME}.${SERVER_ENDPOINT}/${object_name}|g") ret=$($upload 2>&1 | grep -oP '(?<=Code>)[^<]+') # Check if the command execution failed. assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unknown failure in upload of $FILE_1_MB using presigned post policy" if [ -z "$ret" ]; then # Check if the upload succeeded. We expect it to fail. assert_failure "$start_time" "${FUNCNAME[0]}" show_on_success 0 "upload of $FILE_1_MB using presigned post policy should have failed" fi if [ "$ret" != "MethodNotAllowed" ]; then assert_failure "$start_time" "${FUNCNAME[0]}" show_on_success 0 "upload of $FILE_1_MB using presigned post policy should have failed with MethodNotAllowed error, instead failed with $ret error" fi log_success "$start_time" "${FUNCNAME[0]}" } function test_presigned_put_object() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" out=$("${MC_CMD[@]}" --json share upload "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}") assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to get presigned put object url" upload=$(echo "$out" | jq -r .share | sed "s||$FILE_1_MB|g" | sed "s|curl|curl -sSg|g") $upload >/dev/null 2>&1 assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to upload $FILE_1_MB presigned put object url" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_1_MB_MD5SUM" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${object_name}.downloaded" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_presigned_get_object() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" out=$("${MC_CMD[@]}" --json share download "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}") assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to get presigned get object url" download_url=$(echo "$out" | jq -r .share) curl -sSg --output "${object_name}.downloaded" -X GET "$download_url" assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to download $download_url" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_1_MB_MD5SUM" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${object_name}.downloaded" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_cat_object() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" "${MC_CMD[@]}" cat "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" >"${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to download object using 'mc cat'" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_1_MB_MD5SUM" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${object_name}.downloaded" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_cat_stdin() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" bucket_name="mc-test-bucket-$RANDOM" mc_cmd mb "${SERVER_ALIAS}/${bucket_name}" echo "testcontent" | mc_cmd pipe "${SERVER_ALIAS}/${bucket_name}/${object_name}" "${MC_CMD[@]}" cat "${SERVER_ALIAS}/${bucket_name}/${object_name}" >stdout.output assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to redirect stdin to stdout using 'mc cat'" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "42ed9fb3563d8e9c7bb522be443033f4" stdout.output assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm stdout.output log_success "$start_time" "${FUNCNAME[0]}" } function test_mirror_list_objects() { show "${FUNCNAME[0]}" start_time=$(get_time) bucket_name="mc-test-bucket-$RANDOM" object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${bucket_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mirror "$DATA_DIR" "${SERVER_ALIAS}/${bucket_name}" diff -bB <(ls "$DATA_DIR") <("${MC_CMD[@]}" --json ls "${SERVER_ALIAS}/${bucket_name}/" | jq -r .key) >/dev/null 2>&1 assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "mirror and list differs" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rb --force "${SERVER_ALIAS}/${bucket_name}" log_success "$start_time" "${FUNCNAME[0]}" } ## Tests mc mirror command with --storage-class flag set function test_mirror_list_objects_storage_class() { show "${FUNCNAME[0]}" start_time=$(get_time) bucket_name="mc-test-bucket-$RANDOM" object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${bucket_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mirror --storage-class REDUCED_REDUNDANCY "$DATA_DIR" "${SERVER_ALIAS}/${bucket_name}" diff -bB <(ls "$DATA_DIR") <("${MC_CMD[@]}" --json ls "${SERVER_ALIAS}/${bucket_name}/" | jq -r .key) >/dev/null 2>&1 assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "mirror and list differs" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rb --force "${SERVER_ALIAS}/${bucket_name}" log_success "$start_time" "${FUNCNAME[0]}" } ## Tests find command with --older-than set to 1day, should be empty. function test_find_empty() { show "${FUNCNAME[0]}" start_time=$(get_time) bucket_name="mc-test-bucket-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${bucket_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mirror "$DATA_DIR" "${SERVER_ALIAS}/${bucket_name}" # find --older-than 1 day should be empty, so we compare with empty string. diff -bB <(echo "") <("${MC_CMD[@]}" find "${SERVER_ALIAS}/${bucket_name}" --json --older-than 1d | jq -r .key | sed "s/${SERVER_ALIAS}\/${bucket_name}\///g") >/dev/null 2>&1 assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "mirror and list differs" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rb --force "${SERVER_ALIAS}/${bucket_name}" log_success "$start_time" "${FUNCNAME[0]}" } ## Tests find command, should list. function test_find() { show "${FUNCNAME[0]}" start_time=$(get_time) bucket_name="mc-test-bucket-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${bucket_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mirror "$DATA_DIR" "${SERVER_ALIAS}/${bucket_name}" diff -bB <(ls "$DATA_DIR") <("${MC_CMD[@]}" --json find "${SERVER_ALIAS}/${bucket_name}/" | jq -r .key | sed "s/${SERVER_ALIAS}\/${bucket_name}\///g") >/dev/null 2>&1 assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "mirror and list differs" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rb --force "${SERVER_ALIAS}/${bucket_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_watch_object() { show "${FUNCNAME[0]}" start_time=$(get_time) bucket_name="mc-test-bucket-$RANDOM" object_name="mc-test-object-$RANDOM" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${bucket_name}" # start a process to watch on bucket "${MC_CMD[@]}" --json watch "${SERVER_ALIAS}/${bucket_name}" >"$WATCH_OUT_FILE" & watch_cmd_pid=$! sleep 1 (assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${bucket_name}/${object_name}") rv=$? if [ "$rv" -ne 0 ]; then kill "$watch_cmd_pid" exit "$rv" fi sleep 1 if ! jq -r .events.type "$WATCH_OUT_FILE" | grep -qi ObjectCreated; then kill "$watch_cmd_pid" assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure 1 "ObjectCreated event not found" fi (assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${bucket_name}/${object_name}") rv=$? if [ "$rv" -ne 0 ]; then kill "$watch_cmd_pid" exit "$rv" fi sleep 1 if ! jq -r .events.type "$WATCH_OUT_FILE" | grep -qi ObjectRemoved; then kill "$watch_cmd_pid" assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure 1 "ObjectRemoved event not found" fi kill "$watch_cmd_pid" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rb --force "${SERVER_ALIAS}/${bucket_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_config_host_add() { show "${FUNCNAME[0]}" start_time=$(get_time) assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd alias set "${SERVER_ALIAS}1" "$ENDPOINT" "$ACCESS_KEY" "$SECRET_KEY" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd alias list "${SERVER_ALIAS}1" log_success "$start_time" "${FUNCNAME[0]}" } function test_config_host_add_error() { show "${FUNCNAME[0]}" start_time=$(get_time) out=$("${MC_CMD[@]}" --json alias set "${SERVER_ALIAS}1" "$ENDPOINT" "$ACCESS_KEY" "invalid-secret") assert_failure "$start_time" "${FUNCNAME[0]}" show_on_success $? "adding host should fail" got_code=$(echo "$out" | jq -r .error.cause.error.Code) if [ "${got_code}" != "SignatureDoesNotMatch" ]; then assert_failure "$start_time" "${FUNCNAME[0]}" show_on_failure 1 "incorrect error code ${got_code} returned by server" fi log_success "$start_time" "${FUNCNAME[0]}" } function test_put_object_with_sse() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" cli_flag="${SERVER_ALIAS}/${BUCKET_NAME}=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" # put encrypted object; then delete with correct secret key assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag}" "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_put_object_with_sse_error() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" cli_flag="${SERVER_ALIAS}/${BUCKET_NAME}=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MD" # put object with invalid encryption key; should fail assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag}" "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_cat_object_with_sse() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" cli_flag="${SERVER_ALIAS}/${BUCKET_NAME}=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" # put encrypted object; then cat object correct secret key assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag}" "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cat --enc-c "${cli_flag}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_cat_object_with_sse_error() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" cli_flag="${SERVER_ALIAS}/${BUCKET_NAME}=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" # put encrypted object; then cat object with no secret key assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag}" "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cat "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } # Test "mc cp -r" command of a directory with and without a leading slash function test_copy_directory() { show "${FUNCNAME[0]}" random_dir="dir-$RANDOM-$RANDOM" tmpdir="$(mktemp -d)" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}/object-name" assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to upload an object" # Copy a directory with a trailing slash assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp -r "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}/" "${tmpdir}/" assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to copy a directory with a trailing slash" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_1_MB_MD5SUM" "${tmpdir}/object-name" assert_success "$start_time" "${FUNCNAME[0]}" rm "${tmpdir}/object-name" # Copy a directory without a trailing slash assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp -r "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}" "${tmpdir}/" assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to copy a directory without a trailing slash" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_1_MB_MD5SUM" "${tmpdir}/${random_dir}/object-name" # Cleanup assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm -r --force "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}/" assert_success "$start_time" "${FUNCNAME[0]}" rm -r "${tmpdir}" log_success "$start_time" "${FUNCNAME[0]}" } # Test "mc cp -a" command to see if it preserves file system attributes function test_copy_object_preserve_filesystem_attr() { show "${FUNCNAME[0]}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp -a "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" diff -bB <("${MC_CMD[@]}" --json stat "${FILE_1_MB}" | jq -r '.metadata."X-Amz-Meta-Mc-Attrs"') <("${MC_CMD[@]}" --json stat "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" | jq -r '.metadata."X-Amz-Meta-Mc-Attrs"') >/dev/null 2>&1 >/dev/null 2>&1 assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to put object with file system attribute" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } # Test "mc mv" command function test_mv_object() { show "${FUNCNAME[0]}" random_dir="dir-$RANDOM-$RANDOM" tmpdir="$(mktemp -d)" # Test mv command locally assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "${FILE_1_MB}" "${tmpdir}/file.tmp" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mv "${tmpdir}/file.tmp" "${tmpdir}/file" assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd stat "${tmpdir}/file.tmp" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_1_MB_MD5SUM" "${tmpdir}/file" # Test mv command from filesystem to S3 assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mv "${tmpdir}/file" "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}/object-1" assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd stat "${tmpdir}/file" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd stat "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}/object-1" # Test mv command from S3 to S3 assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mv "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}/object-1" "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}/object-2" assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd stat "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}/object-1" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd stat "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}/object-2" # Test mv command from S3 to filesystem assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mv "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}/object-2" "${tmpdir}/file" assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd stat "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}/object-2" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd stat "${tmpdir}/file" # Cleanup assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm -r --force "${SERVER_ALIAS}/${BUCKET_NAME}/${random_dir}/" assert_success "$start_time" "${FUNCNAME[0]}" rm -r "${tmpdir}" log_success "$start_time" "${FUNCNAME[0]}" } function test_copy_object_with_sse_rewrite() { # test server side copy and remove operation - target is unencrypted while source is encrypted show "${FUNCNAME[0]}" start_time=$(get_time) prefix="prefix" object_name="mc-test-object-$RANDOM" cli_flag="${SERVER_ALIAS}/${BUCKET_NAME}/${prefix}=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" # create encrypted object on server assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag}" "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${prefix}/${object_name}" # now do a server side copy and store it unencrypted. assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag}" "${SERVER_ALIAS}/${BUCKET_NAME}/${prefix}/${object_name}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" # cat the unencrypted destination object. should return data without any error "${MC_CMD[@]}" cat "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" >"${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to download object using 'mc cat'" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_1_MB_MD5SUM" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${object_name}.downloaded" # mc rm on with multi-object delete, deletes encrypted object without encryption key. assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${BUCKET_NAME}/${prefix}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_copy_object_with_sse_dest() { # test server side copy and remove operation - target is encrypted with different key show "${FUNCNAME[0]}" start_time=$(get_time) prefix="prefix" object_name="mc-test-object-$RANDOM" cli_flag1="${SERVER_ALIAS}/${BUCKET_NAME}/${prefix}=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" cli_flag2="${SERVER_ALIAS}/${BUCKET_NAME}=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" # create encrypted object on server assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag1}" "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${prefix}/${object_name}" # now do a server side copy and store it eith different encryption key. assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag2}" \ "${SERVER_ALIAS}/${BUCKET_NAME}/${prefix}/${object_name}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" # cat the destination object with the new key. should return data without any error "${MC_CMD[@]}" cat --enc-c "${cli_flag2}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" >"${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to download object using 'mc cat'" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_1_MB_MD5SUM" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${object_name}.downloaded" # mc rm on src object with first encryption key should pass assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${BUCKET_NAME}/${prefix}/${object_name}" # mc rm on encrypted destination object with second encryption key should pass assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_sse_key_rotation() { # test server side copy and remove operation - target is encrypted with different key show "${FUNCNAME[0]}" start_time=$(get_time) prefix="prefix" object_name="mc-test-object-$RANDOM" old_key="MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" new_key="MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" cli_flag1="${SERVER_ALIAS}/${BUCKET_NAME}/${prefix}=${old_key}" cli_flag2="${SERVER_ALIAS_TLS}/${BUCKET_NAME}=${new_key}" # create encrypted object on server assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag1}" "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${prefix}/${object_name}" # now do a server side copy on same object and do a key rotation assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag2}" \ "${SERVER_ALIAS_TLS}/${BUCKET_NAME}/${prefix}/${object_name}" "${SERVER_ALIAS_TLS}/${BUCKET_NAME}/${object_name}" # cat the object with the new key. should return data without any error "${MC_CMD[@]}" cat --enc-c "${cli_flag2}" "${SERVER_ALIAS_TLS}/${BUCKET_NAME}/${object_name}" >"${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "unable to download object using 'mc cat'" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_1_MB_MD5SUM" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${object_name}.downloaded" # mc rm on encrypted object with succeed anyways, without encrypted keys. assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS_TLS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_mirror_with_sse() { # test if mirror operation works with encrypted objects show "${FUNCNAME[0]}" start_time=$(get_time) bucket_name="mc-test-bucket-$RANDOM" cli_flag="${SERVER_ALIAS}/${bucket_name}=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mb "${SERVER_ALIAS}/${bucket_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd mirror --enc-c "${cli_flag}" "$DATA_DIR" "${SERVER_ALIAS}/${bucket_name}" diff -bB <(ls "$DATA_DIR") <("${MC_CMD[@]}" --json ls "${SERVER_ALIAS}/${bucket_name}/" | jq -r .key) >/dev/null 2>&1 assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure $? "mirror and list differs" # Remove the test bucket with its contents assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rb --force "${SERVER_ALIAS}/${bucket_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_rm_object_with_sse() { show "${FUNCNAME[0]}" # test whether remove fails for encrypted object if secret key not provided. start_time=$(get_time) object_name="mc-test-object-$RANDOM" cli_flag="${SERVER_ALIAS}/${BUCKET_NAME}=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag}" "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" # rm will not fail even if the encryption keys are not provided, since mc rm uses multi-object delete. assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_get_object_with_sse() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" cli_flag="${SERVER_ALIAS}/${BUCKET_NAME}=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag}" "${FILE_1_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" check_md5sum "$FILE_1_MB_MD5SUM" "${object_name}.downloaded" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${object_name}.downloaded" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_put_object_multipart_sse() { show "${FUNCNAME[0]}" start_time=$(get_time) object_name="mc-test-object-$RANDOM" cli_flag="${SERVER_ALIAS}/${BUCKET_NAME}=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp --enc-c "${cli_flag}" "${FILE_65_MB}" "${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" log_success "$start_time" "${FUNCNAME[0]}" } function test_admin_users() { show "${FUNCNAME[0]}" start_time=$(get_time) # create a user username=foo password=foobar12345 test_alias="aliasx" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin user add "$SERVER_ALIAS" "$username" "$password" # check that user appears in the user list "${MC_CMD[@]}" --json admin user list "${SERVER_ALIAS}" | jq -r '.accessKey' | grep --quiet "^${username}$" rv=$? assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure ${rv} "user ${username} did NOT appear in the list of users returned by server" # setup temporary alias to make requests as the created user. scheme="https" if [ "$ENABLE_HTTPS" != "1" ]; then scheme="http" fi object1_name="mc-test-object-$RANDOM" object2_name="mc-test-object-$RANDOM" # Adding an alias for the $test_alias assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd alias set $test_alias "${scheme}://${SERVER_ENDPOINT}" ${username} ${password} # check that alias appears in the alias list "${MC_CMD[@]}" --json alias list | jq -r '.alias' | grep --quiet "^${test_alias}$" rv=$? assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure ${rv} "alias ${test_alias} did NOT appear in the list of aliases returned by server" # check that the user can write objects with readwrite policy assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin policy attach "$SERVER_ALIAS" readwrite --user="${username}" # verify that re-attaching an already attached policy to a user does not result in a failure. assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin policy attach "$SERVER_ALIAS" readwrite --user="${username}" # Validate that the correct policy has been added to the user "${MC_CMD[@]}" --json admin user list "${SERVER_ALIAS}" | jq -r '.policyName' | grep --quiet "^readwrite$" rv=$? assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure ${rv} "user ${username} did NOT have the readwrite policy attached" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "$FILE_1_MB" "${test_alias}/${BUCKET_NAME}/${object1_name}" # check that the user cannot write objects with readonly policy assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin policy detach "$SERVER_ALIAS" readwrite --user="${username}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin policy attach "$SERVER_ALIAS" readonly --user="${username}" assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cp "$FILE_1_MB" "${test_alias}/${BUCKET_NAME}/${object2_name}" # check that the user can read with readonly policy assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cat "${test_alias}/${BUCKET_NAME}/${object1_name}" # check that user can delete with readwrite policy assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin policy attach "$SERVER_ALIAS" readwrite --user="${username}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${test_alias}/${BUCKET_NAME}/${object1_name}" # check that user cannot perform admin actions with readwrite policy assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd admin info $test_alias # create object1_name for subsequent tests. assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "$FILE_1_MB" "${test_alias}/${BUCKET_NAME}/${object1_name}" # check that user can be disabled assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin user disable "$SERVER_ALIAS" "$username" # check that disabled cannot perform any action assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cat "${test_alias}/${BUCKET_NAME}/${object1_name}" # check that user can be enabled and can then perform an allowed action assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin user enable "$SERVER_ALIAS" "$username" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cat "${test_alias}/${BUCKET_NAME}/${object1_name}" # check that user can be removed, and then is no longer available assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin user remove "$SERVER_ALIAS" "$username" assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cat "${test_alias}/${BUCKET_NAME}/${object1_name}" log_success "$start_time" "${FUNCNAME[0]}" } function run_test() { test_make_bucket test_make_bucket_error test_rb setup test_list_dir test_put_object test_put_object_error test_put_object_0byte test_put_object_with_storage_class test_put_object_with_storage_class_error test_put_object_with_metadata test_put_object_multipart test_get_object test_get_object_multipart test_od_object test_mv_object test_presigned_post_policy_error test_presigned_put_object test_presigned_get_object test_cat_object test_cat_stdin test_copy_directory test_mirror_list_objects test_mirror_list_objects_storage_class test_copy_object_preserve_filesystem_attr test_find test_find_empty if [ -z "$MINT_MODE" ]; then test_watch_object fi if [ "$ENABLE_HTTPS" == "1" ]; then test_put_object_with_sse test_put_object_with_sse_error test_put_object_multipart_sse test_get_object_with_sse test_cat_object_with_sse test_cat_object_with_sse_error test_copy_object_with_sse_rewrite test_copy_object_with_sse_dest test_sse_key_rotation test_mirror_with_sse test_rm_object_with_sse fi test_config_host_add test_config_host_add_error test_admin_users teardown } function __init__() { set -e if [ -n "$DEBUG" ]; then set -x fi # For Mint, setup is already done. For others, setup the environment if [ -z "$MINT_MODE" ]; then mkdir -p "$WORK_DIR" mkdir -p "$DATA_DIR" # If mc executable binary is not available in current directory, use it in the path. if [ ! -x "$MC" ]; then if ! MC=$(which mc 2>/dev/null); then echo "'mc' executable binary not found in current directory and in path" exit 1 fi fi fi if [ ! -x "$MC" ]; then echo "$MC executable binary not found" exit 1 fi mkdir -p "$MC_CONFIG_DIR" MC_CMD=("${MC}" --config-dir "$MC_CONFIG_DIR" --quiet --no-color) if [ ! -e "$FILE_0_B" ]; then touch "$FILE_0_B" fi if [ ! -e "$FILE_1_MB" ]; then base64 -i /dev/urandom | head -c 1048576 >"$FILE_1_MB" fi if [ ! -e "$FILE_65_MB" ]; then base64 -i /dev/urandom | head -c 68157440 >"$FILE_65_MB" fi set -E set -o pipefail FILE_0_B_MD5SUM="$(get_md5sum "$FILE_0_B")" if [ $? -ne 0 ]; then echo "unable to get md5sum of $FILE_0_B" exit 1 fi FILE_1_MB_MD5SUM="$(get_md5sum "$FILE_1_MB")" if [ $? -ne 0 ]; then echo "unable to get md5sum of $FILE_1_MB" exit 1 fi FILE_65_MB_MD5SUM="$(get_md5sum "$FILE_65_MB")" if [ $? -ne 0 ]; then echo "unable to get md5sum of $FILE_65_MB" exit 1 fi assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd alias set "${SERVER_ALIAS}" "$ENDPOINT" "$ACCESS_KEY" "$SECRET_KEY" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd alias set "${SERVER_ALIAS_TLS}" "$ENDPOINT" "$ACCESS_KEY" "$SECRET_KEY" set +e } function validate_dependencies() { jqVersion=$(jq --version) if [[ $jqVersion == *"jq"* ]]; then echo "Dependency validation complete" else echo "jq is missing, please install: 'sudo apt install jq'" exit 1 fi } function main() { validate_dependencies (run_test) rv=$? rm -fr "$MC_CONFIG_DIR" "$WATCH_OUT_FILE" if [ -z "$MINT_MODE" ]; then rm -fr "$WORK_DIR" "$DATA_DIR" fi exit "$rv" } __init__ "$@" main "$@" minio-client-0.0~20250403/go.mod000066400000000000000000000122351477450377600161130ustar00rootroot00000000000000module github.com/minio/mc go 1.23.0 toolchain go1.23.6 require ( github.com/charmbracelet/bubbles v0.20.0 github.com/charmbracelet/bubbletea v1.3.4 github.com/charmbracelet/lipgloss v1.0.0 github.com/cheggaaa/pb v1.0.29 github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.18.0 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/inconshreveable/mousetrap v1.1.0 github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/juju/ratelimit v1.0.2 github.com/klauspost/compress v1.18.0 github.com/mattn/go-ieproxy v0.0.12 github.com/mattn/go-isatty v0.0.20 github.com/minio/cli v1.24.2 github.com/minio/colorjson v1.0.8 github.com/minio/filepath v1.0.0 github.com/minio/madmin-go/v3 v3.0.102 github.com/minio/minio-go/v7 v7.0.88 github.com/minio/pkg/v3 v3.1.0 github.com/minio/selfupdate v0.6.0 github.com/mitchellh/go-homedir v1.1.0 github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.16.0 github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/xattr v0.4.10 github.com/posener/complete v1.2.3 github.com/prometheus/client_golang v1.21.1 github.com/prometheus/procfs v0.15.1 github.com/rjeczalik/notify v0.9.3 github.com/rs/xid v1.6.0 github.com/shirou/gopsutil/v3 v3.24.5 github.com/tidwall/gjson v1.18.0 github.com/vbauerster/mpb/v8 v8.9.3 golang.org/x/net v0.37.0 golang.org/x/sys v0.31.0 golang.org/x/term v0.30.0 golang.org/x/text v0.23.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 ) require ( aead.dev/minisign v0.3.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/structs v1.1.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.6 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/jwx/v2 v2.1.4 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/minio/crc64nvme v1.0.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/mux v1.9.0 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/prom2json v1.4.1 // indirect github.com/prometheus/prometheus v0.302.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/safchain/ethtool v0.5.10 // indirect github.com/secure-io/sio-go v0.3.1 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tinylib/msgp v1.2.5 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.etcd.io/etcd/api/v3 v3.5.19 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.19 // indirect go.etcd.io/etcd/client/v3 v3.5.19 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/sync v0.12.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250311173030-29e43e6258d7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) minio-client-0.0~20250403/go.sum000066400000000000000000000754261477450377600161530ustar00rootroot00000000000000aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ= aead.dev/minisign v0.3.0 h1:8Xafzy5PEVZqYDNP60yJHARlW1eOQtsKNp/Ph2c0vRA= aead.dev/minisign v0.3.0/go.mod h1:NLvG3Uoq3skkRMDuc3YHpWUTMTrSExqm+Ij73W13F6Y= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= github.com/lestrrat-go/jwx/v2 v2.1.4 h1:uBCMmJX8oRZStmKuMMOFb0Yh9xmEMgNJLgjuKKt4/qc= github.com/lestrrat-go/jwx/v2 v2.1.4/go.mod h1:nWRbDFR1ALG2Z6GJbBXzfQaYyvn751KuuyySN2yR6is= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d h1:fjMbDVUGsMQiVZnSQsmouYJvMdwsGiDipOZoN66v844= github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-ieproxy v0.0.12 h1:OZkUFJC3ESNZPQ+6LzC3VJIFSnreeFLQyqvBWtvfL2M= github.com/mattn/go-ieproxy v0.0.12/go.mod h1:Vn+N61199DAnVeTgaF8eoB9PvLO8P3OBnG95ENh7B7c= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/minio/cli v1.24.2 h1:J+fCUh9mhPLjN3Lj/YhklXvxj8mnyE/D6FpFduXJ2jg= github.com/minio/cli v1.24.2/go.mod h1:bYxnK0uS629N3Bq+AOZZ+6lwF77Sodk4+UL9vNuXhOY= github.com/minio/colorjson v1.0.8 h1:AS6gEQ1dTRYHmC4xuoodPDRILHP/9Wz5wYUGDQfPLpg= github.com/minio/colorjson v1.0.8/go.mod h1:wrs39G/4kqNlGjwqHvPlAnXuc2tlPszo6JKdSBCLN8w= github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/filepath v1.0.0 h1:fvkJu1+6X+ECRA6G3+JJETj4QeAYO9sV43I79H8ubDY= github.com/minio/filepath v1.0.0/go.mod h1:/nRZA2ldl5z6jT9/KQuvZcQlxZIMQoFFQPvEXx9T/Bw= github.com/minio/madmin-go/v3 v3.0.102 h1:bqy15D6d9uQOh/3B/sLfzRtWSJgZeuKnAI5VRDhvRQw= github.com/minio/madmin-go/v3 v3.0.102/go.mod h1:pMLdj9OtN0CANNs5tdm6opvOlDFfj0WhbztboZAjRWE= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.88 h1:v8MoIJjwYxOkehp+eiLIuvXk87P2raUtoU5klrAAshs= github.com/minio/minio-go/v7 v7.0.88/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg= github.com/minio/mux v1.9.0 h1:dWafQFyEfGhJvK6AwLOt83bIG5bxKxKJnKMCi0XAaoA= github.com/minio/mux v1.9.0/go.mod h1:1pAare17ZRL5GpmNL+9YmqHoWnLmMZF9C/ioUCfy0BQ= github.com/minio/pkg/v3 v3.1.0 h1:RoR1TMXV5y4LvKPkaB1WoeuM6CO7A+I55xYb1tzrvLQ= github.com/minio/pkg/v3 v3.1.0/go.mod h1:8vhmdRv9EQnY6a+/gL9mKeIlzkhBsmhYGEmWKHW0uc4= github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU= github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/prom2json v1.4.1 h1:7McxdrHgPEOtMwWjkKtd0v5AhpR2Q6QAnlHKVxq0+tQ= github.com/prometheus/prom2json v1.4.1/go.mod h1:CzOQykSKFxXuC7ELUZHOHQvwKesQ3eN0p2PWLhFitQM= github.com/prometheus/prometheus v0.302.1 h1:xqVdrwrB4WNpdgJqxsz5loqFWNUZitsK8myqLuSZ6Ag= github.com/prometheus/prometheus v0.302.1/go.mod h1:YcyCoTbUR/TM8rY3Aoeqr0AWTu/pu1Ehh+trpX3eRzg= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/safchain/ethtool v0.5.10 h1:Im294gZtuf4pSGJRAOGKaASNi3wMeFaGaWuSaomedpc= github.com/safchain/ethtool v0.5.10/go.mod h1:w9jh2Lx7YBR4UwzLkzCmWl85UY0W2uZdd7/DckVE5+c= github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc= github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/vbauerster/mpb/v8 v8.9.3 h1:PnMeF+sMvYv9u23l6DO6Q3+Mdj408mjLRXIzmUmU2Z8= github.com/vbauerster/mpb/v8 v8.9.3/go.mod h1:hxS8Hz4C6ijnppDSIX6LjG8FYJSoPo9iIOcE53Zik0c= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/etcd/api/v3 v3.5.19 h1:w3L6sQZGsWPuBxRQ4m6pPP3bVUtV8rjW033EGwlr0jw= go.etcd.io/etcd/api/v3 v3.5.19/go.mod h1:QqKGViq4KTgOG43dr/uH0vmGWIaoJY3ggFi6ZH0TH/U= go.etcd.io/etcd/client/pkg/v3 v3.5.19 h1:9VsyGhg0WQGjDWWlDI4VuaS9PZJGNbPkaHEIuLwtixk= go.etcd.io/etcd/client/pkg/v3 v3.5.19/go.mod h1:qaOi1k4ZA9lVLejXNvyPABrVEe7VymMF2433yyRQ7O0= go.etcd.io/etcd/client/v3 v3.5.19 h1:+4byIz6ti3QC28W0zB0cEZWwhpVHXdrKovyycJh1KNo= go.etcd.io/etcd/client/v3 v3.5.19/go.mod h1:FNzyinmMIl0oVsty1zA3hFeUrxXI/JpEnz4sG+POzjU= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250311173030-29e43e6258d7 h1:WpkiKdu5iuBhOW8QJl1NwI4O5DLQ2u6d/VlhsR/TbHc= google.golang.org/genproto/googleapis/api v0.0.0-20250311173030-29e43e6258d7/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= minio-client-0.0~20250403/main.go000066400000000000000000000017131477450377600162570ustar00rootroot00000000000000// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package main // import "github.com/minio/mc" import ( "os" mc "github.com/minio/mc/cmd" "github.com/minio/pkg/v3/console" ) func main() { if e := mc.Main(os.Args); e != nil { console.Fatalln(e) } } minio-client-0.0~20250403/pkg/000077500000000000000000000000001477450377600155635ustar00rootroot00000000000000minio-client-0.0~20250403/pkg/deadlineconn/000077500000000000000000000000001477450377600202065ustar00rootroot00000000000000minio-client-0.0~20250403/pkg/deadlineconn/deadlineconn.go000066400000000000000000000044351477450377600231660ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package deadlineconn implements net.Conn wrapper with configured deadlines. package deadlineconn import ( "net" "time" ) // DeadlineConn - is a generic stream-oriented network connection supporting buffered reader and read/write timeout. type DeadlineConn struct { net.Conn readDeadline time.Duration // sets the read deadline on a connection. writeDeadline time.Duration // sets the write deadline on a connection. } // Sets read deadline func (c *DeadlineConn) setReadDeadline() { if c.readDeadline > 0 { c.SetReadDeadline(time.Now().UTC().Add(c.readDeadline)) } } func (c *DeadlineConn) setWriteDeadline() { if c.writeDeadline > 0 { c.SetWriteDeadline(time.Now().UTC().Add(c.writeDeadline)) } } // Read - reads data from the connection using wrapped buffered reader. func (c *DeadlineConn) Read(b []byte) (n int, err error) { c.setReadDeadline() n, err = c.Conn.Read(b) return n, err } // Write - writes data to the connection. func (c *DeadlineConn) Write(b []byte) (n int, err error) { c.setWriteDeadline() n, err = c.Conn.Write(b) return n, err } // WithReadDeadline sets a new read side net.Conn deadline. func (c *DeadlineConn) WithReadDeadline(d time.Duration) *DeadlineConn { c.readDeadline = d return c } // WithWriteDeadline sets a new write side net.Conn deadline. func (c *DeadlineConn) WithWriteDeadline(d time.Duration) *DeadlineConn { c.writeDeadline = d return c } // New - creates a new connection object wrapping net.Conn with deadlines. func New(c net.Conn) *DeadlineConn { return &DeadlineConn{ Conn: c, } } minio-client-0.0~20250403/pkg/deadlineconn/deadlineconn_test.go000066400000000000000000000057421477450377600242270ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package deadlineconn import ( "bufio" "io" "net" "sync" "testing" "time" ) // Test deadlineconn handles read timeout properly by reading two messages beyond deadline. func TestBuffConnReadTimeout(t *testing.T) { l, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("unable to create listener. %v", err) } defer l.Close() serverAddr := l.Addr().String() tcpListener, ok := l.(*net.TCPListener) if !ok { t.Fatalf("failed to assert to net.TCPListener") } var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() tcpConn, terr := tcpListener.AcceptTCP() if terr != nil { t.Errorf("failed to accept new connection. %v", terr) return } deadlineconn := New(tcpConn) deadlineconn.WithReadDeadline(time.Second) deadlineconn.WithWriteDeadline(time.Second) defer deadlineconn.Close() // Read a line b := make([]byte, 12) _, terr = deadlineconn.Read(b) if terr != nil { t.Errorf("failed to read from client. %v", terr) return } received := string(b) if received != "message one\n" { t.Errorf(`server: expected: "message one\n", got: %v`, received) return } // Wait for more than read timeout to simulate processing. time.Sleep(3 * time.Second) _, terr = deadlineconn.Read(b) if terr != nil { t.Errorf("failed to read from client. %v", terr) return } received = string(b) if received != "message two\n" { t.Errorf(`server: expected: "message two\n", got: %v`, received) return } // Send a response. _, terr = io.WriteString(deadlineconn, "messages received\n") if terr != nil { t.Errorf("failed to write to client. %v", terr) return } }() c, err := net.Dial("tcp", serverAddr) if err != nil { t.Fatalf("unable to connect to server. %v", err) } defer c.Close() _, err = io.WriteString(c, "message one\n") if err != nil { t.Fatalf("failed to write to server. %v", err) } _, err = io.WriteString(c, "message two\n") if err != nil { t.Fatalf("failed to write to server. %v", err) } received, err := bufio.NewReader(c).ReadString('\n') if err != nil { t.Fatalf("failed to read from server. %v", err) } if received != "messages received\n" { t.Fatalf(`client: expected: "messages received\n", got: %v`, received) } wg.Wait() } minio-client-0.0~20250403/pkg/disk/000077500000000000000000000000001477450377600165155ustar00rootroot00000000000000minio-client-0.0~20250403/pkg/disk/stat_darwin.go000066400000000000000000000040171477450377600213650ustar00rootroot00000000000000//go:build darwin // +build darwin // Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package disk import ( "os/user" "strconv" "strings" "syscall" ) // GetFileSystemAttrs return the file system attribute as string; containing mode, // uid, gid, uname, Gname, atime, mtime, ctime and md5 func GetFileSystemAttrs(file string) (string, error) { st := syscall.Stat_t{} err := syscall.Stat(file, &st) if err != nil { return "", err } var fileAttr strings.Builder fileAttr.WriteString("atime:") fileAttr.WriteString(strconv.FormatInt(st.Atimespec.Sec, 10) + "#" + strconv.FormatInt(st.Atimespec.Nsec, 10)) fileAttr.WriteString("/gid:") fileAttr.WriteString(strconv.Itoa(int(st.Gid))) g, err := user.LookupGroupId(strconv.FormatUint(uint64(st.Gid), 10)) if err == nil { fileAttr.WriteString("/gname:") fileAttr.WriteString(g.Name) } fileAttr.WriteString("/mode:") fileAttr.WriteString(strconv.Itoa(int(st.Mode))) fileAttr.WriteString("/mtime:") fileAttr.WriteString(strconv.FormatInt(st.Mtimespec.Sec, 10) + "#" + strconv.FormatInt(st.Mtimespec.Nsec, 10)) fileAttr.WriteString("/uid:") fileAttr.WriteString(strconv.Itoa(int(st.Uid))) u, err := user.LookupId(strconv.FormatUint(uint64(st.Uid), 10)) if err == nil { fileAttr.WriteString("/uname:") fileAttr.WriteString(u.Username) } return fileAttr.String(), nil } minio-client-0.0~20250403/pkg/disk/stat_freebsd.go000066400000000000000000000042271477450377600215160ustar00rootroot00000000000000// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package disk import ( "os/user" "strconv" "strings" "syscall" ) // GetFileSystemAttrs return the file system attribute as string; containing mode, // uid, gid, uname, Gname, atime, mtime, ctime and md5 func GetFileSystemAttrs(file string) (string, error) { st := syscall.Stat_t{} err := syscall.Stat(file, &st) if err != nil { return "", err } var fileAttr strings.Builder fileAttr.WriteString("atime:") // NOTE: Keep int64 casts. See https://github.com/minio/mc/issues/4005 fileAttr.WriteString(strconv.FormatInt(int64(st.Atimespec.Sec), 10) + "#" + strconv.FormatInt(int64(st.Atimespec.Nsec), 10)) fileAttr.WriteString("/gid:") fileAttr.WriteString(strconv.Itoa(int(st.Gid))) g, err := user.LookupGroupId(strconv.FormatUint(uint64(st.Gid), 10)) if err == nil { fileAttr.WriteString("/gname:") fileAttr.WriteString(g.Name) } fileAttr.WriteString("/mode:") fileAttr.WriteString(strconv.Itoa(int(st.Mode))) fileAttr.WriteString("/mtime:") // NOTE: Keep int64 casts. See https://github.com/minio/mc/issues/4005 fileAttr.WriteString(strconv.FormatInt(int64(st.Mtimespec.Sec), 10) + "#" + strconv.FormatInt(int64(st.Mtimespec.Nsec), 10)) fileAttr.WriteString("/uid:") fileAttr.WriteString(strconv.Itoa(int(st.Uid))) u, err := user.LookupId(strconv.FormatUint(uint64(st.Uid), 10)) if err == nil { fileAttr.WriteString("/uname:") fileAttr.WriteString(u.Username) } return fileAttr.String(), nil } minio-client-0.0~20250403/pkg/disk/stat_linux.go000066400000000000000000000041361477450377600212420ustar00rootroot00000000000000//go:build linux // +build linux // Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package disk fetches file system information for various OS package disk import ( "os" "os/user" "strconv" "strings" "syscall" ) // GetFileSystemAttrs return the file system attribute as string; containing mode, // uid, gid, uname, Gname, atime, mtime, ctime and md5 func GetFileSystemAttrs(file string) (string, error) { fi, err := os.Stat(file) if err != nil { return "", err } st := fi.Sys().(*syscall.Stat_t) var fileAttr strings.Builder fileAttr.WriteString("atime:") fileAttr.WriteString(strconv.FormatInt(int64(st.Atim.Sec), 10) + "#" + strconv.FormatInt(int64(st.Atim.Nsec), 10)) fileAttr.WriteString("/gid:") fileAttr.WriteString(strconv.Itoa(int(st.Gid))) g, err := user.LookupGroupId(strconv.FormatUint(uint64(st.Gid), 10)) if err == nil { fileAttr.WriteString("/gname:") fileAttr.WriteString(g.Name) } fileAttr.WriteString("/mode:") fileAttr.WriteString(strconv.Itoa(int(st.Mode))) fileAttr.WriteString("/mtime:") fileAttr.WriteString(strconv.FormatInt(int64(st.Mtim.Sec), 10) + "#" + strconv.FormatInt(int64(st.Mtim.Nsec), 10)) fileAttr.WriteString("/uid:") fileAttr.WriteString(strconv.Itoa(int(st.Uid))) u, err := user.LookupId(strconv.FormatUint(uint64(st.Uid), 10)) if err == nil { fileAttr.WriteString("/uname:") fileAttr.WriteString(u.Username) } return fileAttr.String(), nil } minio-client-0.0~20250403/pkg/disk/stat_netbsd.go000066400000000000000000000037531477450377600213660ustar00rootroot00000000000000// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package disk import ( "os/user" "strconv" "strings" "syscall" ) // GetFileSystemAttrs return the file system attribute as string; containing mode, // uid, gid, uname, Gname, atime, mtime, ctime and md5 func GetFileSystemAttrs(file string) (string, error) { st := syscall.Stat_t{} err := syscall.Stat(file, &st) if err != nil { return "", err } var fileAttr strings.Builder fileAttr.WriteString("atime:") fileAttr.WriteString(strconv.FormatInt(st.Atimespec.Sec, 10) + "#" + strconv.FormatInt(st.Atimespec.Nsec, 10)) fileAttr.WriteString("/gid:") fileAttr.WriteString(strconv.Itoa(int(st.Gid))) g, err := user.LookupGroupId(strconv.FormatUint(uint64(st.Gid), 10)) if err == nil { fileAttr.WriteString("/gname:") fileAttr.WriteString(g.Name) } fileAttr.WriteString("/mode:") fileAttr.WriteString(strconv.Itoa(int(st.Mode))) fileAttr.WriteString("/mtime:") fileAttr.WriteString(strconv.FormatInt(st.Mtimespec.Sec, 10) + "#" + strconv.FormatInt(st.Mtimespec.Nsec, 10)) fileAttr.WriteString("/uid:") fileAttr.WriteString(strconv.Itoa(int(st.Uid))) u, err := user.LookupId(strconv.FormatUint(uint64(st.Uid), 10)) if err == nil { fileAttr.WriteString("/uname:") fileAttr.WriteString(u.Username) } return fileAttr.String(), nil } minio-client-0.0~20250403/pkg/disk/stat_other.go000066400000000000000000000040541477450377600212230ustar00rootroot00000000000000//go:build openbsd || solaris // +build openbsd solaris // Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package disk import ( "os/user" "strconv" "strings" "syscall" ) // GetFileSystemAttrs return the file system attribute as string; containing mode, // uid, gid, uname, Gname, atime, mtime, ctime and md5 func GetFileSystemAttrs(file string) (string, error) { st := syscall.Stat_t{} err := syscall.Stat(file, &st) if err != nil { return "", err } var fileAttr strings.Builder fileAttr.WriteString("atime:") fileAttr.WriteString(strconv.FormatInt(int64(st.Atim.Sec), 10) + "#" + strconv.FormatInt(int64(st.Atim.Nsec), 10)) fileAttr.WriteString("/gid:") fileAttr.WriteString(strconv.Itoa(int(st.Gid))) g, err := user.LookupGroupId(strconv.FormatUint(uint64(st.Gid), 10)) if err == nil { fileAttr.WriteString("/gname:") fileAttr.WriteString(g.Name) } fileAttr.WriteString("/mode:") fileAttr.WriteString(strconv.Itoa(int(st.Mode))) fileAttr.WriteString("/mtime:") fileAttr.WriteString(strconv.FormatInt(int64(st.Mtim.Sec), 10) + "#" + strconv.FormatInt(int64(st.Mtim.Nsec), 10)) fileAttr.WriteString("/uid:") fileAttr.WriteString(strconv.Itoa(int(st.Uid))) u, err := user.LookupId(strconv.FormatUint(uint64(st.Uid), 10)) if err == nil { fileAttr.WriteString("/uname:") fileAttr.WriteString(u.Username) } return fileAttr.String(), nil } minio-client-0.0~20250403/pkg/disk/stat_windows.go000066400000000000000000000017771477450377600216050ustar00rootroot00000000000000//go:build windows // +build windows // Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package disk // GetFileSystemAttrs return the file system attribute as string; containing mode, // uid, gid, uname, Gname, atime, mtime, ctime and md5 func GetFileSystemAttrs(_ string) (string, error) { return "", nil } minio-client-0.0~20250403/pkg/hookreader/000077500000000000000000000000001477450377600177065ustar00rootroot00000000000000minio-client-0.0~20250403/pkg/hookreader/hookreader.go000066400000000000000000000050171477450377600223630ustar00rootroot00000000000000// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package hookreader hooks additional reader in the source // stream. It is useful for making progress bars. Second reader is // appropriately notified about the exact number of bytes read from // the primary source on each Read operation. package hookreader import "io" // hookReader hooks additional reader in the source stream. It is // useful for making progress bars. Second reader is appropriately // notified about the exact number of bytes read from the primary // source on each Read operation. type hookReader struct { source io.Reader hook io.Reader } // Seek implements io.Seeker. Seeks source first, and if necessary // seeks hook if Seek method is appropriately found. func (hr *hookReader) Seek(offset int64, whence int) (n int64, err error) { // Verify for source has embedded Seeker, use it. sourceSeeker, ok := hr.source.(io.Seeker) if ok { return sourceSeeker.Seek(offset, whence) } // Verify if hook has embedded Seeker, use it. hookSeeker, ok := hr.hook.(io.Seeker) if ok { return hookSeeker.Seek(offset, whence) } return n, nil } // Read implements io.Reader. Always reads from the source, the return // value 'n' number of bytes are reported through the hook. Returns // error for all non io.EOF conditions. func (hr *hookReader) Read(b []byte) (n int, err error) { n, err = hr.source.Read(b) if err != nil && err != io.EOF { return n, err } // Progress the hook with the total read bytes from the source. if _, herr := hr.hook.Read(b[:n]); herr != nil { if herr != io.EOF { return n, herr } } return n, err } // NewHook returns a io.Reader which implements hookReader that // reports the data read from the source to the hook. func NewHook(source, hook io.Reader) io.Reader { if hook == nil { return source } return &hookReader{source, hook} } minio-client-0.0~20250403/pkg/hookreader/hookreader_test.go000066400000000000000000000030401477450377600234140ustar00rootroot00000000000000// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package hookreader import ( "bytes" "testing" check "gopkg.in/check.v1" ) func Test(t *testing.T) { check.TestingT(t) } type MySuite struct{} var _ = check.Suite(&MySuite{}) // customReader - implements custom progress reader. type customReader struct { readBytes int } func (c *customReader) Read(b []byte) (n int, err error) { c.readBytes += len(b) return len(b), nil } // Tests hook reader implementation. func (s *MySuite) TestHookReader(c *check.C) { var buffer bytes.Buffer writer := &buffer _, err := writer.Write([]byte("Hello")) c.Assert(err, check.IsNil) progress := &customReader{} reader := NewHook(&buffer, progress) b := make([]byte, 3) n, err := reader.Read(b) c.Assert(err, check.IsNil) c.Assert(n, check.Equals, 3) c.Assert(progress.readBytes, check.Equals, 3) } minio-client-0.0~20250403/pkg/httptracer/000077500000000000000000000000001477450377600177435ustar00rootroot00000000000000minio-client-0.0~20250403/pkg/httptracer/httptracer.go000066400000000000000000000045201477450377600224530ustar00rootroot00000000000000// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package httptracer implements http tracing functionality package httptracer import ( "errors" "net/http" "time" "github.com/minio/pkg/v3/console" ) // HTTPTracer provides callback hook mechanism for HTTP transport. type HTTPTracer interface { Request(req *http.Request) error Response(res *http.Response) error } // RoundTripTrace interposes HTTP transport requests and respsonses using HTTPTracer hooks type RoundTripTrace struct { Trace HTTPTracer // User provides callback methods Transport http.RoundTripper // HTTP transport that needs to be intercepted } // RoundTrip executes user provided request and response hooks for each HTTP call. func (t RoundTripTrace) RoundTrip(req *http.Request) (res *http.Response, err error) { timeStamp := time.Now() if t.Transport == nil { return nil, errors.New("Invalid Argument") } res, err = t.Transport.RoundTrip(req) if err != nil { return res, err } if t.Trace != nil { err = t.Trace.Request(req) if err != nil { return nil, err } err = t.Trace.Response(res) if err != nil { return nil, err } console.Debugln("Response Time: ", time.Since(timeStamp).String()+"\n") } return res, err } // GetNewTraceTransport returns a traceable transport // // Takes first argument a custom tracer which implements Response, Request() conforming to HTTPTracer interface. // Another argument can be a default transport or a custom http.RoundTripper implementation func GetNewTraceTransport(trace HTTPTracer, transport http.RoundTripper) RoundTripTrace { return RoundTripTrace{ Trace: trace, Transport: transport, } } minio-client-0.0~20250403/pkg/httptracer/httptracer_test.go000066400000000000000000000017321477450377600235140ustar00rootroot00000000000000// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package httptracer import ( "testing" check "gopkg.in/check.v1" ) func Test(t *testing.T) { check.TestingT(t) } type MySuite struct{} var _ = check.Suite(&MySuite{}) func (s *MySuite) TestHTTPTracer(_ *check.C) { } minio-client-0.0~20250403/pkg/limiter/000077500000000000000000000000001477450377600172305ustar00rootroot00000000000000minio-client-0.0~20250403/pkg/limiter/limiter.go000066400000000000000000000046231477450377600212310ustar00rootroot00000000000000// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package limiter implements throughput upload and download limits via http.RoundTripper package limiter import ( "errors" "io" "net/http" "github.com/juju/ratelimit" ) type limiter struct { upload *ratelimit.Bucket download *ratelimit.Bucket transport http.RoundTripper // HTTP transport that needs to be intercepted } func (l limiter) limitReader(r io.Reader, b *ratelimit.Bucket) io.Reader { if b == nil { return r } return ratelimit.Reader(r, b) } // RoundTrip executes user provided request and response hooks for each HTTP call. func (l limiter) RoundTrip(req *http.Request) (res *http.Response, err error) { if l.transport == nil { return nil, errors.New("Invalid Argument") } type readCloser struct { io.Reader io.Closer } if req.Body != nil { req.Body = &readCloser{ Reader: l.limitReader(req.Body, l.upload), Closer: req.Body, } } res, err = l.transport.RoundTrip(req) if res != nil && res.Body != nil { res.Body = &readCloser{ Reader: l.limitReader(res.Body, l.download), Closer: res.Body, } } return res, err } // New return a ratelimited transport func New(uploadLimit, downloadLimit int64, transport http.RoundTripper) http.RoundTripper { if uploadLimit == 0 && downloadLimit == 0 { return transport } var ( uploadBucket *ratelimit.Bucket downloadBucket *ratelimit.Bucket ) if uploadLimit > 0 { uploadBucket = ratelimit.NewBucketWithRate(float64(uploadLimit), uploadLimit) } if downloadLimit > 0 { downloadBucket = ratelimit.NewBucketWithRate(float64(downloadLimit), downloadLimit) } return &limiter{ upload: uploadBucket, download: downloadBucket, transport: transport, } } minio-client-0.0~20250403/pkg/probe/000077500000000000000000000000001477450377600166725ustar00rootroot00000000000000minio-client-0.0~20250403/pkg/probe/probe.go000066400000000000000000000144131477450377600203330ustar00rootroot00000000000000// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package probe implements a simple mechanism to trace and return errors in large programs. package probe import ( "fmt" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "github.com/dustin/go-humanize" ) var ( // Root path to the project's source. rootPath string // App specific info to be included reporting. appInfo map[string]string ) // Init initializes probe. It is typically called once from the main() // function or at least from any source file placed at the top level // source directory. func Init() { // Root path is automatically determined from the calling function's source file location. // Catch the calling function's source file path. _, file, _, _ := runtime.Caller(1) // Save the directory alone. rootPath = filepath.Dir(file) appInfo = make(map[string]string) } // SetAppInfo sets app speific key:value to report additionally during call trace dump. // Eg. SetAppInfo("ReleaseTag", "RELEASE_42_0") // // SetAppInfo("Version", "42.0") // SetAppInfo("Commit", "00611fb") func SetAppInfo(key, value string) { appInfo[key] = value } // GetSysInfo returns useful system statistics. func GetSysInfo() map[string]string { host, err := os.Hostname() if err != nil { host = "" } memstats := &runtime.MemStats{} runtime.ReadMemStats(memstats) return map[string]string{ "host.name": host, "host.os": runtime.GOOS, "host.arch": runtime.GOARCH, "host.lang": runtime.Version(), "host.cpus": strconv.Itoa(runtime.NumCPU()), "mem.used": humanize.IBytes(memstats.Alloc), "mem.total": humanize.IBytes(memstats.Sys), "mem.heap.used": humanize.IBytes(memstats.HeapAlloc), "mem.heap.total": humanize.IBytes(memstats.HeapSys), } } // TracePoint container for individual trace entries in overall call trace type TracePoint struct { Line int `json:"line,omitempty"` Filename string `json:"file,omitempty"` Function string `json:"func,omitempty"` Env map[string][]string `json:"env,omitempty"` } // Error implements tracing error functionality. type Error struct { lock sync.RWMutex Cause error `json:"cause,omitempty"` CallTrace []TracePoint `json:"trace,omitempty"` SysInfo map[string]string `json:"sysinfo,omitempty"` } // NewError function instantiates an error probe for tracing. // Default “error“ (golang's error interface) is injected in // only once. Rest of the time, you trace the return path with // “probe.Trace“ and finally handling them at top level // // Following dummy code talks about how one can pass up the // errors and put them in CallTrace. // // func sendError() *probe.Error { // return probe.NewError(errors.New("Help Needed")) // } // func recvError() *probe.Error { // return sendError().Trace() // } // if err := recvError(); err != nil { // log.Fatalln(err.Trace()) // } func NewError(e error) *Error { if e == nil { return nil } Err := Error{lock: sync.RWMutex{}, Cause: e, CallTrace: []TracePoint{}, SysInfo: GetSysInfo()} return Err.trace() // Skip NewError and only instead register the NewError's caller. } // Trace records the point at which it is invoked. // Stack traces are important for debugging purposes. func (e *Error) Trace(fields ...string) *Error { if e == nil { return nil } e.lock.Lock() defer e.lock.Unlock() return e.trace(fields...) } // trace records caller's caller. It is intended for probe's own // internal use. Take a look at probe.NewError for example. func (e *Error) trace(fields ...string) *Error { if e == nil { return nil } pc, file, line, _ := runtime.Caller(2) function := runtime.FuncForPC(pc).Name() _, function = filepath.Split(function) file = strings.TrimPrefix(file, rootPath+string(os.PathSeparator)) // trims project's root path. var tp TracePoint if len(fields) > 0 { tp = TracePoint{Line: line, Filename: file, Function: function, Env: map[string][]string{"Tags": fields}} } else { tp = TracePoint{Line: line, Filename: file, Function: function} } e.CallTrace = append(e.CallTrace, tp) return e } // Untrace erases last known trace entry. func (e *Error) Untrace() *Error { if e == nil { return nil } e.lock.Lock() defer e.lock.Unlock() l := len(e.CallTrace) if l == 0 { return nil } e.CallTrace = e.CallTrace[:l-1] return e } // ToGoError returns original error message. func (e *Error) ToGoError() error { if e == nil || e.Cause == nil { return nil } return e.Cause } // String returns error message. func (e *Error) String() string { if e == nil || e.Cause == nil { return "" } e.lock.RLock() defer e.lock.RUnlock() if e.Cause != nil { str := e.Cause.Error() callLen := len(e.CallTrace) for i := callLen - 1; i >= 0; i-- { if len(e.CallTrace[i].Env) > 0 { str += fmt.Sprintf("\n (%d) %s:%d %s(..) Tags: [%s]", i, e.CallTrace[i].Filename, e.CallTrace[i].Line, e.CallTrace[i].Function, strings.Join(e.CallTrace[i].Env["Tags"], ", ")) } else { str += fmt.Sprintf("\n (%d) %s:%d %s(..)", i, e.CallTrace[i].Filename, e.CallTrace[i].Line, e.CallTrace[i].Function) } } str += "\n " for key, value := range appInfo { str += key + ":" + value + " | " } str += "Host:" + e.SysInfo["host.name"] + " | " str += "OS:" + e.SysInfo["host.os"] + " | " str += "Arch:" + e.SysInfo["host.arch"] + " | " str += "Lang:" + e.SysInfo["host.lang"] + " | " str += "Mem:" + e.SysInfo["mem.used"] + "/" + e.SysInfo["mem.total"] + " | " str += "Heap:" + e.SysInfo["mem.heap.used"] + "/" + e.SysInfo["mem.heap.total"] return str } return "" } minio-client-0.0~20250403/pkg/probe/probe_test.go000066400000000000000000000035551477450377600213770ustar00rootroot00000000000000// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package probe_test import ( "os" "testing" "github.com/minio/mc/pkg/probe" check "gopkg.in/check.v1" ) func Test(t *testing.T) { check.TestingT(t) } type MySuite struct{} var _ = check.Suite(&MySuite{}) func testDummy0() *probe.Error { _, e := os.Stat("this-file-cannot-exit") return probe.NewError(e) } func testDummy1() *probe.Error { return testDummy0().Trace("DummyTag1") } func testDummy2() *probe.Error { return testDummy1().Trace("DummyTag2") } func (s *MySuite) TestProbe(c *check.C) { probe.Init() // Set project's root source path. probe.SetAppInfo("Commit-ID", "7390cc957239") es := testDummy2().Trace("TopOfStack") // Uncomment the following Println to visually test probe call trace. // fmt.Println("Expecting a simulated error here.", es) c.Assert(es, check.Not(check.Equals), nil) newES := es.Trace() c.Assert(newES, check.Not(check.Equals), nil) } func (s *MySuite) TestWrappedError(c *check.C) { _, e := os.Stat("this-file-cannot-exit") es := probe.NewError(e) // *probe.Error e = probe.WrapError(es) // *probe.WrappedError _, ok := probe.UnwrapError(e) c.Assert(ok, check.Equals, true) } minio-client-0.0~20250403/pkg/probe/wrapper.go000066400000000000000000000026741477450377600207120ustar00rootroot00000000000000// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package probe implements a simple mechanism to trace and return errors in large programs. package probe // wrappedError implements a container for *probe.Error. type wrappedError struct { err *Error } // WrapError function wraps a *probe.Error into a 'error' compatible duck type. func WrapError(err *Error) error { return &wrappedError{err: err} } // UnwrapError tries to convert generic 'error' into typed *probe.Error and returns true, false otherwise. func UnwrapError(err error) (*Error, bool) { switch e := err.(type) { case *wrappedError: return e.err, true default: return nil, false } } // Error interface method. func (w *wrappedError) Error() string { return w.err.String() } minio-client-0.0~20250403/staticcheck.conf000066400000000000000000000000331477450377600201320ustar00rootroot00000000000000checks = ["all", "-S1016"] minio-client-0.0~20250403/testdata/000077500000000000000000000000001477450377600166135ustar00rootroot00000000000000minio-client-0.0~20250403/testdata/localhost.crt000066400000000000000000000024521477450377600213200ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDojCCAoqgAwIBAgIUHE3HUt7gKVBp7wT9Hjj4wRT1xywwDQYJKoZIhvcNAQEL BQAwejELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRgwFgYDVQQHDA9jdXN0b20t bG9jYXRpb24xHDAaBgNVBAoME2N1c3RvbS1vcmdhbml6YXRpb24xEjAQBgNVBAsM CWN1c3RvbS1vdTESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIxMDkyMDEyMTAyOFoY DzIxMjEwODI3MTIxMDI4WjB6MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExGDAW BgNVBAcMD2N1c3RvbS1sb2NhdGlvbjEcMBoGA1UECgwTY3VzdG9tLW9yZ2FuaXph dGlvbjESMBAGA1UECwwJY3VzdG9tLW91MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6mSJV5oz171+P/G1X07Y4HpTS ZR/TC5lHAYh2YxecOvuuQHBYCgjXbSBmRQXuPRZIy8mXAa6miwssGgSlMKaH6ZV/ s82ViCSf4Tj1W9on1DvD8bNgCVs0aYA2DmbOyeSyQyyH35TLIaZ2C9fURArixDce rsUjLMv/GbrL2o2Ia+x4vBSEqUEXlfs7nA1jbm0fYoKlcxjnvDmkIYBCdMqNveHD DLXzCuXKq4aMLlM+1s8h5ShVAhxpCzY3xncfOvAGA1JWVjEsMUd8PQ4+K88JKVIG H3HZmL0exjNF4Ks7ocINqQJk7RY0ZUJc10JnCgdkY5bnHuGKzj4Ddq3Ta5FrAgMB AAGjHjAcMBoGA1UdEQQTMBGHBH8AAAGCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsF AAOCAQEAREoEGX34C0+osOgCPwZ4mFXVvssJRJTc4JL/kqOGPyzS/085MzeIpOhu gkOm3zj175SKiZMWWpYhB+Q/1T6tDiVLidEY7HK/dcbjYfoTAd42cQWY4qmG68vG E6WnTQjal0uPuCwKqvqIRgkWLpaIV4TmHtpCFLLlVlnDWQBpAdiK/Vk0EeTDDaqd cROr4tm/8EBxqwUnIQF8vgbXgVT5BNdcp44LWs7A558CCRrCGifch5+kxkSSdAFG Y7BEXvnnsxU1n9AAVKJYnp1wUN2Hk4KWyPcQb9/Ee45DKDR5KNyMqClsWWXMJr40 FrgI6euT50Wzo8Qqbls5Bt/0IzObnA== -----END CERTIFICATE----- minio-client-0.0~20250403/testdata/localhost.key000066400000000000000000000032171477450377600213200ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAupkiVeaM9e9fj/xtV9O2OB6U0mUf0wuZRwGIdmMXnDr7rkBw WAoI120gZkUF7j0WSMvJlwGuposLLBoEpTCmh+mVf7PNlYgkn+E49VvaJ9Q7w/Gz YAlbNGmANg5mzsnkskMsh9+UyyGmdgvX1EQK4sQ3Hq7FIyzL/xm6y9qNiGvseLwU hKlBF5X7O5wNY25tH2KCpXMY57w5pCGAQnTKjb3hwwy18wrlyquGjC5TPtbPIeUo VQIcaQs2N8Z3HzrwBgNSVlYxLDFHfD0OPivPCSlSBh9x2Zi9HsYzReCrO6HCDakC ZO0WNGVCXNdCZwoHZGOW5x7his4+A3at02uRawIDAQABAoIBAElz2mZCGR7+mXmO fmRiPIqezyp7ECn9mNqwqc0geLzRIx2W1CJz4MMce/KGHS2I8mq5faNp0BxTA5Ta sRVtr0A1HNpmJvlD3FbrS4aaH6gqDVS2okudoz9ggE3HIYUpSFM7yh26T1Ie7u3s /4rZNgfKAYCcf5G3Ip5KvJNedvRKC2w5UX4iYU5eMuXs0YPy2+DtTKxZKIEnI3x4 VpMe652I7GhjkLSsyuW5NoDeSWmmqhitm1bibuZQBR0ogFyAU94rZk8+j4hiUl9k 7ZVi00tKNp7EcLIwbY02ZCpZ2xvat8W1SLeWRUpExPj/7nnA9Fafrl5M8aHgl8R1 N0Fd+zECgYEA4GruRDPPNPEXPByes6jjcxfvdoYIDyma1V+FN5Nn+/QPL/vWp9t0 +mH344iwWkEyZZHNUpNuy5dpOA9724iFFLp9Btq06OgzzDuJj1LDbDpB4zwltuQA 4s7ekTFi9rHDOeAyTREHgDu3uya/2qq53Ms6w8gXM4/kmp9/S4N5VhcCgYEA1Nuv J1khBitNiNbzHYG9DRV4HLfIB8FFDKtgnW8HvSaWUtHKI4YitwJVFfa5Epz9MDFy tkFAD9Bu0HqZQ6OQZxGo0rDUcFics9Ftw+w3p/PIocPQBK7PNQ3L9BQS9M3stL+k fW5r+kUvfZaJVE8agCQQnzkJJh9oexJjMWyxB80CgYBa5BQSPWWLjKWba//+xcUx BR2wREKZWYFjL+e1hZcU3VkVVwsuOtza17jdR6wdMdCmgHHHIv05qd4snWDNnjJA HfOrRgMFXZ409lwVVzDc8Y9j6CViOF//fEd6SKVLQt3N3/afbek6z3TvcJc9ie3y 9cCcMLrs4Dd3RGf6/omzCwKBgQChwWxCf6Xr9T5PjeFke/I5niYP1M2KryGU9itO mFCOOmOj/k8ZXdbFsl0Meti7v1dcp0cgH0fafK+peHE+CG81FCNyMPTPh1dWAwHi EIFe/ZBq9c3/sQQ/sgNasWKSbGbEGJqcwywFHUxwqNQloJNn64BCL2q3cMjKNffx WELTxQKBgQDe7adrUBtk8+YZ1/c2VsV1oBq6eI2U+1bWa/8fwXWd9ylETBMtcv20 Fs8UAFWLz78aWaXWWSSEmFvUbjXPBDvzXCObb6HdhXOCF1n74hKU+z06MAUa1eFC V0GvBx4v1rc1pZ7grBZwGteeVWVgCAZ487m5TATWqxuarH6yfU0Ptg== -----END RSA PRIVATE KEY-----