Reviewed-by: berry <senluowanxiangt@gmail.com>master
@@ -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) { | ||||