├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── docker.yml │ ├── goreleaser.yml │ └── testing.yml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yaml ├── .hadolint.yaml ├── DOCS.md ├── LICENSE ├── Makefile ├── README.md ├── README.zh-cn.md ├── README.zh-tw.md ├── bearer.yml ├── command.go ├── command_test.go ├── docker └── Dockerfile ├── go.mod ├── go.sum ├── main.go ├── path.go ├── path_windows.go ├── path_windows_test.go ├── plugin.go ├── plugin_test.go └── tests ├── .ssh ├── id_rsa ├── id_rsa.pub ├── test └── test.pub ├── a.txt ├── b.txt ├── entrypoint.sh └── global ├── c.txt ├── d.txt └── e.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://www.paypal.me/appleboy46'] 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: "41 23 * * 6" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["go"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v3 55 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - "v*" 9 | pull_request: 10 | branches: 11 | - "master" 12 | 13 | jobs: 14 | build-docker: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Setup go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: "^1" 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Build binary 27 | run: | 28 | make build_docker 29 | 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v3 32 | 33 | - name: Set up Docker Buildx 34 | uses: docker/setup-buildx-action@v3 35 | 36 | - name: Login to Docker Hub 37 | uses: docker/login-action@v3 38 | with: 39 | username: ${{ secrets.DOCKERHUB_USERNAME }} 40 | password: ${{ secrets.DOCKERHUB_TOKEN }} 41 | 42 | - name: Login to GitHub Container Registry 43 | uses: docker/login-action@v3 44 | with: 45 | registry: ghcr.io 46 | username: ${{ github.repository_owner }} 47 | password: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | - name: Docker meta 50 | id: docker-meta 51 | uses: docker/metadata-action@v5 52 | with: 53 | images: | 54 | ${{ github.repository }} 55 | ghcr.io/${{ github.repository }} 56 | tags: | 57 | type=raw,value=latest,enable={{is_default_branch}} 58 | type=semver,pattern={{version}} 59 | type=semver,pattern={{major}}.{{minor}} 60 | type=semver,pattern={{major}} 61 | 62 | - name: Build and push 63 | uses: docker/build-push-action@v6 64 | with: 65 | context: . 66 | platforms: linux/amd64,linux/arm64 67 | file: docker/Dockerfile 68 | push: ${{ github.event_name != 'pull_request' }} 69 | tags: ${{ steps.docker-meta.outputs.tags }} 70 | labels: ${{ steps.docker-meta.outputs.labels }} 71 | cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache 72 | cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max 73 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: Goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Setup go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: "^1" 23 | 24 | - name: Run GoReleaser 25 | uses: goreleaser/goreleaser-action@v6 26 | with: 27 | # either 'goreleaser' (default) or 'goreleaser-pro' 28 | distribution: goreleaser 29 | version: latest 30 | args: release --clean 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Testing 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | 14 | - name: Setup go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version-file: go.mod 18 | check-latest: true 19 | 20 | - name: Setup golangci-lint 21 | uses: golangci/golangci-lint-action@v7 22 | with: 23 | version: v2.0 24 | args: --verbose 25 | 26 | - uses: hadolint/hadolint-action@v3.1.0 27 | name: hadolint for Dockerfile 28 | with: 29 | dockerfile: docker/Dockerfile 30 | 31 | # This step uses the Bearer GitHub Action to scan for sensitive data in the codebase. 32 | # The 'uses' keyword specifies the action to be used, in this case, 'bearer/bearer-action' at version 'v2'. 33 | # The 'with' keyword provides input parameters for the action: 34 | # - 'diff: true' indicates that the action should only scan the changes in the current pull request or commit. 35 | - name: Bearer 36 | uses: bearer/bearer-action@v2 37 | with: 38 | diff: true 39 | 40 | testing: 41 | runs-on: ubuntu-latest 42 | container: golang:1.23-alpine 43 | steps: 44 | - name: Checkout repository 45 | uses: actions/checkout@v4 46 | 47 | - name: setup sshd server 48 | run: | 49 | apk add git make curl perl bash build-base zlib-dev ucl-dev 50 | make ssh-server 51 | 52 | - name: testing 53 | run: | 54 | make test 55 | 56 | - name: Upload coverage to Codecov 57 | uses: codecov/codecov-action@v4 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | drone-scp 26 | coverage.txt 27 | .env 28 | dist 29 | .cover 30 | release 31 | bin 32 | .idea 33 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - asciicheck 5 | - durationcheck 6 | - errorlint 7 | - gosec 8 | - misspell 9 | - nakedret 10 | - nilerr 11 | - nolintlint 12 | - perfsprint 13 | - revive 14 | - usestdlibvars 15 | - wastedassign 16 | settings: 17 | gosec: 18 | includes: 19 | - G102 20 | - G106 21 | - G108 22 | - G109 23 | - G111 24 | - G112 25 | - G201 26 | - G203 27 | perfsprint: 28 | int-conversion: true 29 | err-error: true 30 | errorf: true 31 | sprintf1: true 32 | strconcat: true 33 | exclusions: 34 | generated: lax 35 | presets: 36 | - comments 37 | - common-false-positives 38 | - legacy 39 | - std-error-handling 40 | paths: 41 | - third_party$ 42 | - builtin$ 43 | - examples$ 44 | formatters: 45 | enable: 46 | - gci 47 | - gofmt 48 | - goimports 49 | exclusions: 50 | generated: lax 51 | paths: 52 | - third_party$ 53 | - builtin$ 54 | - examples$ 55 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - darwin 10 | - linux 11 | - windows 12 | - freebsd 13 | goarch: 14 | - amd64 15 | - arm 16 | - arm64 17 | goarm: 18 | - "5" 19 | - "6" 20 | - "7" 21 | ignore: 22 | - goos: darwin 23 | goarch: arm 24 | - goos: darwin 25 | goarch: ppc64le 26 | - goos: darwin 27 | goarch: s390x 28 | - goos: windows 29 | goarch: ppc64le 30 | - goos: windows 31 | goarch: s390x 32 | - goos: windows 33 | goarch: arm 34 | goarm: "5" 35 | - goos: windows 36 | goarch: arm 37 | goarm: "6" 38 | - goos: windows 39 | goarch: arm 40 | goarm: "7" 41 | - goos: windows 42 | goarch: arm64 43 | - goos: freebsd 44 | goarch: ppc64le 45 | - goos: freebsd 46 | goarch: s390x 47 | - goos: freebsd 48 | goarch: arm 49 | goarm: "5" 50 | - goos: freebsd 51 | goarch: arm 52 | goarm: "6" 53 | - goos: freebsd 54 | goarch: arm 55 | goarm: "7" 56 | - goos: freebsd 57 | goarch: arm64 58 | flags: 59 | - -trimpath 60 | ldflags: 61 | - -s -w 62 | - -X main.Version={{.Version}} 63 | binary: >- 64 | {{ .ProjectName }}- 65 | {{- if .IsSnapshot }}{{ .Branch }}- 66 | {{- else }}{{- .Version }}-{{ end }} 67 | {{- .Os }}- 68 | {{- if eq .Arch "amd64" }}amd64 69 | {{- else if eq .Arch "amd64_v1" }}amd64 70 | {{- else if eq .Arch "386" }}386 71 | {{- else }}{{ .Arch }}{{ end }} 72 | {{- if .Arm }}-{{ .Arm }}{{ end }} 73 | no_unique_dist_dir: true 74 | hooks: 75 | post: 76 | - cmd: xz -k -9 {{ .Path }} 77 | dir: ./dist/ 78 | 79 | archives: 80 | - format: binary 81 | name_template: "{{ .Binary }}" 82 | allow_different_binary_count: true 83 | 84 | checksum: 85 | name_template: "checksums.txt" 86 | extra_files: 87 | - glob: ./**.xz 88 | 89 | snapshot: 90 | name_template: "{{ incpatch .Version }}" 91 | 92 | release: 93 | # You can add extra pre-existing files to the release. 94 | # The filename on the release will be the last part of the path (base). 95 | # If another file with the same name exists, the last one found will be used. 96 | # 97 | # Templates: allowed 98 | extra_files: 99 | - glob: ./**.xz 100 | 101 | changelog: 102 | use: github 103 | groups: 104 | - title: Features 105 | regexp: "^.*feat[(\\w)]*:+.*$" 106 | order: 0 107 | - title: "Bug fixes" 108 | regexp: "^.*fix[(\\w)]*:+.*$" 109 | order: 1 110 | - title: "Enhancements" 111 | regexp: "^.*chore[(\\w)]*:+.*$" 112 | order: 2 113 | - title: "Refactor" 114 | regexp: "^.*refactor[(\\w)]*:+.*$" 115 | order: 3 116 | - title: "Build process updates" 117 | regexp: ^.*?(build|ci)(\(.+\))??!?:.+$ 118 | order: 4 119 | - title: "Documentation updates" 120 | regexp: ^.*?docs?(\(.+\))??!?:.+$ 121 | order: 4 122 | - title: Others 123 | order: 999 124 | -------------------------------------------------------------------------------- /.hadolint.yaml: -------------------------------------------------------------------------------- 1 | ignored: 2 | - DL3018 3 | - DL3008 4 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2017-01-06T00:00:00+00:00 3 | title: SCP 4 | name: SCP 5 | description: Deploy artifacts using SSH/SCP 6 | author: appleboy 7 | tags: [ publish, ssh, scp ] 8 | logo: term.svg 9 | repo: appleboy/drone-scp 10 | image: appleboy/drone-scp 11 | containerImage: appleboy/drone-scp 12 | containerImageUrl: https://hub.docker.com/r/appleboy/drone-scp 13 | url: https://github.com/appleboy/drone-scp 14 | --- 15 | 16 | The SCP plugin copy files and artifacts to target host machine via SSH. The below pipeline configuration demonstrates simple usage: 17 | 18 | ```yaml 19 | - name: scp files 20 | image: appleboy/drone-scp 21 | settings: 22 | host: example.com 23 | username: foo 24 | password: bar 25 | port: 22 26 | target: /var/www/deploy/${DRONE_REPO_OWNER}/${DRONE_REPO_NAME} 27 | source: release.tar.gz 28 | ``` 29 | 30 | Example configuration with multiple source and target folder: 31 | 32 | ```diff 33 | - name: scp files 34 | image: appleboy/drone-scp 35 | settings: 36 | host: example.com 37 | target: 38 | + - /home/deploy/web1 39 | + - /home/deploy/web2 40 | source: 41 | + - release_1.tar.gz 42 | + - release_2.tar.gz 43 | ``` 44 | 45 | Example configuration with multiple host: 46 | 47 | ```diff 48 | - name: scp files 49 | image: appleboy/drone-scp 50 | settings: 51 | - host: example.com 52 | + host: 53 | + - example1.com 54 | + - example2.com 55 | target: /home/deploy/web 56 | source: release.tar.gz 57 | ``` 58 | 59 | Example configuration with wildcard pattern of source list: 60 | 61 | ```diff 62 | - name: scp files 63 | image: appleboy/drone-scp 64 | settings: 65 | host: 66 | - example1.com 67 | - example2.com 68 | target: /home/deploy/web 69 | source: 70 | - - release/backend.tar.gz 71 | - - release/images.tar.gz 72 | + - release/*.tar.gz 73 | ``` 74 | 75 | Remove target folder before copy files and artifacts to target: 76 | 77 | ```diff 78 | - name: scp files 79 | image: appleboy/drone-scp 80 | settings: 81 | target: /home/deploy/web 82 | source: release.tar.gz 83 | + rm: true 84 | ``` 85 | 86 | Example for remove the specified number of leading path elements: 87 | 88 | ```diff 89 | - name: scp files 90 | image: appleboy/drone-scp 91 | settings: 92 | host: example.com 93 | target: /home/deploy/web 94 | source: dist/release.tar.gz 95 | + strip_components: 1 96 | ``` 97 | 98 | Example configuration using `SSHProxyCommand`: 99 | 100 | ```diff 101 | - name: scp files 102 | image: appleboy/drone-scp 103 | settings: 104 | host: 105 | - example1.com 106 | - example2.com 107 | target: /home/deploy/web 108 | source: 109 | - release/*.tar.gz 110 | + proxy_host: 10.130.33.145 111 | + proxy_user: ubuntu 112 | + proxy_port: 22 113 | + proxy_password: 1234 114 | ``` 115 | 116 | Example configuration using password from secrets: 117 | 118 | ```diff 119 | - name: scp files 120 | image: appleboy/drone-scp 121 | settings: 122 | host: 123 | - example1.com 124 | - example2.com 125 | user: ubuntu 126 | port: 22 127 | - password: 1234 128 | + password: 129 | + from_secret: ssh_password 130 | target: /home/deploy/web 131 | source: 132 | - release/*.tar.gz 133 | ``` 134 | 135 | Example configuration using command timeout: 136 | 137 | ```diff 138 | - name: scp files 139 | image: appleboy/drone-scp 140 | settings: 141 | host: 142 | - example1.com 143 | - example2.com 144 | user: ubuntu 145 | password: 146 | from_secret: ssh_password 147 | port: 22 148 | - command_timeout: 120 149 | + command_timeout: 2m 150 | target: /home/deploy/web 151 | source: 152 | - release/*.tar.gz 153 | ``` 154 | 155 | Example configuration for ignore list: 156 | 157 | ```diff 158 | - name: scp files 159 | image: appleboy/drone-scp 160 | settings: 161 | host: 162 | - example1.com 163 | - example2.com 164 | user: ubuntu 165 | password: 166 | from_secret: ssh_password 167 | port: 22 168 | command_timeout: 2m 169 | target: /home/deploy/web 170 | source: 171 | + - !release/README.md 172 | - release/* 173 | ``` 174 | 175 | Example configuration for passphrase which protecting a private key: 176 | 177 | ```diff 178 | - name: scp files 179 | image: appleboy/drone-scp 180 | settings: 181 | host: 182 | - example1.com 183 | - example2.com 184 | user: ubuntu 185 | + key: 186 | + from_secret: ssh_key 187 | + passphrase: 1234 188 | port: 22 189 | command_timeout: 2m 190 | target: /home/deploy/web 191 | source: 192 | - release/* 193 | ``` 194 | 195 | ## Parameter Reference 196 | 197 | host 198 | : target hostname or IP 199 | 200 | port 201 | : ssh port of target host 202 | 203 | username 204 | : account for target host user 205 | 206 | password 207 | : password for target host user 208 | 209 | key 210 | : plain text of user private key 211 | 212 | passphrase 213 | : The purpose of the passphrase is usually to encrypt the private key. 214 | 215 | fingerprint 216 | : fingerprint SHA256 of the host public key, default is to skip verification 217 | 218 | target 219 | : folder path of target host 220 | 221 | source 222 | : source lists you want to copy 223 | 224 | rm 225 | : remove target folder before copy files and artifacts 226 | 227 | timeout 228 | : Timeout is the maximum amount of time for the ssh connection to establish, default is 30 seconds. 229 | 230 | command_timeout 231 | : Command timeout is the maximum amount of time for the execute commands, default is 10 minutes. 232 | 233 | strip_components 234 | : remove the specified number of leading path elements 235 | 236 | tar_tmp_path 237 | : temporary path for tar file on the dest host 238 | 239 | tar_exec 240 | : alternative `tar` executable to on the dest host 241 | 242 | overwrite 243 | : use `--overwrite` flag with tar 244 | 245 | proxy_host 246 | : proxy hostname or IP 247 | 248 | proxy_port 249 | : ssh port of proxy host 250 | 251 | proxy_username 252 | : account for proxy host user 253 | 254 | proxy_password 255 | : password for proxy host user 256 | 257 | proxy_key 258 | : plain text of proxy private key 259 | 260 | proxy_key_path 261 | : key path of proxy private key 262 | 263 | proxy_passphrase 264 | : The purpose of the passphrase is usually to encrypt the private key. 265 | 266 | proxy_fingerprint 267 | : fingerprint SHA256 of the host public key, default is to skip verification 268 | 269 | ## Template Reference 270 | 271 | repo.owner 272 | : repository owner 273 | 274 | repo.name 275 | : repository name 276 | 277 | build.status 278 | : build status type enumeration, either `success` or `failure` 279 | 280 | build.event 281 | : build event type enumeration, one of `push`, `pull_request`, `tag`, `deployment` 282 | 283 | build.number 284 | : build number 285 | 286 | build.commit 287 | : git sha for current commit 288 | 289 | build.branch 290 | : git branch for current commit 291 | 292 | build.tag 293 | : git tag for current commit 294 | 295 | build.ref 296 | : git ref for current commit 297 | 298 | build.author 299 | : git author for current commit 300 | 301 | build.link 302 | : link the the build results in drone 303 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Bo-Yi Wu 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 | EXECUTABLE := drone-scp 2 | GOFMT ?= gofumpt -l -s -w 3 | GO ?= go 4 | GOFILES := $(shell find . -name "*.go" -type f) 5 | HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" ) 6 | 7 | ifneq ($(shell uname), Darwin) 8 | EXTLDFLAGS = -extldflags "-static" $(null) 9 | else 10 | EXTLDFLAGS = 11 | endif 12 | 13 | ifeq ($(HAS_GO), GO) 14 | GOPATH ?= $(shell $(GO) env GOPATH) 15 | export PATH := $(GOPATH)/bin:$(PATH) 16 | 17 | CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766 18 | CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS) 19 | endif 20 | 21 | ifeq ($(OS), Windows_NT) 22 | GOFLAGS := -v -buildmode=exe 23 | EXECUTABLE ?= $(EXECUTABLE).exe 24 | else ifeq ($(OS), Windows) 25 | GOFLAGS := -v -buildmode=exe 26 | EXECUTABLE ?= $(EXECUTABLE).exe 27 | else 28 | GOFLAGS := -v 29 | EXECUTABLE ?= $(EXECUTABLE) 30 | endif 31 | 32 | ifneq ($(DRONE_TAG),) 33 | VERSION ?= $(DRONE_TAG) 34 | else 35 | VERSION ?= $(shell git describe --tags --always || git rev-parse --short HEAD) 36 | endif 37 | 38 | TAGS ?= 39 | LDFLAGS ?= -X 'main.Version=$(VERSION)' 40 | 41 | all: build 42 | 43 | .PHONY: help 44 | help: ## Print this help message. 45 | @echo "Usage: make [target]" 46 | @echo "" 47 | @echo "Targets:" 48 | @echo "" 49 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 50 | 51 | fmt: ## Format the code 52 | @hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 53 | $(GO) install mvdan.cc/gofumpt@latest; \ 54 | fi 55 | $(GOFMT) -w $(GOFILES) 56 | 57 | vet: ## Run go vet 58 | $(GO) vet ./... 59 | 60 | .PHONY: fmt-check 61 | fmt-check: ## Check if the code is formatted 62 | @hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 63 | $(GO) install mvdan.cc/gofumpt; \ 64 | fi 65 | @diff=$$($(GOFMT) -d $(GOFILES)); \ 66 | if [ -n "$$diff" ]; then \ 67 | echo "Please run 'make fmt' and commit the result:"; \ 68 | echo "$${diff}"; \ 69 | exit 1; \ 70 | fi; 71 | 72 | test: ## Run tests 73 | @$(GO) test -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1 74 | 75 | install: $(GOFILES) ## Install the package 76 | $(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' 77 | 78 | build: $(EXECUTABLE) ## Build the package 79 | 80 | $(EXECUTABLE): $(GOFILES) ## Build the package 81 | $(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o bin/$@ 82 | 83 | build_docker: 84 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/amd64/$(DEPLOY_IMAGE) 85 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm64/$(DEPLOY_IMAGE) 86 | 87 | ssh-server: ## Run ssh server 88 | adduser -h /home/drone-scp -s /bin/sh -D -S drone-scp 89 | echo drone-scp:1234 | chpasswd 90 | mkdir -p /home/drone-scp/.ssh 91 | chmod 700 /home/drone-scp/.ssh 92 | cat tests/.ssh/id_rsa.pub >> /home/drone-scp/.ssh/authorized_keys 93 | cat tests/.ssh/test.pub >> /home/drone-scp/.ssh/authorized_keys 94 | chmod 600 /home/drone-scp/.ssh/authorized_keys 95 | chown -R drone-scp /home/drone-scp/.ssh 96 | apk add --update openssh openrc 97 | rm -rf /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_dsa_key 98 | sed -i 's/^#PubkeyAuthentication yes/PubkeyAuthentication yes/g' /etc/ssh/sshd_config 99 | sed -i 's/AllowTcpForwarding no/AllowTcpForwarding yes/g' /etc/ssh/sshd_config 100 | sed -i 's/^#ListenAddress 0.0.0.0/ListenAddress 0.0.0.0/g' /etc/ssh/sshd_config 101 | sed -i 's/^#ListenAddress ::/ListenAddress ::/g' /etc/ssh/sshd_config 102 | ./tests/entrypoint.sh /usr/sbin/sshd -D & 103 | 104 | clean: ## Clean the build 105 | $(GO) clean -x -i ./... 106 | rm -rf coverage.txt $(EXECUTABLE) 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # drone-scp 2 | 3 | [繁體中文](README.zh-tw.md) | [简体中文](README.zh-cn.md) 4 | 5 | [![GoDoc](https://godoc.org/github.com/appleboy/drone-scp?status.svg)](https://godoc.org/github.com/appleboy/drone-scp) 6 | [![Lint and Testing](https://github.com/appleboy/drone-scp/actions/workflows/testing.yml/badge.svg)](https://github.com/appleboy/drone-scp/actions/workflows/testing.yml) 7 | [![codecov](https://codecov.io/gh/appleboy/drone-scp/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/drone-scp) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/drone-scp)](https://goreportcard.com/report/github.com/appleboy/drone-scp) 9 | [![Docker Pulls](https://img.shields.io/docker/pulls/appleboy/drone-scp.svg)](https://hub.docker.com/r/appleboy/drone-scp/) 10 | 11 | Securely transfer files and artifacts via SSH using a standalone binary, Docker container, or [Drone CI](http://docs.drone.io/) integration. 12 | 13 | ## Features 14 | 15 | - [x] Parallel file transfer with multiple routines 16 | - [x] Support for wildcard patterns in source file selection 17 | - [x] Ability to transfer files to multiple destination hosts 18 | - [x] Support for multiple target directories on each host 19 | - [x] Flexible SSH key authentication via file path or raw content 20 | - [x] Advanced networking with SSH ProxyCommand support 21 | 22 | ```sh 23 | +--------+ +----------+ +-----------+ 24 | | Laptop | <--> | Jumphost | <--> | FooServer | 25 | +--------+ +----------+ +-----------+ 26 | 27 | OR 28 | 29 | +--------+ +----------+ +-----------+ 30 | | Laptop | <--> | Firewall | <--> | FooServer | 31 | +--------+ +----------+ +-----------+ 32 | 192.168.1.5 121.1.2.3 10.10.29.68 33 | ``` 34 | 35 | ## Breaking changes 36 | 37 | `v1.5.0`: change command timeout flag to `Duration`. See the following setting: 38 | 39 | ```diff 40 | - name: scp files 41 | image: appleboy/drone-scp 42 | settings: 43 | host: 44 | - example1.com 45 | - example2.com 46 | username: ubuntu 47 | password: 48 | from_secret: ssh_password 49 | port: 22 50 | - command_timeout: 120 51 | + command_timeout: 2m 52 | target: /home/deploy/web 53 | source: 54 | - release/*.tar.gz 55 | ``` 56 | 57 | ## Build or Download a binary 58 | 59 | The pre-compiled binaries can be downloaded from [release page](https://github.com/appleboy/drone-scp/releases). Support the following OS type. 60 | 61 | - Windows amd64/386 62 | - Linux arm/amd64/386 63 | - Darwin amd64/386 64 | 65 | With `Go` installed 66 | 67 | ```sh 68 | export GO111MODULE=on 69 | go get -u -v github.com/appleboy/drone-scp 70 | ``` 71 | 72 | or build the binary with the following command: 73 | 74 | ```sh 75 | export GOOS=linux 76 | export GOARCH=amd64 77 | export CGO_ENABLED=0 78 | export GO111MODULE=on 79 | 80 | go test -cover ./... 81 | 82 | go build -v -a -tags netgo -o release/linux/amd64/drone-scp . 83 | ``` 84 | 85 | ## Docker 86 | 87 | Build the docker image with the following commands: 88 | 89 | ```sh 90 | make docker 91 | ``` 92 | 93 | ## Usage 94 | 95 | There are three ways to send notification. 96 | 97 | - [usage from binary](#usage-from-binary) 98 | - [usage from docker](#usage-from-docker) 99 | - [usage from drone ci](#usage-from-drone-ci) 100 | 101 | ### Usage from binary 102 | 103 | #### Using public key 104 | 105 | ```bash 106 | drone-scp --host example.com \ 107 | --port 22 \ 108 | --username appleboy \ 109 | --key-path "${HOME}/.ssh/id_rsa" \ 110 | --target /home/appleboy/test \ 111 | --source your_local_folder_path 112 | ``` 113 | 114 | #### Using password 115 | 116 | ```diff 117 | drone-scp --host example.com \ 118 | --port 22 \ 119 | --username appleboy \ 120 | + --password xxxxxxx \ 121 | --target /home/appleboy/test \ 122 | --source your_local_folder_path 123 | ``` 124 | 125 | #### Using ssh-agent 126 | 127 | Start your local ssh agent: 128 | 129 | ```bash 130 | eval `ssh-agent -s` 131 | ``` 132 | 133 | Import your local public key `~/.ssh/id_rsa` 134 | 135 | ```sh 136 | ssh-add 137 | ``` 138 | 139 | You don't need to add `--password` or `--key-path` arguments. 140 | 141 | ```bash 142 | drone-scp --host example.com \ 143 | --port 22 \ 144 | --username appleboy \ 145 | --target /home/appleboy/test \ 146 | --source your_local_folder_path 147 | ``` 148 | 149 | #### Send multiple source or target folder and hosts 150 | 151 | ```diff 152 | drone-scp --host example1.com \ 153 | + --host example2.com \ 154 | --port 22 \ 155 | --username appleboy \ 156 | --password xxxxxxx 157 | --target /home/appleboy/test1 \ 158 | + --target /home/appleboy/test2 \ 159 | --source your_local_folder_path_1 160 | + --source your_local_folder_path_2 161 | ``` 162 | 163 | ### Usage from docker 164 | 165 | Using public key 166 | 167 | ```bash 168 | docker run --rm \ 169 | -e SCP_HOST=example.com \ 170 | -e SCP_USERNAME=xxxxxxx \ 171 | -e SCP_PORT=22 \ 172 | -e SCP_KEY_PATH="${HOME}/.ssh/id_rsa" 173 | -e SCP_SOURCE=SOURCE_FILE_LIST \ 174 | -e SCP_TARGET=TARGET_FOLDER_PATH \ 175 | -v $(pwd):$(pwd) \ 176 | -w $(pwd) \ 177 | appleboy/drone-scp 178 | ``` 179 | 180 | Using password 181 | 182 | ```diff 183 | docker run --rm \ 184 | -e SCP_HOST=example.com \ 185 | -e SCP_USERNAME=xxxxxxx \ 186 | -e SCP_PORT=22 \ 187 | + -e SCP_PASSWORD="xxxxxxx" 188 | -e SCP_SOURCE=SOURCE_FILE_LIST \ 189 | -e SCP_TARGET=TARGET_FOLDER_PATH \ 190 | -v $(pwd):$(pwd) \ 191 | -w $(pwd) \ 192 | appleboy/drone-scp 193 | ``` 194 | 195 | Using ssh-agent, start your local ssh agent: 196 | 197 | ```bash 198 | eval `ssh-agent -s` 199 | ``` 200 | 201 | Import your local public key `~/.ssh/id_rsa` 202 | 203 | ```sh 204 | ssh-add 205 | ``` 206 | 207 | You don't need to add `SCP_PASSWORD` or `SCP_KEY_PATH` arguments. 208 | 209 | ```bash 210 | docker run --rm \ 211 | -e SCP_HOST=example.com \ 212 | -e SCP_USERNAME=xxxxxxx \ 213 | -e SCP_PORT=22 \ 214 | -e SCP_SOURCE=SOURCE_FILE_LIST \ 215 | -e SCP_TARGET=TARGET_FOLDER_PATH \ 216 | -v $(pwd):$(pwd) \ 217 | -w $(pwd) \ 218 | appleboy/drone-scp 219 | ``` 220 | 221 | Send multiple source or target folder and hosts 222 | 223 | ```bash 224 | docker run --rm \ 225 | -e SCP_HOST=example1.com,example2.com \ 226 | -e SCP_USERNAME=xxxxxxx \ 227 | -e SCP_PASSWORD=xxxxxxx \ 228 | -e SCP_PORT=22 \ 229 | -e SCP_SOURCE=SOURCE_FILE_LIST_1,SOURCE_FILE_LIST_2 \ 230 | -e SCP_TARGET=TARGET_FOLDER_PATH_1,TARGET_FOLDER_PATH_2 \ 231 | -v $(pwd):$(pwd) \ 232 | -w $(pwd) \ 233 | appleboy/drone-scp 234 | ``` 235 | 236 | ### Usage from drone ci 237 | 238 | Execute from the working directory: 239 | 240 | ```bash 241 | docker run --rm \ 242 | -e PLUGIN_HOST=example.com \ 243 | -e PLUGIN_USERNAME=xxxxxxx \ 244 | -e PLUGIN_PASSWORD=xxxxxxx \ 245 | -e PLUGIN_PORT=xxxxxxx \ 246 | -e PLUGIN_SOURCE=SOURCE_FILE_LIST \ 247 | -e PLUGIN_TARGET=TARGET_FOLDER_PATH \ 248 | -e PLUGIN_RM=false \ 249 | -e PLUGIN_DEBUG=true \ 250 | -v $(pwd):$(pwd) \ 251 | -w $(pwd) \ 252 | appleboy/drone-scp 253 | ``` 254 | 255 | You can get more [information](http://plugins.drone.io/appleboy/drone-scp/) about how to use scp in drone. 256 | 257 | ## Testing 258 | 259 | Test the package with the following command: 260 | 261 | ```sh 262 | make test 263 | ``` 264 | -------------------------------------------------------------------------------- /README.zh-cn.md: -------------------------------------------------------------------------------- 1 | # drone-scp 2 | 3 | [繁體中文](README.zh-tw.md) | [English](README.md) 4 | 5 | [![GoDoc](https://godoc.org/github.com/appleboy/drone-scp?status.svg)](https://godoc.org/github.com/appleboy/drone-scp) 6 | [![Lint and Testing](https://github.com/appleboy/drone-scp/actions/workflows/testing.yml/badge.svg)](https://github.com/appleboy/drone-scp/actions/workflows/testing.yml) 7 | [![codecov](https://codecov.io/gh/appleboy/drone-scp/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/drone-scp) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/drone-scp)](https://goreportcard.com/report/github.com/appleboy/drone-scp) 9 | [![Docker Pulls](https://img.shields.io/docker/pulls/appleboy/drone-scp.svg)](https://hub.docker.com/r/appleboy/drone-scp/) 10 | 11 | 复制文件和工件通过 SSH 使用二进制文件、docker 或 [Drone CI](http://docs.drone.io/)。 12 | 13 | [English](README.md) | [繁體中文](README.zh-tw.md) 14 | 15 | ## 功能 16 | 17 | - [x] 支持例程。 18 | - [x] 支持来源列表中的通配符模式。 19 | - [x] 支持将文件发送到多个主机。 20 | - [x] 支持将文件发送到主机上的多个目标文件夹。 21 | - [x] 支持从绝对路径或原始主体加载 ssh 密钥。 22 | - [x] 支持 SSH ProxyCommand。 23 | 24 | ```sh 25 | +--------+ +----------+ +-----------+ 26 | | Laptop | <--> | Jumphost | <--> | FooServer | 27 | +--------+ +----------+ +-----------+ 28 | 29 | OR 30 | 31 | +--------+ +----------+ +-----------+ 32 | | Laptop | <--> | Firewall | <--> | FooServer | 33 | +--------+ +----------+ +-----------+ 34 | 192.168.1.5 121.1.2.3 10.10.29.68 35 | ``` 36 | 37 | ## Breaking changes 38 | 39 | `v1.5.0`: change command timeout flag to `Duration`. See the following setting: 40 | 41 | ```diff 42 | - name: scp files 43 | image: appleboy/drone-scp 44 | settings: 45 | host: 46 | - example1.com 47 | - example2.com 48 | username: ubuntu 49 | password: 50 | from_secret: ssh_password 51 | port: 22 52 | - command_timeout: 120 53 | + command_timeout: 2m 54 | target: /home/deploy/web 55 | source: 56 | - release/*.tar.gz 57 | ``` 58 | 59 | ## Build or Download a binary 60 | 61 | The pre-compiled binaries can be downloaded from [release page](https://github.com/appleboy/drone-scp/releases). Support the following OS type. 62 | 63 | - Windows amd64/386 64 | - Linux arm/amd64/386 65 | - Darwin amd64/386 66 | 67 | With `Go` installed 68 | 69 | ```sh 70 | export GO111MODULE=on 71 | go get -u -v github.com/appleboy/drone-scp 72 | ``` 73 | 74 | or build the binary with the following command: 75 | 76 | ```sh 77 | export GOOS=linux 78 | export GOARCH=amd64 79 | export CGO_ENABLED=0 80 | export GO111MODULE=on 81 | 82 | go test -cover ./... 83 | 84 | go build -v -a -tags netgo -o release/linux/amd64/drone-scp . 85 | ``` 86 | 87 | ## Docker 88 | 89 | Build the docker image with the following commands: 90 | 91 | ```sh 92 | make docker 93 | ``` 94 | 95 | ## Usage 96 | 97 | There are three ways to send notification. 98 | 99 | - [usage from binary](#usage-from-binary) 100 | - [usage from docker](#usage-from-docker) 101 | - [usage from drone ci](#usage-from-drone-ci) 102 | 103 | ### Usage from binary 104 | 105 | #### Using public key 106 | 107 | ```bash 108 | drone-scp --host example.com \ 109 | --port 22 \ 110 | --username appleboy \ 111 | --key-path "${HOME}/.ssh/id_rsa" \ 112 | --target /home/appleboy/test \ 113 | --source your_local_folder_path 114 | ``` 115 | 116 | #### Using password 117 | 118 | ```diff 119 | drone-scp --host example.com \ 120 | --port 22 \ 121 | --username appleboy \ 122 | + --password xxxxxxx \ 123 | --target /home/appleboy/test \ 124 | --source your_local_folder_path 125 | ``` 126 | 127 | #### Using ssh-agent 128 | 129 | Start your local ssh agent: 130 | 131 | ```bash 132 | eval `ssh-agent -s` 133 | ``` 134 | 135 | Import your local public key `~/.ssh/id_rsa` 136 | 137 | ```sh 138 | ssh-add 139 | ``` 140 | 141 | You don't need to add `--password` or `--key-path` arguments. 142 | 143 | ```bash 144 | drone-scp --host example.com \ 145 | --port 22 \ 146 | --username appleboy \ 147 | --target /home/appleboy/test \ 148 | --source your_local_folder_path 149 | ``` 150 | 151 | #### Send multiple source or target folder and hosts 152 | 153 | ```diff 154 | drone-scp --host example1.com \ 155 | + --host example2.com \ 156 | --port 22 \ 157 | --username appleboy \ 158 | --password xxxxxxx 159 | --target /home/appleboy/test1 \ 160 | + --target /home/appleboy/test2 \ 161 | --source your_local_folder_path_1 162 | + --source your_local_folder_path_2 163 | ``` 164 | 165 | ### Usage from docker 166 | 167 | Using public key 168 | 169 | ```bash 170 | docker run --rm \ 171 | -e SCP_HOST=example.com \ 172 | -e SCP_USERNAME=xxxxxxx \ 173 | -e SCP_PORT=22 \ 174 | -e SCP_KEY_PATH="${HOME}/.ssh/id_rsa" 175 | -e SCP_SOURCE=SOURCE_FILE_LIST \ 176 | -e SCP_TARGET=TARGET_FOLDER_PATH \ 177 | -v $(pwd):$(pwd) \ 178 | -w $(pwd) \ 179 | appleboy/drone-scp 180 | ``` 181 | 182 | Using password 183 | 184 | ```diff 185 | docker run --rm \ 186 | -e SCP_HOST=example.com \ 187 | -e SCP_USERNAME=xxxxxxx \ 188 | -e SCP_PORT=22 \ 189 | + -e SCP_PASSWORD="xxxxxxx" 190 | -e SCP_SOURCE=SOURCE_FILE_LIST \ 191 | -e SCP_TARGET=TARGET_FOLDER_PATH \ 192 | -v $(pwd):$(pwd) \ 193 | -w $(pwd) \ 194 | appleboy/drone-scp 195 | ``` 196 | 197 | Using ssh-agent, start your local ssh agent: 198 | 199 | ```bash 200 | eval `ssh-agent -s` 201 | ``` 202 | 203 | Import your local public key `~/.ssh/id_rsa` 204 | 205 | ```sh 206 | ssh-add 207 | ``` 208 | 209 | You don't need to add `SCP_PASSWORD` or `SCP_KEY_PATH` arguments. 210 | 211 | ```bash 212 | docker run --rm \ 213 | -e SCP_HOST=example.com \ 214 | -e SCP_USERNAME=xxxxxxx \ 215 | -e SCP_PORT=22 \ 216 | -e SCP_SOURCE=SOURCE_FILE_LIST \ 217 | -e SCP_TARGET=TARGET_FOLDER_PATH \ 218 | -v $(pwd):$(pwd) \ 219 | -w $(pwd) \ 220 | appleboy/drone-scp 221 | ``` 222 | 223 | Send multiple source or target folder and hosts 224 | 225 | ```bash 226 | docker run --rm \ 227 | -e SCP_HOST=example1.com,example2.com \ 228 | -e SCP_USERNAME=xxxxxxx \ 229 | -e SCP_PASSWORD=xxxxxxx \ 230 | -e SCP_PORT=22 \ 231 | -e SCP_SOURCE=SOURCE_FILE_LIST_1,SOURCE_FILE_LIST_2 \ 232 | -e SCP_TARGET=TARGET_FOLDER_PATH_1,TARGET_FOLDER_PATH_2 \ 233 | -v $(pwd):$(pwd) \ 234 | -w $(pwd) \ 235 | appleboy/drone-scp 236 | ``` 237 | 238 | ### Usage from drone ci 239 | 240 | Execute from the working directory: 241 | 242 | ```bash 243 | docker run --rm \ 244 | -e PLUGIN_HOST=example.com \ 245 | -e PLUGIN_USERNAME=xxxxxxx \ 246 | -e PLUGIN_PASSWORD=xxxxxxx \ 247 | -e PLUGIN_PORT=xxxxxxx \ 248 | -e PLUGIN_SOURCE=SOURCE_FILE_LIST \ 249 | -e PLUGIN_TARGET=TARGET_FOLDER_PATH \ 250 | -e PLUGIN_RM=false \ 251 | -e PLUGIN_DEBUG=true \ 252 | -v $(pwd):$(pwd) \ 253 | -w $(pwd) \ 254 | appleboy/drone-scp 255 | ``` 256 | 257 | You can get more [information](http://plugins.drone.io/appleboy/drone-scp/) about how to use scp in drone. 258 | 259 | ## Testing 260 | 261 | Test the package with the following command: 262 | 263 | ```sh 264 | make test 265 | ``` 266 | -------------------------------------------------------------------------------- /README.zh-tw.md: -------------------------------------------------------------------------------- 1 | # drone-scp 2 | 3 | [簡體中文](README.zh-cn.md) | [English](README.md) 4 | 5 | [![GoDoc](https://godoc.org/github.com/appleboy/drone-scp?status.svg)](https://godoc.org/github.com/appleboy/drone-scp) 6 | [![Lint and Testing](https://github.com/appleboy/drone-scp/actions/workflows/testing.yml/badge.svg)](https://github.com/appleboy/drone-scp/actions/workflows/testing.yml) 7 | [![codecov](https://codecov.io/gh/appleboy/drone-scp/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/drone-scp) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/drone-scp)](https://goreportcard.com/report/github.com/appleboy/drone-scp) 9 | [![Docker Pulls](https://img.shields.io/docker/pulls/appleboy/drone-scp.svg)](https://hub.docker.com/r/appleboy/drone-scp/) 10 | 11 | 複製檔案和工件通過 SSH 使用二進制檔案、docker 或 [Drone CI](http://docs.drone.io/)。 12 | 13 | [English](README.md) | [简体中文](README.zh-cn.md) 14 | 15 | ## 功能 16 | 17 | - [x] 支援例程。 18 | - [x] 支援來源列表中的萬用字元模式。 19 | - [x] 支援將檔案發送到多個主機。 20 | - [x] 支援將檔案發送到主機上的多個目標資料夾。 21 | - [x] 支援從絕對路徑或原始主體載入 ssh 金鑰。 22 | - [x] 支援 SSH ProxyCommand。 23 | 24 | ```sh 25 | +--------+ +----------+ +-----------+ 26 | | Laptop | <--> | Jumphost | <--> | FooServer | 27 | +--------+ +----------+ +-----------+ 28 | 29 | OR 30 | 31 | +--------+ +----------+ +-----------+ 32 | | Laptop | <--> | Firewall | <--> | FooServer | 33 | +--------+ +----------+ +-----------+ 34 | 192.168.1.5 121.1.2.3 10.10.29.68 35 | ``` 36 | 37 | ## Breaking changes 38 | 39 | `v1.5.0`: change command timeout flag to `Duration`. See the following setting: 40 | 41 | ```diff 42 | - name: scp files 43 | image: appleboy/drone-scp 44 | settings: 45 | host: 46 | - example1.com 47 | - example2.com 48 | username: ubuntu 49 | password: 50 | from_secret: ssh_password 51 | port: 22 52 | - command_timeout: 120 53 | + command_timeout: 2m 54 | target: /home/deploy/web 55 | source: 56 | - release/*.tar.gz 57 | ``` 58 | 59 | ## Build or Download a binary 60 | 61 | The pre-compiled binaries can be downloaded from [release page](https://github.com/appleboy/drone-scp/releases). Support the following OS type. 62 | 63 | - Windows amd64/386 64 | - Linux arm/amd64/386 65 | - Darwin amd64/386 66 | 67 | With `Go` installed 68 | 69 | ```sh 70 | export GO111MODULE=on 71 | go get -u -v github.com/appleboy/drone-scp 72 | ``` 73 | 74 | or build the binary with the following command: 75 | 76 | ```sh 77 | export GOOS=linux 78 | export GOARCH=amd64 79 | export CGO_ENABLED=0 80 | export GO111MODULE=on 81 | 82 | go test -cover ./... 83 | 84 | go build -v -a -tags netgo -o release/linux/amd64/drone-scp . 85 | ``` 86 | 87 | ## Docker 88 | 89 | Build the docker image with the following commands: 90 | 91 | ```sh 92 | make docker 93 | ``` 94 | 95 | ## Usage 96 | 97 | There are three ways to send notification. 98 | 99 | - [usage from binary](#usage-from-binary) 100 | - [usage from docker](#usage-from-docker) 101 | - [usage from drone ci](#usage-from-drone-ci) 102 | 103 | ### Usage from binary 104 | 105 | #### Using public key 106 | 107 | ```bash 108 | drone-scp --host example.com \ 109 | --port 22 \ 110 | --username appleboy \ 111 | --key-path "${HOME}/.ssh/id_rsa" \ 112 | --target /home/appleboy/test \ 113 | --source your_local_folder_path 114 | ``` 115 | 116 | #### Using password 117 | 118 | ```diff 119 | drone-scp --host example.com \ 120 | --port 22 \ 121 | --username appleboy \ 122 | + --password xxxxxxx \ 123 | --target /home/appleboy/test \ 124 | --source your_local_folder_path 125 | ``` 126 | 127 | #### Using ssh-agent 128 | 129 | Start your local ssh agent: 130 | 131 | ```bash 132 | eval `ssh-agent -s` 133 | ``` 134 | 135 | Import your local public key `~/.ssh/id_rsa` 136 | 137 | ```sh 138 | ssh-add 139 | ``` 140 | 141 | You don't need to add `--password` or `--key-path` arguments. 142 | 143 | ```bash 144 | drone-scp --host example.com \ 145 | --port 22 \ 146 | --username appleboy \ 147 | --target /home/appleboy/test \ 148 | --source your_local_folder_path 149 | ``` 150 | 151 | #### Send multiple source or target folder and hosts 152 | 153 | ```diff 154 | drone-scp --host example1.com \ 155 | + --host example2.com \ 156 | --port 22 \ 157 | --username appleboy \ 158 | --password xxxxxxx 159 | --target /home/appleboy/test1 \ 160 | + --target /home/appleboy/test2 \ 161 | --source your_local_folder_path_1 162 | + --source your_local_folder_path_2 163 | ``` 164 | 165 | ### Usage from docker 166 | 167 | Using public key 168 | 169 | ```bash 170 | docker run --rm \ 171 | -e SCP_HOST=example.com \ 172 | -e SCP_USERNAME=xxxxxxx \ 173 | -e SCP_PORT=22 \ 174 | -e SCP_KEY_PATH="${HOME}/.ssh/id_rsa" 175 | -e SCP_SOURCE=SOURCE_FILE_LIST \ 176 | -e SCP_TARGET=TARGET_FOLDER_PATH \ 177 | -v $(pwd):$(pwd) \ 178 | -w $(pwd) \ 179 | appleboy/drone-scp 180 | ``` 181 | 182 | Using password 183 | 184 | ```diff 185 | docker run --rm \ 186 | -e SCP_HOST=example.com \ 187 | -e SCP_USERNAME=xxxxxxx \ 188 | -e SCP_PORT=22 \ 189 | + -e SCP_PASSWORD="xxxxxxx" 190 | -e SCP_SOURCE=SOURCE_FILE_LIST \ 191 | -e SCP_TARGET=TARGET_FOLDER_PATH \ 192 | -v $(pwd):$(pwd) \ 193 | -w $(pwd) \ 194 | appleboy/drone-scp 195 | ``` 196 | 197 | Using ssh-agent, start your local ssh agent: 198 | 199 | ```bash 200 | eval `ssh-agent -s` 201 | ``` 202 | 203 | Import your local public key `~/.ssh/id_rsa` 204 | 205 | ```sh 206 | ssh-add 207 | ``` 208 | 209 | You don't need to add `SCP_PASSWORD` or `SCP_KEY_PATH` arguments. 210 | 211 | ```bash 212 | docker run --rm \ 213 | -e SCP_HOST=example.com \ 214 | -e SCP_USERNAME=xxxxxxx \ 215 | -e SCP_PORT=22 \ 216 | -e SCP_SOURCE=SOURCE_FILE_LIST \ 217 | -e SCP_TARGET=TARGET_FOLDER_PATH \ 218 | -v $(pwd):$(pwd) \ 219 | -w $(pwd) \ 220 | appleboy/drone-scp 221 | ``` 222 | 223 | Send multiple source or target folder and hosts 224 | 225 | ```bash 226 | docker run --rm \ 227 | -e SCP_HOST=example1.com,example2.com \ 228 | -e SCP_USERNAME=xxxxxxx \ 229 | -e SCP_PASSWORD=xxxxxxx \ 230 | -e SCP_PORT=22 \ 231 | -e SCP_SOURCE=SOURCE_FILE_LIST_1,SOURCE_FILE_LIST_2 \ 232 | -e SCP_TARGET=TARGET_FOLDER_PATH_1,TARGET_FOLDER_PATH_2 \ 233 | -v $(pwd):$(pwd) \ 234 | -w $(pwd) \ 235 | appleboy/drone-scp 236 | ``` 237 | 238 | ### Usage from drone ci 239 | 240 | Execute from the working directory: 241 | 242 | ```bash 243 | docker run --rm \ 244 | -e PLUGIN_HOST=example.com \ 245 | -e PLUGIN_USERNAME=xxxxxxx \ 246 | -e PLUGIN_PASSWORD=xxxxxxx \ 247 | -e PLUGIN_PORT=xxxxxxx \ 248 | -e PLUGIN_SOURCE=SOURCE_FILE_LIST \ 249 | -e PLUGIN_TARGET=TARGET_FOLDER_PATH \ 250 | -e PLUGIN_RM=false \ 251 | -e PLUGIN_DEBUG=true \ 252 | -v $(pwd):$(pwd) \ 253 | -w $(pwd) \ 254 | appleboy/drone-scp 255 | ``` 256 | 257 | You can get more [information](http://plugins.drone.io/appleboy/drone-scp/) about how to use scp in drone. 258 | 259 | ## Testing 260 | 261 | Test the package with the following command: 262 | 263 | ```sh 264 | make test 265 | ``` 266 | -------------------------------------------------------------------------------- /bearer.yml: -------------------------------------------------------------------------------- 1 | disable-version-check: false 2 | log-level: info 3 | report: 4 | fail-on-severity: critical,high,medium,low 5 | format: "" 6 | no-color: false 7 | output: "" 8 | report: security 9 | severity: critical,high,medium,low,warning 10 | rule: 11 | disable-default-rules: false 12 | only-rule: [] 13 | skip-rule: ["go_lang_logger_leak"] 14 | scan: 15 | context: "" 16 | data_subject_mapping: "" 17 | disable-domain-resolution: true 18 | domain-resolution-timeout: 3s 19 | exit-code: -1 20 | external-rule-dir: [] 21 | force: false 22 | hide_progress_bar: false 23 | internal-domains: [] 24 | parallel: 0 25 | quiet: false 26 | scanner: 27 | - sast 28 | skip-path: [] 29 | skip-test: true 30 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This function returns the appropriate command for removing a file/directory based on the operating system. 4 | func rmcmd(os, target string) string { 5 | switch os { 6 | case "windows": 7 | // On Windows, use DEL command to delete files and folders recursively 8 | return "DEL /F /S " + target 9 | case "unix": 10 | // On Unix-based systems, use rm command to delete files and folders recursively 11 | return "rm -rf " + target 12 | } 13 | // Return an empty string if the operating system is not recognized 14 | return "" 15 | } 16 | 17 | // This function returns the appropriate command for creating a directory based on the operating system. 18 | func mkdircmd(os, target string) string { 19 | switch os { 20 | case "windows": 21 | // On Windows, use mkdir command to create directory and check if it exists 22 | return "if not exist " + target + " mkdir " + target 23 | case "unix": 24 | // On Unix-based systems, use mkdir command with -p option to create directories recursively 25 | return "mkdir -p " + target 26 | } 27 | // Return an empty string if the operating system is not recognized 28 | return "" 29 | } 30 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | // Unit tests for rmcmd and mkdircmd 6 | func TestCommands(t *testing.T) { 7 | // Test rmcmd on Windows 8 | os1 := "windows" 9 | target1 := "C:\\path\\to\\file" 10 | expected1 := "DEL /F /S " + target1 11 | actual1 := rmcmd(os1, target1) 12 | if actual1 != expected1 { 13 | t.Errorf("rmcmd(%s, %s) = %s; expected %s", os1, target1, actual1, expected1) 14 | } 15 | 16 | // Test rmcmd on Unix-based system 17 | os2 := "unix" 18 | target2 := "/path/to/folder" 19 | expected2 := "rm -rf " + target2 20 | actual2 := rmcmd(os2, target2) 21 | if actual2 != expected2 { 22 | t.Errorf("rmcmd(%s, %s) = %s; expected %s", os2, target2, actual2, expected2) 23 | } 24 | 25 | // Test mkdircmd on Windows 26 | os3 := "windows" 27 | target3 := "C:\\path\\to\\folder" 28 | expected3 := "if not exist " + target3 + " mkdir " + target3 29 | actual3 := mkdircmd(os3, target3) 30 | if actual3 != expected3 { 31 | t.Errorf("mkdircmd(%s, %s) = %s; expected %s", os3, target3, actual3, expected3) 32 | } 33 | 34 | // Test mkdircmd on Unix-based system 35 | os4 := "unix" 36 | target4 := "/path/to/folder" 37 | expected4 := "mkdir -p " + target4 38 | actual4 := mkdircmd(os4, target4) 39 | if actual4 != expected4 { 40 | t.Errorf("mkdircmd(%s, %s) = %s; expected %s", os4, target4, actual4, expected4) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21 2 | 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | LABEL maintainer="Bo-Yi Wu " 7 | 8 | LABEL org.opencontainers.image.source=https://github.com/appleboy/drone-scp 9 | LABEL org.opencontainers.image.description="Copy files and artifacts via SSH" 10 | LABEL org.opencontainers.image.licenses=MIT 11 | 12 | RUN apk add --no-cache ca-certificates && \ 13 | rm -rf /var/cache/apk/* 14 | 15 | RUN addgroup \ 16 | -S -g 1000 \ 17 | deploy && \ 18 | adduser \ 19 | -S -H -D \ 20 | -h /home/deploy \ 21 | -s /bin/sh \ 22 | -u 1000 \ 23 | -G deploy \ 24 | deploy 25 | 26 | RUN mkdir -p /home/deploy && \ 27 | chown deploy:deploy /home/deploy 28 | 29 | # deploy:deploy 30 | USER 1000:1000 31 | 32 | COPY release/${TARGETOS}/${TARGETARCH}/drone-scp /bin/ 33 | 34 | ENTRYPOINT ["/bin/drone-scp"] 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/appleboy/drone-scp 2 | 3 | go 1.23.8 4 | 5 | require ( 6 | github.com/appleboy/com v0.3.0 7 | github.com/appleboy/easyssh-proxy v1.5.0 8 | github.com/fatih/color v1.18.0 9 | github.com/joho/godotenv v1.5.1 10 | github.com/stretchr/testify v1.8.4 11 | github.com/urfave/cli/v2 v2.27.6 12 | github.com/yassinebenaid/godump v0.11.1 13 | golang.org/x/crypto v0.37.0 14 | ) 15 | 16 | require ( 17 | github.com/ScaleFT/sshkeys v1.4.0 // indirect 18 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect 21 | github.com/mattn/go-colorable v0.1.14 // indirect 22 | github.com/mattn/go-isatty v0.0.20 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 25 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 26 | golang.org/x/sys v0.32.0 // indirect 27 | gopkg.in/yaml.v3 v3.0.1 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ScaleFT/sshkeys v1.4.0 h1:Yqd0cKA5PUvwV0dgRI67BDHGTsMHtGQBZbLXh1dthmE= 2 | github.com/ScaleFT/sshkeys v1.4.0/go.mod h1:GineMkS8SEiELq8q5DzA2Wnrw65SqdD9a+hm8JOU1I4= 3 | github.com/appleboy/com v0.3.0 h1:omze/tJPyi2YVH+m23GSrCGt90A+4vQNpEYBW+GuSr4= 4 | github.com/appleboy/com v0.3.0/go.mod h1:kByEI3/vzI5GM1+O5QdBHLsXaOsmFsJcOpCSgASi4sg= 5 | github.com/appleboy/easyssh-proxy v1.5.0 h1:OYdSPvYQN3mhnsMH5I2OF1TgwSEcSq33kvjQfTwvZww= 6 | github.com/appleboy/easyssh-proxy v1.5.0/go.mod h1:zcEMrStH91/tcUn3gUGP0KpQwUYLm8tX/Ook1AH98uc= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= 8 | github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU= 12 | github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0= 13 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 14 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 15 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 16 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 17 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 18 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 19 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 20 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 24 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 25 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 26 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 27 | github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= 28 | github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 29 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 30 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 31 | github.com/yassinebenaid/godump v0.11.1 h1:SPujx/XaYqGDfmNh7JI3dOyCUVrG0bG2duhO3Eh2EhI= 32 | github.com/yassinebenaid/godump v0.11.1/go.mod h1:dc/0w8wmg6kVIvNGAzbKH1Oa54dXQx8SNKh4dPRyW44= 33 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 34 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 35 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 37 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 38 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 39 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/appleboy/easyssh-proxy" 10 | "github.com/joho/godotenv" 11 | "github.com/urfave/cli/v2" 12 | "github.com/yassinebenaid/godump" 13 | ) 14 | 15 | // Version set at compile-time 16 | var ( 17 | Version string 18 | ) 19 | 20 | func main() { 21 | // Load env-file if it exists first 22 | if filename, found := os.LookupEnv("PLUGIN_ENV_FILE"); found { 23 | _ = godotenv.Load(filename) 24 | } 25 | 26 | if _, err := os.Stat("/run/drone/env"); err == nil { 27 | _ = godotenv.Overload("/run/drone/env") 28 | } 29 | 30 | app := cli.NewApp() 31 | app.Name = "Drone SCP" 32 | app.Usage = "Copy files and artifacts via SSH." 33 | app.Copyright = "Copyright (c) " + strconv.Itoa(time.Now().Year()) + " Bo-Yi Wu" 34 | app.Version = Version 35 | app.Authors = []*cli.Author{ 36 | { 37 | Name: "Bo-Yi Wu", 38 | Email: "appleboy.tw@gmail.com", 39 | }, 40 | } 41 | app.Action = run 42 | app.Version = Version 43 | app.Flags = []cli.Flag{ 44 | &cli.StringSliceFlag{ 45 | Name: "host", 46 | Aliases: []string{"H"}, 47 | Usage: "Remote server host address or IP", 48 | EnvVars: []string{"PLUGIN_HOST", "SSH_HOST", "INPUT_HOST"}, 49 | FilePath: ".host", 50 | }, 51 | &cli.IntFlag{ 52 | Name: "port", 53 | Aliases: []string{"p"}, 54 | Usage: "SSH port number (default: 22)", 55 | EnvVars: []string{"PLUGIN_PORT", "SSH_PORT", "INPUT_PORT"}, 56 | Value: 22, 57 | }, 58 | &cli.StringFlag{ 59 | Name: "protocol", 60 | Usage: "Network protocol to use (tcp, tcp4, tcp6)", 61 | EnvVars: []string{"PLUGIN_PROTOCOL", "SSH_PROTOCOL", "INPUT_PROTOCOL"}, 62 | Value: "tcp", 63 | }, 64 | &cli.StringFlag{ 65 | Name: "username", 66 | Aliases: []string{"user", "u"}, 67 | Usage: "SSH username for authentication", 68 | EnvVars: []string{"PLUGIN_USERNAME", "PLUGIN_USER", "SSH_USERNAME", "INPUT_USERNAME"}, 69 | Value: "root", 70 | }, 71 | &cli.StringFlag{ 72 | Name: "password", 73 | Aliases: []string{"P"}, 74 | Usage: "SSH password for authentication", 75 | EnvVars: []string{"PLUGIN_PASSWORD", "SSH_PASSWORD", "INPUT_PASSWORD"}, 76 | }, 77 | &cli.DurationFlag{ 78 | Name: "timeout", 79 | Usage: "SSH connection timeout duration (default: 30s)", 80 | EnvVars: []string{"PLUGIN_TIMEOUT", "SSH_TIMEOUT", "INPUT_TIMEOUT"}, 81 | Value: 30 * time.Second, 82 | }, 83 | &cli.StringFlag{ 84 | Name: "ssh-key", 85 | Usage: "SSH private key content for authentication", 86 | EnvVars: []string{"PLUGIN_SSH_KEY", "PLUGIN_KEY", "SSH_KEY", "INPUT_KEY"}, 87 | }, 88 | &cli.StringFlag{ 89 | Name: "ssh-passphrase", 90 | Usage: "Passphrase to decrypt the SSH private key", 91 | EnvVars: []string{"PLUGIN_SSH_PASSPHRASE", "PLUGIN_PASSPHRASE", "SSH_PASSPHRASE", "INPUT_PASSPHRASE"}, 92 | }, 93 | &cli.StringFlag{ 94 | Name: "key-path", 95 | Aliases: []string{"i"}, 96 | Usage: "Path to SSH private key file", 97 | EnvVars: []string{"PLUGIN_KEY_PATH", "SSH_KEY_PATH", "INPUT_KEY_PATH"}, 98 | }, 99 | &cli.StringSliceFlag{ 100 | Name: "ciphers", 101 | Usage: "List of allowed SSH encryption algorithms", 102 | EnvVars: []string{"PLUGIN_CIPHERS", "SSH_CIPHERS", "INPUT_CIPHERS"}, 103 | }, 104 | &cli.BoolFlag{ 105 | Name: "useInsecureCipher", 106 | Usage: "Enable less secure encryption algorithms (not recommended)", 107 | EnvVars: []string{"PLUGIN_USE_INSECURE_CIPHER", "SSH_USE_INSECURE_CIPHER", "INPUT_USE_INSECURE_CIPHER"}, 108 | }, 109 | &cli.StringFlag{ 110 | Name: "fingerprint", 111 | Usage: "SHA256 fingerprint of host public key for verification", 112 | EnvVars: []string{"PLUGIN_FINGERPRINT", "SSH_FINGERPRINT", "INPUT_FINGERPRINT"}, 113 | }, 114 | &cli.DurationFlag{ 115 | Name: "command.timeout", 116 | Usage: "Maximum time allowed for command execution (default: 10m)", 117 | EnvVars: []string{"PLUGIN_COMMAND_TIMEOUT", "SSH_COMMAND_TIMEOUT", "INPUT_COMMAND_TIMEOUT"}, 118 | Value: 10 * time.Minute, 119 | }, 120 | &cli.StringSliceFlag{ 121 | Name: "target", 122 | Aliases: []string{"t"}, 123 | Usage: "Destination path on remote server", 124 | EnvVars: []string{"PLUGIN_TARGET", "SSH_TARGET", "INPUT_TARGET"}, 125 | }, 126 | &cli.StringSliceFlag{ 127 | Name: "source", 128 | Aliases: []string{"s"}, 129 | Usage: "Local files/directories to copy", 130 | EnvVars: []string{"PLUGIN_SOURCE", "SCP_SOURCE", "INPUT_SOURCE"}, 131 | }, 132 | &cli.BoolFlag{ 133 | Name: "rm", 134 | Aliases: []string{"r"}, 135 | Usage: "Delete destination folder before copying", 136 | EnvVars: []string{"PLUGIN_RM", "SCP_RM", "INPUT_RM"}, 137 | }, 138 | // Proxy settings remain the same as they are already clear 139 | &cli.StringFlag{ 140 | Name: "proxy.host", 141 | Usage: "Proxy server host address or IP", 142 | EnvVars: []string{"PLUGIN_PROXY_HOST", "PROXY_SSH_HOST", "INPUT_PROXY_HOST"}, 143 | }, 144 | &cli.StringFlag{ 145 | Name: "proxy.port", 146 | Usage: "Proxy server SSH port (default: 22)", 147 | EnvVars: []string{"PLUGIN_PROXY_PORT", "PROXY_SSH_PORT", "INPUT_PROXY_PORT"}, 148 | Value: "22", 149 | }, 150 | &cli.StringFlag{ 151 | Name: "proxy.protocol", 152 | Usage: "The IP protocol to use for the proxy. Valid values are \"tcp\". \"tcp4\" or \"tcp6\". Default to tcp.", 153 | EnvVars: []string{"PLUGIN_PROXY_PROTOCOL", "SSH_PROXY_PROTOCOL", "INPUT_PROXY_PROTOCOL"}, 154 | Value: "tcp", 155 | }, 156 | &cli.StringFlag{ 157 | Name: "proxy.username", 158 | Usage: "connect as user of proxy", 159 | EnvVars: []string{"PLUGIN_PROXY_USERNAME", "PLUGIN_PROXY_USER", "PROXY_SSH_USERNAME", "INPUT_PROXY_USERNAME"}, 160 | Value: "root", 161 | }, 162 | &cli.StringFlag{ 163 | Name: "proxy.password", 164 | Usage: "user password of proxy", 165 | EnvVars: []string{"PLUGIN_PROXY_PASSWORD", "PROXY_SSH_PASSWORD", "INPUT_PROXY_PASSWORD"}, 166 | }, 167 | &cli.StringFlag{ 168 | Name: "proxy.ssh-key", 169 | Usage: "private ssh key of proxy", 170 | EnvVars: []string{"PLUGIN_PROXY_SSH_KEY", "PLUGIN_PROXY_KEY", "PROXY_SSH_KEY", "INPUT_PROXY_KEY"}, 171 | }, 172 | &cli.StringFlag{ 173 | Name: "proxy.ssh-passphrase", 174 | Usage: "The purpose of the passphrase is usually to encrypt the private key.", 175 | EnvVars: []string{"PLUGIN_PROXY_SSH_PASSPHRASE", "PLUGIN_PROXY_PASSPHRASE", "PROXY_SSH_PASSPHRASE", "INPUT_PROXY_PASSPHRASE"}, 176 | }, 177 | &cli.StringFlag{ 178 | Name: "proxy.key-path", 179 | Usage: "ssh private key path of proxy", 180 | EnvVars: []string{"PLUGIN_PROXY_KEY_PATH", "PROXY_SSH_KEY_PATH", "INPUT_PROXY_KEY_PATH"}, 181 | }, 182 | &cli.DurationFlag{ 183 | Name: "proxy.timeout", 184 | Usage: "proxy connection timeout", 185 | EnvVars: []string{"PLUGIN_PROXY_TIMEOUT", "PROXY_SSH_TIMEOUT", "INPUT_PROXY_TIMEOUT"}, 186 | }, 187 | &cli.StringSliceFlag{ 188 | Name: "proxy.ciphers", 189 | Usage: "The allowed cipher algorithms. If unspecified then a sensible", 190 | EnvVars: []string{"PLUGIN_PROXY_CIPHERS", "PROXY_SSH_CIPHERS", "INPUT_PROXY_CIPHERS"}, 191 | }, 192 | &cli.BoolFlag{ 193 | Name: "proxy.useInsecureCipher", 194 | Usage: "include more ciphers with use_insecure_cipher", 195 | EnvVars: []string{"PLUGIN_PROXY_USE_INSECURE_CIPHER", "PROXY_SSH_USE_INSECURE_CIPHER", "INPUT_PROXY_USE_INSECURE_CIPHER"}, 196 | }, 197 | &cli.StringFlag{ 198 | Name: "proxy.fingerprint", 199 | Usage: "fingerprint SHA256 of the host public key, default is to skip verification", 200 | EnvVars: []string{"PLUGIN_PROXY_FINGERPRINT", "PROXY_SSH_FINGERPRINT", "PROXY_FINGERPRINT", "INPUT_PROXY_FINGERPRINT"}, 201 | }, 202 | &cli.IntFlag{ 203 | Name: "strip.components", 204 | Usage: "Strip N leading components from file paths", 205 | EnvVars: []string{"PLUGIN_STRIP_COMPONENTS", "TAR_STRIP_COMPONENTS", "INPUT_STRIP_COMPONENTS"}, 206 | }, 207 | &cli.StringFlag{ 208 | Name: "tar.exec", 209 | Usage: "Custom tar executable path on remote host", 210 | EnvVars: []string{"PLUGIN_TAR_EXEC", "SSH_TAR_EXEC", "INPUT_TAR_EXEC"}, 211 | Value: "tar", 212 | }, 213 | &cli.StringFlag{ 214 | Name: "tar.tmp-path", 215 | Usage: "Temporary directory for tar files on remote host", 216 | EnvVars: []string{"PLUGIN_TAR_TMP_PATH", "SSH_TAR_TMP_PATH", "INPUT_TAR_TMP_PATH"}, 217 | }, 218 | &cli.BoolFlag{ 219 | Name: "debug", 220 | Usage: "Enable debug logging", 221 | EnvVars: []string{"PLUGIN_DEBUG", "INPUT_DEBUG"}, 222 | }, 223 | &cli.BoolFlag{ 224 | Name: "overwrite", 225 | Usage: "Force overwrite of existing files", 226 | EnvVars: []string{"PLUGIN_OVERWRITE", "INPUT_OVERWRITE"}, 227 | }, 228 | &cli.BoolFlag{ 229 | Name: "unlink.first", 230 | Usage: "Remove files before extracting new ones", 231 | EnvVars: []string{"PLUGIN_UNLINK_FIRST", "INPUT_UNLINK_FIRST"}, 232 | }, 233 | &cli.BoolFlag{ 234 | Name: "tar.dereference", 235 | Usage: "Follow symbolic links when copying", 236 | EnvVars: []string{"PLUGIN_TAR_DEREFERENCE", "INPUT_TAR_DEREFERENCE"}, 237 | }, 238 | } 239 | 240 | // Override a template 241 | cli.AppHelpTemplate = ` 242 | ________ ____________________________ 243 | \______ \_______ ____ ____ ____ / _____/\_ ___ \______ \ 244 | | | \_ __ \/ _ \ / \_/ __ \ ______ \_____ \ / \ \/| ___/ 245 | | | \ | \( <_> ) | \ ___/ /_____/ / \\ \___| | 246 | /_______ /__| \____/|___| /\___ > /_______ / \______ /____| 247 | \/ \/ \/ \/ \/ 248 | version: {{.Version}} 249 | NAME: 250 | {{.Name}} - {{.Usage}} 251 | 252 | USAGE: 253 | {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} 254 | {{if len .Authors}} 255 | AUTHOR: 256 | {{range .Authors}}{{ . }}{{end}} 257 | {{end}}{{if .Commands}} 258 | COMMANDS: 259 | {{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} 260 | GLOBAL OPTIONS: 261 | {{range .VisibleFlags}}{{.}} 262 | {{end}}{{end}}{{if .Copyright }} 263 | COPYRIGHT: 264 | {{.Copyright}} 265 | {{end}}{{if .Version}} 266 | VERSION: 267 | {{.Version}} 268 | {{end}} 269 | REPOSITORY: 270 | Github: https://github.com/appleboy/drone-scp 271 | ` 272 | 273 | if err := app.Run(os.Args); err != nil { 274 | log.Fatal(err) 275 | } 276 | } 277 | 278 | func run(c *cli.Context) error { 279 | plugin := Plugin{ 280 | Config: Config{ 281 | Host: c.StringSlice("host"), 282 | Port: c.Int("port"), 283 | Protocol: easyssh.Protocol(c.String("protocol")), 284 | Username: c.String("username"), 285 | Password: c.String("password"), 286 | Passphrase: c.String("ssh-passphrase"), 287 | Fingerprint: c.String("fingerprint"), 288 | Timeout: c.Duration("timeout"), 289 | CommandTimeout: c.Duration("command.timeout"), 290 | Key: c.String("ssh-key"), 291 | KeyPath: c.String("key-path"), 292 | Target: c.StringSlice("target"), 293 | Source: c.StringSlice("source"), 294 | Remove: c.Bool("rm"), 295 | Debug: c.Bool("debug"), 296 | StripComponents: c.Int("strip.components"), 297 | TarExec: c.String("tar.exec"), 298 | TarTmpPath: c.String("tar.tmp-path"), 299 | Overwrite: c.Bool("overwrite"), 300 | UnlinkFirst: c.Bool("unlink.first"), 301 | Ciphers: c.StringSlice("ciphers"), 302 | UseInsecureCipher: c.Bool("useInsecureCipher"), 303 | TarDereference: c.Bool("tar.dereference"), 304 | Proxy: easyssh.DefaultConfig{ 305 | Key: c.String("proxy.ssh-key"), 306 | Passphrase: c.String("proxy.ssh-passphrase"), 307 | Fingerprint: c.String("proxy.fingerprint"), 308 | KeyPath: c.String("proxy.key-path"), 309 | User: c.String("proxy.username"), 310 | Password: c.String("proxy.password"), 311 | Server: c.String("proxy.host"), 312 | Port: c.String("proxy.port"), 313 | Protocol: easyssh.Protocol(c.String("proxy.protocol")), 314 | Timeout: c.Duration("proxy.timeout"), 315 | Ciphers: c.StringSlice("proxy.ciphers"), 316 | UseInsecureCipher: c.Bool("proxy.useInsecureCipher"), 317 | }, 318 | }, 319 | } 320 | 321 | if plugin.Config.Debug { 322 | _ = godump.Dump(plugin) 323 | } 324 | 325 | return plugin.Exec() 326 | } 327 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package main 5 | 6 | func getRealPath(path string) string { 7 | return path 8 | } 9 | -------------------------------------------------------------------------------- /path_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "strings" 8 | ) 9 | 10 | func getRealPath(path string) string { 11 | return "/" + strings.ReplaceAll(strings.ReplaceAll(path, ":", ""), "\\", "/") 12 | } 13 | -------------------------------------------------------------------------------- /path_windows_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetRealPath(t *testing.T) { 8 | type args struct { 9 | path string 10 | } 11 | tests := []struct { 12 | name string 13 | args args 14 | want string 15 | }{ 16 | { 17 | "Test Windows Path", 18 | "C:\\Users\\appleboy\\test.txt", 19 | "/C/Users/appleboy/test.txt", 20 | }, 21 | } 22 | for _, tt := range tests { 23 | if got := getRealPath(tt.args.path); got != tt.want { 24 | t.Errorf("%q. getRealPath() = %v, want %v", tt.name, got, tt.want) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/appleboy/com/random" 15 | "github.com/appleboy/easyssh-proxy" 16 | "github.com/fatih/color" 17 | ) 18 | 19 | var ( 20 | errMissingHost = errors.New("Error: missing server host") 21 | errMissingPasswordOrKey = errors.New("Error: can't connect without a private SSH key or password") 22 | errMissingSourceOrTarget = errors.New("missing source or target config") 23 | ) 24 | 25 | type ( 26 | // Config for the plugin. 27 | Config struct { 28 | Host []string 29 | Port int 30 | Protocol easyssh.Protocol 31 | Username string 32 | Password string 33 | Key string 34 | Passphrase string 35 | Fingerprint string 36 | KeyPath string 37 | Timeout time.Duration 38 | CommandTimeout time.Duration 39 | Target []string 40 | Source []string 41 | Remove bool 42 | StripComponents int 43 | TarExec string 44 | TarTmpPath string 45 | Proxy easyssh.DefaultConfig 46 | Debug bool 47 | Overwrite bool 48 | UnlinkFirst bool 49 | Ciphers []string 50 | UseInsecureCipher bool 51 | TarDereference bool 52 | } 53 | 54 | // Plugin values. 55 | Plugin struct { 56 | Config Config 57 | DestFile string 58 | } 59 | 60 | copyError struct { 61 | host string 62 | message string 63 | } 64 | ) 65 | 66 | func (e copyError) Error() string { 67 | return fmt.Sprintf("error copy file to dest: %s, error message: %s\n", e.host, e.message) 68 | } 69 | 70 | func globList(paths []string) fileList { 71 | var list fileList 72 | 73 | for _, pattern := range paths { 74 | ignore := false 75 | pattern = strings.TrimSpace(pattern) 76 | if string(pattern[0]) == "!" { 77 | pattern = pattern[1:] 78 | ignore = true 79 | } 80 | matches, err := filepath.Glob(pattern) 81 | if err != nil { 82 | fmt.Printf("Glob error for %q: %s\n", pattern, err) 83 | continue 84 | } 85 | 86 | if ignore { 87 | list.Ignore = append(list.Ignore, matches...) 88 | } else { 89 | list.Source = append(list.Source, matches...) 90 | } 91 | } 92 | 93 | return list 94 | } 95 | 96 | func (p Plugin) log(host string, message ...interface{}) { 97 | if count := len(p.Config.Host); count == 1 { 98 | fmt.Printf("%s", fmt.Sprintln(message...)) 99 | } else { 100 | fmt.Printf("%s: %s", host, fmt.Sprintln(message...)) 101 | } 102 | } 103 | 104 | func (p *Plugin) removeDestFile(os string, ssh *easyssh.MakeConfig) error { 105 | p.log(ssh.Server, "remove file", p.DestFile) 106 | _, errStr, _, err := ssh.Run(rmcmd(os, p.DestFile), p.Config.CommandTimeout) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | if errStr != "" { 112 | return errors.New(errStr) 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func (p *Plugin) removeAllDestFile() error { 119 | for _, h := range trimValues(p.Config.Host) { 120 | host, port := p.hostPort(h) 121 | ssh := &easyssh.MakeConfig{ 122 | Server: host, 123 | User: p.Config.Username, 124 | Password: p.Config.Password, 125 | Port: port, 126 | Protocol: p.Config.Protocol, 127 | Key: p.Config.Key, 128 | KeyPath: p.Config.KeyPath, 129 | Passphrase: p.Config.Passphrase, 130 | Timeout: p.Config.Timeout, 131 | Ciphers: p.Config.Ciphers, 132 | Fingerprint: p.Config.Fingerprint, 133 | UseInsecureCipher: p.Config.UseInsecureCipher, 134 | Proxy: easyssh.DefaultConfig{ 135 | Server: p.Config.Proxy.Server, 136 | User: p.Config.Proxy.User, 137 | Password: p.Config.Proxy.Password, 138 | Port: p.Config.Proxy.Port, 139 | Protocol: p.Config.Proxy.Protocol, 140 | Key: p.Config.Proxy.Key, 141 | KeyPath: p.Config.Proxy.KeyPath, 142 | Passphrase: p.Config.Proxy.Passphrase, 143 | Timeout: p.Config.Proxy.Timeout, 144 | Ciphers: p.Config.Proxy.Ciphers, 145 | Fingerprint: p.Config.Proxy.Fingerprint, 146 | UseInsecureCipher: p.Config.Proxy.UseInsecureCipher, 147 | }, 148 | } 149 | 150 | _, _, _, err := ssh.Run("ver", p.Config.CommandTimeout) 151 | systemType := "unix" 152 | if err == nil { 153 | systemType = "windows" 154 | } 155 | 156 | // remove tar file 157 | err = p.removeDestFile(systemType, ssh) 158 | if err != nil { 159 | return err 160 | } 161 | } 162 | 163 | return nil 164 | } 165 | 166 | type fileList struct { 167 | Ignore []string 168 | Source []string 169 | } 170 | 171 | func (p *Plugin) buildTarArgs(src string) []string { 172 | files := globList(trimValues(p.Config.Source)) 173 | args := []string{} 174 | if len(files.Ignore) > 0 { 175 | for _, v := range files.Ignore { 176 | args = append(args, "--exclude") 177 | args = append(args, v) 178 | } 179 | } 180 | 181 | if p.Config.TarDereference { 182 | args = append(args, "--dereference") 183 | } 184 | 185 | args = append(args, "-zcf") 186 | args = append(args, getRealPath(src)) 187 | args = append(args, files.Source...) 188 | 189 | return args 190 | } 191 | 192 | func (p *Plugin) buildUnTarArgs(target string) []string { 193 | args := []string{} 194 | 195 | args = append(args, 196 | p.Config.TarExec, 197 | "-zxf", 198 | p.DestFile, 199 | ) 200 | 201 | if p.Config.StripComponents > 0 { 202 | args = append(args, "--strip-components") 203 | args = append(args, strconv.Itoa(p.Config.StripComponents)) 204 | } 205 | 206 | if p.Config.Overwrite { 207 | args = append(args, "--overwrite") 208 | } 209 | 210 | if p.Config.UnlinkFirst { 211 | args = append(args, "--unlink-first") 212 | } 213 | 214 | args = append(args, 215 | "-C", 216 | target, 217 | ) 218 | 219 | return args 220 | } 221 | 222 | // Exec executes the plugin. 223 | func (p *Plugin) Exec() error { 224 | if len(p.Config.Key) == 0 && len(p.Config.Password) == 0 && len(p.Config.KeyPath) == 0 { 225 | return errMissingPasswordOrKey 226 | } 227 | 228 | if len(p.Config.Source) == 0 || len(p.Config.Target) == 0 { 229 | return errMissingSourceOrTarget 230 | } 231 | 232 | hosts := trimValues(p.Config.Host) 233 | if len(hosts) == 0 { 234 | return errMissingHost 235 | } 236 | 237 | p.DestFile = random.String(10) + ".tar.gz" 238 | 239 | // create a temporary file for the archive 240 | dir := os.TempDir() 241 | src := filepath.Join(dir, p.DestFile) 242 | 243 | // show current version 244 | fmt.Println("drone-scp version: " + Version) 245 | // run archive command 246 | fmt.Println("tar all files into " + src) 247 | args := p.buildTarArgs(src) 248 | cmd := exec.Command(p.Config.TarExec, args...) 249 | if p.Config.Debug { 250 | fmt.Println("$", strings.Join(cmd.Args, " ")) 251 | } 252 | cmd.Stdout = os.Stdout 253 | cmd.Stderr = os.Stderr 254 | if err := cmd.Run(); err != nil { 255 | return err 256 | } 257 | 258 | wg := sync.WaitGroup{} 259 | wg.Add(len(p.Config.Host)) 260 | errChannel := make(chan error) 261 | finished := make(chan struct{}) 262 | for _, host := range hosts { 263 | go func(h string) { 264 | defer wg.Done() 265 | host, port := p.hostPort(h) 266 | // Create MakeConfig instance with remote username, server address and path to private key. 267 | ssh := &easyssh.MakeConfig{ 268 | Server: host, 269 | User: p.Config.Username, 270 | Password: p.Config.Password, 271 | Port: port, 272 | Key: p.Config.Key, 273 | KeyPath: p.Config.KeyPath, 274 | Passphrase: p.Config.Passphrase, 275 | Timeout: p.Config.Timeout, 276 | Ciphers: p.Config.Ciphers, 277 | Fingerprint: p.Config.Fingerprint, 278 | UseInsecureCipher: p.Config.UseInsecureCipher, 279 | Proxy: easyssh.DefaultConfig{ 280 | Server: p.Config.Proxy.Server, 281 | User: p.Config.Proxy.User, 282 | Password: p.Config.Proxy.Password, 283 | Port: p.Config.Proxy.Port, 284 | Key: p.Config.Proxy.Key, 285 | KeyPath: p.Config.Proxy.KeyPath, 286 | Passphrase: p.Config.Proxy.Passphrase, 287 | Timeout: p.Config.Proxy.Timeout, 288 | Ciphers: p.Config.Proxy.Ciphers, 289 | Fingerprint: p.Config.Proxy.Fingerprint, 290 | UseInsecureCipher: p.Config.Proxy.UseInsecureCipher, 291 | }, 292 | } 293 | 294 | systemType := "unix" 295 | _, _, _, err := ssh.Run("ver", p.Config.CommandTimeout) 296 | if err == nil { 297 | systemType = "windows" 298 | } 299 | 300 | // upload file to the tmp path 301 | p.DestFile = fmt.Sprintf("%s%s", p.Config.TarTmpPath, p.DestFile) 302 | 303 | p.log(host, "remote server os type is "+systemType) 304 | // Call Scp method with file you want to upload to remote server. 305 | p.log(host, "scp file to server.") 306 | err = ssh.Scp(src, p.DestFile) 307 | if err != nil { 308 | errChannel <- copyError{host, err.Error()} 309 | return 310 | } 311 | 312 | for _, target := range p.Config.Target { 313 | target = strings.ReplaceAll(target, " ", "\\ ") 314 | // remove target folder before upload data 315 | if p.Config.Remove { 316 | p.log(host, "Remove target folder:", target) 317 | 318 | _, _, _, err := ssh.Run(rmcmd(systemType, target), p.Config.CommandTimeout) 319 | if err != nil { 320 | errChannel <- err 321 | return 322 | } 323 | } 324 | 325 | p.log(host, "create folder", target) 326 | _, errStr, _, err := ssh.Run(mkdircmd(systemType, target), p.Config.CommandTimeout) 327 | if err != nil { 328 | errChannel <- err 329 | return 330 | } 331 | 332 | if len(errStr) != 0 { 333 | errChannel <- fmt.Errorf("%s", errStr) 334 | return 335 | } 336 | 337 | // untar file 338 | p.log(host, "untar file", p.DestFile) 339 | commamd := strings.Join(p.buildUnTarArgs(target), " ") 340 | if p.Config.Debug { 341 | fmt.Println("$", commamd) 342 | } 343 | outStr, errStr, _, err := ssh.Run(commamd, p.Config.CommandTimeout) 344 | 345 | if outStr != "" { 346 | p.log(host, "output: ", outStr) 347 | } 348 | 349 | if errStr != "" { 350 | p.log(host, "error: ", errStr) 351 | } 352 | 353 | if err != nil { 354 | errChannel <- err 355 | return 356 | } 357 | } 358 | 359 | // remove tar file 360 | err = p.removeDestFile(systemType, ssh) 361 | if err != nil { 362 | errChannel <- err 363 | return 364 | } 365 | }(host) 366 | } 367 | 368 | go func() { 369 | wg.Wait() 370 | close(finished) 371 | }() 372 | 373 | select { 374 | case <-finished: 375 | case err := <-errChannel: 376 | if err != nil { 377 | c := color.New(color.FgRed) 378 | c.Println("drone-scp error: ", err) 379 | var cerr copyError 380 | if !errors.As(err, &cerr) { 381 | fmt.Println("drone-scp rollback: remove all target tmp file") 382 | if err := p.removeAllDestFile(); err != nil { 383 | return err 384 | } 385 | } 386 | return err 387 | } 388 | } 389 | 390 | fmt.Println("===================================================") 391 | fmt.Println("✅ Successfully executed transfer data to all host") 392 | fmt.Println("===================================================") 393 | 394 | return nil 395 | } 396 | 397 | func (p Plugin) hostPort(host string) (string, string) { 398 | hosts := strings.Split(host, ":") 399 | port := strconv.Itoa(p.Config.Port) 400 | if len(hosts) > 1 && 401 | (p.Config.Protocol == easyssh.PROTOCOL_TCP || 402 | p.Config.Protocol == easyssh.PROTOCOL_TCP4) { 403 | host = hosts[0] 404 | port = hosts[1] 405 | } 406 | 407 | return host, port 408 | } 409 | 410 | func trimValues(keys []string) []string { 411 | var newKeys []string 412 | 413 | for _, value := range keys { 414 | value = strings.TrimSpace(value) 415 | if len(value) == 0 { 416 | continue 417 | } 418 | 419 | newKeys = append(newKeys, value) 420 | } 421 | 422 | return newKeys 423 | } 424 | -------------------------------------------------------------------------------- /plugin_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "os/user" 9 | "path/filepath" 10 | "reflect" 11 | "testing" 12 | "time" 13 | 14 | "github.com/appleboy/easyssh-proxy" 15 | "github.com/stretchr/testify/assert" 16 | "golang.org/x/crypto/ssh" 17 | ) 18 | 19 | func TestMissingAllConfig(t *testing.T) { 20 | var plugin Plugin 21 | 22 | err := plugin.Exec() 23 | 24 | assert.NotNil(t, err) 25 | } 26 | 27 | func TestMissingSSHConfig(t *testing.T) { 28 | plugin := Plugin{ 29 | Config: Config{ 30 | Host: []string{"example.com"}, 31 | Username: "ubuntu", 32 | }, 33 | } 34 | 35 | err := plugin.Exec() 36 | 37 | assert.NotNil(t, err) 38 | } 39 | 40 | func TestMissingSourceConfig(t *testing.T) { 41 | plugin := Plugin{ 42 | Config: Config{ 43 | Host: []string{"example.com"}, 44 | Username: "ubuntu", 45 | Port: 443, 46 | Password: "1234", 47 | }, 48 | } 49 | 50 | err := plugin.Exec() 51 | 52 | assert.NotNil(t, err) 53 | } 54 | 55 | func TestTrimElement(t *testing.T) { 56 | var input, result []string 57 | 58 | input = []string{"1", " ", "3"} 59 | result = []string{"1", "3"} 60 | 61 | assert.Equal(t, result, trimValues(input)) 62 | 63 | input = []string{"1", "2"} 64 | result = []string{"1", "2"} 65 | 66 | assert.Equal(t, result, trimValues(input)) 67 | } 68 | 69 | func TestSCPFileFromPublicKey(t *testing.T) { 70 | if os.Getenv("SSH_AUTH_SOCK") != "" { 71 | if err := exec.Command("eval", "`ssh-agent -k`").Run(); err != nil { 72 | t.Fatalf("exec: %v", err) 73 | } 74 | } 75 | 76 | u, err := user.Lookup("drone-scp") 77 | if err != nil { 78 | t.Fatalf("Lookup: %v", err) 79 | } 80 | 81 | plugin := Plugin{ 82 | Config: Config{ 83 | Host: []string{"localhost"}, 84 | Username: "drone-scp", 85 | Port: 22, 86 | KeyPath: "tests/.ssh/id_rsa", 87 | Source: []string{"tests/a.txt", "tests/b.txt"}, 88 | Target: []string{filepath.Join(u.HomeDir, "/test")}, 89 | CommandTimeout: 60 * time.Second, 90 | TarExec: "tar", 91 | }, 92 | } 93 | 94 | err = plugin.Exec() 95 | assert.Nil(t, err) 96 | 97 | // check file exist 98 | if _, err := os.Stat(filepath.Join(u.HomeDir, "/test/tests/a.txt")); os.IsNotExist(err) { 99 | t.Fatalf("SCP-error: %v", err) 100 | } 101 | 102 | if _, err := os.Stat(filepath.Join(u.HomeDir, "/test/tests/b.txt")); os.IsNotExist(err) { 103 | t.Fatalf("SCP-error: %v", err) 104 | } 105 | 106 | // Test -rm flag 107 | plugin.Config.Source = []string{"tests/a.txt"} 108 | plugin.Config.Remove = true 109 | 110 | err = plugin.Exec() 111 | assert.Nil(t, err) 112 | 113 | // check file exist 114 | if _, err := os.Stat(filepath.Join(u.HomeDir, "/test/tests/b.txt")); os.IsExist(err) { 115 | t.Fatalf("SCP-error: %v", err) 116 | } 117 | } 118 | 119 | func TestSCPFileFromPublicKeyWithPassphrase(t *testing.T) { 120 | if os.Getenv("SSH_AUTH_SOCK") != "" { 121 | if err := exec.Command("eval", "`ssh-agent -k`").Run(); err != nil { 122 | t.Fatalf("exec: %v", err) 123 | } 124 | } 125 | 126 | u, err := user.Lookup("drone-scp") 127 | if err != nil { 128 | t.Fatalf("Lookup: %v", err) 129 | } 130 | 131 | plugin := Plugin{ 132 | Config: Config{ 133 | Host: []string{"localhost"}, 134 | Username: "drone-scp", 135 | Port: 22, 136 | KeyPath: "tests/.ssh/test", 137 | Passphrase: "1234", 138 | Source: []string{"tests/a.txt", "tests/b.txt"}, 139 | Target: []string{filepath.Join(u.HomeDir, "/test2")}, 140 | CommandTimeout: 60 * time.Second, 141 | TarExec: "tar", 142 | }, 143 | } 144 | 145 | err = plugin.Exec() 146 | assert.Nil(t, err) 147 | 148 | // check file exist 149 | if _, err := os.Stat(filepath.Join(u.HomeDir, "/test2/tests/a.txt")); os.IsNotExist(err) { 150 | t.Fatalf("SCP-error: %v", err) 151 | } 152 | 153 | if _, err := os.Stat(filepath.Join(u.HomeDir, "/test2/tests/b.txt")); os.IsNotExist(err) { 154 | t.Fatalf("SCP-error: %v", err) 155 | } 156 | } 157 | 158 | func TestWrongFingerprint(t *testing.T) { 159 | u, err := user.Lookup("drone-scp") 160 | if err != nil { 161 | t.Fatalf("Lookup: %v", err) 162 | } 163 | 164 | plugin := Plugin{ 165 | Config: Config{ 166 | Host: []string{"localhost"}, 167 | Username: "drone-scp", 168 | Port: 22, 169 | KeyPath: "./tests/.ssh/id_rsa", 170 | Source: []string{"tests/a.txt", "tests/b.txt"}, 171 | Target: []string{filepath.Join(u.HomeDir, "/test2")}, 172 | CommandTimeout: 60 * time.Second, 173 | TarExec: "tar", 174 | Fingerprint: "wrong", 175 | }, 176 | } 177 | 178 | err = plugin.Exec() 179 | log.Println(err) 180 | assert.NotNil(t, err) 181 | } 182 | 183 | func getHostPublicKeyFile(keypath string) (ssh.PublicKey, error) { 184 | var pubkey ssh.PublicKey 185 | var err error 186 | buf, err := os.ReadFile(keypath) 187 | if err != nil { 188 | return nil, err 189 | } 190 | 191 | pubkey, _, _, _, err = ssh.ParseAuthorizedKey(buf) 192 | if err != nil { 193 | return nil, err 194 | } 195 | 196 | return pubkey, nil 197 | } 198 | 199 | func TestSCPFileFromPublicKeyWithFingerprint(t *testing.T) { 200 | if os.Getenv("SSH_AUTH_SOCK") != "" { 201 | if err := exec.Command("eval", "`ssh-agent -k`").Run(); err != nil { 202 | t.Fatalf("exec: %v", err) 203 | } 204 | } 205 | 206 | u, err := user.Lookup("drone-scp") 207 | if err != nil { 208 | t.Fatalf("Lookup: %v", err) 209 | } 210 | 211 | hostKey, err := getHostPublicKeyFile("/etc/ssh/ssh_host_rsa_key.pub") 212 | assert.NoError(t, err) 213 | 214 | plugin := Plugin{ 215 | Config: Config{ 216 | Host: []string{"localhost"}, 217 | Username: "drone-scp", 218 | Port: 22, 219 | KeyPath: "./tests/.ssh/id_rsa", 220 | Fingerprint: ssh.FingerprintSHA256(hostKey), 221 | Source: []string{"tests/a.txt", "tests/b.txt"}, 222 | Target: []string{filepath.Join(u.HomeDir, "/test2")}, 223 | CommandTimeout: 60 * time.Second, 224 | TarExec: "tar", 225 | }, 226 | } 227 | 228 | err = plugin.Exec() 229 | assert.Nil(t, err) 230 | 231 | // check file exist 232 | if _, err := os.Stat(filepath.Join(u.HomeDir, "/test2/tests/a.txt")); os.IsNotExist(err) { 233 | t.Fatalf("SCP-error: %v", err) 234 | } 235 | 236 | if _, err := os.Stat(filepath.Join(u.HomeDir, "/test2/tests/b.txt")); os.IsNotExist(err) { 237 | t.Fatalf("SCP-error: %v", err) 238 | } 239 | } 240 | 241 | func TestSCPWildcardFileList(t *testing.T) { 242 | if os.Getenv("SSH_AUTH_SOCK") != "" { 243 | if err := exec.Command("eval", "`ssh-agent -k`").Run(); err != nil { 244 | t.Fatalf("exec: %v", err) 245 | } 246 | } 247 | 248 | u, err := user.Lookup("drone-scp") 249 | if err != nil { 250 | t.Fatalf("Lookup: %v", err) 251 | } 252 | 253 | plugin := Plugin{ 254 | Config: Config{ 255 | Host: []string{"localhost"}, 256 | Username: "drone-scp", 257 | Port: 22, 258 | KeyPath: "tests/.ssh/id_rsa", 259 | Source: []string{"tests/global/*"}, 260 | Target: []string{filepath.Join(u.HomeDir, "abc")}, 261 | CommandTimeout: 60 * time.Second, 262 | TarExec: "tar", 263 | }, 264 | } 265 | 266 | err = plugin.Exec() 267 | assert.Nil(t, err) 268 | 269 | // check file exist 270 | if _, err := os.Stat(filepath.Join(u.HomeDir, "abc/tests/global/c.txt")); os.IsNotExist(err) { 271 | t.Fatalf("SCP-error: %v", err) 272 | } 273 | 274 | if _, err := os.Stat(filepath.Join(u.HomeDir, "abc/tests/global/d.txt")); os.IsNotExist(err) { 275 | t.Fatalf("SCP-error: %v", err) 276 | } 277 | } 278 | 279 | func TestSCPFromProxySetting(t *testing.T) { 280 | u, err := user.Lookup("drone-scp") 281 | if err != nil { 282 | t.Fatalf("Lookup: %v", err) 283 | } 284 | 285 | plugin := Plugin{ 286 | Config: Config{ 287 | Host: []string{"localhost"}, 288 | Username: "drone-scp", 289 | Port: 22, 290 | KeyPath: "tests/.ssh/id_rsa", 291 | Source: []string{"tests/global/*"}, 292 | Target: []string{filepath.Join(u.HomeDir, "def")}, 293 | CommandTimeout: 60 * time.Second, 294 | TarExec: "tar", 295 | Proxy: easyssh.DefaultConfig{ 296 | Server: "localhost", 297 | User: "drone-scp", 298 | Port: "22", 299 | KeyPath: "./tests/.ssh/id_rsa", 300 | }, 301 | }, 302 | } 303 | 304 | err = plugin.Exec() 305 | assert.Nil(t, err) 306 | 307 | // check file exist 308 | if _, err := os.Stat(filepath.Join(u.HomeDir, "def/tests/global/c.txt")); os.IsNotExist(err) { 309 | t.Fatalf("SCP-error: %v", err) 310 | } 311 | 312 | if _, err := os.Stat(filepath.Join(u.HomeDir, "def/tests/global/d.txt")); os.IsNotExist(err) { 313 | t.Fatalf("SCP-error: %v", err) 314 | } 315 | } 316 | 317 | func TestStripComponentsFlag(t *testing.T) { 318 | if os.Getenv("SSH_AUTH_SOCK") != "" { 319 | if err := exec.Command("eval", "`ssh-agent -k`").Run(); err != nil { 320 | t.Fatalf("exec: %v", err) 321 | } 322 | } 323 | 324 | u, err := user.Lookup("drone-scp") 325 | if err != nil { 326 | t.Fatalf("Lookup: %v", err) 327 | } 328 | 329 | plugin := Plugin{ 330 | Config: Config{ 331 | Host: []string{"localhost"}, 332 | Username: "drone-scp", 333 | Port: 22, 334 | KeyPath: "tests/.ssh/id_rsa", 335 | Source: []string{"tests/global/*"}, 336 | StripComponents: 2, 337 | Target: []string{filepath.Join(u.HomeDir, "123")}, 338 | CommandTimeout: 60 * time.Second, 339 | TarExec: "tar", 340 | }, 341 | } 342 | 343 | err = plugin.Exec() 344 | assert.Nil(t, err) 345 | 346 | // check file exist 347 | if _, err := os.Stat(filepath.Join(u.HomeDir, "123/c.txt")); os.IsNotExist(err) { 348 | t.Fatalf("SCP-error: %v", err) 349 | } 350 | 351 | if _, err := os.Stat(filepath.Join(u.HomeDir, "123/d.txt")); os.IsNotExist(err) { 352 | t.Fatalf("SCP-error: %v", err) 353 | } 354 | } 355 | 356 | func TestUseInsecureCipherFlag(t *testing.T) { 357 | u, err := user.Lookup("drone-scp") 358 | if err != nil { 359 | t.Fatalf("Lookup: %v", err) 360 | } 361 | 362 | plugin := Plugin{ 363 | Config: Config{ 364 | Host: []string{"localhost"}, 365 | Username: "drone-scp", 366 | Port: 22, 367 | KeyPath: "tests/.ssh/id_rsa", 368 | Source: []string{"tests/global/*"}, 369 | StripComponents: 2, 370 | Target: []string{filepath.Join(u.HomeDir, "123")}, 371 | CommandTimeout: 60 * time.Second, 372 | TarExec: "tar", 373 | UseInsecureCipher: true, 374 | }, 375 | } 376 | 377 | err = plugin.Exec() 378 | assert.Nil(t, err) 379 | 380 | // check file exist 381 | if _, err := os.Stat(filepath.Join(u.HomeDir, "123/c.txt")); os.IsNotExist(err) { 382 | t.Fatalf("SCP-error: %v", err) 383 | } 384 | 385 | if _, err := os.Stat(filepath.Join(u.HomeDir, "123/d.txt")); os.IsNotExist(err) { 386 | t.Fatalf("SCP-error: %v", err) 387 | } 388 | } 389 | 390 | func TestIgnoreList(t *testing.T) { 391 | if os.Getenv("SSH_AUTH_SOCK") != "" { 392 | if err := exec.Command("eval", "`ssh-agent -k`").Run(); err != nil { 393 | t.Fatalf("exec: %v", err) 394 | } 395 | } 396 | 397 | u, err := user.Lookup("drone-scp") 398 | if err != nil { 399 | t.Fatalf("Lookup: %v", err) 400 | } 401 | 402 | plugin := Plugin{ 403 | Config: Config{ 404 | Host: []string{"localhost"}, 405 | Username: "drone-scp", 406 | Port: 22, 407 | KeyPath: "tests/.ssh/id_rsa", 408 | Source: []string{"tests/global/*", "!tests/global/c.txt", "!tests/global/e.txt"}, 409 | StripComponents: 2, 410 | Target: []string{filepath.Join(u.HomeDir, "ignore")}, 411 | CommandTimeout: 60 * time.Second, 412 | TarExec: "tar", 413 | Debug: true, 414 | }, 415 | } 416 | 417 | err = plugin.Exec() 418 | assert.Nil(t, err) 419 | 420 | // check file exist 421 | if _, err := os.Stat(filepath.Join(u.HomeDir, "ignore/c.txt")); !os.IsNotExist(err) { 422 | t.Fatal("c.txt file exist") 423 | } 424 | 425 | // check file exist 426 | if _, err := os.Stat(filepath.Join(u.HomeDir, "ignore/e.txt")); !os.IsNotExist(err) { 427 | t.Fatal("c.txt file exist") 428 | } 429 | 430 | if _, err := os.Stat(filepath.Join(u.HomeDir, "ignore/d.txt")); os.IsNotExist(err) { 431 | t.Fatalf("SCP-error: %v", err) 432 | } 433 | } 434 | 435 | // func TestSCPFileFromSSHAgent(t *testing.T) { 436 | // if os.Getenv("SSH_AUTH_SOCK") == "" { 437 | // exec.Command("eval", "`ssh-agent -s`").Run() 438 | // exec.Command("ssh-add", "tests/.ssh/id_rsa").Run() 439 | // } 440 | 441 | // u, err := user.Lookup("drone-scp") 442 | // if err != nil { 443 | // t.Fatalf("Lookup: %v", err) 444 | // } 445 | 446 | // plugin := Plugin{ 447 | // Config: Config{ 448 | // Host: []string{"localhost"}, 449 | // Username: "drone-scp", 450 | // Port: "22", 451 | // Source: []string{"tests/a.txt", "tests/b.txt"}, 452 | // Target: []string{u.HomeDir + "/test"}, 453 | // }, 454 | // } 455 | 456 | // err = plugin.Exec() 457 | // assert.Nil(t, err) 458 | // } 459 | 460 | // func TestSCPFileFromPassword(t *testing.T) { 461 | // u, err := user.Lookup("drone-scp") 462 | // if err != nil { 463 | // t.Fatalf("Lookup: %v", err) 464 | // } 465 | 466 | // plugin := Plugin{ 467 | // Config: Config{ 468 | // Host: []string{"localhost"}, 469 | // Username: "drone-scp", 470 | // Port: "22", 471 | // Password: "1234", 472 | // Source: []string{"tests/a.txt", "tests/b.txt"}, 473 | // Target: []string{u.HomeDir + "/test"}, 474 | // }, 475 | // } 476 | 477 | // err = plugin.Exec() 478 | // assert.Nil(t, err) 479 | // } 480 | 481 | func TestIncorrectPassword(t *testing.T) { 482 | plugin := Plugin{ 483 | Config: Config{ 484 | Host: []string{"localhost"}, 485 | Username: "drone-scp", 486 | Port: 22, 487 | Password: "123456", 488 | Source: []string{"tests/a.txt", "tests/b.txt"}, 489 | Target: []string{"/home"}, 490 | CommandTimeout: 60 * time.Second, 491 | TarExec: "tar", 492 | }, 493 | } 494 | 495 | err := plugin.Exec() 496 | assert.NotNil(t, err) 497 | } 498 | 499 | func TestNoPermissionCreateFolder(t *testing.T) { 500 | u, err := user.Lookup("drone-scp") 501 | if err != nil { 502 | t.Fatalf("Lookup: %v", err) 503 | } 504 | 505 | plugin := Plugin{ 506 | Config: Config{ 507 | Host: []string{"localhost"}, 508 | Username: "drone-scp", 509 | Port: 22, 510 | KeyPath: "tests/.ssh/id_rsa", 511 | Source: []string{"tests/a.txt", "tests/b.txt"}, 512 | Target: []string{"/etc/test"}, 513 | CommandTimeout: 60 * time.Second, 514 | TarExec: "tar", 515 | }, 516 | } 517 | 518 | err = plugin.Exec() 519 | assert.NotNil(t, err) 520 | 521 | // check tmp file exist 522 | if _, err = os.Stat(filepath.Join(u.HomeDir, plugin.DestFile)); os.IsExist(err) { 523 | t.Fatalf("SCP-error: %v", err) 524 | } 525 | } 526 | 527 | func TestGlobList(t *testing.T) { 528 | // wrong patern 529 | paterns := []string{"[]a]", "tests/?.txt"} 530 | expects := []string{"tests/a.txt", "tests/b.txt"} 531 | assert.Equal(t, expects, globList(paterns).Source) 532 | 533 | paterns = []string{"tests/*.txt", "tests/.ssh/*", "abc*"} 534 | expects = []string{"tests/a.txt", "tests/b.txt", "tests/.ssh/id_rsa", "tests/.ssh/id_rsa.pub", "tests/.ssh/test", "tests/.ssh/test.pub"} 535 | assert.Equal(t, expects, globList(paterns).Source) 536 | 537 | paterns = []string{"tests/?.txt"} 538 | expects = []string{"tests/a.txt", "tests/b.txt"} 539 | assert.Equal(t, expects, globList(paterns).Source) 540 | 541 | // remove item which file not found. 542 | paterns = []string{"tests/aa.txt", "tests/b.txt"} 543 | expects = []string{"tests/b.txt"} 544 | assert.Equal(t, expects, globList(paterns).Source) 545 | 546 | paterns = []string{"./tests/b.txt"} 547 | expects = []string{"./tests/b.txt"} 548 | assert.Equal(t, expects, globList(paterns).Source) 549 | 550 | paterns = []string{"./tests/*.txt", "!./tests/b.txt"} 551 | expectSources := []string{"tests/a.txt", "tests/b.txt"} 552 | expectIgnores := []string{"./tests/b.txt"} 553 | result := globList(paterns) 554 | assert.Equal(t, expectSources, result.Source) 555 | assert.Equal(t, expectIgnores, result.Ignore) 556 | } 557 | 558 | func TestRemoveDestFile(t *testing.T) { 559 | ssh := &easyssh.MakeConfig{ 560 | Server: "localhost", 561 | User: "drone-scp", 562 | Port: "22", 563 | KeyPath: "tests/.ssh/id_rsa", 564 | // io timeout 565 | Timeout: 1, 566 | } 567 | plugin := Plugin{ 568 | Config: Config{ 569 | CommandTimeout: 60 * time.Second, 570 | }, 571 | DestFile: "/etc/resolv.conf", 572 | } 573 | 574 | _, _, _, err := ssh.Run("ver", plugin.Config.CommandTimeout) 575 | systemType := "unix" 576 | if err == nil { 577 | systemType = "windows" 578 | } 579 | 580 | // ssh io timeout 581 | err = plugin.removeDestFile(systemType, ssh) 582 | assert.Error(t, err) 583 | 584 | ssh.Timeout = 0 585 | 586 | // permission denied 587 | err = plugin.removeDestFile(systemType, ssh) 588 | assert.Error(t, err) 589 | } 590 | 591 | func TestPlugin_buildUnTarArgs(t *testing.T) { 592 | type fields struct { 593 | Config Config 594 | DestFile string 595 | } 596 | type args struct { 597 | target string 598 | } 599 | tests := []struct { 600 | name string 601 | fields fields 602 | args args 603 | want []string 604 | }{ 605 | { 606 | name: "default command", 607 | fields: fields{ 608 | Config: Config{ 609 | Overwrite: false, 610 | UnlinkFirst: false, 611 | TarExec: "tar", 612 | }, 613 | DestFile: "foo.tar.gz", 614 | }, 615 | args: args{ 616 | target: "foo", 617 | }, 618 | want: []string{"tar", "-zxf", "foo.tar.gz", "-C", "foo"}, 619 | }, 620 | { 621 | name: "strip components", 622 | fields: fields{ 623 | Config: Config{ 624 | Overwrite: false, 625 | UnlinkFirst: false, 626 | TarExec: "tar", 627 | StripComponents: 2, 628 | }, 629 | DestFile: "foo.tar.gz", 630 | }, 631 | args: args{ 632 | target: "foo", 633 | }, 634 | want: []string{"tar", "-zxf", "foo.tar.gz", "--strip-components", "2", "-C", "foo"}, 635 | }, 636 | { 637 | name: "overwrite", 638 | fields: fields{ 639 | Config: Config{ 640 | TarExec: "tar", 641 | StripComponents: 2, 642 | Overwrite: true, 643 | UnlinkFirst: false, 644 | }, 645 | DestFile: "foo.tar.gz", 646 | }, 647 | args: args{ 648 | target: "foo", 649 | }, 650 | want: []string{"tar", "-zxf", "foo.tar.gz", "--strip-components", "2", "--overwrite", "-C", "foo"}, 651 | }, 652 | { 653 | name: "unlink first", 654 | fields: fields{ 655 | Config: Config{ 656 | TarExec: "tar", 657 | StripComponents: 2, 658 | Overwrite: true, 659 | UnlinkFirst: true, 660 | }, 661 | DestFile: "foo.tar.gz", 662 | }, 663 | args: args{ 664 | target: "foo", 665 | }, 666 | want: []string{"tar", "-zxf", "foo.tar.gz", "--strip-components", "2", "--overwrite", "--unlink-first", "-C", "foo"}, 667 | }, 668 | { 669 | name: "output folder path with space", 670 | fields: fields{ 671 | Config: Config{ 672 | TarExec: "tar", 673 | StripComponents: 2, 674 | Overwrite: true, 675 | UnlinkFirst: true, 676 | }, 677 | DestFile: "foo.tar.gz", 678 | }, 679 | args: args{ 680 | target: "foo\\ bar", 681 | }, 682 | want: []string{"tar", "-zxf", "foo.tar.gz", "--strip-components", "2", "--overwrite", "--unlink-first", "-C", "foo\\ bar"}, 683 | }, 684 | } 685 | for _, tt := range tests { 686 | t.Run(tt.name, func(t *testing.T) { 687 | p := &Plugin{ 688 | Config: tt.fields.Config, 689 | DestFile: tt.fields.DestFile, 690 | } 691 | if got := p.buildUnTarArgs(tt.args.target); !reflect.DeepEqual(got, tt.want) { 692 | t.Errorf("Plugin.buildArgs() = %v, want %v", got, tt.want) 693 | } 694 | }) 695 | } 696 | } 697 | 698 | func TestPlugin_buildTarArgs(t *testing.T) { 699 | type fields struct { 700 | Config Config 701 | } 702 | type args struct { 703 | src string 704 | } 705 | tests := []struct { 706 | name string 707 | fields fields 708 | args args 709 | want []string 710 | }{ 711 | { 712 | name: "default command", 713 | fields: fields{ 714 | Config: Config{ 715 | TarExec: "tar", 716 | }, 717 | }, 718 | args: args{ 719 | src: "foo.tar.gz", 720 | }, 721 | want: []string{"-zcf", "foo.tar.gz"}, 722 | }, 723 | { 724 | name: "ignore list", 725 | fields: fields{ 726 | Config: Config{ 727 | TarExec: "tar", 728 | Source: []string{ 729 | "tests/*.txt", 730 | "!tests/a.txt", 731 | }, 732 | }, 733 | }, 734 | args: args{ 735 | src: "foo.tar.gz", 736 | }, 737 | want: []string{"--exclude", "tests/a.txt", "-zcf", "foo.tar.gz", "tests/a.txt", "tests/b.txt"}, 738 | }, 739 | { 740 | name: "dereference flag", 741 | fields: fields{ 742 | Config: Config{ 743 | TarExec: "tar", 744 | TarDereference: true, 745 | Source: []string{ 746 | "tests/*.txt", 747 | "!tests/a.txt", 748 | }, 749 | }, 750 | }, 751 | args: args{ 752 | src: "foo.tar.gz", 753 | }, 754 | want: []string{"--exclude", "tests/a.txt", "--dereference", "-zcf", "foo.tar.gz", "tests/a.txt", "tests/b.txt"}, 755 | }, 756 | } 757 | for _, tt := range tests { 758 | t.Run(tt.name, func(t *testing.T) { 759 | p := &Plugin{ 760 | Config: tt.fields.Config, 761 | } 762 | if got := p.buildTarArgs(tt.args.src); !reflect.DeepEqual(got, tt.want) { 763 | t.Errorf("Plugin.buildTarArgs() = %v, want %v", got, tt.want) 764 | } 765 | }) 766 | } 767 | } 768 | 769 | func TestTargetFolderWithSpaces(t *testing.T) { 770 | if os.Getenv("SSH_AUTH_SOCK") != "" { 771 | if err := exec.Command("eval", "`ssh-agent -k`").Run(); err != nil { 772 | t.Fatalf("exec: %v", err) 773 | } 774 | } 775 | 776 | u, err := user.Lookup("drone-scp") 777 | if err != nil { 778 | t.Fatalf("Lookup: %v", err) 779 | } 780 | 781 | plugin := Plugin{ 782 | Config: Config{ 783 | Host: []string{"localhost"}, 784 | Username: "drone-scp", 785 | Port: 22, 786 | KeyPath: "tests/.ssh/id_rsa", 787 | Source: []string{"tests/global/*"}, 788 | StripComponents: 2, 789 | Target: []string{filepath.Join(u.HomeDir, "123 456 789")}, 790 | CommandTimeout: 60 * time.Second, 791 | TarExec: "tar", 792 | }, 793 | } 794 | 795 | err = plugin.Exec() 796 | assert.Nil(t, err) 797 | 798 | // check file exist 799 | if _, err := os.Stat(filepath.Join(u.HomeDir, "123 456 789", "c.txt")); os.IsNotExist(err) { 800 | t.Fatalf("SCP-error: %v", err) 801 | } 802 | 803 | if _, err := os.Stat(filepath.Join(u.HomeDir, "123 456 789", "d.txt")); os.IsNotExist(err) { 804 | t.Fatalf("SCP-error: %v", err) 805 | } 806 | } 807 | 808 | func TestHostPortString(t *testing.T) { 809 | if os.Getenv("SSH_AUTH_SOCK") != "" { 810 | if err := exec.Command("eval", "`ssh-agent -k`").Run(); err != nil { 811 | t.Fatalf("exec: %v", err) 812 | } 813 | } 814 | 815 | u, err := user.Lookup("drone-scp") 816 | if err != nil { 817 | t.Fatalf("Lookup: %v", err) 818 | } 819 | 820 | plugin := Plugin{ 821 | Config: Config{ 822 | Host: []string{"localhost:22", "localhost:22"}, 823 | Username: "drone-scp", 824 | Protocol: easyssh.PROTOCOL_TCP4, 825 | Port: 8080, 826 | KeyPath: "tests/.ssh/id_rsa", 827 | Source: []string{"tests/global/*"}, 828 | StripComponents: 2, 829 | Target: []string{filepath.Join(u.HomeDir, "1234")}, 830 | CommandTimeout: 60 * time.Second, 831 | TarExec: "tar", 832 | }, 833 | } 834 | 835 | err = plugin.Exec() 836 | assert.Nil(t, err) 837 | 838 | // check file exist 839 | if _, err := os.Stat(filepath.Join(u.HomeDir, "1234", "c.txt")); os.IsNotExist(err) { 840 | t.Fatalf("SCP-error: %v", err) 841 | } 842 | 843 | if _, err := os.Stat(filepath.Join(u.HomeDir, "1234", "d.txt")); os.IsNotExist(err) { 844 | t.Fatalf("SCP-error: %v", err) 845 | } 846 | } 847 | 848 | // Unit test for hostPort 849 | func TestHostPort(t *testing.T) { 850 | p := Plugin{ 851 | Config: Config{ 852 | Port: 8080, 853 | Protocol: easyssh.PROTOCOL_TCP4, 854 | }, 855 | } 856 | 857 | // Test case 1: host string with port 858 | host1 := "example.com:1234" 859 | expectedHost1 := "example.com" 860 | expectedPort1 := "1234" 861 | actualHost1, actualPort1 := p.hostPort(host1) 862 | if actualHost1 != expectedHost1 || actualPort1 != expectedPort1 { 863 | t.Errorf("hostPort(%s) = (%s, %s); expected (%s, %s)", host1, actualHost1, actualPort1, expectedHost1, expectedPort1) 864 | } 865 | 866 | // Test case 2: host string without port 867 | host2 := "example.com" 868 | expectedHost2 := "example.com" 869 | expectedPort2 := "8080" // default port 870 | actualHost2, actualPort2 := p.hostPort(host2) 871 | if actualHost2 != expectedHost2 || actualPort2 != expectedPort2 { 872 | t.Errorf("hostPort(%s) = (%s, %s); expected (%s, %s)", host2, actualHost2, actualPort2, expectedHost2, expectedPort2) 873 | } 874 | } 875 | 876 | func TestPlugin_hostPort(t *testing.T) { 877 | type fields struct { 878 | Config Config 879 | Writer io.Writer 880 | } 881 | type args struct { 882 | h string 883 | } 884 | tests := []struct { 885 | name string 886 | fields fields 887 | args args 888 | wantHost string 889 | wantPort string 890 | }{ 891 | { 892 | name: "default host and port", 893 | fields: fields{ 894 | Config: Config{ 895 | Port: 22, 896 | }, 897 | }, 898 | args: args{ 899 | h: "localhost", 900 | }, 901 | wantHost: "localhost", 902 | wantPort: "22", 903 | }, 904 | { 905 | name: "different port", 906 | fields: fields{ 907 | Config: Config{ 908 | Port: 22, 909 | Protocol: easyssh.PROTOCOL_TCP4, 910 | }, 911 | }, 912 | args: args{ 913 | h: "localhost:443", 914 | }, 915 | wantHost: "localhost", 916 | wantPort: "443", 917 | }, 918 | { 919 | name: "ipv6", 920 | fields: fields{ 921 | Config: Config{ 922 | Port: 22, 923 | Protocol: easyssh.PROTOCOL_TCP6, 924 | }, 925 | }, 926 | args: args{ 927 | h: "::1", 928 | }, 929 | wantHost: "::1", 930 | wantPort: "22", 931 | }, 932 | } 933 | for _, tt := range tests { 934 | t.Run(tt.name, func(t *testing.T) { 935 | p := Plugin{ 936 | Config: tt.fields.Config, 937 | } 938 | gotHost, gotPort := p.hostPort(tt.args.h) 939 | if gotHost != tt.wantHost { 940 | t.Errorf("Plugin.hostPort() gotHost = %v, want %v", gotHost, tt.wantHost) 941 | } 942 | if gotPort != tt.wantPort { 943 | t.Errorf("Plugin.hostPort() gotPort = %v, want %v", gotPort, tt.wantPort) 944 | } 945 | }) 946 | } 947 | } 948 | 949 | func TestIgnoreFolder(t *testing.T) { 950 | if os.Getenv("SSH_AUTH_SOCK") != "" { 951 | if err := exec.Command("eval", "`ssh-agent -k`").Run(); err != nil { 952 | t.Fatalf("exec: %v", err) 953 | } 954 | } 955 | 956 | u, err := user.Lookup("drone-scp") 957 | if err != nil { 958 | t.Fatalf("Lookup: %v", err) 959 | } 960 | 961 | plugin := Plugin{ 962 | Config: Config{ 963 | Host: []string{"localhost"}, 964 | Username: "drone-scp", 965 | Protocol: easyssh.PROTOCOL_TCP4, 966 | Port: 22, 967 | KeyPath: "tests/.ssh/id_rsa", 968 | Source: []string{"tests/*", "!tests/global"}, 969 | Target: []string{filepath.Join(u.HomeDir, "test_ignore")}, 970 | CommandTimeout: 60 * time.Second, 971 | TarExec: "tar", 972 | }, 973 | } 974 | 975 | err = plugin.Exec() 976 | assert.Nil(t, err) 977 | 978 | // check file exist 979 | if _, err := os.Stat(filepath.Join(u.HomeDir, "test_ignore", "global", "c.txt")); !os.IsNotExist(err) { 980 | t.Fatalf("SCP-error: %v", err) 981 | } 982 | 983 | if _, err := os.Stat(filepath.Join(u.HomeDir, "test_ignore", "global", "d.txt")); !os.IsNotExist(err) { 984 | t.Fatalf("SCP-error: %v", err) 985 | } 986 | } 987 | -------------------------------------------------------------------------------- /tests/.ssh/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA4e2D/qPN08pzTac+a8ZmlP1ziJOXk45CynMPtva0rtK/RB26 3 | VbfAF0hIJji7ltvnYnqCU9oFfvEM33cTn7T96+od8ib/Vz25YU8ZbstqtIskPuwC 4 | bv3K0mAHgsviJyRD7yM+QKTbBQEgbGuW6gtbMKhiYfiIB4Dyj7AdS/fk3v26wDgz 5 | 7SHI5OBqu9bv1KhxQYdFEnU3PAtAqeccgzNpbH3eYLyGzuUxEIJlhpZ/uU2G9ppj 6 | /cSrONVPiI8Ahi4RrlZjmP5l57/sq1ClGulyLpFcMw68kP5FikyqHpHJHRBNgU57 7 | 1y0Ph33SjBbs0haCIAcmreWEhGe+/OXnJe6VUQIDAQABAoIBAH97emORIm9DaVSD 8 | 7mD6DqA7c5m5Tmpgd6eszU08YC/Vkz9oVuBPUwDQNIX8tT0m0KVs42VVPIyoj874 9 | bgZMJoucC1G8V5Bur9AMxhkShx9g9A7dNXJTmsKilRpk2TOk7wBdLp9jZoKoZBdJ 10 | jlp6FfaazQjjKD6zsCsMATwAoRCBpBNsmT6QDN0n0bIgY0tE6YGQaDdka0dAv68G 11 | R0VZrcJ9voT6+f+rgJLoojn2DAu6iXaM99Gv8FK91YCymbQlXXgrk6CyS0IHexN7 12 | V7a3k767KnRbrkqd3o6JyNun/CrUjQwHs1IQH34tvkWScbseRaFehcAm6mLT93RP 13 | muauvMECgYEA9AXGtfDMse0FhvDPZx4mx8x+vcfsLvDHcDLkf/lbyPpu97C27b/z 14 | ia07bu5TAXesUZrWZtKA5KeRE5doQSdTOv1N28BEr8ZwzDJwfn0DPUYUOxsN2iIy 15 | MheO5A45Ko7bjKJVkZ61Mb1UxtqCTF9mqu9R3PBdJGthWOd+HUvF460CgYEA7QRf 16 | Z8+vpGA+eSuu29e0xgRKnRzed5zXYpcI4aERc3JzBgO4Z0er9G8l66OWVGdMfpe6 17 | CBajC5ToIiT8zqoYxXwqJgN+glir4gJe3mm8J703QfArZiQrdk0NTi5bY7+vLLG/ 18 | knTrtpdsKih6r3kjhuPPaAsIwmMxIydFvATKjLUCgYEAh/y4EihRSk5WKC8GxeZt 19 | oiZ58vT4z+fqnMIfyJmD5up48JuQNcokw/LADj/ODiFM7GUnWkGxBrvDA3H67WQm 20 | 49bJjs8E+BfUQFdTjYnJRlpJZ+7Zt1gbNQMf5ENw5CCchTDqEq6pN0DVf8PBnSIF 21 | KvkXW9KvdV5J76uCAn15mDkCgYA1y8dHzbjlCz9Cy2pt1aDfTPwOew33gi7U3skS 22 | RTerx29aDyAcuQTLfyrROBkX4TZYiWGdEl5Bc7PYhCKpWawzrsH2TNa7CRtCOh2E 23 | R+V/84+GNNf04ALJYCXD9/ugQVKmR1XfDRCvKeFQFE38Y/dvV2etCswbKt5tRy2p 24 | xkCe/QKBgQCkLqafD4S20YHf6WTp3jp/4H/qEy2X2a8gdVVBi1uKkGDXr0n+AoVU 25 | ib4KbP5ovZlrjL++akMQ7V2fHzuQIFWnCkDA5c2ZAqzlM+ZN+HRG7gWur7Bt4XH1 26 | 7XC9wlRna4b3Ln8ew3q1ZcBjXwD4ppbTlmwAfQIaZTGJUgQbdsO9YA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tests/.ssh/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDh7YP+o83TynNNpz5rxmaU/XOIk5eTjkLKcw+29rSu0r9EHbpVt8AXSEgmOLuW2+dieoJT2gV+8QzfdxOftP3r6h3yJv9XPblhTxluy2q0iyQ+7AJu/crSYAeCy+InJEPvIz5ApNsFASBsa5bqC1swqGJh+IgHgPKPsB1L9+Te/brAODPtIcjk4Gq71u/UqHFBh0USdTc8C0Cp5xyDM2lsfd5gvIbO5TEQgmWGln+5TYb2mmP9xKs41U+IjwCGLhGuVmOY/mXnv+yrUKUa6XIukVwzDryQ/kWKTKoekckdEE2BTnvXLQ+HfdKMFuzSFoIgByat5YSEZ7785ecl7pVR drone-scp@localhost 2 | -------------------------------------------------------------------------------- /tests/.ssh/test: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAZka7A7i 3 | FscMeJBPyPteclAAAAEAAAAAEAAAIXAAAAB3NzaC1yc2EAAAADAQABAAACAQDz6aZ1jY2o 4 | nnuj2YNHJ/HhfvIu0B973v/+pFFOavnTUOhEEKEy3TASu+s9CkHrYZAtRc+QYIkNZI31mh 5 | HBhotdeP/7GoO2UirkFtrzyQKPNJxEcv0RBoG9ssN8jex0PyK6DHIYYFnIWadVBEEOh/H+ 6 | rK7j7u2/big3oTzYBuFrCwmYFcz5na99MzFeAUhazF44gVBma+zO+1quGeqF51UDIg1SMG 7 | vX8I7LNEqrKEBaIUQJKFQcxlOWlRLQsjJCymrOujsXsRrXHAQWcnxDcNevv2ZMOUl0ybvv 8 | 9yH0BiGbRBd1Hy8/QPILbAQaqu0oQE7fubN8Q8lqb3Jg0loID4x/5GPhSY8WAXpuLcXTOr 9 | b93SnCw1JsAgJDNqpuuRFy3BSZ7wBOr1jfeIoo7xk14OHiUjJ0uXDL9cLMkcw6ElWz81mr 10 | D2VCkXUz+qFyjJ+G7aGWRtctZoOzKln4yfNfUmwW8/8ra3QnmrMZ2xW2Ylw3ZhO+tLi7jI 11 | NHYFb54bAdLVPUU1ctIuJns2qkWnjJCxxMiynIqCif20/OU1n8CTJuOWiURmRdmvKOH4PE 12 | 3JxC2Qnk/3tV3Cf8hp1CH5VjBZ9AjGj5MDMHXyu34VY2WvYo5QyzfS3ySPoT8kCO0G0xpv 13 | jwCMHOK+G2RP4kqb/KKZguiKdgintBXuskTlJmD7kcMQAAB1CnEMQGwAKZbd3F1DJqwfPf 14 | KWjoUJKbTRiav6h5pQr65JaqDe/7YE2ZHYo5917AC2vPLwPxAnoHFMsbObd5mWcmpATg/0 15 | K/qkN5Z4Ml5U3bwr51wfSPh1MiAP21Aickt09BDstIJzNNwwgcY31O3k/d6VBjqyM6Ezop 16 | 66LI4s/IIni1BI+cALyEfzE4Qu16GfzIeM+JVxildP4VImhvNBESmmbBL8rNmSzlQ+FTuF 17 | JVmowUbcon1O0CppM1MRVPeG805XDwjxHXKwOp5O7MdTz7H8JeORoe8D6+4rNfJE0eQGY7 18 | Nm4+Wa97HzAFbT9IS433rxoGx9Qps3LAySFONso2JWSOEfo8rxnqO04DrfVHQhY3DkkwQt 19 | FsDnMtkthJa+ZzUYc75fnS0DBPGuF9DZUCqrev5oAUHP6C4Vc4b33JJQD4FZJ+ehk3Xsci 20 | cwJQsmgLyc5Jdh543Dm7kZoM9ku7HDNrB4H/1p45Vo6aBZMAY50x+fTdBeTgCzzhzzTbf+ 21 | 0IF8W3yW3/BYD+S2Byo3JKp6NH0Q8cgPJrGTl6GltGfpVuc6kLjMZ5zvxRbyWaqtIygM46 22 | W1izbA+9jwbHhitCtOk42e/ff6iEB1MVC13LqPty3gPNR8Pv0rDUDjJS4KiVwXqUY+bMr0 23 | C8l/hx93euHjLUJ49Ru6uy/2fBlHZEj6GmEAJhu/i6t2c1Rq0HBLis9X356oQT+YZnIai2 24 | ym0MknPxjeYBAItOV3zhRd1cYnk7CDcl1XALcnh0tqP712x24IJ+Ytqg7nvB2NZV8T469I 25 | 8Fp254Nr89HOMAXaZD0UcIPm7D2rfWV+YJFI3ZcJ/8DM99H3tpXe2j4oHMdmAbBd++09sx 26 | KBRdFLcvnBfd1lqwxpA7hbxzrxi/yehYCqzh5KQGaf2UXej6TPiVzBWVYbp34cMZtsT6mF 27 | K8SS3l5TXoNK2DNEk30o8K3q+vngQpfC9GZ/id4B7LS/3ybellxemZHXQoU4PxDkLKt7jd 28 | AAsd5WO13dv3n/qgyu8iBRiFU+W66NX0RJGkp+lZMnta0YzukafM2n6GDn/r/Cx/y21PAi 29 | ah8i41ByI1QLI4m1r+bRHdUxAarS/XJw4tTSFiZu3zddMYrlzeG9O3VUX9zBvBtfQbSmeJ 30 | omml0zlr/qD7TMsORiujy7XIn7sMW+Ls/NA8TvX8oRnACjXe/MYNEZ8WDu2rkZuY/Dfc+o 31 | NyYWO7kZ3kcejQZ1NusJSA7MG0FFGYSIaC9T9CWqYd5IcRSJW4dZnCt9z8CIJ6TSUFqMb/ 32 | H1Y5Rmi0IIX+8qbGGXVBDIBk5y9xtS43+nz1nsdXwDmkTiXN9+ZX+GDsLxCWoHGryrWDbk 33 | EuOAlqpvxFKzEkNsx+AC5wae6i/hBeiEce9bm4nZp+hFv1ic1Z9WS8B37YOFgJ4utGeOjB 34 | 6hnywUUJ3aH0LnCQNB3UzeFR7BmEaxmYD/phJodmjA5SD3CWpeizdXfrUjtqXGhYlr2jzq 35 | vBAeeYEO4uaHIGxg8GqoqtaseqVcIdtouHxrVAxxXkjShV2ji7oJ/AtrLZNlkKYxMk0TpX 36 | fFiKqL/uKfS78FfvVOhOkHZTD6ZeMgmdL/uOghEAtrf08ChyRvdp7QLjA802aio9eUVIQm 37 | lHb1ltPEbIZNuvQ5kTIwk2eM6EAkOh0MBMoAYOxOpIb00XHNRDGJYuLewByjMQa8EoT6VM 38 | NoiFIzJU9lLAXE6yz6JswctpTpLHK9Aq5vY7ObaOvrmpCQqsXfOuVUo2nR/FyEes97zuXG 39 | E4aKaHK4IAW4UY/oGYk7pU/yRpudhiNRMXzmcQXfVmBEHuvDrh2chg8lDYn++07F7RWqkI 40 | nfMAOWR8UEl4xp4zJtThDjRxNW6QLl8E1ADjndA9wVaKNSzv2i1TLXKBr5luFqY9MSJ2rm 41 | yBR5EwairH/Qn9TUxaDD+0p6J+E9iz1l8UPTJa/cjtwiySljahY/6tHHnr9YQVnox92yfU 42 | UXpfINGjYrpqh6EFwmyRw9fryIMvMhgZYo6ZoCRBCK2GfGAB0VTzJy2FGs4GecZK5ptXKu 43 | sOX8BgGX/Q/nAJ7PWf9hgYlX2YyjmLjQZDMWECp05VFx9znEETNKlwF1FX5/E/37ISyz4d 44 | I1LVSKOEccJX7jCR32LzvRW1UBX47Z+q3LVE4sa0QAV/JoISq6Qn6zAsVIV0yEPmVbd/xx 45 | aX2uBUGHhmd99YJDh81xJIoYEMRzoGVfp0JjfYcDUc+2I6JdrOMF9/KmMA5wsZl4OKiu/F 46 | cTRGjUkgw/cF2EFRGWknee2esYRB7tOr4y56qZ4gxqw8q9rYXhyB42jbdTvt5xcCm/ynid 47 | sn4InokRRoIiMIPL5Ur7FZQHOP+915MWUBsrTJtkCWQuqJheYUi3mCzh/7NadAKplRpaKb 48 | rS/DJIOOkjnGni/sDxJzPq7STDBVy4WStwQl6NI5hq+/c+JvN9GI4Vu/kz0z8qUcdShLaH 49 | l4njcaMpg4tpQMHtCBOicGyV0= 50 | -----END OPENSSH PRIVATE KEY----- 51 | -------------------------------------------------------------------------------- /tests/.ssh/test.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDz6aZ1jY2onnuj2YNHJ/HhfvIu0B973v/+pFFOavnTUOhEEKEy3TASu+s9CkHrYZAtRc+QYIkNZI31mhHBhotdeP/7GoO2UirkFtrzyQKPNJxEcv0RBoG9ssN8jex0PyK6DHIYYFnIWadVBEEOh/H+rK7j7u2/big3oTzYBuFrCwmYFcz5na99MzFeAUhazF44gVBma+zO+1quGeqF51UDIg1SMGvX8I7LNEqrKEBaIUQJKFQcxlOWlRLQsjJCymrOujsXsRrXHAQWcnxDcNevv2ZMOUl0ybvv9yH0BiGbRBd1Hy8/QPILbAQaqu0oQE7fubN8Q8lqb3Jg0loID4x/5GPhSY8WAXpuLcXTOrb93SnCw1JsAgJDNqpuuRFy3BSZ7wBOr1jfeIoo7xk14OHiUjJ0uXDL9cLMkcw6ElWz81mrD2VCkXUz+qFyjJ+G7aGWRtctZoOzKln4yfNfUmwW8/8ra3QnmrMZ2xW2Ylw3ZhO+tLi7jINHYFb54bAdLVPUU1ctIuJns2qkWnjJCxxMiynIqCif20/OU1n8CTJuOWiURmRdmvKOH4PE3JxC2Qnk/3tV3Cf8hp1CH5VjBZ9AjGj5MDMHXyu34VY2WvYo5QyzfS3ySPoT8kCO0G0xpvjwCMHOK+G2RP4kqb/KKZguiKdgintBXuskTlJmD7kcMQ== deploy@easyssh 2 | -------------------------------------------------------------------------------- /tests/a.txt: -------------------------------------------------------------------------------- 1 | appleboy 2 | -------------------------------------------------------------------------------- /tests/b.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/drone-scp/dd5217c90a0459b10906020080296e3eb248aefe/tests/b.txt -------------------------------------------------------------------------------- /tests/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -f "/etc/ssh/ssh_host_rsa_key" ]; then 4 | # generate fresh rsa key 5 | ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa 6 | fi 7 | 8 | if [ ! -f "/etc/ssh/ssh_host_dsa_key" ]; then 9 | # generate fresh dsa key 10 | ssh-keygen -f /etc/ssh/ssh_host_dsa_key -N '' -t dsa 11 | fi 12 | 13 | exec "$@" 14 | -------------------------------------------------------------------------------- /tests/global/c.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/drone-scp/dd5217c90a0459b10906020080296e3eb248aefe/tests/global/c.txt -------------------------------------------------------------------------------- /tests/global/d.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/drone-scp/dd5217c90a0459b10906020080296e3eb248aefe/tests/global/d.txt -------------------------------------------------------------------------------- /tests/global/e.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/drone-scp/dd5217c90a0459b10906020080296e3eb248aefe/tests/global/e.txt --------------------------------------------------------------------------------