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 }