github.heygears.com/openimsdk/tools@v0.0.49/s3/cont/controller.go (about)

     1  // Copyright © 2023 OpenIM. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cont
    16  
    17  import (
    18  	"context"
    19  	"crypto/md5"
    20  	"encoding/hex"
    21  	"errors"
    22  	"fmt"
    23  	"path"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/openimsdk/tools/s3"
    28  
    29  	"github.com/google/uuid"
    30  	"github.com/openimsdk/tools/errs"
    31  	"github.com/openimsdk/tools/log"
    32  )
    33  
    34  func New(cache S3Cache, impl s3.Interface) *Controller {
    35  	return &Controller{
    36  		cache: cache,
    37  		impl:  impl,
    38  	}
    39  }
    40  
    41  type Controller struct {
    42  	cache S3Cache
    43  	impl  s3.Interface
    44  }
    45  
    46  func (c *Controller) Engine() string {
    47  	return c.impl.Engine()
    48  }
    49  
    50  func (c *Controller) HashPath(md5 string) string {
    51  	return path.Join(hashPath, md5)
    52  }
    53  
    54  func (c *Controller) NowPath() string {
    55  	now := time.Now()
    56  	return path.Join(
    57  		fmt.Sprintf("%04d", now.Year()),
    58  		fmt.Sprintf("%02d", now.Month()),
    59  		fmt.Sprintf("%02d", now.Day()),
    60  		fmt.Sprintf("%02d", now.Hour()),
    61  		fmt.Sprintf("%02d", now.Minute()),
    62  		fmt.Sprintf("%02d", now.Second()),
    63  	)
    64  }
    65  
    66  func (c *Controller) UUID() string {
    67  	id := uuid.New()
    68  	return hex.EncodeToString(id[:])
    69  }
    70  
    71  func (c *Controller) PartSize(ctx context.Context, size int64) (int64, error) {
    72  	return c.impl.PartSize(ctx, size)
    73  }
    74  
    75  func (c *Controller) PartLimit() *s3.PartLimit {
    76  	return c.impl.PartLimit()
    77  }
    78  
    79  func (c *Controller) StatObject(ctx context.Context, name string) (*s3.ObjectInfo, error) {
    80  	return c.cache.GetKey(ctx, c.impl.Engine(), name)
    81  }
    82  
    83  func (c *Controller) GetHashObject(ctx context.Context, hash string) (*s3.ObjectInfo, error) {
    84  	return c.StatObject(ctx, c.HashPath(hash))
    85  }
    86  
    87  func (c *Controller) InitiateUpload(ctx context.Context, hash string, size int64, expire time.Duration, maxParts int) (*InitiateUploadResult, error) {
    88  	defer log.ZDebug(ctx, "return")
    89  	if size < 0 {
    90  		return nil, errors.New("invalid size")
    91  	}
    92  	if hashBytes, err := hex.DecodeString(hash); err != nil {
    93  		return nil, err
    94  	} else if len(hashBytes) != md5.Size {
    95  		return nil, errors.New("invalid md5")
    96  	}
    97  	partSize, err := c.impl.PartSize(ctx, size)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  	partNumber := int(size / partSize)
   102  	if size%partSize > 0 {
   103  		partNumber++
   104  	}
   105  	if maxParts > 0 && partNumber > 0 && partNumber < maxParts {
   106  		return nil, fmt.Errorf("too many parts: %d", partNumber)
   107  	}
   108  	if info, err := c.StatObject(ctx, c.HashPath(hash)); err == nil {
   109  		return nil, &HashAlreadyExistsError{Object: info}
   110  	} else if !c.impl.IsNotFound(err) {
   111  		return nil, err
   112  	}
   113  	if size <= partSize {
   114  		// Pre-signed upload
   115  		key := path.Join(tempPath, c.NowPath(), fmt.Sprintf("%s_%d_%s.presigned", hash, size, c.UUID()))
   116  		rawURL, err := c.impl.PresignedPutObject(ctx, key, expire)
   117  		if err != nil {
   118  			return nil, err
   119  		}
   120  		return &InitiateUploadResult{
   121  			UploadID: newMultipartUploadID(multipartUploadID{
   122  				Type: UploadTypePresigned,
   123  				ID:   "",
   124  				Key:  key,
   125  				Size: size,
   126  				Hash: hash,
   127  			}),
   128  			PartSize: partSize,
   129  			Sign: &s3.AuthSignResult{
   130  				Parts: []s3.SignPart{
   131  					{
   132  						PartNumber: 1,
   133  						URL:        rawURL,
   134  					},
   135  				},
   136  			},
   137  		}, nil
   138  	} else {
   139  		// Fragment upload
   140  		upload, err := c.impl.InitiateMultipartUpload(ctx, c.HashPath(hash))
   141  		if err != nil {
   142  			return nil, err
   143  		}
   144  		if maxParts < 0 {
   145  			maxParts = partNumber
   146  		}
   147  		var authSign *s3.AuthSignResult
   148  		if maxParts > 0 {
   149  			partNumbers := make([]int, maxParts)
   150  			for i := 0; i < maxParts; i++ {
   151  				partNumbers[i] = i + 1
   152  			}
   153  			authSign, err = c.impl.AuthSign(ctx, upload.UploadID, upload.Key, time.Hour*24, partNumbers)
   154  			if err != nil {
   155  				return nil, err
   156  			}
   157  		}
   158  		return &InitiateUploadResult{
   159  			UploadID: newMultipartUploadID(multipartUploadID{
   160  				Type: UploadTypeMultipart,
   161  				ID:   upload.UploadID,
   162  				Key:  upload.Key,
   163  				Size: size,
   164  				Hash: hash,
   165  			}),
   166  			PartSize: partSize,
   167  			Sign:     authSign,
   168  		}, nil
   169  	}
   170  }
   171  
   172  func (c *Controller) CompleteUpload(ctx context.Context, uploadID string, partHashs []string) (*UploadResult, error) {
   173  	defer log.ZDebug(ctx, "return")
   174  	upload, err := parseMultipartUploadID(uploadID)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  	if md5Sum := md5.Sum([]byte(strings.Join(partHashs, partSeparator))); hex.EncodeToString(md5Sum[:]) != upload.Hash {
   179  		return nil, errors.New("md5 mismatching")
   180  	}
   181  	if info, err := c.StatObject(ctx, c.HashPath(upload.Hash)); err == nil {
   182  		return &UploadResult{
   183  			Key:  info.Key,
   184  			Size: info.Size,
   185  			Hash: info.ETag,
   186  		}, nil
   187  	} else if !c.IsNotFound(err) {
   188  		return nil, err
   189  	}
   190  	cleanObject := make(map[string]struct{})
   191  	defer func() {
   192  		for key := range cleanObject {
   193  			_ = c.impl.DeleteObject(ctx, key)
   194  		}
   195  	}()
   196  	var targetKey string
   197  	switch upload.Type {
   198  	case UploadTypeMultipart:
   199  		parts := make([]s3.Part, len(partHashs))
   200  		for i, part := range partHashs {
   201  			parts[i] = s3.Part{
   202  				PartNumber: i + 1,
   203  				ETag:       part,
   204  			}
   205  		}
   206  		// todo: Validation size
   207  		result, err := c.impl.CompleteMultipartUpload(ctx, upload.ID, upload.Key, parts)
   208  		if err != nil {
   209  			return nil, err
   210  		}
   211  		targetKey = result.Key
   212  	case UploadTypePresigned:
   213  		uploadInfo, err := c.StatObject(ctx, upload.Key)
   214  		if err != nil {
   215  			return nil, err
   216  		}
   217  		cleanObject[uploadInfo.Key] = struct{}{}
   218  		if uploadInfo.Size != upload.Size {
   219  			return nil, errors.New("upload size mismatching")
   220  		}
   221  		md5Sum := md5.Sum([]byte(strings.Join([]string{uploadInfo.ETag}, partSeparator)))
   222  		if md5val := hex.EncodeToString(md5Sum[:]); md5val != upload.Hash {
   223  			return nil, errs.ErrArgs.WrapMsg(fmt.Sprintf("md5 mismatching %s != %s", md5val, upload.Hash))
   224  		}
   225  		// Prevents concurrent operations at this time that cause files to be overwritten
   226  		copyInfo, err := c.impl.CopyObject(ctx, uploadInfo.Key, upload.Key+"."+c.UUID())
   227  		if err != nil {
   228  			return nil, err
   229  		}
   230  		cleanObject[copyInfo.Key] = struct{}{}
   231  		if copyInfo.ETag != uploadInfo.ETag {
   232  			return nil, errors.New("[concurrency]copy md5 mismatching")
   233  		}
   234  		hashCopyInfo, err := c.impl.CopyObject(ctx, copyInfo.Key, c.HashPath(upload.Hash))
   235  		if err != nil {
   236  			return nil, err
   237  		}
   238  		log.ZInfo(ctx, "hashCopyInfo", "value", fmt.Sprintf("%+v", hashCopyInfo))
   239  		targetKey = hashCopyInfo.Key
   240  	default:
   241  		return nil, errors.New("invalid upload id type")
   242  	}
   243  	if err := c.cache.DelS3Key(ctx, c.impl.Engine(), targetKey); err != nil {
   244  		return nil, err
   245  	}
   246  	return &UploadResult{
   247  		Key:  targetKey,
   248  		Size: upload.Size,
   249  		Hash: upload.Hash,
   250  	}, nil
   251  }
   252  
   253  func (c *Controller) AuthSign(ctx context.Context, uploadID string, partNumbers []int) (*s3.AuthSignResult, error) {
   254  	upload, err := parseMultipartUploadID(uploadID)
   255  	if err != nil {
   256  		return nil, err
   257  	}
   258  	switch upload.Type {
   259  	case UploadTypeMultipart:
   260  		return c.impl.AuthSign(ctx, upload.ID, upload.Key, time.Hour*24, partNumbers)
   261  	case UploadTypePresigned:
   262  		return nil, errors.New("presigned id not support auth sign")
   263  	default:
   264  		return nil, errors.New("invalid upload id type")
   265  	}
   266  }
   267  
   268  func (c *Controller) IsNotFound(err error) bool {
   269  	return c.impl.IsNotFound(err) || errs.ErrRecordNotFound.Is(err)
   270  }
   271  
   272  func (c *Controller) AccessURL(ctx context.Context, name string, expire time.Duration, opt *s3.AccessURLOption) (string, error) {
   273  	if opt.Image != nil {
   274  		opt.Filename = ""
   275  		opt.ContentType = ""
   276  	}
   277  	return c.impl.AccessURL(ctx, name, expire, opt)
   278  }
   279  
   280  func (c *Controller) FormData(ctx context.Context, name string, size int64, contentType string, duration time.Duration) (*s3.FormData, error) {
   281  	return c.impl.FormData(ctx, name, size, contentType, duration)
   282  }
   283  
   284  func (c *Controller) DeleteObject(ctx context.Context, name string) error {
   285  	return c.impl.DeleteObject(ctx, name)
   286  }