├── .codecov.yml ├── _testdata └── test.txt ├── version.go ├── main.go ├── .github └── workflows │ ├── test.yaml │ ├── release.yaml │ └── codeql-analysis.yml ├── renovate.json ├── .gitignore ├── Makefile ├── aws ├── aws.go └── s3 │ ├── s3.go │ └── s3_test.go ├── LICENSE ├── CHANGELOG.md ├── .goreleaser.yml ├── go.mod ├── config ├── config.go └── config_test.go ├── cli └── cli.go ├── go.sum └── README.md /.codecov.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_testdata/test.txt: -------------------------------------------------------------------------------- 1 | This is a test file. 2 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var ( 4 | version string 5 | commit string 6 | date string 7 | ) 8 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/dtan4/s3url/cli" 7 | ) 8 | 9 | func main() { 10 | c := cli.New(os.Stdout, os.Stderr, version, commit, date) 11 | 12 | os.Exit(c.Run(os.Args)) 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Setup Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: 1.19 19 | - name: Run tests 20 | run: GO111MODULE=on make ci-test 21 | - name: Send test coverage to Codecov 22 | uses: codecov/codecov-action@v4 23 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "postUpdateOptions": [ 7 | "gomodTidy" 8 | ], 9 | "ignorePaths": [ 10 | "Dockerfile" 11 | ], 12 | "ignoreDeps": [ 13 | "go" 14 | ], 15 | "packageRules": [ 16 | { 17 | "matchUpdateTypes": [ 18 | "minor", 19 | "patch", 20 | "pin", 21 | "digest" 22 | ], 23 | "automerge": true 24 | } 25 | ], 26 | "constraints": { 27 | "go": "1.19" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go 3 | 4 | ### Go ### 5 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 6 | *.o 7 | *.a 8 | *.so 9 | 10 | # Folders 11 | _obj 12 | _test 13 | 14 | # Architecture specific extensions/prefixes 15 | *.[568vq] 16 | [568vq].out 17 | 18 | *.cgo1.go 19 | *.cgo2.c 20 | _cgo_defun.c 21 | _cgo_gotypes.go 22 | _cgo_export.* 23 | 24 | _testmain.go 25 | 26 | *.exe 27 | *.test 28 | *.prof 29 | 30 | # Output of the go coverage tool, specifically when used with LiteIDE 31 | *.out 32 | 33 | /s3sigurl 34 | /bin 35 | /glide 36 | /vendor 37 | /dist 38 | 39 | /coverage.txt 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "v*.*.*" 6 | 7 | jobs: 8 | goreleaser: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Setup Go 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: 1.19 17 | - name: Run GoReleaser 18 | uses: goreleaser/goreleaser-action@v5.1.0 19 | with: 20 | version: latest 21 | args: release --rm-dist 22 | env: 23 | # To upload Homebrew recipe to dtan4/homebrew-tools, we need a personal token 24 | # instead of Action's temporary token 25 | GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := s3url 2 | VERSION := $(shell git tag | head -n1) 3 | COMMIT := $(shell git rev-parse --short HEAD) 4 | 5 | SRCS := $(shell find . -type f -name '*.go') 6 | LDFLAGS := -ldflags="-s -w -X \"main.version=$(VERSION)\" -X \"main.commit=$(COMMIT)\" -extldflags \"-static\"" 7 | NOVENDOR := $(shell go list ./... | grep -v vendor) 8 | 9 | .DEFAULT_GOAL := bin/$(NAME) 10 | 11 | bin/$(NAME): $(SRCS) 12 | GO111MODULE=on go build $(LDFLAGS) -o bin/$(NAME) 13 | 14 | .PHONY: ci-test 15 | ci-test: 16 | GO111MODULE=on go test -coverpkg=./... -coverprofile=coverage.txt -v ./... 17 | 18 | .PHONY: clean 19 | clean: 20 | rm -rf bin/* 21 | rm -rf dist/* 22 | rm -rf vendor/* 23 | 24 | .PHONY: install 25 | install: 26 | GO111MODULE=on go install $(LDFLAGS) 27 | 28 | test: 29 | GO111MODULE=on go test -coverpkg=./... -v $(NOVENDOR) 30 | -------------------------------------------------------------------------------- /aws/aws.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/config" 8 | s3svc "github.com/aws/aws-sdk-go-v2/service/s3" 9 | "github.com/pkg/errors" 10 | 11 | "github.com/dtan4/s3url/aws/s3" 12 | ) 13 | 14 | var ( 15 | // S3 represents S3 API client 16 | S3 *s3.Client 17 | ) 18 | 19 | // Initialize creates S3 API client objects 20 | func Initialize(ctx context.Context, profile string) error { 21 | var ( 22 | cfg aws.Config 23 | err error 24 | ) 25 | 26 | if profile != "" { 27 | cfg, err = config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(profile)) 28 | if err != nil { 29 | return errors.Wrapf(err, "cannot load config using profile %q", profile) 30 | } 31 | } else { 32 | cfg, err = config.LoadDefaultConfig(ctx) 33 | if err != nil { 34 | return errors.Wrap(err, "cannot load default config") 35 | } 36 | } 37 | 38 | s3Client := s3svc.NewFromConfig(cfg) 39 | s3PresignClient := s3svc.NewPresignClient(s3Client) 40 | 41 | S3 = s3.New(s3Client, s3PresignClient) 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Daisuke Fujita 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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [v1.0.0](https://github.com/dtan4/s3url/releases/tag/v1.0.0) (2017-02-06) 2 | 3 | ## Fixed 4 | 5 | - Use aws-sdk-go v1.6 [#14](https://github.com/dtan4/s3url/pull/14) 6 | - Do not raise panic if invalid URL is given [#12](https://github.com/dtan4/s3url/pull/12) 7 | - Reborn `--upload` flag [#7](https://github.com/dtan4/s3url/pull/7) 8 | - Work with `-b` and `-k` correctly [#6](https://github.com/dtan4/s3url/pull/6/files) 9 | - Use spf13/pflag [#5](https://github.com/dtan4/s3url/pull/5) 10 | 11 | # [v0.3.1](https://github.com/dtan4/s3url/releases/tag/v0.3.1) (2016-10-21) 12 | 13 | ## Features 14 | 15 | - Add `-h`, `--help` flag to print command line usage 16 | - Add `-v`, `--version` flag to print binary version 17 | 18 | # [v0.3.0](https://github.com/dtan4/s3url/releases/tag/v0.3.0) (2016-09-26) 19 | 20 | ## Features 21 | 22 | - Accept virtual-hosted-style URL [#3](https://github.com/dtan4/s3url/pull/3) 23 | 24 | # [v0.2.0](https://github.com/dtan4/s3url/releases/tag/v0.2.0) (2016-09-26) 25 | 26 | ## Features 27 | 28 | - Upload file together [#2](https://github.com/dtan4/s3url/pull/2) 29 | 30 | # [v0.1.0](https://github.com/dtan4/s3url/releases/tag/v0.1.0) (2016-09-21) 31 | 32 | Initial release. 33 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | - make clean 6 | - go mod tidy 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - darwin 12 | - linux 13 | - windows 14 | goarch: 15 | - 386 16 | - amd64 17 | - arm 18 | - arm64 19 | archives: 20 | - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 21 | replacements: 22 | darwin: Darwin 23 | linux: Linux 24 | windows: Windows 25 | 386: i386 26 | amd64: x86_64 27 | format_overrides: 28 | - goos: windows 29 | format: zip 30 | release: 31 | prerelease: auto 32 | brews: 33 | - github: 34 | owner: dtan4 35 | name: homebrew-tools 36 | folder: Formula 37 | homepage: https://github.com/dtan4/s3url 38 | description: Generate S3 object pre-signed URL in one command 39 | skip_upload: auto # skip if the version is rc (e.g. v1.0.0-rc1) 40 | test: | 41 | system "#{bin}/s3url", "-v" 42 | checksum: 43 | name_template: 'checksums.txt' 44 | snapshot: 45 | name_template: "{{ .Tag }}-next" 46 | changelog: 47 | sort: asc 48 | filters: 49 | exclude: 50 | - '^docs:' 51 | - '^test:' 52 | - Merge pull request 53 | - Merge branch 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dtan4/s3url 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.30.5 7 | github.com/aws/aws-sdk-go-v2/config v1.27.35 8 | github.com/aws/aws-sdk-go-v2/service/s3 v1.62.0 9 | github.com/pkg/errors v0.9.1 10 | github.com/spf13/pflag v1.0.10 11 | ) 12 | 13 | require ( 14 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect 15 | github.com/aws/aws-sdk-go-v2/credentials v1.17.33 // indirect 16 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect 17 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect 18 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect 19 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect 20 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect 21 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect 22 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 // indirect 23 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect 24 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/sso v1.22.8 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.8 // indirect 27 | github.com/aws/aws-sdk-go-v2/service/sts v1.30.8 // indirect 28 | github.com/aws/smithy-go v1.20.4 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /aws/s3/s3.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" 10 | "github.com/aws/aws-sdk-go-v2/service/s3" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type s3Client interface { 16 | PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) 17 | } 18 | 19 | type s3PresignClient interface { 20 | PresignGetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) 21 | } 22 | 23 | // Client represents the wrapper of S3 API Client 24 | type Client struct { 25 | s3Client s3Client 26 | s3PresignClient s3PresignClient 27 | } 28 | 29 | // New creates new Client 30 | func New(s3Client s3Client, s3PresignClient s3PresignClient) *Client { 31 | return &Client{ 32 | s3Client: s3Client, 33 | s3PresignClient: s3PresignClient, 34 | } 35 | } 36 | 37 | // GetPresignedURL returns S3 object pre-signed URL 38 | func (c *Client) GetPresignedURL(ctx context.Context, bucket, key string, duration int64) (string, error) { 39 | req, err := c.s3PresignClient.PresignGetObject(ctx, &s3.GetObjectInput{ 40 | Bucket: aws.String(bucket), 41 | Key: aws.String(key), 42 | }, s3.WithPresignExpires(time.Duration(duration)*time.Minute)) 43 | if err != nil { 44 | return "", errors.Wrap(err, "cannot generate signed URL") 45 | } 46 | 47 | return req.URL, nil 48 | } 49 | 50 | // UploadToS3 uploads local file to the specified S3 location 51 | func (c *Client) UploadToS3(ctx context.Context, bucket, key string, reader io.ReadSeeker) error { 52 | _, err := c.s3Client.PutObject(ctx, &s3.PutObjectInput{ 53 | Bucket: aws.String(bucket), 54 | Key: aws.String(key), 55 | Body: reader, 56 | }) 57 | if err != nil { 58 | return errors.Wrap(err, "cannot upload file to S3") 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "net/url" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | const ( 12 | // DefaultDuration represents default valid duration in minutes 13 | DefaultDuration = 5 14 | ) 15 | 16 | var ( 17 | virtualHostRegexp = regexp.MustCompile(`^s3-[a-z0-9-]+\.amazonaws\.com$`) 18 | ) 19 | 20 | // Config represents s3url configurations 21 | type Config struct { 22 | Bucket string 23 | Debug bool 24 | Duration int64 25 | Key string 26 | Profile string 27 | Upload string 28 | Version bool 29 | } 30 | 31 | // ParseS3URL extracts bucket and key from S3 URL 32 | func (c *Config) ParseS3URL(s3URL string) error { 33 | u, err := url.Parse(s3URL) 34 | if err != nil { 35 | return errors.Wrapf(err, "invalid URL: %s", s3URL) 36 | } 37 | 38 | if u.Scheme == "s3" { // s3://bucket/key 39 | c.Bucket = u.Host 40 | c.Key = strings.Replace(u.Path, "/", "", 1) 41 | } else { 42 | if virtualHostRegexp.MatchString(u.Host) { // https://s3-ap-northeast-1.amazonaws.com/bucket/key 43 | ss := strings.SplitN(u.Path, "/", 3) 44 | if len(ss) < 3 { 45 | return errors.Errorf("invalid path: url: %q, path: %q", s3URL, u.Path) 46 | } 47 | 48 | c.Bucket = ss[1] 49 | c.Key = ss[2] 50 | } else { // https://bucket.s3-ap-northeast-1.amazonaws.com/key 51 | ss := strings.Split(u.Host, ".") 52 | if len(ss) < 4 { 53 | return errors.Errorf("invalid hostname: url: %q, hostname: %q", s3URL, u.Host) 54 | } 55 | 56 | c.Bucket = strings.Join(ss[0:len(ss)-3], ".") 57 | c.Key = u.Path[1:] 58 | } 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // Validate validates that current configurations are prepared sufficiently 65 | func (c *Config) Validate() error { 66 | if c.Bucket == "" { 67 | return errors.New("bucket name is required") 68 | } 69 | 70 | if c.Key == "" { 71 | return errors.New("object key is required") 72 | } 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.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: '26 13 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'go' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 68 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestParseS3URL(t *testing.T) { 9 | t.Parallel() 10 | 11 | testcases := []struct { 12 | url string 13 | bucket string 14 | key string 15 | }{ 16 | {"https://s3-ap-northeast-1.amazonaws.com/bucket/key.txt", "bucket", "key.txt"}, 17 | {"https://s3-ap-northeast-1.amazonaws.com/bucket/dir/key.txt", "bucket", "dir/key.txt"}, 18 | {"https://bucket.s3.amazonaws.com/key.txt", "bucket", "key.txt"}, 19 | {"https://bucket.s3.amazonaws.com/dir/key.txt", "bucket", "dir/key.txt"}, 20 | {"https://bucket.s3-ap-northeast-1.amazonaws.com/key.txt", "bucket", "key.txt"}, 21 | {"https://bucket.s3-ap-northeast-1.amazonaws.com/dir/key.txt", "bucket", "dir/key.txt"}, 22 | {"s3://bucket/key.txt", "bucket", "key.txt"}, 23 | {"s3://bucket/dir/key.txt", "bucket", "dir/key.txt"}, 24 | } 25 | 26 | for _, tc := range testcases { 27 | c := &Config{} 28 | 29 | if err := c.ParseS3URL(tc.url); err != nil { 30 | t.Errorf("Error should not be raised. url: %s, error: %v", tc.url, err) 31 | } 32 | 33 | if c.Bucket != tc.bucket { 34 | t.Errorf("Bucket does not match. expected: %s, actual: %s", tc.bucket, c.Bucket) 35 | } 36 | 37 | if c.Key != tc.key { 38 | t.Errorf("Key does not match. expected: %s, actual: %s", tc.key, c.Key) 39 | } 40 | } 41 | } 42 | 43 | func TestParseURL_invalid(t *testing.T) { 44 | t.Parallel() 45 | 46 | testcases := []struct { 47 | url string 48 | errMsg string 49 | }{ 50 | { 51 | url: "foobarbaz", 52 | errMsg: "invalid hostname: url: \"foobarbaz\", hostname: \"\"", 53 | }, 54 | { 55 | url: "https://s3-ap-northeast-1.amazonaws.com/bucket", 56 | errMsg: "invalid path: url: \"https://s3-ap-northeast-1.amazonaws.com/bucket\", path: \"/bucket\"", 57 | }, 58 | } 59 | 60 | for _, tc := range testcases { 61 | c := &Config{} 62 | 63 | err := c.ParseS3URL(tc.url) 64 | if err == nil { 65 | t.Error("Error should be raised.") 66 | } 67 | 68 | if err.Error() != tc.errMsg { 69 | t.Errorf("Error message does not match. expected: %s, actual: %s", tc.errMsg, err.Error()) 70 | } 71 | } 72 | } 73 | 74 | func TestValidate(t *testing.T) { 75 | t.Parallel() 76 | 77 | testcases := []struct { 78 | bucket string 79 | key string 80 | err error 81 | }{ 82 | { 83 | bucket: "foobar", 84 | key: "baz", 85 | err: nil, 86 | }, 87 | { 88 | bucket: "", 89 | key: "baz", 90 | err: fmt.Errorf("bucket name is required"), 91 | }, 92 | { 93 | bucket: "foobar", 94 | key: "", 95 | err: fmt.Errorf("object key is required"), 96 | }, 97 | { 98 | bucket: "", 99 | key: "", 100 | err: fmt.Errorf("bucket name is required"), 101 | }, 102 | } 103 | 104 | for _, tc := range testcases { 105 | c := &Config{ 106 | Bucket: tc.bucket, 107 | Key: tc.key, 108 | } 109 | 110 | err := c.Validate() 111 | 112 | if err == nil && tc.err != nil { 113 | t.Errorf("no error raised, want: %q", tc.err) 114 | continue 115 | } 116 | 117 | if err != nil && tc.err == nil { 118 | t.Errorf("unexpected error raised, got: %q", err) 119 | continue 120 | } 121 | 122 | if err != nil && tc.err != nil && (err.Error() != tc.err.Error()) { 123 | t.Errorf("invalid error, want: %q, got: %q", tc.err, err) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/pkg/errors" 10 | flag "github.com/spf13/pflag" 11 | 12 | "github.com/dtan4/s3url/aws" 13 | "github.com/dtan4/s3url/config" 14 | ) 15 | 16 | const ( 17 | exitCodeOK int = iota 18 | exitCodeError 19 | ) 20 | 21 | const usage = `Usage of %s: 22 | %s https://s3-region.amazonaws.com/BUCKET/KEY [-d DURATION] 23 | %s s3://BUCKET/KEY [-d DURATION] 24 | %s -b BUCKET -k KEY [-d DURATION] 25 | 26 | Options: 27 | ` 28 | 29 | // CLI represent CLI implementation 30 | type CLI struct { 31 | stdout io.Writer 32 | stderr io.Writer 33 | version string 34 | commit string 35 | buildAt string 36 | } 37 | 38 | // New returns new CLI object 39 | func New(stdout, stderr io.Writer, version, commit, buildAt string) *CLI { 40 | return &CLI{ 41 | stdout: stdout, 42 | stderr: stderr, 43 | version: version, 44 | commit: commit, 45 | buildAt: buildAt, 46 | } 47 | } 48 | 49 | // Run executes s3url command process 50 | func (cli *CLI) Run(args []string) int { 51 | f := flag.NewFlagSet("s3url", flag.ExitOnError) 52 | 53 | f.Usage = func() { 54 | fmt.Fprintf(cli.stderr, usage, args[0], args[0], args[0], args[0]) 55 | f.PrintDefaults() 56 | } 57 | 58 | c := config.Config{} 59 | 60 | f.StringVarP(&c.Bucket, "bucket", "b", "", "Bucket name") 61 | f.BoolVar(&c.Debug, "debug", false, "Enable debug output") 62 | f.Int64VarP(&c.Duration, "duration", "d", config.DefaultDuration, "Valid duration in minutes") 63 | f.StringVarP(&c.Key, "key", "k", "", "Object key") 64 | f.StringVar(&c.Profile, "profile", "", "AWS profile name") 65 | f.StringVar(&c.Upload, "upload", "", "File to upload") 66 | f.BoolVarP(&c.Version, "version", "v", false, "Print version") 67 | 68 | f.Parse(args[1:]) 69 | 70 | if c.Version { 71 | cli.printVersion() 72 | return exitCodeOK 73 | } 74 | 75 | var s3URL string 76 | 77 | for 0 < f.NArg() { 78 | s3URL = f.Args()[0] 79 | f.Parse(f.Args()[1:]) 80 | } 81 | 82 | if s3URL == "" && (c.Bucket == "" || c.Key == "") { 83 | f.Usage() 84 | return exitCodeError 85 | } 86 | 87 | if s3URL != "" { 88 | if err := c.ParseS3URL(s3URL); err != nil { 89 | cli.printError(err, c.Debug) 90 | return exitCodeError 91 | } 92 | } 93 | 94 | if err := c.Validate(); err != nil { 95 | cli.printError(err, c.Debug) 96 | return exitCodeError 97 | } 98 | 99 | ctx := context.Background() 100 | 101 | if err := aws.Initialize(ctx, c.Profile); err != nil { 102 | cli.printError(err, c.Debug) 103 | return exitCodeError 104 | } 105 | 106 | if c.Upload != "" { 107 | if err := cli.uploadFile(ctx, c.Bucket, c.Key, c.Upload); err != nil { 108 | cli.printError(err, c.Debug) 109 | return exitCodeError 110 | } 111 | 112 | fmt.Fprintln(cli.stderr, "uploaded: "+c.Upload) 113 | } 114 | 115 | signedURL, err := aws.S3.GetPresignedURL(ctx, c.Bucket, c.Key, c.Duration) 116 | if err != nil { 117 | cli.printError(err, c.Debug) 118 | return exitCodeError 119 | } 120 | 121 | fmt.Fprintln(cli.stdout, signedURL) 122 | 123 | return exitCodeOK 124 | } 125 | 126 | func (cli *CLI) uploadFile(ctx context.Context, bucket, key, filename string) error { 127 | f, err := os.Open(filename) 128 | if err != nil { 129 | return errors.Wrapf(err, "cannot open %q", filename) 130 | } 131 | defer f.Close() 132 | 133 | if err := aws.S3.UploadToS3(ctx, bucket, key, f); err != nil { 134 | return errors.Wrapf(err, "cannot uplaod %q to S3", filename) 135 | } 136 | 137 | return nil 138 | } 139 | 140 | func (cli *CLI) printError(err error, debug bool) { 141 | if debug { 142 | fmt.Fprintf(cli.stderr, "%+v\n", err) 143 | } else { 144 | fmt.Fprintln(cli.stderr, err) 145 | } 146 | } 147 | 148 | func (cli *CLI) printVersion() { 149 | fmt.Fprintf(cli.stdout, "s3url version: %s, commit: %s, build at: %s\n", cli.version, cli.commit, cli.buildAt) 150 | } 151 | -------------------------------------------------------------------------------- /aws/s3/s3_test.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io/ioutil" 8 | "net/url" 9 | "os" 10 | "path/filepath" 11 | "testing" 12 | 13 | "github.com/aws/aws-sdk-go-v2/aws" 14 | v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" 15 | "github.com/aws/aws-sdk-go-v2/service/s3" 16 | ) 17 | 18 | type fakeS3Client struct { 19 | putObject func(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) 20 | } 21 | 22 | func (c *fakeS3Client) PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) { 23 | return c.putObject(ctx, params, optFns...) 24 | } 25 | 26 | type fakeS3PresignClient struct { 27 | presignGetObject func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) 28 | } 29 | 30 | func (c *fakeS3PresignClient) PresignGetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) { 31 | return c.presignGetObject(ctx, params, optFns...) 32 | } 33 | 34 | func TestNew(t *testing.T) { 35 | t.Parallel() 36 | 37 | s3Client := &fakeS3Client{} 38 | s3PresignClient := &fakeS3PresignClient{} 39 | client := New(s3Client, s3PresignClient) 40 | 41 | if client.s3Client != s3Client { 42 | t.Error("s3Client does not match") 43 | } 44 | 45 | if client.s3PresignClient != s3PresignClient { 46 | t.Error("s3Client does not match") 47 | } 48 | } 49 | 50 | func TestGetPresignedURL(t *testing.T) { 51 | t.Parallel() 52 | 53 | bucket := "bucket" 54 | key := "key" 55 | duration := int64(100) 56 | 57 | u := url.URL{ 58 | Scheme: "http", 59 | Host: "bucket.s3-ap-northeast-1.amazonaws.com", 60 | Path: "/key", 61 | } 62 | want := u.String() 63 | 64 | client := &Client{ 65 | s3PresignClient: &fakeS3PresignClient{ 66 | presignGetObject: func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) { 67 | u := &url.URL{ 68 | Scheme: "http", 69 | Host: fmt.Sprintf("%s.s3-ap-northeast-1.amazonaws.com", aws.ToString(params.Bucket)), 70 | Path: fmt.Sprintf("/%s", aws.ToString(params.Key)), 71 | } 72 | 73 | return &v4.PresignedHTTPRequest{ 74 | URL: u.String(), 75 | }, nil 76 | }, 77 | }, 78 | } 79 | 80 | got, err := client.GetPresignedURL(context.Background(), bucket, key, duration) 81 | if err != nil { 82 | t.Fatalf("Error should not be raised. error:%s", err) 83 | } 84 | 85 | if got != want { 86 | t.Errorf("Invalid signed URL. want: %s, got: %s", want, got) 87 | } 88 | } 89 | 90 | func TestUploadToS3(t *testing.T) { 91 | t.Parallel() 92 | 93 | bucket := "bucket" 94 | key := "key" 95 | testfile := filepath.Join("..", "..", "_testdata", "test.txt") 96 | 97 | f, err := os.Open(testfile) 98 | if err != nil { 99 | t.Fatalf("cannot open testdata %q: %s", testfile, err) 100 | } 101 | 102 | client := &Client{ 103 | s3Client: &fakeS3Client{ 104 | putObject: func(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) { 105 | return &s3.PutObjectOutput{}, nil 106 | }, 107 | }, 108 | } 109 | 110 | if err := client.UploadToS3(context.Background(), bucket, key, f); err != nil { 111 | t.Fatalf("Error should not be raised. error: %s", err) 112 | } 113 | } 114 | 115 | func BenchmarkReadFileEntirely(b *testing.B) { 116 | testfile := filepath.Join("..", "..", "_testdata", "test.txt") 117 | b.ResetTimer() 118 | 119 | for i := 0; i < b.N; i++ { 120 | body, _ := ioutil.ReadFile(testfile) 121 | r := bytes.NewReader(body) 122 | _, _ = ioutil.ReadAll(r) 123 | } 124 | } 125 | 126 | func BenchmarkReadFileStream(b *testing.B) { 127 | testfile := filepath.Join("..", "..", "_testdata", "test.txt") 128 | b.ResetTimer() 129 | 130 | for i := 0; i < b.N; i++ { 131 | f, _ := os.Open(testfile) 132 | _, _ = ioutil.ReadAll(f) 133 | f.Close() 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g= 2 | github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= 3 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU= 4 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw= 5 | github.com/aws/aws-sdk-go-v2/config v1.27.35 h1:jeFgiWYNV0vrgdZqB4kZBjYNdy0IKkwrAjr2fwpHIig= 6 | github.com/aws/aws-sdk-go-v2/config v1.27.35/go.mod h1:qnpEvTq8ZfjrCqmJGRfWZuF+lGZ/vG8LK2K0L/TY1gQ= 7 | github.com/aws/aws-sdk-go-v2/credentials v1.17.33 h1:lBHAQQznENv0gLHAZ73ONiTSkCtr8q3pSqWrpbBBZz0= 8 | github.com/aws/aws-sdk-go-v2/credentials v1.17.33/go.mod h1:MBuqCUOT3ChfLuxNDGyra67eskx7ge9e3YKYBce7wpI= 9 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74= 10 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU= 11 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ= 12 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE= 13 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc= 14 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU= 15 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= 16 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= 17 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 h1:Roo69qTpfu8OlJ2Tb7pAYVuF0CpuUMB0IYWwYP/4DZM= 18 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17/go.mod h1:NcWPxQzGM1USQggaTVwz6VpqMZPX1CvDJLDh6jnOCa4= 19 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= 20 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= 21 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 h1:FLMkfEiRjhgeDTCjjLoc3URo/TBkgeQbocA78lfkzSI= 22 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19/go.mod h1:Vx+GucNSsdhaxs3aZIKfSUjKVGsxN25nX2SRcdhuw08= 23 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4= 24 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE= 25 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 h1:u+EfGmksnJc/x5tq3A+OD7LrMbSSR/5TrKLvkdy/fhY= 26 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17/go.mod h1:VaMx6302JHax2vHJWgRo+5n9zvbacs3bLU/23DNQrTY= 27 | github.com/aws/aws-sdk-go-v2/service/s3 v1.62.0 h1:rd/aA3iDq1q7YsL5sc4dEwChutH7OZF9Ihfst6pXQzI= 28 | github.com/aws/aws-sdk-go-v2/service/s3 v1.62.0/go.mod h1:5FmD/Dqq57gP+XwaUnd5WFPipAuzrf0HmupX27Gvjvc= 29 | github.com/aws/aws-sdk-go-v2/service/sso v1.22.8 h1:JRwuL+S1Qe1owZQoxblV7ORgRf2o0SrtzDVIbaVCdQ0= 30 | github.com/aws/aws-sdk-go-v2/service/sso v1.22.8/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ= 31 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.8 h1:+HpGETD9463PFSj7lX5+eq7aLDs85QUIA+NBkeAsscA= 32 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.8/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA= 33 | github.com/aws/aws-sdk-go-v2/service/sts v1.30.8 h1:bAi+4p5EKnni+jrfcAhb7iHFQ24bthOAV9t0taf3DCE= 34 | github.com/aws/aws-sdk-go-v2/service/sts v1.30.8/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o= 35 | github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= 36 | github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 37 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 38 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 39 | github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 40 | github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # s3url(1) 2 | 3 | [![GitHub Actions](https://github.com/dtan4/s3url/workflows/Test/badge.svg)](https://github.com/dtan4/s3url/actions?query=workflow%3ATest+branch%3Amaster) 4 | [![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=dtan4/s3url)](https://dependabot.com) 5 | [![codecov](https://codecov.io/gh/dtan4/s3url/branch/master/graph/badge.svg)](https://codecov.io/gh/dtan4/s3url) 6 | [![GitHub release](https://img.shields.io/github/release/dtan4/s3url.svg)](https://github.com/dtan4/s3url/releases) 7 | 8 | Generate [S3 object pre-signed URL](http://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html) in one command 9 | 10 | ```bash 11 | $ s3url s3://my-bucket/foo.key 12 | https://my-bucket.s3-ap-northeast-1.amazonaws.com/foo.key?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA***************************%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20160923T010227Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=**************************************************************** 13 | ``` 14 | 15 | ## Contents 16 | 17 | * [Installation](#installation) 18 | + [Using Homebrew (OS X only)](#using-homebrew-os-x-only) 19 | + [Precompiled binary](#precompiled-binary) 20 | + [From source](#from-source) 21 | * [Usage](#usage) 22 | + [Upload file together](#upload-file-together) 23 | + [Options](#options) 24 | * [Development](#development) 25 | * [License](#license) 26 | 27 | ## Installation 28 | 29 | ### Using Homebrew (OS X only) 30 | 31 | Formula is available at [dtan4/homebrew-tools](https://github.com/dtan4/homebrew-tools). 32 | 33 | ```bash 34 | $ brew tap dtan4/tools 35 | $ brew install s3url 36 | ``` 37 | 38 | ### Precompiled binary 39 | 40 | Precompiled binaries for Windows, OS X, Linux are available at [Releases](https://github.com/dtan4/s3url/releases). 41 | 42 | ### From source 43 | 44 | ```bash 45 | $ go get -d github.com/dtan4/s3url 46 | $ cd $GOPATH/src/github.com/dtan4/s3url 47 | $ make install 48 | ``` 49 | 50 | ## Usage 51 | 52 | You need to set AWS credentials beforehand, or you can also use [named profile](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-multiple-profiles) written in `~/.aws/credentials`. 53 | 54 | ```bash 55 | export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXX 56 | export AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 57 | # or configure them in ~/.aws/credentials 58 | 59 | export AWS_REGION=xx-yyyy-0 60 | ``` 61 | 62 | Just type the command below and get Pre-signed URL on the screen. 63 | 64 | ```bash 65 | # https:// URL (both virtual-hosted-style and path-style) 66 | $ s3url https://BUCKET.s3-region.amazonaws.com/KEY [-d DURATION] [--profile PROFILE] [--upload UPLOAD] 67 | $ s3url https://s3-region.amazonaws.com/BUCKET/KEY [-d DURATION] [--profile PROFILE] [--upload UPLOAD] 68 | 69 | # s3:// URL 70 | $ s3url s3://BUCKET/KEY [-d DURATION] [--profile PROFILE] [--upload UPLOAD] 71 | 72 | # Using options 73 | $ s3url -b BUCKET -k KEY [-d DURATION] [--profile PROFILE] [--upload UPLOAD] 74 | ``` 75 | 76 | ### Upload file together 77 | 78 | If target object does not exist in the bucket yet, you can upload file with `--upload` flag before getting Pre-signed URL. Following example shows that uploading `foo.key` to `s3://my-bucket/foo.key` and getting Pre-signed URL of `s3://my-bucket/foo.key` will be executed in series. 79 | 80 | ```bash 81 | $ s3url s3://my-bucket/foo.key --upload foo.key 82 | uploaded: /path/to/foo.key 83 | https://my-bucket.s3-ap-northeast-1.amazonaws.com/foo.key?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA***************************%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20160923T010227Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=**************************************************************** 84 | ``` 85 | 86 | ### Options 87 | 88 | |Option|Description|Required|Default| 89 | |---------|-----------|-------|-------| 90 | |`-b`, `-bucket=BUCKET`|Bucket name|Required (if no URL is specified)|| 91 | |`-k`, `-key=KEY`|Object key|Required (if no URL is specified)|| 92 | |`-d`, `-duration=DURATION`|Valid duration in minutes||5| 93 | |`--profile=PROFILE`|AWS profile name||| 94 | |`--upload=UPLOAD`|File to upload||| 95 | |`-h`, `-help`|Print command line usage||| 96 | |`-v`, `-version`|Print version||| 97 | 98 | ## Development 99 | 100 | Retrieve this repository and build using `make`. 101 | 102 | ```bash 103 | $ go get -d github.com/dtan4/s3url 104 | $ cd $GOPATH/src/github.com/dtan4/s3url 105 | $ make 106 | ``` 107 | 108 | ## License 109 | 110 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) 111 | --------------------------------------------------------------------------------