| @@ -6,9 +6,8 @@ | |||
| package models | |||
| import ( | |||
| "fmt" | |||
| "code.gitea.io/gitea/modules/git" | |||
| "fmt" | |||
| ) | |||
| // ErrNotExist represents a non-exist error. | |||
| @@ -1971,3 +1970,19 @@ func IsErrOAuthApplicationNotFound(err error) bool { | |||
| func (err ErrOAuthApplicationNotFound) Error() string { | |||
| 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(EmailHash), | |||
| new(Dataset), | |||
| new(FileChunk), | |||
| ) | |||
| gonicNames := []string{"SSL", "UID"} | |||
| @@ -98,13 +98,17 @@ func getClients()(*minio_ext.Client, *miniov6.Core, error){ | |||
| 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() | |||
| if err != nil { | |||
| log.Error("getClients failed:", err.Error()) | |||
| 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) | |||
| } | |||
| @@ -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) { | |||
| if !setting.Attachment.Enabled { | |||
| ctx.Error(404, "attachment is not enabled") | |||
| @@ -291,15 +313,28 @@ func NewMultipart(ctx *context.Context) { | |||
| if setting.Attachment.StoreType == storage.MinioStorageType { | |||
| uuid := gouuid.NewV4().String() | |||
| url, err := storage.NewMultiPartUpload(uuid) | |||
| uploadID, err := storage.NewMultiPartUpload(uuid) | |||
| if err != nil { | |||
| ctx.ServerError("NewMultipart", err) | |||
| 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{ | |||
| "uuid": uuid, | |||
| "url": url, | |||
| "uploadID": uploadID, | |||
| }) | |||
| } else { | |||
| 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) { | |||
| uuid := ctx.Query("uuid") | |||
| uploadID := ctx.Query("uploadID") | |||
| 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 { | |||
| ctx.Error(500, fmt.Sprintf("CompleteMultiPartUpload failed: %v", err)) | |||
| 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{ | |||
| UUID: uuid, | |||
| UploaderID: ctx.User.ID, | |||
| @@ -337,3 +406,30 @@ func CompleteMultipart(ctx *context.Context) { | |||
| "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.Post("/add", repo.AddAttachment) | |||
| m.Post("/private", repo.UpdatePublicAttachment) | |||
| m.Get("/get_chunks", repo.GetSuccessChunks) | |||
| m.Get("/new_multipart", repo.NewMultipart) | |||
| m.Get("/get_multipart_url", repo.GetMultipartUploadUrl) | |||
| m.Post("/complete_multipart", repo.CompleteMultipart) | |||
| m.Post("/update_multipart", repo.UpdateMultipart) | |||
| }, reqSignIn) | |||
| m.Group("/:username", func() { | |||