| @@ -6,9 +6,8 @@ | |||||
| package models | package models | ||||
| import ( | import ( | ||||
| "fmt" | |||||
| "code.gitea.io/gitea/modules/git" | "code.gitea.io/gitea/modules/git" | ||||
| "fmt" | |||||
| ) | ) | ||||
| // ErrNotExist represents a non-exist error. | // ErrNotExist represents a non-exist error. | ||||
| @@ -1971,3 +1970,19 @@ func IsErrOAuthApplicationNotFound(err error) bool { | |||||
| func (err ErrOAuthApplicationNotFound) Error() string { | func (err ErrOAuthApplicationNotFound) Error() string { | ||||
| return fmt.Sprintf("OAuth application not found [ID: %d]", err.ID) | return fmt.Sprintf("OAuth application not found [ID: %d]", err.ID) | ||||
| } | } | ||||
| // ErrFileChunkNotExist represents a "FileChunkNotExist" kind of error. | |||||
| type ErrFileChunkNotExist struct { | |||||
| Md5 string | |||||
| Uuid string | |||||
| } | |||||
| func (err ErrFileChunkNotExist) Error() string { | |||||
| return fmt.Sprintf("fileChunk does not exist [md5: %s, uuid: %s]", err.Md5, err.Uuid) | |||||
| } | |||||
| // IsErrFileChunkNotExist checks if an error is a ErrFileChunkNotExist. | |||||
| func IsErrFileChunkNotExist(err error) bool { | |||||
| _, ok := err.(ErrFileChunkNotExist) | |||||
| return ok | |||||
| } | |||||
| @@ -0,0 +1,78 @@ | |||||
| package models | |||||
| import ( | |||||
| "code.gitea.io/gitea/modules/timeutil" | |||||
| "xorm.io/xorm" | |||||
| ) | |||||
| const ( | |||||
| FileNotUploaded int = iota | |||||
| FileUploaded | |||||
| ) | |||||
| type FileChunk struct { | |||||
| ID int64 `xorm:"pk autoincr"` | |||||
| UUID string `xorm:"uuid UNIQUE"` | |||||
| Md5 string `xorm:"UNIQUE"` | |||||
| IsUploaded int `xorm:"DEFAULT 0"` // not uploaded: 0, uploaded: 1 | |||||
| HasUploaded string //chunkNumbers 1,2,3 | |||||
| UploadID string `xorm:"UNIQUE"`//minio upload id | |||||
| TotalChunks int64 | |||||
| Size int64 | |||||
| UserID int64 `xorm:"INDEX"` | |||||
| CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||||
| UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||||
| } | |||||
| // GetFileChunkByMD5 returns fileChunk by given id | |||||
| func GetFileChunkByMD5(md5 string) (*FileChunk, error) { | |||||
| return getFileChunkByMD5(x, md5) | |||||
| } | |||||
| func getFileChunkByMD5(e Engine, md5 string) (*FileChunk, error) { | |||||
| fileChunk := new(FileChunk) | |||||
| if has, err := e.Where("md5 = ?", md5).Get(fileChunk); err != nil { | |||||
| return nil, err | |||||
| } else if !has { | |||||
| return nil, ErrFileChunkNotExist{md5, ""} | |||||
| } | |||||
| return fileChunk, nil | |||||
| } | |||||
| // GetAttachmentByID returns attachment by given id | |||||
| func GetFileChunkByUUID(uuid string) (*FileChunk, error) { | |||||
| return getFileChunkByUUID(x, uuid) | |||||
| } | |||||
| func getFileChunkByUUID(e Engine, uuid string) (*FileChunk, error) { | |||||
| fileChunk := new(FileChunk) | |||||
| if has, err := e.Where("uuid = ?", uuid).Get(fileChunk); err != nil { | |||||
| return nil, err | |||||
| } else if !has { | |||||
| return nil, ErrFileChunkNotExist{"", uuid} | |||||
| } | |||||
| return fileChunk, nil | |||||
| } | |||||
| // InsertFileChunk insert a record into file_chunk. | |||||
| func InsertFileChunk(fileChunk *FileChunk) (_ *FileChunk, err error) { | |||||
| if _, err := x.Insert(fileChunk); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return fileChunk,nil | |||||
| } | |||||
| // UpdateAttachment updates the given attachment in database | |||||
| func UpdateFileChunk(fileChunk *FileChunk) error { | |||||
| return updateFileChunk(x, fileChunk) | |||||
| } | |||||
| func updateFileChunk(e Engine, fileChunk *FileChunk) error { | |||||
| var sess *xorm.Session | |||||
| sess = e.Where("uuid = ?", fileChunk.UUID) | |||||
| _, err := sess.Cols("is_uploaded", "has_uploaded").Update(fileChunk) | |||||
| return err | |||||
| } | |||||
| @@ -126,6 +126,7 @@ func init() { | |||||
| new(LanguageStat), | new(LanguageStat), | ||||
| new(EmailHash), | new(EmailHash), | ||||
| new(Dataset), | new(Dataset), | ||||
| new(FileChunk), | |||||
| ) | ) | ||||
| gonicNames := []string{"SSL", "UID"} | gonicNames := []string{"SSL", "UID"} | ||||
| @@ -98,13 +98,17 @@ func getClients()(*minio_ext.Client, *miniov6.Core, error){ | |||||
| return client, core, nil | return client, core, nil | ||||
| } | } | ||||
| func GenMultiPartSignedUrl(bucketName string, objectName string, uploadId string, partNumber int, partSize int64) (string, error) { | |||||
| func GenMultiPartSignedUrl(uuid string, uploadId string, partNumber int, partSize int64) (string, error) { | |||||
| minioClient, _, err := getClients() | minioClient, _, err := getClients() | ||||
| if err != nil { | if err != nil { | ||||
| log.Error("getClients failed:", err.Error()) | log.Error("getClients failed:", err.Error()) | ||||
| return "", err | return "", err | ||||
| } | } | ||||
| minio := setting.Attachment.Minio | |||||
| bucketName := minio.Bucket | |||||
| objectName := strings.TrimPrefix(path.Join(minio.BasePath, path.Join(uuid[0:1], uuid[1:2], uuid)), "/") | |||||
| return minioClient.GenUploadPartSignedUrl(uploadId, bucketName, objectName, partNumber, partSize, PresignedUploadPartUrlExpireTime) | return minioClient.GenUploadPartSignedUrl(uploadId, bucketName, objectName, partNumber, partSize, PresignedUploadPartUrlExpireTime) | ||||
| } | } | ||||
| @@ -277,6 +277,28 @@ func AddAttachment(ctx *context.Context) { | |||||
| }) | }) | ||||
| } | } | ||||
| func GetSuccessChunks(ctx *context.Context) { | |||||
| fileMD5 := ctx.Params("fileMD5") | |||||
| fileChunk, err := models.GetFileChunkByMD5(fileMD5) | |||||
| if err != nil { | |||||
| if models.IsErrFileChunkNotExist(err) { | |||||
| ctx.Error(404) | |||||
| } else { | |||||
| ctx.ServerError("GetFileChunkByMD5", err) | |||||
| } | |||||
| return | |||||
| } | |||||
| ctx.JSON(200, map[string]string{ | |||||
| "uuid": fileChunk.UUID, | |||||
| "uploaded": strconv.Itoa(fileChunk.IsUploaded), | |||||
| "uploadID":fileChunk.UploadID, | |||||
| "chunks": fileChunk.HasUploaded, | |||||
| }) | |||||
| } | |||||
| func NewMultipart(ctx *context.Context) { | func NewMultipart(ctx *context.Context) { | ||||
| if !setting.Attachment.Enabled { | if !setting.Attachment.Enabled { | ||||
| ctx.Error(404, "attachment is not enabled") | ctx.Error(404, "attachment is not enabled") | ||||
| @@ -291,15 +313,28 @@ func NewMultipart(ctx *context.Context) { | |||||
| if setting.Attachment.StoreType == storage.MinioStorageType { | if setting.Attachment.StoreType == storage.MinioStorageType { | ||||
| uuid := gouuid.NewV4().String() | uuid := gouuid.NewV4().String() | ||||
| url, err := storage.NewMultiPartUpload(uuid) | |||||
| uploadID, err := storage.NewMultiPartUpload(uuid) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("NewMultipart", err) | ctx.ServerError("NewMultipart", err) | ||||
| return | return | ||||
| } | } | ||||
| _, err = models.InsertFileChunk(&models.FileChunk{ | |||||
| UUID: uuid, | |||||
| UserID: ctx.User.ID, | |||||
| UploadID: uploadID, | |||||
| Md5: ctx.Params("md5"), | |||||
| Size: ctx.ParamsInt64("size"), | |||||
| }) | |||||
| if err != nil { | |||||
| ctx.Error(500, fmt.Sprintf("InsertFileChunk: %v", err)) | |||||
| return | |||||
| } | |||||
| ctx.JSON(200, map[string]string{ | ctx.JSON(200, map[string]string{ | ||||
| "uuid": uuid, | "uuid": uuid, | ||||
| "url": url, | |||||
| "uploadID": uploadID, | |||||
| }) | }) | ||||
| } else { | } else { | ||||
| ctx.Error(404, "storage type is not enabled") | ctx.Error(404, "storage type is not enabled") | ||||
| @@ -307,18 +342,52 @@ func NewMultipart(ctx *context.Context) { | |||||
| } | } | ||||
| } | } | ||||
| func GetMultipartUploadUrl(ctx *context.Context) { | |||||
| uuid := ctx.Query("uuid") | |||||
| uploadID := ctx.Params("uploadID") | |||||
| partNumber := ctx.ParamsInt("partNumber") | |||||
| size := ctx.ParamsInt64("size") | |||||
| url,err := storage.GenMultiPartSignedUrl(uuid, uploadID, partNumber, size) | |||||
| if err != nil { | |||||
| ctx.Error(500, fmt.Sprintf("GenMultiPartSignedUrl failed: %v", err)) | |||||
| return | |||||
| } | |||||
| ctx.JSON(200, map[string]string{ | |||||
| "url": url, | |||||
| }) | |||||
| } | |||||
| func CompleteMultipart(ctx *context.Context) { | func CompleteMultipart(ctx *context.Context) { | ||||
| uuid := ctx.Query("uuid") | uuid := ctx.Query("uuid") | ||||
| uploadID := ctx.Query("uploadID") | uploadID := ctx.Query("uploadID") | ||||
| completedParts := ctx.Query("completedParts") | completedParts := ctx.Query("completedParts") | ||||
| _, err := storage.CompleteMultiPartUpload(uuid, uploadID, completedParts) | |||||
| fileChunk, err := models.GetFileChunkByUUID(uuid) | |||||
| if err != nil { | |||||
| if models.IsErrFileChunkNotExist(err) { | |||||
| ctx.Error(404) | |||||
| } else { | |||||
| ctx.ServerError("GetFileChunkByUUID", err) | |||||
| } | |||||
| return | |||||
| } | |||||
| _, err = storage.CompleteMultiPartUpload(uuid, uploadID, completedParts) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.Error(500, fmt.Sprintf("CompleteMultiPartUpload failed: %v", err)) | ctx.Error(500, fmt.Sprintf("CompleteMultiPartUpload failed: %v", err)) | ||||
| return | return | ||||
| } | } | ||||
| fileChunk.IsUploaded = models.FileUploaded | |||||
| err = models.UpdateFileChunk(fileChunk) | |||||
| if err != nil { | |||||
| ctx.Error(500, fmt.Sprintf("UpdateFileChunk: %v", err)) | |||||
| return | |||||
| } | |||||
| _, err = models.InsertAttachment(&models.Attachment{ | _, err = models.InsertAttachment(&models.Attachment{ | ||||
| UUID: uuid, | UUID: uuid, | ||||
| UploaderID: ctx.User.ID, | UploaderID: ctx.User.ID, | ||||
| @@ -337,3 +406,30 @@ func CompleteMultipart(ctx *context.Context) { | |||||
| "result_code": "0", | "result_code": "0", | ||||
| }) | }) | ||||
| } | } | ||||
| func UpdateMultipart(ctx *context.Context) { | |||||
| uuid := ctx.Query("uuid") | |||||
| partNumber := ctx.QueryInt("partNumber") | |||||
| fileChunk, err := models.GetFileChunkByUUID(uuid) | |||||
| if err != nil { | |||||
| if models.IsErrFileChunkNotExist(err) { | |||||
| ctx.Error(404) | |||||
| } else { | |||||
| ctx.ServerError("GetFileChunkByUUID", err) | |||||
| } | |||||
| return | |||||
| } | |||||
| fileChunk.HasUploaded += "," + strconv.Itoa(partNumber) | |||||
| err = models.UpdateFileChunk(fileChunk) | |||||
| if err != nil { | |||||
| ctx.Error(500, fmt.Sprintf("UpdateFileChunk: %v", err)) | |||||
| return | |||||
| } | |||||
| ctx.JSON(200, map[string]string{ | |||||
| "result_code": "0", | |||||
| }) | |||||
| } | |||||
| @@ -521,8 +521,11 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Get("/get_pre_url", repo.GetPresignedPutObjectURL) | m.Get("/get_pre_url", repo.GetPresignedPutObjectURL) | ||||
| m.Post("/add", repo.AddAttachment) | m.Post("/add", repo.AddAttachment) | ||||
| m.Post("/private", repo.UpdatePublicAttachment) | m.Post("/private", repo.UpdatePublicAttachment) | ||||
| m.Get("/get_chunks", repo.GetSuccessChunks) | |||||
| m.Get("/new_multipart", repo.NewMultipart) | m.Get("/new_multipart", repo.NewMultipart) | ||||
| m.Get("/get_multipart_url", repo.GetMultipartUploadUrl) | |||||
| m.Post("/complete_multipart", repo.CompleteMultipart) | m.Post("/complete_multipart", repo.CompleteMultipart) | ||||
| m.Post("/update_multipart", repo.UpdateMultipart) | |||||
| }, reqSignIn) | }, reqSignIn) | ||||
| m.Group("/:username", func() { | m.Group("/:username", func() { | ||||