191 lines
4.3 KiB
Go

package router
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"khairul169/garage-webui/schema"
"khairul169/garage-webui/utils"
"net/http"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/smithy-go"
)
type Browse struct{}
func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
bucket := r.PathValue("bucket")
prefix := query.Get("prefix")
continuationToken := query.Get("next")
limit, err := strconv.Atoi(query.Get("limit"))
if err != nil {
limit = 100
}
client, err := getS3Client(bucket)
if err != nil {
utils.ResponseError(w, err)
return
}
objects, err := client.ListObjectsV2(context.Background(), &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
Prefix: aws.String(prefix),
Delimiter: aws.String("/"),
MaxKeys: aws.Int32(int32(limit)),
ContinuationToken: aws.String(continuationToken),
})
if err != nil {
utils.ResponseError(w, err)
return
}
result := schema.BrowseObjectResult{
Prefixes: []string{},
Objects: []schema.BrowserObject{},
Prefix: prefix,
NextToken: objects.NextContinuationToken,
}
for _, prefix := range objects.CommonPrefixes {
result.Prefixes = append(result.Prefixes, *prefix.Prefix)
}
for _, object := range objects.Contents {
key := strings.TrimPrefix(*object.Key, prefix)
if key == "" {
continue
}
result.Objects = append(result.Objects, schema.BrowserObject{
ObjectKey: &key,
LastModified: object.LastModified,
Size: object.Size,
})
}
utils.ResponseSuccess(w, result)
}
func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) {
bucket := r.PathValue("bucket")
key := r.PathValue("key")
view := r.URL.Query().Get("view") == "1"
download := r.URL.Query().Get("dl") == "1"
client, err := getS3Client(bucket)
if err != nil {
utils.ResponseError(w, err)
return
}
object, err := client.GetObject(context.Background(), &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
if err != nil {
var ae smithy.APIError
if errors.As(err, &ae) && ae.ErrorCode() == "NoSuchKey" {
utils.ResponseErrorStatus(w, err, http.StatusNotFound)
return
}
utils.ResponseError(w, err)
return
}
if view || download {
defer object.Body.Close()
keys := strings.Split(key, "/")
w.Header().Set("Content-Type", *object.ContentType)
if download {
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", keys[len(keys)-1]))
}
w.WriteHeader(http.StatusOK)
_, err = io.Copy(w, object.Body)
if err != nil {
utils.ResponseError(w, err)
return
}
return
}
utils.ResponseSuccess(w, object)
}
func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {
cacheKey := fmt.Sprintf("key:%s", bucket)
cacheData := utils.Cache.Get(cacheKey)
if cacheData != nil {
return cacheData.(aws.CredentialsProvider), nil
}
body, err := utils.Garage.Fetch("/v1/bucket?globalAlias="+bucket, &utils.FetchOptions{})
if err != nil {
return nil, err
}
var bucketData schema.Bucket
if err := json.Unmarshal(body, &bucketData); err != nil {
return nil, err
}
var key schema.KeyElement
for _, k := range bucketData.Keys {
if !k.Permissions.Read || !k.Permissions.Write {
continue
}
body, err := utils.Garage.Fetch(fmt.Sprintf("/v1/key?id=%s&showSecretKey=true", k.AccessKeyID), &utils.FetchOptions{})
if err != nil {
return nil, err
}
if err := json.Unmarshal(body, &key); err != nil {
return nil, err
}
break
}
credential := credentials.NewStaticCredentialsProvider(key.AccessKeyID, key.SecretAccessKey, "")
utils.Cache.Set(cacheKey, credential, time.Hour)
return credential, nil
}
func getS3Client(bucket string) (*s3.Client, error) {
creds, err := getBucketCredentials(bucket)
if err != nil {
return nil, fmt.Errorf("cannot get credentials for bucket %s: %w", bucket, err)
}
awsConfig := aws.Config{
Credentials: creds,
Region: "garage",
BaseEndpoint: aws.String(utils.Garage.GetS3Endpoint()),
}
client := s3.NewFromConfig(awsConfig, func(o *s3.Options) {
o.UsePathStyle = true
o.EndpointOptions.DisableHTTPS = true
})
return client, nil
}