1package publisher
2
3import (
4	"context"
5	"mime"
6	"net/http"
7	"os"
8	"path"
9	"strings"
10
11	"github.com/alecthomas/kingpin"
12	"github.com/aws/aws-sdk-go/aws"
13	"github.com/aws/aws-sdk-go/aws/credentials"
14	"github.com/aws/aws-sdk-go/aws/session"
15	"github.com/aws/aws-sdk-go/service/s3"
16	"github.com/aws/aws-sdk-go/service/s3/s3manager"
17	"github.com/develar/app-builder/pkg/util"
18	"github.com/develar/errors"
19)
20
21type ObjectOptions struct {
22	file *string
23
24	endpoint *string
25	region   *string
26	bucket   *string
27	key      *string
28
29	acl          *string
30	storageClass *string
31	encryption   *string
32
33	accessKey *string
34	secretKey *string
35}
36
37func ConfigurePublishToS3Command(app *kingpin.Application) {
38	command := app.Command("publish-s3", "Publish to S3")
39	options := ObjectOptions{
40		file: command.Flag("file", "").Required().String(),
41
42		region:   command.Flag("region", "").String(),
43		bucket:   command.Flag("bucket", "").Required().String(),
44		key:      command.Flag("key", "").Required().String(),
45		endpoint: command.Flag("endpoint", "").String(),
46
47		acl:          command.Flag("acl", "").String(),
48		storageClass: command.Flag("storageClass", "").String(),
49		encryption:   command.Flag("encryption", "").String(),
50
51		accessKey: command.Flag("accessKey", "").String(),
52		secretKey: command.Flag("secretKey", "").String(),
53	}
54
55	command.Action(func(context *kingpin.ParseContext) error {
56		err := upload(&options)
57		if err != nil {
58			return err
59		}
60		return nil
61	})
62
63	configureResolveBucketLocationCommand(app)
64}
65
66func configureResolveBucketLocationCommand(app *kingpin.Application) {
67	command := app.Command("get-bucket-location", "")
68	bucket := command.Flag("bucket", "").Required().String()
69	command.Action(func(context *kingpin.ParseContext) error {
70		requestContext, _ := util.CreateContext()
71		result, err := getBucketRegion(aws.NewConfig(), bucket, requestContext, createHttpClient())
72		if err != nil {
73			return err
74		}
75
76		_, err = os.Stdout.WriteString(result)
77		if err != nil {
78			return errors.WithStack(err)
79		}
80		return nil
81	})
82}
83
84func getBucketRegion(awsConfig *aws.Config, bucket *string, context context.Context, httpClient *http.Client) (string, error) {
85	awsSession, err := session.NewSession(awsConfig, &aws.Config{
86		// any region required
87		Region:     aws.String("us-east-1"),
88		HTTPClient: httpClient,
89	})
90	if err != nil {
91		return "", errors.WithStack(err)
92	}
93
94	client := s3.New(awsSession)
95	result, err := client.GetBucketLocationWithContext(context, &s3.GetBucketLocationInput{
96		Bucket: bucket,
97	})
98	if err != nil {
99		return "", errors.WithStack(err)
100	}
101	if result == nil || result.LocationConstraint == nil || len(*result.LocationConstraint) == 0 {
102		return "us-east-1", nil
103	}
104	return *result.LocationConstraint, nil
105}
106
107func upload(options *ObjectOptions) error {
108	publishContext, _ := util.CreateContext()
109
110	httpClient := createHttpClient()
111
112	awsConfig := &aws.Config{
113		HTTPClient: httpClient,
114	}
115	if *options.endpoint != "" {
116		awsConfig.Endpoint = options.endpoint
117		awsConfig.S3ForcePathStyle = aws.Bool(true)
118	}
119
120	//awsConfig.WithLogLevel(aws.LogDebugWithHTTPBody)
121
122	if *options.accessKey != "" {
123		awsConfig.Credentials = credentials.NewStaticCredentials(*options.accessKey, *options.secretKey, "")
124	}
125
126	switch {
127	case *options.region != "":
128		awsConfig.Region = options.region
129	case *options.endpoint != "":
130		awsConfig.Region = aws.String("us-east-1")
131	default:
132		// AWS SDK for Go requires region
133		region, err := getBucketRegion(awsConfig, options.bucket, publishContext, httpClient)
134		if err != nil {
135			return errors.WithStack(err)
136		}
137		awsConfig.Region = &region
138	}
139
140	awsSession, err := session.NewSession(awsConfig)
141	if err != nil {
142		return errors.WithStack(err)
143	}
144
145	uploader := s3manager.NewUploader(awsSession)
146
147	file, err := os.Open(*options.file)
148	defer util.Close(file)
149	if err != nil {
150		return errors.WithStack(err)
151	}
152
153	uploadInput := s3manager.UploadInput{
154		Bucket:      options.bucket,
155		Key:         options.key,
156		ContentType: aws.String(getMimeType(*options.key)),
157		Body:        file,
158	}
159	if *options.acl != "" {
160		uploadInput.ACL = options.acl
161	}
162	if *options.storageClass != "" {
163		uploadInput.StorageClass = options.storageClass
164	}
165	if *options.encryption != "" {
166		uploadInput.ServerSideEncryption = options.encryption
167	}
168
169	_, err = uploader.UploadWithContext(publishContext, &uploadInput)
170	if err != nil {
171		return errors.WithStack(err)
172	}
173
174	return nil
175}
176
177func createHttpClient() *http.Client {
178	return &http.Client{
179		Transport: &http.Transport{
180			Proxy: util.ProxyFromEnvironmentAndNpm,
181		},
182	}
183}
184
185func getMimeType(key string) string {
186	if strings.HasSuffix(key, ".AppImage") {
187		return "application/vnd.appimage"
188	}
189	if strings.HasSuffix(key, ".exe") {
190		return "application/octet-stream"
191	}
192	if strings.HasSuffix(key, ".zip") {
193		return "application/zip"
194	}
195	if strings.HasSuffix(key, ".blockmap") {
196		return "application/gzip"
197	}
198	if strings.HasSuffix(key, ".snap") {
199		return "application/vnd.snap"
200	}
201	if strings.HasSuffix(key, ".dmg") {
202		//noinspection SpellCheckingInspection
203		return "application/x-apple-diskimage"
204	}
205
206	ext := path.Ext(key)
207	if ext != "" {
208		mimeType := mime.TypeByExtension(ext)
209		if mimeType != "" {
210			return mimeType
211		}
212	}
213	return "application/octet-stream"
214}
215