Reviewed-by: berry <senluowanxiangt@gmail.com>tags/v1.21.12.1
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| @@ -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, " ") | |||
| @@ -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", | |||
| }) | |||
| } | |||
| @@ -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) | |||
| @@ -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) | |||
| @@ -2,7 +2,7 @@ | |||
| <div class="field required dataset-files"> | |||
| <label>{{.i18n.Tr "dataset.file"}}</label> | |||
| <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> | |||
| @@ -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 = $(`<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) => { | |||
| if (file.name in filenameDict) { | |||