PLT-???? Prepare file upload infrastructure for Data Retention. (#7266)

* 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
Этот коммит содержится в:
George Goldberg
2017-08-25 15:38:13 +01:00
коммит произвёл Harrison Healey
родитель 99acf61068
Коммит 50fc6e1e9e
20 изменённых файлов: 682 добавлений и 303 удалений

Просмотреть файл

@@ -266,7 +266,7 @@ func TestDeleteEmoji(t *testing.T) {
func createTestEmoji(t *testing.T, emoji *model.Emoji, imageData []byte) *model.Emoji {
emoji = store.Must(app.Srv.Store.Emoji().Save(emoji)).(*model.Emoji)
if err := app.WriteFile(imageData, "emoji/"+emoji.Id+"/image"); err != nil {
if err := utils.WriteFile(imageData, "emoji/"+emoji.Id+"/image"); err != nil {
store.Must(app.Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix()))
t.Fatalf("failed to write image: %v", err.Error())
}

Просмотреть файл

@@ -96,7 +96,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if data, err := app.ReadFile(info.Path); err != nil {
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 {
@@ -118,7 +118,7 @@ func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if data, err := app.ReadFile(info.ThumbnailPath); err != nil {
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 {
@@ -140,7 +140,7 @@ func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if data, err := app.ReadFile(info.PreviewPath); err != nil {
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 {
@@ -190,7 +190,7 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if data, err := app.ReadFile(info.Path); err != nil {
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 {
@@ -285,7 +285,7 @@ func getPublicFileOld(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if data, err := app.ReadFile(info.Path); err != nil {
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 {

Просмотреть файл

@@ -81,20 +81,22 @@ func TestUploadFile(t *testing.T) {
t.Fatal("file preview path should be set in database")
}
date := time.Now().Format("20060102")
// This also makes sure that the relative path provided above is sanitized out
expectedPath := fmt.Sprintf("teams/%v/channels/%v/users/%v/%v/test.png", team.Id, channel.Id, user.Id, info.Id)
expectedPath := fmt.Sprintf("%v/teams/%v/channels/%v/users/%v/%v/test.png", date, team.Id, channel.Id, user.Id, info.Id)
if info.Path != expectedPath {
t.Logf("file is saved in %v", info.Path)
t.Fatalf("file should've been saved in %v", expectedPath)
}
expectedThumbnailPath := fmt.Sprintf("teams/%v/channels/%v/users/%v/%v/test_thumb.jpg", team.Id, channel.Id, user.Id, info.Id)
expectedThumbnailPath := fmt.Sprintf("%v/teams/%v/channels/%v/users/%v/%v/test_thumb.jpg", date, team.Id, channel.Id, user.Id, info.Id)
if info.ThumbnailPath != expectedThumbnailPath {
t.Logf("file thumbnail is saved in %v", info.ThumbnailPath)
t.Fatalf("file thumbnail should've been saved in %v", expectedThumbnailPath)
}
expectedPreviewPath := fmt.Sprintf("teams/%v/channels/%v/users/%v/%v/test_preview.jpg", team.Id, channel.Id, user.Id, info.Id)
expectedPreviewPath := fmt.Sprintf("%v/teams/%v/channels/%v/users/%v/%v/test_preview.jpg", date, team.Id, channel.Id, user.Id, info.Id)
if info.PreviewPath != expectedPreviewPath {
t.Logf("file preview is saved in %v", info.PreviewPath)
t.Fatalf("file preview should've been saved in %v", expectedPreviewPath)
@@ -466,7 +468,6 @@ func TestGetPublicFileOld(t *testing.T) {
utils.Cfg.FileSettings.EnablePublicLink = true
*utils.Cfg.FileSettings.PublicLinkSalt = model.NewId()
Client := th.BasicClient
channel := th.BasicChannel
var fileId string
@@ -474,7 +475,16 @@ func TestGetPublicFileOld(t *testing.T) {
if err != nil {
t.Fatal(err)
} else {
fileId = Client.MustGeneric(Client.UploadPostAttachment(data, channel.Id, "test.png")).(*model.FileUploadResponse).FileInfos[0].Id
//fileId = Client.MustGeneric(Client.UploadPostAttachment(data, channel.Id, "test.png")).(*model.FileUploadResponse).FileInfos[0].Id
fileId = model.NewId()
fileInfo := model.FileInfo{
Id: fileId,
CreateAt: model.GetMillis(),
CreatorId: th.BasicUser.Id,
Path: fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", th.BasicTeam.Id, channel.Id, th.BasicUser.Id, fileId, "test.png"),
}
store.Must(app.Srv.Store.FileInfo().Save(&fileInfo))
uploadFileOld(t, data, fmt.Sprintf("data/teams/%s/channels/%s/users/%s/%s", th.BasicTeam.Id, channel.Id, th.BasicUser.Id, fileId), "test.png")
}
// Hacky way to assign file to a post (usually would be done by CreatePost call)
@@ -619,7 +629,9 @@ func TestMigrateFilenamesToFileInfos(t *testing.T) {
t.Fatal(err)
} else {
fileId1 = Client.MustGeneric(Client.UploadPostAttachment(data, channel1.Id, "test.png")).(*model.FileUploadResponse).FileInfos[0].Id
uploadFileOld(t, data, fmt.Sprintf("data/teams/%s/channels/%s/users/%s/%s", th.BasicTeam.Id, channel1.Id, user1.Id, fileId1), "test.png")
fileId2 = Client.MustGeneric(Client.UploadPostAttachment(data, channel1.Id, "test.png")).(*model.FileUploadResponse).FileInfos[0].Id
uploadFileOld(t, data, fmt.Sprintf("data/teams/%s/channels/%s/users/%s/%s", th.BasicTeam.Id, channel1.Id, user1.Id, fileId2), "test.png")
}
// Bypass the Client whenever possible since we're trying to simulate a pre-3.5 post
@@ -686,6 +698,25 @@ func TestMigrateFilenamesToFileInfos(t *testing.T) {
}
}
func uploadFileOld(t *testing.T, data []byte, dest string, filename string) {
os.MkdirAll(dest, os.ModePerm)
eFile, err := os.Create(dest + "/" + filename)
if err != nil {
t.Fatal(err)
}
defer eFile.Close()
_, err = io.Copy(eFile, bytes.NewReader(data)) // first var shows number of bytes
if err != nil {
t.Fatal(err)
}
err = eFile.Sync()
if err != nil {
t.Fatal(err)
}
}
func TestFindTeamIdForFilename(t *testing.T) {
th := Setup().InitBasic()
@@ -717,9 +748,11 @@ func TestFindTeamIdForFilename(t *testing.T) {
t.Fatal(err)
} else {
fileId1 = Client.MustGeneric(Client.UploadPostAttachment(data, channel1.Id, "test.png")).(*model.FileUploadResponse).FileInfos[0].Id
uploadFileOld(t, data, fmt.Sprintf("data/teams/%s/channels/%s/users/%s/%s", team1.Id, channel1.Id, user1.Id, fileId1), "test.png")
Client.SetTeamId(team2.Id)
fileId2 = Client.MustGeneric(Client.UploadPostAttachment(data, channel2.Id, "test.png")).(*model.FileUploadResponse).FileInfos[0].Id
uploadFileOld(t, data, fmt.Sprintf("data/teams/%s/channels/%s/users/%s/%s", team2.Id, channel2.Id, user1.Id, fileId2), "test.png")
Client.SetTeamId(team1.Id)
}
@@ -732,6 +765,7 @@ func TestFindTeamIdForFilename(t *testing.T) {
})).(*model.Post)
if teamId := app.FindTeamIdForFilename(post1, post1.Filenames[0]); teamId != team1.Id {
t.Log(teamId)
t.Fatal("file should've been found under team1")
}
@@ -773,6 +807,7 @@ func TestGetInfoForFilename(t *testing.T) {
t.Fatal(err)
} else {
fileId1 = Client.MustGeneric(Client.UploadPostAttachment(data, channel1.Id, "test.png")).(*model.FileUploadResponse).FileInfos[0].Id
uploadFileOld(t, data, fmt.Sprintf("data/teams/%s/channels/%s/users/%s/%s", team1.Id, channel1.Id, user1.Id, fileId1), "test.png")
path = store.Must(app.Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).Path
thumbnailPath = store.Must(app.Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).ThumbnailPath
previewPath = store.Must(app.Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).PreviewPath
@@ -786,6 +821,8 @@ func TestGetInfoForFilename(t *testing.T) {
Filenames: []string{fmt.Sprintf("/%s/%s/%s/%s", channel1.Id, user1.Id, fileId1, "test.png")},
})).(*model.Post)
date := time.Now().Format("20060102")
if info := app.GetInfoForFilename(post1, team1.Id, post1.Filenames[0]); info == nil {
t.Fatal("info shouldn't be nil")
} else if info.Id == "" {
@@ -794,11 +831,11 @@ func TestGetInfoForFilename(t *testing.T) {
t.Fatal("incorrect user id")
} else if info.PostId != post1.Id {
t.Fatal("incorrect user id")
} else if info.Path != path {
} else if fmt.Sprintf("%s/%s", date, info.Path) != path {
t.Fatal("incorrect path")
} else if info.ThumbnailPath != thumbnailPath {
} else if fmt.Sprintf("%s/%s", date, info.ThumbnailPath) != thumbnailPath {
t.Fatal("incorrect thumbnail path")
} else if info.PreviewPath != previewPath {
} else if fmt.Sprintf("%s/%s", date, info.PreviewPath) != previewPath {
t.Fatal("incorrect preview path")
} else if info.Name != "test.png" {
t.Fatal("incorrect name")

Просмотреть файл

@@ -123,7 +123,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
data, err := app.ReadFile(info.Path)
data, err := utils.ReadFile(info.Path)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
@@ -165,7 +165,7 @@ func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if data, err := app.ReadFile(info.ThumbnailPath); err != nil {
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, forceDownload, w, r); err != nil {
@@ -237,7 +237,7 @@ func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if data, err := app.ReadFile(info.PreviewPath); err != nil {
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, forceDownload, w, r); err != nil {
@@ -299,7 +299,7 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if data, err := app.ReadFile(info.Path); err != nil {
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, true, w, r); err != nil {

Просмотреть файл

@@ -71,20 +71,22 @@ func TestUploadFile(t *testing.T) {
t.Fatal("file preview path should be set in database")
}
date := time.Now().Format("20060102")
// This also makes sure that the relative path provided above is sanitized out
expectedPath := fmt.Sprintf("teams/%v/channels/%v/users/%v/%v/test.png", FILE_TEAM_ID, channel.Id, user.Id, info.Id)
expectedPath := fmt.Sprintf("%v/teams/%v/channels/%v/users/%v/%v/test.png", date, FILE_TEAM_ID, channel.Id, user.Id, info.Id)
if info.Path != expectedPath {
t.Logf("file is saved in %v", info.Path)
t.Fatalf("file should've been saved in %v", expectedPath)
}
expectedThumbnailPath := fmt.Sprintf("teams/%v/channels/%v/users/%v/%v/test_thumb.jpg", FILE_TEAM_ID, channel.Id, user.Id, info.Id)
expectedThumbnailPath := fmt.Sprintf("%v/teams/%v/channels/%v/users/%v/%v/test_thumb.jpg", date, FILE_TEAM_ID, channel.Id, user.Id, info.Id)
if info.ThumbnailPath != expectedThumbnailPath {
t.Logf("file thumbnail is saved in %v", info.ThumbnailPath)
t.Fatalf("file thumbnail should've been saved in %v", expectedThumbnailPath)
}
expectedPreviewPath := fmt.Sprintf("teams/%v/channels/%v/users/%v/%v/test_preview.jpg", FILE_TEAM_ID, channel.Id, user.Id, info.Id)
expectedPreviewPath := fmt.Sprintf("%v/teams/%v/channels/%v/users/%v/%v/test_preview.jpg", date, FILE_TEAM_ID, channel.Id, user.Id, info.Id)
if info.PreviewPath != expectedPreviewPath {
t.Logf("file preview is saved in %v", info.PreviewPath)
t.Fatalf("file preview should've been saved in %v", expectedPreviewPath)

Просмотреть файл

@@ -101,7 +101,7 @@ func UploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppErro
if err := gif.EncodeAll(newbuf, resized_gif); err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "", http.StatusBadRequest)
}
if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
if err := utils.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
return err
}
}
@@ -113,13 +113,13 @@ func UploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppErro
if err := png.Encode(newbuf, resized_image); err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "", http.StatusBadRequest)
}
if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
if err := utils.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
return err
}
}
}
} else {
if err := WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil {
if err := utils.WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil {
return err
}
}
@@ -159,7 +159,7 @@ func GetEmojiImage(emojiId string) (imageByte []byte, imageType string, err *mod
} else {
var img []byte
if data, err := ReadFile(getEmojiImagePath(emojiId)); err != nil {
if data, err := utils.ReadFile(getEmojiImagePath(emojiId)); err != nil {
return nil, "", model.NewAppError("getEmojiImage", "api.emoji.get_image.read.app_error", nil, err.Error(), http.StatusNotFound)
} else {
img = data
@@ -219,7 +219,7 @@ func imageToPaletted(img image.Image) *image.Paletted {
}
func deleteEmojiImage(id string) {
if err := MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
if err := utils.MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
l4g.Error("Failed to rename image when deleting emoji %v", id)
}
}

Просмотреть файл

@@ -14,23 +14,21 @@ import (
_ "image/gif"
"image/jpeg"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
l4g "github.com/alecthomas/log4go"
"github.com/disintegration/imaging"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
s3 "github.com/minio/minio-go"
"github.com/minio/minio-go/pkg/credentials"
"github.com/rwcarlsen/goexif/exif"
_ "golang.org/x/image/bmp"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
const (
@@ -57,222 +55,8 @@ const (
IMAGE_THUMBNAIL_PIXEL_WIDTH = 120
IMAGE_THUMBNAIL_PIXEL_HEIGHT = 100
IMAGE_PREVIEW_PIXEL_WIDTH = 1024
TEST_FILE_PATH = "/testfile"
)
// Similar to s3.New() but allows initialization of signature v2 or signature v4 client.
// If signV2 input is false, function always returns signature v4.
//
// Additionally this function also takes a user defined region, if set
// disables automatic region lookup.
func s3New(endpoint, accessKey, secretKey string, secure bool, signV2 bool, region string) (*s3.Client, error) {
var creds *credentials.Credentials
if signV2 {
creds = credentials.NewStatic(accessKey, secretKey, "", credentials.SignatureV2)
} else {
creds = credentials.NewStatic(accessKey, secretKey, "", credentials.SignatureV4)
}
return s3.NewWithCredentials(endpoint, creds, secure, region)
}
func TestFileConnection() *model.AppError {
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
secure := *utils.Cfg.FileSettings.AmazonS3SSL
signV2 := *utils.Cfg.FileSettings.AmazonS3SignV2
region := utils.Cfg.FileSettings.AmazonS3Region
bucket := utils.Cfg.FileSettings.AmazonS3Bucket
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return model.NewLocAppError("TestFileConnection", "Bad connection to S3 or minio.", nil, err.Error())
}
exists, err := s3Clnt.BucketExists(bucket)
if err != nil {
return model.NewLocAppError("TestFileConnection", "Error checking if bucket exists.", nil, err.Error())
}
if !exists {
l4g.Warn("Bucket specified does not exist. Attempting to create...")
err := s3Clnt.MakeBucket(bucket, region)
if err != nil {
l4g.Error("Unable to create bucket.")
return model.NewAppError("TestFileConnection", "Unable to create bucket", nil, err.Error(), http.StatusInternalServerError)
}
}
l4g.Info("Connection to S3 or minio is good. Bucket exists.")
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
f := []byte("testingwrite")
if err := writeFileLocally(f, utils.Cfg.FileSettings.Directory+TEST_FILE_PATH); err != nil {
return model.NewAppError("TestFileConnection", "Don't have permissions to write to local path specified or other error.", nil, err.Error(), http.StatusInternalServerError)
}
os.Remove(utils.Cfg.FileSettings.Directory + TEST_FILE_PATH)
l4g.Info("Able to write files to local storage.")
} else {
return model.NewLocAppError("TestFileConnection", "No file driver selected.", nil, "")
}
return nil
}
func ReadFile(path string) ([]byte, *model.AppError) {
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
secure := *utils.Cfg.FileSettings.AmazonS3SSL
signV2 := *utils.Cfg.FileSettings.AmazonS3SignV2
region := utils.Cfg.FileSettings.AmazonS3Region
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
}
bucket := utils.Cfg.FileSettings.AmazonS3Bucket
minioObject, err := s3Clnt.GetObject(bucket, path)
defer minioObject.Close()
if err != nil {
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
}
if f, err := ioutil.ReadAll(minioObject); err != nil {
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
} else {
return f, nil
}
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if f, err := ioutil.ReadFile(utils.Cfg.FileSettings.Directory + path); err != nil {
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.reading_local.app_error", nil, err.Error())
} else {
return f, nil
}
} else {
return nil, model.NewAppError("ReadFile", "api.file.read_file.configured.app_error", nil, "", http.StatusNotImplemented)
}
}
func MoveFile(oldPath, newPath string) *model.AppError {
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
secure := *utils.Cfg.FileSettings.AmazonS3SSL
signV2 := *utils.Cfg.FileSettings.AmazonS3SignV2
region := utils.Cfg.FileSettings.AmazonS3Region
encrypt := false
if *utils.Cfg.FileSettings.AmazonS3SSE && utils.IsLicensed() && *utils.License().Features.Compliance {
encrypt = true
}
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return model.NewLocAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error())
}
bucket := utils.Cfg.FileSettings.AmazonS3Bucket
source := s3.NewSourceInfo(bucket, oldPath, nil)
destination, err := s3.NewDestinationInfo(bucket, newPath, nil, CopyMetadata(encrypt))
if err != nil {
return model.NewLocAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error())
}
if err = s3Clnt.CopyObject(destination, source); err != nil {
return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
}
if err = s3Clnt.RemoveObject(bucket, oldPath); err != nil {
return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
}
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+newPath), 0774); err != nil {
return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
}
if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil {
return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
}
} else {
return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "")
}
return nil
}
func WriteFile(f []byte, path string) *model.AppError {
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
secure := *utils.Cfg.FileSettings.AmazonS3SSL
signV2 := *utils.Cfg.FileSettings.AmazonS3SignV2
region := utils.Cfg.FileSettings.AmazonS3Region
encrypt := false
if *utils.Cfg.FileSettings.AmazonS3SSE && utils.IsLicensed() && *utils.License().Features.Compliance {
encrypt = true
}
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
}
bucket := utils.Cfg.FileSettings.AmazonS3Bucket
ext := filepath.Ext(path)
metaData := S3Metadata(encrypt, "binary/octet-stream")
if model.IsFileExtImage(ext) {
metaData = S3Metadata(encrypt, model.GetImageMimeType(ext))
}
_, err = s3Clnt.PutObjectWithMetadata(bucket, path, bytes.NewReader(f), metaData, nil)
if err != nil {
return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
}
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if err := writeFileLocally(f, utils.Cfg.FileSettings.Directory+path); err != nil {
return err
}
} else {
return model.NewLocAppError("WriteFile", "api.file.write_file.configured.app_error", nil, "")
}
return nil
}
func writeFileLocally(f []byte, path string) *model.AppError {
if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
directory, _ := filepath.Abs(filepath.Dir(path))
return model.NewLocAppError("WriteFile", "api.file.write_file_locally.create_dir.app_error", nil, "directory="+directory+", err="+err.Error())
}
if err := ioutil.WriteFile(path, f, 0644); err != nil {
return model.NewLocAppError("WriteFile", "api.file.write_file_locally.writing.app_error", nil, err.Error())
}
return nil
}
func openFileWriteStream(path string) (io.Writer, *model.AppError) {
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.s3.app_error", nil, "")
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil {
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.creating_dir.app_error", nil, err.Error())
}
if fileHandle, err := os.Create(utils.Cfg.FileSettings.Directory + path); err != nil {
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.local_server.app_error", nil, err.Error())
} else {
fileHandle.Chmod(0644)
return fileHandle, nil
}
}
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.configured.app_error", nil, "")
}
func closeFileWriteStream(file io.Writer) {
file.(*os.File).Close()
}
func GetInfoForFilename(post *model.Post, teamId string, filename string) *model.FileInfo {
// Find the path from the Filename of the form /{channelId}/{userId}/{uid}/{nameWithExtension}
split := strings.SplitN(filename, "/", 5)
@@ -295,7 +79,7 @@ func GetInfoForFilename(post *model.Post, teamId string, filename string) *model
// Open the file and populate the fields of the FileInfo
var info *model.FileInfo
if data, err := ReadFile(path); err != nil {
if data, err := utils.ReadFile(path); err != nil {
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.file_not_found.error"), post.Id, filename, path, err)
return nil
} else {
@@ -337,7 +121,7 @@ func FindTeamIdForFilename(post *model.Post, filename string) string {
} else {
for _, team := range teams {
path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name)
if _, err := ReadFile(path); err == nil {
if _, err := utils.ReadFile(path); err == nil {
// Found the team that this file was posted from
return team.Id
}
@@ -484,7 +268,7 @@ func UploadFiles(teamId string, channelId string, userId string, fileHeaders []*
io.Copy(buf, file)
data := buf.Bytes()
info, err := DoUploadFile(teamId, channelId, userId, fileHeader.Filename, data)
info, err := DoUploadFile(time.Now(), teamId, channelId, userId, fileHeader.Filename, data)
if err != nil {
return nil, err
}
@@ -507,7 +291,7 @@ func UploadFiles(teamId string, channelId string, userId string, fileHeaders []*
return resStruct, nil
}
func DoUploadFile(teamId string, channelId string, userId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
func DoUploadFile(now time.Time, teamId string, channelId string, userId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
filename := filepath.Base(rawFilename)
info, err := model.GetInfoForBytes(filename, data)
@@ -519,7 +303,7 @@ func DoUploadFile(teamId string, channelId string, userId string, rawFilename st
info.Id = model.NewId()
info.CreatorId = userId
pathPrefix := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + info.Id + "/"
pathPrefix := now.Format("20060102") + "/teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + info.Id + "/"
info.Path = pathPrefix + filename
if info.IsImage() {
@@ -535,7 +319,7 @@ func DoUploadFile(teamId string, channelId string, userId string, rawFilename st
info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
}
if err := WriteFile(data, info.Path); err != nil {
if err := utils.WriteFile(data, info.Path); err != nil {
return nil, err
}
@@ -652,7 +436,7 @@ func generateThumbnailImage(img image.Image, thumbnailPath string, width int, he
return
}
if err := WriteFile(buf.Bytes(), thumbnailPath); err != nil {
if err := utils.WriteFile(buf.Bytes(), thumbnailPath); err != nil {
l4g.Error(utils.T("api.file.handle_images_forget.upload_thumb.error"), thumbnailPath, err)
return
}
@@ -674,7 +458,7 @@ func generatePreviewImage(img image.Image, previewPath string, width int) {
return
}
if err := WriteFile(buf.Bytes(), previewPath); err != nil {
if err := utils.WriteFile(buf.Bytes(), previewPath); err != nil {
l4g.Error(utils.T("api.file.handle_images_forget.upload_preview.error"), previewPath, err)
return
}
@@ -687,20 +471,3 @@ func GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) {
return result.Data.(*model.FileInfo), nil
}
}
func S3Metadata(encrypt bool, contentType string) map[string][]string {
metaData := make(map[string][]string)
if contentType != "" {
metaData["Content-Type"] = []string{"contentType"}
}
if encrypt {
metaData["x-amz-server-side-encryption"] = []string{"AES256"}
}
return metaData
}
func CopyMetadata(encrypt bool) map[string]string {
metaData := make(map[string]string)
metaData["x-amz-server-side-encryption"] = "AES256"
return metaData
}

Просмотреть файл

@@ -4,9 +4,12 @@
package app
import (
"fmt"
"testing"
"time"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
func TestGeneratePublicLinkHash(t *testing.T) {
@@ -31,3 +34,55 @@ func TestGeneratePublicLinkHash(t *testing.T) {
t.Fatal("hashes for the same file with different salts should not be equal")
}
}
func TestDoUploadFile(t *testing.T) {
Setup()
teamId := model.NewId()
channelId := model.NewId()
userId := model.NewId()
filename := "test"
data := []byte("abcd")
info1, err := DoUploadFile(time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamId, channelId, userId, filename, data)
if err != nil {
t.Fatal(err)
} else {
defer func() {
<-Srv.Store.FileInfo().PermanentDelete(info1.Id)
utils.RemoveFile(info1.Path)
}()
}
if info1.Path != fmt.Sprintf("20070204/teams/%v/channels/%v/users/%v/%v/%v", teamId, channelId, userId, info1.Id, filename) {
t.Fatal("stored file at incorrect path", info1.Path)
}
info2, err := DoUploadFile(time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamId, channelId, userId, filename, data)
if err != nil {
t.Fatal(err)
} else {
defer func() {
<-Srv.Store.FileInfo().PermanentDelete(info2.Id)
utils.RemoveFile(info2.Path)
}()
}
if info2.Path != fmt.Sprintf("20070204/teams/%v/channels/%v/users/%v/%v/%v", teamId, channelId, userId, info2.Id, filename) {
t.Fatal("stored file at incorrect path", info2.Path)
}
info3, err := DoUploadFile(time.Date(2008, 3, 5, 1, 2, 3, 4, time.Local), teamId, channelId, userId, filename, data)
if err != nil {
t.Fatal(err)
} else {
defer func() {
<-Srv.Store.FileInfo().PermanentDelete(info3.Id)
utils.RemoveFile(info3.Path)
}()
}
if info3.Path != fmt.Sprintf("20080305/teams/%v/channels/%v/users/%v/%v/%v", teamId, channelId, userId, info3.Id, filename) {
t.Fatal("stored file at incorrect path", info3.Path)
}
}

Просмотреть файл

@@ -12,9 +12,11 @@ import (
"regexp"
"strings"
"sync"
"time"
"unicode/utf8"
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -1486,12 +1488,12 @@ func OldImportChannel(channel *model.Channel) *model.Channel {
}
}
func OldImportFile(file io.Reader, teamId string, channelId string, userId string, fileName string) (*model.FileInfo, error) {
func OldImportFile(timestamp time.Time, file io.Reader, teamId string, channelId string, userId string, fileName string) (*model.FileInfo, error) {
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
data := buf.Bytes()
fileInfo, err := DoUploadFile(teamId, channelId, userId, fileName, data)
fileInfo, err := DoUploadFile(timestamp, teamId, channelId, userId, fileName, data)
if err != nil {
return nil, err
}

Просмотреть файл

@@ -401,7 +401,8 @@ func SlackUploadFile(sPost SlackPost, uploads map[string]*zip.File, teamId strin
}
defer openFile.Close()
uploadedFile, err := OldImportFile(openFile, teamId, channelId, userId, filepath.Base(file.Name))
timestamp := utils.TimeFromMillis(SlackConvertTimeStamp(sPost.TimeStamp))
uploadedFile, err := OldImportFile(timestamp, openFile, teamId, channelId, userId, filepath.Base(file.Name))
if err != nil {
l4g.Warn(utils.T("api.slackimport.slack_add_posts.upload_file_upload_failed.warn", map[string]interface{}{"FileId": sPost.File.Id, "Error": err.Error()}))
return nil, false

Просмотреть файл

@@ -758,7 +758,7 @@ func GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
} else {
path := "users/" + user.Id + "/profile.png"
if data, err := ReadFile(path); err != nil {
if data, err := utils.ReadFile(path); err != nil {
readFailed = true
if img, err = CreateProfileImage(user.Username, user.Id); err != nil {
@@ -766,7 +766,7 @@ func GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
}
if user.LastPictureUpdate == 0 {
if err := WriteFile(img, path); err != nil {
if err := utils.WriteFile(img, path); err != nil {
return nil, false, err
}
}
@@ -819,7 +819,7 @@ func SetProfileImage(userId string, imageData *multipart.FileHeader) *model.AppE
path := "users/" + userId + "/profile.png"
if err := WriteFile(buf.Bytes(), path); err != nil {
if err := utils.WriteFile(buf.Bytes(), path); err != nil {
return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.upload_profile.app_error", nil, "")
}

Просмотреть файл

@@ -67,7 +67,7 @@ func runServer(configFileLocation string) {
*utils.Cfg.ServiceSettings.EnableDeveloper = true
}
if err := app.TestFileConnection(); err != nil {
if err := utils.TestFileConnection(); err != nil {
l4g.Error("Problem with file storage settings: " + err.Error())
}

Просмотреть файл

@@ -1233,22 +1233,6 @@
"id": "api.file.move_file.rename.app_error",
"translation": "Unable to move file locally."
},
{
"id": "api.file.open_file_write_stream.configured.app_error",
"translation": "File storage not configured properly. Please configure for either S3 or local server file storage."
},
{
"id": "api.file.open_file_write_stream.creating_dir.app_error",
"translation": "Encountered an error creating the directory for the new file"
},
{
"id": "api.file.open_file_write_stream.local_server.app_error",
"translation": "Encountered an error writing to local server storage"
},
{
"id": "api.file.open_file_write_stream.s3.app_error",
"translation": "S3 is not supported."
},
{
"id": "api.file.read_file.configured.app_error",
"translation": "File storage not configured properly. Please configure for either S3 or local server file storage."
@@ -1261,6 +1245,30 @@
"id": "api.file.read_file.reading_local.app_error",
"translation": "Encountered an error reading from local server storage"
},
{
"id": "utils.file.remove_file.local.app_error",
"translation": "Encountered an error removing file from local server file storage."
},
{
"id": "utils.file.remove_file.s3.app_error",
"translation": "Encountered an error removing file from S3."
},
{
"id": "utils.file.remove_file.configured.app_error",
"translation": "File storage not configured properly. Please configure for either S3 or local server file storage."
},
{
"id": "utils.file.remove_directory.local.app_error",
"translation": "Encountered an error removing directory from local server file storage."
},
{
"id": "utils.file.remove_directory.s3.app_error",
"translation": "Encountered an error removing directory from S3."
},
{
"id": "utils.file.remove_directory.configured.app_error",
"translation": "File storage not configured properly. Please configure for either S3 or local server file storage."
},
{
"id": "api.file.upload_file.bad_parse.app_error",
"translation": "Unable to upload file. Header cannot be parsed."
@@ -5575,6 +5583,10 @@
"id": "store.sql_file_info.save_or_update.app_error",
"translation": "We couldn't save or update the file info"
},
{
"id": "store.sql_file_info.permanent_delete.app_error",
"translation": "We couldn't permanently delete the file info"
},
{
"id": "store.sql_job.delete.app_error",
"translation": "We couldn't delete the job"

Просмотреть файл

@@ -80,6 +80,9 @@ func (o *FileInfo) PreSave() {
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
}
if o.UpdateAt < o.CreateAt {
o.UpdateAt = o.CreateAt
}
}

Просмотреть файл

@@ -97,7 +97,7 @@ func (fs SqlFileInfoStore) Get(id string) StoreChannel {
if err == sql.ErrNoRows {
result.Err = model.NewAppError("SqlFileInfoStore.Get", "store.sql_file_info.get.app_error", nil, "id="+id+", "+err.Error(), http.StatusNotFound)
} else {
result.Err = model.NewLocAppError("SqlFileInfoStore.Get", "store.sql_file_info.get.app_error", nil, "id="+id+", "+err.Error())
result.Err = model.NewAppError("SqlFileInfoStore.Get", "store.sql_file_info.get.app_error", nil, "id="+id+", "+err.Error(), http.StatusInternalServerError)
}
} else {
result.Data = info
@@ -127,7 +127,7 @@ func (fs SqlFileInfoStore) GetByPath(path string) StoreChannel {
Path = :Path
AND DeleteAt = 0
LIMIT 1`, map[string]interface{}{"Path": path}); err != nil {
result.Err = model.NewLocAppError("SqlFileInfoStore.GetByPath", "store.sql_file_info.get_by_path.app_error", nil, "path="+path+", "+err.Error())
result.Err = model.NewAppError("SqlFileInfoStore.GetByPath", "store.sql_file_info.get_by_path.app_error", nil, "path="+path+", "+err.Error(), http.StatusInternalServerError)
} else {
result.Data = info
}
@@ -139,7 +139,7 @@ func (fs SqlFileInfoStore) GetByPath(path string) StoreChannel {
return storeChannel
}
func (s SqlFileInfoStore) InvalidateFileInfosForPostCache(postId string) {
func (fs SqlFileInfoStore) InvalidateFileInfosForPostCache(postId string) {
fileInfoCache.Remove(postId)
}
@@ -190,8 +190,8 @@ func (fs SqlFileInfoStore) GetForPost(postId string, readFromMaster bool, allowF
AND DeleteAt = 0
ORDER BY
CreateAt`, map[string]interface{}{"PostId": postId}); err != nil {
result.Err = model.NewLocAppError("SqlFileInfoStore.GetForPost",
"store.sql_file_info.get_for_post.app_error", nil, "post_id="+postId+", "+err.Error())
result.Err = model.NewAppError("SqlFileInfoStore.GetForPost",
"store.sql_file_info.get_for_post.app_error", nil, "post_id="+postId+", "+err.Error(), http.StatusInternalServerError)
} else {
if len(infos) > 0 {
fileInfoCache.AddWithExpiresInSecs(postId, infos, FILE_INFO_CACHE_SEC)
@@ -221,8 +221,8 @@ func (fs SqlFileInfoStore) AttachToPost(fileId, postId string) StoreChannel {
WHERE
Id = :Id
AND PostId = ''`, map[string]interface{}{"PostId": postId, "Id": fileId}); err != nil {
result.Err = model.NewLocAppError("SqlFileInfoStore.AttachToPost",
"store.sql_file_info.attach_to_post.app_error", nil, "post_id="+postId+", file_id="+fileId+", err="+err.Error())
result.Err = model.NewAppError("SqlFileInfoStore.AttachToPost",
"store.sql_file_info.attach_to_post.app_error", nil, "post_id="+postId+", file_id="+fileId+", err="+err.Error(), http.StatusInternalServerError)
}
storeChannel <- result
@@ -245,8 +245,8 @@ func (fs SqlFileInfoStore) DeleteForPost(postId string) StoreChannel {
DeleteAt = :DeleteAt
WHERE
PostId = :PostId`, map[string]interface{}{"DeleteAt": model.GetMillis(), "PostId": postId}); err != nil {
result.Err = model.NewLocAppError("SqlFileInfoStore.DeleteForPost",
"store.sql_file_info.delete_for_post.app_error", nil, "post_id="+postId+", err="+err.Error())
result.Err = model.NewAppError("SqlFileInfoStore.DeleteForPost",
"store.sql_file_info.delete_for_post.app_error", nil, "post_id="+postId+", err="+err.Error(), http.StatusInternalServerError)
} else {
result.Data = postId
}
@@ -257,3 +257,25 @@ func (fs SqlFileInfoStore) DeleteForPost(postId string) StoreChannel {
return storeChannel
}
func (fs SqlFileInfoStore) PermanentDelete(fileId string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
if _, err := fs.GetMaster().Exec(
`DELETE FROM
FileInfo
WHERE
Id = :FileId`, map[string]interface{}{"FileId": fileId}); err != nil {
result.Err = model.NewAppError("SqlFileInfoStore.PermanentDelete",
"store.sql_file_info.permanent_delete.app_error", nil, "file_id="+fileId+", err="+err.Error(), http.StatusInternalServerError)
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}

Просмотреть файл

@@ -25,6 +25,9 @@ func TestFileInfoSaveGet(t *testing.T) {
} else {
info = returned
}
defer func() {
<-store.FileInfo().PermanentDelete(info.Id)
}()
if result := <-store.FileInfo().Get(info.Id); result.Err != nil {
t.Fatal(result.Err)
@@ -43,6 +46,9 @@ func TestFileInfoSaveGet(t *testing.T) {
if result := <-store.FileInfo().Get(info2.Id); result.Err == nil {
t.Fatal("shouldn't have gotten deleted file")
}
defer func() {
<-store.FileInfo().PermanentDelete(info2.Id)
}()
}
func TestFileInfoSaveGetByPath(t *testing.T) {
@@ -60,6 +66,9 @@ func TestFileInfoSaveGetByPath(t *testing.T) {
} else {
info = returned
}
defer func() {
<-store.FileInfo().PermanentDelete(info.Id)
}()
if result := <-store.FileInfo().GetByPath(info.Path); result.Err != nil {
t.Fatal(result.Err)
@@ -78,6 +87,9 @@ func TestFileInfoSaveGetByPath(t *testing.T) {
if result := <-store.FileInfo().GetByPath(info2.Id); result.Err == nil {
t.Fatal("shouldn't have gotten deleted file")
}
defer func() {
<-store.FileInfo().PermanentDelete(info2.Id)
}()
}
func TestFileInfoGetForPost(t *testing.T) {
@@ -112,6 +124,9 @@ func TestFileInfoGetForPost(t *testing.T) {
for i, info := range infos {
infos[i] = Must(store.FileInfo().Save(info)).(*model.FileInfo)
defer func(id string) {
<-store.FileInfo().PermanentDelete(id)
}(infos[i].Id)
}
if result := <-store.FileInfo().GetForPost(postId, true, false); result.Err != nil {
@@ -143,6 +158,9 @@ func TestFileInfoAttachToPost(t *testing.T) {
CreatorId: userId,
Path: "file.txt",
})).(*model.FileInfo)
defer func() {
<-store.FileInfo().PermanentDelete(info1.Id)
}()
if len(info1.PostId) != 0 {
t.Fatal("file shouldn't have a PostId")
@@ -162,6 +180,9 @@ func TestFileInfoAttachToPost(t *testing.T) {
CreatorId: userId,
Path: "file.txt",
})).(*model.FileInfo)
defer func() {
<-store.FileInfo().PermanentDelete(info2.Id)
}()
if result := <-store.FileInfo().AttachToPost(info2.Id, postId); result.Err != nil {
t.Fatal(result.Err)
@@ -208,6 +229,9 @@ func TestFileInfoDeleteForPost(t *testing.T) {
for i, info := range infos {
infos[i] = Must(store.FileInfo().Save(info)).(*model.FileInfo)
defer func(id string) {
<-store.FileInfo().PermanentDelete(id)
}(infos[i].Id)
}
if result := <-store.FileInfo().DeleteForPost(postId); result.Err != nil {
@@ -218,3 +242,17 @@ func TestFileInfoDeleteForPost(t *testing.T) {
t.Fatal("shouldn't have returned any file infos")
}
}
func TestFileInfoPermanentDelete(t *testing.T) {
Setup()
info := Must(store.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Path: "file.txt",
})).(*model.FileInfo)
if result := <-store.FileInfo().PermanentDelete(info.Id); result.Err != nil {
t.Fatal(result.Err)
}
}

Просмотреть файл

@@ -386,6 +386,7 @@ type FileInfoStore interface {
InvalidateFileInfosForPostCache(postId string)
AttachToPost(fileId string, postId string) StoreChannel
DeleteForPost(postId string) StoreChannel
PermanentDelete(fileId string) StoreChannel
}
type ReactionStore interface {

313
utils/file.go Обычный файл
Просмотреть файл

@@ -0,0 +1,313 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package utils
import (
"bytes"
"io/ioutil"
"net/http"
"os"
"path/filepath"
l4g "github.com/alecthomas/log4go"
s3 "github.com/minio/minio-go"
"github.com/minio/minio-go/pkg/credentials"
"github.com/mattermost/platform/model"
)
const (
TEST_FILE_PATH = "/testfile"
)
// Similar to s3.New() but allows initialization of signature v2 or signature v4 client.
// If signV2 input is false, function always returns signature v4.
//
// Additionally this function also takes a user defined region, if set
// disables automatic region lookup.
func s3New(endpoint, accessKey, secretKey string, secure bool, signV2 bool, region string) (*s3.Client, error) {
var creds *credentials.Credentials
if signV2 {
creds = credentials.NewStatic(accessKey, secretKey, "", credentials.SignatureV2)
} else {
creds = credentials.NewStatic(accessKey, secretKey, "", credentials.SignatureV4)
}
return s3.NewWithCredentials(endpoint, creds, secure, region)
}
func TestFileConnection() *model.AppError {
if Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
endpoint := Cfg.FileSettings.AmazonS3Endpoint
accessKey := Cfg.FileSettings.AmazonS3AccessKeyId
secretKey := Cfg.FileSettings.AmazonS3SecretAccessKey
secure := *Cfg.FileSettings.AmazonS3SSL
signV2 := *Cfg.FileSettings.AmazonS3SignV2
region := Cfg.FileSettings.AmazonS3Region
bucket := Cfg.FileSettings.AmazonS3Bucket
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return model.NewLocAppError("TestFileConnection", "Bad connection to S3 or minio.", nil, err.Error())
}
exists, err := s3Clnt.BucketExists(bucket)
if err != nil {
return model.NewLocAppError("TestFileConnection", "Error checking if bucket exists.", nil, err.Error())
}
if !exists {
l4g.Warn("Bucket specified does not exist. Attempting to create...")
err := s3Clnt.MakeBucket(bucket, region)
if err != nil {
l4g.Error("Unable to create bucket.")
return model.NewAppError("TestFileConnection", "Unable to create bucket", nil, err.Error(), http.StatusInternalServerError)
}
}
l4g.Info("Connection to S3 or minio is good. Bucket exists.")
} else if Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
f := []byte("testingwrite")
if err := writeFileLocally(f, Cfg.FileSettings.Directory+TEST_FILE_PATH); err != nil {
return model.NewAppError("TestFileConnection", "Don't have permissions to write to local path specified or other error.", nil, err.Error(), http.StatusInternalServerError)
}
os.Remove(Cfg.FileSettings.Directory + TEST_FILE_PATH)
l4g.Info("Able to write files to local storage.")
} else {
return model.NewLocAppError("TestFileConnection", "No file driver selected.", nil, "")
}
return nil
}
func ReadFile(path string) ([]byte, *model.AppError) {
if Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
endpoint := Cfg.FileSettings.AmazonS3Endpoint
accessKey := Cfg.FileSettings.AmazonS3AccessKeyId
secretKey := Cfg.FileSettings.AmazonS3SecretAccessKey
secure := *Cfg.FileSettings.AmazonS3SSL
signV2 := *Cfg.FileSettings.AmazonS3SignV2
region := Cfg.FileSettings.AmazonS3Region
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
}
bucket := Cfg.FileSettings.AmazonS3Bucket
minioObject, err := s3Clnt.GetObject(bucket, path)
defer minioObject.Close()
if err != nil {
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
}
if f, err := ioutil.ReadAll(minioObject); err != nil {
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
} else {
return f, nil
}
} else if Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if f, err := ioutil.ReadFile(Cfg.FileSettings.Directory + path); err != nil {
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.reading_local.app_error", nil, err.Error())
} else {
return f, nil
}
} else {
return nil, model.NewAppError("ReadFile", "api.file.read_file.configured.app_error", nil, "", http.StatusNotImplemented)
}
}
func MoveFile(oldPath, newPath string) *model.AppError {
if Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
endpoint := Cfg.FileSettings.AmazonS3Endpoint
accessKey := Cfg.FileSettings.AmazonS3AccessKeyId
secretKey := Cfg.FileSettings.AmazonS3SecretAccessKey
secure := *Cfg.FileSettings.AmazonS3SSL
signV2 := *Cfg.FileSettings.AmazonS3SignV2
region := Cfg.FileSettings.AmazonS3Region
encrypt := false
if *Cfg.FileSettings.AmazonS3SSE && IsLicensed() && *License().Features.Compliance {
encrypt = true
}
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return model.NewLocAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error())
}
bucket := Cfg.FileSettings.AmazonS3Bucket
source := s3.NewSourceInfo(bucket, oldPath, nil)
destination, err := s3.NewDestinationInfo(bucket, newPath, nil, CopyMetadata(encrypt))
if err != nil {
return model.NewLocAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error())
}
if err = s3Clnt.CopyObject(destination, source); err != nil {
return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
}
if err = s3Clnt.RemoveObject(bucket, oldPath); err != nil {
return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
}
} else if Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if err := os.MkdirAll(filepath.Dir(Cfg.FileSettings.Directory+newPath), 0774); err != nil {
return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
}
if err := os.Rename(Cfg.FileSettings.Directory+oldPath, Cfg.FileSettings.Directory+newPath); err != nil {
return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
}
} else {
return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "")
}
return nil
}
func WriteFile(f []byte, path string) *model.AppError {
if Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
endpoint := Cfg.FileSettings.AmazonS3Endpoint
accessKey := Cfg.FileSettings.AmazonS3AccessKeyId
secretKey := Cfg.FileSettings.AmazonS3SecretAccessKey
secure := *Cfg.FileSettings.AmazonS3SSL
signV2 := *Cfg.FileSettings.AmazonS3SignV2
region := Cfg.FileSettings.AmazonS3Region
encrypt := false
if *Cfg.FileSettings.AmazonS3SSE && IsLicensed() && *License().Features.Compliance {
encrypt = true
}
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
}
bucket := Cfg.FileSettings.AmazonS3Bucket
ext := filepath.Ext(path)
metaData := S3Metadata(encrypt, "binary/octet-stream")
if model.IsFileExtImage(ext) {
metaData = S3Metadata(encrypt, model.GetImageMimeType(ext))
}
_, err = s3Clnt.PutObjectWithMetadata(bucket, path, bytes.NewReader(f), metaData, nil)
if err != nil {
return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
}
} else if Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if err := writeFileLocally(f, Cfg.FileSettings.Directory+path); err != nil {
return err
}
} else {
return model.NewLocAppError("WriteFile", "api.file.write_file.configured.app_error", nil, "")
}
return nil
}
func writeFileLocally(f []byte, path string) *model.AppError {
if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
directory, _ := filepath.Abs(filepath.Dir(path))
return model.NewLocAppError("WriteFile", "api.file.write_file_locally.create_dir.app_error", nil, "directory="+directory+", err="+err.Error())
}
if err := ioutil.WriteFile(path, f, 0644); err != nil {
return model.NewLocAppError("WriteFile", "api.file.write_file_locally.writing.app_error", nil, err.Error())
}
return nil
}
func RemoveFile(path string) *model.AppError {
if Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
endpoint := Cfg.FileSettings.AmazonS3Endpoint
accessKey := Cfg.FileSettings.AmazonS3AccessKeyId
secretKey := Cfg.FileSettings.AmazonS3SecretAccessKey
secure := *Cfg.FileSettings.AmazonS3SSL
signV2 := *Cfg.FileSettings.AmazonS3SignV2
region := Cfg.FileSettings.AmazonS3Region
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return model.NewLocAppError("RemoveFile", "utils.file.remove_file.s3.app_error", nil, err.Error())
}
bucket := Cfg.FileSettings.AmazonS3Bucket
if err := s3Clnt.RemoveObject(bucket, path); err != nil {
return model.NewLocAppError("RemoveFile", "utils.file.remove_file.s3.app_error", nil, err.Error())
}
} else if Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if err := os.Remove(Cfg.FileSettings.Directory + path); err != nil {
return model.NewLocAppError("RemoveFile", "utils.file.remove_file.local.app_error", nil, err.Error())
}
} else {
return model.NewLocAppError("RemoveFile", "utils.file.remove_file.configured.app_error", nil, "")
}
return nil
}
func getPathsFromObjectInfos(in <-chan s3.ObjectInfo) <-chan string {
out := make(chan string, 1)
go func() {
defer close(out)
for {
info, done := <-in
if !done {
break
}
out <- info.Key
}
}()
return out
}
func RemoveDirectory(path string) *model.AppError {
if Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
endpoint := Cfg.FileSettings.AmazonS3Endpoint
accessKey := Cfg.FileSettings.AmazonS3AccessKeyId
secretKey := Cfg.FileSettings.AmazonS3SecretAccessKey
secure := *Cfg.FileSettings.AmazonS3SSL
signV2 := *Cfg.FileSettings.AmazonS3SignV2
region := Cfg.FileSettings.AmazonS3Region
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return model.NewLocAppError("RemoveDirectory", "utils.file.remove_directory.s3.app_error", nil, err.Error())
}
doneCh := make(chan struct{})
bucket := Cfg.FileSettings.AmazonS3Bucket
for err := range s3Clnt.RemoveObjects(bucket, getPathsFromObjectInfos(s3Clnt.ListObjects(bucket, path, true, doneCh))) {
if err.Err != nil {
doneCh <- struct{}{}
return model.NewLocAppError("RemoveDirectory", "utils.file.remove_directory.s3.app_error", nil, err.Err.Error())
}
}
close(doneCh)
} else if Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if err := os.RemoveAll(Cfg.FileSettings.Directory + path); err != nil {
return model.NewLocAppError("RemoveDirectory", "utils.file.remove_directory.local.app_error", nil, err.Error())
}
} else {
return model.NewLocAppError("RemoveDirectory", "utils.file.remove_directory.configured.app_error", nil, "")
}
return nil
}
func S3Metadata(encrypt bool, contentType string) map[string][]string {
metaData := make(map[string][]string)
if contentType != "" {
metaData["Content-Type"] = []string{"contentType"}
}
if encrypt {
metaData["x-amz-server-side-encryption"] = []string{"AES256"}
}
return metaData
}
func CopyMetadata(encrypt bool) map[string]string {
metaData := make(map[string]string)
metaData["x-amz-server-side-encryption"] = "AES256"
return metaData
}

122
utils/file_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,122 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package utils
import (
"testing"
"github.com/mattermost/platform/model"
)
func TestReadWriteFile(t *testing.T) {
TranslationsPreInit()
LoadConfig("config.json")
InitTranslations(Cfg.LocalizationSettings)
b := []byte("test")
path := "tests/" + model.NewId()
if err := WriteFile(b, path); err != nil {
t.Fatal(err)
}
defer RemoveFile(path)
if read, err := ReadFile(path); err != nil {
t.Fatal(err)
} else if readString := string(read); readString != "test" {
t.Fatal("should've read back contents of file")
}
}
func TestMoveFile(t *testing.T) {
TranslationsPreInit()
LoadConfig("config.json")
InitTranslations(Cfg.LocalizationSettings)
b := []byte("test")
path1 := "tests/" + model.NewId()
path2 := "tests/" + model.NewId()
if err := WriteFile(b, path1); err != nil {
t.Fatal(err)
}
defer RemoveFile(path1)
if err := MoveFile(path1, path2); err != nil {
t.Fatal(err)
}
defer RemoveFile(path2)
if _, err := ReadFile(path1); err == nil {
t.Fatal("file should no longer exist at old path")
}
if _, err := ReadFile(path2); err != nil {
t.Fatal("file should exist at new path", err)
}
}
func TestRemoveFile(t *testing.T) {
TranslationsPreInit()
LoadConfig("config.json")
InitTranslations(Cfg.LocalizationSettings)
b := []byte("test")
path := "tests/" + model.NewId()
if err := WriteFile(b, path); err != nil {
t.Fatal(err)
}
if err := RemoveFile(path); err != nil {
t.Fatal(err)
}
if _, err := ReadFile(path); err == nil {
t.Fatal("should've removed file")
}
if err := WriteFile(b, "tests2/foo"); err != nil {
t.Fatal(err)
}
if err := WriteFile(b, "tests2/bar"); err != nil {
t.Fatal(err)
}
if err := WriteFile(b, "tests2/asdf"); err != nil {
t.Fatal(err)
}
if err := RemoveDirectory("tests2"); err != nil {
t.Fatal(err)
}
}
func TestRemoveDirectory(t *testing.T) {
TranslationsPreInit()
LoadConfig("config.json")
InitTranslations(Cfg.LocalizationSettings)
b := []byte("test")
if err := WriteFile(b, "tests2/foo"); err != nil {
t.Fatal(err)
}
if err := WriteFile(b, "tests2/bar"); err != nil {
t.Fatal(err)
}
if err := WriteFile(b, "tests2/aaa"); err != nil {
t.Fatal(err)
}
if err := RemoveDirectory("tests2"); err != nil {
t.Fatal(err)
}
if _, err := ReadFile("tests2/foo"); err == nil {
t.Fatal("should've removed file")
} else if _, err := ReadFile("tests2/bar"); err == nil {
t.Fatal("should've removed file")
} else if _, err := ReadFile("tests2/asdf"); err == nil {
t.Fatal("should've removed file")
}
}

Просмотреть файл

@@ -8,6 +8,10 @@ func MillisFromTime(t time.Time) int64 {
return t.UnixNano() / int64(time.Millisecond)
}
func TimeFromMillis(millis int64) time.Time {
return time.Unix(0, millis*int64(time.Millisecond))
}
func StartOfDay(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, t.Location())