2020-03-06 16:39:33 +00:00
|
|
|
package objectStorage
|
|
|
|
|
|
|
|
import (
|
2020-09-20 05:53:01 +00:00
|
|
|
"bytes"
|
|
|
|
"encoding/xml"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
errors "git.sequentialread.com/forest/pkg-errors"
|
|
|
|
"git.sequentialread.com/forest/rootsystem/configuration"
|
|
|
|
|
|
|
|
"github.com/minio/minio-go/pkg/signer"
|
2020-03-06 16:39:33 +00:00
|
|
|
)
|
|
|
|
|
2020-06-10 00:31:14 +00:00
|
|
|
type S3Compatible struct {
|
2020-09-20 05:53:01 +00:00
|
|
|
KeyId string
|
|
|
|
SecretKey string
|
|
|
|
HTTPClient http.Client
|
|
|
|
Domain string
|
|
|
|
Region string
|
2020-03-06 16:39:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type s3Error struct {
|
2020-09-20 05:53:01 +00:00
|
|
|
XMLName xml.Name `xml:"Error"`
|
|
|
|
Code string
|
2020-03-06 16:39:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type listBucketResult struct {
|
2020-09-20 05:53:01 +00:00
|
|
|
XMLName xml.Name `xml:"ListBucketResult"`
|
|
|
|
IsTruncated bool
|
|
|
|
Files []string `xml:"Contents>Key"`
|
|
|
|
FilesLastModified []string `xml:"Contents>LastModified"`
|
|
|
|
Directories []string `xml:"CommonPrefixes>Prefix"`
|
2020-03-06 16:39:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const S3_LAST_MODIFIED_HEADER_FORMAT = "Mon, 2 Jan 2006 15:04:05 MST"
|
|
|
|
|
|
|
|
// We can't use Digital Ocean Spaces because it comes with a monthly service charge.
|
|
|
|
// Press F in the chat for digital ocean
|
2020-06-10 00:31:14 +00:00
|
|
|
// func NewDigitalOceanSpacesClient(cred credential, region, spaceName string) S3Compatible {
|
2020-09-18 21:21:39 +00:00
|
|
|
// return S3Compatible{
|
|
|
|
// Credential: cred,
|
|
|
|
// HTTPClient: http.Client{
|
|
|
|
// // TODO test this timeout for both connect timeout and read timeout.
|
|
|
|
// // Do we need to include a custom Transport as well for a Dial Timeout?
|
|
|
|
// Timeout: time.Second * 60,
|
|
|
|
// },
|
|
|
|
// Domain: fmt.Sprintf("https://%s.%s.digitaloceanspaces.com", spaceName, region),
|
|
|
|
// Region: region,
|
|
|
|
// }
|
2020-03-06 16:39:33 +00:00
|
|
|
// }
|
|
|
|
|
2020-06-10 00:31:14 +00:00
|
|
|
func NewS3Client(keyId, secretKey, region, bucketName string) S3Compatible {
|
2020-03-06 16:39:33 +00:00
|
|
|
|
2020-09-20 05:53:01 +00:00
|
|
|
subdomain := fmt.Sprintf("s3-%s", region)
|
|
|
|
if region == "us-east-1" {
|
|
|
|
subdomain = "s3"
|
|
|
|
}
|
|
|
|
|
|
|
|
return S3Compatible{
|
|
|
|
KeyId: keyId,
|
|
|
|
SecretKey: secretKey,
|
|
|
|
HTTPClient: http.Client{
|
|
|
|
// TODO test this timeout for both connect timeout and read timeout.
|
|
|
|
// Do we need to include a custom Transport as well for a Dial Timeout?
|
|
|
|
Timeout: time.Second * 60,
|
|
|
|
},
|
|
|
|
Domain: fmt.Sprintf("https://%s.%s.amazonaws.com", bucketName, subdomain),
|
|
|
|
Region: region,
|
|
|
|
}
|
2020-03-06 16:39:33 +00:00
|
|
|
}
|
|
|
|
|
2020-06-10 00:31:14 +00:00
|
|
|
func (self S3Compatible) CreateIfNotExists() error {
|
2020-09-20 05:53:01 +00:00
|
|
|
return errors.New("not implemented")
|
|
|
|
// response, err := self.doReq("GET", "/?delimiter=/", nil)
|
|
|
|
// if err != nil {
|
|
|
|
// return err
|
|
|
|
// }
|
|
|
|
// bytes, err := ioutil.ReadAll(response.Body)
|
|
|
|
// if err != nil {
|
|
|
|
// return errors.Wrap(err, "could not read response body")
|
|
|
|
// }
|
|
|
|
// if strings.Contains(string(bytes), "ListBucketResult") {
|
|
|
|
// return nil
|
|
|
|
// }
|
|
|
|
// return fmt.Errorf("recieved weird response from %s: %s", self.Domain, string(bytes))
|
2020-03-06 16:39:33 +00:00
|
|
|
}
|
|
|
|
|
2020-06-10 00:31:14 +00:00
|
|
|
func (self S3Compatible) CreateAccessKeyIfNotExists(key ObjectStorageKey) ([]configuration.Credential, error) {
|
2020-09-20 05:53:01 +00:00
|
|
|
return nil, errors.New("not implemented")
|
2020-05-27 22:57:42 +00:00
|
|
|
}
|
|
|
|
|
2020-06-10 00:31:14 +00:00
|
|
|
func (self S3Compatible) List(key string) ([]ObjectStorageFileInfo, error) {
|
2020-09-20 05:53:01 +00:00
|
|
|
if !strings.HasPrefix(key, "/") {
|
|
|
|
key = fmt.Sprintf("/%s", key)
|
|
|
|
}
|
|
|
|
response, err := self.doReq("GET", fmt.Sprintf("%s?delimiter=/", key), nil)
|
|
|
|
if err != nil {
|
|
|
|
return []ObjectStorageFileInfo{}, err
|
|
|
|
}
|
|
|
|
bytes, err := ioutil.ReadAll(response.Body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "could not read response body")
|
|
|
|
}
|
|
|
|
|
|
|
|
result := listBucketResult{}
|
|
|
|
err = xml.Unmarshal(bytes, &result)
|
|
|
|
if err != nil {
|
|
|
|
s3ErrorResult := s3Error{}
|
|
|
|
s3ErrorUnmarshalError := xml.Unmarshal(bytes, &s3ErrorResult)
|
|
|
|
if s3ErrorUnmarshalError != nil {
|
|
|
|
return []ObjectStorageFileInfo{}, err
|
|
|
|
}
|
|
|
|
if s3ErrorResult.Code == "NoSuchKey" {
|
|
|
|
return []ObjectStorageFileInfo{}, nil
|
|
|
|
}
|
|
|
|
return []ObjectStorageFileInfo{}, fmt.Errorf("recieved weird List() response from %s: %s", self.Domain, string(bytes))
|
|
|
|
}
|
|
|
|
|
|
|
|
toReturn := make([]ObjectStorageFileInfo, len(result.Directories)+len(result.Files))
|
|
|
|
for i, directory := range result.Directories {
|
|
|
|
if strings.HasSuffix(directory, "/") {
|
|
|
|
directory = directory[:len(directory)-1]
|
|
|
|
}
|
|
|
|
toReturn[i] = ObjectStorageFileInfo{
|
|
|
|
IsDirectory: true,
|
|
|
|
Name: directory,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for i, file := range result.Files {
|
|
|
|
//2020-02-15T22:35:15.788Z
|
|
|
|
lastModified, err := time.Parse(time.RFC3339, result.FilesLastModified[i])
|
|
|
|
if err != nil {
|
|
|
|
return []ObjectStorageFileInfo{}, errors.Wrapf(err, "can't parse file last modified date")
|
|
|
|
}
|
|
|
|
toReturn[len(result.Directories)+i] = ObjectStorageFileInfo{
|
|
|
|
IsDirectory: false,
|
|
|
|
LastModified: lastModified,
|
|
|
|
Name: file,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return toReturn, nil
|
2020-03-06 16:39:33 +00:00
|
|
|
}
|
|
|
|
|
2020-06-10 00:31:14 +00:00
|
|
|
func (self S3Compatible) Get(key string) (ObjectStorageFile, bool, error) {
|
2020-09-20 05:53:01 +00:00
|
|
|
if !strings.HasPrefix(key, "/") {
|
|
|
|
key = fmt.Sprintf("/%s", key)
|
|
|
|
}
|
|
|
|
response, err := self.doReq("GET", fmt.Sprintf("%s?delimiter=/", key), nil)
|
|
|
|
if err != nil {
|
|
|
|
return ObjectStorageFile{}, false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if response.StatusCode == http.StatusNotFound {
|
|
|
|
return ObjectStorageFile{}, true, errors.New("404 not found")
|
|
|
|
}
|
|
|
|
|
|
|
|
//Sat, 15 Feb 2020 22:47:15 GMT
|
|
|
|
lastModified, err := time.Parse(S3_LAST_MODIFIED_HEADER_FORMAT, response.Header.Get("Last-Modified"))
|
|
|
|
if err != nil {
|
|
|
|
return ObjectStorageFile{}, false, errors.Wrapf(err, "can't parse Last-Modified header")
|
|
|
|
}
|
|
|
|
|
|
|
|
bytes, err := ioutil.ReadAll(response.Body)
|
|
|
|
if err != nil {
|
|
|
|
return ObjectStorageFile{}, false, errors.Wrap(err, "could not read response body")
|
|
|
|
}
|
|
|
|
|
|
|
|
toReturn := ObjectStorageFile{
|
|
|
|
Name: filepath.Base(key),
|
|
|
|
LastModified: lastModified,
|
|
|
|
Content: bytes,
|
|
|
|
}
|
|
|
|
|
|
|
|
return toReturn, false, nil
|
2020-03-06 16:39:33 +00:00
|
|
|
}
|
|
|
|
|
2020-06-10 00:31:14 +00:00
|
|
|
func (self S3Compatible) Put(key string, value []byte) error {
|
2020-09-20 05:53:01 +00:00
|
|
|
_, err := self.doReq("PUT", fmt.Sprintf("%s?delimiter=/", key), bytes.NewBuffer(value))
|
|
|
|
return err
|
2020-03-06 16:39:33 +00:00
|
|
|
}
|
|
|
|
|
2020-06-10 00:31:14 +00:00
|
|
|
func (self S3Compatible) Delete(key string) error {
|
2020-09-20 05:53:01 +00:00
|
|
|
_, err := self.doReq("DELETE", fmt.Sprintf("%s?delimiter=/", key), nil)
|
|
|
|
return err
|
2020-03-06 16:39:33 +00:00
|
|
|
}
|
|
|
|
|
2020-06-10 00:31:14 +00:00
|
|
|
func (self *S3Compatible) doReq(method, pathAndQuery string, body io.Reader) (*http.Response, error) {
|
2020-09-20 05:53:01 +00:00
|
|
|
url := fmt.Sprintf("%s%s", self.Domain, pathAndQuery)
|
|
|
|
request, err := http.NewRequest(method, url, body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "could not create request")
|
|
|
|
}
|
|
|
|
request = signer.SignV4(*request, self.KeyId, self.SecretKey, "", self.Region)
|
|
|
|
|
|
|
|
response, err := self.HTTPClient.Do(request)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "could not HTTP %s %s", method, url)
|
|
|
|
}
|
|
|
|
if response.StatusCode == http.StatusForbidden {
|
|
|
|
return nil, fmt.Errorf("invalid credential for %s, access was forbidden", url)
|
|
|
|
} else if response.StatusCode == http.StatusNotFound || (response.StatusCode >= 200 && response.StatusCode < 300) {
|
|
|
|
return response, nil
|
|
|
|
} else {
|
|
|
|
return nil, fmt.Errorf("got %d %s from %s", response.StatusCode, response.Status, url)
|
|
|
|
}
|
2020-03-06 16:39:33 +00:00
|
|
|
}
|