PLT-3105 Files table migration (#4068)
* Implemented initial changes for files table * Removed *_benchmark_test.go files * Re-implemented GetPublicFile and added support for old path * Localization for files table * Moved file system code into utils package * Finished server-side changes and added initial upgrade script * Added getPostFiles api * Re-add Extension and HasPreviewImage fields to FileInfo * Removed unused translation * Fixed merge conflicts left over after permissions changes * Forced FileInfo.extension to be lower case * Changed FileUploadResponse to contain the FileInfos instead of FileIds * Fixed permissions on getFile* calls * Fixed notifications for file uploads * Added initial version of client code for files changes * Permanently added FileIds field to Post object and removed Post.HasFiles * Updated PostStore.Update to be usable in more circumstances * Re-added Filenames field and switched file migration to be entirely lazy-loaded * Increased max listener count for FileStore * Removed unused fileInfoCache * Moved file system code back into api * Removed duplicate test case * Fixed unit test running on ports other than 8065 * Renamed HasPermissionToPostContext to HasPermissionToChannelByPostContext * Refactored handleImages to make it more easily understandable * Renamed getPostFiles to getFileInfosForPost * Re-added pre-FileIds posts to analytics * Changed files to be saved as their ids as opposed to id/filename.ext * Renamed FileInfo.UserId to FileInfo.CreatorId * Fixed detection of language in CodePreview * Fixed switching between threads in the RHS not loading new files * Add serverside protection against a rare bug where the client sends the same file twice for a single post * Refactored the important parts of uploadFile api call into a function that can be called without a web context
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
a2deeed597
Коммит
8a0e649f98
@@ -33,7 +33,9 @@ type Routes struct {
|
||||
Commands *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/commands'
|
||||
Hooks *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/hooks'
|
||||
|
||||
Files *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/files'
|
||||
TeamFiles *mux.Router // 'api/v3/teams/{team_id:[A-Za-z0-9]+}/files'
|
||||
Files *mux.Router // 'api/v3/files'
|
||||
NeedFile *mux.Router // 'api/v3/files/{file_id:[A-Za-z0-9]+}'
|
||||
|
||||
OAuth *mux.Router // 'api/v3/oauth'
|
||||
|
||||
@@ -70,7 +72,9 @@ func InitApi() {
|
||||
BaseRoutes.Posts = BaseRoutes.NeedChannel.PathPrefix("/posts").Subrouter()
|
||||
BaseRoutes.NeedPost = BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
|
||||
BaseRoutes.Commands = BaseRoutes.NeedTeam.PathPrefix("/commands").Subrouter()
|
||||
BaseRoutes.Files = BaseRoutes.NeedTeam.PathPrefix("/files").Subrouter()
|
||||
BaseRoutes.TeamFiles = BaseRoutes.NeedTeam.PathPrefix("/files").Subrouter()
|
||||
BaseRoutes.Files = BaseRoutes.ApiRoot.PathPrefix("/files").Subrouter()
|
||||
BaseRoutes.NeedFile = BaseRoutes.Files.PathPrefix("/{file_id:[A-Za-z0-9]+}").Subrouter()
|
||||
BaseRoutes.Hooks = BaseRoutes.NeedTeam.PathPrefix("/hooks").Subrouter()
|
||||
BaseRoutes.OAuth = BaseRoutes.ApiRoot.PathPrefix("/oauth").Subrouter()
|
||||
BaseRoutes.Admin = BaseRoutes.ApiRoot.PathPrefix("/admin").Subrouter()
|
||||
|
||||
@@ -114,6 +114,42 @@ func HasPermissionToChannel(user *model.User, teamMember *model.TeamMember, chan
|
||||
return HasPermissionToTeam(user, teamMember, permission)
|
||||
}
|
||||
|
||||
func HasPermissionToChannelByPostContext(c *Context, postId string, permission *model.Permission) bool {
|
||||
cmc := Srv.Store.Channel().GetMemberForPost(postId, c.Session.UserId)
|
||||
|
||||
var channelRoles []string
|
||||
if cmcresult := <-cmc; cmcresult.Err == nil {
|
||||
channelMember := cmcresult.Data.(*model.ChannelMember)
|
||||
channelRoles = channelMember.GetRoles()
|
||||
|
||||
if CheckIfRolesGrantPermission(channelRoles, permission.Id) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
cc := Srv.Store.Channel().GetForPost(postId)
|
||||
if ccresult := <-cc; ccresult.Err == nil {
|
||||
channel := ccresult.Data.(*model.Channel)
|
||||
|
||||
if teamMember := c.Session.GetTeamByTeamId(channel.TeamId); teamMember != nil {
|
||||
roles := teamMember.GetRoles()
|
||||
|
||||
if CheckIfRolesGrantPermission(roles, permission.Id) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if HasPermissionToContext(c, permission) {
|
||||
return true
|
||||
}
|
||||
|
||||
c.Err = model.NewLocAppError("HasPermissionToChannelByPostContext", "api.context.permissions.app_error", nil, "userId="+c.Session.UserId+", "+"permission="+permission.Id+" channelRoles="+model.RoleIdsToString(channelRoles))
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return false
|
||||
}
|
||||
|
||||
func HasPermissionToUser(c *Context, userId string) bool {
|
||||
// You are the user (users autmaticly have permissions to themselves)
|
||||
if c.Session.UserId == userId {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -40,53 +39,31 @@ func NewAutoPostCreator(client *model.Client, channelid string) *AutoPostCreator
|
||||
}
|
||||
|
||||
func (cfg *AutoPostCreator) UploadTestFile() ([]string, bool) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
filename := cfg.ImageFilenames[utils.RandIntFromRange(utils.Range{0, len(cfg.ImageFilenames) - 1})]
|
||||
|
||||
part, err := writer.CreateFormFile("files", filename)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
path := utils.FindDir("web/static/images")
|
||||
file, err := os.Open(path + "/" + filename)
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(part, file)
|
||||
data := &bytes.Buffer{}
|
||||
_, err = io.Copy(data, file)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
field, err := writer.CreateFormField("channel_id")
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
_, err = field.Write([]byte(cfg.channelid))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
resp, appErr := cfg.client.UploadPostAttachment(body.Bytes(), writer.FormDataContentType())
|
||||
resp, appErr := cfg.client.UploadPostAttachment(data.Bytes(), cfg.channelid, filename)
|
||||
if appErr != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return resp.Data.(*model.FileUploadResponse).Filenames, true
|
||||
return []string{resp.FileInfos[0].Id}, true
|
||||
}
|
||||
|
||||
func (cfg *AutoPostCreator) CreateRandomPost() (*model.Post, bool) {
|
||||
var filenames []string
|
||||
var fileIds []string
|
||||
if cfg.HasImage {
|
||||
var err1 bool
|
||||
filenames, err1 = cfg.UploadTestFile()
|
||||
fileIds, err1 = cfg.UploadTestFile()
|
||||
if err1 == false {
|
||||
return nil, false
|
||||
}
|
||||
@@ -102,7 +79,7 @@ func (cfg *AutoPostCreator) CreateRandomPost() (*model.Post, bool) {
|
||||
post := &model.Post{
|
||||
ChannelId: cfg.channelid,
|
||||
Message: postText,
|
||||
Filenames: filenames}
|
||||
FileIds: fileIds}
|
||||
result, err2 := cfg.client.CreatePost(post)
|
||||
if err2 != nil {
|
||||
return nil, false
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/store"
|
||||
"github.com/mattermost/platform/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
NUM_CHANNELS = 140
|
||||
NUM_USERS = 40
|
||||
)
|
||||
|
||||
func BenchmarkCreateChannel(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
|
||||
channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
channelCreator.CreateTestChannels(utils.Range{NUM_CHANNELS, NUM_CHANNELS})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCreateDirectChannel(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
|
||||
userCreator := NewAutoUserCreator(th.BasicClient, th.BasicTeam)
|
||||
users, err := userCreator.CreateTestUsers(utils.Range{NUM_USERS, NUM_USERS})
|
||||
if err == false {
|
||||
b.Fatal("Could not create users")
|
||||
}
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := 0; j < NUM_USERS; j++ {
|
||||
th.BasicClient.CreateDirectChannel(users[j].Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUpdateChannel(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
|
||||
var (
|
||||
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
|
||||
CHANNEL_HEADER_LEN = 50
|
||||
)
|
||||
|
||||
channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam)
|
||||
channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE)
|
||||
if valid == false {
|
||||
b.Fatal("Unable to create test channels")
|
||||
}
|
||||
|
||||
for i := range channels {
|
||||
channels[i].Header = utils.RandString(CHANNEL_HEADER_LEN, utils.ALPHANUMERIC)
|
||||
}
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := range channels {
|
||||
if _, err := th.BasicClient.UpdateChannel(channels[j]); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetChannels(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
|
||||
var (
|
||||
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
|
||||
)
|
||||
|
||||
channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam)
|
||||
_, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE)
|
||||
if valid == false {
|
||||
b.Fatal("Unable to create test channels")
|
||||
}
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
th.BasicClient.Must(th.BasicClient.GetChannels(""))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetMoreChannels(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
|
||||
var (
|
||||
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
|
||||
)
|
||||
|
||||
channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam)
|
||||
_, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE)
|
||||
if valid == false {
|
||||
b.Fatal("Unable to create test channels")
|
||||
}
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
th.BasicClient.Must(th.BasicClient.GetMoreChannels(""))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJoinChannel(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
|
||||
var (
|
||||
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
|
||||
)
|
||||
|
||||
channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam)
|
||||
channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE)
|
||||
if valid == false {
|
||||
b.Fatal("Unable to create test channels")
|
||||
}
|
||||
|
||||
// Secondary test user to join channels created by primary test user
|
||||
user := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "That Guy", Password: "pwd"}
|
||||
user = th.BasicClient.Must(th.BasicClient.CreateUser(user, "")).Data.(*model.User)
|
||||
LinkUserToTeam(user, th.BasicTeam)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||
th.BasicClient.Login(user.Email, "pwd")
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := range channels {
|
||||
th.BasicClient.Must(th.BasicClient.JoinChannel(channels[j].Id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDeleteChannel(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
|
||||
var (
|
||||
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
|
||||
)
|
||||
|
||||
channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam)
|
||||
channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE)
|
||||
if valid == false {
|
||||
b.Fatal("Unable to create test channels")
|
||||
}
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := range channels {
|
||||
th.BasicClient.Must(th.BasicClient.DeleteChannel(channels[j].Id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetChannelExtraInfo(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
|
||||
var (
|
||||
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
|
||||
)
|
||||
|
||||
channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam)
|
||||
channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE)
|
||||
if valid == false {
|
||||
b.Fatal("Unable to create test channels")
|
||||
}
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := range channels {
|
||||
th.BasicClient.Must(th.BasicClient.GetChannelExtraInfo(channels[j].Id, -1, ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAddChannelMember(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
|
||||
var (
|
||||
NUM_USERS = 100
|
||||
NUM_USERS_RANGE = utils.Range{NUM_USERS, NUM_USERS}
|
||||
)
|
||||
|
||||
channel := &model.Channel{DisplayName: "Test Channel", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: th.BasicTeam.Id}
|
||||
channel = th.BasicClient.Must(th.BasicClient.CreateChannel(channel)).Data.(*model.Channel)
|
||||
|
||||
userCreator := NewAutoUserCreator(th.BasicClient, th.BasicTeam)
|
||||
users, valid := userCreator.CreateTestUsers(NUM_USERS_RANGE)
|
||||
if valid == false {
|
||||
b.Fatal("Unable to create test users")
|
||||
}
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := range users {
|
||||
if _, err := th.BasicClient.AddChannelMember(channel.Id, users[j].Id); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Is this benchmark failing? Raise your file ulimit! 2048 worked for me.
|
||||
func BenchmarkRemoveChannelMember(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
|
||||
var (
|
||||
NUM_USERS = 140
|
||||
NUM_USERS_RANGE = utils.Range{NUM_USERS, NUM_USERS}
|
||||
)
|
||||
|
||||
channel := &model.Channel{DisplayName: "Test Channel", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: th.BasicTeam.Id}
|
||||
channel = th.BasicClient.Must(th.BasicClient.CreateChannel(channel)).Data.(*model.Channel)
|
||||
|
||||
userCreator := NewAutoUserCreator(th.BasicClient, th.BasicTeam)
|
||||
users, valid := userCreator.CreateTestUsers(NUM_USERS_RANGE)
|
||||
if valid == false {
|
||||
b.Fatal("Unable to create test users")
|
||||
}
|
||||
|
||||
for i := range users {
|
||||
if _, err := th.BasicClient.AddChannelMember(channel.Id, users[i].Id); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := range users {
|
||||
if _, err := th.BasicClient.RemoveChannelMember(channel.Id, users[j].Id); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUpdateNotifyProps(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
|
||||
var (
|
||||
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
|
||||
)
|
||||
|
||||
channelCreator := NewAutoChannelCreator(th.BasicClient, th.BasicTeam)
|
||||
channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE)
|
||||
if valid == false {
|
||||
b.Fatal("Unable to create test channels")
|
||||
}
|
||||
|
||||
data := make([]map[string]string, len(channels))
|
||||
|
||||
for i := range data {
|
||||
newmap := map[string]string{
|
||||
"channel_id": channels[i].Id,
|
||||
"user_id": th.BasicUser.Id,
|
||||
"desktop": model.CHANNEL_NOTIFY_MENTION,
|
||||
"mark_unread": model.CHANNEL_MARK_UNREAD_MENTION,
|
||||
}
|
||||
data[i] = newmap
|
||||
}
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := range channels {
|
||||
th.BasicClient.Must(th.BasicClient.UpdateNotifyProps(data[j]))
|
||||
}
|
||||
}
|
||||
}
|
||||
719
api/file.go
719
api/file.go
@@ -16,6 +16,7 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -57,17 +58,19 @@ const (
|
||||
MaxImageSize = 6048 * 4032 // 24 megapixels, roughly 36MB as a raw image
|
||||
)
|
||||
|
||||
var fileInfoCache *utils.Cache = utils.NewLru(1000)
|
||||
|
||||
func InitFile() {
|
||||
l4g.Debug(utils.T("api.file.init.debug"))
|
||||
|
||||
BaseRoutes.Files.Handle("/upload", ApiUserRequired(uploadFile)).Methods("POST")
|
||||
BaseRoutes.Files.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiUserRequiredTrustRequester(getFile)).Methods("GET")
|
||||
BaseRoutes.Files.Handle("/get_info/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiUserRequired(getFileInfo)).Methods("GET")
|
||||
BaseRoutes.Files.Handle("/get_public_link", ApiUserRequired(getPublicLink)).Methods("POST")
|
||||
BaseRoutes.TeamFiles.Handle("/upload", ApiUserRequired(uploadFile)).Methods("POST")
|
||||
|
||||
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(getPublicFile)).Methods("GET")
|
||||
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) {
|
||||
@@ -83,8 +86,7 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err := r.ParseMultipartForm(*utils.Cfg.FileSettings.MaxFileSize)
|
||||
if err != nil {
|
||||
if err := r.ParseMultipartForm(*utils.Cfg.FileSettings.MaxFileSize); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -92,7 +94,6 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
m := r.MultipartForm
|
||||
|
||||
props := m.Value
|
||||
|
||||
if len(props["channel_id"]) == 0 {
|
||||
c.SetInvalidParam("uploadFile", "channel_id")
|
||||
return
|
||||
@@ -103,91 +104,108 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
files := m.File["files"]
|
||||
|
||||
resStruct := &model.FileUploadResponse{
|
||||
Filenames: []string{},
|
||||
ClientIds: []string{},
|
||||
}
|
||||
|
||||
imageNameList := []string{}
|
||||
imageDataList := [][]byte{}
|
||||
|
||||
if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_UPLOAD_FILE) {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range files {
|
||||
file, err := files[i].Open()
|
||||
resStruct := &model.FileUploadResponse{
|
||||
FileInfos: []*model.FileInfo{},
|
||||
ClientIds: []string{},
|
||||
}
|
||||
|
||||
previewPathList := []string{}
|
||||
thumbnailPathList := []string{}
|
||||
imageDataList := [][]byte{}
|
||||
|
||||
for i, fileHeader := range m.File["files"] {
|
||||
file, fileErr := fileHeader.Open()
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
if fileErr != nil {
|
||||
http.Error(w, fileErr.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
io.Copy(buf, file)
|
||||
data := buf.Bytes()
|
||||
|
||||
filename := filepath.Base(files[i].Filename)
|
||||
|
||||
uid := model.NewId()
|
||||
|
||||
if model.IsFileExtImage(filepath.Ext(files[i].Filename)) {
|
||||
imageNameList = append(imageNameList, uid+"/"+filename)
|
||||
imageDataList = append(imageDataList, buf.Bytes())
|
||||
|
||||
// Decode image config first to check dimensions before loading the whole thing into memory later on
|
||||
config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes()))
|
||||
if err != nil {
|
||||
c.Err = model.NewLocAppError("uploadFile", "api.file.upload_file.image.app_error", nil, err.Error())
|
||||
return
|
||||
} else if config.Width*config.Height > MaxImageSize {
|
||||
c.Err = model.NewLocAppError("uploadFile", "api.file.upload_file.large_image.app_error", nil, c.T("api.file.file_upload.exceeds"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
path := "teams/" + c.TeamId + "/channels/" + channelId + "/users/" + c.Session.UserId + "/" + uid + "/" + filename
|
||||
|
||||
if err := WriteFile(buf.Bytes(), path); err != nil {
|
||||
info, err := doUploadFile(c.TeamId, channelId, c.Session.UserId, fileHeader.Filename, data)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
encName := utils.UrlEncode(filename)
|
||||
if info.PreviewPath != "" || info.ThumbnailPath != "" {
|
||||
previewPathList = append(previewPathList, info.PreviewPath)
|
||||
thumbnailPathList = append(thumbnailPathList, info.ThumbnailPath)
|
||||
imageDataList = append(imageDataList, data)
|
||||
}
|
||||
|
||||
fileUrl := "/" + channelId + "/" + c.Session.UserId + "/" + uid + "/" + encName
|
||||
resStruct.Filenames = append(resStruct.Filenames, fileUrl)
|
||||
resStruct.FileInfos = append(resStruct.FileInfos, info)
|
||||
|
||||
if len(m.Value["client_ids"]) > 0 {
|
||||
resStruct.ClientIds = append(resStruct.ClientIds, m.Value["client_ids"][i])
|
||||
}
|
||||
}
|
||||
|
||||
for _, clientId := range props["client_ids"] {
|
||||
resStruct.ClientIds = append(resStruct.ClientIds, clientId)
|
||||
}
|
||||
|
||||
go handleImages(imageNameList, imageDataList, c.TeamId, channelId, c.Session.UserId)
|
||||
handleImages(previewPathList, thumbnailPathList, imageDataList)
|
||||
|
||||
w.Write([]byte(resStruct.ToJson()))
|
||||
}
|
||||
|
||||
func handleImages(filenames []string, fileData [][]byte, teamId, channelId, userId string) {
|
||||
dest := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/"
|
||||
func doUploadFile(teamId string, channelId string, userId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
|
||||
filename := filepath.Base(rawFilename)
|
||||
|
||||
for i, filename := range filenames {
|
||||
name := filename[:strings.LastIndex(filename, ".")]
|
||||
info, err := model.GetInfoForBytes(filename, data)
|
||||
if err != nil {
|
||||
err.StatusCode = http.StatusBadRequest
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info.Id = model.NewId()
|
||||
info.CreatorId = userId
|
||||
|
||||
pathPrefix := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + info.Id + "/"
|
||||
info.Path = pathPrefix + filename
|
||||
|
||||
if info.IsImage() {
|
||||
// Check dimensions before loading the whole thing into memory later on
|
||||
if info.Width*info.Height > MaxImageSize {
|
||||
err := model.NewLocAppError("uploadFile", "api.file.upload_file.large_image.app_error", nil, "")
|
||||
err.StatusCode = http.StatusBadRequest
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nameWithoutExtension := filename[:strings.LastIndex(filename, ".")]
|
||||
info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
|
||||
info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
|
||||
}
|
||||
|
||||
if err := WriteFile(data, info.Path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.FileInfo().Save(info); result.Err != nil {
|
||||
return nil, result.Err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func handleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) {
|
||||
for i := range fileData {
|
||||
go func() {
|
||||
// Decode image bytes into Image object
|
||||
img, imgType, err := image.Decode(bytes.NewReader(fileData[i]))
|
||||
if err != nil {
|
||||
l4g.Error(utils.T("api.file.handle_images_forget.decode.error"), channelId, userId, filename, err)
|
||||
l4g.Error(utils.T("api.file.handle_images_forget.decode.error"), err)
|
||||
return
|
||||
}
|
||||
|
||||
width := img.Bounds().Dx()
|
||||
height := img.Bounds().Dy()
|
||||
|
||||
// Get the image's orientation and ignore any errors since not all images will have orientation data
|
||||
orientation, _ := getImageOrientation(fileData[i])
|
||||
|
||||
// Fill in the background of a potentially-transparent png file as white
|
||||
if imgType == "png" {
|
||||
dst := image.NewRGBA(img.Bounds())
|
||||
draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
|
||||
@@ -195,6 +213,9 @@ func handleImages(filenames []string, fileData [][]byte, teamId, channelId, user
|
||||
img = dst
|
||||
}
|
||||
|
||||
// Flip the image to be upright
|
||||
orientation, _ := getImageOrientation(fileData[i])
|
||||
|
||||
switch orientation {
|
||||
case UprightMirrored:
|
||||
img = imaging.FlipH(img)
|
||||
@@ -212,57 +233,8 @@ func handleImages(filenames []string, fileData [][]byte, teamId, channelId, user
|
||||
img = imaging.Rotate90(img)
|
||||
}
|
||||
|
||||
// Create thumbnail
|
||||
go func() {
|
||||
thumbWidth := float64(utils.Cfg.FileSettings.ThumbnailWidth)
|
||||
thumbHeight := float64(utils.Cfg.FileSettings.ThumbnailHeight)
|
||||
imgWidth := float64(width)
|
||||
imgHeight := float64(height)
|
||||
|
||||
var thumbnail image.Image
|
||||
if imgHeight < thumbHeight && imgWidth < thumbWidth {
|
||||
thumbnail = img
|
||||
} else if imgHeight/imgWidth < thumbHeight/thumbWidth {
|
||||
thumbnail = imaging.Resize(img, 0, utils.Cfg.FileSettings.ThumbnailHeight, imaging.Lanczos)
|
||||
} else {
|
||||
thumbnail = imaging.Resize(img, utils.Cfg.FileSettings.ThumbnailWidth, 0, imaging.Lanczos)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err = jpeg.Encode(buf, thumbnail, &jpeg.Options{Quality: 90})
|
||||
if err != nil {
|
||||
l4g.Error(utils.T("api.file.handle_images_forget.encode_jpeg.error"), channelId, userId, filename, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := WriteFile(buf.Bytes(), dest+name+"_thumb.jpg"); err != nil {
|
||||
l4g.Error(utils.T("api.file.handle_images_forget.upload_thumb.error"), channelId, userId, filename, err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// Create preview
|
||||
go func() {
|
||||
var preview image.Image
|
||||
if width > int(utils.Cfg.FileSettings.PreviewWidth) {
|
||||
preview = imaging.Resize(img, utils.Cfg.FileSettings.PreviewWidth, utils.Cfg.FileSettings.PreviewHeight, imaging.Lanczos)
|
||||
} else {
|
||||
preview = img
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
err = jpeg.Encode(buf, preview, &jpeg.Options{Quality: 90})
|
||||
if err != nil {
|
||||
l4g.Error(utils.T("api.file.handle_images_forget.encode_preview.error"), channelId, userId, filename, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := WriteFile(buf.Bytes(), dest+name+"_preview.jpg"); err != nil {
|
||||
l4g.Error(utils.T("api.file.handle_images_forget.upload_preview.error"), channelId, userId, filename, err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
go generateThumbnailImage(img, thumbnailPathList[i], width, height)
|
||||
go generatePreviewImage(img, previewPathList[i], width)
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -284,88 +256,214 @@ func getImageOrientation(imageData []byte) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
type ImageGetResult struct {
|
||||
Error error
|
||||
ImageData []byte
|
||||
func generateThumbnailImage(img image.Image, thumbnailPath string, width int, height int) {
|
||||
thumbWidth := float64(utils.Cfg.FileSettings.ThumbnailWidth)
|
||||
thumbHeight := float64(utils.Cfg.FileSettings.ThumbnailHeight)
|
||||
imgWidth := float64(width)
|
||||
imgHeight := float64(height)
|
||||
|
||||
var thumbnail image.Image
|
||||
if imgHeight < thumbHeight && imgWidth < thumbWidth {
|
||||
thumbnail = img
|
||||
} else if imgHeight/imgWidth < thumbHeight/thumbWidth {
|
||||
thumbnail = imaging.Resize(img, 0, utils.Cfg.FileSettings.ThumbnailHeight, imaging.Lanczos)
|
||||
} else {
|
||||
thumbnail = imaging.Resize(img, utils.Cfg.FileSettings.ThumbnailWidth, 0, imaging.Lanczos)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := jpeg.Encode(buf, thumbnail, &jpeg.Options{Quality: 90}); err != nil {
|
||||
l4g.Error(utils.T("api.file.handle_images_forget.encode_jpeg.error"), thumbnailPath, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := WriteFile(buf.Bytes(), thumbnailPath); err != nil {
|
||||
l4g.Error(utils.T("api.file.handle_images_forget.upload_thumb.error"), thumbnailPath, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func generatePreviewImage(img image.Image, previewPath string, width int) {
|
||||
var preview image.Image
|
||||
if width > int(utils.Cfg.FileSettings.PreviewWidth) {
|
||||
preview = imaging.Resize(img, utils.Cfg.FileSettings.PreviewWidth, utils.Cfg.FileSettings.PreviewHeight, imaging.Lanczos)
|
||||
} else {
|
||||
preview = img
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if err := jpeg.Encode(buf, preview, &jpeg.Options{Quality: 90}); err != nil {
|
||||
l4g.Error(utils.T("api.file.handle_images_forget.encode_preview.error"), previewPath, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := WriteFile(buf.Bytes(), previewPath); err != nil {
|
||||
l4g.Error(utils.T("api.file.handle_images_forget.upload_preview.error"), previewPath, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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 := ReadFile(info.Path); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
} else if err := writeFileResponse(info.Name, 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 := ReadFile(info.ThumbnailPath); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
} else if err := writeFileResponse(info.Name, 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 := ReadFile(info.PreviewPath); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
} else if err := writeFileResponse(info.Name, data, w, r); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if len(utils.Cfg.FileSettings.DriverName) == 0 {
|
||||
c.Err = model.NewLocAppError("uploadFile", "api.file.upload_file.storage.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
info, err := getFileInfoForRequest(c, r, true)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
params := mux.Vars(r)
|
||||
|
||||
channelId := params["channel_id"]
|
||||
if len(channelId) != 26 {
|
||||
c.SetInvalidParam("getFileInfo", "channel_id")
|
||||
return
|
||||
}
|
||||
|
||||
userId := params["user_id"]
|
||||
if len(userId) != 26 {
|
||||
c.SetInvalidParam("getFileInfo", "user_id")
|
||||
return
|
||||
}
|
||||
|
||||
filename := params["filename"]
|
||||
if len(filename) == 0 {
|
||||
c.SetInvalidParam("getFileInfo", "filename")
|
||||
return
|
||||
}
|
||||
|
||||
if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
|
||||
return
|
||||
}
|
||||
|
||||
path := "teams/" + c.TeamId + "/channels/" + channelId + "/users/" + userId + "/" + filename
|
||||
var info *model.FileInfo
|
||||
|
||||
if cached, ok := fileInfoCache.Get(path); ok {
|
||||
info = cached.(*model.FileInfo)
|
||||
} else {
|
||||
fileData := make(chan []byte)
|
||||
go readFile(path, fileData)
|
||||
|
||||
newInfo, err := model.GetInfoForBytes(filename, <-fileData)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
fileInfoCache.Add(path, newInfo)
|
||||
info = newInfo
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "max-age=2592000, public")
|
||||
|
||||
w.Write([]byte(info.ToJson()))
|
||||
}
|
||||
|
||||
func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
params := mux.Vars(r)
|
||||
|
||||
teamId := c.TeamId
|
||||
channelId := params["channel_id"]
|
||||
userId := params["user_id"]
|
||||
filename := params["filename"]
|
||||
|
||||
if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
|
||||
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
|
||||
}
|
||||
|
||||
if err, bytes := getFileData(teamId, channelId, userId, filename); err != nil {
|
||||
info, err := getFileInfoForRequest(c, r, false)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else if err := writeFileResponse(filename, bytes, w, r); err != nil {
|
||||
}
|
||||
|
||||
hash := r.URL.Query().Get("h")
|
||||
|
||||
if len(hash) > 0 {
|
||||
correctHash := 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 := ReadFile(info.Path); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
} else if err := writeFileResponse(info.Name, data, w, r); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
func getFileInfoForRequest(c *Context, r *http.Request, requireFileVisible bool) (*model.FileInfo, *model.AppError) {
|
||||
if len(utils.Cfg.FileSettings.DriverName) == 0 {
|
||||
err := model.NewLocAppError("getFileInfoForRequest", "api.file.get_file_info_for_request.storage.app_error", nil, "")
|
||||
err.StatusCode = http.StatusNotImplemented
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params := mux.Vars(r)
|
||||
|
||||
fileId := params["file_id"]
|
||||
if len(fileId) != 26 {
|
||||
return nil, NewInvalidParamError("getFileInfoForRequest", "file_id")
|
||||
}
|
||||
|
||||
var info *model.FileInfo
|
||||
if result := <-Srv.Store.FileInfo().Get(fileId); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
info = result.Data.(*model.FileInfo)
|
||||
}
|
||||
|
||||
// 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 !HasPermissionToChannelByPostContext(c, info.PostId, 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"]
|
||||
@@ -375,12 +473,6 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
hash := r.URL.Query().Get("h")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if len(hash) > 0 {
|
||||
correctHash := generatePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
|
||||
|
||||
@@ -395,49 +487,28 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err, bytes := getFileData(teamId, channelId, userId, filename); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else if err := writeFileResponse(filename, bytes, w, r); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getFileData(teamId string, channelId string, userId string, filename string) (*model.AppError, []byte) {
|
||||
if len(utils.Cfg.FileSettings.DriverName) == 0 {
|
||||
err := model.NewLocAppError("getFileData", "api.file.upload_file.storage.app_error", nil, "")
|
||||
err.StatusCode = http.StatusNotImplemented
|
||||
return err, nil
|
||||
}
|
||||
|
||||
if len(teamId) != 26 {
|
||||
return NewInvalidParamError("getFileData", "team_id"), nil
|
||||
}
|
||||
|
||||
if len(channelId) != 26 {
|
||||
return NewInvalidParamError("getFileData", "channel_id"), nil
|
||||
}
|
||||
|
||||
if len(userId) != 26 {
|
||||
return NewInvalidParamError("getFileData", "user_id"), nil
|
||||
}
|
||||
|
||||
if len(filename) == 0 {
|
||||
return NewInvalidParamError("getFileData", "filename"), nil
|
||||
}
|
||||
|
||||
path := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + filename
|
||||
|
||||
fileChan := make(chan []byte)
|
||||
go readFile(path, fileChan)
|
||||
|
||||
if bytes := <-fileChan; bytes == nil {
|
||||
err := model.NewLocAppError("writeFileResponse", "api.file.get_file.not_found.app_error", nil, "path="+path)
|
||||
err.StatusCode = http.StatusNotFound
|
||||
return err, nil
|
||||
var info *model.FileInfo
|
||||
if result := <-Srv.Store.FileInfo().GetByPath(path); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
return nil, bytes
|
||||
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 := ReadFile(info.Path); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
} else if err := writeFileResponse(info.Name, data, w, r); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,9 +521,7 @@ func writeFileResponse(filename string, bytes []byte, w http.ResponseWriter, r *
|
||||
ua := user_agent.New(r.UserAgent())
|
||||
bname, _ := ua.Browser()
|
||||
|
||||
parts := strings.Split(filename, "/")
|
||||
filePart := strings.Split(parts[len(parts)-1], "?")[0]
|
||||
w.Header().Set("Content-Disposition", "attachment;filename=\""+filePart+"\"")
|
||||
w.Header().Set("Content-Disposition", "attachment;filename=\""+filename+"\"")
|
||||
|
||||
if bname == "Edge" || bname == "Internet Explorer" || bname == "Safari" {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
@@ -467,71 +536,183 @@ func writeFileResponse(filename string, bytes []byte, w http.ResponseWriter, r *
|
||||
return nil
|
||||
}
|
||||
|
||||
func readFile(path string, fileData chan []byte) {
|
||||
data, getErr := ReadFile(path)
|
||||
if getErr != nil {
|
||||
l4g.Error(getErr)
|
||||
fileData <- nil
|
||||
} else {
|
||||
fileData <- data
|
||||
}
|
||||
}
|
||||
|
||||
func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if len(utils.Cfg.FileSettings.DriverName) == 0 {
|
||||
c.Err = model.NewLocAppError("uploadFile", "api.file.upload_file.storage.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
props := model.MapFromJson(r.Body)
|
||||
|
||||
filename := props["filename"]
|
||||
if len(filename) == 0 {
|
||||
c.SetInvalidParam("getPublicLink", "filename")
|
||||
info, err := getFileInfoForRequest(c, r, true)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
matches := model.PartialUrlRegex.FindAllStringSubmatch(filename, -1)
|
||||
if len(matches) == 0 || len(matches[0]) < 4 {
|
||||
c.SetInvalidParam("getPublicLink", "filename")
|
||||
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
|
||||
}
|
||||
|
||||
channelId := matches[0][1]
|
||||
userId := matches[0][2]
|
||||
filename = matches[0][3]
|
||||
|
||||
if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_GET_PUBLIC_LINK) {
|
||||
return
|
||||
}
|
||||
|
||||
url := generatePublicLink(c.GetSiteURL(), c.TeamId, channelId, userId, filename)
|
||||
|
||||
w.Write([]byte(model.StringToJson(url)))
|
||||
w.Write([]byte(model.StringToJson(generatePublicLink(c.GetSiteURL(), info))))
|
||||
}
|
||||
|
||||
func generatePublicLink(siteURL, teamId, channelId, userId, filename string) string {
|
||||
hash := generatePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
|
||||
return fmt.Sprintf("%s%s/public/files/get/%s/%s/%s/%s?h=%s", siteURL, model.API_URL_SUFFIX, teamId, channelId, userId, filename, hash)
|
||||
func generatePublicLink(siteURL string, info *model.FileInfo) string {
|
||||
hash := generatePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
|
||||
return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX, info.Id, hash)
|
||||
}
|
||||
|
||||
func generatePublicLinkHash(filename, salt string) string {
|
||||
func generatePublicLinkHash(fileId, salt string) string {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(salt))
|
||||
hash.Write([]byte(filename))
|
||||
hash.Write([]byte(fileId))
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func WriteFile(f []byte, path string) *model.AppError {
|
||||
// Creates and stores FileInfos for a post created before the FileInfos table existed.
|
||||
func migrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
|
||||
if len(post.Filenames) == 0 {
|
||||
l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.no_filenames.warn"), post.Id)
|
||||
return []*model.FileInfo{}
|
||||
}
|
||||
|
||||
cchan := Srv.Store.Channel().Get(post.ChannelId)
|
||||
|
||||
// There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those
|
||||
filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames)
|
||||
|
||||
var channel *model.Channel
|
||||
if result := <-cchan; result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.channel.app_error"), post.Id, post.ChannelId, result.Err)
|
||||
return []*model.FileInfo{}
|
||||
} else {
|
||||
channel = result.Data.(*model.Channel)
|
||||
}
|
||||
|
||||
// Find the team that was used to make this post since its part of the file path that isn't saved in the Filename
|
||||
var teamId string
|
||||
if channel.TeamId == "" {
|
||||
// This post was made in a cross-team DM channel so we need to find where its files were saved
|
||||
teamId = findTeamIdForFilename(post, filenames[0])
|
||||
} else {
|
||||
teamId = channel.TeamId
|
||||
}
|
||||
|
||||
// Create FileInfo objects for this post
|
||||
infos := make([]*model.FileInfo, 0, len(filenames))
|
||||
fileIds := make([]string, 0, len(filenames))
|
||||
if teamId == "" {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.team_id.error"), post.Id, filenames)
|
||||
} else {
|
||||
for _, filename := range filenames {
|
||||
info := getInfoForFilename(post, teamId, filename)
|
||||
if info == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.FileInfo().Save(info); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_file_info.app_error"), post.Id, info.Id, filename, result.Err)
|
||||
continue
|
||||
}
|
||||
|
||||
fileIds = append(fileIds, info.Id)
|
||||
infos = append(infos, info)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy and save the updated post
|
||||
newPost := &model.Post{}
|
||||
*newPost = *post
|
||||
|
||||
newPost.Filenames = []string{}
|
||||
newPost.FileIds = fileIds
|
||||
|
||||
// Update Posts to clear Filenames and set FileIds
|
||||
if result := <-Srv.Store.Post().Update(newPost, post); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_post.app_error"), post.Id, newPost.FileIds, post.Filenames, result.Err)
|
||||
return []*model.FileInfo{}
|
||||
} else {
|
||||
return infos
|
||||
}
|
||||
}
|
||||
|
||||
func findTeamIdForFilename(post *model.Post, filename string) string {
|
||||
split := strings.SplitN(filename, "/", 5)
|
||||
id := split[3]
|
||||
name, _ := url.QueryUnescape(split[4])
|
||||
|
||||
// This post is in a direct channel so we need to figure out what team the files are stored under.
|
||||
if result := <-Srv.Store.Team().GetTeamsByUserId(post.UserId); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.teams.app_error"), post.Id, result.Err)
|
||||
} else if teams := result.Data.([]*model.Team); len(teams) == 1 {
|
||||
// The user has only one team so the post must've been sent from it
|
||||
return teams[0].Id
|
||||
} 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 {
|
||||
// Found the team that this file was posted from
|
||||
return team.Id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
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)
|
||||
if len(split) < 5 {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.unexpected_filename.error"), post.Id, filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
channelId := split[1]
|
||||
userId := split[2]
|
||||
oldId := split[3]
|
||||
name, _ := url.QueryUnescape(split[4])
|
||||
|
||||
if split[0] != "" || split[1] != post.ChannelId || split[2] != post.UserId || strings.Contains(split[4], "/") {
|
||||
l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.mismatched_filename.warn"), post.Id, post.ChannelId, post.UserId, filename)
|
||||
}
|
||||
|
||||
pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamId, channelId, userId, oldId)
|
||||
path := pathPrefix + name
|
||||
|
||||
// Open the file and populate the fields of the FileInfo
|
||||
var info *model.FileInfo
|
||||
if data, err := 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 {
|
||||
var err *model.AppError
|
||||
info, err = model.GetInfoForBytes(name, data)
|
||||
if err != nil {
|
||||
l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.info.app_error"), post.Id, filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file
|
||||
info.Id = model.NewId()
|
||||
info.CreatorId = post.UserId
|
||||
info.PostId = post.Id
|
||||
info.CreateAt = post.CreateAt
|
||||
info.UpdateAt = post.UpdateAt
|
||||
info.Path = path
|
||||
|
||||
if info.IsImage() {
|
||||
nameWithoutExtension := name[:strings.LastIndex(name, ".")]
|
||||
info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
|
||||
info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func WriteFile(f []byte, path string) *model.AppError {
|
||||
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
|
||||
var auth aws.Auth
|
||||
auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
|
||||
@@ -556,7 +737,7 @@ func WriteFile(f []byte, path string) *model.AppError {
|
||||
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 {
|
||||
if err := writeFileLocally(f, utils.Cfg.FileSettings.Directory+path); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -568,9 +749,7 @@ func WriteFile(f []byte, path string) *model.AppError {
|
||||
|
||||
func MoveFile(oldPath, newPath string) *model.AppError {
|
||||
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
|
||||
fileData := make(chan []byte)
|
||||
go readFile(oldPath, fileData)
|
||||
fileBytes := <-fileData
|
||||
fileBytes, _ := ReadFile(oldPath)
|
||||
|
||||
if fileBytes == nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.get_from_s3.app_error", nil, "")
|
||||
@@ -606,7 +785,7 @@ func MoveFile(oldPath, newPath string) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteFileLocally(f []byte, path string) *model.AppError {
|
||||
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())
|
||||
@@ -620,7 +799,6 @@ func WriteFileLocally(f []byte, path string) *model.AppError {
|
||||
}
|
||||
|
||||
func ReadFile(path string) ([]byte, *model.AppError) {
|
||||
|
||||
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
|
||||
var auth aws.Auth
|
||||
auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId
|
||||
@@ -668,7 +846,6 @@ func openFileWriteStream(path string) (io.Writer, *model.AppError) {
|
||||
fileHandle.Chmod(0644)
|
||||
return fileHandle, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.configured.app_error", nil, "")
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/utils"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func BenchmarkUploadFile(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
channel := th.BasicChannel
|
||||
|
||||
testPoster := NewAutoPostCreator(Client, channel.Id)
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
testPoster.UploadTestFile()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetFile(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
channel := th.BasicChannel
|
||||
|
||||
testPoster := NewAutoPostCreator(Client, channel.Id)
|
||||
filenames, err := testPoster.UploadTestFile()
|
||||
if err == false {
|
||||
b.Fatal("Unable to upload file for benchmark")
|
||||
}
|
||||
|
||||
// wait a bit for files to ready
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, downErr := Client.GetFile(filenames[0]+"?h="+generatePublicLinkHash(filenames[0], *utils.Cfg.FileSettings.PublicLinkSalt), true); downErr != nil {
|
||||
b.Fatal(downErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetPublicLink(b *testing.B) {
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
channel := th.BasicChannel
|
||||
|
||||
testPoster := NewAutoPostCreator(Client, channel.Id)
|
||||
filenames, err := testPoster.UploadTestFile()
|
||||
if err == false {
|
||||
b.Fatal("Unable to upload file for benchmark")
|
||||
}
|
||||
|
||||
// wait a bit for files to ready
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, downErr := Client.GetPublicLink(filenames[0]); downErr != nil {
|
||||
b.Fatal(downErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
1072
api/file_test.go
1072
api/file_test.go
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
168
api/post.go
168
api/post.go
@@ -49,6 +49,7 @@ func InitPost() {
|
||||
BaseRoutes.NeedPost.Handle("/delete", ApiUserRequired(deletePost)).Methods("POST")
|
||||
BaseRoutes.NeedPost.Handle("/before/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsBefore)).Methods("GET")
|
||||
BaseRoutes.NeedPost.Handle("/after/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsAfter)).Methods("GET")
|
||||
BaseRoutes.NeedPost.Handle("/get_file_infos", ApiUserRequired(getFileInfosForPost)).Methods("GET")
|
||||
}
|
||||
|
||||
func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -135,48 +136,26 @@ func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post
|
||||
|
||||
post.Hashtags, _ = model.ParseHashtags(post.Message)
|
||||
|
||||
if len(post.Filenames) > 0 {
|
||||
doRemove := false
|
||||
for i := len(post.Filenames) - 1; i >= 0; i-- {
|
||||
path := post.Filenames[i]
|
||||
|
||||
doRemove = false
|
||||
if model.UrlRegex.MatchString(path) {
|
||||
continue
|
||||
} else if model.PartialUrlRegex.MatchString(path) {
|
||||
matches := model.PartialUrlRegex.FindAllStringSubmatch(path, -1)
|
||||
if len(matches) == 0 || len(matches[0]) < 4 {
|
||||
doRemove = true
|
||||
}
|
||||
|
||||
channelId := matches[0][1]
|
||||
if channelId != post.ChannelId {
|
||||
doRemove = true
|
||||
}
|
||||
|
||||
userId := matches[0][2]
|
||||
if userId != post.UserId {
|
||||
doRemove = true
|
||||
}
|
||||
} else {
|
||||
doRemove = true
|
||||
}
|
||||
if doRemove {
|
||||
l4g.Error(utils.T("api.post.create_post.bad_filename.error"), path)
|
||||
post.Filenames = append(post.Filenames[:i], post.Filenames[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var rpost *model.Post
|
||||
if result := <-Srv.Store.Post().Save(post); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
rpost = result.Data.(*model.Post)
|
||||
|
||||
go handlePostEvents(c, rpost, triggerWebhooks)
|
||||
}
|
||||
|
||||
if len(post.FileIds) > 0 {
|
||||
// There's a rare bug where the client sends up duplicate FileIds so protect against that
|
||||
post.FileIds = utils.RemoveDuplicatesFromStringArray(post.FileIds)
|
||||
|
||||
for _, fileId := range post.FileIds {
|
||||
if result := <-Srv.Store.FileInfo().AttachToPost(fileId, post.Id); result.Err != nil {
|
||||
l4g.Error(utils.T("api.post.create_post.attach_files.error"), post.Id, post.FileIds, c.Session.UserId, result.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go handlePostEvents(c, rpost, triggerWebhooks)
|
||||
|
||||
return rpost, nil
|
||||
}
|
||||
|
||||
@@ -566,6 +545,7 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
|
||||
pchan := Srv.Store.User().GetProfiles(c.TeamId)
|
||||
dpchan := Srv.Store.User().GetDirectProfiles(c.Session.UserId)
|
||||
mchan := Srv.Store.Channel().GetMembers(post.ChannelId)
|
||||
fchan := Srv.Store.FileInfo().GetForPost(post.Id)
|
||||
|
||||
var profileMap map[string]*model.User
|
||||
if result := <-pchan; result.Err != nil {
|
||||
@@ -785,12 +765,18 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
|
||||
message.Add("sender_name", senderName)
|
||||
message.Add("team_id", team.Id)
|
||||
|
||||
if len(post.Filenames) != 0 {
|
||||
if len(post.FileIds) != 0 {
|
||||
message.Add("otherFile", "true")
|
||||
|
||||
for _, filename := range post.Filenames {
|
||||
ext := filepath.Ext(filename)
|
||||
if model.IsFileExtImage(ext) {
|
||||
var infos []*model.FileInfo
|
||||
if result := <-fchan; result.Err != nil {
|
||||
l4g.Warn(utils.T("api.post.send_notifications.files.error"), post.Id, result.Err)
|
||||
} else {
|
||||
infos = result.Data.([]*model.FileInfo)
|
||||
}
|
||||
|
||||
for _, info := range infos {
|
||||
if info.IsImage() {
|
||||
message.Add("image", "true")
|
||||
break
|
||||
}
|
||||
@@ -915,22 +901,29 @@ func sendNotificationEmail(c *Context, post *model.Post, user *model.User, chann
|
||||
}
|
||||
|
||||
func getMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
|
||||
if len(strings.TrimSpace(post.Message)) != 0 || len(post.Filenames) == 0 {
|
||||
if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 {
|
||||
return post.Message
|
||||
}
|
||||
|
||||
// extract the filenames from their paths and determine what type of files are attached
|
||||
filenames := make([]string, len(post.Filenames))
|
||||
var infos []*model.FileInfo
|
||||
if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil {
|
||||
l4g.Warn(utils.T("api.post.get_message_for_notification.get_files.error"), post.Id, result.Err)
|
||||
} else {
|
||||
infos = result.Data.([]*model.FileInfo)
|
||||
}
|
||||
|
||||
filenames := make([]string, len(infos))
|
||||
onlyImages := true
|
||||
for i, filename := range post.Filenames {
|
||||
var err error
|
||||
if filenames[i], err = url.QueryUnescape(filepath.Base(filename)); err != nil {
|
||||
for i, info := range infos {
|
||||
if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil {
|
||||
// this should never error since filepath was escaped using url.QueryEscape
|
||||
filenames[i] = filepath.Base(filename)
|
||||
filenames[i] = escaped
|
||||
} else {
|
||||
filenames[i] = info.Name
|
||||
}
|
||||
|
||||
ext := filepath.Ext(filename)
|
||||
onlyImages = onlyImages && model.IsFileExtImage(ext)
|
||||
onlyImages = onlyImages && info.IsImage()
|
||||
}
|
||||
|
||||
props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")}
|
||||
@@ -1099,9 +1092,6 @@ func SendEphemeralPost(teamId, userId string, post *model.Post) {
|
||||
if post.Props == nil {
|
||||
post.Props = model.StringInterface{}
|
||||
}
|
||||
if post.Filenames == nil {
|
||||
post.Filenames = []string{}
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil)
|
||||
message.Add("post", post.ToJson())
|
||||
@@ -1156,9 +1146,13 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
hashtags, _ := model.ParseHashtags(post.Message)
|
||||
newPost := &model.Post{}
|
||||
*newPost = *oldPost
|
||||
|
||||
if result := <-Srv.Store.Post().Update(oldPost, post.Message, hashtags); result.Err != nil {
|
||||
newPost.Message = post.Message
|
||||
newPost.Hashtags, _ = model.ParseHashtags(post.Message)
|
||||
|
||||
if result := <-Srv.Store.Post().Update(newPost, oldPost); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
@@ -1449,7 +1443,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
message.Add("post", post.ToJson())
|
||||
|
||||
go Publish(message)
|
||||
go DeletePostFiles(c.TeamId, post)
|
||||
go DeletePostFiles(post)
|
||||
go DeleteFlaggedPost(c.Session.UserId, post)
|
||||
|
||||
result := make(map[string]string)
|
||||
@@ -1465,17 +1459,13 @@ func DeleteFlaggedPost(userId string, post *model.Post) {
|
||||
}
|
||||
}
|
||||
|
||||
func DeletePostFiles(teamId string, post *model.Post) {
|
||||
if len(post.Filenames) == 0 {
|
||||
func DeletePostFiles(post *model.Post) {
|
||||
if len(post.FileIds) != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
prefix := "teams/" + teamId + "/channels/" + post.ChannelId + "/users/" + post.UserId + "/"
|
||||
for _, filename := range post.Filenames {
|
||||
splitUrl := strings.Split(filename, "/")
|
||||
oldPath := prefix + splitUrl[len(splitUrl)-2] + "/" + splitUrl[len(splitUrl)-1]
|
||||
newPath := prefix + splitUrl[len(splitUrl)-2] + "/deleted_" + splitUrl[len(splitUrl)-1]
|
||||
MoveFile(oldPath, newPath)
|
||||
if result := <-Srv.Store.FileInfo().DeleteForPost(post.Id); result.Err != nil {
|
||||
l4g.Warn(utils.T("api.post.delete_post_files.app_error.warn"), post.Id, result.Err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1583,3 +1573,59 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Write([]byte(posts.ToJson()))
|
||||
}
|
||||
|
||||
func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
params := mux.Vars(r)
|
||||
|
||||
channelId := params["channel_id"]
|
||||
if len(channelId) != 26 {
|
||||
c.SetInvalidParam("getFileInfosForPost", "channelId")
|
||||
return
|
||||
}
|
||||
|
||||
postId := params["post_id"]
|
||||
if len(postId) != 26 {
|
||||
c.SetInvalidParam("getFileInfosForPost", "postId")
|
||||
return
|
||||
}
|
||||
|
||||
pchan := Srv.Store.Post().Get(postId)
|
||||
fchan := Srv.Store.FileInfo().GetForPost(postId)
|
||||
|
||||
if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
|
||||
return
|
||||
}
|
||||
|
||||
var infos []*model.FileInfo
|
||||
if result := <-fchan; result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
infos = result.Data.([]*model.FileInfo)
|
||||
}
|
||||
|
||||
if len(infos) == 0 {
|
||||
// No FileInfos were returned so check if they need to be created for this post
|
||||
var post *model.Post
|
||||
if result := <-pchan; result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
post = result.Data.(*model.PostList).Posts[postId]
|
||||
}
|
||||
|
||||
if len(post.Filenames) > 0 {
|
||||
// The post has Filenames that need to be replaced with FileInfos
|
||||
infos = migrateFilenamesToFileInfos(post)
|
||||
}
|
||||
}
|
||||
|
||||
etag := model.GetEtagForFileInfos(infos)
|
||||
|
||||
if HandleEtag(etag, w, r) {
|
||||
return
|
||||
} else {
|
||||
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
|
||||
w.Write([]byte(model.FileInfosToJson(infos)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
NUM_POSTS = 100
|
||||
)
|
||||
|
||||
func BenchmarkCreatePost(b *testing.B) {
|
||||
var (
|
||||
NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS}
|
||||
)
|
||||
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
channel := th.BasicChannel
|
||||
|
||||
testPoster := NewAutoPostCreator(Client, channel.Id)
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
testPoster.CreateTestPosts(NUM_POSTS_RANGE)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUpdatePost(b *testing.B) {
|
||||
var (
|
||||
NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS}
|
||||
UPDATE_POST_LEN = 100
|
||||
)
|
||||
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
channel := th.BasicChannel
|
||||
|
||||
testPoster := NewAutoPostCreator(Client, channel.Id)
|
||||
posts, valid := testPoster.CreateTestPosts(NUM_POSTS_RANGE)
|
||||
if valid == false {
|
||||
b.Fatal("Unable to create test posts")
|
||||
}
|
||||
|
||||
for i := range posts {
|
||||
posts[i].Message = utils.RandString(UPDATE_POST_LEN, utils.ALPHANUMERIC)
|
||||
}
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for i := range posts {
|
||||
if _, err := Client.UpdatePost(posts[i]); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetPosts(b *testing.B) {
|
||||
var (
|
||||
NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS}
|
||||
)
|
||||
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
channel := th.BasicChannel
|
||||
|
||||
testPoster := NewAutoPostCreator(Client, channel.Id)
|
||||
testPoster.CreateTestPosts(NUM_POSTS_RANGE)
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Client.Must(Client.GetPosts(channel.Id, 0, NUM_POSTS, ""))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSearchPosts(b *testing.B) {
|
||||
var (
|
||||
NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS}
|
||||
)
|
||||
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
channel := th.BasicChannel
|
||||
|
||||
testPoster := NewAutoPostCreator(Client, channel.Id)
|
||||
testPoster.CreateTestPosts(NUM_POSTS_RANGE)
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Client.Must(Client.SearchPosts("nothere", false))
|
||||
Client.Must(Client.SearchPosts("n", false))
|
||||
Client.Must(Client.SearchPosts("#tag", false))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEtagCache(b *testing.B) {
|
||||
var (
|
||||
NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS}
|
||||
)
|
||||
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
channel := th.BasicChannel
|
||||
|
||||
testPoster := NewAutoPostCreator(Client, channel.Id)
|
||||
testPoster.CreateTestPosts(NUM_POSTS_RANGE)
|
||||
|
||||
etag := Client.Must(Client.GetPosts(channel.Id, 0, NUM_POSTS/2, "")).Etag
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Client.Must(Client.GetPosts(channel.Id, 0, NUM_POSTS/2, etag))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDeletePosts(b *testing.B) {
|
||||
var (
|
||||
NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS}
|
||||
)
|
||||
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
channel := th.BasicChannel
|
||||
|
||||
testPoster := NewAutoPostCreator(Client, channel.Id)
|
||||
posts, valid := testPoster.CreateTestPosts(NUM_POSTS_RANGE)
|
||||
if valid == false {
|
||||
b.Fatal("Unable to create test posts")
|
||||
}
|
||||
|
||||
// Benchmark Start
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for i := range posts {
|
||||
Client.Must(Client.DeletePost(channel.Id, posts[i].Id))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
131
api/post_test.go
131
api/post_test.go
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/store"
|
||||
"github.com/mattermost/platform/utils"
|
||||
)
|
||||
|
||||
@@ -23,15 +24,12 @@ func TestCreatePost(t *testing.T) {
|
||||
Client := th.BasicClient
|
||||
team := th.BasicTeam
|
||||
team2 := th.CreateTeam(th.BasicClient)
|
||||
user1 := th.BasicUser
|
||||
user3 := th.CreateUser(th.BasicClient)
|
||||
LinkUserToTeam(user3, team2)
|
||||
channel1 := th.BasicChannel
|
||||
channel2 := th.CreateChannel(Client, team)
|
||||
|
||||
filenames := []string{"/12345678901234567890123456/12345678901234567890123456/12345678901234567890123456/test.png", "/" + channel1.Id + "/" + user1.Id + "/test.png", "www.mattermost.com/fake/url", "junk"}
|
||||
|
||||
post1 := &model.Post{ChannelId: channel1.Id, Message: "#hashtag a" + model.NewId() + "a", Filenames: filenames}
|
||||
post1 := &model.Post{ChannelId: channel1.Id, Message: "#hashtag a" + model.NewId() + "a"}
|
||||
rpost1, err := Client.CreatePost(post1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -45,8 +43,8 @@ func TestCreatePost(t *testing.T) {
|
||||
t.Fatal("hashtag didn't match")
|
||||
}
|
||||
|
||||
if len(rpost1.Data.(*model.Post).Filenames) != 2 {
|
||||
t.Fatal("filenames didn't parse correctly")
|
||||
if len(rpost1.Data.(*model.Post).FileIds) != 0 {
|
||||
t.Fatal("shouldn't have files")
|
||||
}
|
||||
|
||||
post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id}
|
||||
@@ -109,6 +107,35 @@ func TestCreatePost(t *testing.T) {
|
||||
if _, err = Client.DoApiPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil {
|
||||
t.Fatal("should have been an error")
|
||||
}
|
||||
|
||||
fileIds := make([]string, 4)
|
||||
if data, err := readTestFile("test.png"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
for i := 0; i < 3; i++ {
|
||||
fileIds[i] = Client.MustGeneric(Client.UploadPostAttachment(data, channel3.Id, "test.png")).(*model.FileUploadResponse).FileInfos[0].Id
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure duplicated file ids are removed
|
||||
fileIds[3] = fileIds[0]
|
||||
|
||||
post9 := &model.Post{
|
||||
ChannelId: channel3.Id,
|
||||
Message: "test",
|
||||
FileIds: fileIds,
|
||||
}
|
||||
if resp, err := Client.CreatePost(post9); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if rpost9 := resp.Data.(*model.Post); len(rpost9.FileIds) != 3 {
|
||||
t.Fatal("post should have 3 files")
|
||||
} else {
|
||||
infos := store.Must(Srv.Store.FileInfo().GetForPost(rpost9.Id)).([]*model.FileInfo)
|
||||
|
||||
if len(infos) != 3 {
|
||||
t.Fatal("should've attached all 3 files to post")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCreatePostWithOutgoingHook(
|
||||
@@ -800,10 +827,8 @@ func TestFuzzyPosts(t *testing.T) {
|
||||
Client := th.BasicClient
|
||||
channel1 := th.BasicChannel
|
||||
|
||||
filenames := []string{"junk"}
|
||||
|
||||
for i := 0; i < len(utils.FUZZY_STRINGS_POSTS); i++ {
|
||||
post := &model.Post{ChannelId: channel1.Id, Message: utils.FUZZY_STRINGS_POSTS[i], Filenames: filenames}
|
||||
post := &model.Post{ChannelId: channel1.Id, Message: utils.FUZZY_STRINGS_POSTS[i]}
|
||||
|
||||
_, err := Client.CreatePost(post)
|
||||
if err != nil {
|
||||
@@ -1150,19 +1175,49 @@ func TestGetFlaggedPosts(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetMessageForNotification(t *testing.T) {
|
||||
Setup()
|
||||
Setup().InitBasic()
|
||||
|
||||
testPng := store.Must(Srv.Store.FileInfo().Save(&model.FileInfo{
|
||||
CreatorId: model.NewId(),
|
||||
Path: "test1.png",
|
||||
Name: "test1.png",
|
||||
MimeType: "image/png",
|
||||
})).(*model.FileInfo)
|
||||
|
||||
testJpg1 := store.Must(Srv.Store.FileInfo().Save(&model.FileInfo{
|
||||
CreatorId: model.NewId(),
|
||||
Path: "test2.jpg",
|
||||
Name: "test2.jpg",
|
||||
MimeType: "image/jpeg",
|
||||
})).(*model.FileInfo)
|
||||
|
||||
testFile := store.Must(Srv.Store.FileInfo().Save(&model.FileInfo{
|
||||
CreatorId: model.NewId(),
|
||||
Path: "test1.go",
|
||||
Name: "test1.go",
|
||||
MimeType: "text/plain",
|
||||
})).(*model.FileInfo)
|
||||
|
||||
testJpg2 := store.Must(Srv.Store.FileInfo().Save(&model.FileInfo{
|
||||
CreatorId: model.NewId(),
|
||||
Path: "test3.jpg",
|
||||
Name: "test3.jpg",
|
||||
MimeType: "image/jpeg",
|
||||
})).(*model.FileInfo)
|
||||
|
||||
translateFunc := utils.GetUserTranslations("en")
|
||||
|
||||
post := &model.Post{
|
||||
Message: "test",
|
||||
Filenames: model.StringArray{},
|
||||
Id: model.NewId(),
|
||||
Message: "test",
|
||||
}
|
||||
|
||||
if getMessageForNotification(post, translateFunc) != "test" {
|
||||
t.Fatal("should've returned message text")
|
||||
}
|
||||
|
||||
post.Filenames = model.StringArray{"test1.png"}
|
||||
post.FileIds = model.StringArray{testPng.Id}
|
||||
store.Must(Srv.Store.FileInfo().AttachToPost(testPng.Id, post.Id))
|
||||
if getMessageForNotification(post, translateFunc) != "test" {
|
||||
t.Fatal("should've returned message text, even with attachments")
|
||||
}
|
||||
@@ -1172,18 +1227,60 @@ func TestGetMessageForNotification(t *testing.T) {
|
||||
t.Fatal("should've returned number of images:", message)
|
||||
}
|
||||
|
||||
post.Filenames = model.StringArray{"test1.png", "test2.jpg"}
|
||||
post.FileIds = model.StringArray{testPng.Id, testJpg1.Id}
|
||||
store.Must(Srv.Store.FileInfo().AttachToPost(testJpg1.Id, post.Id))
|
||||
if message := getMessageForNotification(post, translateFunc); message != "2 images sent: test1.png, test2.jpg" {
|
||||
t.Fatal("should've returned number of images:", message)
|
||||
}
|
||||
|
||||
post.Filenames = model.StringArray{"test1.go"}
|
||||
post.Id = model.NewId()
|
||||
post.FileIds = model.StringArray{testFile.Id}
|
||||
store.Must(Srv.Store.FileInfo().AttachToPost(testFile.Id, post.Id))
|
||||
if message := getMessageForNotification(post, translateFunc); message != "1 file sent: test1.go" {
|
||||
t.Fatal("should've returned number of files:", message)
|
||||
}
|
||||
|
||||
post.Filenames = model.StringArray{"test1.go", "test2.jpg"}
|
||||
if message := getMessageForNotification(post, translateFunc); message != "2 files sent: test1.go, test2.jpg" {
|
||||
store.Must(Srv.Store.FileInfo().AttachToPost(testJpg2.Id, post.Id))
|
||||
post.FileIds = model.StringArray{testFile.Id, testJpg2.Id}
|
||||
if message := getMessageForNotification(post, translateFunc); message != "2 files sent: test1.go, test3.jpg" {
|
||||
t.Fatal("should've returned number of mixed files:", message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFileInfosForPost(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
channel1 := th.BasicChannel
|
||||
|
||||
fileIds := make([]string, 3, 3)
|
||||
if data, err := readTestFile("test.png"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
for i := 0; i < 3; i++ {
|
||||
fileIds[i] = Client.MustGeneric(Client.UploadPostAttachment(data, channel1.Id, "test.png")).(*model.FileUploadResponse).FileInfos[0].Id
|
||||
}
|
||||
}
|
||||
|
||||
post1 := Client.Must(Client.CreatePost(&model.Post{
|
||||
ChannelId: channel1.Id,
|
||||
Message: "test",
|
||||
FileIds: fileIds,
|
||||
})).Data.(*model.Post)
|
||||
|
||||
var etag string
|
||||
if infos, err := Client.GetFileInfosForPost(channel1.Id, post1.Id, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(infos) != 3 {
|
||||
t.Fatal("should've received 3 files")
|
||||
} else if Client.Etag == "" {
|
||||
t.Fatal("should've received etag")
|
||||
} else {
|
||||
etag = Client.Etag
|
||||
}
|
||||
|
||||
if infos, err := Client.GetFileInfosForPost(channel1.Id, post1.Id, etag); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(infos) != 0 {
|
||||
t.Fatal("should've returned nothing because of etag")
|
||||
}
|
||||
}
|
||||
|
||||
166
i18n/en.json
166
i18n/en.json
@@ -829,14 +829,6 @@
|
||||
"id": "api.emoji.upload.large_image.app_error",
|
||||
"translation": "Unable to create emoji. Image must be at most 128 by 128 pixels."
|
||||
},
|
||||
{
|
||||
"id": "api.file.file_upload.exceeds",
|
||||
"translation": "File exceeds max image size."
|
||||
},
|
||||
{
|
||||
"id": "api.file.get_file.not_found.app_error",
|
||||
"translation": "Could not find file."
|
||||
},
|
||||
{
|
||||
"id": "api.file.get_file.public_disabled.app_error",
|
||||
"translation": "Public links have been disabled by the system administrator"
|
||||
@@ -849,30 +841,98 @@
|
||||
"id": "api.file.get_public_link.disabled.app_error",
|
||||
"translation": "Public links have been disabled"
|
||||
},
|
||||
{
|
||||
"id": "api.file.get_public_link.no_post.app_error",
|
||||
"translation": "Unable to get public link for file. File must be attached to a post that can be read by the current user."
|
||||
},
|
||||
{
|
||||
"id": "api.file.get_file_preview.no_thumbnail.app_error",
|
||||
"translation": "File doesn't have a preview image"
|
||||
},
|
||||
{
|
||||
"id": "api.file.get_file_thumbnail.no_thumbnail.app_error",
|
||||
"translation": "File doesn't have a thumbnail image"
|
||||
},
|
||||
{
|
||||
"id": "api.file.get_public_file_old.storage.app_error",
|
||||
"translation": "Unable to get file. Image storage is not configured."
|
||||
},
|
||||
{
|
||||
"id": "api.file.get_public_file_old.storage.app_error",
|
||||
"translation": "Unable to get file. Image storage is not configured."
|
||||
},
|
||||
{
|
||||
"id": "api.file.get_info_for_request.no_post.app_error",
|
||||
"translation": "Unable to get info for file. File must be attached to a post that can be read by the current user."
|
||||
},
|
||||
{
|
||||
"id": "api.file.get_info_for_request.storage.app_error",
|
||||
"translation": "Unable to get info for file. Image storage is not configured."
|
||||
},
|
||||
{
|
||||
"id": "api.file.handle_images_forget.decode.error",
|
||||
"translation": "Unable to decode image channelId=%v userId=%v filename=%v err=%v"
|
||||
"translation": "Unable to decode image err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.handle_images_forget.encode_jpeg.error",
|
||||
"translation": "Unable to encode image as jpeg channelId=%v userId=%v filename=%v err=%v"
|
||||
"translation": "Unable to encode image as jpeg path=%v err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.handle_images_forget.encode_preview.error",
|
||||
"translation": "Unable to encode image as preview jpg channelId=%v userId=%v filename=%v err=%v"
|
||||
"translation": "Unable to encode image as preview jpg path=%v err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.handle_images_forget.upload_preview.error",
|
||||
"translation": "Unable to upload preview channelId=%v userId=%v filename=%v err=%v"
|
||||
"translation": "Unable to upload preview path=%v err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.handle_images_forget.upload_thumb.error",
|
||||
"translation": "Unable to upload thumbnail channelId=%v userId=%v filename=%v err=%v"
|
||||
"translation": "Unable to upload thumbnail path=%v err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.init.debug",
|
||||
"translation": "Initializing file api routes"
|
||||
},
|
||||
{
|
||||
"id": "api.file.migrate_filenames_to_file_infos.channel.app_error",
|
||||
"translation": "Unable to get channel when migrating post to use FileInfos, post_id=%v, channel_id=%v, err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.migrate_filenames_to_file_infos.file_not_found.warn",
|
||||
"translation": "Unable to find file when migrating post to use FileInfos, post_id=%v, filename=%v, path=%v, err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.migrate_filenames_to_file_infos.info.app_error",
|
||||
"translation": "Unable to fully decode file info when migrating post to use FileInfos, post_id=%v, filename=%v, err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.migrate_filenames_to_file_infos.mismatched_filename.warn",
|
||||
"translation": "Found an unusual filename when migrating post to use FileInfos, post_id=%v, channel_id=%v, user_id=%v, filename=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.migrate_filenames_to_file_infos.no_filenames.warn",
|
||||
"translation": "Unable to migrate post to use FileInfos with an empty Filenames field, post_id=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.migrate_filenames_to_file_infos.save_file_info.warn",
|
||||
"translation": "Unable to save post when migrating post to use FileInfos, post_id=%v, new_file_ids=%v, old_filenames=%v, err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.migrate_filenames_to_file_infos.save_post.warn",
|
||||
"translation": "Unable to save file info when migrating post to use FileInfos, post_id=%v, file_id=%v, filename=%v, err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.migrate_filenames_to_file_infos.team_id.app_error",
|
||||
"translation": "Unable to find team for FileInfos, post_id=%v, filenames=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.migrate_filenames_to_file_infos.teams.app_error",
|
||||
"translation": "Unable to get teams when migrating post to use FileInfos, post_id=%v, err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.migrate_filenames_to_file_infos.unexpected_filename.error",
|
||||
"translation": "Unable to decipher filename when migrating post to use FileInfos, post_id=%v, filename=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.file.move_file.configured.app_error",
|
||||
"translation": "File storage not configured properly. Please configure for either S3 or local server file storage."
|
||||
@@ -917,22 +977,6 @@
|
||||
"id": "api.file.read_file.reading_local.app_error",
|
||||
"translation": "Encountered an error reading from local server storage"
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.image.app_error",
|
||||
"translation": "Unable to upload image file."
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.large_image.app_error",
|
||||
"translation": "Unable to upload image file. File is too large."
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.storage.app_error",
|
||||
"translation": "Unable to upload file. Image storage is not configured."
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.too_large.app_error",
|
||||
"translation": "Unable to upload file. File is too large."
|
||||
},
|
||||
{
|
||||
"id": "api.file.write_file.configured.app_error",
|
||||
"translation": "File storage not configured properly. Please configure for either S3 or local server file storage."
|
||||
@@ -949,6 +993,18 @@
|
||||
"id": "api.file.write_file_locally.writing.app_error",
|
||||
"translation": "Encountered an error writing to local server storage"
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.large_image.app_error",
|
||||
"translation": "Unable to upload image file. File is too large."
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.storage.app_error",
|
||||
"translation": "Unable to upload file. Image storage is not configured."
|
||||
},
|
||||
{
|
||||
"id": "api.file.upload_file.too_large.app_error",
|
||||
"translation": "Unable to upload file. File is too large."
|
||||
},
|
||||
{
|
||||
"id": "api.general.init.debug",
|
||||
"translation": "Initializing general api routes"
|
||||
@@ -1161,6 +1217,10 @@
|
||||
"id": "api.post.check_for_out_of_channel_mentions.message.one",
|
||||
"translation": "{{.Username}} was mentioned, but they did not receive a notification because they do not belong to this channel."
|
||||
},
|
||||
{
|
||||
"id": "api.post.create_post.attach_files.error",
|
||||
"translation": "Encountered error attaching files to post, post_id=%s, user_id=%s, file_ids=%v, err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.post.create_post.bad_filename.error",
|
||||
"translation": "Bad filename discarded, filename=%v"
|
||||
@@ -1197,6 +1257,14 @@
|
||||
"id": "api.post.delete_post.permissions.app_error",
|
||||
"translation": "You do not have the appropriate permissions"
|
||||
},
|
||||
{
|
||||
"id": "api.post.delete_post_files.app_error.warn",
|
||||
"translation": "Encountered error when deleting files for post, post_id=%v, err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.post.get_message_for_notification.get_files.error",
|
||||
"translation": "Encountered error when getting files for notification message, post_id=%v, err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.post.get_message_for_notification.files_sent",
|
||||
"translation": {
|
||||
@@ -1287,6 +1355,10 @@
|
||||
"id": "api.post.send_notifications_and_forget.comment_thread.error",
|
||||
"translation": "Failed to retrieve comment thread posts in notifications root_post_id=%v, err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.post.send_notifications_and_forget.files.error",
|
||||
"translation": "Failed to get files for post notification post_id=%v, err=%v"
|
||||
},
|
||||
{
|
||||
"id": "api.post.send_notifications_and_forget.get_teams.error",
|
||||
"translation": "Failed to get teams when sending cross-team DM user_id=%v, err=%v"
|
||||
@@ -3423,6 +3495,10 @@
|
||||
"id": "model.post.is_valid.filenames.app_error",
|
||||
"translation": "Invalid filenames"
|
||||
},
|
||||
{
|
||||
"id": "model.post.is_valid.file_ids.app_error",
|
||||
"translation": "Invalid file ids"
|
||||
},
|
||||
{
|
||||
"id": "model.post.is_valid.hashtags.app_error",
|
||||
"translation": "Invalid hashtags"
|
||||
@@ -3875,6 +3951,10 @@
|
||||
"id": "store.sql_channel.get_extra_members.app_error",
|
||||
"translation": "We couldn't get the extra info for channel members"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_channel.get_for_post.app_error",
|
||||
"translation": "We couldn't get the channel for the given post"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_channel.get_member.app_error",
|
||||
"translation": "We couldn't get the channel member"
|
||||
@@ -3887,6 +3967,10 @@
|
||||
"id": "store.sql_channel.get_member_count.app_error",
|
||||
"translation": "We couldn't get the channel member count"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_channel.get_member_for_post.app_error",
|
||||
"translation": "We couldn't get the channel member for the given post"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_channel.get_members.app_error",
|
||||
"translation": "We couldn't get the channel members"
|
||||
@@ -4071,6 +4155,30 @@
|
||||
"id": "store.sql_emoji.save.app_error",
|
||||
"translation": "We couldn't save the emoji"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_file_info.attach_to_post.app_error",
|
||||
"translation": "We couldn't attach the file info to the post"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_file_info.delete_for_post.app_error",
|
||||
"translation": "We couldn't delete the file info to the post"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_file_info.get.app_error",
|
||||
"translation": "We couldn't get the file info"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_file_info.get_by_path.app_error",
|
||||
"translation": "We couldn't get the file info by path"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_file_info.get_for_post.app_error",
|
||||
"translation": "We couldn't get the file info for the post"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_file_info.save.app_error",
|
||||
"translation": "We couldn't save the file info"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_license.get.app_error",
|
||||
"translation": "We encountered an error getting the license"
|
||||
|
||||
119
model/client.go
119
model/client.go
@@ -124,6 +124,10 @@ func (c *Client) GetGeneralRoute() string {
|
||||
return "/general"
|
||||
}
|
||||
|
||||
func (c *Client) GetFileRoute(fileId string) string {
|
||||
return fmt.Sprintf("/files/%v", fileId)
|
||||
}
|
||||
|
||||
func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppError) {
|
||||
rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data))
|
||||
rq.Header.Set("Content-Type", contentType)
|
||||
@@ -1289,8 +1293,33 @@ func (c *Client) UploadProfileFile(data []byte, contentType string) (*Result, *A
|
||||
return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType)
|
||||
}
|
||||
|
||||
func (c *Client) UploadPostAttachment(data []byte, contentType string) (*Result, *AppError) {
|
||||
return c.uploadFile(c.ApiUrl+c.GetTeamRoute()+"/files/upload", data, contentType)
|
||||
func (c *Client) UploadPostAttachment(data []byte, channelId string, filename string) (*FileUploadResponse, *AppError) {
|
||||
c.clearExtraProperties()
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
if part, err := writer.CreateFormFile("files", filename); err != nil {
|
||||
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.file.app_error", nil, err.Error())
|
||||
} else if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
|
||||
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.file.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
if part, err := writer.CreateFormField("channel_id"); err != nil {
|
||||
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.channel_id.app_error", nil, err.Error())
|
||||
} else if _, err = io.Copy(part, strings.NewReader(channelId)); err != nil {
|
||||
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.channel_id.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.writer.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
if result, err := c.uploadFile(c.ApiUrl+c.GetTeamRoute()+"/files/upload", body.Bytes(), writer.FormDataContentType()); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.Data.(*FileUploadResponse), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) uploadFile(url string, data []byte, contentType string) (*Result, *AppError) {
|
||||
@@ -1312,55 +1341,51 @@ func (c *Client) uploadFile(url string, data []byte, contentType string) (*Resul
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) {
|
||||
var rq *http.Request
|
||||
if isFullUrl {
|
||||
rq, _ = http.NewRequest("GET", url, nil)
|
||||
func (c *Client) GetFile(fileId string) (io.ReadCloser, *AppError) {
|
||||
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get", "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
rq, _ = http.NewRequest("GET", c.ApiUrl+c.GetTeamRoute()+"/files/get"+url, nil)
|
||||
}
|
||||
|
||||
if len(c.AuthToken) > 0 {
|
||||
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
|
||||
}
|
||||
|
||||
if rp, err := c.HttpClient.Do(rq); err != nil {
|
||||
return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
|
||||
} else if rp.StatusCode >= 300 {
|
||||
return nil, AppErrorFromJson(rp.Body)
|
||||
} else {
|
||||
defer closeBody(rp)
|
||||
return &Result{rp.Header.Get(HEADER_REQUEST_ID),
|
||||
rp.Header.Get(HEADER_ETAG_SERVER), rp.Body}, nil
|
||||
c.fillInExtraProperties(r)
|
||||
return r.Body, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetFileInfo(url string) (*Result, *AppError) {
|
||||
var rq *http.Request
|
||||
rq, _ = http.NewRequest("GET", c.ApiUrl+c.GetTeamRoute()+"/files/get_info"+url, nil)
|
||||
|
||||
if len(c.AuthToken) > 0 {
|
||||
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
|
||||
}
|
||||
|
||||
if rp, err := c.HttpClient.Do(rq); err != nil {
|
||||
return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
|
||||
} else if rp.StatusCode >= 300 {
|
||||
return nil, AppErrorFromJson(rp.Body)
|
||||
func (c *Client) GetFileThumbnail(fileId string) (io.ReadCloser, *AppError) {
|
||||
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_thumbnail", "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer closeBody(rp)
|
||||
return &Result{rp.Header.Get(HEADER_REQUEST_ID),
|
||||
rp.Header.Get(HEADER_ETAG_SERVER), FileInfoFromJson(rp.Body)}, nil
|
||||
c.fillInExtraProperties(r)
|
||||
return r.Body, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetPublicLink(filename string) (*Result, *AppError) {
|
||||
if r, err := c.DoApiPost(c.GetTeamRoute()+"/files/get_public_link", MapToJson(map[string]string{"filename": filename})); err != nil {
|
||||
func (c *Client) GetFilePreview(fileId string) (io.ReadCloser, *AppError) {
|
||||
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_preview", "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
r.Header.Get(HEADER_ETAG_SERVER), StringFromJson(r.Body)}, nil
|
||||
c.fillInExtraProperties(r)
|
||||
return r.Body, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetFileInfo(fileId string) (*FileInfo, *AppError) {
|
||||
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_info", "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
c.fillInExtraProperties(r)
|
||||
return FileInfoFromJson(r.Body), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetPublicLink(fileId string) (string, *AppError) {
|
||||
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_public_link", "", ""); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
c.fillInExtraProperties(r)
|
||||
return StringFromJson(r.Body), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1930,3 +1955,17 @@ func (c *Client) GetWebrtcToken() (map[string]string, *AppError) {
|
||||
return MapFromJson(r.Body), nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetFileInfosForPost returns a list of FileInfo objects for a given post id, if successful.
|
||||
// Otherwise, it returns an error.
|
||||
func (c *Client) GetFileInfosForPost(channelId string, postId string, etag string) ([]*FileInfo, *AppError) {
|
||||
c.clearExtraProperties()
|
||||
|
||||
if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/get_file_infos", postId), "", etag); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
c.fillInExtraProperties(r)
|
||||
return FileInfosFromJson(r.Body), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ type CompliancePost struct {
|
||||
PostType string
|
||||
PostProps string
|
||||
PostHashtags string
|
||||
PostFilenames string
|
||||
PostFileIds string
|
||||
}
|
||||
|
||||
func CompliancePostHeader() []string {
|
||||
@@ -60,7 +60,7 @@ func CompliancePostHeader() []string {
|
||||
"PostType",
|
||||
"PostProps",
|
||||
"PostHashtags",
|
||||
"PostFilenames",
|
||||
"PostFileIds",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,6 @@ func (me *CompliancePost) Row() []string {
|
||||
me.PostType,
|
||||
me.PostProps,
|
||||
me.PostHashtags,
|
||||
me.PostFilenames,
|
||||
me.PostFileIds,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestCompliancePostHeader(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCompliancePost(t *testing.T) {
|
||||
o := CompliancePost{TeamName: "test", PostFilenames: "files", PostCreateAt: GetMillis()}
|
||||
o := CompliancePost{TeamName: "test", PostFileIds: "files", PostCreateAt: GetMillis()}
|
||||
r := o.Row()
|
||||
|
||||
if r[0] != "test" {
|
||||
|
||||
@@ -14,8 +14,8 @@ var (
|
||||
)
|
||||
|
||||
type FileUploadResponse struct {
|
||||
Filenames []string `json:"filenames"`
|
||||
ClientIds []string `json:"client_ids"`
|
||||
FileInfos []*FileInfo `json:"file_infos"`
|
||||
ClientIds []string `json:"client_ids"`
|
||||
}
|
||||
|
||||
func FileUploadResponseFromJson(data io.Reader) *FileUploadResponse {
|
||||
|
||||
@@ -6,54 +6,31 @@ package model
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"image"
|
||||
"image/gif"
|
||||
"io"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FileInfo struct {
|
||||
Filename string `json:"filename"`
|
||||
Size int `json:"size"`
|
||||
Id string `json:"id"`
|
||||
CreatorId string `json:"user_id"`
|
||||
PostId string `json:"post_id,omitempty"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
DeleteAt int64 `json:"delete_at"`
|
||||
Path string `json:"-"` // not sent back to the client
|
||||
ThumbnailPath string `json:"-"` // not sent back to the client
|
||||
PreviewPath string `json:"-"` // not sent back to the client
|
||||
Name string `json:"name"`
|
||||
Extension string `json:"extension"`
|
||||
Size int64 `json:"size"`
|
||||
MimeType string `json:"mime_type"`
|
||||
HasPreviewImage bool `json:"has_preview_image"`
|
||||
}
|
||||
|
||||
func GetInfoForBytes(filename string, data []byte) (*FileInfo, *AppError) {
|
||||
size := len(data)
|
||||
|
||||
var mimeType string
|
||||
extension := filepath.Ext(filename)
|
||||
isImage := IsFileExtImage(extension)
|
||||
if isImage {
|
||||
mimeType = GetImageMimeType(extension)
|
||||
} else {
|
||||
mimeType = mime.TypeByExtension(extension)
|
||||
}
|
||||
|
||||
if extension != "" && extension[0] == '.' {
|
||||
// the client expects a file extension without the leading period
|
||||
extension = extension[1:]
|
||||
}
|
||||
|
||||
hasPreviewImage := isImage
|
||||
if mimeType == "image/gif" {
|
||||
// just show the gif itself instead of a preview image for animated gifs
|
||||
if gifImage, err := gif.DecodeAll(bytes.NewReader(data)); err != nil {
|
||||
return nil, NewLocAppError("GetInfoForBytes", "model.file_info.get.gif.app_error", nil, "filename="+filename)
|
||||
} else {
|
||||
hasPreviewImage = len(gifImage.Image) == 1
|
||||
}
|
||||
}
|
||||
|
||||
return &FileInfo{
|
||||
Filename: filename,
|
||||
Size: size,
|
||||
Extension: extension,
|
||||
MimeType: mimeType,
|
||||
HasPreviewImage: hasPreviewImage,
|
||||
}, nil
|
||||
Width int `json:"width,omitempty"`
|
||||
Height int `json:"height,omitempty"`
|
||||
HasPreviewImage bool `json:"has_preview_image,omitempty"`
|
||||
}
|
||||
|
||||
func (info *FileInfo) ToJson() string {
|
||||
@@ -75,3 +52,123 @@ func FileInfoFromJson(data io.Reader) *FileInfo {
|
||||
return &info
|
||||
}
|
||||
}
|
||||
|
||||
func FileInfosToJson(infos []*FileInfo) string {
|
||||
b, err := json.Marshal(infos)
|
||||
if err != nil {
|
||||
return ""
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func FileInfosFromJson(data io.Reader) []*FileInfo {
|
||||
decoder := json.NewDecoder(data)
|
||||
|
||||
var infos []*FileInfo
|
||||
if err := decoder.Decode(&infos); err != nil {
|
||||
return nil
|
||||
} else {
|
||||
return infos
|
||||
}
|
||||
}
|
||||
|
||||
func (o *FileInfo) PreSave() {
|
||||
if o.Id == "" {
|
||||
o.Id = NewId()
|
||||
}
|
||||
|
||||
if o.CreateAt == 0 {
|
||||
o.CreateAt = GetMillis()
|
||||
o.UpdateAt = o.CreateAt
|
||||
}
|
||||
}
|
||||
|
||||
func (o *FileInfo) IsValid() *AppError {
|
||||
if len(o.Id) != 26 {
|
||||
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.id.app_error", nil, "")
|
||||
}
|
||||
|
||||
if len(o.CreatorId) != 26 {
|
||||
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.user_id.app_error", nil, "id="+o.Id)
|
||||
}
|
||||
|
||||
if len(o.PostId) != 0 && len(o.PostId) != 26 {
|
||||
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.post_id.app_error", nil, "id="+o.Id)
|
||||
}
|
||||
|
||||
if o.CreateAt == 0 {
|
||||
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.create_at.app_error", nil, "id="+o.Id)
|
||||
}
|
||||
|
||||
if o.UpdateAt == 0 {
|
||||
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.update_at.app_error", nil, "id="+o.Id)
|
||||
}
|
||||
|
||||
if o.Path == "" {
|
||||
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.path.app_error", nil, "id="+o.Id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *FileInfo) IsImage() bool {
|
||||
return strings.HasPrefix(o.MimeType, "image")
|
||||
}
|
||||
|
||||
func GetInfoForBytes(name string, data []byte) (*FileInfo, *AppError) {
|
||||
info := &FileInfo{
|
||||
Name: name,
|
||||
Size: int64(len(data)),
|
||||
}
|
||||
var err *AppError
|
||||
|
||||
extension := strings.ToLower(filepath.Ext(name))
|
||||
info.MimeType = mime.TypeByExtension(extension)
|
||||
|
||||
if extension != "" && extension[0] == '.' {
|
||||
// The client expects a file extension without the leading period
|
||||
info.Extension = extension[1:]
|
||||
} else {
|
||||
info.Extension = extension
|
||||
}
|
||||
|
||||
if info.IsImage() {
|
||||
// Only set the width and height if it's actually an image that we can understand
|
||||
if config, _, err := image.DecodeConfig(bytes.NewReader(data)); err == nil {
|
||||
info.Width = config.Width
|
||||
info.Height = config.Height
|
||||
|
||||
if info.MimeType == "image/gif" {
|
||||
// Just show the gif itself instead of a preview image for animated gifs
|
||||
if gifConfig, err := gif.DecodeAll(bytes.NewReader(data)); err != nil {
|
||||
// Still return the rest of the info even though it doesn't appear to be an actual gif
|
||||
info.HasPreviewImage = true
|
||||
err = NewLocAppError("GetInfoForBytes", "model.file_info.get.gif.app_error", nil, "name="+name)
|
||||
} else {
|
||||
info.HasPreviewImage = len(gifConfig.Image) == 1
|
||||
}
|
||||
} else {
|
||||
info.HasPreviewImage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info, err
|
||||
}
|
||||
|
||||
func GetEtagForFileInfos(infos []*FileInfo) string {
|
||||
if len(infos) == 0 {
|
||||
return Etag()
|
||||
}
|
||||
|
||||
var maxUpdateAt int64
|
||||
|
||||
for _, info := range infos {
|
||||
if info.UpdateAt > maxUpdateAt {
|
||||
maxUpdateAt = info.UpdateAt
|
||||
}
|
||||
}
|
||||
|
||||
return Etag(infos[0].PostId, maxUpdateAt)
|
||||
}
|
||||
|
||||
@@ -5,56 +5,137 @@ package model
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
_ "image/gif"
|
||||
_ "image/png"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetInfoForBytes(t *testing.T) {
|
||||
func TestFileInfoIsValid(t *testing.T) {
|
||||
info := &FileInfo{
|
||||
Id: NewId(),
|
||||
CreatorId: NewId(),
|
||||
CreateAt: 1234,
|
||||
UpdateAt: 1234,
|
||||
PostId: "",
|
||||
Path: "fake/path.png",
|
||||
}
|
||||
|
||||
if err := info.IsValid(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
info.Id = ""
|
||||
if err := info.IsValid(); err == nil {
|
||||
t.Fatal("empty Id isn't valid")
|
||||
}
|
||||
|
||||
info.Id = NewId()
|
||||
info.CreateAt = 0
|
||||
if err := info.IsValid(); err == nil {
|
||||
t.Fatal("empty CreateAt isn't valid")
|
||||
}
|
||||
|
||||
info.CreateAt = 1234
|
||||
info.UpdateAt = 0
|
||||
if err := info.IsValid(); err == nil {
|
||||
t.Fatal("empty UpdateAt isn't valid")
|
||||
}
|
||||
|
||||
info.UpdateAt = 1234
|
||||
info.PostId = NewId()
|
||||
if err := info.IsValid(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
info.Path = ""
|
||||
if err := info.IsValid(); err == nil {
|
||||
t.Fatal("empty Path isn't valid")
|
||||
}
|
||||
|
||||
info.Path = "fake/path.png"
|
||||
if err := info.IsValid(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileInfoIsImage(t *testing.T) {
|
||||
info := &FileInfo{
|
||||
MimeType: "image/png",
|
||||
}
|
||||
|
||||
if !info.IsImage() {
|
||||
t.Fatal("file is an image")
|
||||
}
|
||||
|
||||
info.MimeType = "text/plain"
|
||||
if info.IsImage() {
|
||||
t.Fatal("file is not an image")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInfoForFile(t *testing.T) {
|
||||
fakeFile := make([]byte, 1000)
|
||||
|
||||
if info, err := GetInfoForBytes("file.txt", fakeFile); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if info.Filename != "file.txt" {
|
||||
t.Fatalf("Got incorrect filename: %v", info.Filename)
|
||||
} else if info.Name != "file.txt" {
|
||||
t.Fatalf("Got incorrect filename: %v", info.Name)
|
||||
} else if info.Extension != "txt" {
|
||||
t.Fatalf("Got incorrect extension: %v", info.Extension)
|
||||
} else if info.Size != 1000 {
|
||||
t.Fatalf("Got incorrect size: %v", info.Size)
|
||||
} else if info.Extension != "txt" {
|
||||
t.Fatalf("Got incorrect file extension: %v", info.Extension)
|
||||
} else if !strings.HasPrefix(info.MimeType, "text/plain") {
|
||||
t.Fatalf("Got incorrect mime type: %v", info.MimeType)
|
||||
} else if info.Width != 0 {
|
||||
t.Fatalf("Got incorrect width: %v", info.Width)
|
||||
} else if info.Height != 0 {
|
||||
t.Fatalf("Got incorrect height: %v", info.Height)
|
||||
} else if info.HasPreviewImage {
|
||||
t.Fatalf("Got HasPreviewImage = true for non-image file")
|
||||
t.Fatalf("Got incorrect has preview image: %v", info.HasPreviewImage)
|
||||
}
|
||||
|
||||
if info, err := GetInfoForBytes("file.png", fakeFile); err != nil {
|
||||
pngFile, err := ioutil.ReadFile("../tests/test.png")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load test.png: %v", err.Error())
|
||||
}
|
||||
if info, err := GetInfoForBytes("test.png", pngFile); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if info.Filename != "file.png" {
|
||||
t.Fatalf("Got incorrect filename: %v", info.Filename)
|
||||
} else if info.Size != 1000 {
|
||||
t.Fatalf("Got incorrect size: %v", info.Size)
|
||||
} else if info.Name != "test.png" {
|
||||
t.Fatalf("Got incorrect filename: %v", info.Name)
|
||||
} else if info.Extension != "png" {
|
||||
t.Fatalf("Got incorrect file extension: %v", info.Extension)
|
||||
t.Fatalf("Got incorrect extension: %v", info.Extension)
|
||||
} else if info.Size != 279591 {
|
||||
t.Fatalf("Got incorrect size: %v", info.Size)
|
||||
} else if info.MimeType != "image/png" {
|
||||
t.Fatalf("Got incorrect mime type: %v", info.MimeType)
|
||||
} else if info.Width != 408 {
|
||||
t.Fatalf("Got incorrect width: %v", info.Width)
|
||||
} else if info.Height != 336 {
|
||||
t.Fatalf("Got incorrect height: %v", info.Height)
|
||||
} else if !info.HasPreviewImage {
|
||||
t.Fatalf("Got HasPreviewImage = false for image")
|
||||
t.Fatalf("Got incorrect has preview image: %v", info.HasPreviewImage)
|
||||
}
|
||||
|
||||
// base 64 encoded version of handtinywhite.gif from http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever
|
||||
gifFile, _ := base64.StdEncoding.DecodeString("R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=")
|
||||
if info, err := GetInfoForBytes("handtinywhite.gif", gifFile); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if info.Filename != "handtinywhite.gif" {
|
||||
t.Fatalf("Got incorrect filename: %v", info.Filename)
|
||||
} else if info.Name != "handtinywhite.gif" {
|
||||
t.Fatalf("Got incorrect filename: %v", info.Name)
|
||||
} else if info.Extension != "gif" {
|
||||
t.Fatalf("Got incorrect extension: %v", info.Extension)
|
||||
} else if info.Size != 35 {
|
||||
t.Fatalf("Got incorrect size: %v", info.Size)
|
||||
} else if info.Extension != "gif" {
|
||||
t.Fatalf("Got incorrect file extension: %v", info.Extension)
|
||||
} else if info.MimeType != "image/gif" {
|
||||
t.Fatalf("Got incorrect mime type: %v", info.MimeType)
|
||||
} else if info.Width != 1 {
|
||||
t.Fatalf("Got incorrect width: %v", info.Width)
|
||||
} else if info.Height != 1 {
|
||||
t.Fatalf("Got incorrect height: %v", info.Height)
|
||||
} else if !info.HasPreviewImage {
|
||||
t.Fatalf("Got HasPreviewImage = false for static gif")
|
||||
t.Fatalf("Got incorrect has preview image: %v", info.HasPreviewImage)
|
||||
}
|
||||
|
||||
animatedGifFile, err := ioutil.ReadFile("../tests/testgif.gif")
|
||||
@@ -63,29 +144,57 @@ func TestGetInfoForBytes(t *testing.T) {
|
||||
}
|
||||
if info, err := GetInfoForBytes("testgif.gif", animatedGifFile); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if info.Filename != "testgif.gif" {
|
||||
t.Fatalf("Got incorrect filename: %v", info.Filename)
|
||||
} else if info.Name != "testgif.gif" {
|
||||
t.Fatalf("Got incorrect filename: %v", info.Name)
|
||||
} else if info.Extension != "gif" {
|
||||
t.Fatalf("Got incorrect extension: %v", info.Extension)
|
||||
} else if info.Size != 38689 {
|
||||
t.Fatalf("Got incorrect size: %v", info.Size)
|
||||
} else if info.Extension != "gif" {
|
||||
t.Fatalf("Got incorrect file extension: %v", info.Extension)
|
||||
} else if info.MimeType != "image/gif" {
|
||||
t.Fatalf("Got incorrect mime type: %v", info.MimeType)
|
||||
} else if info.Width != 118 {
|
||||
t.Fatalf("Got incorrect width: %v", info.Width)
|
||||
} else if info.Height != 118 {
|
||||
t.Fatalf("Got incorrect height: %v", info.Height)
|
||||
} else if info.HasPreviewImage {
|
||||
t.Fatalf("Got HasPreviewImage = true for animated gif")
|
||||
t.Fatalf("Got incorrect has preview image: %v", info.HasPreviewImage)
|
||||
}
|
||||
|
||||
if info, err := GetInfoForBytes("filewithoutextension", fakeFile); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if info.Filename != "filewithoutextension" {
|
||||
t.Fatalf("Got incorrect filename: %v", info.Filename)
|
||||
} else if info.Name != "filewithoutextension" {
|
||||
t.Fatalf("Got incorrect filename: %v", info.Name)
|
||||
} else if info.Extension != "" {
|
||||
t.Fatalf("Got incorrect extension: %v", info.Extension)
|
||||
} else if info.Size != 1000 {
|
||||
t.Fatalf("Got incorrect size: %v", info.Size)
|
||||
} else if info.Extension != "" {
|
||||
t.Fatalf("Got incorrect file extension: %v", info.Extension)
|
||||
} else if info.MimeType != "" {
|
||||
t.Fatalf("Got incorrect mime type: %v", info.MimeType)
|
||||
} else if info.Width != 0 {
|
||||
t.Fatalf("Got incorrect width: %v", info.Width)
|
||||
} else if info.Height != 0 {
|
||||
t.Fatalf("Got incorrect height: %v", info.Height)
|
||||
} else if info.HasPreviewImage {
|
||||
t.Fatalf("Got HasPreviewImage = true for non-image file")
|
||||
t.Fatalf("Got incorrect has preview image: %v", info.HasPreviewImage)
|
||||
}
|
||||
|
||||
// Always make the extension lower case to make it easier to use in other places
|
||||
if info, err := GetInfoForBytes("file.TXT", fakeFile); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if info.Name != "file.TXT" {
|
||||
t.Fatalf("Got incorrect filename: %v", info.Name)
|
||||
} else if info.Extension != "txt" {
|
||||
t.Fatalf("Got incorrect extension: %v", info.Extension)
|
||||
}
|
||||
|
||||
// Don't error out for image formats we don't support
|
||||
if info, err := GetInfoForBytes("file.tif", fakeFile); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if info.Name != "file.tif" {
|
||||
t.Fatalf("Got incorrect filename: %v", info.Name)
|
||||
} else if info.Extension != "tif" {
|
||||
t.Fatalf("Got incorrect extension: %v", info.Extension)
|
||||
} else if info.MimeType != "image/tiff" && info.MimeType != "image/x-tiff" {
|
||||
t.Fatalf("Got incorrect mime type: %v", info.MimeType)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ type Post struct {
|
||||
Type string `json:"type"`
|
||||
Props StringInterface `json:"props"`
|
||||
Hashtags string `json:"hashtags"`
|
||||
Filenames StringArray `json:"filenames"`
|
||||
Filenames StringArray `json:"filenames,omitempty"` // Deprecated, do not use this field any more
|
||||
FileIds StringArray `json:"file_ids,omitempty"`
|
||||
PendingPostId string `json:"pending_post_id" db:"-"`
|
||||
}
|
||||
|
||||
@@ -118,6 +119,10 @@ func (o *Post) IsValid() *AppError {
|
||||
return NewLocAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id)
|
||||
}
|
||||
|
||||
if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > 150 {
|
||||
return NewLocAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id)
|
||||
}
|
||||
|
||||
if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > 8000 {
|
||||
return NewLocAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id)
|
||||
}
|
||||
@@ -145,15 +150,16 @@ func (o *Post) PreSave() {
|
||||
if o.Filenames == nil {
|
||||
o.Filenames = []string{}
|
||||
}
|
||||
|
||||
if o.FileIds == nil {
|
||||
o.FileIds = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Post) MakeNonNil() {
|
||||
if o.Props == nil {
|
||||
o.Props = make(map[string]interface{})
|
||||
}
|
||||
if o.Filenames == nil {
|
||||
o.Filenames = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Post) AddProp(key string, value interface{}) {
|
||||
|
||||
@@ -596,6 +596,36 @@ func (s SqlChannelStore) GetMember(channelId string, userId string) StoreChannel
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) GetMemberForPost(postId string, userId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
member := &model.ChannelMember{}
|
||||
if err := s.GetReplica().SelectOne(
|
||||
member,
|
||||
`SELECT
|
||||
ChannelMembers.*
|
||||
FROM
|
||||
ChannelMembers,
|
||||
Posts
|
||||
WHERE
|
||||
ChannelMembers.ChannelId = Posts.ChannelId
|
||||
AND ChannelMembers.UserId = :UserId
|
||||
AND Posts.Id = :PostId`, map[string]interface{}{"UserId": userId, "PostId": postId}); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlChannelStore.GetMemberForPost", "store.sql_channel.get_member_for_post.app_error", nil, "postId="+postId+", err="+err.Error())
|
||||
} else {
|
||||
result.Data = member
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) GetMemberCount(channelId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
@@ -878,6 +908,35 @@ func (s SqlChannelStore) GetAll(teamId string) StoreChannel {
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) GetForPost(postId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
channel := &model.Channel{}
|
||||
if err := s.GetReplica().SelectOne(
|
||||
channel,
|
||||
`SELECT
|
||||
Channels.*
|
||||
FROM
|
||||
Channels,
|
||||
Posts
|
||||
WHERE
|
||||
Channels.Id = Posts.ChannelId
|
||||
AND Posts.Id = :PostId`, map[string]interface{}{"PostId": postId}); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlChannelStore.GetForPost", "store.sql_channel.get_for_post.app_error", nil, "postId="+postId+", err="+err.Error())
|
||||
} else {
|
||||
result.Data = channel
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) AnalyticsTypeCount(teamId string, channelType string) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
|
||||
@@ -197,6 +197,29 @@ func TestChannelStoreGet(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelStoreGetForPost(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
o1 := Must(store.Channel().Save(&model.Channel{
|
||||
TeamId: model.NewId(),
|
||||
DisplayName: "Name",
|
||||
Name: "a" + model.NewId() + "b",
|
||||
Type: model.CHANNEL_OPEN,
|
||||
})).(*model.Channel)
|
||||
|
||||
p1 := Must(store.Post().Save(&model.Post{
|
||||
UserId: model.NewId(),
|
||||
ChannelId: o1.Id,
|
||||
Message: "test",
|
||||
})).(*model.Post)
|
||||
|
||||
if r1 := <-store.Channel().GetForPost(p1.Id); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else if r1.Data.(*model.Channel).Id != o1.Id {
|
||||
t.Fatal("incorrect channel returned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelStoreDelete(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
@@ -745,6 +768,39 @@ func TestGetMember(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelStoreGetMemberForPost(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
o1 := Must(store.Channel().Save(&model.Channel{
|
||||
TeamId: model.NewId(),
|
||||
DisplayName: "Name",
|
||||
Name: "a" + model.NewId() + "b",
|
||||
Type: model.CHANNEL_OPEN,
|
||||
})).(*model.Channel)
|
||||
|
||||
m1 := Must(store.Channel().SaveMember(&model.ChannelMember{
|
||||
ChannelId: o1.Id,
|
||||
UserId: model.NewId(),
|
||||
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
||||
})).(*model.ChannelMember)
|
||||
|
||||
p1 := Must(store.Post().Save(&model.Post{
|
||||
UserId: model.NewId(),
|
||||
ChannelId: o1.Id,
|
||||
Message: "test",
|
||||
})).(*model.Post)
|
||||
|
||||
if r1 := <-store.Channel().GetMemberForPost(p1.Id, m1.UserId); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else if r1.Data.(*model.ChannelMember).ToJson() != m1.ToJson() {
|
||||
t.Fatal("invalid returned channel member")
|
||||
}
|
||||
|
||||
if r2 := <-store.Channel().GetMemberForPost(p1.Id, model.NewId()); r2.Err == nil {
|
||||
t.Fatal("shouldn't have returned a member")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMemberCount(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ func (s SqlComplianceStore) ComplianceExport(job *model.Compliance) StoreChannel
|
||||
Posts.Type AS PostType,
|
||||
Posts.Props AS PostProps,
|
||||
Posts.Hashtags AS PostHashtags,
|
||||
Posts.Filenames AS PostFilenames
|
||||
Posts.FileIds AS PostFileIds
|
||||
FROM
|
||||
Teams,
|
||||
Channels,
|
||||
|
||||
197
store/sql_file_info_store.go
Обычный файл
197
store/sql_file_info_store.go
Обычный файл
@@ -0,0 +1,197 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
type SqlFileInfoStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func NewSqlFileInfoStore(sqlStore *SqlStore) FileInfoStore {
|
||||
s := &SqlFileInfoStore{sqlStore}
|
||||
|
||||
for _, db := range sqlStore.GetAllConns() {
|
||||
table := db.AddTableWithName(model.FileInfo{}, "FileInfo").SetKeys(false, "Id")
|
||||
table.ColMap("Id").SetMaxSize(26)
|
||||
table.ColMap("CreatorId").SetMaxSize(26)
|
||||
table.ColMap("PostId").SetMaxSize(26)
|
||||
table.ColMap("Path").SetMaxSize(512)
|
||||
table.ColMap("ThumbnailPath").SetMaxSize(512)
|
||||
table.ColMap("PreviewPath").SetMaxSize(512)
|
||||
table.ColMap("Name").SetMaxSize(256)
|
||||
table.ColMap("Extension").SetMaxSize(64)
|
||||
table.ColMap("MimeType").SetMaxSize(256)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) CreateIndexesIfNotExists() {
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) Save(info *model.FileInfo) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
info.PreSave()
|
||||
if result.Err = info.IsValid(); result.Err != nil {
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
return
|
||||
}
|
||||
|
||||
if err := fs.GetMaster().Insert(info); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlFileInfoStore.Save", "store.sql_file_info.save.app_error", nil, err.Error())
|
||||
} else {
|
||||
result.Data = info
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) Get(id string) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
info := &model.FileInfo{}
|
||||
|
||||
if err := fs.GetReplica().SelectOne(info,
|
||||
`SELECT
|
||||
*
|
||||
FROM
|
||||
FileInfo
|
||||
WHERE
|
||||
Id = :Id
|
||||
AND DeleteAt = 0`, map[string]interface{}{"Id": id}); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlFileInfoStore.Get", "store.sql_file_info.get.app_error", nil, "id="+id+", "+err.Error())
|
||||
} else {
|
||||
result.Data = info
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) GetByPath(path string) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
info := &model.FileInfo{}
|
||||
|
||||
if err := fs.GetReplica().SelectOne(info,
|
||||
`SELECT
|
||||
*
|
||||
FROM
|
||||
FileInfo
|
||||
WHERE
|
||||
Path = :Path
|
||||
AND DeleteAt = 0`, 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())
|
||||
} else {
|
||||
result.Data = info
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) GetForPost(postId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
var infos []*model.FileInfo
|
||||
|
||||
if _, err := fs.GetReplica().Select(&infos,
|
||||
`SELECT
|
||||
*
|
||||
FROM
|
||||
FileInfo
|
||||
WHERE
|
||||
PostId = :PostId
|
||||
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())
|
||||
} else {
|
||||
result.Data = infos
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) AttachToPost(fileId, postId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
if _, err := fs.GetMaster().Exec(
|
||||
`UPDATE
|
||||
FileInfo
|
||||
SET
|
||||
PostId = :PostId
|
||||
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())
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) DeleteForPost(postId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
if _, err := fs.GetMaster().Exec(
|
||||
`UPDATE
|
||||
FileInfo
|
||||
SET
|
||||
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())
|
||||
} else {
|
||||
result.Data = postId
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
208
store/sql_file_info_store_test.go
Обычный файл
208
store/sql_file_info_store_test.go
Обычный файл
@@ -0,0 +1,208 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
func TestFileInfoSaveGet(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
info := &model.FileInfo{
|
||||
CreatorId: model.NewId(),
|
||||
Path: "file.txt",
|
||||
}
|
||||
|
||||
if result := <-store.FileInfo().Save(info); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if returned := result.Data.(*model.FileInfo); len(returned.Id) == 0 {
|
||||
t.Fatal("should've assigned an id to FileInfo")
|
||||
} else {
|
||||
info = returned
|
||||
}
|
||||
|
||||
if result := <-store.FileInfo().Get(info.Id); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if returned := result.Data.(*model.FileInfo); returned.Id != info.Id {
|
||||
t.Log(info)
|
||||
t.Log(returned)
|
||||
t.Fatal("should've returned correct FileInfo")
|
||||
}
|
||||
|
||||
info2 := Must(store.FileInfo().Save(&model.FileInfo{
|
||||
CreatorId: model.NewId(),
|
||||
Path: "file.txt",
|
||||
DeleteAt: 123,
|
||||
})).(*model.FileInfo)
|
||||
|
||||
if result := <-store.FileInfo().Get(info2.Id); result.Err == nil {
|
||||
t.Fatal("shouldn't have gotten deleted file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileInfoSaveGetByPath(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
info := &model.FileInfo{
|
||||
CreatorId: model.NewId(),
|
||||
Path: fmt.Sprintf("%v/file.txt", model.NewId()),
|
||||
}
|
||||
|
||||
if result := <-store.FileInfo().Save(info); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if returned := result.Data.(*model.FileInfo); len(returned.Id) == 0 {
|
||||
t.Fatal("should've assigned an id to FileInfo")
|
||||
} else {
|
||||
info = returned
|
||||
}
|
||||
|
||||
if result := <-store.FileInfo().GetByPath(info.Path); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if returned := result.Data.(*model.FileInfo); returned.Id != info.Id {
|
||||
t.Log(info)
|
||||
t.Log(returned)
|
||||
t.Fatal("should've returned correct FileInfo")
|
||||
}
|
||||
|
||||
info2 := Must(store.FileInfo().Save(&model.FileInfo{
|
||||
CreatorId: model.NewId(),
|
||||
Path: "file.txt",
|
||||
DeleteAt: 123,
|
||||
})).(*model.FileInfo)
|
||||
|
||||
if result := <-store.FileInfo().GetByPath(info2.Id); result.Err == nil {
|
||||
t.Fatal("shouldn't have gotten deleted file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileInfoGetForPost(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
userId := model.NewId()
|
||||
postId := model.NewId()
|
||||
|
||||
infos := []*model.FileInfo{
|
||||
{
|
||||
PostId: postId,
|
||||
CreatorId: userId,
|
||||
Path: "file.txt",
|
||||
},
|
||||
{
|
||||
PostId: postId,
|
||||
CreatorId: userId,
|
||||
Path: "file.txt",
|
||||
},
|
||||
{
|
||||
PostId: postId,
|
||||
CreatorId: userId,
|
||||
Path: "file.txt",
|
||||
DeleteAt: 123,
|
||||
},
|
||||
{
|
||||
PostId: model.NewId(),
|
||||
CreatorId: userId,
|
||||
Path: "file.txt",
|
||||
},
|
||||
}
|
||||
|
||||
for i, info := range infos {
|
||||
infos[i] = Must(store.FileInfo().Save(info)).(*model.FileInfo)
|
||||
}
|
||||
|
||||
if result := <-store.FileInfo().GetForPost(postId); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if returned := result.Data.([]*model.FileInfo); len(returned) != 2 {
|
||||
t.Fatal("should've returned exactly 2 file infos")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileInfoAttachToPost(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
userId := model.NewId()
|
||||
postId := model.NewId()
|
||||
|
||||
info1 := Must(store.FileInfo().Save(&model.FileInfo{
|
||||
CreatorId: userId,
|
||||
Path: "file.txt",
|
||||
})).(*model.FileInfo)
|
||||
|
||||
if len(info1.PostId) != 0 {
|
||||
t.Fatal("file shouldn't have a PostId")
|
||||
}
|
||||
|
||||
if result := <-store.FileInfo().AttachToPost(info1.Id, postId); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else {
|
||||
info1 = Must(store.FileInfo().Get(info1.Id)).(*model.FileInfo)
|
||||
}
|
||||
|
||||
if len(info1.PostId) == 0 {
|
||||
t.Fatal("file should now have a PostId")
|
||||
}
|
||||
|
||||
info2 := Must(store.FileInfo().Save(&model.FileInfo{
|
||||
CreatorId: userId,
|
||||
Path: "file.txt",
|
||||
})).(*model.FileInfo)
|
||||
|
||||
if result := <-store.FileInfo().AttachToPost(info2.Id, postId); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else {
|
||||
info2 = Must(store.FileInfo().Get(info2.Id)).(*model.FileInfo)
|
||||
}
|
||||
|
||||
if result := <-store.FileInfo().GetForPost(postId); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if infos := result.Data.([]*model.FileInfo); len(infos) != 2 {
|
||||
t.Fatal("should've returned exactly 2 file infos")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileInfoDeleteForPost(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
userId := model.NewId()
|
||||
postId := model.NewId()
|
||||
|
||||
infos := []*model.FileInfo{
|
||||
{
|
||||
PostId: postId,
|
||||
CreatorId: userId,
|
||||
Path: "file.txt",
|
||||
},
|
||||
{
|
||||
PostId: postId,
|
||||
CreatorId: userId,
|
||||
Path: "file.txt",
|
||||
},
|
||||
{
|
||||
PostId: postId,
|
||||
CreatorId: userId,
|
||||
Path: "file.txt",
|
||||
DeleteAt: 123,
|
||||
},
|
||||
{
|
||||
PostId: model.NewId(),
|
||||
CreatorId: userId,
|
||||
Path: "file.txt",
|
||||
},
|
||||
}
|
||||
|
||||
for i, info := range infos {
|
||||
infos[i] = Must(store.FileInfo().Save(info)).(*model.FileInfo)
|
||||
}
|
||||
|
||||
if result := <-store.FileInfo().DeleteForPost(postId); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
if infos := Must(store.FileInfo().GetForPost(postId)).([]*model.FileInfo); len(infos) != 0 {
|
||||
t.Fatal("shouldn't have returned any file infos")
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ func NewSqlPostStore(sqlStore *SqlStore) PostStore {
|
||||
table.ColMap("Hashtags").SetMaxSize(1000)
|
||||
table.ColMap("Props").SetMaxSize(8000)
|
||||
table.ColMap("Filenames").SetMaxSize(4000)
|
||||
table.ColMap("FileIds").SetMaxSize(150)
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -94,42 +95,39 @@ func (s SqlPostStore) Save(post *model.Post) StoreChannel {
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlPostStore) Update(oldPost *model.Post, newMessage string, newHashtags string) StoreChannel {
|
||||
func (s SqlPostStore) Update(newPost *model.Post, oldPost *model.Post) StoreChannel {
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
editPost := *oldPost
|
||||
editPost.Message = newMessage
|
||||
editPost.UpdateAt = model.GetMillis()
|
||||
editPost.Hashtags = newHashtags
|
||||
newPost.UpdateAt = model.GetMillis()
|
||||
|
||||
oldPost.DeleteAt = editPost.UpdateAt
|
||||
oldPost.UpdateAt = editPost.UpdateAt
|
||||
oldPost.DeleteAt = newPost.UpdateAt
|
||||
oldPost.UpdateAt = newPost.UpdateAt
|
||||
oldPost.OriginalId = oldPost.Id
|
||||
oldPost.Id = model.NewId()
|
||||
|
||||
if result.Err = editPost.IsValid(); result.Err != nil {
|
||||
if result.Err = newPost.IsValid(); result.Err != nil {
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := s.GetMaster().Update(&editPost); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlPostStore.Update", "store.sql_post.update.app_error", nil, "id="+editPost.Id+", "+err.Error())
|
||||
if _, err := s.GetMaster().Update(newPost); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlPostStore.Update", "store.sql_post.update.app_error", nil, "id="+newPost.Id+", "+err.Error())
|
||||
} else {
|
||||
time := model.GetMillis()
|
||||
s.GetMaster().Exec("UPDATE Channels SET LastPostAt = :LastPostAt WHERE Id = :ChannelId", map[string]interface{}{"LastPostAt": time, "ChannelId": editPost.ChannelId})
|
||||
s.GetMaster().Exec("UPDATE Channels SET LastPostAt = :LastPostAt WHERE Id = :ChannelId", map[string]interface{}{"LastPostAt": time, "ChannelId": newPost.ChannelId})
|
||||
|
||||
if len(editPost.RootId) > 0 {
|
||||
s.GetMaster().Exec("UPDATE Posts SET UpdateAt = :UpdateAt WHERE Id = :RootId", map[string]interface{}{"UpdateAt": time, "RootId": editPost.RootId})
|
||||
if len(newPost.RootId) > 0 {
|
||||
s.GetMaster().Exec("UPDATE Posts SET UpdateAt = :UpdateAt WHERE Id = :RootId", map[string]interface{}{"UpdateAt": time, "RootId": newPost.RootId})
|
||||
}
|
||||
|
||||
// mark the old post as deleted
|
||||
s.GetMaster().Insert(oldPost)
|
||||
|
||||
result.Data = &editPost
|
||||
result.Data = newPost
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
@@ -972,7 +970,7 @@ func (s SqlPostStore) AnalyticsPostCount(teamId string, mustHaveFile bool, mustH
|
||||
}
|
||||
|
||||
if mustHaveFile {
|
||||
query += " AND Posts.Filenames != '[]'"
|
||||
query += " AND (Posts.FileIds != '[]' OR Posts.Filenames != '[]')"
|
||||
}
|
||||
|
||||
if mustHaveHashtag {
|
||||
|
||||
@@ -87,45 +87,73 @@ func TestPostStoreUpdate(t *testing.T) {
|
||||
|
||||
ro1 := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o1.Id]
|
||||
ro2 := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o2.Id]
|
||||
ro6 := (<-store.Post().Get(o3.Id)).Data.(*model.PostList).Posts[o3.Id]
|
||||
ro3 := (<-store.Post().Get(o3.Id)).Data.(*model.PostList).Posts[o3.Id]
|
||||
|
||||
if ro1.Message != o1.Message {
|
||||
t.Fatal("Failed to save/get")
|
||||
}
|
||||
|
||||
msg := o1.Message + "BBBBBBBBBB"
|
||||
if result := <-store.Post().Update(ro1, msg, ""); result.Err != nil {
|
||||
o1a := &model.Post{}
|
||||
*o1a = *ro1
|
||||
o1a.Message = ro1.Message + "BBBBBBBBBB"
|
||||
if result := <-store.Post().Update(o1a, ro1); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
msg2 := o2.Message + "DDDDDDD"
|
||||
if result := <-store.Post().Update(ro2, msg2, ""); result.Err != nil {
|
||||
ro1a := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o1.Id]
|
||||
|
||||
if ro1a.Message != o1a.Message {
|
||||
t.Fatal("Failed to update/get")
|
||||
}
|
||||
|
||||
o2a := &model.Post{}
|
||||
*o2a = *ro2
|
||||
o2a.Message = ro2.Message + "DDDDDDD"
|
||||
if result := <-store.Post().Update(o2a, ro2); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
msg3 := o3.Message + "WWWWWWW"
|
||||
if result := <-store.Post().Update(ro6, msg3, "#hashtag"); result.Err != nil {
|
||||
ro2a := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o2.Id]
|
||||
|
||||
if ro2a.Message != o2a.Message {
|
||||
t.Fatal("Failed to update/get")
|
||||
}
|
||||
|
||||
o3a := &model.Post{}
|
||||
*o3a = *ro3
|
||||
o3a.Message = ro3.Message + "WWWWWWW"
|
||||
if result := <-store.Post().Update(o3a, ro3); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
ro3 := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o1.Id]
|
||||
ro3a := (<-store.Post().Get(o3.Id)).Data.(*model.PostList).Posts[o3.Id]
|
||||
|
||||
if ro3.Message != msg {
|
||||
if ro3a.Message != o3a.Message && ro3a.Hashtags != o3a.Hashtags {
|
||||
t.Fatal("Failed to update/get")
|
||||
}
|
||||
|
||||
ro4 := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o2.Id]
|
||||
o4 := Must(store.Post().Save(&model.Post{
|
||||
ChannelId: model.NewId(),
|
||||
UserId: model.NewId(),
|
||||
Message: model.NewId(),
|
||||
Filenames: []string{"test"},
|
||||
})).(*model.Post)
|
||||
|
||||
if ro4.Message != msg2 {
|
||||
t.Fatal("Failed to update/get")
|
||||
ro4 := (<-store.Post().Get(o4.Id)).Data.(*model.PostList).Posts[o4.Id]
|
||||
|
||||
o4a := &model.Post{}
|
||||
*o4a = *ro4
|
||||
o4a.Filenames = []string{}
|
||||
o4a.FileIds = []string{model.NewId()}
|
||||
if result := <-store.Post().Update(o4a, ro4); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
ro5 := (<-store.Post().Get(o3.Id)).Data.(*model.PostList).Posts[o3.Id]
|
||||
|
||||
if ro5.Message != msg3 && ro5.Hashtags != "#hashtag" {
|
||||
t.Fatal("Failed to update/get")
|
||||
if ro4a := Must(store.Post().Get(o4.Id)).(*model.PostList).Posts[o4.Id]; len(ro4a.Filenames) != 0 {
|
||||
t.Fatal("Failed to clear Filenames")
|
||||
} else if len(ro4a.FileIds) != 1 {
|
||||
t.Fatal("Failed to set FileIds")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPostStoreDelete(t *testing.T) {
|
||||
|
||||
@@ -81,6 +81,7 @@ type SqlStore struct {
|
||||
recovery PasswordRecoveryStore
|
||||
emoji EmojiStore
|
||||
status StatusStore
|
||||
fileInfo FileInfoStore
|
||||
SchemaVersion string
|
||||
}
|
||||
|
||||
@@ -129,6 +130,7 @@ func NewSqlStore() Store {
|
||||
sqlStore.recovery = NewSqlPasswordRecoveryStore(sqlStore)
|
||||
sqlStore.emoji = NewSqlEmojiStore(sqlStore)
|
||||
sqlStore.status = NewSqlStatusStore(sqlStore)
|
||||
sqlStore.fileInfo = NewSqlFileInfoStore(sqlStore)
|
||||
|
||||
err := sqlStore.master.CreateTablesIfNotExists()
|
||||
if err != nil {
|
||||
@@ -155,6 +157,7 @@ func NewSqlStore() Store {
|
||||
sqlStore.recovery.(*SqlPasswordRecoveryStore).CreateIndexesIfNotExists()
|
||||
sqlStore.emoji.(*SqlEmojiStore).CreateIndexesIfNotExists()
|
||||
sqlStore.status.(*SqlStatusStore).CreateIndexesIfNotExists()
|
||||
sqlStore.fileInfo.(*SqlFileInfoStore).CreateIndexesIfNotExists()
|
||||
|
||||
sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
|
||||
|
||||
@@ -643,6 +646,10 @@ func (ss SqlStore) Status() StatusStore {
|
||||
return ss.status
|
||||
}
|
||||
|
||||
func (ss SqlStore) FileInfo() FileInfoStore {
|
||||
return ss.fileInfo
|
||||
}
|
||||
|
||||
func (ss SqlStore) DropAllTables() {
|
||||
ss.master.TruncateTables()
|
||||
}
|
||||
|
||||
@@ -181,7 +181,6 @@ func UpgradeDatabaseToVersion33(sqlStore *SqlStore) {
|
||||
|
||||
func UpgradeDatabaseToVersion34(sqlStore *SqlStore) {
|
||||
if shouldPerformUpgrade(sqlStore, VERSION_3_3_0, VERSION_3_4_0) {
|
||||
|
||||
sqlStore.CreateColumnIfNotExists("Status", "Manual", "BOOLEAN", "BOOLEAN", "0")
|
||||
sqlStore.CreateColumnIfNotExists("Status", "ActiveChannel", "varchar(26)", "varchar(26)", "")
|
||||
|
||||
@@ -199,6 +198,9 @@ func UpgradeDatabaseToVersion35(sqlStore *SqlStore) {
|
||||
sqlStore.GetMaster().Exec("UPDATE ChannelMembers SET Roles = 'channel_user' WHERE Roles = ''")
|
||||
sqlStore.GetMaster().Exec("UPDATE ChannelMembers SET Roles = 'channel_user channel_admin' WHERE Roles = 'admin'")
|
||||
|
||||
// The rest of the migration from Filenames -> FileIds is done lazily in api.GetFileInfosForPost
|
||||
sqlStore.CreateColumnIfNotExists("Posts", "FileIds", "varchar(150)", "varchar(150)", "[]")
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
// UNCOMMENT WHEN WE DO RELEASE
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
@@ -45,6 +45,7 @@ type Store interface {
|
||||
PasswordRecovery() PasswordRecoveryStore
|
||||
Emoji() EmojiStore
|
||||
Status() StatusStore
|
||||
FileInfo() FileInfoStore
|
||||
MarkSystemRanUnitTests()
|
||||
Close()
|
||||
DropAllTables()
|
||||
@@ -86,11 +87,13 @@ type ChannelStore interface {
|
||||
GetMoreChannels(teamId string, userId string) StoreChannel
|
||||
GetChannelCounts(teamId string, userId string) StoreChannel
|
||||
GetAll(teamId string) StoreChannel
|
||||
GetForPost(postId string) StoreChannel
|
||||
|
||||
SaveMember(member *model.ChannelMember) StoreChannel
|
||||
UpdateMember(member *model.ChannelMember) StoreChannel
|
||||
GetMembers(channelId string) StoreChannel
|
||||
GetMember(channelId string, userId string) StoreChannel
|
||||
GetMemberForPost(postId string, userId string) StoreChannel
|
||||
GetMemberCount(channelId string) StoreChannel
|
||||
RemoveMember(channelId string, userId string) StoreChannel
|
||||
PermanentDeleteMembersByUser(userId string) StoreChannel
|
||||
@@ -104,7 +107,7 @@ type ChannelStore interface {
|
||||
|
||||
type PostStore interface {
|
||||
Save(post *model.Post) StoreChannel
|
||||
Update(post *model.Post, newMessage string, newHashtags string) StoreChannel
|
||||
Update(newPost *model.Post, oldPost *model.Post) StoreChannel
|
||||
Get(id string) StoreChannel
|
||||
Delete(postId string, time int64) StoreChannel
|
||||
PermanentDeleteByUser(userId string) StoreChannel
|
||||
@@ -277,3 +280,12 @@ type StatusStore interface {
|
||||
GetTotalActiveUsersCount() StoreChannel
|
||||
UpdateLastActivityAt(userId string, lastActivityAt int64) StoreChannel
|
||||
}
|
||||
|
||||
type FileInfoStore interface {
|
||||
Save(info *model.FileInfo) StoreChannel
|
||||
Get(id string) StoreChannel
|
||||
GetByPath(path string) StoreChannel
|
||||
GetForPost(postId string) StoreChannel
|
||||
AttachToPost(fileId string, postId string) StoreChannel
|
||||
DeleteForPost(postId string) StoreChannel
|
||||
}
|
||||
|
||||
@@ -34,3 +34,17 @@ func FileExistsInConfigFolder(filename string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RemoveDuplicatesFromStringArray(arr []string) []string {
|
||||
result := make([]string, 0, len(arr))
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, item := range arr {
|
||||
if !seen[item] {
|
||||
result = append(result, item)
|
||||
seen[item] = true
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -28,3 +28,19 @@ func TestStringArrayIntersection(t *testing.T) {
|
||||
t.Fatal("should be 1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDuplicatesFromStringArray(t *testing.T) {
|
||||
a := []string{
|
||||
"a",
|
||||
"b",
|
||||
"a",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"a",
|
||||
}
|
||||
|
||||
if len(RemoveDuplicatesFromStringArray(a)) != 3 {
|
||||
t.Fatal("should be 3")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,11 +294,11 @@ export function showGetPostLinkModal(post) {
|
||||
});
|
||||
}
|
||||
|
||||
export function showGetPublicLinkModal(filename) {
|
||||
export function showGetPublicLinkModal(fileId) {
|
||||
AppDispatcher.handleViewAction({
|
||||
type: ActionTypes.TOGGLE_GET_PUBLIC_LINK_MODAL,
|
||||
value: true,
|
||||
filename
|
||||
fileId
|
||||
});
|
||||
}
|
||||
|
||||
@@ -388,7 +388,6 @@ export function sendEphemeralPost(message, channelId) {
|
||||
type: Constants.POST_TYPE_EPHEMERAL,
|
||||
create_at: timestamp,
|
||||
update_at: timestamp,
|
||||
filenames: [],
|
||||
props: {}
|
||||
};
|
||||
|
||||
|
||||
@@ -113,10 +113,14 @@ export default class Client {
|
||||
return `${this.url}${this.urlVersion}/users`;
|
||||
}
|
||||
|
||||
getFilesRoute() {
|
||||
getTeamFilesRoute() {
|
||||
return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/files`;
|
||||
}
|
||||
|
||||
getFileRoute(fileId) {
|
||||
return `${this.url}${this.urlVersion}/files/${fileId}`;
|
||||
}
|
||||
|
||||
getOAuthRoute() {
|
||||
return `${this.url}${this.urlVersion}/oauth`;
|
||||
}
|
||||
@@ -1520,34 +1524,20 @@ export default class Client {
|
||||
end(this.handleResponse.bind(this, 'getFlaggedPosts', success, error));
|
||||
}
|
||||
|
||||
getFileInfosForPost(channelId, postId, success, error) {
|
||||
request.
|
||||
get(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/get_file_infos`).
|
||||
set(this.defaultHeaders).
|
||||
type('application/json').
|
||||
accept('application/json').
|
||||
end(this.handleResponse.bind(this, 'getFileInfosForPost', success, error));
|
||||
}
|
||||
|
||||
// Routes for Files
|
||||
|
||||
getFileInfo(filename, success, error) {
|
||||
request.
|
||||
get(`${this.getFilesRoute()}/get_info${filename}`).
|
||||
set(this.defaultHeaders).
|
||||
type('application/json').
|
||||
accept('application/json').
|
||||
end(this.handleResponse.bind(this, 'getFileInfo', success, error));
|
||||
}
|
||||
|
||||
getPublicLink(filename, success, error) {
|
||||
const data = {
|
||||
filename
|
||||
};
|
||||
|
||||
request.
|
||||
post(`${this.getFilesRoute()}/get_public_link`).
|
||||
set(this.defaultHeaders).
|
||||
type('application/json').
|
||||
accept('application/json').
|
||||
send(data).
|
||||
end(this.handleResponse.bind(this, 'getPublicLink', success, error));
|
||||
}
|
||||
|
||||
uploadFile(file, filename, channelId, clientId, success, error) {
|
||||
return request.
|
||||
post(`${this.getFilesRoute()}/upload`).
|
||||
post(`${this.getTeamFilesRoute()}/upload`).
|
||||
set(this.defaultHeaders).
|
||||
attach('files', file, filename).
|
||||
field('channel_id', channelId).
|
||||
@@ -1556,6 +1546,51 @@ export default class Client {
|
||||
end(this.handleResponse.bind(this, 'uploadFile', success, error));
|
||||
}
|
||||
|
||||
getFile(fileId, success, error) {
|
||||
request.
|
||||
get(`${this.getFileRoute(fileId)}/get`).
|
||||
set(this.defaultHeaders).
|
||||
type('application/json').
|
||||
accept('application/json').
|
||||
end(this.handleResponse.bind(this, 'getFile', success, error));
|
||||
}
|
||||
|
||||
getFileThumbnail(fileId, success, error) {
|
||||
request.
|
||||
get(`${this.getFileRoute(fileId)}/get_thumbnail`).
|
||||
set(this.defaultHeaders).
|
||||
type('application/json').
|
||||
accept('application/json').
|
||||
end(this.handleResponse.bind(this, 'getFileThumbnail', success, error));
|
||||
}
|
||||
|
||||
getFilePreview(fileId, success, error) {
|
||||
request.
|
||||
get(`${this.getFileRoute(fileId)}/get`).
|
||||
set(this.defaultHeaders).
|
||||
type('application/json').
|
||||
accept('application/json').
|
||||
end(this.handleResponse.bind(this, 'getFilePreview', success, error));
|
||||
}
|
||||
|
||||
getFileInfo(fileId, success, error) {
|
||||
request.
|
||||
get(`${this.getFileRoute(fileId)}/get_info`).
|
||||
set(this.defaultHeaders).
|
||||
type('application/json').
|
||||
accept('application/json').
|
||||
end(this.handleResponse.bind(this, 'getFileInfo', success, error));
|
||||
}
|
||||
|
||||
getPublicLink(fileId, success, error) {
|
||||
request.
|
||||
get(`${this.getFileRoute(fileId)}/get_public_link`).
|
||||
set(this.defaultHeaders).
|
||||
type('application/json').
|
||||
accept('application/json').
|
||||
end(this.handleResponse.bind(this, 'getPublicLink', success, error));
|
||||
}
|
||||
|
||||
// Routes for OAuth
|
||||
|
||||
registerOAuthApp(app, success, error) {
|
||||
|
||||
@@ -76,10 +76,8 @@ export default class AudioVideoPreview extends React.Component {
|
||||
if (!this.state.canPlay) {
|
||||
return (
|
||||
<FileInfoPreview
|
||||
filename={this.props.filename}
|
||||
fileUrl={this.props.fileUrl}
|
||||
fileInfo={this.props.fileInfo}
|
||||
formatMessage={this.props.formatMessage}
|
||||
fileUrl={this.props.fileUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -94,7 +92,7 @@ export default class AudioVideoPreview extends React.Component {
|
||||
// add a key to the video to prevent React from using an old video source while a new one is loading
|
||||
return (
|
||||
<video
|
||||
key={this.props.filename}
|
||||
key={this.props.fileInfo.id}
|
||||
ref='video'
|
||||
style={{maxHeight: this.props.maxHeight}}
|
||||
data-setup='{}'
|
||||
@@ -112,9 +110,7 @@ export default class AudioVideoPreview extends React.Component {
|
||||
}
|
||||
|
||||
AudioVideoPreview.propTypes = {
|
||||
filename: React.PropTypes.string.isRequired,
|
||||
fileUrl: React.PropTypes.string.isRequired,
|
||||
fileInfo: React.PropTypes.object.isRequired,
|
||||
maxHeight: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]).isRequired,
|
||||
formatMessage: React.PropTypes.func.isRequired
|
||||
fileUrl: React.PropTypes.string.isRequired,
|
||||
maxHeight: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]).isRequired
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class CodePreview extends React.Component {
|
||||
}
|
||||
|
||||
updateStateFromProps(props) {
|
||||
var usedLanguage = SyntaxHighlighting.getLanguageFromFilename(props.filename);
|
||||
const usedLanguage = SyntaxHighlighting.getLanguageFromFileExtension(props.fileInfo.extension);
|
||||
|
||||
if (!usedLanguage || props.fileInfo.size > Constants.CODE_PREVIEW_MAX_FILE_SIZE) {
|
||||
this.setState({code: '', lang: '', loading: false, success: false});
|
||||
@@ -64,8 +64,8 @@ export default class CodePreview extends React.Component {
|
||||
this.setState({loading: false, success: false});
|
||||
}
|
||||
|
||||
static support(filename) {
|
||||
return Boolean(SyntaxHighlighting.getLanguageFromFilename(filename));
|
||||
static supports(fileInfo) {
|
||||
return Boolean(SyntaxHighlighting.getLanguageFromFileExtension(fileInfo.extension));
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -83,10 +83,8 @@ export default class CodePreview extends React.Component {
|
||||
if (!this.state.success) {
|
||||
return (
|
||||
<FileInfoPreview
|
||||
filename={this.props.filename}
|
||||
fileUrl={this.props.fileUrl}
|
||||
fileInfo={this.props.fileInfo}
|
||||
formatMessage={this.props.formatMessage}
|
||||
fileUrl={this.props.fileUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -106,12 +104,10 @@ export default class CodePreview extends React.Component {
|
||||
|
||||
const highlighted = SyntaxHighlighting.highlight(this.state.lang, this.state.code);
|
||||
|
||||
const fileName = this.props.filename.substring(this.props.filename.lastIndexOf('/') + 1, this.props.filename.length);
|
||||
|
||||
return (
|
||||
<div className='post-code'>
|
||||
<span className='post-code__language'>
|
||||
{`${fileName} - ${language}`}
|
||||
{`${this.props.fileInfo.name} - ${language}`}
|
||||
</span>
|
||||
<code className='hljs'>
|
||||
<table>
|
||||
@@ -129,8 +125,6 @@ export default class CodePreview extends React.Component {
|
||||
}
|
||||
|
||||
CodePreview.propTypes = {
|
||||
filename: React.PropTypes.string.isRequired,
|
||||
fileUrl: React.PropTypes.string.isRequired,
|
||||
fileInfo: React.PropTypes.object.isRequired,
|
||||
formatMessage: React.PropTypes.func.isRequired
|
||||
fileUrl: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ export default class CreateComment extends React.Component {
|
||||
this.state = {
|
||||
messageText: draft.message,
|
||||
uploadsInProgress: draft.uploadsInProgress,
|
||||
previews: draft.previews,
|
||||
fileInfos: draft.fileInfos,
|
||||
submitting: false,
|
||||
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
|
||||
showPostDeletedModal: false
|
||||
@@ -99,10 +99,10 @@ export default class CreateComment extends React.Component {
|
||||
}
|
||||
|
||||
const post = {};
|
||||
post.filenames = [];
|
||||
post.file_ids = [];
|
||||
post.message = this.state.messageText;
|
||||
|
||||
if (post.message.trim().length === 0 && this.state.previews.length === 0) {
|
||||
if (post.message.trim().length === 0 && this.state.fileInfos.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ export default class CreateComment extends React.Component {
|
||||
post.channel_id = this.props.channelId;
|
||||
post.root_id = this.props.rootId;
|
||||
post.parent_id = this.props.rootId;
|
||||
post.filenames = this.state.previews;
|
||||
post.file_ids = this.state.fileInfos.map((info) => info.id);
|
||||
const time = Utils.getTimestamp();
|
||||
post.pending_post_id = `${userId}:${time}`;
|
||||
post.user_id = userId;
|
||||
@@ -163,7 +163,7 @@ export default class CreateComment extends React.Component {
|
||||
messageText: '',
|
||||
submitting: false,
|
||||
postError: null,
|
||||
previews: [],
|
||||
fileInfos: [],
|
||||
serverError: null
|
||||
});
|
||||
}
|
||||
@@ -245,7 +245,7 @@ export default class CreateComment extends React.Component {
|
||||
this.focusTextbox();
|
||||
}
|
||||
|
||||
handleFileUploadComplete(filenames, clientIds) {
|
||||
handleFileUploadComplete(fileInfos, clientIds) {
|
||||
const draft = PostStore.getCommentDraft(this.props.rootId);
|
||||
|
||||
// remove each finished file from uploads
|
||||
@@ -257,10 +257,10 @@ export default class CreateComment extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
draft.previews = draft.previews.concat(filenames);
|
||||
draft.fileInfos = draft.fileInfos.concat(fileInfos);
|
||||
PostStore.storeCommentDraft(this.props.rootId, draft);
|
||||
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress, fileInfos: draft.fileInfos});
|
||||
}
|
||||
|
||||
handleUploadError(err, clientId) {
|
||||
@@ -281,11 +281,11 @@ export default class CreateComment extends React.Component {
|
||||
}
|
||||
|
||||
removePreview(id) {
|
||||
const previews = this.state.previews;
|
||||
const fileInfos = this.state.fileInfos;
|
||||
const uploadsInProgress = this.state.uploadsInProgress;
|
||||
|
||||
// id can either be the path of an uploaded file or the client id of an in progress upload
|
||||
let index = previews.indexOf(id);
|
||||
// id can either be the id of an uploaded file or the client id of an in progress upload
|
||||
let index = fileInfos.findIndex((info) => info.id === id);
|
||||
if (index === -1) {
|
||||
index = uploadsInProgress.indexOf(id);
|
||||
|
||||
@@ -294,26 +294,26 @@ export default class CreateComment extends React.Component {
|
||||
this.refs.fileUpload.getWrappedInstance().cancelUpload(id);
|
||||
}
|
||||
} else {
|
||||
previews.splice(index, 1);
|
||||
fileInfos.splice(index, 1);
|
||||
}
|
||||
|
||||
const draft = PostStore.getCommentDraft(this.props.rootId);
|
||||
draft.previews = previews;
|
||||
draft.fileInfos = fileInfos;
|
||||
draft.uploadsInProgress = uploadsInProgress;
|
||||
PostStore.storeCommentDraft(this.props.rootId, draft);
|
||||
|
||||
this.setState({previews, uploadsInProgress});
|
||||
this.setState({fileInfos, uploadsInProgress});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.rootId !== this.props.rootId) {
|
||||
const draft = PostStore.getCommentDraft(newProps.rootId);
|
||||
this.setState({messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
|
||||
this.setState({messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, fileInfos: draft.fileInfos});
|
||||
}
|
||||
}
|
||||
|
||||
getFileCount() {
|
||||
return this.state.previews.length + this.state.uploadsInProgress.length;
|
||||
return this.state.fileInfos.length + this.state.uploadsInProgress.length;
|
||||
}
|
||||
|
||||
focusTextbox() {
|
||||
@@ -350,10 +350,10 @@ export default class CreateComment extends React.Component {
|
||||
}
|
||||
|
||||
let preview = null;
|
||||
if (this.state.previews.length > 0 || this.state.uploadsInProgress.length > 0) {
|
||||
if (this.state.fileInfos.length > 0 || this.state.uploadsInProgress.length > 0) {
|
||||
preview = (
|
||||
<FilePreview
|
||||
files={this.state.previews}
|
||||
fileInfos={this.state.fileInfos}
|
||||
onRemove={this.removePreview}
|
||||
uploadsInProgress={this.state.uploadsInProgress}
|
||||
/>
|
||||
|
||||
@@ -67,7 +67,7 @@ export default class CreatePost extends React.Component {
|
||||
channelId: ChannelStore.getCurrentId(),
|
||||
messageText: draft.messageText,
|
||||
uploadsInProgress: draft.uploadsInProgress,
|
||||
previews: draft.previews,
|
||||
fileInfos: draft.fileInfos,
|
||||
submitting: false,
|
||||
initialText: draft.messageText,
|
||||
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
|
||||
@@ -79,14 +79,14 @@ export default class CreatePost extends React.Component {
|
||||
|
||||
getCurrentDraft() {
|
||||
const draft = PostStore.getCurrentDraft();
|
||||
const safeDraft = {previews: [], messageText: '', uploadsInProgress: []};
|
||||
const safeDraft = {fileInfos: [], messageText: '', uploadsInProgress: []};
|
||||
|
||||
if (draft) {
|
||||
if (draft.message) {
|
||||
safeDraft.messageText = draft.message;
|
||||
}
|
||||
if (draft.previews) {
|
||||
safeDraft.previews = draft.previews;
|
||||
if (draft.fileInfos) {
|
||||
safeDraft.fileInfos = draft.fileInfos;
|
||||
}
|
||||
if (draft.uploadsInProgress) {
|
||||
safeDraft.uploadsInProgress = draft.uploadsInProgress;
|
||||
@@ -104,10 +104,10 @@ export default class CreatePost extends React.Component {
|
||||
}
|
||||
|
||||
const post = {};
|
||||
post.filenames = [];
|
||||
post.file_ids = [];
|
||||
post.message = this.state.messageText;
|
||||
|
||||
if (post.message.trim().length === 0 && this.state.previews.length === 0) {
|
||||
if (post.message.trim().length === 0 && this.state.fileInfos.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ export default class CreatePost extends React.Component {
|
||||
|
||||
if (post.message.indexOf('/') === 0) {
|
||||
PostStore.storeDraft(this.state.channelId, null);
|
||||
this.setState({messageText: '', postError: null, previews: []});
|
||||
this.setState({messageText: '', postError: null, fileInfos: []});
|
||||
|
||||
ChannelActions.executeCommand(
|
||||
this.state.channelId,
|
||||
@@ -153,7 +153,7 @@ export default class CreatePost extends React.Component {
|
||||
|
||||
sendMessage(post) {
|
||||
post.channel_id = this.state.channelId;
|
||||
post.filenames = this.state.previews;
|
||||
post.file_ids = this.state.fileInfos.map((info) => info.id);
|
||||
|
||||
const time = Utils.getTimestamp();
|
||||
const userId = UserStore.getCurrentId();
|
||||
@@ -163,7 +163,7 @@ export default class CreatePost extends React.Component {
|
||||
post.parent_id = this.state.parentId;
|
||||
|
||||
GlobalActions.emitUserPostedEvent(post);
|
||||
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
|
||||
this.setState({messageText: '', submitting: false, postError: null, fileInfos: [], serverError: null});
|
||||
|
||||
Client.createPost(post,
|
||||
(data) => {
|
||||
@@ -236,7 +236,7 @@ export default class CreatePost extends React.Component {
|
||||
this.focusTextbox();
|
||||
}
|
||||
|
||||
handleFileUploadComplete(filenames, clientIds, channelId) {
|
||||
handleFileUploadComplete(fileInfos, clientIds, channelId) {
|
||||
const draft = PostStore.getDraft(channelId);
|
||||
|
||||
// remove each finished file from uploads
|
||||
@@ -248,11 +248,11 @@ export default class CreatePost extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
draft.previews = draft.previews.concat(filenames);
|
||||
draft.fileInfos = draft.fileInfos.concat(fileInfos);
|
||||
PostStore.storeDraft(channelId, draft);
|
||||
|
||||
if (channelId === this.state.channelId) {
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress, fileInfos: draft.fileInfos});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,11 +282,11 @@ export default class CreatePost extends React.Component {
|
||||
}
|
||||
|
||||
removePreview(id) {
|
||||
const previews = Object.assign([], this.state.previews);
|
||||
const fileInfos = Object.assign([], this.state.fileInfos);
|
||||
const uploadsInProgress = this.state.uploadsInProgress;
|
||||
|
||||
// id can either be the path of an uploaded file or the client id of an in progress upload
|
||||
let index = previews.indexOf(id);
|
||||
// id can either be the id of an uploaded file or the client id of an in progress upload
|
||||
let index = fileInfos.findIndex((info) => info.id === id);
|
||||
if (index === -1) {
|
||||
index = uploadsInProgress.indexOf(id);
|
||||
|
||||
@@ -295,15 +295,15 @@ export default class CreatePost extends React.Component {
|
||||
this.refs.fileUpload.getWrappedInstance().cancelUpload(id);
|
||||
}
|
||||
} else {
|
||||
previews.splice(index, 1);
|
||||
fileInfos.splice(index, 1);
|
||||
}
|
||||
|
||||
const draft = PostStore.getCurrentDraft();
|
||||
draft.previews = previews;
|
||||
draft.fileInfos = fileInfos;
|
||||
draft.uploadsInProgress = uploadsInProgress;
|
||||
PostStore.storeCurrentDraft(draft);
|
||||
|
||||
this.setState({previews, uploadsInProgress});
|
||||
this.setState({fileInfos, uploadsInProgress});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
@@ -336,6 +336,7 @@ export default class CreatePost extends React.Component {
|
||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||
document.removeEventListener('keydown', this.showShortcuts);
|
||||
}
|
||||
|
||||
showShortcuts(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === Constants.KeyCodes.FORWARD_SLASH) {
|
||||
e.preventDefault();
|
||||
@@ -359,7 +360,7 @@ export default class CreatePost extends React.Component {
|
||||
if (this.state.channelId !== channelId) {
|
||||
const draft = this.getCurrentDraft();
|
||||
|
||||
this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
|
||||
this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, fileInfos: draft.fileInfos, uploadsInProgress: draft.uploadsInProgress});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,11 +375,11 @@ export default class CreatePost extends React.Component {
|
||||
|
||||
getFileCount(channelId) {
|
||||
if (channelId === this.state.channelId) {
|
||||
return this.state.previews.length + this.state.uploadsInProgress.length;
|
||||
return this.state.fileInfos.length + this.state.uploadsInProgress.length;
|
||||
}
|
||||
|
||||
const draft = PostStore.getDraft(channelId);
|
||||
return draft.previews.length + draft.uploadsInProgress.length;
|
||||
return draft.fileInfos.length + draft.uploadsInProgress.length;
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
@@ -474,10 +475,10 @@ export default class CreatePost extends React.Component {
|
||||
}
|
||||
|
||||
let preview = null;
|
||||
if (this.state.previews.length > 0 || this.state.uploadsInProgress.length > 0) {
|
||||
if (this.state.fileInfos.length > 0 || this.state.uploadsInProgress.length > 0) {
|
||||
preview = (
|
||||
<FilePreview
|
||||
files={this.state.previews}
|
||||
fileInfos={this.state.fileInfos}
|
||||
onRemove={this.removePreview}
|
||||
uploadsInProgress={this.state.uploadsInProgress}
|
||||
/>
|
||||
|
||||
@@ -1,204 +1,111 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as utils from 'utils/utils.jsx';
|
||||
import Client from 'client/web_client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import FileStore from 'stores/file_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages} from 'react-intl';
|
||||
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
|
||||
|
||||
const holders = defineMessages({
|
||||
download: {
|
||||
id: 'file_attachment.download',
|
||||
defaultMessage: 'Download'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class FileAttachment extends React.Component {
|
||||
export default class FileAttachment extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.loadFiles = this.loadFiles.bind(this);
|
||||
this.addBackgroundImage = this.addBackgroundImage.bind(this);
|
||||
this.onAttachmentClick = this.onAttachmentClick.bind(this);
|
||||
|
||||
this.canSetState = false;
|
||||
this.state = {fileSize: -1};
|
||||
this.state = {
|
||||
loaded: Utils.getFileType(props.fileInfo.extension) !== 'image'
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadFiles();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.fileInfo.id !== this.props.fileInfo.id) {
|
||||
this.setState({
|
||||
loaded: Utils.getFileType(nextProps.fileInfo.extension) !== 'image'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.filename !== prevProps.filename) {
|
||||
if (!this.state.loaded && this.props.fileInfo.id !== prevProps.fileInfo.id) {
|
||||
this.loadFiles();
|
||||
}
|
||||
}
|
||||
|
||||
loadFiles() {
|
||||
this.canSetState = true;
|
||||
const fileInfo = this.props.fileInfo;
|
||||
const fileType = Utils.getFileType(fileInfo.extension);
|
||||
|
||||
var filename = this.props.filename;
|
||||
if (fileType === 'image') {
|
||||
const thumbnailUrl = FileStore.getFileThumbnailUrl(fileInfo.id);
|
||||
|
||||
if (filename) {
|
||||
var fileInfo = this.getFileInfoFromName(filename);
|
||||
var type = utils.getFileType(fileInfo.ext);
|
||||
|
||||
if (type === 'image') {
|
||||
var self = this; // Need this reference since we use the given "this"
|
||||
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg').on('load', (function loadWrapper(path, name) {
|
||||
return function loader() {
|
||||
$(this).remove();
|
||||
if (name in self.refs) {
|
||||
var imgDiv = ReactDOM.findDOMNode(self.refs[name]);
|
||||
|
||||
$(imgDiv).removeClass('post-image__load');
|
||||
$(imgDiv).addClass('post-image');
|
||||
|
||||
var width = this.width || $(this).width();
|
||||
var height = this.height || $(this).height();
|
||||
|
||||
if (width < Constants.THUMBNAIL_WIDTH &&
|
||||
height < Constants.THUMBNAIL_HEIGHT) {
|
||||
$(imgDiv).addClass('small');
|
||||
} else {
|
||||
$(imgDiv).addClass('normal');
|
||||
}
|
||||
|
||||
self.addBackgroundImage(name, path);
|
||||
}
|
||||
};
|
||||
}(fileInfo.path, filename)));
|
||||
}
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
this.setState({loaded: true});
|
||||
};
|
||||
img.load(thumbnailUrl);
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
// keep track of when this component is mounted so that we can asynchronously change state without worrying about whether or not we're mounted
|
||||
this.canSetState = false;
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!utils.areObjectsEqual(nextProps, this.props)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// the only time this object should update is when it receives an updated file size which we can usually handle without re-rendering
|
||||
if (nextState.fileSize !== this.state.fileSize) {
|
||||
if (this.refs.fileSize) {
|
||||
// update the UI element to display the file size without re-rendering the whole component
|
||||
ReactDOM.findDOMNode(this.refs.fileSize).innerHTML = utils.fileSizeToString(nextState.fileSize);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// we can't find the element that should hold the file size so we must not have rendered yet
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
getFileInfoFromName(name) {
|
||||
var fileInfo = utils.splitFileLocation(name);
|
||||
|
||||
fileInfo.path = Client.getFilesRoute() + '/get' + fileInfo.path;
|
||||
|
||||
return fileInfo;
|
||||
}
|
||||
addBackgroundImage(name, path) {
|
||||
var fileUrl = path;
|
||||
|
||||
if (name in this.refs) {
|
||||
if (!path) {
|
||||
fileUrl = this.getFileInfoFromName(name).path;
|
||||
}
|
||||
|
||||
var imgDiv = ReactDOM.findDOMNode(this.refs[name]);
|
||||
var re1 = new RegExp(' ', 'g');
|
||||
var re2 = new RegExp('\\(', 'g');
|
||||
var re3 = new RegExp('\\)', 'g');
|
||||
var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
|
||||
|
||||
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)');
|
||||
}
|
||||
}
|
||||
removeBackgroundImage(name) {
|
||||
if (name in this.refs) {
|
||||
$(ReactDOM.findDOMNode(this.refs[name])).css('background-image', 'initial');
|
||||
}
|
||||
}
|
||||
onAttachmentClick(e) {
|
||||
e.preventDefault();
|
||||
this.props.handleImageClick(this.props.index);
|
||||
}
|
||||
|
||||
render() {
|
||||
var filename = this.props.filename;
|
||||
const fileInfo = this.props.fileInfo;
|
||||
const fileName = fileInfo.name;
|
||||
const fileUrl = FileStore.getFileUrl(fileInfo.id);
|
||||
|
||||
var fileInfo = utils.splitFileLocation(filename);
|
||||
var fileUrl = utils.getFileUrl(filename);
|
||||
var type = utils.getFileType(fileInfo.ext);
|
||||
let thumbnail;
|
||||
if (this.state.loaded) {
|
||||
const type = Utils.getFileType(fileInfo.extension);
|
||||
|
||||
var thumbnail;
|
||||
if (type === 'image') {
|
||||
thumbnail = (
|
||||
<div
|
||||
ref={filename}
|
||||
className='post-image__load'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
thumbnail = <div className={'file-icon ' + utils.getIconClassName(type)}/>;
|
||||
}
|
||||
if (type === 'image') {
|
||||
let className = 'post-image';
|
||||
|
||||
var fileSizeString = '';
|
||||
if (this.state.fileSize < 0) {
|
||||
Client.getFileInfo(
|
||||
filename,
|
||||
(data) => {
|
||||
if (this.canSetState) {
|
||||
this.setState({fileSize: parseInt(data.size, 10)});
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// Do nothing
|
||||
if (fileInfo.width < Constants.THUMBNAIL_WIDTH && fileInfo.height < Constants.THUMBNAIL_HEIGHT) {
|
||||
className += ' small';
|
||||
} else {
|
||||
className += ' normal';
|
||||
}
|
||||
);
|
||||
|
||||
thumbnail = (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
backgroundImage: `url(${FileStore.getFileThumbnailUrl(fileInfo.id)})`
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
thumbnail = <div className={'file-icon ' + Utils.getIconClassName(type)}/>;
|
||||
}
|
||||
} else {
|
||||
fileSizeString = utils.fileSizeToString(this.state.fileSize);
|
||||
thumbnail = <div className='post-image__load'/>;
|
||||
}
|
||||
|
||||
var filenameString = decodeURIComponent(utils.getFileName(filename));
|
||||
var trimmedFilename;
|
||||
if (filenameString.length > 35) {
|
||||
trimmedFilename = filenameString.substring(0, Math.min(35, filenameString.length)) + '...';
|
||||
let trimmedFilename;
|
||||
if (fileName.length > 35) {
|
||||
trimmedFilename = fileName.substring(0, Math.min(35, fileName.length)) + '...';
|
||||
} else {
|
||||
trimmedFilename = filenameString;
|
||||
trimmedFilename = fileName;
|
||||
}
|
||||
var filenameOverlay = (
|
||||
<OverlayTrigger
|
||||
delayShow={1000}
|
||||
placement='top'
|
||||
overlay={<Tooltip id='file-name__tooltip'>{this.props.intl.formatMessage(holders.download) + ' "' + filenameString + '"'}</Tooltip>}
|
||||
>
|
||||
<a
|
||||
href={fileUrl}
|
||||
download={filenameString}
|
||||
className='post-image__name'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{trimmedFilename}
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
let filenameOverlay;
|
||||
if (this.props.compactDisplay) {
|
||||
filenameOverlay = (
|
||||
<OverlayTrigger
|
||||
delayShow={1000}
|
||||
placement='top'
|
||||
overlay={<Tooltip id='file-name__tooltip'>{filenameString}</Tooltip>}
|
||||
overlay={<Tooltip id='file-name__tooltip'>{fileName}</Tooltip>}
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
@@ -214,13 +121,28 @@ class FileAttachment extends React.Component {
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
} else {
|
||||
filenameOverlay = (
|
||||
<OverlayTrigger
|
||||
delayShow={1000}
|
||||
placement='top'
|
||||
overlay={<Tooltip id='file-name__tooltip'>{Utils.localizeMessage('file_attachment.download', 'Download') + ' "' + fileName + '"'}</Tooltip>}
|
||||
>
|
||||
<a
|
||||
href={fileUrl}
|
||||
download={fileName}
|
||||
className='post-image__name'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{trimmedFilename}
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='post-image__column'
|
||||
key={filename}
|
||||
>
|
||||
<div className='post-image__column'>
|
||||
<a
|
||||
className='post-image__thumbnail'
|
||||
href='#'
|
||||
@@ -233,17 +155,15 @@ class FileAttachment extends React.Component {
|
||||
<div>
|
||||
<a
|
||||
href={fileUrl}
|
||||
download={filenameString}
|
||||
download={fileName}
|
||||
className='post-image__download'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<span
|
||||
className='fa fa-download'
|
||||
/>
|
||||
<span className='fa fa-download'/>
|
||||
</a>
|
||||
<span className='post-image__type'>{fileInfo.ext.toUpperCase()}</span>
|
||||
<span className='post-image__size'>{fileSizeString}</span>
|
||||
<span className='post-image__type'>{fileInfo.extension.toUpperCase()}</span>
|
||||
<span className='post-image__size'>{Utils.fileSizeToString(fileInfo.size)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,10 +172,7 @@ class FileAttachment extends React.Component {
|
||||
}
|
||||
|
||||
FileAttachment.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
// a list of file pathes displayed by the parent FileAttachmentList
|
||||
filename: React.PropTypes.string.isRequired,
|
||||
fileInfo: React.PropTypes.object.isRequired,
|
||||
|
||||
// the index of this attachment preview in the parent FileAttachmentList
|
||||
index: React.PropTypes.number.isRequired,
|
||||
@@ -264,6 +181,4 @@ FileAttachment.propTypes = {
|
||||
handleImageClick: React.PropTypes.func,
|
||||
|
||||
compactDisplay: React.PropTypes.bool
|
||||
};
|
||||
|
||||
export default injectIntl(FileAttachment);
|
||||
};
|
||||
@@ -13,25 +13,34 @@ export default class FileAttachmentList extends React.Component {
|
||||
|
||||
this.handleImageClick = this.handleImageClick.bind(this);
|
||||
|
||||
this.state = {showPreviewModal: false, startImgId: 0};
|
||||
this.state = {showPreviewModal: false, startImgIndex: 0};
|
||||
}
|
||||
handleImageClick(indexClicked) {
|
||||
this.setState({showPreviewModal: true, startImgId: indexClicked});
|
||||
}
|
||||
render() {
|
||||
var filenames = this.props.filenames;
|
||||
|
||||
var postFiles = [];
|
||||
for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) {
|
||||
postFiles.push(
|
||||
<FileAttachment
|
||||
key={'file_attachment_' + i}
|
||||
filename={filenames[i]}
|
||||
index={i}
|
||||
handleImageClick={this.handleImageClick}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
/>
|
||||
);
|
||||
handleImageClick(indexClicked) {
|
||||
this.setState({showPreviewModal: true, startImgIndex: indexClicked});
|
||||
}
|
||||
|
||||
render() {
|
||||
const postFiles = [];
|
||||
if (this.props.fileInfos && this.props.fileInfos.length > 0) {
|
||||
for (let i = 0; i < Math.min(this.props.fileInfos.length, Constants.MAX_DISPLAY_FILES); i++) {
|
||||
const fileInfo = this.props.fileInfos[i];
|
||||
|
||||
postFiles.push(
|
||||
<FileAttachment
|
||||
key={fileInfo.id}
|
||||
fileInfo={this.props.fileInfos[i]}
|
||||
index={i}
|
||||
handleImageClick={this.handleImageClick}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (this.props.fileCount > 0) {
|
||||
for (let i = 0; i < Math.min(this.props.fileCount, Constants.MAX_DISPLAY_FILES); i++) {
|
||||
// Add a placeholder to avoid pop-in once we get the file infos for this post
|
||||
postFiles.push(<div className='post-image__column post-image__column--placeholder'/>);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -42,10 +51,8 @@ export default class FileAttachmentList extends React.Component {
|
||||
<ViewImageModal
|
||||
show={this.state.showPreviewModal}
|
||||
onModalDismissed={() => this.setState({showPreviewModal: false})}
|
||||
channelId={this.props.channelId}
|
||||
userId={this.props.userId}
|
||||
startId={this.state.startImgId}
|
||||
filenames={filenames}
|
||||
startId={this.state.startImgIndex}
|
||||
fileInfos={this.props.fileInfos}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -53,15 +60,7 @@ export default class FileAttachmentList extends React.Component {
|
||||
}
|
||||
|
||||
FileAttachmentList.propTypes = {
|
||||
|
||||
// a list of file pathes displayed by this
|
||||
filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
|
||||
|
||||
// the channel that this is part of
|
||||
channelId: React.PropTypes.string,
|
||||
|
||||
// the user that owns the post that this is attached to
|
||||
userId: React.PropTypes.string,
|
||||
|
||||
fileCount: React.PropTypes.number.isRequired,
|
||||
fileInfos: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
||||
compactDisplay: React.PropTypes.bool
|
||||
};
|
||||
|
||||
90
webapp/components/file_attachment_list_container.jsx
Обычный файл
90
webapp/components/file_attachment_list_container.jsx
Обычный файл
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import FileStore from 'stores/file_store.jsx';
|
||||
|
||||
import FileAttachmentList from './file_attachment_list.jsx';
|
||||
|
||||
export default class FileAttachmentListContainer extends React.Component {
|
||||
static propTypes = {
|
||||
post: React.PropTypes.object.isRequired,
|
||||
compactDisplay: React.PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleFileChange = this.handleFileChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
fileInfos: FileStore.getInfosForPost(props.post.id)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
FileStore.addChangeListener(this.handleFileChange);
|
||||
|
||||
if (this.props.post.id && !FileStore.hasInfosForPost(this.props.post.id)) {
|
||||
AsyncClient.getFileInfosForPost(this.props.post.channel_id, this.props.post.id);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.post.id !== this.props.post.id) {
|
||||
this.setState({
|
||||
fileInfos: FileStore.getInfosForPost(nextProps.post.id)
|
||||
});
|
||||
|
||||
if (nextProps.post.id && !FileStore.hasInfosForPost(nextProps.post.id)) {
|
||||
AsyncClient.getFileInfosForPost(nextProps.post.channel_id, nextProps.post.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (this.props.post.id !== nextProps.post.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.props.compactDisplay !== nextProps.compactDisplay) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// fileInfos are treated as immutable by the FileStore
|
||||
if (nextState.fileInfos !== this.state.fileInfos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
handleFileChange() {
|
||||
this.setState({
|
||||
fileInfos: FileStore.getInfosForPost(this.props.post.id)
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
FileStore.removeChangeListener(this.handleFileChange);
|
||||
}
|
||||
|
||||
render() {
|
||||
let fileCount = 0;
|
||||
if (this.props.post.file_ids) {
|
||||
fileCount = this.props.post.file_ids.length;
|
||||
} else if (this.props.post.filenames) {
|
||||
fileCount = this.props.post.filenames.length;
|
||||
}
|
||||
|
||||
return (
|
||||
<FileAttachmentList
|
||||
fileCount={fileCount}
|
||||
fileInfos={this.state.fileInfos}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,59 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {defineMessages} from 'react-intl';
|
||||
import React from 'react';
|
||||
import {Link} from 'react-router/es6';
|
||||
export default class FileInfoPreview extends React.Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.fileUrl !== this.props.fileUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const holders = defineMessages({
|
||||
type: {
|
||||
id: 'file_info_preview.type',
|
||||
defaultMessage: 'File type '
|
||||
},
|
||||
size: {
|
||||
id: 'file_info_preview.size',
|
||||
defaultMessage: 'Size '
|
||||
}
|
||||
});
|
||||
if (!Utils.areObjectsEqual(nextProps.fileInfo, this.props.fileInfo)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function FileInfoPreview({filename, fileUrl, fileInfo, formatMessage}) {
|
||||
// non-image files include a section providing details about the file
|
||||
const infoParts = [];
|
||||
|
||||
if (fileInfo.extension !== '') {
|
||||
infoParts.push(formatMessage(holders.type) + fileInfo.extension.toUpperCase());
|
||||
return false;
|
||||
}
|
||||
|
||||
infoParts.push(formatMessage(holders.size) + Utils.fileSizeToString(fileInfo.size));
|
||||
render() {
|
||||
const fileInfo = this.props.fileInfo;
|
||||
const fileUrl = this.props.fileUrl;
|
||||
|
||||
const infoString = infoParts.join(', ');
|
||||
// non-image files include a section providing details about the file
|
||||
const infoParts = [];
|
||||
|
||||
const name = decodeURIComponent(Utils.getFileName(filename));
|
||||
if (fileInfo.extension !== '') {
|
||||
infoParts.push(Utils.localizeMessage('file_info_preview.type', 'File type ') + fileInfo.extension.toUpperCase());
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='file-details__container'>
|
||||
<Link
|
||||
className={'file-details__preview'}
|
||||
to={fileUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<span className='file-details__preview-helper'/>
|
||||
<img src={Utils.getPreviewImagePath(filename)}/>
|
||||
</Link>
|
||||
<div className='file-details'>
|
||||
<div className='file-details__name'>{name}</div>
|
||||
<div className='file-details__info'>{infoString}</div>
|
||||
infoParts.push(Utils.localizeMessage('file_info_preview.size', 'Size ') + Utils.fileSizeToString(fileInfo.size));
|
||||
|
||||
const infoString = infoParts.join(', ');
|
||||
|
||||
return (
|
||||
<div className='file-details__container'>
|
||||
<a
|
||||
className={'file-details__preview'}
|
||||
to={fileUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<span className='file-details__preview-helper'/>
|
||||
<img src={Utils.getFileIconPath(fileInfo)}/>
|
||||
</a>
|
||||
<div className='file-details'>
|
||||
<div className='file-details__name'>{fileInfo.name}</div>
|
||||
<div className='file-details__info'>{infoString}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileInfoPreview.propTypes = {
|
||||
filename: React.PropTypes.string.isRequired,
|
||||
fileUrl: React.PropTypes.string.isRequired,
|
||||
fileInfo: React.PropTypes.object.isRequired,
|
||||
formatMessage: React.PropTypes.func.isRequired
|
||||
fileUrl: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import FileStore from 'stores/file_store.jsx';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
@@ -21,63 +22,43 @@ export default class FilePreview extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleRemove(e) {
|
||||
var previewDiv = e.target.parentNode.parentNode;
|
||||
|
||||
if (previewDiv.hasAttribute('data-filename')) {
|
||||
this.props.onRemove(previewDiv.getAttribute('data-filename'));
|
||||
} else if (previewDiv.hasAttribute('data-client-id')) {
|
||||
this.props.onRemove(previewDiv.getAttribute('data-client-id'));
|
||||
}
|
||||
handleRemove(id) {
|
||||
this.props.onRemove(id);
|
||||
}
|
||||
|
||||
render() {
|
||||
var previews = [];
|
||||
this.props.files.forEach((fullFilename) => {
|
||||
var filename = fullFilename;
|
||||
var originalFilename = filename;
|
||||
var filenameSplit = filename.split('.');
|
||||
var ext = filenameSplit[filenameSplit.length - 1];
|
||||
var type = Utils.getFileType(ext);
|
||||
|
||||
filename = Utils.getFileUrl(filename);
|
||||
this.props.fileInfos.forEach((info) => {
|
||||
const type = Utils.getFileType(info.extension);
|
||||
|
||||
let className = 'file-preview';
|
||||
let previewImage;
|
||||
if (type === 'image') {
|
||||
previews.push(
|
||||
<div
|
||||
key={filename}
|
||||
className='file-preview'
|
||||
data-filename={originalFilename}
|
||||
>
|
||||
<img
|
||||
className='file-preview__image'
|
||||
src={filename}
|
||||
/>
|
||||
<a
|
||||
className='file-preview__remove'
|
||||
onClick={this.handleRemove}
|
||||
>
|
||||
<i className='fa fa-remove'/>
|
||||
</a>
|
||||
</div>
|
||||
previewImage = (
|
||||
<img
|
||||
className='file-preview__image'
|
||||
src={FileStore.getFileUrl(info.id)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
previews.push(
|
||||
<div
|
||||
key={filename}
|
||||
className='file-preview custom-file'
|
||||
data-filename={originalFilename}
|
||||
>
|
||||
<div className={'file-icon ' + Utils.getIconClassName(type)}/>
|
||||
<a
|
||||
className='file-preview__remove'
|
||||
onClick={this.handleRemove}
|
||||
>
|
||||
<i className='fa fa-remove'/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
className += ' custom-file';
|
||||
previewImage = <div className={'file-icon ' + Utils.getIconClassName(type)}/>;
|
||||
}
|
||||
|
||||
previews.push(
|
||||
<div
|
||||
key={info.id}
|
||||
className={className}
|
||||
>
|
||||
{previewImage}
|
||||
<a
|
||||
className='file-preview__remove'
|
||||
onClick={this.handleRemove.bind(this, info.id)}
|
||||
>
|
||||
<i className='fa fa-remove'/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
this.props.uploadsInProgress.forEach((clientId) => {
|
||||
@@ -94,7 +75,7 @@ export default class FilePreview extends React.Component {
|
||||
/>
|
||||
<a
|
||||
className='file-preview__remove'
|
||||
onClick={this.handleRemove}
|
||||
onClick={this.handleRemove.bind(this, clientId)}
|
||||
>
|
||||
<i className='fa fa-remove'/>
|
||||
</a>
|
||||
@@ -111,11 +92,11 @@ export default class FilePreview extends React.Component {
|
||||
}
|
||||
|
||||
FilePreview.defaultProps = {
|
||||
files: [],
|
||||
fileInfos: [],
|
||||
uploadsInProgress: []
|
||||
};
|
||||
FilePreview.propTypes = {
|
||||
onRemove: React.PropTypes.func.isRequired,
|
||||
files: React.PropTypes.array,
|
||||
fileInfos: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
||||
uploadsInProgress: React.PropTypes.array
|
||||
};
|
||||
|
||||
@@ -49,13 +49,12 @@ class FileUpload extends React.Component {
|
||||
this.keyUpload = this.keyUpload.bind(this);
|
||||
|
||||
this.state = {
|
||||
maxFileSize: global.window.mm_config.MaxFileSize,
|
||||
requests: {}
|
||||
};
|
||||
}
|
||||
|
||||
fileUploadSuccess(channelId, data) {
|
||||
this.props.onFileUpload(data.filenames, data.client_ids, channelId);
|
||||
this.props.onFileUpload(data.file_infos, data.client_ids, channelId);
|
||||
|
||||
const requests = Object.assign({}, this.state.requests);
|
||||
for (var j = 0; j < data.client_ids.length; j++) {
|
||||
@@ -81,7 +80,7 @@ class FileUpload extends React.Component {
|
||||
const tooLargeFiles = [];
|
||||
|
||||
for (let i = 0; i < files.length && numUploads < uploadsRemaining; i++) {
|
||||
if (files[i].size > this.state.maxFileSize) {
|
||||
if (files[i].size > global.mm_config.MaxFileSize) {
|
||||
tooLargeFiles.push(files[i]);
|
||||
continue;
|
||||
}
|
||||
@@ -112,9 +111,9 @@ class FileUpload extends React.Component {
|
||||
} else if (tooLargeFiles.length > 1) {
|
||||
var tooLargeFilenames = tooLargeFiles.map((file) => file.name).join(', ');
|
||||
|
||||
this.props.onUploadError(formatMessage(holders.filesAbove, {max: (this.state.maxFileSize / 1048576), filenames: tooLargeFilenames}));
|
||||
this.props.onUploadError(formatMessage(holders.filesAbove, {max: (global.mm_config.MaxFileSize / 1048576), filenames: tooLargeFilenames}));
|
||||
} else if (tooLargeFiles.length > 0) {
|
||||
this.props.onUploadError(formatMessage(holders.fileAbove, {max: (this.state.maxFileSize / 1048576), filename: tooLargeFiles[0].name}));
|
||||
this.props.onUploadError(formatMessage(holders.fileAbove, {max: (global.mm_config.MaxFileSize / 1048576), filename: tooLargeFiles[0].name}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export default class GetPublicLinkModal extends React.Component {
|
||||
|
||||
this.state = {
|
||||
show: false,
|
||||
filename: '',
|
||||
fileId: '',
|
||||
link: ''
|
||||
};
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export default class GetPublicLinkModal extends React.Component {
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.show && !prevState.show) {
|
||||
AsyncClient.getPublicLink(decodeURIComponent(this.state.filename), this.handlePublicLink);
|
||||
AsyncClient.getPublicLink(this.state.fileId, this.handlePublicLink);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export default class GetPublicLinkModal extends React.Component {
|
||||
handleToggle(value, args) {
|
||||
this.setState({
|
||||
show: value,
|
||||
filename: args.filename,
|
||||
fileId: args.fileId,
|
||||
link: ''
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
import FileInfoPreview from './file_info_preview.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import loadingGif from 'images/load.gif';
|
||||
|
||||
import React from 'react';
|
||||
@@ -109,18 +107,8 @@ export default class PDFPreview extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
static support(filename) {
|
||||
const fileInfo = Utils.splitFileLocation(filename);
|
||||
const ext = fileInfo.ext;
|
||||
if (!ext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ext === 'pdf') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
static supports(fileInfo) {
|
||||
return fileInfo.extension === 'pdf';
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -138,10 +126,8 @@ export default class PDFPreview extends React.Component {
|
||||
if (!this.state.success) {
|
||||
return (
|
||||
<FileInfoPreview
|
||||
filename={this.props.filename}
|
||||
fileUrl={this.props.fileUrl}
|
||||
fileInfo={this.props.fileInfo}
|
||||
formatMessage={this.props.formatMessage}
|
||||
fileUrl={this.props.fileUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -185,8 +171,6 @@ export default class PDFPreview extends React.Component {
|
||||
}
|
||||
|
||||
PDFPreview.propTypes = {
|
||||
filename: React.PropTypes.string.isRequired,
|
||||
fileUrl: React.PropTypes.string.isRequired,
|
||||
fileInfo: React.PropTypes.object.isRequired,
|
||||
formatMessage: React.PropTypes.func.isRequired
|
||||
fileUrl: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import FileStore from 'stores/file_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
export default class CommentedOnFilesMessageContainer extends React.Component {
|
||||
static propTypes = {
|
||||
parentPostChannelId: React.PropTypes.string.isRequired,
|
||||
parentPostId: React.PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleFileChange = this.handleFileChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
fileInfos: FileStore.getInfosForPost(this.props.parentPostId)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
FileStore.addChangeListener(this.handleFileChange);
|
||||
|
||||
if (!FileStore.hasInfosForPost(this.props.parentPostId)) {
|
||||
AsyncClient.getFileInfosForPost(this.props.parentPostChannelId, this.props.parentPostId);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.parentPostId !== this.props.parentPostId) {
|
||||
this.setState({
|
||||
fileInfos: FileStore.getInfosForPost(this.props.parentPostId)
|
||||
});
|
||||
|
||||
if (!FileStore.hasInfosForPost(this.props.parentPostId)) {
|
||||
AsyncClient.getFileInfosForPost(this.props.parentPostChannelId, this.props.parentPostId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (nextProps.parentPostId !== this.props.parentPostId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.parentPostChannelId !== this.props.parentPostChannelId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// fileInfos are treated as immutable by the FileStore
|
||||
if (nextState.fileInfos !== this.state.fileInfos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
handleFileChange() {
|
||||
this.setState({
|
||||
fileInfos: FileStore.getInfosForPost(this.props.parentPostId)
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
FileStore.removeChangeListener(this.handleFileChange);
|
||||
}
|
||||
|
||||
render() {
|
||||
let message = ' ';
|
||||
|
||||
if (this.state.fileInfos && this.state.fileInfos.length > 0) {
|
||||
message = this.state.fileInfos[0].name;
|
||||
|
||||
if (this.state.fileInfos.length === 2) {
|
||||
message += Utils.localizeMessage('post_body.plusOne', ' plus 1 other file');
|
||||
} else if (this.state.fileInfos.length > 2) {
|
||||
message += Utils.localizeMessage('post_body.plusMore', ' plus {count} other files').replace('{count}', (this.state.fileInfos.length - 1).toString());
|
||||
}
|
||||
}
|
||||
|
||||
return <span>{message}</span>;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import FileAttachmentList from 'components/file_attachment_list.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import CommentedOnFilesMessageContainer from './commented_on_files_message_container.jsx';
|
||||
import FileAttachmentListContainer from 'components/file_attachment_list_container.jsx';
|
||||
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
|
||||
import PostMessageContainer from './post_message_container.jsx';
|
||||
import PendingPostOptions from './pending_post_options.jsx';
|
||||
@@ -22,6 +23,7 @@ export default class PostBody extends React.Component {
|
||||
|
||||
this.removePost = this.removePost.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.isCommentMention !== this.props.isCommentMention) {
|
||||
return true;
|
||||
@@ -56,7 +58,6 @@ export default class PostBody extends React.Component {
|
||||
|
||||
render() {
|
||||
const post = this.props.post;
|
||||
const filenames = this.props.post.filenames;
|
||||
const parentPost = this.props.parentPost;
|
||||
|
||||
let comment = '';
|
||||
@@ -94,14 +95,13 @@ export default class PostBody extends React.Component {
|
||||
let message = '';
|
||||
if (parentPost.message) {
|
||||
message = Utils.replaceHtmlEntities(parentPost.message);
|
||||
} else if (parentPost.filenames.length) {
|
||||
message = parentPost.filenames[0].split('/').pop();
|
||||
|
||||
if (parentPost.filenames.length === 2) {
|
||||
message += Utils.localizeMessage('post_body.plusOne', ' plus 1 other file');
|
||||
} else if (parentPost.filenames.length > 2) {
|
||||
message += Utils.localizeMessage('post_body.plusMore', ' plus {count} other files').replace('{count}', (parentPost.filenames.length - 1).toString());
|
||||
}
|
||||
} else if (parentPost.file_ids && parentPost.file_ids.length > 0) {
|
||||
message = (
|
||||
<CommentedOnFilesMessageContainer
|
||||
parentPostChannelId={parentPost.channel_id}
|
||||
parentPostId={parentPost.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
comment = (
|
||||
@@ -140,14 +140,11 @@ export default class PostBody extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
let fileAttachmentHolder = '';
|
||||
if (filenames && filenames.length > 0) {
|
||||
let fileAttachmentHolder = null;
|
||||
if ((post.file_ids && post.file_ids.length > 0) || (post.filenames && post.filenames.length > 0)) {
|
||||
fileAttachmentHolder = (
|
||||
<FileAttachmentList
|
||||
|
||||
filenames={filenames}
|
||||
channelId={post.channel_id}
|
||||
userId={post.user_id}
|
||||
<FileAttachmentListContainer
|
||||
post={post}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import UserProfile from './user_profile.jsx';
|
||||
import FileAttachmentList from './file_attachment_list.jsx';
|
||||
import FileAttachmentListContainer from './file_attachment_list_container.jsx';
|
||||
import PendingPostOptions from 'components/post_view/components/pending_post_options.jsx';
|
||||
import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
|
||||
import ProfilePicture from 'components/profile_picture.jsx';
|
||||
@@ -295,13 +295,11 @@ export default class RhsComment extends React.Component {
|
||||
|
||||
var dropdown = this.createDropdown();
|
||||
|
||||
var fileAttachment;
|
||||
if (post.filenames && post.filenames.length > 0) {
|
||||
let fileAttachment = null;
|
||||
if (post.file_ids && post.file_ids.length > 0) {
|
||||
fileAttachment = (
|
||||
<FileAttachmentList
|
||||
filenames={post.filenames}
|
||||
channelId={post.channel_id}
|
||||
userId={post.user_id}
|
||||
<FileAttachmentListContainer
|
||||
post={post}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import UserProfile from './user_profile.jsx';
|
||||
import PostBodyAdditionalContent from 'components/post_view/components/post_body_additional_content.jsx';
|
||||
import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
|
||||
import FileAttachmentList from './file_attachment_list.jsx';
|
||||
import FileAttachmentListContainer from './file_attachment_list_container.jsx';
|
||||
import ProfilePicture from 'components/profile_picture.jsx';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
@@ -242,13 +242,11 @@ export default class RhsRootPost extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
var fileAttachment;
|
||||
if (post.filenames && post.filenames.length > 0) {
|
||||
let fileAttachment = null;
|
||||
if (post.file_ids && post.file_ids.length > 0) {
|
||||
fileAttachment = (
|
||||
<FileAttachmentList
|
||||
filenames={post.filenames}
|
||||
channelId={post.channel_id}
|
||||
userId={post.user_id}
|
||||
<FileAttachmentListContainer
|
||||
post={post}
|
||||
compactDisplay={this.props.compactDisplay}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,6 @@ import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
|
||||
import FileStore from 'stores/file_store.jsx';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
@@ -19,19 +18,11 @@ const KeyCodes = Constants.KeyCodes;
|
||||
|
||||
import $ from 'jquery';
|
||||
import React from 'react';
|
||||
import {intlShape, injectIntl, defineMessages} from 'react-intl';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import loadingGif from 'images/load.gif';
|
||||
|
||||
const holders = defineMessages({
|
||||
loading: {
|
||||
id: 'view_image.loading',
|
||||
defaultMessage: 'Loading '
|
||||
}
|
||||
});
|
||||
|
||||
class ViewImageModal extends React.Component {
|
||||
export default class ViewImageModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -45,18 +36,15 @@ class ViewImageModal extends React.Component {
|
||||
this.onModalShown = this.onModalShown.bind(this);
|
||||
this.onModalHidden = this.onModalHidden.bind(this);
|
||||
|
||||
this.onFileStoreChange = this.onFileStoreChange.bind(this);
|
||||
|
||||
this.handleGetPublicLink = this.handleGetPublicLink.bind(this);
|
||||
this.onMouseEnterImage = this.onMouseEnterImage.bind(this);
|
||||
this.onMouseLeaveImage = this.onMouseLeaveImage.bind(this);
|
||||
|
||||
this.state = {
|
||||
imgId: this.props.startId,
|
||||
fileInfo: null,
|
||||
imgHeight: '100%',
|
||||
loaded: Utils.fillArray(false, this.props.filenames.length),
|
||||
progress: Utils.fillArray(0, this.props.filenames.length),
|
||||
loaded: Utils.fillArray(false, this.props.fileInfos.length),
|
||||
progress: Utils.fillArray(0, this.props.fileInfos.length),
|
||||
showFooter: false
|
||||
};
|
||||
}
|
||||
@@ -66,7 +54,7 @@ class ViewImageModal extends React.Component {
|
||||
e.stopPropagation();
|
||||
}
|
||||
let id = this.state.imgId + 1;
|
||||
if (id > this.props.filenames.length - 1) {
|
||||
if (id > this.props.fileInfos.length - 1) {
|
||||
id = 0;
|
||||
}
|
||||
this.showImage(id);
|
||||
@@ -78,7 +66,7 @@ class ViewImageModal extends React.Component {
|
||||
}
|
||||
let id = this.state.imgId - 1;
|
||||
if (id < 0) {
|
||||
id = this.props.filenames.length - 1;
|
||||
id = this.props.fileInfos.length - 1;
|
||||
}
|
||||
this.showImage(id);
|
||||
}
|
||||
@@ -95,8 +83,6 @@ class ViewImageModal extends React.Component {
|
||||
$(window).on('keyup', this.handleKeyPress);
|
||||
|
||||
this.showImage(nextProps.startId);
|
||||
|
||||
FileStore.addChangeListener(this.onFileStoreChange);
|
||||
}
|
||||
|
||||
onModalHidden() {
|
||||
@@ -105,8 +91,6 @@ class ViewImageModal extends React.Component {
|
||||
if (this.refs.video) {
|
||||
this.refs.video.stop();
|
||||
}
|
||||
|
||||
FileStore.removeChangeListener(this.onFileStoreChange);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
@@ -116,64 +100,36 @@ class ViewImageModal extends React.Component {
|
||||
this.onModalHidden();
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(this.props.filenames, nextProps.filenames)) {
|
||||
if (this.props.fileInfos !== nextProps.fileInfos) {
|
||||
this.setState({
|
||||
loaded: Utils.fillArray(false, nextProps.filenames.length),
|
||||
progress: Utils.fillArray(0, nextProps.filenames.length)
|
||||
loaded: Utils.fillArray(false, nextProps.fileInfos.length),
|
||||
progress: Utils.fillArray(0, nextProps.fileInfos.length)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onFileStoreChange(filename) {
|
||||
const id = this.props.filenames.indexOf(filename);
|
||||
|
||||
if (id !== -1) {
|
||||
if (id === this.state.imgId) {
|
||||
this.setState({
|
||||
fileInfo: FileStore.getInfo(filename)
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.state.loaded[id]) {
|
||||
this.loadImage(id, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showImage(id) {
|
||||
this.setState({imgId: id});
|
||||
|
||||
const imgHeight = $(window).height() - 100;
|
||||
this.setState({imgHeight});
|
||||
|
||||
const filename = this.props.filenames[id];
|
||||
|
||||
if (!FileStore.hasInfo(filename)) {
|
||||
// the image will actually be loaded once we know what we need to load
|
||||
AsyncClient.getFileInfo(filename);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
fileInfo: FileStore.getInfo(filename)
|
||||
});
|
||||
|
||||
if (!this.state.loaded[id]) {
|
||||
this.loadImage(id, filename);
|
||||
this.loadImage(id);
|
||||
}
|
||||
}
|
||||
|
||||
loadImage(id, filename) {
|
||||
const fileInfo = FileStore.getInfo(filename);
|
||||
loadImage(index) {
|
||||
const fileInfo = this.props.fileInfos[index];
|
||||
const fileType = Utils.getFileType(fileInfo.extension);
|
||||
|
||||
if (fileType === 'image') {
|
||||
let previewUrl;
|
||||
if (fileInfo.has_image_preview) {
|
||||
previewUrl = Utils.getPreviewImagePath(filename);
|
||||
previewUrl = FileStore.getFilePreviewUrl(fileInfo.id);
|
||||
} else {
|
||||
// some images (eg animated gifs) just show the file itself and not a preview
|
||||
previewUrl = Utils.getFileUrl(filename);
|
||||
previewUrl = FileStore.getFileUrl(fileInfo.id);
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
@@ -181,19 +137,19 @@ class ViewImageModal extends React.Component {
|
||||
previewUrl,
|
||||
() => {
|
||||
const progress = this.state.progress;
|
||||
progress[id] = img.completedPercentage;
|
||||
progress[index] = img.completedPercentage;
|
||||
this.setState({progress});
|
||||
}
|
||||
);
|
||||
img.onload = () => {
|
||||
const loaded = this.state.loaded;
|
||||
loaded[id] = true;
|
||||
loaded[index] = true;
|
||||
this.setState({loaded});
|
||||
};
|
||||
} else {
|
||||
// there's nothing to load for non-image files
|
||||
var loaded = this.state.loaded;
|
||||
loaded[id] = true;
|
||||
loaded[index] = true;
|
||||
this.setState({loaded});
|
||||
}
|
||||
}
|
||||
@@ -201,7 +157,7 @@ class ViewImageModal extends React.Component {
|
||||
handleGetPublicLink() {
|
||||
this.props.onModalDismissed();
|
||||
|
||||
GlobalActions.showGetPublicLinkModal(this.props.filenames[this.state.imgId]);
|
||||
GlobalActions.showGetPublicLinkModal(this.props.fileInfos[this.state.imgId].id);
|
||||
}
|
||||
|
||||
onMouseEnterImage() {
|
||||
@@ -213,63 +169,52 @@ class ViewImageModal extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.filenames.length < 1 || this.props.filenames.length - 1 < this.state.imgId) {
|
||||
return <div/>;
|
||||
if (this.props.fileInfos.length < 1 || this.props.fileInfos.length - 1 < this.state.imgId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filename = this.props.filenames[this.state.imgId];
|
||||
const fileUrl = Utils.getFileUrl(filename);
|
||||
const fileInfo = this.props.fileInfos[this.state.imgId];
|
||||
const fileUrl = FileStore.getFileUrl(fileInfo.id);
|
||||
|
||||
var content;
|
||||
let content;
|
||||
if (this.state.loaded[this.state.imgId]) {
|
||||
// this.state.fileInfo is for the current image and we shoudl have it before we load the image
|
||||
const fileInfo = this.state.fileInfo;
|
||||
const fileType = Utils.getFileType(fileInfo.extension);
|
||||
|
||||
if (fileType === 'image') {
|
||||
content = (
|
||||
<ImagePreview
|
||||
filename={filename}
|
||||
fileUrl={fileUrl}
|
||||
fileInfo={fileInfo}
|
||||
fileUrl={fileUrl}
|
||||
maxHeight={this.state.imgHeight}
|
||||
/>
|
||||
);
|
||||
} else if (fileType === 'video' || fileType === 'audio') {
|
||||
content = (
|
||||
<AudioVideoPreview
|
||||
filename={filename}
|
||||
fileInfo={fileInfo}
|
||||
fileUrl={fileUrl}
|
||||
fileInfo={this.state.fileInfo}
|
||||
maxHeight={this.state.imgHeight}
|
||||
formatMessage={this.props.intl.formatMessage}
|
||||
/>
|
||||
);
|
||||
} else if (PDFPreview.support(filename)) {
|
||||
} else if (PDFPreview.supports(fileInfo)) {
|
||||
content = (
|
||||
<PDFPreview
|
||||
filename={filename}
|
||||
fileUrl={fileUrl}
|
||||
fileInfo={fileInfo}
|
||||
formatMessage={this.props.intl.formatMessage}
|
||||
fileUrl={fileUrl}
|
||||
/>
|
||||
);
|
||||
} else if (CodePreview.support(filename)) {
|
||||
} else if (CodePreview.supports(fileInfo)) {
|
||||
content = (
|
||||
<CodePreview
|
||||
filename={filename}
|
||||
fileUrl={fileUrl}
|
||||
fileInfo={fileInfo}
|
||||
formatMessage={this.props.intl.formatMessage}
|
||||
fileUrl={fileUrl}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<FileInfoPreview
|
||||
filename={filename}
|
||||
fileUrl={fileUrl}
|
||||
fileInfo={fileInfo}
|
||||
formatMessage={this.props.intl.formatMessage}
|
||||
fileUrl={fileUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -280,14 +225,14 @@ class ViewImageModal extends React.Component {
|
||||
content = (
|
||||
<LoadingImagePreview
|
||||
progress={progress}
|
||||
loading={this.props.intl.formatMessage(holders.loading)}
|
||||
loading={Utils.localizeMessage('view_image.loading', 'Loading ')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let leftArrow = null;
|
||||
let rightArrow = null;
|
||||
if (this.props.filenames.length > 1) {
|
||||
if (this.props.fileInfos.length > 1) {
|
||||
leftArrow = (
|
||||
<a
|
||||
ref='previewArrowLeft'
|
||||
@@ -346,8 +291,8 @@ class ViewImageModal extends React.Component {
|
||||
<ViewImagePopoverBar
|
||||
show={this.state.showFooter}
|
||||
fileId={this.state.imgId}
|
||||
totalFiles={this.props.filenames.length}
|
||||
filename={name}
|
||||
totalFiles={this.props.fileInfos.length}
|
||||
filename={fileInfo.name}
|
||||
fileURL={fileUrl}
|
||||
onGetPublicLink={this.handleGetPublicLink}
|
||||
/>
|
||||
@@ -363,19 +308,13 @@ class ViewImageModal extends React.Component {
|
||||
|
||||
ViewImageModal.defaultProps = {
|
||||
show: false,
|
||||
filenames: [],
|
||||
channelId: '',
|
||||
userId: '',
|
||||
fileInfos: [],
|
||||
startId: 0
|
||||
};
|
||||
ViewImageModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onModalDismissed: React.PropTypes.func.isRequired,
|
||||
filenames: React.PropTypes.array,
|
||||
modalId: React.PropTypes.string,
|
||||
channelId: React.PropTypes.string,
|
||||
userId: React.PropTypes.string,
|
||||
fileInfos: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
||||
startId: React.PropTypes.number
|
||||
};
|
||||
|
||||
@@ -405,10 +344,10 @@ LoadingImagePreview.propTypes = {
|
||||
loading: React.PropTypes.string
|
||||
};
|
||||
|
||||
function ImagePreview({filename, fileUrl, fileInfo, maxHeight}) {
|
||||
function ImagePreview({fileInfo, fileUrl, maxHeight}) {
|
||||
let previewUrl;
|
||||
if (fileInfo.has_preview_image) {
|
||||
previewUrl = Utils.getPreviewImagePath(filename);
|
||||
previewUrl = FileStore.getFilePreviewUrl(fileInfo.id);
|
||||
} else {
|
||||
previewUrl = fileUrl;
|
||||
}
|
||||
@@ -429,10 +368,7 @@ function ImagePreview({filename, fileUrl, fileInfo, maxHeight}) {
|
||||
}
|
||||
|
||||
ImagePreview.propTypes = {
|
||||
filename: React.PropTypes.string.isRequired,
|
||||
fileUrl: React.PropTypes.string.isRequired,
|
||||
fileInfo: React.PropTypes.object.isRequired,
|
||||
fileUrl: React.PropTypes.string.isRequired,
|
||||
maxHeight: React.PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(ViewImageModal);
|
||||
|
||||
@@ -166,6 +166,10 @@
|
||||
margin: 5px 10px 5px 0;
|
||||
position: relative;
|
||||
width: 240px;
|
||||
|
||||
&--placeholder {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.post-image__load {
|
||||
|
||||
@@ -16,41 +16,58 @@ class FileStore extends EventEmitter {
|
||||
this.handleEventPayload = this.handleEventPayload.bind(this);
|
||||
this.dispatchToken = AppDispatcher.register(this.handleEventPayload);
|
||||
|
||||
this.fileInfo = new Map();
|
||||
this.setMaxListeners(600);
|
||||
|
||||
this.fileInfosByPost = new Map();
|
||||
}
|
||||
|
||||
addChangeListener(callback) {
|
||||
this.on(CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeChangeListener(callback) {
|
||||
this.removeListener(CHANGE_EVENT, callback);
|
||||
}
|
||||
emitChange(filename) {
|
||||
this.emit(CHANGE_EVENT, filename);
|
||||
|
||||
emitChange() {
|
||||
this.emit(CHANGE_EVENT);
|
||||
}
|
||||
|
||||
hasInfo(filename) {
|
||||
return this.fileInfo.has(filename);
|
||||
hasInfosForPost(postId) {
|
||||
return this.fileInfosByPost.has(postId);
|
||||
}
|
||||
|
||||
getInfo(filename) {
|
||||
return this.fileInfo.get(filename);
|
||||
getInfosForPost(postId) {
|
||||
return this.fileInfosByPost.get(postId);
|
||||
}
|
||||
|
||||
setInfo(filename, info) {
|
||||
this.fileInfo.set(filename, info);
|
||||
saveInfos(postId, infos) {
|
||||
this.fileInfosByPost.set(postId, infos);
|
||||
}
|
||||
|
||||
getFileUrl(fileId) {
|
||||
return `/api/v3/files/${fileId}/get`;
|
||||
}
|
||||
|
||||
getFileThumbnailUrl(fileId) {
|
||||
return `/api/v3/files/${fileId}/get_thumbnail`;
|
||||
}
|
||||
|
||||
getFilePreviewUrl(fileId) {
|
||||
return `/api/v3/files/${fileId}/get_preview`;
|
||||
}
|
||||
|
||||
handleEventPayload(payload) {
|
||||
const action = payload.action;
|
||||
|
||||
switch (action.type) {
|
||||
case ActionTypes.RECEIVED_FILE_INFO:
|
||||
this.setInfo(action.filename, action.info);
|
||||
this.emitChange(action.filename);
|
||||
case ActionTypes.RECEIVED_FILE_INFOS:
|
||||
// This assumes that all received file infos are for a single post
|
||||
this.saveInfos(action.postId, action.infos);
|
||||
this.emitChange(action.postId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new FileStore();
|
||||
export default new FileStore();
|
||||
@@ -84,7 +84,7 @@ class NotificationStoreClass extends EventEmitter {
|
||||
if (msgProps.image) {
|
||||
body = username + Utils.localizeMessage('channel_loader.uploadedImage', ' uploaded an image');
|
||||
} else if (msgProps.otherFile) {
|
||||
body = Utils.localizeMessage('channel_loader.uploadedFile', ' uploaded a file');
|
||||
body = username + Utils.localizeMessage('channel_loader.uploadedFile', ' uploaded a file');
|
||||
} else {
|
||||
body = username + Utils.localizeMessage('channel_loader.something', ' did something new');
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ class PostStoreClass extends EventEmitter {
|
||||
} else if (combinedPosts.posts.hasOwnProperty(pid)) {
|
||||
combinedPosts.posts[pid] = Object.assign({}, np, {
|
||||
state: Constants.POST_DELETED,
|
||||
filenames: []
|
||||
fileIds: []
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -318,7 +318,7 @@ class PostStoreClass extends EventEmitter {
|
||||
// make sure to copy the post so that component state changes work properly
|
||||
postList.posts[post.id] = Object.assign({}, post, {
|
||||
state: Constants.POST_DELETED,
|
||||
filenames: []
|
||||
fileIds: []
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -514,7 +514,7 @@ class PostStoreClass extends EventEmitter {
|
||||
}
|
||||
|
||||
getEmptyDraft() {
|
||||
return {message: '', uploadsInProgress: [], previews: []};
|
||||
return {message: '', uploadsInProgress: [], fileInfos: []};
|
||||
}
|
||||
|
||||
storeCurrentDraft(draft) {
|
||||
|
||||
248
webapp/tests/client_file.test.jsx
Обычный файл
248
webapp/tests/client_file.test.jsx
Обычный файл
@@ -0,0 +1,248 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
import TestHelper from './test_helper.jsx';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
describe('Client.File', function() {
|
||||
this.timeout(100000);
|
||||
|
||||
before(function() {
|
||||
// write a temporary file so that we have something to upload for testing
|
||||
const buffer = new Buffer('R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=', 'base64');
|
||||
|
||||
const testGif = fs.openSync('test.gif', 'w+');
|
||||
fs.writeFileSync(testGif, buffer);
|
||||
});
|
||||
|
||||
after(function() {
|
||||
fs.unlinkSync('test.gif');
|
||||
});
|
||||
|
||||
it('uploadFile', function(done) {
|
||||
TestHelper.initBasic(() => {
|
||||
const clientId = TestHelper.generateId();
|
||||
|
||||
TestHelper.basicClient().uploadFile(
|
||||
fs.createReadStream('test.gif'),
|
||||
'test.gif',
|
||||
TestHelper.basicChannel().id,
|
||||
clientId,
|
||||
function(resp) {
|
||||
assert.equal(resp.file_infos.length, 1);
|
||||
assert.equal(resp.client_ids.length, 1);
|
||||
assert.equal(resp.client_ids[0], clientId);
|
||||
|
||||
done();
|
||||
},
|
||||
function(err) {
|
||||
done(new Error(err.message));
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('getFile', function(done) {
|
||||
TestHelper.initBasic(() => {
|
||||
TestHelper.basicClient().uploadFile(
|
||||
fs.createReadStream('test.gif'),
|
||||
'test.gif',
|
||||
TestHelper.basicChannel().id,
|
||||
'',
|
||||
function(resp) {
|
||||
TestHelper.basicClient().getFile(
|
||||
resp.file_infos[0].id,
|
||||
function() {
|
||||
done();
|
||||
},
|
||||
function(err2) {
|
||||
done(new Error(err2.message));
|
||||
}
|
||||
);
|
||||
},
|
||||
function(err) {
|
||||
done(new Error(err.message));
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('getFileThumbnail', function(done) {
|
||||
TestHelper.initBasic(() => {
|
||||
TestHelper.basicClient().uploadFile(
|
||||
fs.createReadStream('test.gif'),
|
||||
'test.gif',
|
||||
TestHelper.basicChannel().id,
|
||||
'',
|
||||
function(resp) {
|
||||
TestHelper.basicClient().getFileThumbnail(
|
||||
resp.file_infos[0].id,
|
||||
function() {
|
||||
done();
|
||||
},
|
||||
function(err) {
|
||||
done(new Error(err.message));
|
||||
}
|
||||
);
|
||||
},
|
||||
function(err) {
|
||||
done(new Error(err.message));
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('getFilePreview', function(done) {
|
||||
TestHelper.initBasic(() => {
|
||||
TestHelper.basicClient().uploadFile(
|
||||
fs.createReadStream('test.gif'),
|
||||
'test.gif',
|
||||
TestHelper.basicChannel().id,
|
||||
'',
|
||||
function(resp) {
|
||||
TestHelper.basicClient().getFilePreview(
|
||||
resp.file_infos[0].id,
|
||||
function() {
|
||||
done();
|
||||
},
|
||||
function(err2) {
|
||||
done(new Error(err2.message));
|
||||
}
|
||||
);
|
||||
},
|
||||
function(err) {
|
||||
done(new Error(err.message));
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('getFileInfo', function(done) {
|
||||
TestHelper.initBasic(() => {
|
||||
TestHelper.basicClient().uploadFile(
|
||||
fs.createReadStream('test.gif'),
|
||||
'test.gif',
|
||||
TestHelper.basicChannel().id,
|
||||
'',
|
||||
function(resp) {
|
||||
const fileId = resp.file_infos[0].id;
|
||||
|
||||
TestHelper.basicClient().getFileInfo(
|
||||
fileId,
|
||||
function(info) {
|
||||
assert.equal(info.id, fileId);
|
||||
assert.equal(info.name, 'test.gif');
|
||||
|
||||
done();
|
||||
},
|
||||
function(err2) {
|
||||
done(new Error(err2.message));
|
||||
}
|
||||
);
|
||||
},
|
||||
function(err) {
|
||||
done(new Error(err.message));
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('getPublicLink', function(done) {
|
||||
TestHelper.initBasic(() => {
|
||||
TestHelper.basicClient().uploadFile(
|
||||
fs.createReadStream('test.gif'),
|
||||
'test.gif',
|
||||
TestHelper.basicChannel().id,
|
||||
'',
|
||||
function(resp) {
|
||||
const post = TestHelper.fakePost();
|
||||
post.channel_id = TestHelper.basicChannel().id;
|
||||
post.file_ids = resp.file_infos.map((info) => info.id);
|
||||
|
||||
TestHelper.basicClient().createPost(
|
||||
post,
|
||||
function(data) {
|
||||
assert.deepEqual(data.file_ids, post.file_ids);
|
||||
|
||||
TestHelper.basicClient().getPublicLink(
|
||||
post.file_ids[0],
|
||||
function() {
|
||||
done(new Error('public links should be disabled by default'));
|
||||
|
||||
// request.
|
||||
// get(link).
|
||||
// end(TestHelper.basicChannel().handleResponse.bind(
|
||||
// this,
|
||||
// 'getPublicLink',
|
||||
// function() {
|
||||
// done();
|
||||
// },
|
||||
// function(err4) {
|
||||
// done(new Error(err4.message));
|
||||
// }
|
||||
// ));
|
||||
},
|
||||
function() {
|
||||
done();
|
||||
|
||||
// done(new Error(err3.message));
|
||||
}
|
||||
);
|
||||
},
|
||||
function(err2) {
|
||||
done(new Error(err2.message));
|
||||
}
|
||||
);
|
||||
},
|
||||
function(err) {
|
||||
done(new Error(err.message));
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('getFileInfosForPost', function(done) {
|
||||
TestHelper.initBasic(() => {
|
||||
TestHelper.basicClient().uploadFile(
|
||||
fs.createReadStream('test.gif'),
|
||||
'test.gif',
|
||||
TestHelper.basicChannel().id,
|
||||
'',
|
||||
function(resp) {
|
||||
const post = TestHelper.fakePost();
|
||||
post.channel_id = TestHelper.basicChannel().id;
|
||||
post.file_ids = resp.file_infos.map((info) => info.id);
|
||||
|
||||
TestHelper.basicClient().createPost(
|
||||
post,
|
||||
function(data) {
|
||||
assert.deepEqual(data.file_ids, post.file_ids);
|
||||
|
||||
TestHelper.basicClient().getFileInfosForPost(
|
||||
post.channel_id,
|
||||
data.id,
|
||||
function(files) {
|
||||
assert.equal(files.length, 1);
|
||||
assert.equal(files[0].id, resp.file_infos[0].id);
|
||||
|
||||
done();
|
||||
},
|
||||
function(err3) {
|
||||
done(new Error(err3.message));
|
||||
}
|
||||
);
|
||||
},
|
||||
function(err2) {
|
||||
done(new Error(err2.message));
|
||||
}
|
||||
);
|
||||
},
|
||||
function(err) {
|
||||
done(new Error(err.message));
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -43,43 +43,5 @@ describe('Client.General', function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('File.getFileInfo', function(done) {
|
||||
TestHelper.initBasic(() => {
|
||||
TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
|
||||
|
||||
TestHelper.basicClient().getFileInfo(
|
||||
`/${TestHelper.basicChannel().id}/${TestHelper.basicUser().id}/filename.txt`,
|
||||
function(data) {
|
||||
assert.equal(data.filename, 'filename.txt');
|
||||
done();
|
||||
},
|
||||
function(err) {
|
||||
done(new Error(err.message));
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('File.getPublicLink', function(done) {
|
||||
TestHelper.initBasic(() => {
|
||||
TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
|
||||
var data = {};
|
||||
data.channel_id = TestHelper.basicChannel().id;
|
||||
data.user_id = TestHelper.basicUser().id;
|
||||
data.filename = `/${TestHelper.basicChannel().id}/${TestHelper.basicUser().id}/filename.txt`;
|
||||
|
||||
TestHelper.basicClient().getPublicLink(
|
||||
data,
|
||||
function() {
|
||||
done(new Error('not enabled'));
|
||||
},
|
||||
function(err) {
|
||||
assert.equal(err.id, 'api.file.get_public_link.disabled.app_error');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -230,5 +230,7 @@ describe('Client.Posts', function() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// getFileInfosForPost is tested in client_files.test.jsx
|
||||
});
|
||||
|
||||
|
||||
@@ -715,6 +715,32 @@ export function getPostsAfter(postId, offset, numPost, isPost) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getFileInfosForPost(channelId, postId) {
|
||||
const callName = 'getFileInfosForPost' + postId;
|
||||
|
||||
if (isCallInProgress(callName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Client.getFileInfosForPost(
|
||||
channelId,
|
||||
postId,
|
||||
(data) => {
|
||||
callTracker[callName] = 0;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_FILE_INFOS,
|
||||
postId,
|
||||
infos: data
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
callTracker[callName] = 0;
|
||||
dispatchError(err, 'getPostFile');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function getMe() {
|
||||
if (isCallInProgress('getMe')) {
|
||||
return null;
|
||||
@@ -923,34 +949,6 @@ export function getSuggestedCommands(command, suggestionId, component) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getFileInfo(filename) {
|
||||
const callName = 'getFileInfo' + filename;
|
||||
|
||||
if (isCallInProgress(callName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callTracker[callName] = utils.getTimestamp();
|
||||
|
||||
Client.getFileInfo(
|
||||
filename,
|
||||
(data) => {
|
||||
callTracker[callName] = 0;
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_FILE_INFO,
|
||||
filename,
|
||||
info: data
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
callTracker[callName] = 0;
|
||||
|
||||
dispatchError(err, 'getFileInfo');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function getStandardAnalytics(teamId) {
|
||||
const callName = 'getStandardAnaytics' + teamId;
|
||||
|
||||
@@ -1432,8 +1430,8 @@ export function regenCommandToken(id) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getPublicLink(filename, success, error) {
|
||||
const callName = 'getPublicLink' + filename;
|
||||
export function getPublicLink(fileId, success, error) {
|
||||
const callName = 'getPublicLink' + fileId;
|
||||
|
||||
if (isCallInProgress(callName)) {
|
||||
return;
|
||||
@@ -1442,7 +1440,7 @@ export function getPublicLink(filename, success, error) {
|
||||
callTracker[callName] = utils.getTimestamp();
|
||||
|
||||
Client.getPublicLink(
|
||||
filename,
|
||||
fileId,
|
||||
(link) => {
|
||||
callTracker[callName] = 0;
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ActionTypes = keyMirror({
|
||||
RECEIVED_PREFERENCE: null,
|
||||
RECEIVED_PREFERENCES: null,
|
||||
DELETED_PREFERENCES: null,
|
||||
RECEIVED_FILE_INFO: null,
|
||||
RECEIVED_FILE_INFOS: null,
|
||||
RECEIVED_ANALYTICS: null,
|
||||
|
||||
RECEIVED_INCOMING_WEBHOOKS: null,
|
||||
|
||||
@@ -136,14 +136,9 @@ export function highlight(lang, code) {
|
||||
return TextFormatting.sanitizeHtml(code);
|
||||
}
|
||||
|
||||
export function getLanguageFromFilename(filename) {
|
||||
const fileSplit = filename.split('.');
|
||||
|
||||
let ext = fileSplit.length > 1 ? fileSplit[fileSplit.length - 1] : '';
|
||||
ext = ext.toLowerCase();
|
||||
|
||||
export function getLanguageFromFileExtension(extension) {
|
||||
for (var key in HighlightedLanguages) {
|
||||
if (HighlightedLanguages[key].extensions.find((x) => x === ext)) {
|
||||
if (HighlightedLanguages[key].extensions.find((x) => x === extension)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,8 +413,8 @@ export function getFileType(extin) {
|
||||
return 'other';
|
||||
}
|
||||
|
||||
export function getPreviewImagePathForFileType(fileTypeIn) {
|
||||
var fileType = fileTypeIn.toLowerCase();
|
||||
export function getFileIconPath(fileInfo) {
|
||||
const fileType = getFileType(fileInfo.extension);
|
||||
|
||||
var icon;
|
||||
if (fileType in Constants.ICON_FROM_TYPE) {
|
||||
@@ -451,19 +451,6 @@ export function splitFileLocation(fileLocation) {
|
||||
return {ext, name: filename, path: filePath};
|
||||
}
|
||||
|
||||
export function getPreviewImagePath(filename) {
|
||||
// Returns the path to a preview image that can be used to represent a file.
|
||||
const fileInfo = splitFileLocation(filename);
|
||||
const fileType = getFileType(fileInfo.ext);
|
||||
|
||||
if (fileType === 'image') {
|
||||
return getFileUrl(fileInfo.path + '_preview.jpg');
|
||||
}
|
||||
|
||||
// only images have proper previews, so just use a placeholder icon for non-images
|
||||
return getPreviewImagePathForFileType(fileType);
|
||||
}
|
||||
|
||||
export function toTitleCase(str) {
|
||||
function doTitleCase(txt) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
|
||||
Ссылка в новой задаче
Block a user