mirror of
https://github.com/khairul169/garage-webui.git
synced 2026-09-15 00:43:21 +07:00
feat: add bucket object browser
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetAllBuckets(w http.ResponseWriter, r *http.Request) {
|
||||
type Buckets struct{}
|
||||
|
||||
func (b *Buckets) GetAll(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := utils.Garage.Fetch("/v1/bucket?list", &utils.FetchOptions{})
|
||||
if err != nil {
|
||||
utils.ResponseError(w, err)
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
type Config struct{}
|
||||
|
||||
func (c *Config) GetAll(w http.ResponseWriter, r *http.Request) {
|
||||
config := utils.Garage.Config
|
||||
utils.ResponseSuccess(w, config)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package router
|
||||
|
||||
import "net/http"
|
||||
|
||||
func HandleApiRouter() *http.ServeMux {
|
||||
router := http.NewServeMux()
|
||||
|
||||
config := &Config{}
|
||||
router.HandleFunc("GET /config", config.GetAll)
|
||||
|
||||
buckets := &Buckets{}
|
||||
router.HandleFunc("GET /buckets", buckets.GetAll)
|
||||
|
||||
browse := &Browse{}
|
||||
router.HandleFunc("GET /browse/{bucket}", browse.GetObjects)
|
||||
router.HandleFunc("GET /browse/{bucket}/{key...}", browse.GetOneObject)
|
||||
|
||||
router.HandleFunc("/", ProxyHandler)
|
||||
|
||||
return router
|
||||
}
|
||||
Reference in New Issue
Block a user