diff --git a/models/attachment.go b/models/attachment.go index 1bd5b1da4..6a2aa84de 100644 --- a/models/attachment.go +++ b/models/attachment.go @@ -318,3 +318,13 @@ func (a *Attachment) LinkedDataSet() (*Dataset, error) { } return nil, nil } + +// InsertAttachment insert a record into attachment. +func InsertAttachment(attach *Attachment) (_ *Attachment, err error) { + + if _, err := x.Insert(attach); err != nil { + return nil, err + } + + return attach, nil +} diff --git a/modules/storage/local.go b/modules/storage/local.go index 076a7f5e2..c462dcd9e 100644 --- a/modules/storage/local.go +++ b/modules/storage/local.go @@ -65,6 +65,14 @@ func (l *LocalStorage) Delete(path string) error { return os.Remove(p) } -func (l *LocalStorage) PresignedGetURL(path string, fileName string) (string,error) { - return "",nil +func (l *LocalStorage) PresignedGetURL(path string, fileName string) (string, error) { + return "", nil +} + +func (l *LocalStorage) PresignedPutURL(path string) (string, error) { + return "", nil +} + +func (l *LocalStorage) HasObject(path string) (bool, error) { + return false, nil } diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 5fafa9965..83a60f376 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -18,7 +18,10 @@ var ( _ ObjectStorage = &MinioStorage{} ) -const PRESIGNED_URL_EXPIRE_TIME = time.Hour * 24 * 7 +const ( + PresignedGetUrlExpireTime = time.Hour * 24 * 7 + PresignedPutUrlExpireTime = time.Hour * 24 * 7 +) // MinioStorage returns a minio bucket storage type MinioStorage struct { @@ -73,17 +76,49 @@ func (m *MinioStorage) Delete(path string) error { return m.client.RemoveObject(m.bucket, m.buildMinioPath(path)) } -//Get Presigned URL -func (m *MinioStorage) PresignedGetURL(path string, fileName string) (string,error) { +//Get Presigned URL for get object +func (m *MinioStorage) PresignedGetURL(path string, fileName string) (string, error) { // Set request parameters for content-disposition. reqParams := make(url.Values) - reqParams.Set("response-content-disposition", "attachment; filename=\"" + fileName + "\"") + reqParams.Set("response-content-disposition", "attachment; filename=\""+fileName+"\"") + + var preURL *url.URL + preURL, err := m.client.PresignedGetObject(m.bucket, m.buildMinioPath(path), PresignedGetUrlExpireTime, reqParams) + if err != nil { + return "", err + } + + return preURL.String(), nil +} +//Get Presigned URL for put object +func (m *MinioStorage) PresignedPutURL(path string) (string, error) { var preURL *url.URL - preURL,err := m.client.PresignedGetObject(m.bucket, m.buildMinioPath(path), PRESIGNED_URL_EXPIRE_TIME, reqParams) + preURL, err := m.client.PresignedPutObject(m.bucket, m.buildMinioPath(path), PresignedPutUrlExpireTime) if err != nil { - return "",err + return "", err + } + + return preURL.String(), nil +} + +//check if has the object +func (m *MinioStorage) HasObject(path string) (bool, error) { + hasObject := false + // Create a done channel to control 'ListObjects' go routine. + doneCh := make(chan struct{}) + + // Indicate to our routine to exit cleanly upon return. + defer close(doneCh) + + objectCh := m.client.ListObjects(m.bucket, m.buildMinioPath(path), false, doneCh) + for object := range objectCh { + if object.Err != nil { + return hasObject, object.Err + } + + hasObject = true } - return preURL.String(),nil + return hasObject, nil } diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 912454ac9..d06ec7208 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -22,6 +22,8 @@ type ObjectStorage interface { Open(path string) (io.ReadCloser, error) Delete(path string) error PresignedGetURL(path string, fileName string) (string, error) + PresignedPutURL(path string) (string, error) + HasObject(path string) (bool, error) } // Copy copys a file from source ObjectStorage to dest ObjectStorage diff --git a/modules/upload/filetype.go b/modules/upload/filetype.go index 2ab326d11..a79178719 100644 --- a/modules/upload/filetype.go +++ b/modules/upload/filetype.go @@ -31,6 +31,10 @@ func (err ErrFileTypeForbidden) Error() string { func VerifyAllowedContentType(buf []byte, allowedTypes []string) error { fileType := http.DetectContentType(buf) + return VerifyFileType(fileType, allowedTypes) +} + +func VerifyFileType(fileType string, allowedTypes []string) error { for _, t := range allowedTypes { t := strings.Trim(t, " ") diff --git a/routers/repo/attachment.go b/routers/repo/attachment.go index da429e06f..9ee167b0b 100644 --- a/routers/repo/attachment.go +++ b/routers/repo/attachment.go @@ -16,6 +16,8 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/upload" + + gouuid "github.com/satori/go.uuid" ) func RenderAttachmentSettings(ctx *context.Context) { @@ -210,3 +212,66 @@ func increaseDownloadCount(attach *models.Attachment, dataSet *models.Dataset) e return nil } + +// Get a presigned url for put object +func GetPresignedPutObjectURL(ctx *context.Context) { + if !setting.Attachment.Enabled { + ctx.Error(404, "attachment is not enabled") + return + } + + err := upload.VerifyFileType(ctx.Params("file_type"), strings.Split(setting.Attachment.AllowedTypes, ",")) + if err != nil { + ctx.Error(400, err.Error()) + return + } + + if setting.Attachment.StoreType == storage.MinioStorageType { + uuid := gouuid.NewV4().String() + url, err := storage.Attachments.PresignedPutURL(models.AttachmentRelativePath(uuid)) + if err != nil { + ctx.ServerError("PresignedPutURL", err) + return + } + + ctx.JSON(200, map[string]string{ + "uuid": uuid, + "url": url, + }) + } else { + ctx.Error(404, "storage type is not enabled") + return + } +} + +// AddAttachment response for add attachment record +func AddAttachment(ctx *context.Context) { + uuid := ctx.Query("uuid") + has, err := storage.Attachments.HasObject(models.AttachmentRelativePath(uuid)) + if err != nil { + ctx.ServerError("HasObject", err) + return + } + + if !has { + ctx.Error(404, "attachment has not been uploaded") + return + } + + _, err = models.InsertAttachment(&models.Attachment{ + UUID: uuid, + UploaderID: ctx.User.ID, + Name: ctx.Query("file_name"), + Size: ctx.QueryInt64("size"), + DatasetID: ctx.QueryInt64("dataset_id"), + }) + + if err != nil { + ctx.Error(500, fmt.Sprintf("InsertAttachment: %v", err)) + return + } + + ctx.JSON(200, map[string]string{ + "result_code": "0", + }) +} diff --git a/routers/repo/dataset.go b/routers/repo/dataset.go index 33d949702..879f779c6 100644 --- a/routers/repo/dataset.go +++ b/routers/repo/dataset.go @@ -1,13 +1,18 @@ package repo import ( + "net/url" "sort" + "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" + + gouuid "github.com/satori/go.uuid" ) const ( @@ -77,6 +82,18 @@ func DatasetIndex(ctx *context.Context) { ctx.Data["dataset"] = dataset ctx.Data["Attachments"] = attachments ctx.Data["IsOwner"] = true + uuid := gouuid.NewV4().String() + tmpUrl, err := storage.Attachments.PresignedPutURL(models.AttachmentRelativePath(uuid)) + if err != nil { + ctx.ServerError("PresignedPutURL", err) + } + preUrl, err := url.QueryUnescape(tmpUrl) + if err != nil { + ctx.ServerError("QueryUnescape", err) + } + + ctx.Data["uuid"] = uuid + ctx.Data["url"] = preUrl renderAttachmentSettings(ctx) ctx.HTML(200, tplIndex) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 1d821be6d..ea9dd7f65 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -518,6 +518,8 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/attachments", func() { m.Post("", repo.UploadAttachment) m.Post("/delete", repo.DeleteAttachment) + m.Get("/get_pre_url", repo.GetPresignedPutObjectURL) + m.Post("/add", repo.AddAttachment) m.Post("/private", repo.UpdatePublicAttachment) }, reqSignIn) diff --git a/templates/repo/datasets/dataset.tmpl b/templates/repo/datasets/dataset.tmpl index 29f90f9af..8ea4d9f64 100644 --- a/templates/repo/datasets/dataset.tmpl +++ b/templates/repo/datasets/dataset.tmpl @@ -2,7 +2,7 @@
-
+
diff --git a/web_src/js/index.js b/web_src/js/index.js index 05851707a..4169a61d8 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -2396,6 +2396,7 @@ $(document).ready(async () => { await createDropzone('#dataset', { url: $dataset.data('upload-url'), + method: 'put', headers: {'X-Csrf-Token': csrf}, maxFiles: $dataset.data('max-file'), maxFilesize: $dataset.data('max-size'), @@ -2411,13 +2412,19 @@ $(document).ready(async () => { this.on('sending', (_file, _xhr, formData) => { formData.append('dataset_id', $dataset.data('dataset-id')); }); - this.on('success', (file, data) => { - filenameDict[file.name] = data.uuid; - const input = $(``).val(data.uuid); - $('.files').append(input); - }); - this.on('queuecomplete', () => { - window.location.realod(); + this.on('success', (file, _data) => { + const uuid = $dataset.data('uuid'); + if ($dataset.data('add-url') && $dataset.data('csrf')) { + $.post($dataset.data('add-url'), { + uuid, + file_name: file.name, + size: file.size, + dataset_id: $dataset.data('dataset-id'), + _csrf: $dataset.data('csrf') + }).done(() => { + window.location.reload(); + }); + } }); this.on('removedfile', (file) => { if (file.name in filenameDict) {