mirror of
https://github.com/AlistGo/alist.git
synced 2025-11-25 03:15:10 +08:00
287 lines
7.3 KiB
Go
287 lines
7.3 KiB
Go
|
|
package s3
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"net/url"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/alist-org/alist/v3/internal/errs"
|
||
|
|
"github.com/alist-org/alist/v3/internal/model"
|
||
|
|
"github.com/aws/aws-sdk-go/aws"
|
||
|
|
"github.com/aws/aws-sdk-go/service/s3"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
OtherMethodArchive = "archive"
|
||
|
|
OtherMethodArchiveStatus = "archive_status"
|
||
|
|
OtherMethodThaw = "thaw"
|
||
|
|
OtherMethodThawStatus = "thaw_status"
|
||
|
|
)
|
||
|
|
|
||
|
|
type ArchiveRequest struct {
|
||
|
|
StorageClass string `json:"storage_class"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type ThawRequest struct {
|
||
|
|
Days int64 `json:"days"`
|
||
|
|
Tier string `json:"tier"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type ObjectDescriptor struct {
|
||
|
|
Path string `json:"path"`
|
||
|
|
Bucket string `json:"bucket"`
|
||
|
|
Key string `json:"key"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type ArchiveResponse struct {
|
||
|
|
Action string `json:"action"`
|
||
|
|
Object ObjectDescriptor `json:"object"`
|
||
|
|
StorageClass string `json:"storage_class"`
|
||
|
|
RequestID string `json:"request_id,omitempty"`
|
||
|
|
VersionID string `json:"version_id,omitempty"`
|
||
|
|
ETag string `json:"etag,omitempty"`
|
||
|
|
LastModified string `json:"last_modified,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type ThawResponse struct {
|
||
|
|
Action string `json:"action"`
|
||
|
|
Object ObjectDescriptor `json:"object"`
|
||
|
|
RequestID string `json:"request_id,omitempty"`
|
||
|
|
Status *RestoreStatus `json:"status,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type RestoreStatus struct {
|
||
|
|
Ongoing bool `json:"ongoing"`
|
||
|
|
Expiry string `json:"expiry,omitempty"`
|
||
|
|
Raw string `json:"raw"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d *S3) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||
|
|
if args.Obj == nil {
|
||
|
|
return nil, fmt.Errorf("missing object reference")
|
||
|
|
}
|
||
|
|
if args.Obj.IsDir() {
|
||
|
|
return nil, errs.NotSupport
|
||
|
|
}
|
||
|
|
|
||
|
|
switch strings.ToLower(strings.TrimSpace(args.Method)) {
|
||
|
|
case "archive":
|
||
|
|
return d.archive(ctx, args)
|
||
|
|
case "archive_status":
|
||
|
|
return d.archiveStatus(ctx, args)
|
||
|
|
case "thaw":
|
||
|
|
return d.thaw(ctx, args)
|
||
|
|
case "thaw_status":
|
||
|
|
return d.thawStatus(ctx, args)
|
||
|
|
default:
|
||
|
|
return nil, errs.NotSupport
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d *S3) archive(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||
|
|
key := getKey(args.Obj.GetPath(), false)
|
||
|
|
payload := ArchiveRequest{}
|
||
|
|
if err := DecodeOtherArgs(args.Data, &payload); err != nil {
|
||
|
|
return nil, fmt.Errorf("parse archive request: %w", err)
|
||
|
|
}
|
||
|
|
if payload.StorageClass == "" {
|
||
|
|
return nil, fmt.Errorf("storage_class is required")
|
||
|
|
}
|
||
|
|
storageClass := NormalizeStorageClass(payload.StorageClass)
|
||
|
|
input := &s3.CopyObjectInput{
|
||
|
|
Bucket: &d.Bucket,
|
||
|
|
Key: &key,
|
||
|
|
CopySource: aws.String(url.PathEscape(d.Bucket + "/" + key)),
|
||
|
|
MetadataDirective: aws.String(s3.MetadataDirectiveCopy),
|
||
|
|
StorageClass: aws.String(storageClass),
|
||
|
|
}
|
||
|
|
copyReq, output := d.client.CopyObjectRequest(input)
|
||
|
|
copyReq.SetContext(ctx)
|
||
|
|
if err := copyReq.Send(); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
resp := ArchiveResponse{
|
||
|
|
Action: "archive",
|
||
|
|
Object: d.describeObject(args.Obj, key),
|
||
|
|
StorageClass: storageClass,
|
||
|
|
RequestID: copyReq.RequestID,
|
||
|
|
}
|
||
|
|
if output.VersionId != nil {
|
||
|
|
resp.VersionID = aws.StringValue(output.VersionId)
|
||
|
|
}
|
||
|
|
if result := output.CopyObjectResult; result != nil {
|
||
|
|
resp.ETag = aws.StringValue(result.ETag)
|
||
|
|
if result.LastModified != nil {
|
||
|
|
resp.LastModified = result.LastModified.UTC().Format(time.RFC3339)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if status, err := d.describeObjectStatus(ctx, key); err == nil {
|
||
|
|
if status.StorageClass != "" {
|
||
|
|
resp.StorageClass = status.StorageClass
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return resp, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d *S3) archiveStatus(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||
|
|
key := getKey(args.Obj.GetPath(), false)
|
||
|
|
status, err := d.describeObjectStatus(ctx, key)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return ArchiveResponse{
|
||
|
|
Action: "archive_status",
|
||
|
|
Object: d.describeObject(args.Obj, key),
|
||
|
|
StorageClass: status.StorageClass,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d *S3) thaw(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||
|
|
key := getKey(args.Obj.GetPath(), false)
|
||
|
|
payload := ThawRequest{Days: 1}
|
||
|
|
if err := DecodeOtherArgs(args.Data, &payload); err != nil {
|
||
|
|
return nil, fmt.Errorf("parse thaw request: %w", err)
|
||
|
|
}
|
||
|
|
if payload.Days <= 0 {
|
||
|
|
payload.Days = 1
|
||
|
|
}
|
||
|
|
restoreRequest := &s3.RestoreRequest{
|
||
|
|
Days: aws.Int64(payload.Days),
|
||
|
|
}
|
||
|
|
if tier := NormalizeRestoreTier(payload.Tier); tier != "" {
|
||
|
|
restoreRequest.GlacierJobParameters = &s3.GlacierJobParameters{Tier: aws.String(tier)}
|
||
|
|
}
|
||
|
|
input := &s3.RestoreObjectInput{
|
||
|
|
Bucket: &d.Bucket,
|
||
|
|
Key: &key,
|
||
|
|
RestoreRequest: restoreRequest,
|
||
|
|
}
|
||
|
|
restoreReq, _ := d.client.RestoreObjectRequest(input)
|
||
|
|
restoreReq.SetContext(ctx)
|
||
|
|
if err := restoreReq.Send(); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
status, _ := d.describeObjectStatus(ctx, key)
|
||
|
|
resp := ThawResponse{
|
||
|
|
Action: "thaw",
|
||
|
|
Object: d.describeObject(args.Obj, key),
|
||
|
|
RequestID: restoreReq.RequestID,
|
||
|
|
}
|
||
|
|
if status != nil {
|
||
|
|
resp.Status = status.Restore
|
||
|
|
}
|
||
|
|
return resp, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d *S3) thawStatus(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||
|
|
key := getKey(args.Obj.GetPath(), false)
|
||
|
|
status, err := d.describeObjectStatus(ctx, key)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return ThawResponse{
|
||
|
|
Action: "thaw_status",
|
||
|
|
Object: d.describeObject(args.Obj, key),
|
||
|
|
Status: status.Restore,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d *S3) describeObject(obj model.Obj, key string) ObjectDescriptor {
|
||
|
|
return ObjectDescriptor{
|
||
|
|
Path: obj.GetPath(),
|
||
|
|
Bucket: d.Bucket,
|
||
|
|
Key: key,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type objectStatus struct {
|
||
|
|
StorageClass string
|
||
|
|
Restore *RestoreStatus
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d *S3) describeObjectStatus(ctx context.Context, key string) (*objectStatus, error) {
|
||
|
|
head, err := d.client.HeadObjectWithContext(ctx, &s3.HeadObjectInput{Bucket: &d.Bucket, Key: &key})
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
status := &objectStatus{
|
||
|
|
StorageClass: aws.StringValue(head.StorageClass),
|
||
|
|
Restore: parseRestoreHeader(head.Restore),
|
||
|
|
}
|
||
|
|
return status, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func parseRestoreHeader(header *string) *RestoreStatus {
|
||
|
|
if header == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
value := strings.TrimSpace(*header)
|
||
|
|
if value == "" {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
status := &RestoreStatus{Raw: value}
|
||
|
|
parts := strings.Split(value, ",")
|
||
|
|
for _, part := range parts {
|
||
|
|
part = strings.TrimSpace(part)
|
||
|
|
if part == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if strings.HasPrefix(part, "ongoing-request=") {
|
||
|
|
status.Ongoing = strings.Contains(part, "\"true\"")
|
||
|
|
}
|
||
|
|
if strings.HasPrefix(part, "expiry-date=") {
|
||
|
|
expiry := strings.Trim(part[len("expiry-date="):], "\"")
|
||
|
|
if expiry != "" {
|
||
|
|
if t, err := time.Parse(time.RFC1123, expiry); err == nil {
|
||
|
|
status.Expiry = t.UTC().Format(time.RFC3339)
|
||
|
|
} else {
|
||
|
|
status.Expiry = expiry
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return status
|
||
|
|
}
|
||
|
|
|
||
|
|
func DecodeOtherArgs(data interface{}, target interface{}) error {
|
||
|
|
if data == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
raw, err := json.Marshal(data)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return json.Unmarshal(raw, target)
|
||
|
|
}
|
||
|
|
|
||
|
|
func NormalizeStorageClass(value string) string {
|
||
|
|
normalized := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(value, "-", "_")))
|
||
|
|
if normalized == "" {
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
if v, ok := storageClassLookup[normalized]; ok {
|
||
|
|
return v
|
||
|
|
}
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
|
||
|
|
func NormalizeRestoreTier(value string) string {
|
||
|
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
||
|
|
switch normalized {
|
||
|
|
case "", "default":
|
||
|
|
return ""
|
||
|
|
case "bulk":
|
||
|
|
return s3.TierBulk
|
||
|
|
case "standard":
|
||
|
|
return s3.TierStandard
|
||
|
|
case "expedited":
|
||
|
|
return s3.TierExpedited
|
||
|
|
default:
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
}
|