github.com/vnforks/kid/v5@v5.22.1-0.20200408055009-b89d99c65676/services/filesstore/s3store.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package filesstore 5 6 import ( 7 "bytes" 8 "io" 9 "io/ioutil" 10 "net/http" 11 "os" 12 "path/filepath" 13 "strings" 14 15 s3 "github.com/minio/minio-go/v6" 16 "github.com/minio/minio-go/v6/pkg/credentials" 17 "github.com/minio/minio-go/v6/pkg/encrypt" 18 19 "github.com/vnforks/kid/v5/mlog" 20 "github.com/vnforks/kid/v5/model" 21 ) 22 23 type S3FileBackend struct { 24 endpoint string 25 accessKey string 26 secretKey string 27 secure bool 28 signV2 bool 29 region string 30 bucket string 31 encrypt bool 32 trace bool 33 } 34 35 // Similar to s3.New() but allows initialization of signature v2 or signature v4 client. 36 // If signV2 input is false, function always returns signature v4. 37 // 38 // Additionally this function also takes a user defined region, if set 39 // disables automatic region lookup. 40 func (b *S3FileBackend) s3New() (*s3.Client, error) { 41 var creds *credentials.Credentials 42 43 if b.accessKey == "" && b.secretKey == "" { 44 creds = credentials.NewIAM("") 45 } else if b.signV2 { 46 creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV2) 47 } else { 48 creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV4) 49 } 50 51 s3Clnt, err := s3.NewWithCredentials(b.endpoint, creds, b.secure, b.region) 52 if err != nil { 53 return nil, err 54 } 55 56 if b.trace { 57 s3Clnt.TraceOn(os.Stdout) 58 } 59 60 return s3Clnt, nil 61 } 62 63 func (b *S3FileBackend) TestConnection() *model.AppError { 64 s3Clnt, err := b.s3New() 65 if err != nil { 66 return model.NewAppError("TestFileConnection", "api.file.test_connection.s3.connection.app_error", nil, err.Error(), http.StatusInternalServerError) 67 } 68 69 exists, err := s3Clnt.BucketExists(b.bucket) 70 if err != nil { 71 return model.NewAppError("TestFileConnection", "api.file.test_connection.s3.bucket_exists.app_error", nil, err.Error(), http.StatusInternalServerError) 72 } 73 74 if !exists { 75 mlog.Warn("Bucket specified does not exist. Attempting to create...") 76 err := s3Clnt.MakeBucket(b.bucket, b.region) 77 if err != nil { 78 mlog.Error("Unable to create bucket.") 79 return model.NewAppError("TestFileConnection", "api.file.test_connection.s3.bucked_create.app_error", nil, err.Error(), http.StatusInternalServerError) 80 } 81 } 82 mlog.Debug("Connection to S3 or minio is good. Bucket exists.") 83 return nil 84 } 85 86 // Caller must close the first return value 87 func (b *S3FileBackend) Reader(path string) (ReadCloseSeeker, *model.AppError) { 88 s3Clnt, err := b.s3New() 89 if err != nil { 90 return nil, model.NewAppError("Reader", "api.file.reader.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 91 } 92 minioObject, err := s3Clnt.GetObject(b.bucket, path, s3.GetObjectOptions{}) 93 if err != nil { 94 return nil, model.NewAppError("Reader", "api.file.reader.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 95 } 96 return minioObject, nil 97 } 98 99 func (b *S3FileBackend) ReadFile(path string) ([]byte, *model.AppError) { 100 s3Clnt, err := b.s3New() 101 if err != nil { 102 return nil, model.NewAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 103 } 104 minioObject, err := s3Clnt.GetObject(b.bucket, path, s3.GetObjectOptions{}) 105 if err != nil { 106 return nil, model.NewAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 107 } 108 defer minioObject.Close() 109 if f, err := ioutil.ReadAll(minioObject); err != nil { 110 return nil, model.NewAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 111 } else { 112 return f, nil 113 } 114 } 115 116 func (b *S3FileBackend) FileExists(path string) (bool, *model.AppError) { 117 s3Clnt, err := b.s3New() 118 119 if err != nil { 120 return false, model.NewAppError("FileExists", "api.file.file_exists.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 121 } 122 _, err = s3Clnt.StatObject(b.bucket, path, s3.StatObjectOptions{}) 123 124 if err == nil { 125 return true, nil 126 } 127 128 if err.(s3.ErrorResponse).Code == "NoSuchKey" { 129 return false, nil 130 } 131 132 return false, model.NewAppError("FileExists", "api.file.file_exists.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 133 } 134 135 func (b *S3FileBackend) CopyFile(oldPath, newPath string) *model.AppError { 136 s3Clnt, err := b.s3New() 137 if err != nil { 138 return model.NewAppError("copyFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 139 } 140 141 source := s3.NewSourceInfo(b.bucket, oldPath, nil) 142 destination, err := s3.NewDestinationInfo(b.bucket, newPath, encrypt.NewSSE(), nil) 143 if err != nil { 144 return model.NewAppError("copyFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 145 } 146 if err = s3Clnt.CopyObject(destination, source); err != nil { 147 return model.NewAppError("copyFile", "api.file.move_file.copy_within_s3.app_error", nil, err.Error(), http.StatusInternalServerError) 148 } 149 return nil 150 } 151 152 func (b *S3FileBackend) MoveFile(oldPath, newPath string) *model.AppError { 153 s3Clnt, err := b.s3New() 154 if err != nil { 155 return model.NewAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 156 } 157 158 source := s3.NewSourceInfo(b.bucket, oldPath, nil) 159 destination, err := s3.NewDestinationInfo(b.bucket, newPath, encrypt.NewSSE(), nil) 160 if err != nil { 161 return model.NewAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 162 } 163 if err = s3Clnt.CopyObject(destination, source); err != nil { 164 return model.NewAppError("moveFile", "api.file.move_file.copy_within_s3.app_error", nil, err.Error(), http.StatusInternalServerError) 165 } 166 if err = s3Clnt.RemoveObject(b.bucket, oldPath); err != nil { 167 return model.NewAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error(), http.StatusInternalServerError) 168 } 169 return nil 170 } 171 172 func (b *S3FileBackend) WriteFile(fr io.Reader, path string) (int64, *model.AppError) { 173 s3Clnt, err := b.s3New() 174 if err != nil { 175 return 0, model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 176 } 177 178 var contentType string 179 if ext := filepath.Ext(path); model.IsFileExtImage(ext) { 180 contentType = model.GetImageMimeType(ext) 181 } else { 182 contentType = "binary/octet-stream" 183 } 184 185 options := s3PutOptions(b.encrypt, contentType) 186 var buf bytes.Buffer 187 _, err = buf.ReadFrom(fr) 188 if err != nil { 189 return 0, model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 190 } 191 written, err := s3Clnt.PutObject(b.bucket, path, &buf, int64(buf.Len()), options) 192 if err != nil { 193 return written, model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 194 } 195 196 return written, nil 197 } 198 199 func (b *S3FileBackend) RemoveFile(path string) *model.AppError { 200 s3Clnt, err := b.s3New() 201 if err != nil { 202 return model.NewAppError("RemoveFile", "utils.file.remove_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 203 } 204 205 if err := s3Clnt.RemoveObject(b.bucket, path); err != nil { 206 return model.NewAppError("RemoveFile", "utils.file.remove_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 207 } 208 209 return nil 210 } 211 212 func getPathsFromObjectInfos(in <-chan s3.ObjectInfo) <-chan string { 213 out := make(chan string, 1) 214 215 go func() { 216 defer close(out) 217 218 for { 219 info, done := <-in 220 221 if !done { 222 break 223 } 224 225 out <- info.Key 226 } 227 }() 228 229 return out 230 } 231 232 func (b *S3FileBackend) ListDirectory(path string) (*[]string, *model.AppError) { 233 var paths []string 234 235 s3Clnt, err := b.s3New() 236 if err != nil { 237 return nil, model.NewAppError("ListDirectory", "utils.file.list_directory.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 238 } 239 240 doneCh := make(chan struct{}) 241 defer close(doneCh) 242 243 if !strings.HasSuffix(path, "/") && len(path) > 0 { 244 // s3Clnt returns only the path itself when "/" is not present 245 // appending "/" to make it consistent across all filesstores 246 path = path + "/" 247 } 248 for object := range s3Clnt.ListObjects(b.bucket, path, false, doneCh) { 249 if object.Err != nil { 250 return nil, model.NewAppError("ListDirectory", "utils.file.list_directory.s3.app_error", nil, object.Err.Error(), http.StatusInternalServerError) 251 } 252 paths = append(paths, strings.Trim(object.Key, "/")) 253 } 254 255 return &paths, nil 256 } 257 258 func (b *S3FileBackend) RemoveDirectory(path string) *model.AppError { 259 s3Clnt, err := b.s3New() 260 if err != nil { 261 return model.NewAppError("RemoveDirectory", "utils.file.remove_directory.s3.app_error", nil, err.Error(), http.StatusInternalServerError) 262 } 263 264 doneCh := make(chan struct{}) 265 266 for err := range s3Clnt.RemoveObjects(b.bucket, getPathsFromObjectInfos(s3Clnt.ListObjects(b.bucket, path, true, doneCh))) { 267 if err.Err != nil { 268 doneCh <- struct{}{} 269 return model.NewAppError("RemoveDirectory", "utils.file.remove_directory.s3.app_error", nil, err.Err.Error(), http.StatusInternalServerError) 270 } 271 } 272 273 close(doneCh) 274 return nil 275 } 276 277 func s3PutOptions(encrypted bool, contentType string) s3.PutObjectOptions { 278 options := s3.PutObjectOptions{} 279 if encrypted { 280 options.ServerSideEncryption = encrypt.NewSSE() 281 } 282 options.ContentType = contentType 283 284 return options 285 } 286 287 func CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError { 288 if settings.AmazonS3Bucket == nil || len(*settings.AmazonS3Bucket) == 0 { 289 return model.NewAppError("S3File", "api.admin.test_s3.missing_s3_bucket", nil, "", http.StatusBadRequest) 290 } 291 292 // if S3 endpoint is not set call the set defaults to set that 293 if settings.AmazonS3Endpoint == nil || len(*settings.AmazonS3Endpoint) == 0 { 294 settings.SetDefaults(true) 295 } 296 297 return nil 298 }