mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-24 16:05:00 +08:00
* Prepare file upload infrastructure for Data Retention. This commit prepares the file upload infrastructure for the data retention feature that is under construction. Changes are: * Move file management code to utils to allow access to it from jobs. * From now on, store all file uploads in a top level folder which is the date of the day on which they were uploaded. This commit is based on Harrison Healey's branch, but updated to work with the latest master. * Use NewAppError
347 lines
10 KiB
Go
347 lines
10 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See License.txt for license information.
|
|
|
|
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
l4g "github.com/alecthomas/log4go"
|
|
"github.com/gorilla/mux"
|
|
"github.com/mattermost/platform/app"
|
|
"github.com/mattermost/platform/model"
|
|
"github.com/mattermost/platform/utils"
|
|
)
|
|
|
|
const (
|
|
PREVIEW_IMAGE_TYPE = "image/jpeg"
|
|
THUMBNAIL_IMAGE_TYPE = "image/jpeg"
|
|
)
|
|
|
|
var UNSAFE_CONTENT_TYPES = [...]string{
|
|
"application/javascript",
|
|
"application/ecmascript",
|
|
"text/javascript",
|
|
"text/ecmascript",
|
|
"application/x-javascript",
|
|
"text/html",
|
|
}
|
|
|
|
func InitFile() {
|
|
l4g.Debug(utils.T("api.file.init.debug"))
|
|
|
|
BaseRoutes.TeamFiles.Handle("/upload", ApiUserRequired(uploadFile)).Methods("POST")
|
|
|
|
BaseRoutes.NeedFile.Handle("/get", ApiUserRequiredTrustRequester(getFile)).Methods("GET")
|
|
BaseRoutes.NeedFile.Handle("/get_thumbnail", ApiUserRequiredTrustRequester(getFileThumbnail)).Methods("GET")
|
|
BaseRoutes.NeedFile.Handle("/get_preview", ApiUserRequiredTrustRequester(getFilePreview)).Methods("GET")
|
|
BaseRoutes.NeedFile.Handle("/get_info", ApiUserRequired(getFileInfo)).Methods("GET")
|
|
BaseRoutes.NeedFile.Handle("/get_public_link", ApiUserRequired(getPublicLink)).Methods("GET")
|
|
|
|
BaseRoutes.Public.Handle("/files/{file_id:[A-Za-z0-9]+}/get", ApiAppHandlerTrustRequesterIndependent(getPublicFile)).Methods("GET")
|
|
BaseRoutes.Public.Handle("/files/get/{team_id:[A-Za-z0-9]+}/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:(?:[A-Za-z0-9]+/)?.+(?:\\.[A-Za-z0-9]{3,})?}", ApiAppHandlerTrustRequesterIndependent(getPublicFileOld)).Methods("GET")
|
|
}
|
|
|
|
func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if !*utils.Cfg.FileSettings.EnableFileAttachments {
|
|
c.Err = model.NewAppError("uploadFile", "api.file.attachments.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
return
|
|
}
|
|
|
|
if r.ContentLength > *utils.Cfg.FileSettings.MaxFileSize {
|
|
c.Err = model.NewLocAppError("uploadFile", "api.file.upload_file.too_large.app_error", nil, "")
|
|
c.Err.StatusCode = http.StatusRequestEntityTooLarge
|
|
return
|
|
}
|
|
|
|
if err := r.ParseMultipartForm(*utils.Cfg.FileSettings.MaxFileSize); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
m := r.MultipartForm
|
|
|
|
props := m.Value
|
|
if len(props["channel_id"]) == 0 {
|
|
c.SetInvalidParam("uploadFile", "channel_id")
|
|
return
|
|
}
|
|
channelId := props["channel_id"][0]
|
|
if len(channelId) == 0 {
|
|
c.SetInvalidParam("uploadFile", "channel_id")
|
|
return
|
|
}
|
|
|
|
if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_UPLOAD_FILE) {
|
|
c.SetPermissionError(model.PERMISSION_UPLOAD_FILE)
|
|
return
|
|
}
|
|
|
|
resStruct, err := app.UploadFiles(c.TeamId, channelId, c.Session.UserId, m.File["files"], m.Value["client_ids"])
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
w.Write([]byte(resStruct.ToJson()))
|
|
}
|
|
|
|
func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
info, err := getFileInfoForRequest(c, r, true)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if data, err := utils.ReadFile(info.Path); err != nil {
|
|
c.Err = err
|
|
c.Err.StatusCode = http.StatusNotFound
|
|
} else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
}
|
|
|
|
func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
info, err := getFileInfoForRequest(c, r, true)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if info.ThumbnailPath == "" {
|
|
c.Err = model.NewLocAppError("getFileThumbnail", "api.file.get_file_thumbnail.no_thumbnail.app_error", nil, "file_id="+info.Id)
|
|
c.Err.StatusCode = http.StatusBadRequest
|
|
return
|
|
}
|
|
|
|
if data, err := utils.ReadFile(info.ThumbnailPath); err != nil {
|
|
c.Err = err
|
|
c.Err.StatusCode = http.StatusNotFound
|
|
} else if err := writeFileResponse(info.Name, THUMBNAIL_IMAGE_TYPE, data, w, r); err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
}
|
|
|
|
func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
info, err := getFileInfoForRequest(c, r, true)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if info.PreviewPath == "" {
|
|
c.Err = model.NewLocAppError("getFilePreview", "api.file.get_file_preview.no_preview.app_error", nil, "file_id="+info.Id)
|
|
c.Err.StatusCode = http.StatusBadRequest
|
|
return
|
|
}
|
|
|
|
if data, err := utils.ReadFile(info.PreviewPath); err != nil {
|
|
c.Err = err
|
|
c.Err.StatusCode = http.StatusNotFound
|
|
} else if err := writeFileResponse(info.Name, PREVIEW_IMAGE_TYPE, data, w, r); err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
}
|
|
|
|
func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
info, err := getFileInfoForRequest(c, r, true)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Cache-Control", "max-age=2592000, public")
|
|
|
|
w.Write([]byte(info.ToJson()))
|
|
}
|
|
|
|
func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if !utils.Cfg.FileSettings.EnablePublicLink {
|
|
c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_disabled.app_error", nil, "")
|
|
c.Err.StatusCode = http.StatusNotImplemented
|
|
return
|
|
}
|
|
|
|
info, err := getFileInfoForRequest(c, r, false)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
hash := r.URL.Query().Get("h")
|
|
|
|
if len(hash) > 0 {
|
|
correctHash := app.GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
|
|
|
|
if hash != correctHash {
|
|
c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "")
|
|
c.Err.StatusCode = http.StatusBadRequest
|
|
return
|
|
}
|
|
} else {
|
|
c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "")
|
|
c.Err.StatusCode = http.StatusBadRequest
|
|
return
|
|
}
|
|
|
|
if data, err := utils.ReadFile(info.Path); err != nil {
|
|
c.Err = err
|
|
c.Err.StatusCode = http.StatusNotFound
|
|
} else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
}
|
|
|
|
func getFileInfoForRequest(c *Context, r *http.Request, requireFileVisible bool) (*model.FileInfo, *model.AppError) {
|
|
if len(utils.Cfg.FileSettings.DriverName) == 0 {
|
|
return nil, model.NewAppError("getFileInfoForRequest", "api.file.get_info_for_request.storage.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
params := mux.Vars(r)
|
|
|
|
fileId := params["file_id"]
|
|
if len(fileId) != 26 {
|
|
return nil, NewInvalidParamError("getFileInfoForRequest", "file_id")
|
|
}
|
|
|
|
info, err := app.GetFileInfo(fileId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// only let users access files visible in a channel, unless they're the one who uploaded the file
|
|
if info.CreatorId != c.Session.UserId {
|
|
if len(info.PostId) == 0 {
|
|
err := model.NewLocAppError("getFileInfoForRequest", "api.file.get_file_info_for_request.no_post.app_error", nil, "file_id="+fileId)
|
|
err.StatusCode = http.StatusBadRequest
|
|
return nil, err
|
|
}
|
|
|
|
if requireFileVisible {
|
|
if !app.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) {
|
|
c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
|
|
return nil, c.Err
|
|
}
|
|
}
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func getPublicFileOld(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if len(utils.Cfg.FileSettings.DriverName) == 0 {
|
|
c.Err = model.NewLocAppError("getPublicFile", "api.file.get_public_file_old.storage.app_error", nil, "")
|
|
c.Err.StatusCode = http.StatusNotImplemented
|
|
return
|
|
} else if !utils.Cfg.FileSettings.EnablePublicLink {
|
|
c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_disabled.app_error", nil, "")
|
|
c.Err.StatusCode = http.StatusNotImplemented
|
|
return
|
|
}
|
|
|
|
params := mux.Vars(r)
|
|
|
|
teamId := params["team_id"]
|
|
channelId := params["channel_id"]
|
|
userId := params["user_id"]
|
|
filename := params["filename"]
|
|
|
|
hash := r.URL.Query().Get("h")
|
|
|
|
if len(hash) > 0 {
|
|
correctHash := app.GeneratePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
|
|
|
|
if hash != correctHash {
|
|
c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "")
|
|
c.Err.StatusCode = http.StatusBadRequest
|
|
return
|
|
}
|
|
} else {
|
|
c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "")
|
|
c.Err.StatusCode = http.StatusBadRequest
|
|
return
|
|
}
|
|
|
|
path := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + filename
|
|
|
|
var info *model.FileInfo
|
|
if result := <-app.Srv.Store.FileInfo().GetByPath(path); result.Err != nil {
|
|
c.Err = result.Err
|
|
return
|
|
} else {
|
|
info = result.Data.(*model.FileInfo)
|
|
}
|
|
|
|
if len(info.PostId) == 0 {
|
|
c.Err = model.NewLocAppError("getPublicFileOld", "api.file.get_public_file_old.no_post.app_error", nil, "file_id="+info.Id)
|
|
c.Err.StatusCode = http.StatusBadRequest
|
|
return
|
|
}
|
|
|
|
if data, err := utils.ReadFile(info.Path); err != nil {
|
|
c.Err = err
|
|
c.Err.StatusCode = http.StatusNotFound
|
|
} else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
}
|
|
|
|
func writeFileResponse(filename string, contentType string, bytes []byte, w http.ResponseWriter, r *http.Request) *model.AppError {
|
|
w.Header().Set("Cache-Control", "max-age=2592000, private")
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(bytes)))
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
} else {
|
|
for _, unsafeContentType := range UNSAFE_CONTENT_TYPES {
|
|
if strings.HasPrefix(contentType, unsafeContentType) {
|
|
contentType = "text/plain"
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", contentType)
|
|
|
|
w.Header().Set("Content-Disposition", "attachment;filename=\""+filename+"\"; filename*=UTF-8''"+url.QueryEscape(filename))
|
|
|
|
// prevent file links from being embedded in iframes
|
|
w.Header().Set("X-Frame-Options", "DENY")
|
|
w.Header().Set("Content-Security-Policy", "Frame-ancestors 'none'")
|
|
|
|
w.Write(bytes)
|
|
|
|
return nil
|
|
}
|
|
|
|
func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if !utils.Cfg.FileSettings.EnablePublicLink {
|
|
c.Err = model.NewLocAppError("getPublicLink", "api.file.get_public_link.disabled.app_error", nil, "")
|
|
c.Err.StatusCode = http.StatusNotImplemented
|
|
return
|
|
}
|
|
|
|
info, err := getFileInfoForRequest(c, r, true)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if len(info.PostId) == 0 {
|
|
c.Err = model.NewLocAppError("getPublicLink", "api.file.get_public_link.no_post.app_error", nil, "file_id="+info.Id)
|
|
c.Err.StatusCode = http.StatusBadRequest
|
|
return
|
|
}
|
|
|
|
w.Write([]byte(model.StringToJson(app.GeneratePublicLinkV3(c.GetSiteURLHeader(), info))))
|
|
}
|