├── .github ├── .typo-ci.yml ├── FUNDING.yml ├── dependabot.yml ├── pre-commit ├── release-drafter.yml ├── settings.yml └── workflows │ ├── backup.yaml │ ├── coverage-report.yaml │ ├── pull-request.yaml │ ├── release-drafter.yml │ └── release.yaml ├── .gitignore ├── .gitpod.yml ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── OWNERS ├── README-zh.md ├── README.md ├── action.yml ├── cmd ├── doc.go ├── fetch.go ├── fetch_test.go ├── get.go ├── get_test.go ├── install.go ├── install_test.go ├── root.go ├── root_test.go ├── search.go ├── search_test.go ├── setup.go ├── setup_test.go ├── testdata │ └── magnet.html ├── util.go └── util_test.go ├── codecov.yml ├── go.mod ├── go.sum ├── hack ├── README.md ├── deploy.yaml └── init.sh ├── main.go ├── mock └── mhttp │ └── roundtripper.go ├── pkg ├── common │ ├── doc.go │ ├── util.go │ └── util_test.go ├── compress │ ├── bzip2.go │ ├── doc.go │ ├── gzip.go │ ├── testdata │ │ ├── simple.tar.gz │ │ └── simple.zip │ ├── types.go │ ├── types_test.go │ ├── xz.go │ └── zip.go ├── doc.go ├── error.go ├── error_test.go ├── http.go ├── installer │ ├── check.go │ ├── check_test.go │ ├── cmd.go │ ├── cmd_test.go │ ├── doc.go │ ├── fetch.go │ ├── fetch_test.go │ ├── process.go │ ├── process_test.go │ └── types.go ├── log │ ├── doc.go │ ├── level.go │ └── level_test.go ├── net │ ├── doc.go │ ├── error.go │ ├── error_test.go │ ├── fake.go │ ├── fake_test.go │ ├── http.go │ ├── http_test.go │ ├── multi_thread.go │ ├── progress.go │ ├── retry_client.go │ ├── retry_client_test.go │ └── setup_test.go ├── os │ ├── apk │ │ ├── common.go │ │ ├── common_test.go │ │ └── doc.go │ ├── apt │ │ ├── asciinema.go │ │ ├── bash-completion.go │ │ ├── common.go │ │ ├── common_test.go │ │ ├── conntrack.go │ │ ├── doc.go │ │ ├── docker.go │ │ ├── ffmpeg.go │ │ ├── git.go │ │ ├── golang.go │ │ ├── init.go │ │ ├── init_test.go │ │ ├── resource │ │ │ └── kubernetes.list │ │ ├── socat.go │ │ └── vim.go │ ├── brew │ │ ├── common.go │ │ ├── common_test.go │ │ └── doc.go │ ├── core │ │ ├── doc.go │ │ ├── fake.go │ │ ├── fake_test.go │ │ └── types.go │ ├── dnf │ │ ├── common.go │ │ ├── common_test.go │ │ ├── doc.go │ │ ├── docker.go │ │ ├── init.go │ │ └── init_test.go │ ├── doc.go │ ├── docker │ │ ├── bitbucket.go │ │ ├── doc.go │ │ ├── init.go │ │ └── init_test.go │ ├── fake │ │ ├── doc.go │ │ ├── fake.go │ │ └── fake_test.go │ ├── generic │ │ ├── common.go │ │ ├── common_test.go │ │ └── doc.go │ ├── generic_installer.go │ ├── generic_installer_test.go │ ├── installer.go │ ├── installer_test.go │ ├── npm │ │ ├── common.go │ │ ├── common_test.go │ │ └── doc.go │ ├── scoop │ │ ├── common.go │ │ └── doc.go │ ├── snap │ │ ├── common.go │ │ ├── common_test.go │ │ └── doc.go │ ├── winget │ │ ├── common.go │ │ └── doc.go │ └── yum │ │ ├── bash-completion.go │ │ ├── common.go │ │ ├── common_test.go │ │ ├── conntrack.go │ │ ├── doc.go │ │ ├── docker.go │ │ ├── git.go │ │ ├── golang.go │ │ ├── init.go │ │ ├── init_test.go │ │ ├── resource │ │ ├── go-repo.repo │ │ └── kubernetes.repo │ │ ├── socat.go │ │ └── vim.go ├── progress.go ├── release.go ├── setup_test.go └── version │ ├── doc.go │ ├── parser.go │ └── parser_test.go └── wix.json /.github/.typo-ci.yml: -------------------------------------------------------------------------------- 1 | dictionaries: 2 | - en 3 | - en_GB 4 | exclude_fiels: 5 | - ".github/**/*" 6 | - "go.mod" 7 | - "go.sum" 8 | - Makefile 9 | excluded_words: 10 | - KubeSphere 11 | - kubespheredev 12 | - minio 13 | - jcli 14 | - ioutil 15 | - Unmarshal 16 | - Errorf 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: http-downloader 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | pull-request-branch-name: 8 | separator: "-" 9 | - package-ecosystem: "gomod" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | pull-request-branch-name: 14 | separator: "-" 15 | -------------------------------------------------------------------------------- /.github/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | make pre-commit 3 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Release Drafter: https://github.com/toolmantim/release-drafter 2 | name-template: 'v$NEXT_PATCH_VERSION 🌈' 3 | tag-template: 'v$NEXT_PATCH_VERSION' 4 | version-template: $MAJOR.$MINOR.$PATCH 5 | # Emoji reference: https://gitmoji.carloscuesta.me/ 6 | categories: 7 | - title: '🚀 Features' 8 | labels: 9 | - 'feature' 10 | - 'enhancement' 11 | - 'kind/feature' 12 | - title: '🐛 Bug Fixes' 13 | labels: 14 | - 'fix' 15 | - 'bugfix' 16 | - 'bug' 17 | - 'regression' 18 | - 'kind/bug' 19 | - title: 📝 Documentation updates 20 | labels: 21 | - documentation 22 | - 'kind/doc' 23 | - title: 👻 Maintenance 24 | labels: 25 | - chore 26 | - dependencies 27 | - 'kind/chore' 28 | - 'kind/dep' 29 | - title: 🚦 Tests 30 | labels: 31 | - test 32 | - tests 33 | exclude-labels: 34 | - reverted 35 | - no-changelog 36 | - skip-changelog 37 | - invalid 38 | change-template: '* $TITLE (#$NUMBER) @$AUTHOR' 39 | template: | 40 | ## What’s Changed 41 | 42 | $CHANGES 43 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: http-downloader 3 | description: HTTP download tool 4 | homepage: https://github.com/linuxsuren 5 | private: false 6 | has_issues: true 7 | has_wiki: false 8 | has_downloads: false 9 | default_branch: master 10 | allow_squash_merge: true 11 | allow_merge_commit: true 12 | allow_rebase_merge: true 13 | labels: 14 | - name: newbie 15 | color: abe7f4 16 | description: 新手上路 17 | - name: bug 18 | color: d73a4a 19 | description: Something isn't working 20 | - name: feature 21 | color: ffc6a3 22 | - name: enhancement 23 | color: a2eeef 24 | description: New feature or request 25 | - name: help wanted 26 | color: 008672 27 | description: Extra attention is needed 28 | - name: bugfix 29 | color: 0412d6 30 | - name: regression 31 | color: c5def5 32 | - name: documentation 33 | color: 5ce05e 34 | - name: Hacktoberfest 35 | description: More details from https://hacktoberfest.digitalocean.com/ 36 | color: 5ce05e 37 | - name: test 38 | color: c2c2fc 39 | - name: chore 40 | color: c2c2fc 41 | - name: dependencies 42 | color: 0366d6 43 | description: Pull requests that update a dependency file 44 | - name: no-changelog 45 | color: c2c2fc 46 | - name: kind/bug 47 | color: c2c2fc 48 | - name: kind/feature 49 | color: c2c2fc 50 | - name: kind/doc 51 | color: c2c2fc 52 | - name: kind/dep 53 | color: c2c2fc 54 | - name: kind/chore 55 | color: c2c2fc 56 | branches: 57 | - name: master 58 | protection: 59 | required_pull_request_reviews: 60 | required_approving_review_count: 1 61 | dismiss_stale_reviews: true 62 | require_code_owner_reviews: true 63 | dismissal_restrictions: 64 | users: [] 65 | teams: [] 66 | required_status_checks: 67 | strict: true 68 | contexts: [] 69 | enforce_admins: false 70 | restrictions: 71 | users: [] 72 | teams: [] 73 | -------------------------------------------------------------------------------- /.github/workflows/backup.yaml: -------------------------------------------------------------------------------- 1 | name: Backup Git repository 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | BackupGit: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v3.6.0 14 | - name: backup 15 | uses: jenkins-zh/git-backup-actions@v0.0.8 16 | env: 17 | GIT_DEPLOY_KEY: ${{ secrets.GIT_DEPLOY_KEY }} 18 | TARGET_GIT: "git@gitee.com:linuxsuren/http-downloader.git" 19 | -------------------------------------------------------------------------------- /.github/workflows/coverage-report.yaml: -------------------------------------------------------------------------------- 1 | name: Coverage Report 2 | 3 | on: 4 | - push 5 | - pull_request 6 | - release 7 | 8 | jobs: 9 | TestAndReport: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - name: Set up Go 13 | uses: actions/setup-go@v4 14 | with: 15 | go-version: 1.19.x 16 | - uses: actions/checkout@v3.6.0 17 | - name: Test 18 | run: | 19 | go test ./... -coverprofile coverage.out 20 | - name: Report 21 | env: 22 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} 23 | run: | 24 | bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r coverage.out --force-coverage-parser go 25 | - name: Upload coverage to Codecov 26 | uses: codecov/codecov-action@v3.1.4 27 | with: 28 | token: ${{ secrets.CODECOV_TOKEN }} 29 | files: coverage.out 30 | flags: unittests 31 | name: codecov-umbrella 32 | fail_ci_if_error: true 33 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | env: 8 | REGISTRY: ghcr.io 9 | 10 | jobs: 11 | build: 12 | name: Build 13 | runs-on: ubuntu-22.04 14 | steps: 15 | - name: Set up Go 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: 1.19 19 | id: go 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v3.6.0 22 | - name: Run GoReleaser 23 | uses: goreleaser/goreleaser-action@v4.4.0 24 | with: 25 | github_token: ${{ secrets.GH_PUBLISH_SECRETS }} 26 | version: v1.14.0 27 | args: release --skip-publish --rm-dist --snapshot 28 | - name: Test against the cmd 29 | run: | 30 | sudo cp ./release/http-downloader_linux_amd64_v1/hd /usr/local/bin 31 | 32 | # test with fullpath 33 | sudo hd install jenkins-zh/jenkins-cli/jcli 34 | jcli version 35 | 36 | # test with simple path 37 | sudo hd i mde 38 | 39 | # test with specific version 40 | sudo hd i mde@v0.0.4 -f 41 | - name: Run Trivy vulnerability scanner 42 | uses: aquasecurity/trivy-action@0.2.1 43 | if: github.event_name == 'pull_request' 44 | with: 45 | scan-type: 'fs' 46 | format: 'table' 47 | exit-code: '1' 48 | ignore-unfixed: true 49 | vuln-type: 'os,library' 50 | severity: 'CRITICAL,HIGH' 51 | 52 | GoLint: 53 | name: Lint 54 | runs-on: ubuntu-22.04 55 | steps: 56 | - name: Set up Go 57 | uses: actions/setup-go@v4 58 | with: 59 | go-version: 1.19 60 | id: go 61 | - name: Check out code into the Go module directory 62 | uses: actions/checkout@v3.6.0 63 | - name: Go-linter-1 64 | uses: Jerome1337/golint-action@v1.0.2 65 | with: 66 | golint-path: ./... 67 | Security: 68 | name: Security 69 | runs-on: ubuntu-22.04 70 | env: 71 | GO111MODULE: on 72 | steps: 73 | - name: Checkout Source 74 | uses: actions/checkout@v3.6.0 75 | - name: Run Gosec Security Scanner 76 | uses: securego/gosec@master 77 | with: 78 | args: '-exclude=G402,G204,G304,G110,G306,G107 ./...' 79 | CodeQL: 80 | name: CodeQL 81 | runs-on: ubuntu-22.04 82 | env: 83 | GO111MODULE: on 84 | steps: 85 | - name: Checkout Source 86 | uses: actions/checkout@v3.6.0 87 | - name: Initialize CodeQL 88 | uses: github/codeql-action/init@v1 89 | with: 90 | languages: go 91 | - name: Perform CodeQL Analysis 92 | uses: github/codeql-action/analyze@v1 93 | MarkdownLinkCheck: 94 | name: MarkdownLinkCheck 95 | runs-on: ubuntu-22.04 96 | steps: 97 | - uses: actions/checkout@v3.6.0 98 | - uses: gaurav-nelson/github-action-markdown-link-check@1.0.13 99 | with: 100 | use-verbose-mode: 'yes' 101 | 102 | image: 103 | runs-on: ubuntu-22.04 104 | steps: 105 | - name: Checkout 106 | uses: actions/checkout@v4 107 | with: 108 | fetch-tags: true 109 | fetch-depth: 0 110 | - name: Setup Docker buildx 111 | uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf 112 | - name: Extract Docker metadata 113 | id: meta 114 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 115 | with: 116 | images: ${{ env.REGISTRY }}/linuxsuren/hd 117 | - name: Build and push Docker image 118 | id: build-and-push 119 | uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a 120 | with: 121 | context: . 122 | push: false 123 | tags: ${{ steps.meta.outputs.tags }} 124 | labels: ${{ steps.meta.outputs.labels }} 125 | platforms: linux/amd64,linux/arm64 126 | cache-from: type=gha 127 | cache-to: type=gha,mode=max 128 | build-args: VERSION=${{ steps.vars.outputs.tag }} -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | UpdateReleaseDraft: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - uses: release-drafter/release-drafter@v5 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GH_PUBLISH_SECRETS }} 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | env: 8 | REGISTRY: ghcr.io 9 | 10 | jobs: 11 | goreleaser: 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3.6.0 16 | with: 17 | fetch-tags: true 18 | fetch-depth: 0 19 | - name: Set output 20 | id: vars 21 | run: echo "tag=$(git describe --tags)" >> $GITHUB_OUTPUT 22 | - name: Set up Go 23 | uses: actions/setup-go@v4 24 | with: 25 | go-version: 1.19 26 | - name: Image Registry Login 27 | run: | 28 | docker login --username ${{ secrets.DOCKER_HUB_USER }} --password ${{secrets.DOCKER_HUB_TOKEN}} 29 | docker login ${{ env.REGISTRY }}/linuxsuren --username linuxsuren --password ${{secrets.GH_PUBLISH_SECRETS}} 30 | - name: Run GoReleaser 31 | uses: goreleaser/goreleaser-action@v4.4.0 32 | with: 33 | github_token: ${{ secrets.GH_PUBLISH_SECRETS }} 34 | version: v1.14.0 35 | args: release --rm-dist 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GH_PUBLISH_SECRETS }} 38 | - name: Upload via oras 39 | run: | 40 | export TAG=${{ steps.vars.outputs.tag }} 41 | TAG=${TAG#v} 42 | oras push docker.io/linuxsuren/hd:$TAG release 43 | oras push ${{ env.REGISTRY }}/linuxsuren/hd:$TAG release 44 | 45 | image: 46 | runs-on: ubuntu-22.04 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | with: 51 | fetch-tags: true 52 | fetch-depth: 0 53 | - name: Setup Docker buildx 54 | uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf 55 | - name: Log into registry ${{ env.REGISTRY }} 56 | if: github.event_name != 'pull_request' 57 | uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c 58 | with: 59 | registry: ${{ env.REGISTRY }} 60 | username: ${{ github.actor }} 61 | password: ${{ secrets.GH_PUBLISH_SECRETS }} 62 | - name: Extract Docker metadata 63 | id: meta 64 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 65 | with: 66 | images: ${{ env.REGISTRY }}/linuxsuren/hd 67 | - name: Build and push Docker image 68 | id: build-and-push 69 | uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a 70 | with: 71 | context: . 72 | push: ${{ github.event_name != 'pull_request' }} 73 | tags: ${{ steps.meta.outputs.tags }} 74 | labels: ${{ steps.meta.outputs.labels }} 75 | platforms: linux/amd64,linux/arm64 76 | cache-from: type=gha 77 | cache-to: type=gha,mode=max 78 | build-args: VERSION=${{ steps.vars.outputs.tag }} 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | bin/ 3 | 4 | */**/*.xml 5 | coverage.out 6 | node_modules 7 | release/ 8 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: make build 3 | command: make copy 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Official documentation at http://goreleaser.com 2 | project_name: http-downloader 3 | builds: 4 | - env: 5 | - CGO_ENABLED=0 6 | binary: hd 7 | goarch: 8 | - amd64 9 | - arm64 10 | - arm 11 | goarm: 12 | - 7 13 | - 6 14 | goos: 15 | - windows 16 | - linux 17 | - darwin 18 | ignore: 19 | - goos: windows 20 | goarch: arm 21 | - goos: windows 22 | goarch: arm64 23 | - goos: darwin 24 | goarch: arm 25 | ldflags: 26 | - -X github.com/linuxsuren/cobra-extension/version.version={{.Version}} 27 | - -X github.com/linuxsuren/cobra-extension/version.commit={{.ShortCommit}} 28 | - -X github.com/linuxsuren/cobra-extension/version.date={{.Date}} 29 | - -w 30 | - -s 31 | dist: release 32 | archives: 33 | - name_template: "{{ .Binary }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}" 34 | replacements: 35 | darwin: darwin 36 | linux: linux 37 | windows: windows 38 | amd64: amd64 39 | arm64: arm64 40 | format_overrides: 41 | - goos: windows 42 | format: zip 43 | files: 44 | - README.md 45 | checksum: 46 | name_template: 'checksums.txt' 47 | snapshot: 48 | name_template: "{{ .Tag }}-next-{{.ShortCommit}}" 49 | changelog: 50 | skip: true 51 | sort: asc 52 | filters: 53 | exclude: 54 | - '^docs:' 55 | - '^test:' 56 | brews: 57 | - name: hd 58 | tap: 59 | owner: linuxsuren 60 | name: homebrew-linuxsuren 61 | folder: Formula 62 | homepage: "https://github.com/linuxsuren/http-downloader" 63 | description: HTTP download tool 64 | dependencies: 65 | - name: bash-completion 66 | type: optional 67 | test: | 68 | version_output = shell_output("#{bin}/hd version") 69 | assert_match version.to_s, version_output 70 | install: | 71 | bin.install name 72 | 73 | prefix.install_metafiles 74 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/golang:1.19 AS builder 2 | 3 | WORKDIR /workspace 4 | COPY cmd/ cmd/ 5 | COPY hack/ hack/ 6 | COPY mock/ mock/ 7 | COPY pkg/ pkg/ 8 | COPY main.go . 9 | COPY README.md README.md 10 | COPY go.mod go.mod 11 | COPY go.sum go.sum 12 | RUN CGO_ENABLED=0 go build -ldflags "-w -s" -o /usr/local/bin/hd . 13 | 14 | FROM alpine:3.10 15 | 16 | COPY --from=builder /usr/local/bin/hd /usr/local/bin/hd 17 | RUN hd fetch 18 | 19 | CMD ["hd"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Zhao Xiaojie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: fmt test 2 | export GOPROXY=https://goproxy.io 3 | CGO_ENABLED=0 go build -ldflags "-w -s" -o bin/hd 4 | 5 | build-windows: 6 | GOOS=windows CGO_ENABLED=0 go build -ldflags "-w -s" -o bin/windows/hd.exe 7 | build-linux: fmt lint build-linux-no-check 8 | build-linux-no-check: 9 | export GOPROXY=https://goproxy.io 10 | CGO_ENABLED=0 GOOS=linux go build -ldflags "-w -s" -o bin/linux/hd 11 | upx bin/linux/hd 12 | 13 | test: fmt 14 | go test ./... -coverprofile coverage.out 15 | pre-commit: fmt test build 16 | cp-pre-commit: 17 | cp .github/pre-commit .git/hooks/pre-commit 18 | run: 19 | go run main.go 20 | 21 | fmt: 22 | go fmt ./... 23 | 24 | lint: 25 | golangci-lint run ./... 26 | 27 | copy: build 28 | sudo cp bin/hd /usr/local/bin/ 29 | 30 | init: gen-mock 31 | gen-mock: 32 | go get github.com/golang/mock/gomock 33 | go install github.com/golang/mock/mockgen 34 | mockgen -destination ./mock/mhttp/roundtripper.go -package mhttp net/http RoundTripper 35 | 36 | update: 37 | git fetch 38 | git reset --hard origin/$(shell git branch --show-current) 39 | goreleaser: 40 | goreleaser build --snapshot --rm-dist 41 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - linuxsuren 3 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | [![](https://goreportcard.com/badge/linuxsuren/http-downloader)](https://goreportcard.com/report/linuxsuren/github-go) 2 | [![](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square)](https://godoc.org/github.com/linuxsuren/http-downloader) 3 | [![Contributors](https://img.shields.io/github/contributors/linuxsuren/http-downloader.svg)](https://github.com/linuxsuren/github-go/graphs/contributors) 4 | [![GitHub release](https://img.shields.io/github/release/linuxsuren/http-downloader.svg?label=release)](https://github.com/linuxsuren/github-go/releases/latest) 5 | ![GitHub All Releases](https://img.shields.io/github/downloads/linuxsuren/http-downloader/total) 6 | 7 | # 入门 8 | 9 | `hd` 是一个基于 HTTP 协议的下载工具。 10 | 11 | 通过命令:`brew install linuxsuren/linuxsuren/hd` 来安装 12 | 13 | 或者,对于 Linux 用户可以直接通过命令下载: 14 | ```shell 15 | curl -L https://github.com/linuxsuren/http-downloader/releases/latest/download/hd-linux-amd64.tar.gz | tar xzv 16 | mv hd /usr/local/bin 17 | ``` 18 | 19 | 想要浏览该项目的代码吗?[GitPod](https://gitpod.io/#https://github.com/linuxsuren/http-downloader) 绝对可以帮助你! 20 | 21 | # 用法 22 | 23 | ```shell 24 | hd get https://github.com/jenkins-zh/jenkins-cli/releases/latest/download/jcli-linux-amd64.tar.gz --thread 6 25 | ``` 26 | 27 | 或者,用一个更加简便的办法: 28 | 29 | ```shell 30 | hd get jenkins-zh/jenkins-cli/jcli -t 6 31 | ``` 32 | 33 | 获取,你也可以安装一个来自 GitHub 的软件包: 34 | 35 | ```shell 36 | hd install jenkins-zh/jenkins-cli/jcli -t 6 37 | ``` 38 | 39 | 或者,你也可以从 GitHub 上下载预发布的二进制包: 40 | 41 | ```shell 42 | hd get --pre ks 43 | ``` 44 | 45 | # 功能 46 | 47 | * 基于 HTTP 协议下载文件的 Golang 工具库 48 | * 多线程 49 | * 断点续传 (TODO) 50 | * 对 GitHub release 文件下载(安装)友好 51 | 52 | ## 使用多阶段构建 53 | 你想要在 Docker 构建中下载工具吗?这个很容易的,请查看下面的例子: 54 | 55 | ```dockerfile 56 | FROM ghcr.io/linuxsuren/hd:v0.0.42 as downloader 57 | RUN hd install kubesphere-sigs/ks@v0.0.50 58 | 59 | FROM alpine:3.10 60 | COPY --from=downloader /usr/local/bin/ks /usr/local/bin/ks 61 | CMD ["ks"] 62 | ``` 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/LinuxSuRen/http-downloader) 2 | [![](https://goreportcard.com/badge/linuxsuren/http-downloader)](https://goreportcard.com/report/linuxsuren/github-go) 3 | [![](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square)](https://godoc.org/github.com/linuxsuren/http-downloader) 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/7cc20ea84e0543068c320e471bde560e)](https://app.codacy.com/gh/LinuxSuRen/http-downloader/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 5 | [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/7cc20ea84e0543068c320e471bde560e)](https://app.codacy.com/gh/LinuxSuRen/http-downloader/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage) 6 | [![codecov](https://codecov.io/gh/LinuxSuRen/http-downloader/branch/master/graph/badge.svg?token=Ntc8z2iEQ2)](https://codecov.io/gh/LinuxSuRen/http-downloader) 7 | [![Contributors](https://img.shields.io/github/contributors/linuxsuren/http-downloader.svg)](https://github.com/linuxsuren/github-go/graphs/contributors) 8 | [![GitHub release](https://img.shields.io/github/release/linuxsuren/http-downloader.svg?label=release)](https://github.com/linuxsuren/github-go/releases/latest) 9 | ![GitHub All Releases](https://img.shields.io/github/downloads/linuxsuren/http-downloader/total) 10 | [![LinuxSuRen/open-source-best-practice](https://img.shields.io/static/v1?label=OSBP&message=%E5%BC%80%E6%BA%90%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5&color=blue)](https://github.com/LinuxSuRen/open-source-best-practice) 11 | 12 | # Get started 13 | `hd` is a HTTP download tool. 14 | 15 | Install it via: `brew install linuxsuren/linuxsuren/hd` 16 | 17 | Or download it directly (for Linux): 18 | ``` 19 | curl https://linuxsuren.github.io/tools/install.sh|bash 20 | ``` 21 | 22 | Or download it via proxy: 23 | ``` 24 | curl https://gitee.com/linuxsuren/tools/raw/master/install-zh.sh|bash 25 | ``` 26 | 27 | for Windows users (you might need to add this program into the Windows Defence exclude list): 28 | ``` 29 | winget install 'HTTP downloader' 30 | ``` 31 | 32 | Want to go through the code? [GitPod](https://gitpod.io/#https://github.com/linuxsuren/http-downloader) definitely can help you. 33 | 34 | # Usage 35 | 36 | ## Download 37 | ```shell 38 | hd get https://github.com/jenkins-zh/jenkins-cli/releases/latest/download/mde-linux-amd64.tar.gz --thread 6 39 | ``` 40 | 41 | Or use a simple way instead of typing the whole URL: 42 | 43 | ```shell 44 | hd get mde 45 | ``` 46 | 47 | Or you might want to download a pre-released binary package from GitHub: 48 | 49 | ```shell 50 | hd get --pre ks 51 | ``` 52 | 53 | ## Install 54 | You can also install a package from GitHub: 55 | 56 | ```shell 57 | #!title: Install mde with specific threads 58 | hd install mde -t 6 59 | ``` 60 | 61 | or install by a category name: 62 | 63 | ```shell 64 | hd install --category security 65 | ``` 66 | 67 | ## Search 68 | hd can download or install via the format of `$org/$repo`. If you find that it's not working. It might because of there's 69 | no record in [hd-home](https://github.com/LinuxSuRen/hd-home). You're welcome to help us to maintain it. 70 | 71 | When you first run it, please init via: `hd fetch` 72 | 73 | then you can search it by a keyword: `hd search jenkins` 74 | 75 | ## Use multi-stage builds 76 | Do you want to download tools in the Docker builds? It's pretty easy. Please see the following example: 77 | 78 | ```dockerfile 79 | FROM ghcr.io/linuxsuren/hd:v0.0.42 as downloader 80 | RUN hd install kubesphere-sigs/ks@v0.0.50 81 | 82 | FROM alpine:3.10 83 | COPY --from=downloader /usr/local/bin/ks /usr/local/bin/ks 84 | CMD ["ks"] 85 | ``` 86 | 87 | ## As a library 88 | You can import it from `github.com/linuxsuren/http-downloader/pkg/installer`, then put the following code to your CLI. 89 | It can help you to download desired tools: 90 | 91 | ```go 92 | is := installer.Installer{ 93 | Provider: "github", 94 | } 95 | if err = is.CheckDepAndInstall(map[string]string{ 96 | "ks": "linuxsuren/ks", 97 | "kk": "kubekey", 98 | }); err != nil { 99 | return 100 | } 101 | ``` 102 | 103 | ## Install other services 104 | It supports to install other services, for example: `bitbucket`. 105 | 106 | ```shell 107 | hd install bitbucket 108 | ``` 109 | 110 | # Features 111 | * go library for HTTP 112 | * multi-thread 113 | * continuously (TODO) 114 | * GitHub release asset friendly 115 | 116 | ## Release 117 | 118 | This project can be released via [linuxsuren-versions](https://github.com/linuxsuren/linuxsuren-versions). 119 | 120 | ![Visitor Count](https://profile-counter.glitch.me/{http-downloader}/count.svg) 121 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: setup-hd 2 | description: Setup hd, on GitHub Actions runners 3 | inputs: 4 | version: 5 | description: Version of hd CLI to install 6 | required: false 7 | default: 0.0.69 8 | runs: 9 | using: composite 10 | steps: 11 | # We verify the version against a SHA **in the published action itself**, not in the GCS bucket. 12 | - shell: bash 13 | run: | 14 | #!/bin/bash 15 | curl -L https://github.com/LinuxSuRen/http-downloader/releases/download/v${{ inputs.version }}/hd-linux-amd64.tar.gz | tar xzv hd 16 | sudo mv hd /usr/bin/hd 17 | hd fetch 18 | -------------------------------------------------------------------------------- /cmd/doc.go: -------------------------------------------------------------------------------- 1 | // Package cmd provides the CLI commands 2 | package cmd 3 | -------------------------------------------------------------------------------- /cmd/fetch.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/linuxsuren/http-downloader/pkg" 11 | "github.com/linuxsuren/http-downloader/pkg/log" 12 | 13 | "github.com/linuxsuren/http-downloader/pkg/installer" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func newFetchCmd(ctx context.Context) (cmd *cobra.Command) { 18 | opt := &fetchOption{ 19 | fetcher: &installer.DefaultFetcher{}, 20 | } 21 | cmd = &cobra.Command{ 22 | Use: "fetch", 23 | Short: "Fetch the latest hd config", 24 | PreRunE: opt.preRunE, 25 | RunE: opt.runE, 26 | GroupID: configGroup.ID, 27 | } 28 | 29 | flags := cmd.Flags() 30 | opt.addFlags(flags) 31 | flags.StringVarP(&opt.branch, "branch", "b", installer.ConfigBranch, 32 | "The branch of git repository (not support currently)") 33 | flags.BoolVarP(&opt.reset, "reset", "", false, 34 | "If you want to reset the hd-config which means delete and clone it again") 35 | flags.IntVarP(&opt.retry, "retry", "", 6, "Retry times due to timeout error") 36 | flags.DurationVarP(&opt.timeout, "timeout", "", time.Second*60, "Timeout of fetching") 37 | return 38 | } 39 | 40 | func (o *fetchOption) setTimeout(c *cobra.Command) { 41 | if c.Context() != nil { 42 | var ctx context.Context 43 | ctx, o.cancel = context.WithTimeout(c.Context(), o.timeout) 44 | o.fetcher.SetContext(ctx) 45 | } 46 | } 47 | 48 | func (o *fetchOption) preRunE(c *cobra.Command, _ []string) (err error) { 49 | o.setTimeout(c) 50 | if o.reset { 51 | var configDir string 52 | if configDir, err = o.fetcher.GetConfigDir(); err == nil { 53 | err = os.RemoveAll(configDir) 54 | err = pkg.ErrorWrap(err, "failed to remove directory: %s, error %v", configDir, err) 55 | } else { 56 | err = fmt.Errorf("failed to get config directory, error %v", err) 57 | } 58 | } 59 | return 60 | } 61 | 62 | func (o *fetchOption) runE(c *cobra.Command, _ []string) (err error) { 63 | logger := log.GetLoggerFromContextOrDefault(c) 64 | 65 | var i int 66 | for i = 0; i < o.retry; i++ { 67 | err = o.fetcher.FetchLatestRepo(o.Provider, o.branch, c.OutOrStdout()) 68 | if err == nil || (!strings.Contains(err.Error(), "context deadline exceeded") && 69 | !strings.Contains(err.Error(), "i/o timeout")) { 70 | break 71 | } 72 | o.setTimeout(c) 73 | c.Print(".") 74 | } 75 | if i >= 1 { 76 | logger.Println() 77 | } 78 | return 79 | } 80 | 81 | type fetchOption struct { 82 | searchOption 83 | 84 | branch string 85 | reset bool 86 | fetcher installer.Fetcher 87 | cancel context.CancelFunc 88 | retry int 89 | timeout time.Duration 90 | } 91 | -------------------------------------------------------------------------------- /cmd/fetch_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os" 7 | "path" 8 | "testing" 9 | "time" 10 | 11 | "github.com/linuxsuren/http-downloader/pkg/installer" 12 | "github.com/spf13/cobra" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func Test_newFetchCmd(t *testing.T) { 17 | cmd := newFetchCmd(context.Background()) 18 | assert.Equal(t, "fetch", cmd.Name()) 19 | 20 | flags := []struct { 21 | name string 22 | shorthand string 23 | }{{ 24 | name: "branch", 25 | shorthand: "b", 26 | }, { 27 | name: "reset", 28 | }} 29 | for i := range flags { 30 | tt := flags[i] 31 | t.Run(tt.name, func(t *testing.T) { 32 | flag := cmd.Flag(tt.name) 33 | assert.NotNil(t, flag) 34 | assert.NotEmpty(t, flag.Usage) 35 | assert.Equal(t, tt.shorthand, flag.Shorthand) 36 | }) 37 | } 38 | } 39 | 40 | func TestSetTimeoutWithContext(t *testing.T) { 41 | tests := []struct { 42 | name string 43 | context context.Context 44 | timeout time.Duration 45 | expected bool 46 | }{ 47 | { 48 | name: "context with timeout", 49 | context: context.Background(), 50 | timeout: time.Second * 10, 51 | expected: true, 52 | }, 53 | { 54 | name: "context without timeout", 55 | context: nil, 56 | timeout: time.Second * 10, 57 | expected: false, 58 | }, 59 | } 60 | 61 | for _, tt := range tests { 62 | t.Run(tt.name, func(t *testing.T) { 63 | cmd := &cobra.Command{} 64 | cmd.SetContext(tt.context) 65 | opt := &fetchOption{ 66 | timeout: tt.timeout, 67 | fetcher: &installer.FakeFetcher{}, 68 | } 69 | opt.setTimeout(cmd) 70 | if tt.expected { 71 | assert.NotNil(t, opt.cancel) 72 | } else { 73 | assert.Nil(t, opt.cancel) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func TestFetchPreRunE(t *testing.T) { 80 | tests := []struct { 81 | name string 82 | opt *fetchOption 83 | hasErr bool 84 | }{{ 85 | name: "not reset", 86 | opt: &fetchOption{}, 87 | hasErr: false, 88 | }, { 89 | name: "reset, cannot get config dir", 90 | opt: &fetchOption{ 91 | reset: true, 92 | fetcher: &installer.FakeFetcher{ 93 | GetConfigDirErr: errors.New("no config dir"), 94 | }, 95 | }, 96 | hasErr: true, 97 | }, { 98 | name: "reset, remove dir", 99 | opt: &fetchOption{ 100 | reset: true, 101 | fetcher: &installer.FakeFetcher{ 102 | ConfigDir: path.Join(os.TempDir(), "hd-config"), 103 | }, 104 | }, 105 | hasErr: false, 106 | }} 107 | for _, tt := range tests { 108 | t.Run(tt.name, func(t *testing.T) { 109 | c := &cobra.Command{} 110 | err := tt.opt.preRunE(c, nil) 111 | if tt.hasErr { 112 | assert.NotNil(t, err) 113 | } else { 114 | assert.Nil(t, err) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | func TestFetchRunE(t *testing.T) { 121 | tests := []struct { 122 | name string 123 | opt *fetchOption 124 | hasErr bool 125 | }{ 126 | { 127 | name: "normal", 128 | opt: &fetchOption{ 129 | fetcher: &installer.FakeFetcher{}, 130 | }, 131 | hasErr: false, 132 | }, 133 | { 134 | name: "fetch with retry", 135 | opt: &fetchOption{ 136 | fetcher: &installer.FakeFetcher{ 137 | FetchLatestRepoErr: errors.New("context deadline exceeded"), 138 | }, 139 | retry: 3, 140 | }, 141 | hasErr: true, 142 | }, 143 | { 144 | name: "fetch with non-retryable error", 145 | opt: &fetchOption{ 146 | fetcher: &installer.FakeFetcher{ 147 | FetchLatestRepoErr: errors.New("some other error"), 148 | }, 149 | retry: 3, 150 | }, 151 | hasErr: true, 152 | }, 153 | } 154 | 155 | for _, tt := range tests { 156 | t.Run(tt.name, func(t *testing.T) { 157 | c := &cobra.Command{} 158 | err := tt.opt.runE(c, nil) 159 | if tt.hasErr { 160 | assert.NotNil(t, err) 161 | } else { 162 | assert.Nil(t, err) 163 | } 164 | }) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /cmd/install_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "testing" 8 | 9 | cotesting "github.com/linuxsuren/cobra-extension/pkg/testing" 10 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 11 | "github.com/linuxsuren/http-downloader/pkg/installer" 12 | "github.com/spf13/cobra" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func Test_newInstallCmd(t *testing.T) { 17 | cmd := newInstallCmd(context.Background()) 18 | assert.Equal(t, "install", cmd.Name()) 19 | 20 | test := cotesting.FlagsValidation{{ 21 | Name: "category", 22 | Shorthand: "c", 23 | }, { 24 | Name: "show-progress", 25 | }, { 26 | Name: "accept-preRelease", 27 | }, { 28 | Name: "pre", 29 | }, { 30 | Name: "from-source", 31 | }, { 32 | Name: "from-branch", 33 | }, { 34 | Name: "goget", 35 | }, { 36 | Name: "download", 37 | }, { 38 | Name: "force", 39 | Shorthand: "f", 40 | }, { 41 | Name: "clean-package", 42 | }, { 43 | Name: "thread", 44 | Shorthand: "t", 45 | }, { 46 | Name: "keep-part", 47 | }, { 48 | Name: "os", 49 | }, { 50 | Name: "arch", 51 | }, { 52 | Name: "proxy-github", 53 | }, { 54 | Name: "fetch", 55 | }, { 56 | Name: "provider", 57 | }, { 58 | Name: "no-proxy", 59 | }} 60 | test.Valid(t, cmd.Flags()) 61 | } 62 | 63 | func TestInstallPreRunE(t *testing.T) { 64 | type args struct { 65 | cmd *cobra.Command 66 | args []string 67 | } 68 | for i, tt := range []struct { 69 | name string 70 | opt *installOption 71 | args args 72 | expectErr bool 73 | }{{ 74 | name: "tool and category are empty", 75 | opt: &installOption{ 76 | downloadOption: &downloadOption{}, 77 | }, 78 | expectErr: true, 79 | }, { 80 | name: "a fake tool that have an invalid path, no category", 81 | opt: &installOption{ 82 | downloadOption: &downloadOption{ 83 | searchOption: searchOption{Fetch: false}, 84 | wait: &sync.WaitGroup{}, 85 | }, 86 | }, 87 | args: args{ 88 | args: []string{"xx@xx@xx"}, 89 | cmd: &cobra.Command{}, 90 | }, 91 | expectErr: true, 92 | }, { 93 | name: "have category", 94 | opt: &installOption{ 95 | downloadOption: &downloadOption{ 96 | searchOption: searchOption{Fetch: false}, 97 | Category: "tool", 98 | wait: &sync.WaitGroup{}, 99 | }, 100 | }, 101 | args: args{ 102 | args: []string{"xx@xx@xx"}, 103 | cmd: &cobra.Command{}, 104 | }, 105 | expectErr: false, 106 | }} { 107 | t.Run(tt.name, func(t *testing.T) { 108 | err := tt.opt.preRunE(tt.args.cmd, tt.args.args) 109 | if tt.expectErr { 110 | assert.NotNil(t, err, "failed with [%d] - case [%s]", i, tt.name) 111 | } else { 112 | assert.Nil(t, err, "failed with [%d] - case [%s]", i, tt.name) 113 | } 114 | }) 115 | } 116 | } 117 | 118 | func TestShouldInstall(t *testing.T) { 119 | opt := &installOption{ 120 | downloadOption: &downloadOption{}, 121 | execer: &fakeruntime.FakeExecer{ 122 | ExpectOutput: "v1.2.3", 123 | }, 124 | tool: "fake", 125 | } 126 | should, exist := opt.shouldInstall() 127 | assert.False(t, should) 128 | assert.True(t, exist) 129 | 130 | { 131 | optGreater := &installOption{ 132 | execer: &fakeruntime.FakeExecer{ 133 | ExpectOutput: "v1.2.3", 134 | }, 135 | downloadOption: &downloadOption{ 136 | Package: &installer.HDConfig{ 137 | Version: "v1.2.4", 138 | VersionCmd: "-v", 139 | }, 140 | }, 141 | tool: "fake", 142 | } 143 | should, exist := optGreater.shouldInstall() 144 | assert.True(t, should) 145 | assert.True(t, exist) 146 | } 147 | 148 | // force to install 149 | opt.force = true 150 | should, exist = opt.shouldInstall() 151 | assert.True(t, should) 152 | assert.True(t, exist) 153 | 154 | // not exist 155 | opt.execer = &fakeruntime.FakeExecer{ 156 | ExpectError: errors.New("fake"), 157 | ExpectLookPathError: errors.New("error"), 158 | } 159 | should, exist = opt.shouldInstall() 160 | assert.True(t, should) 161 | assert.False(t, exist) 162 | } 163 | 164 | func TestInstall(t *testing.T) { 165 | type args struct { 166 | cmd *cobra.Command 167 | args []string 168 | } 169 | for i, tt := range []struct { 170 | name string 171 | opt *installOption 172 | args args 173 | expectErr bool 174 | }{{ 175 | name: "is a nativePackage, but it's exist", 176 | opt: &installOption{ 177 | downloadOption: &downloadOption{}, 178 | nativePackage: true, 179 | execer: fakeruntime.FakeExecer{}, 180 | }, 181 | args: args{cmd: &cobra.Command{}}, 182 | expectErr: false, 183 | }} { 184 | t.Run(tt.name, func(t *testing.T) { 185 | err := tt.opt.install(tt.args.cmd, tt.args.args) 186 | if tt.expectErr { 187 | assert.NotNil(t, err, "failed with [%d] - case [%s]", i, tt.name) 188 | } else { 189 | assert.Nil(t, err, "failed with [%d] - case [%s]", i, tt.name) 190 | } 191 | }) 192 | } 193 | } 194 | 195 | func TestGetDefaultInstallDir(t *testing.T) { 196 | tests := []struct { 197 | name string 198 | execer fakeruntime.Execer 199 | expect string 200 | }{{ 201 | name: "linux", 202 | execer: fakeruntime.FakeExecer{ 203 | ExpectOS: "linux", 204 | }, 205 | expect: "/usr/local/bin", 206 | }, { 207 | name: "darwin", 208 | execer: fakeruntime.FakeExecer{ 209 | ExpectOS: "darwin", 210 | }, 211 | expect: "/usr/local/bin", 212 | }, { 213 | name: "windows", 214 | execer: fakeruntime.FakeExecer{ 215 | ExpectOS: "windows", 216 | }, 217 | expect: `C:\Program Files (x86)\Common Files`, 218 | }, { 219 | name: "unknown", 220 | execer: fakeruntime.FakeExecer{}, 221 | expect: "", 222 | }} 223 | for _, tt := range tests { 224 | t.Run(tt.name, func(t *testing.T) { 225 | opt := &installOption{execer: tt.execer} 226 | result := opt.getDefaultInstallDir() 227 | assert.Equal(t, tt.expect, result) 228 | }) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | 10 | "github.com/linuxsuren/http-downloader/pkg/log" 11 | "github.com/mitchellh/go-homedir" 12 | 13 | "github.com/AlecAivazis/survey/v2/terminal" 14 | extver "github.com/linuxsuren/cobra-extension/version" 15 | "github.com/spf13/cobra" 16 | "github.com/spf13/viper" 17 | ) 18 | 19 | var coreGroup *cobra.Group 20 | var configGroup *cobra.Group 21 | 22 | func init() { 23 | coreGroup = &cobra.Group{ID: "core", Title: "Core"} 24 | configGroup = &cobra.Group{ID: "conig", Title: "Config"} 25 | } 26 | 27 | // NewRoot returns the root command 28 | func NewRoot(cxt context.Context) (cmd *cobra.Command) { 29 | cmd = &cobra.Command{ 30 | Use: "hd", 31 | Short: "HTTP download tool", 32 | } 33 | cmd.AddGroup(coreGroup, configGroup) 34 | 35 | v := viper.GetViper() 36 | if err := loadConfig(v); err != nil { 37 | panic(err) 38 | } 39 | 40 | stdio := terminal.Stdio{ 41 | Out: os.Stdout, 42 | In: os.Stdin, 43 | Err: os.Stderr, 44 | } 45 | 46 | cxt = context.WithValue(cxt, log.LoggerContextKey, log.GetLogger()) 47 | cmd.AddCommand( 48 | newGetCmd(cxt), newInstallCmd(cxt), newFetchCmd(cxt), newSearchCmd(cxt), newSetupCommand(v, stdio), 49 | extver.NewVersionCmd("linuxsuren", "http-downloader", "hd", nil)) 50 | 51 | for _, c := range cmd.Commands() { 52 | registerFlagCompletionFunc(c, "provider", ArrayCompletion(ProviderGitHub, ProviderGitee)) 53 | registerFlagCompletionFunc(c, "os", ArrayCompletion("window", "linux", "darwin")) 54 | registerFlagCompletionFunc(c, "arch", ArrayCompletion("amd64", "arm64")) 55 | registerFlagCompletionFunc(c, "format", ArrayCompletion("tar.gz", "zip", "msi")) 56 | } 57 | return 58 | } 59 | 60 | func registerFlagCompletionFunc(cmd *cobra.Command, flag string, completionFunc CompletionFunc) { 61 | if p := cmd.Flag(flag); p != nil { 62 | if err := cmd.RegisterFlagCompletionFunc(flag, completionFunc); err != nil { 63 | cmd.Println(err) 64 | } 65 | } 66 | } 67 | 68 | func loadConfig(v *viper.Viper) (err error) { 69 | var userHome string 70 | if userHome, err = homedir.Dir(); err != nil { 71 | return 72 | } 73 | 74 | configDir := filepath.Join(userHome, ".config") 75 | v.SetConfigName("hd") 76 | v.SetConfigType("yaml") 77 | v.AddConfigPath(configDir) 78 | if err = v.ReadInConfig(); err != nil { 79 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 80 | // Config file not found; ignore error if desired 81 | err = nil 82 | } else { 83 | err = fmt.Errorf("failed to load config: %s, error: %v", os.ExpandEnv("$HOME/.config/hd.yaml"), err) 84 | } 85 | } 86 | v.SetDefault("provider", ProviderGitHub) 87 | v.SetDefault("fetch", false) 88 | v.SetDefault("goget", false) 89 | v.SetDefault("no-proxy", false) 90 | 91 | thread := runtime.NumCPU() 92 | if thread > 4 { 93 | thread = thread / 2 94 | } 95 | v.SetDefault("thread", thread) 96 | return 97 | } 98 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNewRoot(t *testing.T) { 11 | cmd := NewRoot(context.Background()) 12 | assert.Equal(t, "hd", cmd.Name()) 13 | } 14 | -------------------------------------------------------------------------------- /cmd/search.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/linuxsuren/http-downloader/pkg/installer" 9 | "github.com/linuxsuren/http-downloader/pkg/log" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/pflag" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | func newSearchCmd(context.Context) (cmd *cobra.Command) { 16 | opt := &searchOption{ 17 | fetcher: &installer.DefaultFetcher{}, 18 | } 19 | 20 | cmd = &cobra.Command{ 21 | Use: "search", 22 | Aliases: []string{"s", "find", "f"}, 23 | Short: "Search packages from the hd config repo", 24 | Args: cobra.MinimumNArgs(1), 25 | RunE: opt.runE, 26 | GroupID: configGroup.ID, 27 | } 28 | opt.addFlags(cmd.Flags()) 29 | return 30 | } 31 | 32 | type searchOption struct { 33 | Fetch bool 34 | Provider string 35 | ProxyGitHub string 36 | fetcher installer.Fetcher 37 | } 38 | 39 | func (s *searchOption) addFlags(flags *pflag.FlagSet) { 40 | flags.BoolVarP(&s.Fetch, "fetch", "", viper.GetBool("fetch"), 41 | "If fetch the latest config from https://github.com/LinuxSuRen/hd-home") 42 | flags.StringVarP(&s.Provider, "provider", "", viper.GetString("provider"), "The file provider") 43 | flags.StringVarP(&s.ProxyGitHub, "proxy-github", "", viper.GetString("proxy-github"), 44 | `The proxy address of github.com, the proxy address will be the prefix of the final address. 45 | Submit the new proxy server in https://github.com/LinuxSuRen/hd-home`) 46 | } 47 | 48 | func (s *searchOption) runE(c *cobra.Command, args []string) (err error) { 49 | logger := log.GetLoggerFromContextOrDefault(c) 50 | 51 | err = search(args[0], s.Fetch, s.fetcher, c.OutOrStdout(), logger) 52 | return 53 | } 54 | 55 | func search(keyword string, fetch bool, fetcher installer.Fetcher, writer io.Writer, logger *log.LevelLog) (err error) { 56 | if fetch { 57 | if err = fetcher.FetchLatestRepo("", "", writer); err != nil { 58 | return 59 | } 60 | } 61 | 62 | var configDir string 63 | if configDir, err = fetcher.GetConfigDir(); err != nil { 64 | return 65 | } 66 | 67 | logger.Info("start to search in:", configDir) 68 | result := installer.FindByKeyword(keyword, configDir) 69 | for _, item := range result { 70 | fmt.Fprintln(writer, item) 71 | } 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /cmd/search_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "os" 8 | "path" 9 | "testing" 10 | 11 | "github.com/linuxsuren/http-downloader/pkg/installer" 12 | "github.com/linuxsuren/http-downloader/pkg/log" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func Test_search(t *testing.T) { 17 | logger := log.GetLogger() 18 | 19 | buf := &bytes.Buffer{} 20 | err := search("keyword", true, &installer.FakeFetcher{}, buf, logger) 21 | assert.Nil(t, err) 22 | assert.Empty(t, buf.String()) 23 | 24 | // expect an error with GetConfigDir 25 | err = search("", true, &installer.FakeFetcher{GetConfigDirErr: errors.New("fake")}, buf, logger) 26 | assert.NotNil(t, err) 27 | 28 | // expect an error with FetchLatestRepo 29 | err = search("", true, &installer.FakeFetcher{FetchLatestRepoErr: errors.New("fake")}, buf, logger) 30 | assert.NotNil(t, err) 31 | 32 | tempDir, err := os.MkdirTemp("", "config") 33 | assert.Nil(t, err) 34 | defer func() { 35 | _ = os.RemoveAll(tempDir) 36 | }() 37 | 38 | configDir := path.Join(tempDir, "config") 39 | orgDir := path.Join(configDir, "org") 40 | err = os.MkdirAll(orgDir, 0755) 41 | assert.Nil(t, err) 42 | err = os.WriteFile(path.Join(orgDir, "repo.yml"), []byte("x=x"), os.ModeAppend) 43 | assert.Nil(t, err) 44 | err = os.WriteFile(path.Join(orgDir, "fake.yml"), []byte{}, os.ModeAppend) 45 | assert.Nil(t, err) 46 | 47 | err = search("repo", true, &installer.FakeFetcher{ConfigDir: tempDir}, buf, logger) 48 | assert.Nil(t, err) 49 | } 50 | 51 | func Test_newSearchCmd(t *testing.T) { 52 | cmd := newSearchCmd(context.Background()) 53 | assert.Equal(t, "search", cmd.Name()) 54 | 55 | flags := []struct { 56 | name string 57 | shorthand string 58 | }{{ 59 | name: "fetch", 60 | }, { 61 | name: "provider", 62 | }, { 63 | name: "proxy-github", 64 | }} 65 | for i := range flags { 66 | tt := flags[i] 67 | t.Run(tt.name, func(t *testing.T) { 68 | flag := cmd.Flag(tt.name) 69 | assert.NotNil(t, flag) 70 | assert.NotEmpty(t, flag.Usage) 71 | assert.Equal(t, tt.shorthand, flag.Shorthand) 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cmd/setup.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/AlecAivazis/survey/v2" 9 | "github.com/AlecAivazis/survey/v2/terminal" 10 | "github.com/linuxsuren/http-downloader/pkg/installer" 11 | "github.com/linuxsuren/http-downloader/pkg/log" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | func newSetupCommand(v *viper.Viper, stdio terminal.Stdio) (cmd *cobra.Command) { 17 | opt := &setupOption{ 18 | stdio: stdio, 19 | v: v, 20 | } 21 | cmd = &cobra.Command{ 22 | Use: "setup", 23 | Short: "Init the configuration of hd", 24 | RunE: opt.runE, 25 | GroupID: configGroup.ID, 26 | } 27 | flags := cmd.Flags() 28 | flags.StringVarP(&opt.proxy, "proxy", "p", "", "The proxy of GitHub") 29 | flags.StringVarP(&opt.provider, "provider", "", "", "The provider of hd configuration") 30 | return 31 | } 32 | 33 | type setupOption struct { 34 | stdio terminal.Stdio 35 | v *viper.Viper 36 | 37 | proxy string 38 | provider string 39 | } 40 | 41 | func (o *setupOption) runE(cmd *cobra.Command, args []string) (err error) { 42 | logger := log.GetLoggerFromContextOrDefault(cmd) 43 | proxyServers := []string{""} 44 | proxyServers = append(proxyServers, installer.GetProxyServers()...) 45 | 46 | if o.proxy == "" { 47 | if o.proxy, err = selectFromList(proxyServers, 48 | o.v.GetString("proxy-github"), 49 | "Select proxy-github", o.stdio); err != nil { 50 | return 51 | } 52 | } 53 | o.v.Set("proxy-github", o.proxy) 54 | 55 | if o.provider == "" { 56 | if o.provider, err = selectFromList([]string{"github", "gitee"}, o.v.GetString("provider"), 57 | "Select provider", o.stdio); err != nil { 58 | return 59 | } 60 | } 61 | o.v.Set("provider", o.provider) 62 | 63 | var configDir string 64 | fetcher := &installer.DefaultFetcher{} 65 | if configDir, err = fetcher.GetHomeDir(); err == nil { 66 | if err = os.MkdirAll(configDir, 0750); err != nil { 67 | err = fmt.Errorf("failed to create directory: %s, error: %v", configDir, err) 68 | return 69 | } 70 | 71 | configPath := filepath.Join(configDir, ".config", "hd.yaml") 72 | logger.Info("write config into:", configPath) 73 | err = o.v.WriteConfigAs(configPath) 74 | } 75 | return 76 | } 77 | 78 | func selectFromList(items []string, defaultItem, title string, stdio terminal.Stdio) (val string, err error) { 79 | selector := &survey.Select{ 80 | Message: title, 81 | Options: items, 82 | Default: defaultItem, 83 | } 84 | err = survey.AskOne(selector, &val, survey.WithStdio(stdio.In, stdio.Out, stdio.Err)) 85 | return 86 | } 87 | -------------------------------------------------------------------------------- /cmd/setup_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/AlecAivazis/survey/v2/terminal" 8 | expect "github.com/Netflix/go-expect" 9 | pseudotty "github.com/creack/pty" 10 | "github.com/hinshun/vt10x" 11 | "github.com/linuxsuren/http-downloader/pkg/log" 12 | "github.com/spf13/afero" 13 | "github.com/spf13/viper" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func Test_newSetupCommand(t *testing.T) { 18 | t.Run("normal", func(t *testing.T) { 19 | RunTest(t, func(c expectConsole) { 20 | c.ExpectString("Select proxy-github") 21 | // c.Send("99988866") 22 | c.SendLine("") 23 | 24 | c.ExpectString("Select provider") 25 | c.Send("gitee") 26 | c.SendLine("") 27 | c.ExpectEOF() 28 | }, func(tr terminal.Stdio) error { 29 | fs := afero.NewMemMapFs() 30 | v := viper.New() 31 | v.SetFs(fs) 32 | v.Set("provider", "github") 33 | 34 | cmd := newSetupCommand(v, tr) 35 | assert.Equal(t, "setup", cmd.Name()) 36 | 37 | err := cmd.Execute() 38 | assert.Nil(t, err) 39 | // assert.Equal(t, "gh.api.99988866.xyz", v.GetString("proxy-github")) 40 | assert.Equal(t, "gitee", v.GetString("provider")) 41 | return err 42 | }) 43 | }) 44 | 45 | t.Run("test the default value", func(t *testing.T) { 46 | RunTest(t, func(c expectConsole) { 47 | c.ExpectString("Select proxy-github") 48 | c.SendLine("") 49 | 50 | c.ExpectString("Select provider") 51 | c.SendLine("") 52 | c.ExpectEOF() 53 | }, func(tr terminal.Stdio) error { 54 | fs := afero.NewMemMapFs() 55 | v := viper.New() 56 | v.SetFs(fs) 57 | v.Set("provider", "gitee") 58 | v.Set("proxy-github", "gh.api.99988866.xyz") 59 | 60 | cmd := newSetupCommand(v, tr) 61 | assert.Equal(t, "setup", cmd.Name()) 62 | 63 | err := cmd.Execute() 64 | assert.Nil(t, err) 65 | assert.Equal(t, "gh.api.99988866.xyz", v.GetString("proxy-github")) 66 | assert.Equal(t, "gitee", v.GetString("provider")) 67 | return err 68 | }) 69 | }) 70 | 71 | t.Run("setup with given flags", func(t *testing.T) { 72 | RunTest(t, func(c expectConsole) { 73 | }, func(tr terminal.Stdio) error { 74 | fs := afero.NewMemMapFs() 75 | v := viper.New() 76 | v.SetFs(fs) 77 | v.Set("provider", "gitee") 78 | v.Set("proxy-github", "gh.api.99988866.xyz") 79 | 80 | cmd := newSetupCommand(v, tr) 81 | assert.Equal(t, "setup", cmd.Name()) 82 | cmd.SetArgs([]string{"--proxy", "fake.com", "--provider", "fake"}) 83 | cmd.SetContext(log.NewContextWithLogger(context.Background(), 0)) 84 | 85 | err := cmd.Execute() 86 | assert.Nil(t, err) 87 | assert.Equal(t, "fake.com", v.GetString("proxy-github")) 88 | assert.Equal(t, "fake", v.GetString("provider")) 89 | return err 90 | }) 91 | }) 92 | } 93 | 94 | func TestSelectFromList(t *testing.T) { 95 | RunTest(t, func(c expectConsole) { 96 | c.ExpectString("title") 97 | c.SendLine(string(terminal.KeyArrowDown)) 98 | c.SendLine("") 99 | c.ExpectEOF() 100 | }, func(tr terminal.Stdio) error { 101 | val, err := selectFromList([]string{"one", "two", "three"}, "", "title", tr) 102 | assert.Equal(t, "two", val) 103 | return err 104 | }) 105 | } 106 | 107 | type expectConsole interface { 108 | ExpectString(string) 109 | ExpectEOF() 110 | SendLine(string) 111 | Send(string) 112 | } 113 | 114 | func RunTest(t *testing.T, procedure func(expectConsole), test func(terminal.Stdio) error) { 115 | t.Helper() 116 | t.Parallel() 117 | 118 | pty, tty, err := pseudotty.Open() 119 | if err != nil { 120 | t.Fatalf("failed to open pseudotty: %v", err) 121 | } 122 | 123 | term := vt10x.New(vt10x.WithWriter(tty)) 124 | c, err := expect.NewConsole(expect.WithStdin(pty), expect.WithStdout(term), expect.WithCloser(pty, tty)) 125 | if err != nil { 126 | t.Fatalf("failed to create console: %v", err) 127 | } 128 | defer c.Close() 129 | 130 | donec := make(chan struct{}) 131 | go func() { 132 | defer close(donec) 133 | procedure(&consoleWithErrorHandling{console: c, t: t}) 134 | }() 135 | 136 | stdio := terminal.Stdio{In: c.Tty(), Out: c.Tty(), Err: c.Tty()} 137 | if err := test(stdio); err != nil { 138 | t.Error(err) 139 | } 140 | 141 | if err := c.Tty().Close(); err != nil { 142 | t.Errorf("error closing Tty: %v", err) 143 | } 144 | <-donec 145 | } 146 | 147 | type consoleWithErrorHandling struct { 148 | console *expect.Console 149 | t *testing.T 150 | } 151 | 152 | func (c *consoleWithErrorHandling) ExpectString(s string) { 153 | if _, err := c.console.ExpectString(s); err != nil { 154 | c.t.Helper() 155 | c.t.Fatalf("ExpectString(%q) = %v", s, err) 156 | } 157 | } 158 | 159 | func (c *consoleWithErrorHandling) SendLine(s string) { 160 | if _, err := c.console.SendLine(s); err != nil { 161 | c.t.Helper() 162 | c.t.Fatalf("SendLine(%q) = %v", s, err) 163 | } 164 | } 165 | 166 | func (c *consoleWithErrorHandling) Send(s string) { 167 | if _, err := c.console.Send(s); err != nil { 168 | c.t.Helper() 169 | c.t.Fatalf("Send(%q) = %v", s, err) 170 | } 171 | } 172 | 173 | func (c *consoleWithErrorHandling) ExpectEOF() { 174 | if _, err := c.console.ExpectEOF(); err != nil { 175 | c.t.Helper() 176 | c.t.Fatalf("ExpectEOF() = %v", err) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /cmd/testdata/magnet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 12 |
6 | magnet:?xxx 7 | 9 | github 10 |
13 | 14 | -------------------------------------------------------------------------------- /cmd/util.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func getOrDefault(key, def string, data map[string]string) (result string) { 11 | var ok bool 12 | if result, ok = data[key]; !ok { 13 | result = def 14 | } 15 | return 16 | } 17 | 18 | type contextRoundTripper string 19 | 20 | func getRoundTripper(ctx context.Context) (tripper http.RoundTripper) { 21 | if ctx == nil { 22 | return 23 | } 24 | roundTripper := ctx.Value(contextRoundTripper("roundTripper")) 25 | 26 | switch v := roundTripper.(type) { 27 | case *http.Transport: 28 | tripper = v 29 | } 30 | return 31 | } 32 | 33 | // CompletionFunc is the function for command completion 34 | type CompletionFunc func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) 35 | 36 | // ArrayCompletion return a completion which base on an array 37 | func ArrayCompletion(array ...string) CompletionFunc { 38 | return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 39 | return array, cobra.ShellCompDirectiveNoFileComp 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cmd/util_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "github.com/spf13/cobra" 6 | "github.com/stretchr/testify/assert" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func Test_getRoundTripper(t *testing.T) { 13 | type args struct { 14 | ctx context.Context 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | wantTripper http.RoundTripper 20 | }{{ 21 | name: "context is nil", 22 | wantTripper: nil, 23 | }, { 24 | name: "invalid type of RounderTripper in the context", 25 | args: args{ 26 | ctx: context.WithValue(context.TODO(), contextRoundTripper("roundTripper"), "invalid"), 27 | }, 28 | wantTripper: nil, 29 | }, { 30 | name: "valid type of RounderTripper in the context", 31 | args: args{ 32 | ctx: context.WithValue(context.TODO(), contextRoundTripper("roundTripper"), &http.Transport{}), 33 | }, 34 | wantTripper: &http.Transport{}, 35 | }} 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | if gotTripper := getRoundTripper(tt.args.ctx); !reflect.DeepEqual(gotTripper, tt.wantTripper) { 39 | t.Errorf("getRoundTripper() = %v, want %v", gotTripper, tt.wantTripper) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func Test_getOrDefault(t *testing.T) { 46 | type args struct { 47 | key string 48 | def string 49 | data map[string]string 50 | } 51 | tests := []struct { 52 | name string 53 | args args 54 | wantResult string 55 | }{{ 56 | name: "no key exist", 57 | args: args{ 58 | key: "key", 59 | def: "def", 60 | data: map[string]string{}, 61 | }, 62 | wantResult: "def", 63 | }, { 64 | name: "key exist", 65 | args: args{ 66 | key: "key", 67 | def: "def", 68 | data: map[string]string{ 69 | "key": "key", 70 | }, 71 | }, 72 | wantResult: "key", 73 | }} 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | if gotResult := getOrDefault(tt.args.key, tt.args.def, tt.args.data); gotResult != tt.wantResult { 77 | t.Errorf("getOrDefault() = %v, want %v", gotResult, tt.wantResult) 78 | } 79 | }) 80 | } 81 | } 82 | 83 | func TestArrayCompletion(t *testing.T) { 84 | function := ArrayCompletion("a", "b") 85 | assert.NotNil(t, function) 86 | 87 | array, direct := function(nil, nil, "") 88 | assert.Equal(t, []string{"a", "b"}, array) 89 | assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, direct) 90 | } 91 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # https://docs.codecov.com/docs/commit-status 2 | # it's hard to have a coverage for some code lines 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 0.1% 9 | patch: 10 | default: 11 | target: auto 12 | threshold: 0% 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/linuxsuren/http-downloader 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.3.2 7 | github.com/go-git/go-git/v5 v5.10.1 8 | github.com/golang/mock v1.6.0 9 | github.com/google/go-github/v29 v29.0.3 10 | github.com/h2non/gock v1.0.9 11 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec 12 | github.com/linuxsuren/cobra-extension v0.0.16 13 | github.com/mitchellh/go-homedir v1.1.0 14 | github.com/onsi/ginkgo v1.16.5 15 | github.com/onsi/gomega v1.27.10 16 | github.com/spf13/cobra v1.6.1 17 | github.com/spf13/pflag v1.0.5 18 | github.com/spf13/viper v1.9.0 19 | github.com/stretchr/testify v1.8.2 20 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 21 | gopkg.in/yaml.v3 v3.0.1 22 | ) 23 | 24 | require ( 25 | github.com/antonmedv/expr v1.11.1 26 | github.com/creack/pty v1.1.17 27 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 28 | github.com/linuxsuren/go-fake-runtime v0.0.0-20230426144714-1a7a0d160d3f 29 | github.com/schollz/progressbar/v3 v3.13.0 30 | ) 31 | 32 | require ( 33 | dario.cat/mergo v1.0.0 // indirect 34 | github.com/cloudflare/circl v1.3.3 // indirect 35 | github.com/cyphar/filepath-securejoin v0.2.4 // indirect 36 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 37 | github.com/google/go-cmp v0.6.0 // indirect 38 | github.com/mattn/go-runewidth v0.0.14 // indirect 39 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 40 | github.com/pjbgf/sha1cd v0.3.0 // indirect 41 | github.com/prometheus/client_golang v1.11.1 // indirect 42 | github.com/rivo/uniseg v0.4.3 // indirect 43 | github.com/skeema/knownhosts v1.2.1 // indirect 44 | golang.org/x/mod v0.12.0 // indirect 45 | golang.org/x/tools v0.13.0 // indirect 46 | ) 47 | 48 | require ( 49 | github.com/Microsoft/go-winio v0.6.1 // indirect 50 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 51 | github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect 52 | github.com/blang/semver/v4 v4.0.0 53 | github.com/davecgh/go-spew v1.1.1 // indirect 54 | github.com/emirpasic/gods v1.18.1 // indirect 55 | github.com/fsnotify/fsnotify v1.5.1 // indirect 56 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 57 | github.com/go-git/go-billy/v5 v5.5.0 // indirect 58 | github.com/golang/protobuf v1.5.3 // indirect 59 | github.com/google/go-querystring v1.0.0 // indirect 60 | github.com/hashicorp/hcl v1.0.0 // indirect 61 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 62 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 63 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 64 | github.com/kevinburke/ssh_config v1.2.0 // indirect 65 | github.com/magiconair/properties v1.8.5 // indirect 66 | github.com/mattn/go-colorable v0.1.6 // indirect 67 | github.com/mattn/go-isatty v0.0.17 // indirect 68 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 69 | github.com/mitchellh/mapstructure v1.4.2 // indirect 70 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect 71 | github.com/nxadm/tail v1.4.8 // indirect 72 | github.com/pelletier/go-toml v1.9.4 // indirect 73 | github.com/pmezard/go-difflib v1.0.0 // indirect 74 | github.com/sergi/go-diff v1.1.0 // indirect 75 | github.com/spf13/afero v1.6.0 76 | github.com/spf13/cast v1.4.1 // indirect 77 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 78 | github.com/subosito/gotenv v1.2.0 // indirect 79 | github.com/xanzy/ssh-agent v0.3.3 // indirect 80 | golang.org/x/crypto v0.15.0 // indirect 81 | golang.org/x/net v0.18.0 82 | golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect 83 | golang.org/x/sys v0.14.0 // indirect 84 | golang.org/x/term v0.14.0 // indirect 85 | golang.org/x/text v0.14.0 // indirect 86 | google.golang.org/appengine v1.6.7 // indirect 87 | google.golang.org/protobuf v1.28.0 // indirect 88 | gopkg.in/ini.v1 v1.63.2 // indirect 89 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 90 | gopkg.in/warnings.v0 v0.1.2 // indirect 91 | gopkg.in/yaml.v2 v2.4.0 // indirect 92 | ) 93 | -------------------------------------------------------------------------------- /hack/README.md: -------------------------------------------------------------------------------- 1 | Add as a sidecar: 2 | 3 | ```shell 4 | kubectl patch deployment you-app -p'{"spec":{"template":{"spec":{"containers":[{"name":"hd","image":"ghcr.io/linuxsuren/hd","command":["/bin/sh"],"args":["-c","while true; do echo hello; sleep 10;done"]}]}}}}' 5 | ``` 6 | -------------------------------------------------------------------------------- /hack/deploy.yaml: -------------------------------------------------------------------------------- 1 | kind: Deployment 2 | apiVersion: apps/v1 3 | metadata: 4 | name: hd 5 | labels: 6 | app: hd 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: hd 12 | template: 13 | metadata: 14 | creationTimestamp: null 15 | labels: 16 | app: hd 17 | spec: 18 | containers: 19 | - name: hd 20 | image: 'ghcr.io/linuxsuren/hd:v0.0.60' 21 | command: 22 | - /bin/sh 23 | args: 24 | - '-c' 25 | - while true; do echo hello; sleep 10;done 26 | resources: {} 27 | terminationMessagePath: /dev/termination-log 28 | terminationMessagePolicy: File 29 | imagePullPolicy: IfNotPresent 30 | restartPolicy: Always 31 | terminationGracePeriodSeconds: 30 32 | dnsPolicy: ClusterFirst 33 | securityContext: {} 34 | schedulerName: default-scheduler 35 | strategy: 36 | type: RollingUpdate 37 | rollingUpdate: 38 | maxUnavailable: 25% 39 | maxSurge: 25% 40 | revisionHistoryLimit: 10 41 | progressDeadlineSeconds: 600 42 | -------------------------------------------------------------------------------- /hack/init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [[ "$1" == "" ]]; then 4 | echo "please give a appropriate name" 5 | exit 1 6 | fi 7 | 8 | find . -name "*.yaml" -exec sed -i '' s/github-go/"$1"/ {} + 9 | find . -name "*.yml" -exec sed -i '' s/github-go/"$1"/ {} + 10 | find . -name "*.md" -exec sed -i '' s/github-go/"$1"/ {} + 11 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/linuxsuren/http-downloader/cmd" 8 | ) 9 | 10 | func main() { 11 | ctx := context.Background() 12 | if err := cmd.NewRoot(ctx).ExecuteContext(ctx); err != nil { 13 | os.Exit(1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mock/mhttp/roundtripper.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: net/http (interfaces: RoundTripper) 3 | 4 | // Package mhttp is a generated GoMock package. 5 | package mhttp 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | http "net/http" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockRoundTripper is a mock of RoundTripper interface 14 | type MockRoundTripper struct { 15 | ctrl *gomock.Controller 16 | recorder *MockRoundTripperMockRecorder 17 | } 18 | 19 | // MockRoundTripperMockRecorder is the mock recorder for MockRoundTripper 20 | type MockRoundTripperMockRecorder struct { 21 | mock *MockRoundTripper 22 | } 23 | 24 | // NewMockRoundTripper creates a new mock instance 25 | func NewMockRoundTripper(ctrl *gomock.Controller) *MockRoundTripper { 26 | mock := &MockRoundTripper{ctrl: ctrl} 27 | mock.recorder = &MockRoundTripperMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockRoundTripper) EXPECT() *MockRoundTripperMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // RoundTrip mocks base method 37 | func (m *MockRoundTripper) RoundTrip(arg0 *http.Request) (*http.Response, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "RoundTrip", arg0) 40 | ret0, _ := ret[0].(*http.Response) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // RoundTrip indicates an expected call of RoundTrip 46 | func (mr *MockRoundTripperMockRecorder) RoundTrip(arg0 interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RoundTrip", reflect.TypeOf((*MockRoundTripper)(nil).RoundTrip), arg0) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/common/doc.go: -------------------------------------------------------------------------------- 1 | // Package common provides some common functions for the whole project. 2 | package common 3 | -------------------------------------------------------------------------------- /pkg/common/util.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | ) 9 | 10 | // GetOrDefault returns the value or a default value from a map 11 | func GetOrDefault(key, def string, data map[string]string) (result string) { 12 | var ok bool 13 | if result, ok = data[key]; !ok { 14 | result = def 15 | } 16 | return 17 | } 18 | 19 | // GetReplacement returns a string which replace via a map 20 | func GetReplacement(key string, data map[string]string) (result string) { 21 | return GetOrDefault(key, key, data) 22 | } 23 | 24 | // PathExists checks if the target path exist or not 25 | func PathExists(path string) (bool, error) { 26 | _, err := os.Stat(path) 27 | if err == nil { 28 | return true, nil 29 | } 30 | if os.IsNotExist(err) { 31 | return false, nil 32 | } 33 | return false, err 34 | } 35 | 36 | const ( 37 | // PrivateFileMode grants owner to read/write a file. 38 | PrivateFileMode = 0600 39 | // PrivateDirMode grants owner to make/remove files inside the directory. 40 | PrivateDirMode = 0700 41 | ) 42 | 43 | // IsDirWriteable checks if dir is writable by writing and removing a file 44 | // to dir. It returns nil if dir is writable. 45 | func IsDirWriteable(dir string) error { 46 | f := filepath.Join(dir, ".touch") 47 | if err := os.WriteFile(f, []byte(""), PrivateFileMode); err != nil { 48 | return err 49 | } 50 | return os.Remove(f) 51 | } 52 | 53 | // CheckDirPermission checks permission on an existing dir. 54 | // Returns error if dir is empty or exist with a different permission than specified. 55 | func CheckDirPermission(dir string, perm os.FileMode) error { 56 | if !Exist(dir) { 57 | return fmt.Errorf("directory %q empty, cannot check permission", dir) 58 | } 59 | // check the existing permission on the directory 60 | dirInfo, err := os.Stat(dir) 61 | if err != nil { 62 | return err 63 | } 64 | dirMode := dirInfo.Mode().Perm() 65 | if dirMode != perm { 66 | err = fmt.Errorf("directory %q exist, but the permission is %q. The recommended permission is %q to prevent possible unprivileged access to the data", dir, dirInfo.Mode(), os.FileMode(PrivateDirMode)) 67 | return err 68 | } 69 | return nil 70 | } 71 | 72 | // Exist returns true if a file or directory exists. 73 | func Exist(name string) bool { 74 | ok, _ := PathExists(name) 75 | return ok 76 | } 77 | 78 | // ParseVersionNum split version from release or tag 79 | func ParseVersionNum(release string) string { 80 | return regexp.MustCompile(`^.*v`).ReplaceAllString(release, "") 81 | } 82 | 83 | // GetEnvironment retrieves the value of the environment variable named by the key. 84 | // If the environment doesn't exist, we will lookup for alternative environment variables 85 | // until we find an environment. Return empty environment value while no environment variables found. 86 | func GetEnvironment(key string, alternativeKeys ...string) string { 87 | if value, exists := os.LookupEnv(key); exists { 88 | return value 89 | } 90 | for _, alternativeKey := range alternativeKeys { 91 | if value, exists := os.LookupEnv(alternativeKey); exists { 92 | return value 93 | } 94 | } 95 | return "" 96 | } 97 | -------------------------------------------------------------------------------- /pkg/compress/bzip2.go: -------------------------------------------------------------------------------- 1 | package compress 2 | 3 | import ( 4 | "compress/bzip2" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | // Bzip2 implements a compress which based is based on bzip2 13 | type Bzip2 struct { 14 | additionBinaries []string 15 | } 16 | 17 | // NewBzip2 creates an instance of Bzip2 18 | func NewBzip2(additionBinaries []string) *Bzip2 { 19 | return &Bzip2{additionBinaries: additionBinaries} 20 | } 21 | 22 | // make sure Bzip2 implements the interface Compress 23 | var _ Compress = &Bzip2{} 24 | 25 | // ExtractFiles extracts files from a target compress file 26 | func (x *Bzip2) ExtractFiles(sourceFile, targetName string) (err error) { 27 | if sourceFile == "" || targetName == "" { 28 | err = errors.New("source or target filename is empty") 29 | return 30 | } 31 | var f *os.File 32 | if f, err = os.Open(sourceFile); err != nil { 33 | return err 34 | } 35 | defer func() { 36 | _ = f.Close() 37 | }() 38 | 39 | // Create a Bzip2 Reader 40 | r := bzip2.NewReader(f) 41 | 42 | var targetFile *os.File 43 | if targetFile, err = os.OpenFile(fmt.Sprintf("%s/%s", filepath.Dir(sourceFile), targetName), 44 | os.O_CREATE|os.O_RDWR, 0600); err != nil { 45 | return 46 | } 47 | if _, err = io.Copy(targetFile, r); err != nil { 48 | return 49 | } 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /pkg/compress/doc.go: -------------------------------------------------------------------------------- 1 | // Package compress provides the compress and uncompress functions. 2 | package compress 3 | -------------------------------------------------------------------------------- /pkg/compress/gzip.go: -------------------------------------------------------------------------------- 1 | package compress 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path" 11 | ) 12 | 13 | // GZip implements a compress which based is based on gzip 14 | type GZip struct { 15 | additionBinaries []string 16 | } 17 | 18 | // NewGZip creates an instance of GZip 19 | // additionBinaries could be empty or nil 20 | func NewGZip(additionBinaries []string) *GZip { 21 | return &GZip{additionBinaries: additionBinaries} 22 | } 23 | 24 | // make sure GZip implements the interface Compress 25 | var _ Compress = &GZip{} 26 | 27 | // ExtractFiles extracts files from a target compress file 28 | func (c *GZip) ExtractFiles(sourceFile, targetName string) (err error) { 29 | if sourceFile == "" || targetName == "" { 30 | err = errors.New("source or target filename is empty") 31 | return 32 | } 33 | 34 | var f *os.File 35 | var gzf *gzip.Reader 36 | if f, err = os.Open(sourceFile); err != nil { 37 | return 38 | } 39 | defer func() { 40 | _ = f.Close() 41 | }() 42 | 43 | if gzf, err = gzip.NewReader(f); err != nil { 44 | return 45 | } 46 | 47 | err = zipProcess(tar.NewReader(gzf), targetName, sourceFile, c.additionBinaries) 48 | return 49 | } 50 | 51 | func zipProcess(tarReader *tar.Reader, targetName, sourceFile string, additionBinaries []string) (err error) { 52 | var header *tar.Header 53 | var found bool 54 | for { 55 | if header, err = tarReader.Next(); err == io.EOF { 56 | err = nil 57 | break 58 | } else if err != nil { 59 | break 60 | } 61 | name := path.Base(header.Name) 62 | 63 | switch header.Typeflag { 64 | case tar.TypeReg: 65 | if name == targetName { 66 | if err = extraFile(name, targetName, sourceFile, header, tarReader); err == nil { 67 | found = true 68 | } else { 69 | break 70 | } 71 | } else { 72 | for i := range additionBinaries { 73 | addition := additionBinaries[i] 74 | if name != addition { 75 | continue 76 | } 77 | 78 | if err = extraFile(addition, addition, sourceFile, header, tarReader); err != nil { 79 | return 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | if err == nil && !found { 87 | err = fmt.Errorf("cannot find item '%s' from '%s'", targetName, sourceFile) 88 | } 89 | return 90 | } 91 | -------------------------------------------------------------------------------- /pkg/compress/testdata/simple.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinuxSuRen/http-downloader/ec90fa5ea59a1aef39fef78b71e42f7ff32a28bf/pkg/compress/testdata/simple.tar.gz -------------------------------------------------------------------------------- /pkg/compress/testdata/simple.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinuxSuRen/http-downloader/ec90fa5ea59a1aef39fef78b71e42f7ff32a28bf/pkg/compress/testdata/simple.zip -------------------------------------------------------------------------------- /pkg/compress/types.go: -------------------------------------------------------------------------------- 1 | package compress 2 | 3 | import ( 4 | "archive/tar" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // Compress is a common compress interface 12 | type Compress interface { 13 | ExtractFiles(sourceFile, targetName string) error 14 | } 15 | 16 | func extraFile(name, targetName, tarFile string, header *tar.Header, tarReader *tar.Reader) (err error) { 17 | if name != targetName && !strings.HasSuffix(name, "/"+targetName) { 18 | return 19 | } 20 | var targetFile *os.File 21 | if targetFile, err = os.OpenFile(filepath.Join(filepath.Dir(tarFile), targetName), 22 | os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)); err != nil { 23 | return 24 | } 25 | defer func() { 26 | _ = targetFile.Close() 27 | }() 28 | _, err = io.Copy(targetFile, tarReader) 29 | return 30 | } 31 | 32 | // GetCompressor gets the compressor base on file extension 33 | func GetCompressor(extension string, additionBinaries []string) Compress { 34 | // Select the right decompressor based on file type 35 | switch extension { 36 | case ".xz": 37 | return NewXz(additionBinaries) 38 | case ".zip": 39 | return NewZip(additionBinaries) 40 | case ".gz", ".tar.gz", ".tgz": 41 | return NewGZip(additionBinaries) 42 | case ".bz2": 43 | return NewBzip2(additionBinaries) 44 | } 45 | return nil 46 | } 47 | 48 | // IsSupport checks if the desired file extension 49 | func IsSupport(extension string) bool { 50 | return GetCompressor(extension, nil) != nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/compress/types_test.go: -------------------------------------------------------------------------------- 1 | package compress 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "fmt" 7 | "github.com/stretchr/testify/assert" 8 | "os" 9 | "path" 10 | "reflect" 11 | "testing" 12 | ) 13 | 14 | func TestGetCompressor(t *testing.T) { 15 | type args struct { 16 | extension string 17 | additionBinaries []string 18 | } 19 | tests := []struct { 20 | name string 21 | args args 22 | want Compress 23 | }{{ 24 | name: "unknown type", 25 | args: args{extension: ".xdf"}, 26 | want: nil, 27 | }, { 28 | name: ".zip", 29 | args: args{extension: ".zip"}, 30 | want: NewZip(nil), 31 | }, { 32 | name: ".xz", 33 | args: args{extension: ".xz"}, 34 | want: NewXz(nil), 35 | }, { 36 | name: ".tar.gz", 37 | args: args{extension: ".tar.gz"}, 38 | want: NewGZip(nil), 39 | }, { 40 | name: ".bz2", 41 | args: args{extension: ".bz2"}, 42 | want: NewBzip2(nil), 43 | }} 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | if got := GetCompressor(tt.args.extension, tt.args.additionBinaries); !reflect.DeepEqual(got, tt.want) { 47 | t.Errorf("GetCompressor() = %v, want %v", got, tt.want) 48 | } else if got != nil { 49 | err := got.ExtractFiles("", "") 50 | assert.NotNil(t, err) 51 | 52 | // test with a regular file 53 | var f *os.File 54 | if f, err = os.CreateTemp(os.TempDir(), "fake"); err != nil { 55 | return 56 | } 57 | assert.Nil(t, err) 58 | assert.NotNil(t, f) 59 | defer func() { 60 | _ = os.RemoveAll(f.Name()) 61 | }() 62 | 63 | err = got.ExtractFiles(f.Name(), "fake") 64 | assert.NotNil(t, err) 65 | 66 | // try to read a non-exist file 67 | err = got.ExtractFiles(path.Join(os.TempDir(), "fake"), "fake") 68 | assert.NotNil(t, err) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func TestIsSupport(t *testing.T) { 75 | type args struct { 76 | extension string 77 | } 78 | tests := []struct { 79 | name string 80 | args args 81 | want bool 82 | }{{ 83 | name: "supported extension: .tar.gz", 84 | args: args{ 85 | extension: path.Ext("test.tar.gz"), 86 | }, 87 | want: true, 88 | }, { 89 | name: "supported extension: .xz", 90 | args: args{ 91 | extension: path.Ext("test.xz"), 92 | }, 93 | want: true, 94 | }, { 95 | name: "supported extension: .zip", 96 | args: args{ 97 | extension: path.Ext("test.zip"), 98 | }, 99 | want: true, 100 | }, { 101 | name: "supported extension: .gz", 102 | args: args{ 103 | extension: path.Ext("test.gz"), 104 | }, 105 | want: true, 106 | }, { 107 | name: "supported extension: .tgz", 108 | args: args{ 109 | extension: path.Ext("test.tgz"), 110 | }, 111 | want: true, 112 | }, { 113 | name: "supported extension: .bz2", 114 | args: args{ 115 | extension: path.Ext("test.bz2"), 116 | }, 117 | want: true, 118 | }, { 119 | name: "not supported extension: .ab", 120 | args: args{ 121 | extension: path.Ext("test.ab"), 122 | }, 123 | want: false, 124 | }} 125 | for _, tt := range tests { 126 | t.Run(tt.name, func(t *testing.T) { 127 | if got := IsSupport(tt.args.extension); got != tt.want { 128 | t.Errorf("IsSupport() = %v, want %v", got, tt.want) 129 | } 130 | }) 131 | } 132 | } 133 | 134 | func Test_extraFile(t *testing.T) { 135 | type args struct { 136 | name string 137 | targetName string 138 | tarFile string 139 | header *tar.Header 140 | tarReader *tar.Reader 141 | } 142 | tests := []struct { 143 | name string 144 | args args 145 | wantErr assert.ErrorAssertionFunc 146 | }{{ 147 | name: "invalid name", 148 | args: args{ 149 | name: "fake", 150 | targetName: "fake.go", 151 | tarFile: "", 152 | header: nil, 153 | tarReader: nil, 154 | }, 155 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 156 | assert.Nil(t, err) 157 | return true 158 | }, 159 | }, { 160 | name: "normal", 161 | args: args{ 162 | name: "fake", 163 | targetName: "fake", 164 | tarFile: "fake", 165 | header: &tar.Header{Mode: 400}, 166 | tarReader: tar.NewReader(bytes.NewBufferString("fake")), 167 | }, 168 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 169 | assert.Nil(t, err) 170 | return true 171 | }, 172 | }} 173 | for _, tt := range tests { 174 | t.Run(tt.name, func(t *testing.T) { 175 | var file *os.File 176 | var err error 177 | if tt.args.tarFile != "" { 178 | file, err = os.CreateTemp(os.TempDir(), tt.args.tarFile) 179 | assert.Nil(t, err) 180 | assert.NotNil(t, file) 181 | } 182 | 183 | if err != nil || file == nil { 184 | return 185 | } 186 | 187 | defer func() { 188 | _ = os.RemoveAll(tt.args.tarFile) 189 | }() 190 | err = extraFile(tt.args.name, tt.args.targetName, tt.args.tarFile, tt.args.header, tt.args.tarReader) 191 | tt.wantErr(t, err, fmt.Sprintf("extraFile(%v, %v, %v, %v, %v)", tt.args.name, tt.args.targetName, tt.args.tarFile, tt.args.header, tt.args.tarReader)) 192 | }) 193 | } 194 | } 195 | 196 | func TestExtractFiles(t *testing.T) { 197 | t.Run("test .tar.gz", func(t *testing.T) { 198 | compressor := GetCompressor(".tar.gz", []string{"bb", "cc"}) 199 | assert.NotNil(t, compressor) 200 | 201 | err := compressor.ExtractFiles("testdata/simple.tar.gz", "aa") 202 | assert.NoError(t, err) 203 | 204 | assertFileContentEqual(t, "testdata/aa", "aa\n") 205 | assertFileContentEqual(t, "testdata/bb", "bb\n") 206 | assertFileContentEqual(t, "testdata/cc", "cc\n") 207 | }) 208 | 209 | t.Run("test .zip", func(t *testing.T) { 210 | compressor := GetCompressor(".zip", []string{"bb", "cc"}) 211 | assert.NotNil(t, compressor) 212 | 213 | err := compressor.ExtractFiles("testdata/simple.zip", "aa") 214 | assert.NoError(t, err) 215 | 216 | assertFileContentEqual(t, "testdata/aa", "aa\n") 217 | assertFileContentEqual(t, "testdata/bb", "bb\n") 218 | assertFileContentEqual(t, "testdata/cc", "cc\n") 219 | 220 | // invalid parameters 221 | err = compressor.ExtractFiles("", "") 222 | assert.Error(t, err) 223 | }) 224 | } 225 | 226 | func assertFileContentEqual(t *testing.T, filePath string, expectedContent string) { 227 | defer func() { 228 | _ = os.RemoveAll(filePath) 229 | }() 230 | if data, err := os.ReadFile(filePath); err == nil { 231 | assert.Equal(t, expectedContent, string(data)) 232 | } else { 233 | t.Fatalf("not found %q: %v", filePath, err) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /pkg/compress/xz.go: -------------------------------------------------------------------------------- 1 | package compress 2 | 3 | import ( 4 | "archive/tar" 5 | "errors" 6 | "github.com/xi2/xz" 7 | "os" 8 | ) 9 | 10 | // Xz implements a compress which based is based on xz 11 | type Xz struct { 12 | additionBinaries []string 13 | } 14 | 15 | // NewXz creates an instance of Xz 16 | func NewXz(additionBinaries []string) *Xz { 17 | return &Xz{additionBinaries: additionBinaries} 18 | } 19 | 20 | // make sure Xz implements the interface Compress 21 | var _ Compress = &Xz{} 22 | 23 | // ExtractFiles extracts files from a target compress file 24 | func (x *Xz) ExtractFiles(sourceFile, targetName string) (err error) { 25 | if sourceFile == "" || targetName == "" { 26 | err = errors.New("source or target filename is empty") 27 | return 28 | } 29 | var f *os.File 30 | if f, err = os.Open(sourceFile); err != nil { 31 | return err 32 | } 33 | defer func() { 34 | _ = f.Close() 35 | }() 36 | 37 | // Create a xz Reader 38 | r, err := xz.NewReader(f, 0) 39 | if err != nil { 40 | return 41 | } 42 | 43 | err = zipProcess(tar.NewReader(r), targetName, sourceFile, x.additionBinaries) 44 | return 45 | } 46 | -------------------------------------------------------------------------------- /pkg/compress/zip.go: -------------------------------------------------------------------------------- 1 | package compress 2 | 3 | import ( 4 | "archive/zip" 5 | "errors" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | // Zip implements a compress which is base on zip file 12 | type Zip struct { 13 | additionBinaries []string 14 | } 15 | 16 | // NewZip creates an instance of zip 17 | func NewZip(additionBinaries []string) *Zip { 18 | return &Zip{additionBinaries: additionBinaries} 19 | } 20 | 21 | // make sure Zip implements the interface Compress 22 | var _ Compress = &Zip{} 23 | 24 | // ExtractFiles extracts files from a target compress file 25 | func (z *Zip) ExtractFiles(sourceFile, targetName string) (err error) { 26 | if sourceFile == "" || targetName == "" { 27 | err = errors.New("source or target filename is empty") 28 | return 29 | } 30 | 31 | var archive *zip.ReadCloser 32 | if archive, err = zip.OpenReader(sourceFile); err != nil { 33 | return 34 | } 35 | defer func() { 36 | _ = archive.Close() 37 | }() 38 | 39 | z.additionBinaries = append(z.additionBinaries, targetName) 40 | for _, f := range archive.File { 41 | if f.FileInfo().IsDir() { 42 | continue 43 | } 44 | 45 | for _, ff := range z.additionBinaries { 46 | if filepath.Base(f.Name) != ff { 47 | continue 48 | } 49 | 50 | var targetFile *os.File 51 | if targetFile, err = os.OpenFile(filepath.Join(filepath.Dir(sourceFile), ff), 52 | os.O_CREATE|os.O_RDWR, f.Mode()); err != nil { 53 | return 54 | } 55 | 56 | var fileInArchive io.ReadCloser 57 | fileInArchive, err = f.Open() 58 | if err != nil { 59 | return 60 | } 61 | if _, err = io.Copy(targetFile, fileInArchive); err != nil { 62 | return 63 | } 64 | _ = targetFile.Close() 65 | break 66 | } 67 | } 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /pkg/doc.go: -------------------------------------------------------------------------------- 1 | // Package pkg provides the features to interal and external users. 2 | // All CLI related logics should not be in this package. 3 | package pkg 4 | -------------------------------------------------------------------------------- /pkg/error.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | /** 4 | * This file was deprecated, please use the following package instead 5 | * github.com/linuxsuren/http-downloader/pkg/net 6 | */ 7 | 8 | import ( 9 | "fmt" 10 | "github.com/linuxsuren/http-downloader/pkg/net" 11 | ) 12 | 13 | // DownloadError represents the error of HTTP download 14 | type DownloadError net.DownloadError 15 | 16 | // Error print the error message 17 | func (e *DownloadError) Error() string { 18 | return (*net.DownloadError)(e).Error() 19 | } 20 | 21 | // ErrorWrap warps the error if it is not nil 22 | func ErrorWrap(err error, format string, a ...any) error { 23 | if err != nil { 24 | return fmt.Errorf(format, a...) 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/error_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "errors" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestDownloadError_Error(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | e DownloadError 13 | want string 14 | }{{ 15 | name: "normal", 16 | e: DownloadError{Message: "message", StatusCode: 1}, 17 | want: "message: status code: 1", 18 | }} 19 | for _, tt := range tests { 20 | t.Run(tt.name, func(t *testing.T) { 21 | if got := tt.e.Error(); got != tt.want { 22 | t.Errorf("Error() = %v, want %v", got, tt.want) 23 | } 24 | }) 25 | } 26 | } 27 | 28 | func TestErrorWrap(t *testing.T) { 29 | sampleErr := errors.New("sample") 30 | 31 | type arg struct { 32 | err error 33 | format string 34 | args []string 35 | } 36 | tests := []struct { 37 | name string 38 | arg arg 39 | hasErr bool 40 | }{{ 41 | name: "err is not nil", 42 | arg: arg{ 43 | err: sampleErr, 44 | format: "", 45 | }, 46 | hasErr: true, 47 | }, { 48 | name: "error is nil", 49 | arg: arg{}, 50 | hasErr: false, 51 | }} 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | err := ErrorWrap(tt.arg.err, tt.arg.format, tt.arg.args) 55 | assert.Equal(t, tt.hasErr, err != nil) 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/http.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | /** 4 | * This file was deprecated, please use the following package instead 5 | * github.com/linuxsuren/http-downloader/pkg/net 6 | */ 7 | 8 | import ( 9 | "github.com/linuxsuren/http-downloader/pkg/net" 10 | ) 11 | 12 | const ( 13 | // ContentType is for the http header of content type 14 | ContentType = net.ContentType 15 | // ApplicationForm is for the form submit 16 | ApplicationForm = net.ApplicationForm 17 | ) 18 | 19 | // HTTPDownloader is the downloader for http request 20 | type HTTPDownloader net.HTTPDownloader 21 | 22 | // DownloadFile download a file with the progress 23 | // deprecated 24 | func (h *HTTPDownloader) DownloadFile() error { 25 | return (*net.HTTPDownloader)(h).DownloadFile() 26 | } 27 | 28 | // DownloadFileWithMultipleThread downloads the files with multiple threads 29 | func DownloadFileWithMultipleThread(targetURL, targetFilePath string, thread int, showProgress bool) (err error) { 30 | return net.DownloadFileWithMultipleThreadKeepParts(targetURL, targetFilePath, thread, false, showProgress) 31 | } 32 | 33 | // DownloadFileWithMultipleThreadKeepParts downloads the files with multiple threads 34 | // deprecated 35 | func DownloadFileWithMultipleThreadKeepParts(targetURL, targetFilePath string, thread int, keepParts, showProgress bool) (err error) { 36 | return net.DownloadFileWithMultipleThreadKeepParts(targetURL, targetFilePath, thread, keepParts, showProgress) 37 | } 38 | 39 | // DownloadWithContinue downloads the files continuously 40 | // deprecated 41 | func DownloadWithContinue(targetURL, output string, index, continueAt, end int64, showProgress bool) (err error) { 42 | downloader := &net.ContinueDownloader{} 43 | return downloader.DownloadWithContinue(targetURL, output, index, continueAt, end, showProgress) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/installer/cmd.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | func (o *Installer) runCommandList(cmds []CmdWithArgs) (err error) { 4 | for i := range cmds { 5 | cmd := cmds[i] 6 | if err = o.Execer.RunCommand(cmd.Cmd, cmd.Args...); err != nil { 7 | return 8 | } 9 | } 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /pkg/installer/cmd_test.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRunCommandList(t *testing.T) { 12 | i := &Installer{ 13 | Execer: &fakeruntime.FakeExecer{}, 14 | } 15 | assert.Nil(t, i.runCommandList(nil)) 16 | assert.Nil(t, i.runCommandList([]CmdWithArgs{{ 17 | Cmd: "ls", 18 | }})) 19 | 20 | errInstaller := &Installer{ 21 | Execer: fakeruntime.FakeExecer{ 22 | ExpectError: errors.New("error"), 23 | }, 24 | } 25 | assert.NotNil(t, errInstaller.runCommandList([]CmdWithArgs{{ 26 | Cmd: "ls", 27 | }})) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/installer/doc.go: -------------------------------------------------------------------------------- 1 | // Package installer is the core part of the software package installation. 2 | package installer 3 | -------------------------------------------------------------------------------- /pkg/installer/fetch_test.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "context" 5 | "os/user" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGetConfigDir(t *testing.T) { 13 | u, err := user.Current() 14 | assert.Nil(t, err) 15 | 16 | var fetcher Fetcher 17 | fetcher = &DefaultFetcher{} 18 | dir, err := fetcher.GetConfigDir() 19 | assert.Nil(t, err) 20 | assert.Equal(t, filepath.Join(u.HomeDir, ".config", "hd-home"), dir) 21 | fetcher.SetContext(context.TODO()) 22 | 23 | // test the fake fetcher 24 | fetcher = &FakeFetcher{ConfigDir: "fake"} 25 | dir, err = fetcher.GetConfigDir() 26 | assert.Nil(t, err) 27 | assert.Equal(t, "fake", dir) 28 | err = fetcher.FetchLatestRepo("", "", nil) 29 | assert.Nil(t, err) 30 | fetcher.SetContext(context.TODO()) 31 | 32 | dir, err = fetcher.GetHomeDir() 33 | assert.Equal(t, "", dir) 34 | assert.Nil(t, err) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/installer/process.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "strings" 10 | 11 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 12 | 13 | "github.com/linuxsuren/http-downloader/pkg/common" 14 | "github.com/linuxsuren/http-downloader/pkg/compress" 15 | ) 16 | 17 | // Install installs a package 18 | func (o *Installer) Install() (err error) { 19 | if o.Execer.OS() == "windows" { 20 | o.Name = fmt.Sprintf("%s.exe", o.Name) 21 | } 22 | targetBinary := o.Name 23 | if o.Package != nil && o.Package.TargetBinary != "" { 24 | // this is the desired binary file 25 | targetBinary = o.Package.TargetBinary 26 | } 27 | 28 | var source string 29 | var target string 30 | tarFile := o.Output 31 | if o.Tar { 32 | if err = o.extractFiles(tarFile, o.Name); err == nil { 33 | source = fmt.Sprintf("%s/%s", filepath.Dir(tarFile), o.Name) 34 | target = path.Join(o.TargetDirectory, targetBinary) 35 | } else { 36 | err = fmt.Errorf("cannot extract %s from tar file, error: %v", tarFile, err) 37 | } 38 | } else { 39 | source = o.Source 40 | target = path.Join(o.TargetDirectory, targetBinary) 41 | } 42 | 43 | if err == nil { 44 | if o.Package != nil && o.Package.PreInstalls != nil { 45 | if err = o.runCommandList(o.Package.PreInstalls); err != nil { 46 | return 47 | } 48 | } 49 | 50 | if o.Package != nil && o.Package.Installation != nil { 51 | err = o.Execer.RunCommand(o.Package.Installation.Cmd, o.Package.Installation.Args...) 52 | } else { 53 | if err = o.OverWriteBinary(source, target); err != nil { 54 | return 55 | } 56 | 57 | for i := range o.AdditionBinaries { 58 | addition := o.AdditionBinaries[i] 59 | if err = o.OverWriteBinary(addition, path.Join(o.TargetDirectory, filepath.Base(addition))); err != nil { 60 | return 61 | } 62 | } 63 | } 64 | 65 | if o.Package != nil { 66 | for i := range o.Package.DefaultConfigFile { 67 | configFile := o.Package.DefaultConfigFile[i] 68 | configFilePath := configFile.Path 69 | configDir := filepath.Dir(configFilePath) 70 | 71 | if configFile.OS == o.Execer.OS() { 72 | if err = os.MkdirAll(configDir, 0750); err != nil { 73 | if strings.Contains(err.Error(), "permission denied") { 74 | err = o.Execer.RunCommandWithSudo("mkdir", "-p", configDir) 75 | } 76 | 77 | if err != nil { 78 | err = fmt.Errorf("cannot create config dir: %s, error: %v", configDir, err) 79 | return 80 | } 81 | } 82 | 83 | if err = os.WriteFile(configFilePath, []byte(configFile.Content), 0622); err != nil { 84 | if strings.Contains(err.Error(), "permission denied") { 85 | if err = o.Execer.RunCommandWithSudo("touch", configFilePath); err == nil { 86 | err = o.Execer.RunCommandWithSudo("chmod", "+w", configFilePath) 87 | } 88 | } 89 | 90 | if err != nil { 91 | err = fmt.Errorf("cannot write config file: %s, error: %v", configFilePath, err) 92 | return 93 | } 94 | } 95 | 96 | fmt.Printf("config file [%s] is ready.\n", configFilePath) 97 | } 98 | } 99 | } 100 | 101 | if err == nil && o.Package != nil && o.Package.PostInstalls != nil { 102 | err = o.runCommandList(o.Package.PostInstalls) 103 | } 104 | 105 | if err == nil && o.Package != nil && o.Package.TestInstalls != nil { 106 | err = o.runCommandList(o.Package.TestInstalls) 107 | } 108 | 109 | if err == nil && o.CleanPackage { 110 | if cleanErr := os.RemoveAll(tarFile); cleanErr != nil { 111 | fmt.Println("cannot remove file", tarFile, ", error:", cleanErr) 112 | } 113 | } 114 | } 115 | return 116 | } 117 | 118 | // OverWriteBinary install a binary file 119 | func (o *Installer) OverWriteBinary(sourceFile, targetPath string) (err error) { 120 | fmt.Println("install", sourceFile, "to", targetPath) 121 | switch o.Execer.OS() { 122 | case fakeruntime.OSLinux, fakeruntime.OSDarwin: 123 | if err = o.Execer.RunCommand("chmod", "u+x", sourceFile); err != nil { 124 | return 125 | } 126 | 127 | if common.IsDirWriteable(path.Dir(targetPath)) != nil { 128 | if err = o.Execer.RunCommandWithSudo("rm", "-rf", targetPath); err != nil { 129 | return 130 | } 131 | } else { 132 | if err = o.Execer.RunCommand("rm", "-rf", targetPath); err != nil { 133 | return 134 | } 135 | } 136 | 137 | if common.IsDirWriteable(path.Dir(targetPath)) != nil { 138 | err = o.Execer.RunCommandWithSudo("mv", sourceFile, targetPath) 139 | } else { 140 | err = o.Execer.RunCommand("mv", sourceFile, targetPath) 141 | } 142 | default: 143 | sourceF, sourceE := os.Open(sourceFile) 144 | targetF, targetE := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, 0600) 145 | if sourceE != nil || targetE != nil { 146 | err = fmt.Errorf("failed to open source file: %v, or target file: %v", sourceE, targetE) 147 | return 148 | } 149 | 150 | if _, err = io.Copy(targetF, sourceF); err != nil { 151 | err = fmt.Errorf("cannot copy %s from %s to %v, error: %v", o.Name, sourceFile, targetPath, err) 152 | } else { 153 | _ = os.RemoveAll(sourceFile) 154 | } 155 | } 156 | return 157 | } 158 | 159 | func (o *Installer) extractFiles(tarFile, targetName string) (err error) { 160 | compressor := compress.GetCompressor(path.Ext(tarFile), o.AdditionBinaries) 161 | if compressor == nil { 162 | err = fmt.Errorf("no compressor support for %s", tarFile) 163 | } else { 164 | err = compressor.ExtractFiles(tarFile, targetName) 165 | } 166 | return 167 | } 168 | -------------------------------------------------------------------------------- /pkg/installer/process_test.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestInstallerExtractFiles(t *testing.T) { 13 | installer := &Installer{} 14 | 15 | assert.NotNil(t, installer.extractFiles("fake.fake", "")) 16 | assert.NotNil(t, installer.extractFiles("a.tar.gz", "")) 17 | } 18 | 19 | func TestOverwriteBinary(t *testing.T) { 20 | installer := &Installer{ 21 | Execer: &fakeruntime.FakeExecer{}, 22 | } 23 | 24 | sourceFile := path.Join(os.TempDir(), "fake-1") 25 | targetFile := path.Join(os.TempDir(), "fake-2") 26 | 27 | _ = os.WriteFile(sourceFile, []byte("fake"), 0600) 28 | 29 | defer func() { 30 | _ = os.RemoveAll(sourceFile) 31 | }() 32 | defer func() { 33 | _ = os.RemoveAll(targetFile) 34 | }() 35 | 36 | assert.Nil(t, installer.OverWriteBinary(sourceFile, targetFile)) 37 | } 38 | 39 | func TestInstall(t *testing.T) { 40 | tests := []struct { 41 | name string 42 | installer *Installer 43 | hasErr bool 44 | }{{ 45 | name: "empty", 46 | installer: &Installer{ 47 | Execer: fakeruntime.FakeExecer{}, 48 | }, 49 | hasErr: true, 50 | }, { 51 | name: "fake linux", 52 | installer: &Installer{ 53 | Execer: fakeruntime.FakeExecer{ 54 | ExpectOS: fakeruntime.OSLinux, 55 | }, 56 | }, 57 | hasErr: false, 58 | }} 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | err := tt.installer.Install() 62 | if tt.hasErr { 63 | assert.NotNil(t, err) 64 | } else { 65 | assert.Nil(t, err) 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/installer/types.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // PackagingFormat is used for containing config depending on machine 10 | type PackagingFormat struct { 11 | Windows string `yaml:"windows"` 12 | Linux string `yaml:"linux"` 13 | Format string 14 | } 15 | 16 | // String returns a pretty string 17 | func (f PackagingFormat) String() string { 18 | return fmt.Sprintf("Windows: %s, Linux: %s, Format: %s", f.Windows, f.Linux, f.Format) 19 | } 20 | 21 | // HDConfig is the config of http-downloader 22 | type HDConfig struct { 23 | Name string `yaml:"name"` 24 | Categories []string `yaml:"categories"` 25 | Filename string `yaml:"filename"` 26 | FormatOverrides PackagingFormat `yaml:"formatOverrides"` 27 | Binary string `yaml:"binary"` 28 | TargetBinary string `yaml:"targetBinary"` 29 | TargetDirectory string `yaml:"targetDirectory"` 30 | AdditionBinaries []string `yaml:"additionBinaries"` 31 | FromSource bool `yaml:"fromSource"` 32 | URL string `yaml:"url"` 33 | Tar string `yaml:"tar"` 34 | LatestVersion string `yaml:"latestVersion"` 35 | SupportOS []string `yaml:"supportOS"` 36 | SupportArch []string `yaml:"supportArch"` 37 | Replacements map[string]string `yaml:"replacements"` 38 | Requirements []string `yaml:"requirements"` 39 | Installation *CmdWithArgs `yaml:"installation"` 40 | DefaultConfigFile []ConfigFile `yaml:"defaultConfigFiles"` 41 | PreInstalls []CmdWithArgs `yaml:"preInstalls"` 42 | PostInstalls []CmdWithArgs `yaml:"postInstalls"` 43 | TestInstalls []CmdWithArgs `yaml:"testInstalls"` 44 | Version string `yaml:"version"` 45 | VersionCmd string `yaml:"versionCmd"` 46 | 47 | Org, Repo string 48 | } 49 | 50 | // ConfigFile represents a config file 51 | type ConfigFile struct { 52 | OS string `yaml:"os"` 53 | Path string `yaml:"path"` 54 | Content string `yaml:"content"` 55 | } 56 | 57 | // CmdWithArgs is a command with arguments 58 | type CmdWithArgs struct { 59 | Cmd string `yaml:"cmd"` 60 | Args []string `yaml:"args"` 61 | } 62 | 63 | // HDPackage represents a package of http-downloader 64 | type HDPackage struct { 65 | Name string 66 | Version string // e.g. v1.0.1 67 | VersionNum string // e.g. 1.0.1 68 | OS string // e.g. linux, darwin 69 | Arch string // e.g. amd64 70 | AdditionBinaries []string 71 | } 72 | 73 | // Installer is a tool to install a package 74 | type Installer struct { 75 | Package *HDConfig 76 | Tar bool 77 | Output string 78 | TargetDirectory string 79 | Source string 80 | Name string 81 | CleanPackage bool 82 | Provider string 83 | OS string 84 | Arch string 85 | Fetch bool 86 | AdditionBinaries []string 87 | 88 | Org string 89 | Repo string 90 | ProxyGitHub string 91 | 92 | Execer fakeruntime.Execer 93 | } 94 | -------------------------------------------------------------------------------- /pkg/log/doc.go: -------------------------------------------------------------------------------- 1 | // Package log provides the core logger functions. 2 | // Basically, this is a wrapper of log.Logger 3 | package log 4 | -------------------------------------------------------------------------------- /pkg/log/level.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "io" 6 | syslog "log" 7 | ) 8 | 9 | // LevelLog is the wrapper of built-in log.Logger 10 | type LevelLog struct { 11 | *syslog.Logger 12 | level int 13 | } 14 | 15 | // Info prints the info level message. 16 | // Info level means the level >= 3 17 | func (l *LevelLog) Info(v ...any) { 18 | if l.level >= 3 { 19 | l.Println(v...) 20 | } 21 | } 22 | 23 | // Debug prints the debug level message. 24 | // Debug level means the level >= 7 25 | func (l *LevelLog) Debug(v ...any) { 26 | if l.level >= 7 { 27 | l.Println(v...) 28 | } 29 | } 30 | 31 | // SetLevel sets the level of logger 32 | func (l *LevelLog) SetLevel(level int) *LevelLog { 33 | l.level = level 34 | return l 35 | } 36 | 37 | // GetLevel returns the level of logger 38 | func (l *LevelLog) GetLevel() int { 39 | return l.level 40 | } 41 | 42 | // SetOutput sets the output destination for the logger. 43 | func (l *LevelLog) SetOutput(writer io.Writer) *LevelLog { 44 | l.Logger.SetOutput(writer) 45 | return l 46 | } 47 | 48 | // LoggerContext used to get and set context value 49 | type LoggerContext string 50 | 51 | // LoggerContextKey is the key of context for get/set Logger 52 | const LoggerContextKey = LoggerContext("LoggerContext") 53 | 54 | // ContextAware is the interface for getting context.Context 55 | type ContextAware interface { 56 | // Context returns the instance of context.Context 57 | Context() context.Context 58 | } 59 | 60 | // GetLoggerFromContextOrDefault returns a Logger instance from context, 61 | // or a default instance if no Logger in the context 62 | func GetLoggerFromContextOrDefault(aware ContextAware) (logger *LevelLog) { 63 | var ok bool 64 | if aware.Context() != nil { 65 | val := aware.Context().Value(LoggerContextKey) 66 | logger, ok = val.(*LevelLog) 67 | } 68 | 69 | if !ok { 70 | logger = GetLogger() 71 | } 72 | return 73 | } 74 | 75 | // GetLogger returns an instance of Logger 76 | func GetLogger() *LevelLog { 77 | return &LevelLog{ 78 | Logger: syslog.Default(), 79 | level: 3, 80 | } 81 | } 82 | 83 | // NewContextWithLogger returns a new context with given logger level 84 | func NewContextWithLogger(ctx context.Context, level int) context.Context { 85 | logger := GetLogger().SetLevel(level) 86 | return context.WithValue(ctx, LoggerContextKey, logger) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/log/level_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "testing" 7 | 8 | "github.com/linuxsuren/http-downloader/pkg/log" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestLogger(t *testing.T) { 13 | t.Run("default level", func(t *testing.T) { 14 | logger := log.GetLoggerFromContextOrDefault(&fakeContextAwareObj{}) 15 | assert.Equal(t, 3, logger.GetLevel()) 16 | 17 | logger = log.GetLoggerFromContextOrDefault(&fakeContextAwareObj{ctx: context.Background()}) 18 | assert.Equal(t, 3, logger.GetLevel()) 19 | 20 | ctx := log.NewContextWithLogger(context.Background(), 5) 21 | logger = log.GetLoggerFromContextOrDefault(&fakeContextAwareObj{ctx: ctx}) 22 | assert.Equal(t, 5, logger.GetLevel()) 23 | }) 24 | 25 | t.Run("print in different level", func(t *testing.T) { 26 | buf := new(bytes.Buffer) 27 | logger := log.GetLogger().SetOutput(buf) 28 | 29 | logger.Debug("debug") 30 | logger.Info("info") 31 | 32 | assert.Contains(t, buf.String(), "info") 33 | 34 | logger.SetLevel(7) 35 | logger.Debug("debug") 36 | assert.Contains(t, buf.String(), "debug") 37 | }) 38 | } 39 | 40 | type fakeContextAwareObj struct { 41 | ctx context.Context 42 | } 43 | 44 | func (f *fakeContextAwareObj) Context() context.Context { 45 | return f.ctx 46 | } 47 | -------------------------------------------------------------------------------- /pkg/net/doc.go: -------------------------------------------------------------------------------- 1 | // Package net provides the core download functions 2 | package net 3 | -------------------------------------------------------------------------------- /pkg/net/error.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import "fmt" 4 | 5 | // DownloadError represents the error of HTTP download 6 | type DownloadError struct { 7 | StatusCode int 8 | Message string 9 | } 10 | 11 | // Error print the error message 12 | func (e *DownloadError) Error() string { 13 | return fmt.Sprintf("%s: status code: %d", e.Message, e.StatusCode) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/net/error_test.go: -------------------------------------------------------------------------------- 1 | package net_test 2 | 3 | import ( 4 | "github.com/linuxsuren/http-downloader/pkg/net" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestError(t *testing.T) { 10 | err := net.DownloadError{ 11 | Message: "message", 12 | StatusCode: 200, 13 | } 14 | assert.Contains(t, err.Error(), "message") 15 | assert.Contains(t, err.Error(), "200") 16 | } 17 | -------------------------------------------------------------------------------- /pkg/net/fake.go: -------------------------------------------------------------------------------- 1 | // Package net provides net related functions 2 | package net 3 | 4 | import "fmt" 5 | 6 | // FakeReader is a fake reader for the test purpose 7 | type FakeReader struct { 8 | ExpectErr error 9 | } 10 | 11 | // Read is a fake method 12 | func (e *FakeReader) Read(p []byte) (n int, err error) { 13 | err = e.ExpectErr 14 | fmt.Println(err, "fake") 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /pkg/net/fake_test.go: -------------------------------------------------------------------------------- 1 | package net_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/linuxsuren/http-downloader/pkg/net" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestFakeReader(t *testing.T) { 12 | reader := &net.FakeReader{ 13 | ExpectErr: errors.New("error"), 14 | } 15 | _, err := reader.Read(nil) 16 | assert.NotNil(t, err) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/net/progress.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "sync" 8 | 9 | "github.com/k0kubun/go-ansi" 10 | "github.com/schollz/progressbar/v3" 11 | ) 12 | 13 | // ProgressIndicator hold the progress of io operation 14 | type ProgressIndicator struct { 15 | Writer io.Writer 16 | Reader io.Reader 17 | Title string 18 | 19 | // bytes.Buffer 20 | Total float64 21 | line int 22 | bar *progressbar.ProgressBar 23 | } 24 | 25 | var line int = 0 26 | var currentLine int = 0 27 | var guard sync.Mutex = sync.Mutex{} 28 | 29 | // GetCurrentLine returns the current line 30 | func GetCurrentLine() int { 31 | return currentLine 32 | } 33 | 34 | // Init set the default value for progress indicator 35 | func (i *ProgressIndicator) Init() { 36 | i.line = line 37 | line++ 38 | i.bar = progressbar.NewOptions64(int64(i.Total), 39 | progressbar.OptionSetWriter(ansi.NewAnsiStdout()), 40 | progressbar.OptionEnableColorCodes(true), 41 | progressbar.OptionShowBytes(true), 42 | progressbar.OptionSetWidth(10), 43 | progressbar.OptionFullWidth(), 44 | progressbar.OptionSetDescription(fmt.Sprintf("[cyan][reset] %s", i.Title)), 45 | progressbar.OptionSetTheme(progressbar.Theme{ 46 | Saucer: "[green]=[reset]", 47 | SaucerHead: "[green]>[reset]", 48 | SaucerPadding: " ", 49 | BarStart: "[", 50 | BarEnd: "]", 51 | })) 52 | } 53 | 54 | // Close shutdowns the ui process 55 | func (i ProgressIndicator) Close() { 56 | _ = i.bar.Close() 57 | } 58 | 59 | // Write writes the progress 60 | // See also https://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements 61 | func (i *ProgressIndicator) Write(p []byte) (n int, err error) { 62 | guard.Lock() 63 | defer guard.Unlock() 64 | bias := currentLine - i.line 65 | currentLine = i.line 66 | if bias > 0 { 67 | // move up 68 | fmt.Fprintf(os.Stdout, "\r\033[%dA", bias) 69 | } else if bias < 0 { 70 | // move down 71 | fmt.Fprintf(os.Stdout, "\r\033[%dB", -bias) 72 | } 73 | n, err = io.MultiWriter(i.Writer, i.bar).Write(p) 74 | return 75 | } 76 | 77 | // Read reads the progress 78 | func (i *ProgressIndicator) Read(p []byte) (n int, err error) { 79 | n, err = io.MultiReader(i.Reader, i.bar).Read(p) 80 | return 81 | } 82 | -------------------------------------------------------------------------------- /pkg/net/retry_client.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "strings" 7 | ) 8 | 9 | // RetryClient is the wrap of http.Client 10 | type RetryClient struct { 11 | http.Client 12 | MaxAttempts int 13 | currentAttempts int 14 | } 15 | 16 | // NewRetryClient creates the instance of RetryClient 17 | func NewRetryClient(client http.Client) *RetryClient { 18 | return &RetryClient{ 19 | Client: client, 20 | MaxAttempts: 3, 21 | } 22 | } 23 | 24 | // Do is the wrap of http.Client.Do 25 | func (c *RetryClient) Do(req *http.Request) (rsp *http.Response, err error) { 26 | rsp, err = c.Client.Do(req) 27 | // fmt.Println("client error", err, c.Client.Timeout, reflect.TypeOf(err)) 28 | 29 | if _, ok := err.(*url.Error); ok && !strings.Contains(err.Error(), "context canceled") { 30 | // fmt.Println("retry", c.currentAttempts, c.MaxAttempts) 31 | if c.currentAttempts < c.MaxAttempts { 32 | c.currentAttempts++ 33 | // fmt.Println("try", c.currentAttempts) 34 | return c.Do(req) 35 | } 36 | } 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /pkg/net/retry_client_test.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/golang/mock/gomock" 12 | "github.com/linuxsuren/http-downloader/mock/mhttp" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | const fakeURL = "http://fake" 17 | 18 | func TestRetry(t *testing.T) { 19 | ctrl := gomock.NewController(t) 20 | roundTripper := mhttp.NewMockRoundTripper(ctrl) 21 | 22 | client := &RetryClient{ 23 | Client: http.Client{ 24 | Transport: roundTripper, 25 | }, 26 | MaxAttempts: 3, 27 | } 28 | 29 | mockRequest, _ := http.NewRequest(http.MethodGet, fakeURL, nil) 30 | mockResponse := &http.Response{ 31 | StatusCode: http.StatusOK, 32 | Proto: "HTTP/1.1", 33 | Request: mockRequest, 34 | Body: io.NopCloser(bytes.NewBufferString("responseBody")), 35 | } 36 | roundTripper.EXPECT(). 37 | RoundTrip(mockRequest).Return(mockResponse, nil) 38 | 39 | request, _ := http.NewRequest(http.MethodGet, fakeURL, nil) 40 | response, err := client.Do(request) 41 | fmt.Println(reflect.TypeOf(err)) 42 | assert.Nil(t, err) 43 | assert.NotNil(t, response) 44 | assert.Equal(t, http.StatusOK, response.StatusCode) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/net/setup_test.go: -------------------------------------------------------------------------------- 1 | package net_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo/reporters" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestUtils(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | junitReporter := reporters.NewJUnitReporter("test-net.xml") 15 | RunSpecsWithDefaultAndCustomReporters(t, "util", []Reporter{junitReporter}) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/os/apk/common.go: -------------------------------------------------------------------------------- 1 | package apk 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | const ( 10 | // Tool is the tool name of apk 11 | Tool = "apk" 12 | ) 13 | 14 | // CommonInstaller is the installer of a common apk 15 | type CommonInstaller struct { 16 | Name string 17 | Execer fakeruntime.Execer 18 | } 19 | 20 | // Available check if support current platform 21 | func (d *CommonInstaller) Available() (ok bool) { 22 | if d.Execer.OS() == fakeruntime.OSLinux { 23 | _, err := d.Execer.LookPath(Tool) 24 | ok = err == nil 25 | } 26 | return 27 | } 28 | 29 | // Install installs the target package 30 | func (d *CommonInstaller) Install() (err error) { 31 | err = d.Execer.RunCommand(Tool, "add", d.Name) 32 | return 33 | } 34 | 35 | // Uninstall uninstalls the target package 36 | func (d *CommonInstaller) Uninstall() (err error) { 37 | err = d.Execer.RunCommand(Tool, "del", d.Name) 38 | return 39 | } 40 | 41 | // WaitForStart waits for the service be started 42 | func (d *CommonInstaller) WaitForStart() (ok bool, err error) { 43 | ok = true 44 | return 45 | } 46 | 47 | // Start starts the target service 48 | func (d *CommonInstaller) Start() error { 49 | fmt.Println("not supported yet") 50 | return nil 51 | } 52 | 53 | // Stop stops the target service 54 | func (d *CommonInstaller) Stop() error { 55 | fmt.Println("not supported yet") 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/os/apk/common_test.go: -------------------------------------------------------------------------------- 1 | package apk 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCommon(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | installer CommonInstaller 15 | expectAvailable bool 16 | hasErr bool 17 | }{{ 18 | name: "normal", 19 | installer: CommonInstaller{ 20 | Execer: fakeruntime.FakeExecer{ 21 | ExpectError: nil, 22 | ExpectOutput: "", 23 | ExpectOS: "linux", 24 | ExpectArch: "amd64", 25 | }, 26 | }, 27 | expectAvailable: true, 28 | hasErr: false, 29 | }, { 30 | name: "not is linux", 31 | installer: CommonInstaller{ 32 | Execer: fakeruntime.FakeExecer{ExpectOS: "darwin"}, 33 | }, 34 | expectAvailable: false, 35 | hasErr: false, 36 | }, { 37 | name: "command not found", 38 | installer: CommonInstaller{ 39 | Execer: fakeruntime.FakeExecer{ExpectError: errors.New("error")}, 40 | }, 41 | expectAvailable: false, 42 | hasErr: true, 43 | }} 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | assert.Equal(t, tt.expectAvailable, tt.installer.Available()) 47 | assert.Nil(t, tt.installer.Start()) 48 | assert.Nil(t, tt.installer.Stop()) 49 | 50 | ok, err := tt.installer.WaitForStart() 51 | assert.True(t, ok) 52 | assert.Nil(t, err) 53 | assert.Equal(t, tt.hasErr, tt.installer.Install() != nil) 54 | assert.Equal(t, tt.hasErr, tt.installer.Uninstall() != nil) 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/os/apk/doc.go: -------------------------------------------------------------------------------- 1 | // Package apk provides the common functions to install a package via apk. 2 | // See also https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper. 3 | package apk 4 | -------------------------------------------------------------------------------- /pkg/os/apt/asciinema.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // asciinemaInstallerInUbuntu is the installer of asciinema in CentOS 10 | type asciinemaInstallerInUbuntu struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *asciinemaInstallerInUbuntu) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("apt-get") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the asciinema 24 | func (d *asciinemaInstallerInUbuntu) Install() (err error) { 25 | if err = d.Execer.RunCommand("apt-get", "update", "-y"); err != nil { 26 | return 27 | } 28 | if err = d.Execer.RunCommand("apt-get", "install", "-y", 29 | "asciinema"); err != nil { 30 | return 31 | } 32 | return 33 | } 34 | 35 | // Uninstall uninstalls the asciinema 36 | func (d *asciinemaInstallerInUbuntu) Uninstall() (err error) { 37 | err = d.Execer.RunCommand("apt-get", "remove", "-y", 38 | "asciinema") 39 | return 40 | } 41 | 42 | // WaitForStart waits for the service be started 43 | func (d *asciinemaInstallerInUbuntu) WaitForStart() (ok bool, err error) { 44 | ok = true 45 | return 46 | } 47 | 48 | // Start starts the asciinema service 49 | func (d *asciinemaInstallerInUbuntu) Start() error { 50 | fmt.Println("not supported yet") 51 | return nil 52 | } 53 | 54 | // Stop stops the asciinema service 55 | func (d *asciinemaInstallerInUbuntu) Stop() error { 56 | fmt.Println("not supported yet") 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/os/apt/bash-completion.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // bashCompletionInstallerInUbuntu is the installer of bashCompletion in CentOS 10 | type bashCompletionInstallerInUbuntu struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *bashCompletionInstallerInUbuntu) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("apt-get") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the bashCompletion 24 | func (d *bashCompletionInstallerInUbuntu) Install() (err error) { 25 | if err = d.Execer.RunCommand("apt-get", "update", "-y"); err != nil { 26 | return 27 | } 28 | if err = d.Execer.RunCommand("apt-get", "install", "-y", 29 | "bash-completion"); err != nil { 30 | return 31 | } 32 | return 33 | } 34 | 35 | // Uninstall uninstalls the bashCompletion 36 | func (d *bashCompletionInstallerInUbuntu) Uninstall() (err error) { 37 | err = d.Execer.RunCommand("apt-get", "remove", "-y", 38 | "bash-completion") 39 | return 40 | } 41 | 42 | // WaitForStart waits for the service be started 43 | func (d *bashCompletionInstallerInUbuntu) WaitForStart() (ok bool, err error) { 44 | ok = true 45 | return 46 | } 47 | 48 | // Start starts the bashCompletion service 49 | func (d *bashCompletionInstallerInUbuntu) Start() error { 50 | fmt.Println("not supported yet") 51 | return nil 52 | } 53 | 54 | // Stop stops the bashCompletion service 55 | func (d *bashCompletionInstallerInUbuntu) Stop() error { 56 | fmt.Println("not supported yet") 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/os/apt/common.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | const ( 10 | // Tool is the tool name of apt-get 11 | Tool = "apt-get" 12 | ) 13 | 14 | // CommonInstaller is the installer of Conntrack in CentOS 15 | type CommonInstaller struct { 16 | Name string 17 | Execer fakeruntime.Execer 18 | } 19 | 20 | // Available check if support current platform 21 | func (d *CommonInstaller) Available() (ok bool) { 22 | if d.Execer.OS() == fakeruntime.OSLinux { 23 | _, err := d.Execer.LookPath(Tool) 24 | ok = err == nil 25 | } 26 | return 27 | } 28 | 29 | // Install installs the Conntrack 30 | func (d *CommonInstaller) Install() (err error) { 31 | if err = d.Execer.RunCommand(Tool, "update", "-y"); err == nil { 32 | err = d.Execer.RunCommand(Tool, "install", "-y", d.Name) 33 | } 34 | return 35 | } 36 | 37 | // Uninstall uninstalls the Conntrack 38 | func (d *CommonInstaller) Uninstall() (err error) { 39 | err = d.Execer.RunCommand(Tool, "remove", "-y", d.Name) 40 | return 41 | } 42 | 43 | // WaitForStart waits for the service be started 44 | func (d *CommonInstaller) WaitForStart() (ok bool, err error) { 45 | ok = true 46 | return 47 | } 48 | 49 | // Start starts the Conntrack service 50 | func (d *CommonInstaller) Start() error { 51 | fmt.Println("not supported yet") 52 | return nil 53 | } 54 | 55 | // Stop stops the Conntrack service 56 | func (d *CommonInstaller) Stop() error { 57 | fmt.Println("not supported yet") 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/os/apt/common_test.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCommon(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | installer CommonInstaller 15 | expectAvailable bool 16 | hasErr bool 17 | }{{ 18 | name: "normal", 19 | installer: CommonInstaller{ 20 | Execer: fakeruntime.FakeExecer{ 21 | ExpectError: nil, 22 | ExpectOutput: "", 23 | ExpectOS: "linux", 24 | ExpectArch: "amd64", 25 | }, 26 | }, 27 | expectAvailable: true, 28 | hasErr: false, 29 | }, { 30 | name: "not is linux", 31 | installer: CommonInstaller{ 32 | Execer: fakeruntime.FakeExecer{ExpectOS: "darwin"}, 33 | }, 34 | expectAvailable: false, 35 | hasErr: false, 36 | }, { 37 | name: "command not found", 38 | installer: CommonInstaller{ 39 | Execer: fakeruntime.FakeExecer{ExpectError: errors.New("error")}, 40 | }, 41 | expectAvailable: false, 42 | hasErr: true, 43 | }} 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | assert.Equal(t, tt.expectAvailable, tt.installer.Available()) 47 | assert.Nil(t, tt.installer.Start()) 48 | assert.Nil(t, tt.installer.Stop()) 49 | 50 | ok, err := tt.installer.WaitForStart() 51 | assert.True(t, ok) 52 | assert.Nil(t, err) 53 | assert.Equal(t, tt.hasErr, tt.installer.Install() != nil) 54 | assert.Equal(t, tt.hasErr, tt.installer.Uninstall() != nil) 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/os/apt/conntrack.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // conntrackInstallerInUbuntu is the installer of Conntrack in CentOS 10 | type conntrackInstallerInUbuntu struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *conntrackInstallerInUbuntu) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("apt-get") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the Conntrack 24 | func (d *conntrackInstallerInUbuntu) Install() (err error) { 25 | if err = d.Execer.RunCommand("apt-get", "update", "-y"); err != nil { 26 | return 27 | } 28 | if err = d.Execer.RunCommand("apt-get", "install", "-y", 29 | "conntrack"); err != nil { 30 | return 31 | } 32 | return 33 | } 34 | 35 | // Uninstall uninstalls the Conntrack 36 | func (d *conntrackInstallerInUbuntu) Uninstall() (err error) { 37 | err = d.Execer.RunCommand("apt-get", "remove", "-y", 38 | "conntrack") 39 | return 40 | } 41 | 42 | // WaitForStart waits for the service be started 43 | func (d *conntrackInstallerInUbuntu) WaitForStart() (ok bool, err error) { 44 | ok = true 45 | return 46 | } 47 | 48 | // Start starts the Conntrack service 49 | func (d *conntrackInstallerInUbuntu) Start() error { 50 | fmt.Println("not supported yet") 51 | return nil 52 | } 53 | 54 | // Stop stops the Conntrack service 55 | func (d *conntrackInstallerInUbuntu) Stop() error { 56 | fmt.Println("not supported yet") 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/os/apt/doc.go: -------------------------------------------------------------------------------- 1 | // Package apt provides the common functions to install a package via apt. 2 | // See also https://en.wikipedia.org/wiki/APT_(software). 3 | package apt 4 | -------------------------------------------------------------------------------- /pkg/os/apt/docker.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 10 | ) 11 | 12 | // dockerInstallerInUbuntu is the installer of Docker in Ubuntu 13 | type dockerInstallerInUbuntu struct { 14 | Execer fakeruntime.Execer 15 | count int 16 | } 17 | 18 | // Available check if support current platform 19 | func (d *dockerInstallerInUbuntu) Available() (ok bool) { 20 | if d.Execer.OS() == "linux" { 21 | _, err := d.Execer.LookPath("apt-get") 22 | ok = err == nil 23 | } 24 | return 25 | } 26 | 27 | // Install installs the Docker 28 | func (d *dockerInstallerInUbuntu) Install() (err error) { 29 | if d.isDebian() { 30 | return d.installOnDebian() 31 | } 32 | 33 | if err = d.Execer.RunCommand("apt-get", "update", "-y"); err != nil { 34 | return 35 | } 36 | if err = d.Execer.RunCommand("apt-get", "install", "-y", 37 | "apt-transport-https", 38 | "ca-certificates", 39 | "curl", 40 | "gnupg", 41 | "lsb-release"); err != nil { 42 | return 43 | } 44 | if err = d.Execer.RunCommand("rm", "-rf", "/usr/share/keyrings/docker-archive-keyring.gpg"); err != nil { 45 | return 46 | } 47 | const dockerGPG = "docker.gpg" 48 | defer func() { 49 | _ = d.Execer.RunCommand("rm", "-rf", dockerGPG) 50 | }() 51 | if err = d.Execer.RunCommand("curl", "-fsSL", 52 | "https://download.docker.com/linux/ubuntu/gpg", "-o", dockerGPG); err == nil { 53 | if err = d.Execer.RunCommand("gpg", 54 | "--dearmor", 55 | "-o", 56 | "/usr/share/keyrings/docker-archive-keyring.gpg", dockerGPG); err != nil { 57 | err = fmt.Errorf("failed to install docker-archive-keyring.gpg, error: %v", err) 58 | return 59 | } 60 | } else { 61 | err = fmt.Errorf("failed to download docker gpg file, error: %v", err) 62 | return 63 | } 64 | var release string 65 | if release, err = d.Execer.RunCommandAndReturn("lsb_release", "", "-cs"); err == nil { 66 | item := fmt.Sprintf("deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu %s stable", 67 | strings.TrimSpace(release)) 68 | if err = os.WriteFile("/etc/apt/sources.list.d/docker.list", []byte(item), 0622); err != nil { 69 | err = fmt.Errorf("failed to write docker.list, error: %v", err) 70 | return 71 | } 72 | } else { 73 | err = fmt.Errorf("failed to run command lsb_release -cs, error: %v", err) 74 | return 75 | } 76 | if err = d.Execer.RunCommand("apt-get", "update", "-y"); err != nil { 77 | return 78 | } 79 | if err = d.Execer.RunCommand("apt-get", "install", "-y", 80 | "docker-ce", "docker-ce-cli", "containerd.io"); err != nil { 81 | return 82 | } 83 | return 84 | } 85 | 86 | func (d *dockerInstallerInUbuntu) isDebian() bool { 87 | output, err := d.Execer.RunCommandAndReturn("lsb_release", "", "-d") 88 | if err == nil { 89 | return strings.Contains(output, "Debian") 90 | } 91 | return false 92 | } 93 | 94 | // see also https://docs.docker.com/engine/install/debian/ 95 | func (d *dockerInstallerInUbuntu) installOnDebian() (err error) { 96 | if err = d.Execer.RunCommand("apt-get", "update", "-y"); err != nil { 97 | return 98 | } 99 | 100 | if err = d.Execer.RunCommand("apt-get", "install", "-y", 101 | "ca-certificates", "curl", "gnupg", "lsb-release"); err != nil { 102 | return 103 | } 104 | 105 | if err = os.MkdirAll("/etc/apt/keyrings", 0750); err != nil { 106 | return 107 | } 108 | 109 | const dockerGPG = "docker.gpg" 110 | defer func() { 111 | _ = d.Execer.RunCommand("rm", "-rf", dockerGPG) 112 | }() 113 | if err = d.Execer.RunCommand("curl", "-fsSL", 114 | "https://download.docker.com/linux/debian/gpg", "-o", dockerGPG); err == nil { 115 | if err = d.Execer.RunCommand("gpg", 116 | "--dearmor", 117 | "-o", 118 | "/etc/apt/keyrings/docker.gpg", dockerGPG); err != nil { 119 | err = fmt.Errorf("failed to install docker-archive-keyring.gpg, error: %v", err) 120 | return 121 | } 122 | } else { 123 | err = fmt.Errorf("failed to download docker gpg file, error: %v", err) 124 | return 125 | } 126 | 127 | var release string 128 | if release, err = d.Execer.RunCommandAndReturn("lsb_release", "", "-cs"); err == nil { 129 | item := fmt.Sprintf("deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian %s stable", 130 | strings.TrimSpace(release)) 131 | if err = os.WriteFile("/etc/apt/sources.list.d/docker.list", []byte(item), 0622); err != nil { 132 | err = fmt.Errorf("failed to write docker.list, error: %v", err) 133 | return 134 | } 135 | } else { 136 | err = fmt.Errorf("failed to run command lsb_release -cs, error: %v", err) 137 | return 138 | } 139 | 140 | if err = d.Execer.RunCommand("apt-get", "update", "-y"); err != nil { 141 | return 142 | } 143 | err = d.Execer.RunCommand("apt-get", "install", "docker-ce", "docker-ce-cli", "containerd.io", "-y") 144 | return 145 | } 146 | 147 | // Uninstall uninstalls the Docker 148 | func (d *dockerInstallerInUbuntu) Uninstall() (err error) { 149 | if err = d.Execer.RunCommand("apt-get", "remove", "-y", 150 | "docker", 151 | "docker-engine", 152 | "docker.io", 153 | "containerd", 154 | "runc"); err == nil { 155 | err = d.Execer.RunCommand("apt-get", "purge", "-y", 156 | "docker-ce", 157 | "docker-ce-cli", 158 | "containerd.io") 159 | } 160 | return 161 | } 162 | 163 | // WaitForStart waits for the service be started 164 | func (d *dockerInstallerInUbuntu) WaitForStart() (ok bool, err error) { 165 | var result string 166 | if result, err = d.Execer.RunCommandAndReturn("systemctl", "", "status", "docker"); err != nil { 167 | return 168 | } else if strings.Contains(result, "Unit docker.service could not be found") { 169 | err = fmt.Errorf("unit docker.service could not be found") 170 | } else if strings.Contains(result, "Active: active") { 171 | ok = true 172 | } else { 173 | if d.count > 0 { 174 | fmt.Println("waiting for Docker service start") 175 | } else if d.count > 9 { 176 | return 177 | } 178 | 179 | d.count++ 180 | time.Sleep(time.Second * 1) 181 | return d.WaitForStart() 182 | } 183 | return 184 | } 185 | 186 | // Start starts the Docker service 187 | func (d *dockerInstallerInUbuntu) Start() error { 188 | fmt.Println("not implemented yet") 189 | return nil 190 | } 191 | 192 | // Stop stops the Docker service 193 | func (d *dockerInstallerInUbuntu) Stop() error { 194 | return d.Execer.RunCommand("systemctl", "start", "docker") 195 | } 196 | -------------------------------------------------------------------------------- /pkg/os/apt/ffmpeg.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // ffmpegInstallerInUbuntu is the installer of ffmpeg in CentOS 10 | type ffmpegInstallerInUbuntu struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *ffmpegInstallerInUbuntu) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("apt-get") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the ffmpeg 24 | func (d *ffmpegInstallerInUbuntu) Install() (err error) { 25 | if err = d.Execer.RunCommand("apt-get", "update", "-y"); err != nil { 26 | return 27 | } 28 | if err = d.Execer.RunCommand("apt-get", "install", "-y", 29 | "ffmpeg"); err != nil { 30 | return 31 | } 32 | return 33 | } 34 | 35 | // Uninstall uninstalls the ffmpeg 36 | func (d *ffmpegInstallerInUbuntu) Uninstall() (err error) { 37 | err = d.Execer.RunCommand("apt-get", "remove", "-y", 38 | "ffmpeg") 39 | return 40 | } 41 | 42 | // WaitForStart waits for the service be started 43 | func (d *ffmpegInstallerInUbuntu) WaitForStart() (ok bool, err error) { 44 | ok = true 45 | return 46 | } 47 | 48 | // Start starts the ffmpeg service 49 | func (d *ffmpegInstallerInUbuntu) Start() error { 50 | fmt.Println("not supported yet") 51 | return nil 52 | } 53 | 54 | // Stop stops the ffmpeg service 55 | func (d *ffmpegInstallerInUbuntu) Stop() error { 56 | fmt.Println("not supported yet") 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/os/apt/git.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // gitInstallerInUbuntu is the installer of git in CentOS 10 | type gitInstallerInUbuntu struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *gitInstallerInUbuntu) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("apt-get") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the git 24 | func (d *gitInstallerInUbuntu) Install() (err error) { 25 | if err = d.Execer.RunCommand("apt-get", "update", "-y"); err == nil { 26 | err = d.Execer.RunCommand("apt-get", "install", "-y", 27 | "git") 28 | } 29 | return 30 | } 31 | 32 | // Uninstall uninstalls the git 33 | func (d *gitInstallerInUbuntu) Uninstall() (err error) { 34 | err = d.Execer.RunCommand("apt-get", "remove", "-y", "git") 35 | return 36 | } 37 | 38 | // WaitForStart waits for the service be started 39 | func (d *gitInstallerInUbuntu) WaitForStart() (ok bool, err error) { 40 | ok = true 41 | return 42 | } 43 | 44 | // Start starts the git service 45 | func (d *gitInstallerInUbuntu) Start() error { 46 | fmt.Println("not supported yet") 47 | return nil 48 | } 49 | 50 | // Stop stops the git service 51 | func (d *gitInstallerInUbuntu) Stop() error { 52 | fmt.Println("not supported yet") 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/os/apt/golang.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // golangInstallerInUbuntu is the installer of golang in CentOS 10 | type golangInstallerInUbuntu struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *golangInstallerInUbuntu) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("apt-get") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the golang 24 | func (d *golangInstallerInUbuntu) Install() (err error) { 25 | if err = d.Execer.RunCommand("apt-get", "update", "-y"); err != nil { 26 | return 27 | } 28 | if err = d.Execer.RunCommand("apt-get", "install", "-y", "golang-go"); err != nil { 29 | return 30 | } 31 | return 32 | } 33 | 34 | // Uninstall uninstalls the golang 35 | func (d *golangInstallerInUbuntu) Uninstall() (err error) { 36 | err = d.Execer.RunCommand("apt-get", "remove", "-y", "golang-go") 37 | return 38 | } 39 | 40 | // WaitForStart waits for the service be started 41 | func (d *golangInstallerInUbuntu) WaitForStart() (ok bool, err error) { 42 | ok = true 43 | return 44 | } 45 | 46 | // Start starts the golang service 47 | func (d *golangInstallerInUbuntu) Start() error { 48 | fmt.Println("not supported yet") 49 | return nil 50 | } 51 | 52 | // Stop stops the golang service 53 | func (d *golangInstallerInUbuntu) Stop() error { 54 | fmt.Println("not supported yet") 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/os/apt/init.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 5 | "github.com/linuxsuren/http-downloader/pkg/os/core" 6 | ) 7 | 8 | // SetInstallerRegistry sets the installer of registry 9 | func SetInstallerRegistry(registry core.InstallerRegistry, defaultExecer fakeruntime.Execer) { 10 | registry.Registry("docker", &dockerInstallerInUbuntu{Execer: defaultExecer}) 11 | registry.Registry("conntrack", &conntrackInstallerInUbuntu{Execer: defaultExecer}) 12 | registry.Registry("socat", &socatInstallerInUbuntu{Execer: defaultExecer}) 13 | registry.Registry("vim", &vimInstallerInUbuntu{Execer: defaultExecer}) 14 | registry.Registry("golang", &golangInstallerInUbuntu{Execer: defaultExecer}) 15 | registry.Registry("git", &gitInstallerInUbuntu{Execer: defaultExecer}) 16 | registry.Registry("bash-completion", &bashCompletionInstallerInUbuntu{Execer: defaultExecer}) 17 | registry.Registry("asciinema", &asciinemaInstallerInUbuntu{Execer: defaultExecer}) 18 | registry.Registry("ffmpge", &ffmpegInstallerInUbuntu{Execer: defaultExecer}) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/os/apt/init_test.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 8 | "github.com/linuxsuren/http-downloader/pkg/os/core" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestCommonCase(t *testing.T) { 13 | registry := &core.FakeRegistry{} 14 | SetInstallerRegistry(registry, fakeruntime.FakeExecer{ 15 | ExpectOS: "linux", 16 | }) 17 | 18 | registry.Walk(func(s string, i core.Installer) { 19 | t.Run(s, func(t *testing.T) { 20 | assert.True(t, i.Available()) 21 | assert.Nil(t, i.Uninstall()) 22 | if s != "docker" { 23 | assert.Nil(t, i.Install()) 24 | assert.Nil(t, i.Start()) 25 | assert.Nil(t, i.Stop()) 26 | ok, err := i.WaitForStart() 27 | assert.True(t, ok) 28 | assert.Nil(t, err) 29 | } 30 | }) 31 | }) 32 | 33 | errRegistry := &core.FakeRegistry{} 34 | SetInstallerRegistry(errRegistry, fakeruntime.FakeExecer{ 35 | ExpectLookPathError: errors.New("error"), 36 | ExpectError: errors.New("error"), 37 | ExpectOS: "linux", 38 | }) 39 | errRegistry.Walk(func(s string, i core.Installer) { 40 | t.Run(s, func(t *testing.T) { 41 | assert.False(t, i.Available()) 42 | assert.NotNil(t, i.Uninstall()) 43 | if s != "docker" { 44 | assert.NotNil(t, i.Install()) 45 | assert.Nil(t, i.Start()) 46 | assert.Nil(t, i.Stop()) 47 | ok, err := i.WaitForStart() 48 | assert.True(t, ok) 49 | assert.Nil(t, err) 50 | } 51 | }) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/os/apt/resource/kubernetes.list: -------------------------------------------------------------------------------- 1 | deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main 2 | -------------------------------------------------------------------------------- /pkg/os/apt/socat.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // socatInstallerInUbuntu is the installer of socat in CentOS 10 | type socatInstallerInUbuntu struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *socatInstallerInUbuntu) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("apt-get") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the socat 24 | func (d *socatInstallerInUbuntu) Install() (err error) { 25 | if err = d.Execer.RunCommand("apt-get", "update", "-y"); err != nil { 26 | return 27 | } 28 | if err = d.Execer.RunCommand("apt-get", "install", "-y", 29 | "socat"); err != nil { 30 | return 31 | } 32 | return 33 | } 34 | 35 | // Uninstall uninstalls the socat 36 | func (d *socatInstallerInUbuntu) Uninstall() (err error) { 37 | err = d.Execer.RunCommand("apt-get", "remove", "-y", 38 | "socat") 39 | return 40 | } 41 | 42 | // WaitForStart waits for the service be started 43 | func (d *socatInstallerInUbuntu) WaitForStart() (ok bool, err error) { 44 | ok = true 45 | return 46 | } 47 | 48 | // Start starts the socat service 49 | func (d *socatInstallerInUbuntu) Start() error { 50 | fmt.Println("not supported yet") 51 | return nil 52 | } 53 | 54 | // Stop stops the socat service 55 | func (d *socatInstallerInUbuntu) Stop() error { 56 | fmt.Println("not supported yet") 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/os/apt/vim.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // vimInstallerInUbuntu is the installer of vim in CentOS 10 | type vimInstallerInUbuntu struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *vimInstallerInUbuntu) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("apt-get") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the vim 24 | func (d *vimInstallerInUbuntu) Install() (err error) { 25 | if err = d.Execer.RunCommand("apt-get", "update", "-y"); err != nil { 26 | return 27 | } 28 | if err = d.Execer.RunCommand("apt-get", "install", "-y", 29 | "vim"); err != nil { 30 | return 31 | } 32 | return 33 | } 34 | 35 | // Uninstall uninstalls the vim 36 | func (d *vimInstallerInUbuntu) Uninstall() (err error) { 37 | err = d.Execer.RunCommand("apt-get", "remove", "-y", 38 | "vim") 39 | return 40 | } 41 | 42 | // WaitForStart waits for the service be started 43 | func (d *vimInstallerInUbuntu) WaitForStart() (ok bool, err error) { 44 | ok = true 45 | return 46 | } 47 | 48 | // Start starts the vim service 49 | func (d *vimInstallerInUbuntu) Start() error { 50 | fmt.Println("not supported yet") 51 | return nil 52 | } 53 | 54 | // Stop stops the vim service 55 | func (d *vimInstallerInUbuntu) Stop() error { 56 | fmt.Println("not supported yet") 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/os/brew/common.go: -------------------------------------------------------------------------------- 1 | package brew 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | const ( 10 | // Tool is the tool name of brew 11 | Tool = "brew" 12 | ) 13 | 14 | // CommonInstaller is the installer of a common brew 15 | type CommonInstaller struct { 16 | Name string 17 | Execer fakeruntime.Execer 18 | } 19 | 20 | // Available check if support current platform 21 | func (d *CommonInstaller) Available() (ok bool) { 22 | if d.Execer.OS() == fakeruntime.OSDarwin || d.Execer.OS() == fakeruntime.OSLinux { 23 | _, err := d.Execer.LookPath(Tool) 24 | ok = err == nil 25 | } 26 | return 27 | } 28 | 29 | // Install installs the target package 30 | func (d *CommonInstaller) Install() (err error) { 31 | if err = d.Execer.RunCommand(Tool, "install", d.Name); err != nil { 32 | return 33 | } 34 | return 35 | } 36 | 37 | // Uninstall uninstalls the Conntrack 38 | func (d *CommonInstaller) Uninstall() (err error) { 39 | err = d.Execer.RunCommand(Tool, "remove", d.Name) 40 | return 41 | } 42 | 43 | // WaitForStart waits for the service be started 44 | func (d *CommonInstaller) WaitForStart() (ok bool, err error) { 45 | ok = true 46 | return 47 | } 48 | 49 | // Start starts the Conntrack service 50 | func (d *CommonInstaller) Start() error { 51 | fmt.Println("not supported yet") 52 | return nil 53 | } 54 | 55 | // Stop stops the Conntrack service 56 | func (d *CommonInstaller) Stop() error { 57 | fmt.Println("not supported yet") 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/os/brew/common_test.go: -------------------------------------------------------------------------------- 1 | package brew 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCommon(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | installer CommonInstaller 15 | expectAvailable bool 16 | hasErr bool 17 | }{{ 18 | name: "normal", 19 | installer: CommonInstaller{ 20 | Execer: fakeruntime.FakeExecer{ 21 | ExpectError: nil, 22 | ExpectOutput: "", 23 | ExpectOS: "darwin", 24 | ExpectArch: "amd64", 25 | }, 26 | }, 27 | expectAvailable: true, 28 | hasErr: false, 29 | }, { 30 | name: "not is darwin", 31 | installer: CommonInstaller{ 32 | Execer: fakeruntime.FakeExecer{ExpectOS: fakeruntime.OSWindows}, 33 | }, 34 | expectAvailable: false, 35 | hasErr: false, 36 | }, { 37 | name: "command not found", 38 | installer: CommonInstaller{ 39 | Execer: fakeruntime.FakeExecer{ExpectError: errors.New("error")}, 40 | }, 41 | expectAvailable: false, 42 | hasErr: true, 43 | }} 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | assert.Equal(t, tt.expectAvailable, tt.installer.Available()) 47 | assert.Nil(t, tt.installer.Start()) 48 | assert.Nil(t, tt.installer.Stop()) 49 | 50 | ok, err := tt.installer.WaitForStart() 51 | assert.True(t, ok) 52 | assert.Nil(t, err) 53 | assert.Equal(t, tt.hasErr, tt.installer.Install() != nil) 54 | assert.Equal(t, tt.hasErr, tt.installer.Uninstall() != nil) 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/os/brew/doc.go: -------------------------------------------------------------------------------- 1 | // Package brew provides the common functions to install a package via brew. 2 | // See also https://brew.sh/. 3 | package brew 4 | -------------------------------------------------------------------------------- /pkg/os/core/doc.go: -------------------------------------------------------------------------------- 1 | // Package core provides the core structs about the os-related package manager. 2 | package core 3 | -------------------------------------------------------------------------------- /pkg/os/core/fake.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // FakeRegistry is a fake registry which only for the test purpose 4 | type FakeRegistry struct { 5 | data map[string]Installer 6 | } 7 | 8 | // Registry puts the registry to memory 9 | func (r *FakeRegistry) Registry(id string, installer Installer) { 10 | if r.data == nil { 11 | r.data = map[string]Installer{} 12 | } 13 | r.data[id] = installer 14 | } 15 | 16 | // Walk allows to iterate all the installers 17 | func (r *FakeRegistry) Walk(walkFunc func(string, Installer)) { 18 | if r.data != nil { 19 | for id, installer := range r.data { 20 | walkFunc(id, installer) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/os/core/fake_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestRegistry(t *testing.T) { 9 | reg := &FakeRegistry{} 10 | reg.Registry("id", nil) 11 | 12 | reg.Walk(func(s string, installer Installer) { 13 | assert.Equal(t, "id", s) 14 | assert.Nil(t, installer) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/os/core/types.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // Installer is the interface of a installer 4 | // Deprecated use AdvanceInstaller instead 5 | type Installer interface { 6 | Available() bool 7 | Install() error 8 | Uninstall() error 9 | 10 | WaitForStart() (bool, error) 11 | Start() error 12 | Stop() error 13 | } 14 | 15 | // AdvanceInstaller is a generic installer 16 | type AdvanceInstaller interface { 17 | Installer 18 | 19 | IsService() bool 20 | } 21 | 22 | // InstallerRegistry is the interface of install registry 23 | type InstallerRegistry interface { 24 | Registry(string, Installer) 25 | } 26 | 27 | // ProxyAble define the proxy support feature 28 | type ProxyAble interface { 29 | SetURLReplace(map[string]string) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/os/dnf/common.go: -------------------------------------------------------------------------------- 1 | package dnf 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // DNFName is the tool name 10 | const DNFName = "dnf" 11 | 12 | // CommonInstaller is the installer of a common dnf 13 | type CommonInstaller struct { 14 | Name string 15 | Execer fakeruntime.Execer 16 | } 17 | 18 | // Available check if support current platform 19 | func (d *CommonInstaller) Available() (ok bool) { 20 | if d.Execer.OS() == fakeruntime.OSLinux { 21 | _, err := d.Execer.LookPath(DNFName) 22 | ok = err == nil 23 | } 24 | return 25 | } 26 | 27 | // Install installs the target package 28 | func (d *CommonInstaller) Install() (err error) { 29 | if err = d.Execer.RunCommand(DNFName, "install", d.Name, "-y"); err != nil { 30 | return 31 | } 32 | return 33 | } 34 | 35 | // Uninstall uninstalls the target 36 | func (d *CommonInstaller) Uninstall() (err error) { 37 | err = d.Execer.RunCommand(DNFName, "remove", d.Name, "-y") 38 | return 39 | } 40 | 41 | // WaitForStart waits for the service be started 42 | func (d *CommonInstaller) WaitForStart() (ok bool, err error) { 43 | ok = true 44 | return 45 | } 46 | 47 | // Start starts the target service 48 | func (d *CommonInstaller) Start() error { 49 | fmt.Println("not supported yet") 50 | return nil 51 | } 52 | 53 | // Stop stops the target service 54 | func (d *CommonInstaller) Stop() error { 55 | fmt.Println("not supported yet") 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/os/dnf/common_test.go: -------------------------------------------------------------------------------- 1 | package dnf 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCommon(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | installer CommonInstaller 15 | expectAvailable bool 16 | hasErr bool 17 | }{{ 18 | name: "normal", 19 | installer: CommonInstaller{ 20 | Execer: fakeruntime.FakeExecer{ 21 | ExpectError: nil, 22 | ExpectOutput: "", 23 | ExpectOS: "linux", 24 | ExpectArch: "amd64", 25 | }, 26 | }, 27 | expectAvailable: true, 28 | hasErr: false, 29 | }, { 30 | name: "not is linux", 31 | installer: CommonInstaller{ 32 | Execer: fakeruntime.FakeExecer{ExpectOS: "darwin"}, 33 | }, 34 | expectAvailable: false, 35 | hasErr: false, 36 | }, { 37 | name: "command not found", 38 | installer: CommonInstaller{ 39 | Execer: fakeruntime.FakeExecer{ExpectError: errors.New("error")}, 40 | }, 41 | expectAvailable: false, 42 | hasErr: true, 43 | }} 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | assert.Equal(t, tt.expectAvailable, tt.installer.Available()) 47 | assert.Nil(t, tt.installer.Start()) 48 | assert.Nil(t, tt.installer.Stop()) 49 | 50 | ok, err := tt.installer.WaitForStart() 51 | assert.True(t, ok) 52 | assert.Nil(t, err) 53 | assert.Equal(t, tt.hasErr, tt.installer.Install() != nil) 54 | assert.Equal(t, tt.hasErr, tt.installer.Uninstall() != nil) 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/os/dnf/doc.go: -------------------------------------------------------------------------------- 1 | // Package dnf provides the common function of dnf package manager. 2 | // See also https://en.wikipedia.org/wiki/DNF_(software). 3 | package dnf 4 | -------------------------------------------------------------------------------- /pkg/os/dnf/docker.go: -------------------------------------------------------------------------------- 1 | package dnf 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 9 | ) 10 | 11 | // dockerInstallerInFedora is the installer of Docker in Fedora 12 | type dockerInstallerInFedora struct { 13 | Execer fakeruntime.Execer 14 | count int 15 | } 16 | 17 | // Available check if support current platform 18 | func (d *dockerInstallerInFedora) Available() (ok bool) { 19 | if d.Execer.OS() == "linux" { 20 | _, err := d.Execer.LookPath("dnf") 21 | ok = err == nil 22 | } 23 | return 24 | } 25 | 26 | // Install installs the Docker 27 | func (d *dockerInstallerInFedora) Install() (err error) { 28 | if err = d.Execer.RunCommand("dnf", "-y", "install", "dnf-plugins-core"); err != nil { 29 | return 30 | } 31 | 32 | if err = d.Execer.RunCommand("dnf", "config-manager", 33 | "--add-repo", "https://download.docker.com/linux/fedora/docker-ce.repo"); err != nil { 34 | return 35 | } 36 | 37 | err = d.Execer.RunCommand("dnf", "install", "docker-ce", "docker-ce-cli", "containerd.io", "docker-compose-plugin") 38 | return 39 | } 40 | 41 | // Uninstall uninstalls the Docker 42 | func (d *dockerInstallerInFedora) Uninstall() (err error) { 43 | err = d.Execer.RunCommand("dnf", "remove", "docker", 44 | "docker-client", "docker-client-latest", "docker-common", "docker-latest", 45 | "docker-latest-logrotate", 46 | "docker-logrotate", 47 | "docker-selinux", 48 | "docker-engine-selinux", 49 | "docker-engine") 50 | return 51 | } 52 | 53 | // WaitForStart waits for the service be started 54 | func (d *dockerInstallerInFedora) WaitForStart() (ok bool, err error) { 55 | var result string 56 | if result, err = d.Execer.RunCommandAndReturn("systemctl", "", "status", "docker"); err != nil { 57 | return 58 | } else if strings.Contains(result, "Unit docker.service could not be found") { 59 | err = fmt.Errorf("unit docker.service could not be found") 60 | } else if strings.Contains(result, "Active: active") { 61 | ok = true 62 | } else { 63 | if d.count > 0 { 64 | fmt.Println("waiting for Docker service start") 65 | } else if d.count > 9 { 66 | return 67 | } 68 | 69 | d.count++ 70 | time.Sleep(time.Second * 1) 71 | return d.WaitForStart() 72 | } 73 | return 74 | } 75 | 76 | // Start starts the Docker service 77 | func (d *dockerInstallerInFedora) Start() (err error) { 78 | err = d.Execer.RunCommand("systemctl", "start", "docker") 79 | return 80 | } 81 | 82 | // Stop stops the Docker service 83 | func (d *dockerInstallerInFedora) Stop() (err error) { 84 | err = d.Execer.RunCommand("systemctl", "stop", "docker") 85 | return 86 | } 87 | -------------------------------------------------------------------------------- /pkg/os/dnf/init.go: -------------------------------------------------------------------------------- 1 | package dnf 2 | 3 | import ( 4 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 5 | "github.com/linuxsuren/http-downloader/pkg/os/core" 6 | ) 7 | 8 | // SetInstallerRegistry sets the installer of registry 9 | func SetInstallerRegistry(registry core.InstallerRegistry, defaultExecer fakeruntime.Execer) { 10 | registry.Registry("docker", &dockerInstallerInFedora{Execer: defaultExecer}) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/os/dnf/init_test.go: -------------------------------------------------------------------------------- 1 | package dnf 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 8 | "github.com/linuxsuren/http-downloader/pkg/os/core" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestCommonCase(t *testing.T) { 13 | registry := &core.FakeRegistry{} 14 | SetInstallerRegistry(registry, fakeruntime.FakeExecer{ 15 | ExpectOS: "linux", 16 | }) 17 | 18 | registry.Walk(func(s string, i core.Installer) { 19 | t.Run(s, func(t *testing.T) { 20 | assert.True(t, i.Available()) 21 | assert.Nil(t, i.Uninstall()) 22 | assert.Nil(t, i.Install()) 23 | assert.Nil(t, i.Start()) 24 | assert.Nil(t, i.Stop()) 25 | }) 26 | }) 27 | 28 | errRegistry := &core.FakeRegistry{} 29 | SetInstallerRegistry(errRegistry, fakeruntime.FakeExecer{ 30 | ExpectLookPathError: errors.New("error"), 31 | ExpectError: errors.New("error"), 32 | ExpectOS: "linux", 33 | }) 34 | errRegistry.Walk(func(s string, i core.Installer) { 35 | t.Run(s, func(t *testing.T) { 36 | assert.False(t, i.Available()) 37 | assert.NotNil(t, i.Uninstall()) 38 | if s != "docker" { 39 | assert.NotNil(t, i.Install()) 40 | assert.Nil(t, i.Start()) 41 | assert.Nil(t, i.Stop()) 42 | ok, err := i.WaitForStart() 43 | assert.True(t, ok) 44 | assert.Nil(t, err) 45 | } 46 | }) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/os/doc.go: -------------------------------------------------------------------------------- 1 | // Package os provides a generic installer for all platforms. 2 | // See the different native packages in the sub-packages, such as: apt, apk, brew etc. 3 | package os 4 | -------------------------------------------------------------------------------- /pkg/os/docker/bitbucket.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import fakeruntime "github.com/linuxsuren/go-fake-runtime" 4 | 5 | // bitbucket is the installer of bitbucket in CentOS 6 | type bitbucket struct { 7 | Execer fakeruntime.Execer 8 | } 9 | 10 | // Available check if support current platform 11 | func (d *bitbucket) Available() (ok bool) { 12 | _, err := d.Execer.LookPath("docker") 13 | ok = err == nil 14 | return 15 | } 16 | 17 | // Install installs the bitbucket 18 | func (d *bitbucket) Install() (err error) { 19 | err = d.Execer.RunCommand("docker", "run", `--name=bitbucket`, "-d", 20 | "-p", "7990:7990", "-p", "7999:7999", "atlassian/bitbucket") 21 | return 22 | } 23 | 24 | // Uninstall uninstalls the bitbucket 25 | func (d *bitbucket) Uninstall() (err error) { 26 | if err = d.Stop(); err == nil { 27 | err = d.Execer.RunCommand("docker", "rm", "bitbucket") 28 | } 29 | return 30 | } 31 | 32 | // WaitForStart waits for the service be started 33 | func (d *bitbucket) WaitForStart() (ok bool, err error) { 34 | ok = true 35 | return 36 | } 37 | 38 | // Start starts the bitbucket service 39 | func (d *bitbucket) Start() (err error) { 40 | err = d.Execer.RunCommand("docker", "start", "bitbucket") 41 | return 42 | } 43 | 44 | // Stop stops the bitbucket service 45 | func (d *bitbucket) Stop() (err error) { 46 | err = d.Execer.RunCommand("docker", "stop", "bitbucket") 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /pkg/os/docker/doc.go: -------------------------------------------------------------------------------- 1 | // Package docker provides the way to install a package via docker CLI. 2 | package docker 3 | -------------------------------------------------------------------------------- /pkg/os/docker/init.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 5 | "github.com/linuxsuren/http-downloader/pkg/os/core" 6 | ) 7 | 8 | // SetInstallerRegistry sets the installer of registry 9 | func SetInstallerRegistry(registry core.InstallerRegistry, execer fakeruntime.Execer) { 10 | registry.Registry("bitbucket", &bitbucket{Execer: execer}) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/os/docker/init_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "testing" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | "github.com/linuxsuren/http-downloader/pkg/os/core" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCommonCase(t *testing.T) { 12 | registry := &core.FakeRegistry{} 13 | SetInstallerRegistry(registry, fakeruntime.FakeExecer{ 14 | ExpectOS: "linux", 15 | }) 16 | 17 | registry.Walk(func(s string, i core.Installer) { 18 | t.Run(s, func(t *testing.T) { 19 | assert.True(t, i.Available()) 20 | assert.Nil(t, i.Start()) 21 | assert.Nil(t, i.Stop()) 22 | assert.Nil(t, i.Install()) 23 | assert.Nil(t, i.Uninstall()) 24 | ok, err := i.WaitForStart() 25 | assert.Nil(t, err) 26 | assert.True(t, ok) 27 | }) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/os/fake/doc.go: -------------------------------------------------------------------------------- 1 | // Package fake provides functions for unit test. 2 | package fake 3 | -------------------------------------------------------------------------------- /pkg/os/fake/fake.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/linuxsuren/http-downloader/pkg/os/core" 7 | ) 8 | 9 | // Installer only for test purpose 10 | type Installer struct { 11 | hasError bool 12 | support bool 13 | data map[string]string 14 | } 15 | 16 | // NewFakeInstaller returns a Installer 17 | func NewFakeInstaller(support bool, hasError bool) core.Installer { 18 | return &Installer{ 19 | hasError: hasError, 20 | support: support, 21 | } 22 | } 23 | 24 | // Available check if support current platform 25 | func (d *Installer) Available() bool { 26 | return true 27 | } 28 | 29 | // Install installs the vim 30 | func (d *Installer) Install() (err error) { 31 | if d.hasError { 32 | err = fmt.Errorf("fake error") 33 | } 34 | return 35 | } 36 | 37 | // Uninstall uninstalls the vim 38 | func (d *Installer) Uninstall() (err error) { 39 | if d.hasError { 40 | err = fmt.Errorf("fake error") 41 | } 42 | return 43 | } 44 | 45 | // WaitForStart waits for the service be started 46 | func (d *Installer) WaitForStart() (ok bool, err error) { 47 | if d.hasError { 48 | err = fmt.Errorf("fake error") 49 | } 50 | ok = true 51 | return 52 | } 53 | 54 | // Start starts the vim service 55 | func (d *Installer) Start() (err error) { 56 | if d.hasError { 57 | err = fmt.Errorf("fake error") 58 | } 59 | return 60 | } 61 | 62 | // Stop stops the vim service 63 | func (d *Installer) Stop() (err error) { 64 | if d.hasError { 65 | err = fmt.Errorf("fake error") 66 | } 67 | return 68 | } 69 | 70 | // SetURLReplace is a fake method 71 | func (d *Installer) SetURLReplace(data map[string]string) { 72 | d.data = data 73 | } 74 | -------------------------------------------------------------------------------- /pkg/os/fake/fake_test.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/linuxsuren/http-downloader/pkg/os/core" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNewFakeInstaller(t *testing.T) { 11 | var installer core.Installer = NewFakeInstaller(true, true) 12 | 13 | assert.True(t, installer.Available()) 14 | assert.NotNil(t, installer.Install()) 15 | assert.NotNil(t, installer.Uninstall()) 16 | assert.NotNil(t, installer.Start()) 17 | assert.NotNil(t, installer.Stop()) 18 | 19 | ok, err := installer.WaitForStart() 20 | assert.True(t, ok) 21 | assert.NotNil(t, err) 22 | 23 | proxyAbleInstaller := &Installer{} 24 | proxyAbleInstaller.SetURLReplace(map[string]string{ 25 | "key": "value", 26 | }) 27 | assert.Equal(t, map[string]string{ 28 | "key": "value", 29 | }, proxyAbleInstaller.data) 30 | 31 | var _ core.ProxyAble = &Installer{} 32 | } 33 | -------------------------------------------------------------------------------- /pkg/os/generic/common.go: -------------------------------------------------------------------------------- 1 | package generic 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 9 | ) 10 | 11 | // CommonInstaller is the installer of a common bash 12 | type CommonInstaller struct { 13 | Name string 14 | OS string 15 | InstallCmd CmdWithArgs 16 | UninstallCmd CmdWithArgs 17 | Execer fakeruntime.Execer 18 | 19 | // inner fields 20 | proxyMap map[string]string 21 | } 22 | 23 | // CmdWithArgs is a command and with args 24 | type CmdWithArgs struct { 25 | Cmd string `yaml:"cmd"` 26 | Args []string `yaml:"args"` 27 | SystemCall bool `yaml:"systemCall"` 28 | Execer fakeruntime.Execer 29 | } 30 | 31 | // Run runs the current command 32 | func (c CmdWithArgs) Run() (err error) { 33 | if c.SystemCall { 34 | var targetBinary string 35 | if targetBinary, err = c.Execer.LookPath(c.Cmd); err != nil { 36 | err = fmt.Errorf("cannot find %s", c.Cmd) 37 | } else { 38 | sysCallArgs := []string{c.Cmd} 39 | sysCallArgs = append(sysCallArgs, c.Args...) 40 | fmt.Println(c.Cmd, strings.Join(sysCallArgs, " ")) 41 | err = c.Execer.SystemCall(targetBinary, sysCallArgs, os.Environ()) 42 | } 43 | } else { 44 | fmt.Println(c.Cmd, strings.Join(c.Args, " ")) 45 | err = c.Execer.RunCommand(c.Cmd, c.Args...) 46 | } 47 | return 48 | } 49 | 50 | // SetURLReplace set the URL replace map 51 | func (d *CommonInstaller) SetURLReplace(data map[string]string) { 52 | d.proxyMap = data 53 | } 54 | 55 | func (d *CommonInstaller) sliceReplace(args []string) []string { 56 | for i, arg := range args { 57 | if result := d.urlReplace(arg); result != arg { 58 | args[i] = result 59 | } 60 | } 61 | return args 62 | } 63 | 64 | func (d *CommonInstaller) urlReplace(old string) string { 65 | if d.proxyMap == nil { 66 | return old 67 | } 68 | 69 | for k, v := range d.proxyMap { 70 | if !strings.Contains(old, k) { 71 | continue 72 | } 73 | old = strings.ReplaceAll(old, k, v) 74 | } 75 | return old 76 | } 77 | 78 | // Available check if support current platform 79 | func (d *CommonInstaller) Available() (ok bool) { 80 | ok = d.OS == "" || d.Execer.OS() == d.OS 81 | return 82 | } 83 | 84 | // Install installs the target package 85 | func (d *CommonInstaller) Install() (err error) { 86 | d.InstallCmd.Args = d.sliceReplace(d.InstallCmd.Args) 87 | d.InstallCmd.Execer = d.Execer 88 | err = d.InstallCmd.Run() 89 | return 90 | } 91 | 92 | // Uninstall uninstalls the target package 93 | func (d *CommonInstaller) Uninstall() (err error) { 94 | d.UninstallCmd.Args = d.sliceReplace(d.UninstallCmd.Args) 95 | d.UninstallCmd.Execer = d.Execer 96 | err = d.UninstallCmd.Run() 97 | return 98 | } 99 | 100 | // WaitForStart waits for the service be started 101 | func (d *CommonInstaller) WaitForStart() (ok bool, err error) { 102 | ok = true 103 | return 104 | } 105 | 106 | // Start starts the desired service 107 | func (d *CommonInstaller) Start() error { 108 | fmt.Println("not supported yet") 109 | return nil 110 | } 111 | 112 | // Stop stops the desired service 113 | func (d *CommonInstaller) Stop() error { 114 | fmt.Println("not supported yet") 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /pkg/os/generic/common_test.go: -------------------------------------------------------------------------------- 1 | package generic 2 | 3 | import ( 4 | "testing" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSliceReplace(t *testing.T) { 11 | installer := &CommonInstaller{} 12 | installer.SetURLReplace(map[string]string{ 13 | "https://raw.githubusercontent.com": "https://gh-proxy.com/https://raw.githubusercontent.com", 14 | }) 15 | 16 | // a normal case 17 | result := installer.sliceReplace([]string{ 18 | "abc", 19 | "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh", 20 | }) 21 | assert.Equal(t, []string{"abc", 22 | "https://gh-proxy.com/https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"}, result) 23 | 24 | // an empty slice 25 | noProxyInstaller := &CommonInstaller{} 26 | assert.Equal(t, []string{"abc"}, noProxyInstaller.sliceReplace([]string{"abc"})) 27 | } 28 | 29 | func TestCommonInstaller(t *testing.T) { 30 | installer := &CommonInstaller{ 31 | Execer: fakeruntime.FakeExecer{}, 32 | } 33 | assert.Nil(t, installer.Install()) 34 | assert.Nil(t, installer.Uninstall()) 35 | assert.True(t, installer.Available()) 36 | assert.Nil(t, installer.Stop()) 37 | assert.Nil(t, installer.Start()) 38 | 39 | ok, err := installer.WaitForStart() 40 | assert.True(t, ok) 41 | assert.Nil(t, err) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/os/generic/doc.go: -------------------------------------------------------------------------------- 1 | // Package generic provides the generic packge installation. 2 | // It does not care about the real package mananger. Because 3 | // It will accept arbitrary command line. 4 | package generic 5 | -------------------------------------------------------------------------------- /pkg/os/generic_installer_test.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "testing" 8 | 9 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 10 | "github.com/linuxsuren/http-downloader/pkg/os/fake" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestURLReplace(t *testing.T) { 15 | genericPkg := &genericPackage{ 16 | env: map[string]string{ 17 | "key": "value", 18 | }, 19 | execer: fakeruntime.FakeExecer{ExpectOS: fakeruntime.OSLinux}, 20 | } 21 | genericPkg.SetURLReplace(map[string]string{ 22 | "github": "ghproxy", 23 | }) 24 | genericPkg.loadEnv() 25 | assert.Equal(t, "ghproxy-value", genericPkg.urlReplace("github-{{.key}}")) 26 | assert.Equal(t, "value", genericPkg.urlReplace("{{.key}}")) 27 | assert.Equal(t, []string{"value"}, genericPkg.sliceReplace([]string{"{{.key}}"})) 28 | 29 | emptyGenericPkg := &genericPackage{ 30 | execer: fakeruntime.FakeExecer{ExpectOS: fakeruntime.OSLinux}, 31 | } 32 | emptyGenericPkg.loadEnv() 33 | assert.NotNil(t, emptyGenericPkg.env) 34 | assert.Nil(t, emptyGenericPkg.Start()) 35 | assert.Nil(t, emptyGenericPkg.Stop()) 36 | assert.False(t, emptyGenericPkg.IsService()) 37 | assert.False(t, emptyGenericPkg.Available()) 38 | assert.NotNil(t, emptyGenericPkg.Install()) 39 | assert.NotNil(t, emptyGenericPkg.Uninstall()) 40 | ok, err := emptyGenericPkg.WaitForStart() 41 | assert.True(t, ok) 42 | assert.Nil(t, err) 43 | 44 | withPreInstall := &genericPackage{ 45 | execer: fakeruntime.FakeExecer{ 46 | ExpectOS: fakeruntime.OSLinux, 47 | }, 48 | PreInstall: []preInstall{{ 49 | Cmd: CmdWithArgs{ 50 | Cmd: "ls", 51 | }, 52 | }, { 53 | IssuePrefix: "good", 54 | Cmd: CmdWithArgs{ 55 | Cmd: "ls", 56 | }, 57 | }, { 58 | IssuePrefix: "Ubuntu", 59 | Cmd: CmdWithArgs{ 60 | Cmd: "ls", 61 | }, 62 | }}, 63 | CommonInstaller: fake.NewFakeInstaller(true, false), 64 | } 65 | assert.Nil(t, withPreInstall.Install()) 66 | 67 | withErrorPreInstall := &genericPackage{ 68 | execer: fakeruntime.FakeExecer{ 69 | ExpectOS: fakeruntime.OSLinux, 70 | ExpectError: errors.New("error"), 71 | }, 72 | PreInstall: []preInstall{{ 73 | Cmd: CmdWithArgs{ 74 | Cmd: "ls", 75 | }, 76 | }}, 77 | CommonInstaller: fake.NewFakeInstaller(true, true), 78 | } 79 | assert.NotNil(t, withErrorPreInstall.Install()) 80 | assert.NotNil(t, withErrorPreInstall.Uninstall()) 81 | assert.True(t, withErrorPreInstall.Available()) 82 | 83 | tmpFile, err := os.CreateTemp(os.TempDir(), "installer") 84 | assert.Nil(t, err) 85 | defer func() { 86 | os.Remove(tmpFile.Name()) 87 | }() 88 | writeToFileInstall := &genericPackage{ 89 | execer: fakeruntime.FakeExecer{ 90 | ExpectOS: fakeruntime.OSLinux, 91 | }, 92 | PreInstall: []preInstall{{ 93 | Cmd: CmdWithArgs{ 94 | WriteTo: &WriteTo{ 95 | File: tmpFile.Name(), 96 | Content: "sample", 97 | }, 98 | }, 99 | }}, 100 | CommonInstaller: fake.NewFakeInstaller(true, false), 101 | } 102 | err = writeToFileInstall.Install() 103 | assert.Nil(t, err) 104 | data, err := io.ReadAll(tmpFile) 105 | assert.Nil(t, err) 106 | assert.Equal(t, "sample", string(data)) 107 | } 108 | 109 | func TestShould(t *testing.T) { 110 | tests := []struct { 111 | name string 112 | writeTo *WriteTo 113 | wantOK bool 114 | wantErr bool 115 | }{{ 116 | name: "expr is empty", 117 | writeTo: &WriteTo{}, 118 | wantOK: true, 119 | wantErr: false, 120 | }, { 121 | name: "1==1", 122 | writeTo: &WriteTo{ 123 | When: "1==1", 124 | }, 125 | wantOK: true, 126 | wantErr: false, 127 | }, { 128 | name: "not bool expr", 129 | writeTo: &WriteTo{ 130 | When: "not-expect", 131 | }, 132 | wantOK: false, 133 | wantErr: true, 134 | }, { 135 | name: "false", 136 | writeTo: &WriteTo{ 137 | When: "false", 138 | }, 139 | wantOK: false, 140 | wantErr: false, 141 | }, { 142 | name: "true", 143 | writeTo: &WriteTo{ 144 | When: "true", 145 | }, 146 | wantOK: true, 147 | wantErr: false, 148 | }, { 149 | name: "expr is number", 150 | writeTo: &WriteTo{ 151 | When: "123", 152 | }, 153 | wantOK: false, 154 | wantErr: true, 155 | }, { 156 | name: "with env, equal", 157 | writeTo: &WriteTo{ 158 | env: map[string]string{ 159 | "OS": "ubuntu", 160 | }, 161 | When: "OS=='ubuntu'", 162 | }, 163 | wantOK: true, 164 | wantErr: false, 165 | }, { 166 | name: "with env, not equal", 167 | writeTo: &WriteTo{ 168 | env: map[string]string{ 169 | "OS": "ubuntu", 170 | }, 171 | When: "OS!='ubuntu'", 172 | }, 173 | wantOK: false, 174 | wantErr: false, 175 | }} 176 | for _, tt := range tests { 177 | t.Run(tt.name, func(t *testing.T) { 178 | ok, err := tt.writeTo.Should() 179 | assert.Equal(t, tt.wantOK, ok) 180 | assert.Equal(t, tt.wantErr, err != nil, err) 181 | }) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /pkg/os/installer.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 9 | "github.com/linuxsuren/http-downloader/pkg/os/apt" 10 | "github.com/linuxsuren/http-downloader/pkg/os/core" 11 | "github.com/linuxsuren/http-downloader/pkg/os/dnf" 12 | "github.com/linuxsuren/http-downloader/pkg/os/docker" 13 | "github.com/linuxsuren/http-downloader/pkg/os/yum" 14 | "github.com/mitchellh/go-homedir" 15 | ) 16 | 17 | // DefaultInstallerRegistry is the default installer registry 18 | type DefaultInstallerRegistry struct { 19 | installerMap map[string][]core.Installer 20 | } 21 | 22 | var defaultInstallerRegistry *DefaultInstallerRegistry 23 | 24 | func init() { 25 | defaultExecer := fakeruntime.DefaultExecer{} 26 | defaultInstallerRegistry = &DefaultInstallerRegistry{ 27 | installerMap: map[string][]core.Installer{}, 28 | } 29 | yum.SetInstallerRegistry(defaultInstallerRegistry, defaultExecer) 30 | apt.SetInstallerRegistry(defaultInstallerRegistry, defaultExecer) 31 | docker.SetInstallerRegistry(defaultInstallerRegistry, defaultExecer) 32 | dnf.SetInstallerRegistry(defaultInstallerRegistry, defaultExecer) 33 | 34 | var userHome string 35 | var err error 36 | if userHome, err = homedir.Dir(); err == nil { 37 | configDir := filepath.Join(userHome, "/.config/hd-home") 38 | if err = GenericInstallerRegistry(filepath.Join(configDir, "config/generic.yaml"), defaultInstallerRegistry); err != nil { 39 | fmt.Println(err) 40 | } 41 | } 42 | } 43 | 44 | // Registry registries a DockerInstaller 45 | func (r *DefaultInstallerRegistry) Registry(name string, installer core.Installer) { 46 | _, ok := r.installerMap[name] 47 | if ok { 48 | r.installerMap[name] = append(r.installerMap[name], installer) 49 | } else { 50 | r.installerMap[name] = []core.Installer{installer} 51 | } 52 | } 53 | 54 | // GetInstallers returns all the installers belong to a package 55 | func GetInstallers(name string) (installers []core.Installer, ok bool) { 56 | installers, ok = defaultInstallerRegistry.installerMap[name] 57 | return 58 | } 59 | 60 | // HasPackage finds if the target package installer exist 61 | func HasPackage(name string) bool { 62 | if installers, ok := GetInstallers(name); ok { 63 | for _, item := range installers { 64 | if item.Available() { 65 | return true 66 | } 67 | } 68 | } 69 | return false 70 | } 71 | 72 | // SearchPackages searches the packages by keyword 73 | func SearchPackages(keyword string) (pkgs []string) { 74 | for key, pms := range defaultInstallerRegistry.installerMap { 75 | if strings.Contains(key, keyword) { 76 | var available bool 77 | for _, pm := range pms { 78 | if pm.Available() { 79 | available = true 80 | break 81 | } 82 | } 83 | 84 | if available { 85 | pkgs = append(pkgs, key) 86 | } 87 | } 88 | } 89 | return 90 | } 91 | 92 | // InstallWithProxy installs a package with name 93 | func InstallWithProxy(name string, proxy map[string]string) (err error) { 94 | var installer core.Installer 95 | if installers, ok := GetInstallers(name); ok { 96 | for _, installer = range installers { 97 | if installer.Available() { 98 | if proxyAble, ok := installer.(core.ProxyAble); ok { 99 | proxyAble.SetURLReplace(proxy) 100 | } 101 | 102 | err = installer.Install() 103 | break 104 | } 105 | } 106 | } 107 | 108 | if installer != nil && err == nil { 109 | if err = installer.Start(); err != nil { 110 | err = fmt.Errorf("failed to start service %s, error: %v", name, err) 111 | return 112 | } 113 | 114 | var ok bool 115 | if ok, err = installer.WaitForStart(); err != nil { 116 | err = fmt.Errorf("failed to check the service status of %s, error: %v", name, err) 117 | } else if !ok { 118 | err = fmt.Errorf("%s was not started yet, please check it manually", name) 119 | } 120 | } 121 | return 122 | } 123 | 124 | // Uninstall uninstalls a package with name 125 | func Uninstall(name string) (err error) { 126 | if installers, ok := GetInstallers(name); ok { 127 | for _, installer := range installers { 128 | if installer.Available() { 129 | err = installer.Uninstall() 130 | break 131 | } 132 | } 133 | } 134 | return 135 | } 136 | -------------------------------------------------------------------------------- /pkg/os/installer_test.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/linuxsuren/http-downloader/pkg/os/fake" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestHasPackage(t *testing.T) { 12 | // currently, this function only support Linux 13 | if runtime.GOOS != "linux" { 14 | return 15 | } 16 | 17 | type args struct { 18 | name string 19 | } 20 | tests := []struct { 21 | name string 22 | args args 23 | want bool 24 | }{{ 25 | name: "fake-package", 26 | args: args{ 27 | name: "fake-package", 28 | }, 29 | want: false, 30 | }, { 31 | name: "docker", 32 | args: args{ 33 | name: "docker", 34 | }, 35 | want: true, 36 | }, { 37 | name: "golang", 38 | args: args{ 39 | name: "golang", 40 | }, 41 | want: true, 42 | }, { 43 | name: "git", 44 | args: args{ 45 | name: "git", 46 | }, 47 | want: true, 48 | }, { 49 | name: "bash-completion", 50 | args: args{ 51 | name: "bash-completion", 52 | }, 53 | want: true, 54 | }} 55 | for _, tt := range tests { 56 | t.Run(tt.name, func(t *testing.T) { 57 | if got := HasPackage(tt.args.name); got != tt.want { 58 | t.Errorf("test: %s, HasPackage() = %v, want %v", tt.name, got, tt.want) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestWithFakeInstaller(t *testing.T) { 65 | // test uninstall a fake package 66 | err := Uninstall("fake") 67 | assert.Nil(t, err) 68 | assert.False(t, HasPackage("fake")) 69 | 70 | defaultInstallerRegistry.Registry("fake", fake.NewFakeInstaller(true, false)) 71 | err = Uninstall("fake") 72 | assert.Nil(t, err) 73 | err = InstallWithProxy("fake", nil) 74 | assert.Nil(t, err) 75 | 76 | defaultInstallerRegistry.Registry("fake-with-err", fake.NewFakeInstaller(true, true)) 77 | err = Uninstall("fake-with-err") 78 | assert.NotNil(t, err) 79 | } 80 | 81 | func TestSearchPackages(t *testing.T) { 82 | // currently, this function only support Linux 83 | if runtime.GOOS != "linux" { 84 | return 85 | } 86 | pkgs := SearchPackages("vim") 87 | assert.NotEmpty(t, pkgs) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/os/npm/common.go: -------------------------------------------------------------------------------- 1 | package npm 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // NPMName is the name of npm 10 | const NPMName = "npm" 11 | 12 | // CommonInstaller is the installer of a common npm 13 | type CommonInstaller struct { 14 | Name string 15 | Execer fakeruntime.Execer 16 | } 17 | 18 | // Available check if support current platform 19 | func (d *CommonInstaller) Available() (ok bool) { 20 | _, err := d.Execer.LookPath(NPMName) 21 | ok = err == nil 22 | return 23 | } 24 | 25 | // Install installs the target package 26 | func (d *CommonInstaller) Install() (err error) { 27 | err = d.Execer.RunCommand(NPMName, "i", "-g", d.Name) 28 | return 29 | } 30 | 31 | // Uninstall uninstalls the target package 32 | func (d *CommonInstaller) Uninstall() (err error) { 33 | err = d.Execer.RunCommand(NPMName, "uninstall", "-g", d.Name) 34 | return 35 | } 36 | 37 | // WaitForStart waits for the service be started 38 | func (d *CommonInstaller) WaitForStart() (ok bool, err error) { 39 | ok = true 40 | return 41 | } 42 | 43 | // Start starts the target service 44 | func (d *CommonInstaller) Start() error { 45 | fmt.Println("not supported yet") 46 | return nil 47 | } 48 | 49 | // Stop stops the target service 50 | func (d *CommonInstaller) Stop() error { 51 | fmt.Println("not supported yet") 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/os/npm/common_test.go: -------------------------------------------------------------------------------- 1 | package npm 2 | 3 | import ( 4 | "testing" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCommon(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | installer CommonInstaller 14 | expectAvailable bool 15 | hasErr bool 16 | }{{ 17 | name: "normal", 18 | installer: CommonInstaller{ 19 | Execer: fakeruntime.FakeExecer{ 20 | ExpectError: nil, 21 | ExpectOutput: "", 22 | ExpectOS: "linux", 23 | ExpectArch: "amd64", 24 | }, 25 | }, 26 | expectAvailable: true, 27 | hasErr: false, 28 | }} 29 | for _, tt := range tests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | assert.Equal(t, tt.expectAvailable, tt.installer.Available()) 32 | assert.Nil(t, tt.installer.Start()) 33 | assert.Nil(t, tt.installer.Stop()) 34 | 35 | ok, err := tt.installer.WaitForStart() 36 | assert.True(t, ok) 37 | assert.Nil(t, err) 38 | assert.Equal(t, tt.hasErr, tt.installer.Install() != nil) 39 | assert.Equal(t, tt.hasErr, tt.installer.Uninstall() != nil) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/os/npm/doc.go: -------------------------------------------------------------------------------- 1 | // Package npm provides the common usage of npm. 2 | // See also https://www.npmjs.com/. 3 | package npm 4 | -------------------------------------------------------------------------------- /pkg/os/scoop/common.go: -------------------------------------------------------------------------------- 1 | package scoop 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // Tool is the tool name of this intergration 10 | const Tool = "scoop" 11 | 12 | // CommonInstaller is the installer of Conntrack in CentOS 13 | type CommonInstaller struct { 14 | Name string 15 | Execer fakeruntime.Execer 16 | } 17 | 18 | // Available check if support current platform 19 | func (d *CommonInstaller) Available() (ok bool) { 20 | if d.Execer.OS() == fakeruntime.OSWindows { 21 | _, err := d.Execer.LookPath(Tool) 22 | ok = err == nil 23 | } 24 | return 25 | } 26 | 27 | // Install installs the Conntrack 28 | func (d *CommonInstaller) Install() (err error) { 29 | if err = d.Execer.RunCommand(Tool, "bucket", "add", "extras"); err == nil { 30 | err = d.Execer.RunCommand(Tool, "install", d.Name) 31 | } 32 | return 33 | } 34 | 35 | // Uninstall uninstalls the Conntrack 36 | func (d *CommonInstaller) Uninstall() (err error) { 37 | err = d.Execer.RunCommand(Tool, "uninstall", d.Name) 38 | return 39 | } 40 | 41 | // WaitForStart waits for the service be started 42 | func (d *CommonInstaller) WaitForStart() (ok bool, err error) { 43 | ok = true 44 | return 45 | } 46 | 47 | // Start starts the Conntrack service 48 | func (d *CommonInstaller) Start() error { 49 | fmt.Println("not supported yet") 50 | return nil 51 | } 52 | 53 | // Stop stops the Conntrack service 54 | func (d *CommonInstaller) Stop() error { 55 | fmt.Println("not supported yet") 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/os/scoop/doc.go: -------------------------------------------------------------------------------- 1 | // Package scoop implements the scoop integration 2 | package scoop 3 | -------------------------------------------------------------------------------- /pkg/os/snap/common.go: -------------------------------------------------------------------------------- 1 | package snap 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // SnapName is the name of the snap 10 | const SnapName = "snap" 11 | 12 | // CommonInstaller is the installer of a common snap 13 | type CommonInstaller struct { 14 | Name string 15 | Args []string `yaml:"args"` 16 | Execer fakeruntime.Execer 17 | } 18 | 19 | // Available check if support current platform 20 | func (d *CommonInstaller) Available() (ok bool) { 21 | if d.Execer.OS() == fakeruntime.OSLinux { 22 | _, err := d.Execer.LookPath(SnapName) 23 | ok = err == nil 24 | } 25 | return 26 | } 27 | 28 | // Install installs the target package 29 | func (d *CommonInstaller) Install() (err error) { 30 | args := []string{"install", d.Name} 31 | args = append(args, d.Args...) 32 | if err = d.Execer.RunCommand(SnapName, args...); err != nil { 33 | return 34 | } 35 | return 36 | } 37 | 38 | // Uninstall uninstalls the target package 39 | func (d *CommonInstaller) Uninstall() (err error) { 40 | err = d.Execer.RunCommand(SnapName, "remove", d.Name) 41 | return 42 | } 43 | 44 | // WaitForStart waits for the service be started 45 | func (d *CommonInstaller) WaitForStart() (ok bool, err error) { 46 | ok = true 47 | return 48 | } 49 | 50 | // Start starts the target service 51 | func (d *CommonInstaller) Start() error { 52 | fmt.Println("not supported yet") 53 | return nil 54 | } 55 | 56 | // Stop stops the target service 57 | func (d *CommonInstaller) Stop() error { 58 | fmt.Println("not supported yet") 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /pkg/os/snap/common_test.go: -------------------------------------------------------------------------------- 1 | package snap 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCommon(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | installer CommonInstaller 15 | expectAvailable bool 16 | hasErr bool 17 | }{{ 18 | name: "normal", 19 | installer: CommonInstaller{ 20 | Execer: fakeruntime.FakeExecer{ 21 | ExpectError: nil, 22 | ExpectOutput: "", 23 | ExpectOS: "linux", 24 | ExpectArch: "amd64", 25 | }, 26 | }, 27 | expectAvailable: true, 28 | hasErr: false, 29 | }, { 30 | name: "not is linux", 31 | installer: CommonInstaller{ 32 | Execer: fakeruntime.FakeExecer{ExpectOS: "darwin"}, 33 | }, 34 | expectAvailable: false, 35 | hasErr: false, 36 | }, { 37 | name: "command not found", 38 | installer: CommonInstaller{ 39 | Execer: fakeruntime.FakeExecer{ExpectError: errors.New("error")}, 40 | }, 41 | expectAvailable: false, 42 | hasErr: true, 43 | }} 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | assert.Equal(t, tt.expectAvailable, tt.installer.Available()) 47 | assert.Nil(t, tt.installer.Start()) 48 | assert.Nil(t, tt.installer.Stop()) 49 | 50 | ok, err := tt.installer.WaitForStart() 51 | assert.True(t, ok) 52 | assert.Nil(t, err) 53 | assert.Equal(t, tt.hasErr, tt.installer.Install() != nil) 54 | assert.Equal(t, tt.hasErr, tt.installer.Uninstall() != nil) 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/os/snap/doc.go: -------------------------------------------------------------------------------- 1 | // Package snap provides the common usage of snap package manager. 2 | // See also https://snapcraft.io/. 3 | package snap 4 | -------------------------------------------------------------------------------- /pkg/os/winget/common.go: -------------------------------------------------------------------------------- 1 | package winget 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // Tool is the tool name of this intergration 10 | const Tool = "winget" 11 | 12 | // CommonInstaller is the installer of Conntrack in CentOS 13 | type CommonInstaller struct { 14 | Name string 15 | Execer fakeruntime.Execer 16 | } 17 | 18 | // Available check if support current platform 19 | func (d *CommonInstaller) Available() (ok bool) { 20 | if d.Execer.OS() == fakeruntime.OSWindows { 21 | _, err := d.Execer.LookPath(Tool) 22 | ok = err == nil 23 | } 24 | return 25 | } 26 | 27 | // Install installs the Conntrack 28 | func (d *CommonInstaller) Install() (err error) { 29 | if err = d.Execer.RunCommand(Tool, "install", "--silent", "--accept-package-agreements", d.Name); err != nil { 30 | return 31 | } 32 | return 33 | } 34 | 35 | // Uninstall uninstalls the Conntrack 36 | func (d *CommonInstaller) Uninstall() (err error) { 37 | err = d.Execer.RunCommand(Tool, "uninstall", "--silent", d.Name) 38 | return 39 | } 40 | 41 | // WaitForStart waits for the service be started 42 | func (d *CommonInstaller) WaitForStart() (ok bool, err error) { 43 | ok = true 44 | return 45 | } 46 | 47 | // Start starts the Conntrack service 48 | func (d *CommonInstaller) Start() error { 49 | fmt.Println("not supported yet") 50 | return nil 51 | } 52 | 53 | // Stop stops the Conntrack service 54 | func (d *CommonInstaller) Stop() error { 55 | fmt.Println("not supported yet") 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/os/winget/doc.go: -------------------------------------------------------------------------------- 1 | // Package winget implements the winget integration 2 | package winget 3 | -------------------------------------------------------------------------------- /pkg/os/yum/bash-completion.go: -------------------------------------------------------------------------------- 1 | package yum 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // bashCompletionInstallerInCentOS is the installer of bashCompletion in CentOS 10 | type bashCompletionInstallerInCentOS struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *bashCompletionInstallerInCentOS) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("yum") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the bashCompletion 24 | func (d *bashCompletionInstallerInCentOS) Install() (err error) { 25 | if err = d.Execer.RunCommand("yum", "install", "-y", "bash-completion"); err != nil { 26 | return 27 | } 28 | return 29 | } 30 | 31 | // Uninstall uninstalls the bashCompletion 32 | func (d *bashCompletionInstallerInCentOS) Uninstall() (err error) { 33 | err = d.Execer.RunCommand("yum", "remove", "-y", "bash-completion") 34 | return 35 | } 36 | 37 | // WaitForStart waits for the service be started 38 | func (d *bashCompletionInstallerInCentOS) WaitForStart() (ok bool, err error) { 39 | ok = true 40 | return 41 | } 42 | 43 | // Start starts the bashCompletion service 44 | func (d *bashCompletionInstallerInCentOS) Start() error { 45 | fmt.Println("not supported yet") 46 | return nil 47 | } 48 | 49 | // Stop stops the bashCompletion service 50 | func (d *bashCompletionInstallerInCentOS) Stop() error { 51 | fmt.Println("not supported yet") 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/os/yum/common.go: -------------------------------------------------------------------------------- 1 | package yum 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | const ( 10 | // Tool is the tool name of yum 11 | Tool = "yum" 12 | ) 13 | 14 | // CommonInstaller is the installer of Conntrack in CentOS 15 | type CommonInstaller struct { 16 | Name string 17 | Execer fakeruntime.Execer 18 | } 19 | 20 | // Available check if support current platform 21 | func (d *CommonInstaller) Available() (ok bool) { 22 | if d.Execer.OS() == fakeruntime.OSLinux { 23 | _, err := d.Execer.LookPath(Tool) 24 | ok = err == nil 25 | } 26 | return 27 | } 28 | 29 | // Install installs the Conntrack 30 | func (d *CommonInstaller) Install() (err error) { 31 | if err = d.Execer.RunCommand(Tool, "update", "-y"); err != nil { 32 | return 33 | } 34 | if err = d.Execer.RunCommand(Tool, "install", "-y", d.Name); err != nil { 35 | return 36 | } 37 | return 38 | } 39 | 40 | // Uninstall uninstalls the Conntrack 41 | func (d *CommonInstaller) Uninstall() (err error) { 42 | err = d.Execer.RunCommand(Tool, "remove", "-y", d.Name) 43 | return 44 | } 45 | 46 | // WaitForStart waits for the service be started 47 | func (d *CommonInstaller) WaitForStart() (ok bool, err error) { 48 | ok = true 49 | return 50 | } 51 | 52 | // Start starts the Conntrack service 53 | func (d *CommonInstaller) Start() error { 54 | fmt.Println("not supported yet") 55 | return nil 56 | } 57 | 58 | // Stop stops the Conntrack service 59 | func (d *CommonInstaller) Stop() error { 60 | fmt.Println("not supported yet") 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/os/yum/common_test.go: -------------------------------------------------------------------------------- 1 | package yum 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCommon(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | installer CommonInstaller 15 | expectAvailable bool 16 | hasErr bool 17 | }{{ 18 | name: "normal", 19 | installer: CommonInstaller{ 20 | Execer: fakeruntime.FakeExecer{ 21 | ExpectError: nil, 22 | ExpectOutput: "", 23 | ExpectOS: "linux", 24 | ExpectArch: "amd64", 25 | }, 26 | }, 27 | expectAvailable: true, 28 | hasErr: false, 29 | }, { 30 | name: "not is linux", 31 | installer: CommonInstaller{ 32 | Execer: fakeruntime.FakeExecer{ExpectOS: "darwin"}, 33 | }, 34 | expectAvailable: false, 35 | hasErr: false, 36 | }, { 37 | name: "command not found", 38 | installer: CommonInstaller{ 39 | Execer: fakeruntime.FakeExecer{ExpectError: errors.New("error")}, 40 | }, 41 | expectAvailable: false, 42 | hasErr: true, 43 | }} 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | assert.Equal(t, tt.expectAvailable, tt.installer.Available()) 47 | assert.Nil(t, tt.installer.Start()) 48 | assert.Nil(t, tt.installer.Stop()) 49 | 50 | ok, err := tt.installer.WaitForStart() 51 | assert.True(t, ok) 52 | assert.Nil(t, err) 53 | assert.Equal(t, tt.hasErr, tt.installer.Install() != nil) 54 | assert.Equal(t, tt.hasErr, tt.installer.Uninstall() != nil) 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/os/yum/conntrack.go: -------------------------------------------------------------------------------- 1 | package yum 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // conntrackInstallerInCentOS is the installer of Docker in CentOS 10 | type conntrackInstallerInCentOS struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *conntrackInstallerInCentOS) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("yum") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the Docker 24 | func (d *conntrackInstallerInCentOS) Install() (err error) { 25 | if err = d.Execer.RunCommand("yum", "install", "-y", 26 | "conntrack"); err != nil { 27 | return 28 | } 29 | return 30 | } 31 | 32 | // Uninstall uninstalls the Docker 33 | func (d *conntrackInstallerInCentOS) Uninstall() (err error) { 34 | err = d.Execer.RunCommand("yum", "remove", "-y", 35 | "conntrack") 36 | return 37 | } 38 | 39 | // WaitForStart waits for the service be started 40 | func (d *conntrackInstallerInCentOS) WaitForStart() (ok bool, err error) { 41 | ok = true 42 | return 43 | } 44 | 45 | // Start starts the Docker service 46 | func (d *conntrackInstallerInCentOS) Start() error { 47 | fmt.Println("not supported yet") 48 | return nil 49 | } 50 | 51 | // Stop stops the Docker service 52 | func (d *conntrackInstallerInCentOS) Stop() error { 53 | fmt.Println("not supported yet") 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/os/yum/doc.go: -------------------------------------------------------------------------------- 1 | // Package yum provides the common usage of yum package manager. 2 | // See also https://en.wikipedia.org/wiki/Yum_(software). 3 | package yum 4 | -------------------------------------------------------------------------------- /pkg/os/yum/docker.go: -------------------------------------------------------------------------------- 1 | package yum 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 9 | ) 10 | 11 | // dockerInstallerInCentOS is the installer of Docker in CentOS 12 | type dockerInstallerInCentOS struct { 13 | Execer fakeruntime.Execer 14 | count int 15 | } 16 | 17 | // Available check if support current platform 18 | func (d *dockerInstallerInCentOS) Available() (ok bool) { 19 | if d.Execer.OS() == "linux" { 20 | _, err := d.Execer.LookPath("yum") 21 | ok = err == nil 22 | } 23 | return 24 | } 25 | 26 | // Install installs the Docker 27 | func (d *dockerInstallerInCentOS) Install() (err error) { 28 | if err = d.Execer.RunCommand("yum", "install", "-y", 29 | "yum-utils"); err != nil { 30 | return 31 | } 32 | if err = d.Execer.RunCommand("yum-config-manager", "--add-repo", 33 | "https://download.docker.com/linux/centos/docker-ce.repo"); err != nil { 34 | return 35 | } 36 | if err = d.Execer.RunCommand("yum", "install", "-y", 37 | "docker-ce", 38 | "docker-ce-cli", 39 | "containerd.io"); err != nil { 40 | return 41 | } 42 | return 43 | } 44 | 45 | // Uninstall uninstalls the Docker 46 | func (d *dockerInstallerInCentOS) Uninstall() (err error) { 47 | err = d.Execer.RunCommand("yum", "remove", "-y", 48 | "docker", 49 | "docker-client", 50 | "docker-client-latest", 51 | "docker-common", 52 | "docker-latest", 53 | "docker-latest-logrotate", 54 | "docker-logrotate", 55 | "docker-engine", 56 | "docker-ce", 57 | "docker-ce-cli", 58 | "containerd.io") 59 | return 60 | } 61 | 62 | // WaitForStart waits for the service be started 63 | func (d *dockerInstallerInCentOS) WaitForStart() (ok bool, err error) { 64 | var result string 65 | if result, err = d.Execer.RunCommandAndReturn("systemctl", "", "status", "docker"); err != nil { 66 | return 67 | } else if strings.Contains(result, "Unit docker.service could not be found") { 68 | err = fmt.Errorf("unit docker.service could not be found") 69 | } else if strings.Contains(result, "Active: active") { 70 | ok = true 71 | } else { 72 | if d.count > 0 { 73 | fmt.Println("waiting for Docker service start") 74 | } else if d.count > 9 { 75 | return 76 | } 77 | 78 | d.count++ 79 | time.Sleep(time.Second * 1) 80 | return d.WaitForStart() 81 | } 82 | return 83 | } 84 | 85 | // Start starts the Docker service 86 | func (d *dockerInstallerInCentOS) Start() error { 87 | return d.Execer.RunCommand("systemctl", "start", "docker") 88 | } 89 | 90 | // Stop stops the Docker service 91 | func (d *dockerInstallerInCentOS) Stop() error { 92 | return d.Execer.RunCommand("systemctl", "stop", "docker") 93 | } 94 | -------------------------------------------------------------------------------- /pkg/os/yum/git.go: -------------------------------------------------------------------------------- 1 | package yum 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // gitInstallerInCentOS is the installer of git in CentOS 10 | type gitInstallerInCentOS struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *gitInstallerInCentOS) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("yum") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the git 24 | func (d *gitInstallerInCentOS) Install() (err error) { 25 | if err = d.Execer.RunCommand("yum", "install", "-y", 26 | "git"); err != nil { 27 | return 28 | } 29 | return 30 | } 31 | 32 | // Uninstall uninstalls the git 33 | func (d *gitInstallerInCentOS) Uninstall() (err error) { 34 | err = d.Execer.RunCommand("yum", "remove", "-y", 35 | "git") 36 | return 37 | } 38 | 39 | // WaitForStart waits for the service be started 40 | func (d *gitInstallerInCentOS) WaitForStart() (ok bool, err error) { 41 | ok = true 42 | return 43 | } 44 | 45 | // Start starts the git service 46 | func (d *gitInstallerInCentOS) Start() error { 47 | fmt.Println("not supported yet") 48 | return nil 49 | } 50 | 51 | // Stop stops the git service 52 | func (d *gitInstallerInCentOS) Stop() error { 53 | fmt.Println("not supported yet") 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/os/yum/golang.go: -------------------------------------------------------------------------------- 1 | package yum 2 | 3 | import ( 4 | // Enable go embed 5 | _ "embed" 6 | "fmt" 7 | "os" 8 | 9 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 10 | ) 11 | 12 | //go:embed resource/go-repo.repo 13 | var goRepo string 14 | 15 | // golangInstallerInCentOS is the installer of golang in CentOS 16 | type golangInstallerInCentOS struct { 17 | Execer fakeruntime.Execer 18 | } 19 | 20 | // Available check if support current platform 21 | func (d *golangInstallerInCentOS) Available() (ok bool) { 22 | if d.Execer.OS() == "linux" { 23 | _, err := d.Execer.LookPath("yum") 24 | ok = err == nil 25 | } 26 | return 27 | } 28 | 29 | // Install installs the golang 30 | func (d *golangInstallerInCentOS) Install() (err error) { 31 | if err = d.Execer.RunCommand("rpm", "--import", "https://mirror.go-repo.io/centos/RPM-GPG-KEY-GO-REPO"); err != nil { 32 | return 33 | } 34 | 35 | if err = os.WriteFile("/etc/yum.repos.d/go-repo.repo", []byte(goRepo), 0644); err != nil { 36 | err = fmt.Errorf("failed to save go-repo.repo, error: %v", err) 37 | return 38 | } 39 | if err = d.Execer.RunCommand("yum", "install", "-y", "golang"); err != nil { 40 | return 41 | } 42 | return 43 | } 44 | 45 | // Uninstall uninstalls the golang 46 | func (d *golangInstallerInCentOS) Uninstall() (err error) { 47 | err = d.Execer.RunCommand("yum", "remove", "-y", "golang") 48 | return 49 | } 50 | 51 | // WaitForStart waits for the service be started 52 | func (d *golangInstallerInCentOS) WaitForStart() (ok bool, err error) { 53 | ok = true 54 | return 55 | } 56 | 57 | // Start starts the golang service 58 | func (d *golangInstallerInCentOS) Start() error { 59 | fmt.Println("not supported yet") 60 | return nil 61 | } 62 | 63 | // Stop stops the golang service 64 | func (d *golangInstallerInCentOS) Stop() error { 65 | fmt.Println("not supported yet") 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/os/yum/init.go: -------------------------------------------------------------------------------- 1 | package yum 2 | 3 | import ( 4 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 5 | "github.com/linuxsuren/http-downloader/pkg/os/core" 6 | ) 7 | 8 | // SetInstallerRegistry sets the installer of registry 9 | func SetInstallerRegistry(registry core.InstallerRegistry, defaultExecer fakeruntime.Execer) { 10 | registry.Registry("docker", &dockerInstallerInCentOS{Execer: defaultExecer}) 11 | registry.Registry("conntrack", &conntrackInstallerInCentOS{Execer: defaultExecer}) 12 | registry.Registry("socat", &socatInstallerInCentOS{Execer: defaultExecer}) 13 | registry.Registry("vim", &vimInstallerInCentOS{Execer: defaultExecer}) 14 | registry.Registry("golang", &golangInstallerInCentOS{Execer: defaultExecer}) 15 | registry.Registry("git", &gitInstallerInCentOS{Execer: defaultExecer}) 16 | registry.Registry("bash-completion", &bashCompletionInstallerInCentOS{Execer: defaultExecer}) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/os/yum/init_test.go: -------------------------------------------------------------------------------- 1 | package yum 2 | 3 | import ( 4 | "testing" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | "github.com/linuxsuren/http-downloader/pkg/os/core" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCommonCase(t *testing.T) { 12 | registry := &core.FakeRegistry{} 13 | SetInstallerRegistry(registry, fakeruntime.FakeExecer{ 14 | ExpectOutput: "Active: active", 15 | ExpectOS: "linux", 16 | }) 17 | 18 | registry.Walk(func(s string, i core.Installer) { 19 | t.Run(s, func(t *testing.T) { 20 | assert.True(t, i.Available()) 21 | if s != "golang" { 22 | assert.Nil(t, i.Install()) 23 | } 24 | assert.Nil(t, i.Uninstall()) 25 | if s != "docker" { 26 | assert.Nil(t, i.Start()) 27 | assert.Nil(t, i.Stop()) 28 | } 29 | ok, err := i.WaitForStart() 30 | assert.True(t, ok) 31 | assert.Nil(t, err) 32 | }) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/os/yum/resource/go-repo.repo: -------------------------------------------------------------------------------- 1 | [go-repo] 2 | name=go-repo - CentOS 3 | baseurl=https://mirror.go-repo.io/centos/$releasever/$basearch/ 4 | enabled=1 5 | gpgcheck=1 6 | gpgkey=https://mirror.go-repo.io/centos/RPM-GPG-KEY-GO-REPO 7 | -------------------------------------------------------------------------------- /pkg/os/yum/resource/kubernetes.repo: -------------------------------------------------------------------------------- 1 | [kubernetes] 2 | name=Kubernetes 3 | baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64 4 | enabled=1 5 | gpgcheck=1 6 | repo_gpgcheck=1 7 | gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg 8 | -------------------------------------------------------------------------------- /pkg/os/yum/socat.go: -------------------------------------------------------------------------------- 1 | package yum 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // socatInstallerInCentOS is the installer of socat in CentOS 10 | type socatInstallerInCentOS struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *socatInstallerInCentOS) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("yum") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the socat 24 | func (d *socatInstallerInCentOS) Install() (err error) { 25 | if err = d.Execer.RunCommand("yum", "install", "-y", 26 | "socat"); err != nil { 27 | return 28 | } 29 | return 30 | } 31 | 32 | // Uninstall uninstalls the socat 33 | func (d *socatInstallerInCentOS) Uninstall() (err error) { 34 | err = d.Execer.RunCommand("yum", "remove", "-y", 35 | "socat") 36 | return 37 | } 38 | 39 | // WaitForStart waits for the service be started 40 | func (d *socatInstallerInCentOS) WaitForStart() (ok bool, err error) { 41 | ok = true 42 | return 43 | } 44 | 45 | // Start starts the socat service 46 | func (d *socatInstallerInCentOS) Start() error { 47 | fmt.Println("not supported yet") 48 | return nil 49 | } 50 | 51 | // Stop stops the socat service 52 | func (d *socatInstallerInCentOS) Stop() error { 53 | fmt.Println("not supported yet") 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/os/yum/vim.go: -------------------------------------------------------------------------------- 1 | package yum 2 | 3 | import ( 4 | "fmt" 5 | 6 | fakeruntime "github.com/linuxsuren/go-fake-runtime" 7 | ) 8 | 9 | // vimInstallerInCentOS is the installer of vim in CentOS 10 | type vimInstallerInCentOS struct { 11 | Execer fakeruntime.Execer 12 | } 13 | 14 | // Available check if support current platform 15 | func (d *vimInstallerInCentOS) Available() (ok bool) { 16 | if d.Execer.OS() == "linux" { 17 | _, err := d.Execer.LookPath("yum") 18 | ok = err == nil 19 | } 20 | return 21 | } 22 | 23 | // Install installs the vim 24 | func (d *vimInstallerInCentOS) Install() (err error) { 25 | if err = d.Execer.RunCommand("yum", "install", "-y", 26 | "vim"); err != nil { 27 | return 28 | } 29 | return 30 | } 31 | 32 | // Uninstall uninstalls the vim 33 | func (d *vimInstallerInCentOS) Uninstall() (err error) { 34 | err = d.Execer.RunCommand("yum", "remove", "-y", 35 | "vim") 36 | return 37 | } 38 | 39 | // WaitForStart waits for the service be started 40 | func (d *vimInstallerInCentOS) WaitForStart() (ok bool, err error) { 41 | ok = true 42 | return 43 | } 44 | 45 | // Start starts the vim service 46 | func (d *vimInstallerInCentOS) Start() error { 47 | fmt.Println("not supported yet") 48 | return nil 49 | } 50 | 51 | // Stop stops the vim service 52 | func (d *vimInstallerInCentOS) Stop() error { 53 | fmt.Println("not supported yet") 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/progress.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | /** 4 | * This file was deprecated, please use the following package instead 5 | * github.com/linuxsuren/http-downloader/pkg/net 6 | */ 7 | 8 | import ( 9 | "github.com/linuxsuren/http-downloader/pkg/net" 10 | ) 11 | 12 | // ProgressIndicator hold the progress of io operation 13 | type ProgressIndicator net.ProgressIndicator 14 | 15 | // Init set the default value for progress indicator 16 | func (i *ProgressIndicator) Init() { 17 | (*net.ProgressIndicator)(i).Init() 18 | } 19 | 20 | // Write writes the progress 21 | func (i *ProgressIndicator) Write(p []byte) (n int, err error) { 22 | return (*net.ProgressIndicator)(i).Write(p) 23 | } 24 | 25 | // Read reads the progress 26 | func (i *ProgressIndicator) Read(p []byte) (n int, err error) { 27 | return (*net.ProgressIndicator)(i).Read(p) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/release.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "github.com/google/go-github/v29/github" 6 | ) 7 | 8 | // ReleaseClient is the client of jcli github 9 | type ReleaseClient struct { 10 | Client *github.Client 11 | Org string 12 | Repo string 13 | 14 | ctx context.Context 15 | } 16 | 17 | // ReleaseAsset is the asset from GitHub release 18 | type ReleaseAsset struct { 19 | TagName string 20 | Body string 21 | } 22 | 23 | // Init init the GitHub client 24 | func (g *ReleaseClient) Init() { 25 | g.Client = github.NewClient(nil) 26 | g.ctx = context.TODO() 27 | } 28 | 29 | // ListReleases returns the release list 30 | func (g *ReleaseClient) ListReleases(owner, repo string, count int) (list []ReleaseAsset, err error) { 31 | opt := &github.ListOptions{ 32 | PerPage: count, 33 | } 34 | 35 | var releaseList []*github.RepositoryRelease 36 | if releaseList, _, err = g.Client.Repositories.ListReleases(g.ctx, owner, repo, opt); err == nil { 37 | for i := range releaseList { 38 | list = append(list, ReleaseAsset{ 39 | TagName: releaseList[i].GetTagName(), 40 | Body: releaseList[i].GetBody(), 41 | }) 42 | } 43 | } 44 | return 45 | } 46 | 47 | // GetLatestJCLIAsset returns the latest jcli asset 48 | // deprecated, please use GetLatestAsset instead 49 | func (g *ReleaseClient) GetLatestJCLIAsset() (*ReleaseAsset, error) { 50 | return g.GetLatestReleaseAsset(g.Org, g.Repo) 51 | } 52 | 53 | // GetLatestAsset returns the latest release asset which can accept preRelease or not 54 | func (g *ReleaseClient) GetLatestAsset(acceptPreRelease bool) (*ReleaseAsset, error) { 55 | if acceptPreRelease { 56 | return g.GetLatestPreReleaseAsset(g.Org, g.Repo) 57 | } 58 | return g.GetLatestReleaseAsset(g.Org, g.Repo) 59 | } 60 | 61 | // GetLatestPreReleaseAsset returns the release asset that could be preRelease 62 | func (g *ReleaseClient) GetLatestPreReleaseAsset(owner, repo string) (ra *ReleaseAsset, err error) { 63 | ctx := context.Background() 64 | 65 | var list []*github.RepositoryRelease 66 | if list, _, err = g.Client.Repositories.ListReleases(ctx, owner, repo, &github.ListOptions{ 67 | Page: 1, 68 | PerPage: 5, 69 | }); err == nil { 70 | ra = &ReleaseAsset{ 71 | TagName: list[0].GetTagName(), 72 | Body: list[0].GetBody(), 73 | } 74 | } 75 | return 76 | } 77 | 78 | // GetLatestReleaseAsset returns the latest release asset 79 | func (g *ReleaseClient) GetLatestReleaseAsset(owner, repo string) (ra *ReleaseAsset, err error) { 80 | ctx := context.Background() 81 | 82 | var release *github.RepositoryRelease 83 | if release, _, err = g.Client.Repositories.GetLatestRelease(ctx, owner, repo); err == nil { 84 | ra = &ReleaseAsset{ 85 | TagName: release.GetTagName(), 86 | Body: release.GetBody(), 87 | } 88 | } 89 | return 90 | } 91 | 92 | // GetJCLIAsset returns the asset from a tag name 93 | func (g *ReleaseClient) GetJCLIAsset(tagName string) (*ReleaseAsset, error) { 94 | return g.GetReleaseAssetByTagName(g.Org, g.Repo, tagName) 95 | } 96 | 97 | // GetReleaseAssetByTagName returns the release asset by tag name 98 | func (g *ReleaseClient) GetReleaseAssetByTagName(owner, repo, tagName string) (ra *ReleaseAsset, err error) { 99 | var list []ReleaseAsset 100 | if list, err = g.ListReleases(owner, repo, 99999); err == nil { 101 | for _, item := range list { 102 | if item.TagName == tagName { 103 | ra = &ReleaseAsset{ 104 | TagName: item.TagName, 105 | Body: item.Body, 106 | } 107 | break 108 | } 109 | } 110 | } 111 | return 112 | } 113 | -------------------------------------------------------------------------------- /pkg/setup_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo/reporters" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestUtils(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | junitReporter := reporters.NewJUnitReporter("test-utils.xml") 15 | RunSpecsWithDefaultAndCustomReporters(t, "util", []Reporter{junitReporter}) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/version/doc.go: -------------------------------------------------------------------------------- 1 | // Package version provides the functions which related to version parse. 2 | package version 3 | -------------------------------------------------------------------------------- /pkg/version/parser.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/blang/semver/v4" 8 | ) 9 | 10 | // GetVersion returns the version string from a string 11 | func GetVersion(output string) string { 12 | ver, _ := GetSemVersion(output) 13 | return ver.String() 14 | } 15 | 16 | // GreatThan return true if target is great than output 17 | func GreatThan(target, output string) (ok bool) { 18 | var ( 19 | targetVer semver.Version 20 | currentVer semver.Version 21 | err error 22 | ) 23 | 24 | if targetVer, err = semver.ParseTolerant(target); err == nil { 25 | if currentVer, err = GetSemVersion(output); err == nil { 26 | ok = targetVer.GT(currentVer) 27 | } 28 | } 29 | return 30 | } 31 | 32 | // GetSemVersion parses the output and returns the semversion 33 | func GetSemVersion(output string) (semVersion semver.Version, err error) { 34 | var verReg *regexp.Regexp 35 | verReg, err = regexp.Compile(`(v\d.+\d+.\d+)|(\d.+\d+.\d+)`) 36 | if err == nil { 37 | for _, line := range strings.Split(output, "\n") { 38 | line = verReg.FindString(line) 39 | if line == "" { 40 | continue 41 | } 42 | 43 | semVersion, err = semver.ParseTolerant(line) 44 | if err == nil { 45 | break 46 | } 47 | } 48 | } 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /pkg/version/parser_test.go: -------------------------------------------------------------------------------- 1 | package version_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/linuxsuren/http-downloader/pkg/version" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetVersion(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | output string 14 | expectVer string 15 | }{{ 16 | name: "invalid", 17 | output: "abc", 18 | expectVer: "0.0.0", 19 | }, { 20 | name: "normal", 21 | output: `minikube version: v1.28.0 22 | commit: 986b1ebd987211ed16f8cc10aed7d2c42fc8392f`, 23 | expectVer: "1.28.0", 24 | }, { 25 | name: "complex", 26 | output: ` 27 | ____ __.________ 28 | | |/ _/ __ \______ 29 | | < \____ / ___/ 30 | | | \ / /\___ \ 31 | |____|__ \ /____//____ > 32 | \/ \/ 33 | 34 | Version: v0.26.3 35 | Commit: 0893f13b3ca6b563dd0c38fdebaefdb8be594825 36 | Date: 2022-08-04T05:18:24Z`, 37 | expectVer: "0.26.3", 38 | }, { 39 | name: "without prefix v", 40 | output: "git version 2.34.1", 41 | expectVer: "2.34.1", 42 | }} 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | result := version.GetVersion(tt.output) 46 | assert.Equal(t, tt.expectVer, result) 47 | }) 48 | } 49 | } 50 | 51 | func TestGreatThan(t *testing.T) { 52 | tests := []struct { 53 | name string 54 | target string 55 | output string 56 | expect bool 57 | }{{ 58 | name: "normal", 59 | target: "v1.28.1", 60 | output: `minikube version: v1.28.0 61 | commit: 986b1ebd987211ed16f8cc10aed7d2c42fc8392f`, 62 | expect: true, 63 | }, { 64 | name: "normal", 65 | target: "v1.28.0", 66 | output: `minikube version: v1.28.1 67 | commit: 986b1ebd987211ed16f8cc10aed7d2c42fc8392f`, 68 | expect: false, 69 | }, { 70 | name: "version is equal", 71 | target: "v1.28.0", 72 | output: `minikube version: v1.28.0 73 | commit: 986b1ebd987211ed16f8cc10aed7d2c42fc8392f`, 74 | expect: false, 75 | }} 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | result := version.GreatThan(tt.target, tt.output) 79 | assert.Equal(t, tt.expect, result) 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /wix.json: -------------------------------------------------------------------------------- 1 | { 2 | "product": "HTTP downloader", 3 | "company": "LinuxSuRen", 4 | "license": "LICENSE", 5 | "upgrade-code": "7c0a5736-5b8e-4176-b350-613fa2d8a1b3", 6 | "files": { 7 | "guid": "6e6dcb19-3cf6-46d1-ac56-c6fb39485c9d", 8 | "items": [ 9 | "hd.exe" 10 | ] 11 | }, 12 | "env": { 13 | "guid": "94faac3d-4478-431c-8497-fba55dcfb249", 14 | "vars": [ 15 | { 16 | "name": "PATH", 17 | "value": "[INSTALLDIR]", 18 | "permanent": "yes", 19 | "system": "no", 20 | "action": "set", 21 | "part": "last" 22 | }, 23 | { 24 | "name": "PATH", 25 | "value": "C:\\Program Files (x86)\\Common Files", 26 | "permanent": "yes", 27 | "system": "no", 28 | "action": "set", 29 | "part": "last" 30 | } 31 | ] 32 | }, 33 | "shortcuts": {} 34 | } 35 | --------------------------------------------------------------------------------