Reviewed-by: berry <senluowanxiangt@gmail.com>tags/v1.21.12.1
| @@ -318,3 +318,13 @@ func (a *Attachment) LinkedDataSet() (*Dataset, error) { | |||||
| } | } | ||||
| return nil, nil | 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 | |||||
| } | |||||
| @@ -65,6 +65,14 @@ func (l *LocalStorage) Delete(path string) error { | |||||
| return os.Remove(p) | 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 | |||||
| } | } | ||||
| @@ -18,7 +18,10 @@ var ( | |||||
| _ ObjectStorage = &MinioStorage{} | _ 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 | // MinioStorage returns a minio bucket storage | ||||
| type MinioStorage struct { | type MinioStorage struct { | ||||
| @@ -73,17 +76,49 @@ func (m *MinioStorage) Delete(path string) error { | |||||
| return m.client.RemoveObject(m.bucket, m.buildMinioPath(path)) | 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. | // Set request parameters for content-disposition. | ||||
| reqParams := make(url.Values) | 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 | 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 { | 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 | |||||
| } | } | ||||
| @@ -22,6 +22,8 @@ type ObjectStorage interface { | |||||
| Open(path string) (io.ReadCloser, error) | Open(path string) (io.ReadCloser, error) | ||||
| Delete(path string) error | Delete(path string) error | ||||
| PresignedGetURL(path string, fileName string) (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 | // Copy copys a file from source ObjectStorage to dest ObjectStorage | ||||
| @@ -31,6 +31,10 @@ func (err ErrFileTypeForbidden) Error() string { | |||||
| func VerifyAllowedContentType(buf []byte, allowedTypes []string) error { | func VerifyAllowedContentType(buf []byte, allowedTypes []string) error { | ||||
| fileType := http.DetectContentType(buf) | fileType := http.DetectContentType(buf) | ||||
| return VerifyFileType(fileType, allowedTypes) | |||||
| } | |||||
| func VerifyFileType(fileType string, allowedTypes []string) error { | |||||
| for _, t := range allowedTypes { | for _, t := range allowedTypes { | ||||
| t := strings.Trim(t, " ") | t := strings.Trim(t, " ") | ||||
| @@ -16,6 +16,8 @@ import ( | |||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "code.gitea.io/gitea/modules/storage" | "code.gitea.io/gitea/modules/storage" | ||||
| "code.gitea.io/gitea/modules/upload" | "code.gitea.io/gitea/modules/upload" | ||||
| gouuid "github.com/satori/go.uuid" | |||||
| ) | ) | ||||
| func RenderAttachmentSettings(ctx *context.Context) { | func RenderAttachmentSettings(ctx *context.Context) { | ||||
| @@ -210,3 +212,66 @@ func increaseDownloadCount(attach *models.Attachment, dataSet *models.Dataset) e | |||||
| return nil | 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", | |||||
| }) | |||||
| } | |||||
| @@ -1,13 +1,18 @@ | |||||
| package repo | package repo | ||||
| import ( | import ( | ||||
| "net/url" | |||||
| "sort" | "sort" | ||||
| "code.gitea.io/gitea/modules/storage" | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/auth" | "code.gitea.io/gitea/modules/auth" | ||||
| "code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
| "code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| gouuid "github.com/satori/go.uuid" | |||||
| ) | ) | ||||
| const ( | const ( | ||||
| @@ -77,6 +82,18 @@ func DatasetIndex(ctx *context.Context) { | |||||
| ctx.Data["dataset"] = dataset | ctx.Data["dataset"] = dataset | ||||
| ctx.Data["Attachments"] = attachments | ctx.Data["Attachments"] = attachments | ||||
| ctx.Data["IsOwner"] = true | 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) | renderAttachmentSettings(ctx) | ||||
| ctx.HTML(200, tplIndex) | ctx.HTML(200, tplIndex) | ||||
| @@ -518,6 +518,8 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Group("/attachments", func() { | m.Group("/attachments", func() { | ||||
| m.Post("", repo.UploadAttachment) | m.Post("", repo.UploadAttachment) | ||||
| m.Post("/delete", repo.DeleteAttachment) | m.Post("/delete", repo.DeleteAttachment) | ||||
| m.Get("/get_pre_url", repo.GetPresignedPutObjectURL) | |||||
| m.Post("/add", repo.AddAttachment) | |||||
| m.Post("/private", repo.UpdatePublicAttachment) | m.Post("/private", repo.UpdatePublicAttachment) | ||||
| }, reqSignIn) | }, reqSignIn) | ||||
| @@ -2,7 +2,7 @@ | |||||
| <div class="field required dataset-files"> | <div class="field required dataset-files"> | ||||
| <label>{{.i18n.Tr "dataset.file"}}</label> | <label>{{.i18n.Tr "dataset.file"}}</label> | ||||
| <div class="files"></div> | <div class="files"></div> | ||||
| <div class="ui dropzone" id="dataset" data-upload-url="{{AppSubUrl}}/attachments" data-accepts="{{.AttachmentAllowedTypes}}" data-remove-url="{{AppSubUrl}}/attachments/delete" data-csrf="{{.CsrfToken}}" dataset-id={{.dataset.ID}} data-max-file="100" data-dataset-id="{{.dataset.ID}}" data-max-size="{{.AttachmentMaxSize}}" data-default-message="{{.i18n.Tr "dropzone.default_message"}}" data-invalid-input-type="{{.i18n.Tr "dropzone.invalid_input_type"}}" data-file-too-big="{{.i18n.Tr "dropzone.file_too_big"}}" data-remove-file="{{.i18n.Tr "dropzone.remove_file"}}"> | |||||
| <div class="ui dropzone" id="dataset" data-upload-url="{{.url}}" data-uuid="{{.uuid}}" data-add-url="{{AppSubUrl}}/attachments/add" data-accepts="{{.AttachmentAllowedTypes}}" data-remove-url="{{AppSubUrl}}/attachments/delete" data-csrf="{{.CsrfToken}}" dataset-id={{.dataset.ID}} data-max-file="100" data-dataset-id="{{.dataset.ID}}" data-max-size="{{.AttachmentMaxSize}}" data-default-message="{{.i18n.Tr "dropzone.default_message"}}" data-invalid-input-type="{{.i18n.Tr "dropzone.invalid_input_type"}}" data-file-too-big="{{.i18n.Tr "dropzone.file_too_big"}}" data-remove-file="{{.i18n.Tr "dropzone.remove_file"}}"> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -2396,6 +2396,7 @@ $(document).ready(async () => { | |||||
| await createDropzone('#dataset', { | await createDropzone('#dataset', { | ||||
| url: $dataset.data('upload-url'), | url: $dataset.data('upload-url'), | ||||
| method: 'put', | |||||
| headers: {'X-Csrf-Token': csrf}, | headers: {'X-Csrf-Token': csrf}, | ||||
| maxFiles: $dataset.data('max-file'), | maxFiles: $dataset.data('max-file'), | ||||
| maxFilesize: $dataset.data('max-size'), | maxFilesize: $dataset.data('max-size'), | ||||
| @@ -2411,13 +2412,19 @@ $(document).ready(async () => { | |||||
| this.on('sending', (_file, _xhr, formData) => { | this.on('sending', (_file, _xhr, formData) => { | ||||
| formData.append('dataset_id', $dataset.data('dataset-id')); | formData.append('dataset_id', $dataset.data('dataset-id')); | ||||
| }); | }); | ||||
| this.on('success', (file, data) => { | |||||
| filenameDict[file.name] = data.uuid; | |||||
| const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).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) => { | this.on('removedfile', (file) => { | ||||
| if (file.name in filenameDict) { | if (file.name in filenameDict) { | ||||