├── .gitignore ├── packages └── presign │ └── url │ ├── .env │ ├── go.mod │ ├── go.sum │ └── url.go ├── project.yml ├── .do └── deployment.template.yaml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /packages/presign/url/.env: -------------------------------------------------------------------------------- 1 | SPACES_KEY="" 2 | SPACES_SECRET="" 3 | BUCKET="" 4 | REGION="" -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | SPACES_KEY: ${SPACES_KEY} 3 | SPACES_SECRET: ${SPACES_SECRET} 4 | BUCKET: ${BUCKET} 5 | REGION: ${REGION} -------------------------------------------------------------------------------- /packages/presign/url/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/digitalocean/sample-functions-golang-url/packages/presign/url 2 | 3 | go 1.18 4 | 5 | require github.com/aws/aws-sdk-go v1.44.51 6 | 7 | require github.com/jmespath/go-jmespath v0.4.0 // indirect 8 | -------------------------------------------------------------------------------- /.do/deployment.template.yaml: -------------------------------------------------------------------------------- 1 | spec: 2 | alerts: 3 | - rules: DEPLOYMENT_FAILED 4 | - rules: DOMAIN_FAILED 5 | functions: 6 | - github: 7 | branch: main 8 | deploy_on_push: true 9 | repo: digitalocean/sample-functions-golang-presigned-url 10 | name: url-api 11 | routes: 12 | - path: / 13 | source_dir: / 14 | name: url -------------------------------------------------------------------------------- /packages/presign/url/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.44.51 h1:jO9hoLynZOrMM4dj0KjeKIK+c6PA+HQbKoHOkAEye2Y= 2 | github.com/aws/aws-sdk-go v1.44.51/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 6 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 7 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 8 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 9 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= 14 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 15 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 17 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 18 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 19 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 20 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 23 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample Function: Go "Presigned URL" 2 | 3 | ## Introduction 4 | 5 | This repository contains a sample presigned URL function written in Go. You are able to choose to get a presigned URL to upload a file to a DigitalOcean Space or to download a file from a DigitalOcean Space. You can deploy it on DigitalOcean's App Platform as a Serverless Function component. 6 | Documentation is available at https://docs.digitalocean.com/products/functions. 7 | 8 | ### Requirements 9 | 10 | * You need a DigitalOcean account. If you don't already have one, you can sign up at [https://cloud.digitalocean.com/registrations/new](https://cloud.digitalocean.com/registrations/new). 11 | * You need a DigitalOcean Space. If you don't have one, you can create one at https://www.digitalocean.com/products/spaces. 12 | * You need to add your `SPACES_KEY`, `SPACES_SECRET`, `BUCKET`, and `REGION` to the `.env` file to connect to Spaces API as well as your bucket. 13 | * To deploy from the command line, you will need the [DigitalOcean `doctl` CLI](https://github.com/digitalocean/doctl/releases). 14 | 15 | ## Deploying the Function 16 | 17 | ```bash 18 | # clone this repo 19 | git clone https://github.com/digitalocean/sample-functions-golang-presigned-url 20 | ``` 21 | 22 | ``` 23 | # deploy the project, using a remote build so that compiled executable matched runtime environment 24 | > doctl serverless deploy sample-functions-golang-presigned-url --remote-build 25 | Deploying 'sample-functions-golang-presigned-url' 26 | to namespace 'fn-...' 27 | on host 'https://faas-...' 28 | Submitted action 'url' for remote building and deployment in runtime go:default (id: ...) 29 | 30 | Deployed functions ('doctl sls fn get --url' for URL): 31 | - presign/url 32 | ``` 33 | 34 | ## Using the Function 35 | 36 | ```bash 37 | doctl serverless functions invoke presign/url -p filename:new-file.txt type:GET 38 | ``` 39 | ```json 40 | { 41 | "body": "{presigned url}" 42 | } 43 | ``` 44 | 45 | ### To get a presigned url using curl: 46 | ``` 47 | curl -X PUT -H 'Content-Type: application/json' {your-DO-app-url} -d '{"filename":"{filename}", "type":"GET or PUT"}' 48 | ``` 49 | 50 | ### To Upload or Download a file using curl: 51 | ``` 52 | curl -X PUT -d 'The contents of the file.' "{url}" 53 | ``` 54 | 55 | ### Learn More 56 | 57 | You can learn more about Functions and App Platform integration in [the official App Platform Documentation](https://www.digitalocean.com/docs/app-platform/). 58 | -------------------------------------------------------------------------------- /packages/presign/url/url.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/aws/credentials" 12 | "github.com/aws/aws-sdk-go/aws/session" 13 | "github.com/aws/aws-sdk-go/service/s3" 14 | ) 15 | 16 | // Request takes in the user's input for the filename they want and if the type is a GET or PUT. 17 | type Request struct { 18 | // Filename is the name of the file that will be uploaded or downloaded. 19 | Filename string `json:"filename"` 20 | // Type is a presigned request type to "GET" or "PUT" an object. 21 | Type string `json:"type"` 22 | // Duration is the duration in which the presigned url will last. 23 | Duration string `json:"duration"` 24 | } 25 | 26 | // Response returns back the http code, type of data, and the presigned url to the user. 27 | type Response struct { 28 | // StatusCode is the http code that will be returned back to the user. 29 | StatusCode int `json:"statusCode,omitempty"` 30 | // Headers is the information about the type of data being returned back. 31 | Headers map[string]string `json:"headers,omitempty"` 32 | // Body will contain the presigned url to upload or download files. 33 | Body string `json:"body,omitempty"` 34 | } 35 | 36 | var ( 37 | key, secret, bucket, region string 38 | // ErrNoFilename will return an error if no filename is provided by the user. 39 | ErrNoFilename = errors.New("no filename provided") 40 | // ErrNoFilename will return an error if no request type is provided by the user. 41 | ErrNoRequest = errors.New("no request type provided") 42 | // ErrNoDuration will return an error if no duration is provided by the user. 43 | ErrNoDuration = errors.New("no duration provided") 44 | // ErrNegativeDuration will return an error if a negative duration is provided by the user. 45 | ErrNegativeDuration = errors.New("negative duration provided") 46 | ) 47 | 48 | const ( 49 | // RequestTypeGet is the presigned request type to download a file. 50 | RequestTypeGet = "GET" 51 | // RequestTypePUT is the presigned request type to upload a file. 52 | RequestTypePut = "PUT" 53 | ) 54 | 55 | func init() { 56 | key = os.Getenv("SPACES_KEY") 57 | if key == "" { 58 | panic("no key provided") 59 | } 60 | secret = os.Getenv("SPACES_SECRET") 61 | if secret == "" { 62 | panic("no secret provided") 63 | } 64 | bucket = os.Getenv("BUCKET") 65 | if bucket == "" { 66 | panic("no bucket provided") 67 | } 68 | region = os.Getenv("REGION") 69 | if region == "" { 70 | panic("no region provided") 71 | } 72 | } 73 | 74 | // Main configures a client using the key, secret, and region provided and returns a presigned 75 | // url to upload a file or download a file from a DigitalOcean Space. 76 | func Main(in Request) (*Response, error) { 77 | var url string 78 | if in.Filename == "" { 79 | return &Response{StatusCode: http.StatusBadRequest}, ErrNoFilename 80 | } 81 | 82 | duration, err := time.ParseDuration(in.Duration) 83 | if err != nil { 84 | return &Response{StatusCode: http.StatusBadRequest}, ErrNoDuration 85 | } 86 | if duration < 0 { 87 | return &Response{StatusCode: http.StatusBadRequest}, ErrNegativeDuration 88 | } 89 | 90 | config := &aws.Config{ 91 | Credentials: credentials.NewStaticCredentials(key, secret, ""), 92 | Endpoint: aws.String(fmt.Sprintf("%s.digitaloceanspaces.com:443", region)), 93 | Region: aws.String(region), 94 | } 95 | sess := session.New(config) 96 | 97 | switch in.Type { 98 | case RequestTypeGet: 99 | url, err = downloadURL(sess, bucket, in.Filename, duration) 100 | if err != nil { 101 | return &Response{StatusCode: http.StatusBadRequest}, err 102 | } 103 | case RequestTypePut: 104 | url, err = uploadURL(sess, bucket, in.Filename, duration) 105 | if err != nil { 106 | return &Response{StatusCode: http.StatusBadRequest}, err 107 | } 108 | default: 109 | return &Response{StatusCode: http.StatusBadRequest}, ErrNoRequest 110 | } 111 | 112 | return &Response{ 113 | StatusCode: http.StatusOK, 114 | Body: url, 115 | }, nil 116 | } 117 | 118 | func uploadURL(sess *session.Session, bucket string, filename string, duration time.Duration) (string, error) { 119 | client := s3.New(sess) 120 | req, _ := client.PutObjectRequest(&s3.PutObjectInput{ 121 | Bucket: aws.String(bucket), 122 | Key: aws.String(filename), 123 | }) 124 | url, err := req.Presign(duration) 125 | if err != nil { 126 | return "", err 127 | } 128 | return url, nil 129 | } 130 | 131 | func downloadURL(sess *session.Session, bucket string, filename string, duration time.Duration) (string, error) { 132 | client := s3.New(sess) 133 | req, _ := client.GetObjectRequest(&s3.GetObjectInput{ 134 | Bucket: aws.String(bucket), 135 | Key: aws.String(filename), 136 | }) 137 | url, err := req.Presign(duration) 138 | if err != nil { 139 | return "", err 140 | } 141 | return url, nil 142 | } 143 | --------------------------------------------------------------------------------