mirror of
				https://github.com/khairul169/garage-webui.git
				synced 2025-10-31 07:09:32 +07:00 
			
		
		
		
	
		
			
				
	
	
		
			347 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			347 lines
		
	
	
		
			7.9 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/aws-sdk-go-v2/service/s3/types"
 | |
| 	"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,
 | |
| 			Url:          fmt.Sprintf("/browse/%s/%s", bucket, *object.Key),
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	utils.ResponseSuccess(w, result)
 | |
| }
 | |
| 
 | |
| func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) {
 | |
| 	bucket := r.PathValue("bucket")
 | |
| 	key := r.PathValue("key")
 | |
| 	queryParams := r.URL.Query()
 | |
| 	view := queryParams.Get("view") == "1"
 | |
| 	thumbnail := queryParams.Get("thumb") == "1"
 | |
| 	download := queryParams.Get("dl") == "1"
 | |
| 
 | |
| 	client, err := getS3Client(bucket)
 | |
| 	if err != nil {
 | |
| 		utils.ResponseError(w, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if !view && !download && !thumbnail {
 | |
| 		object, err := client.HeadObject(context.Background(), &s3.HeadObjectInput{
 | |
| 			Bucket: aws.String(bucket),
 | |
| 			Key:    aws.String(key),
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			utils.ResponseError(w, err)
 | |
| 		}
 | |
| 		utils.ResponseSuccess(w, object)
 | |
| 		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
 | |
| 	}
 | |
| 
 | |
| 	defer object.Body.Close()
 | |
| 	keys := strings.Split(key, "/")
 | |
| 
 | |
| 	if download {
 | |
| 		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", keys[len(keys)-1]))
 | |
| 	} else if thumbnail {
 | |
| 		body, err := io.ReadAll(object.Body)
 | |
| 		if err != nil {
 | |
| 			utils.ResponseError(w, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		thumb, err := utils.CreateThumbnailImage(body, 64, 64)
 | |
| 		if err != nil {
 | |
| 
 | |
| 			utils.ResponseError(w, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		w.Header().Set("Content-Type", "image/png")
 | |
| 		w.Write(thumb)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	w.Header().Set("Cache-Control", "max-age=86400")
 | |
| 	w.Header().Set("Last-Modified", object.LastModified.Format(time.RFC1123))
 | |
| 
 | |
| 	if object.ContentType != nil {
 | |
| 		w.Header().Set("Content-Type", *object.ContentType)
 | |
| 	} else {
 | |
| 		w.Header().Set("Content-Type", "application/octet-stream")
 | |
| 	}
 | |
| 	if object.ContentLength != nil {
 | |
| 		w.Header().Set("Content-Length", strconv.FormatInt(*object.ContentLength, 10))
 | |
| 	}
 | |
| 	if object.ETag != nil {
 | |
| 		w.Header().Set("Etag", *object.ETag)
 | |
| 	}
 | |
| 
 | |
| 	_, err = io.Copy(w, object.Body)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		utils.ResponseError(w, err)
 | |
| 		return
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) {
 | |
| 	bucket := r.PathValue("bucket")
 | |
| 	key := r.PathValue("key")
 | |
| 	isDirectory := strings.HasSuffix(key, "/")
 | |
| 
 | |
| 	file, headers, err := r.FormFile("file")
 | |
| 	if err != nil && !isDirectory {
 | |
| 		utils.ResponseError(w, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if file != nil {
 | |
| 		defer file.Close()
 | |
| 	}
 | |
| 
 | |
| 	client, err := getS3Client(bucket)
 | |
| 	if err != nil {
 | |
| 		utils.ResponseError(w, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var contentType string = ""
 | |
| 	var size int64 = 0
 | |
| 
 | |
| 	if file != nil {
 | |
| 		contentType = headers.Header.Get("Content-Type")
 | |
| 		size = headers.Size
 | |
| 	}
 | |
| 
 | |
| 	result, err := client.PutObject(context.Background(), &s3.PutObjectInput{
 | |
| 		Bucket:        aws.String(bucket),
 | |
| 		Key:           aws.String(key),
 | |
| 		Body:          file,
 | |
| 		ContentLength: aws.Int64(size),
 | |
| 		ContentType:   aws.String(contentType),
 | |
| 	})
 | |
| 
 | |
| 	if err != nil {
 | |
| 		utils.ResponseError(w, fmt.Errorf("cannot put object: %w", err))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	utils.ResponseSuccess(w, result)
 | |
| }
 | |
| 
 | |
| func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
 | |
| 	bucket := r.PathValue("bucket")
 | |
| 	key := r.PathValue("key")
 | |
| 	recursive := r.URL.Query().Get("recursive") == "true"
 | |
| 	isDirectory := strings.HasSuffix(key, "/")
 | |
| 
 | |
| 	client, err := getS3Client(bucket)
 | |
| 	if err != nil {
 | |
| 		utils.ResponseError(w, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Delete directory and its content
 | |
| 	if isDirectory && recursive {
 | |
| 		objects, err := client.ListObjectsV2(context.Background(), &s3.ListObjectsV2Input{
 | |
| 			Bucket: aws.String(bucket),
 | |
| 			Prefix: aws.String(key),
 | |
| 		})
 | |
| 
 | |
| 		if err != nil {
 | |
| 			utils.ResponseError(w, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		if len(objects.Contents) == 0 {
 | |
| 			utils.ResponseSuccess(w, true)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		keys := make([]types.ObjectIdentifier, 0, len(objects.Contents))
 | |
| 
 | |
| 		for _, object := range objects.Contents {
 | |
| 			keys = append(keys, types.ObjectIdentifier{
 | |
| 				Key: object.Key,
 | |
| 			})
 | |
| 		}
 | |
| 
 | |
| 		res, err := client.DeleteObjects(context.Background(), &s3.DeleteObjectsInput{
 | |
| 			Bucket: aws.String(bucket),
 | |
| 			Delete: &types.Delete{Objects: keys},
 | |
| 		})
 | |
| 
 | |
| 		if err != nil {
 | |
| 			utils.ResponseError(w, fmt.Errorf("cannot delete object: %w", err))
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		if len(res.Errors) > 0 {
 | |
| 			utils.ResponseError(w, fmt.Errorf("cannot delete object: %v", res.Errors[0]))
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		utils.ResponseSuccess(w, res)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Delete single object
 | |
| 	res, err := client.DeleteObject(context.Background(), &s3.DeleteObjectInput{
 | |
| 		Bucket: aws.String(bucket),
 | |
| 		Key:    aws.String(key),
 | |
| 	})
 | |
| 
 | |
| 	if err != nil {
 | |
| 		utils.ResponseError(w, fmt.Errorf("cannot delete object: %w", err))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	utils.ResponseSuccess(w, res)
 | |
| }
 | |
| 
 | |
| 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:       utils.Garage.GetS3Region(),
 | |
| 		BaseEndpoint: aws.String(utils.Garage.GetS3Endpoint()),
 | |
| 	}
 | |
| 
 | |
| 	client := s3.NewFromConfig(awsConfig, func(o *s3.Options) {
 | |
| 		o.UsePathStyle = true
 | |
| 		o.EndpointOptions.DisableHTTPS = true
 | |
| 	})
 | |
| 
 | |
| 	return client, nil
 | |
| }
 |