Adding ability to export data from mattermost
Этот коммит содержится в:
292
api/export.go
Обычный файл
292
api/export.go
Обычный файл
@@ -0,0 +1,292 @@
|
|||||||
|
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/mattermost/platform/model"
|
||||||
|
"github.com/mattermost/platform/utils"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EXPORT_PATH = "export/"
|
||||||
|
EXPORT_FILENAME = "MattermostExport.zip"
|
||||||
|
EXPORT_OPTIONS_FILE = "options.json"
|
||||||
|
EXPORT_TEAMS_FOLDER = "teams"
|
||||||
|
EXPORT_CHANNELS_FOLDER = "channels"
|
||||||
|
EXPORT_CHANNEL_MEMBERS_FOLDER = "members"
|
||||||
|
EXPORT_POSTS_FOLDER = "posts"
|
||||||
|
EXPORT_USERS_FOLDER = "users"
|
||||||
|
EXPORT_LOCAL_STORAGE_FOLDER = "files"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExportWriter interface {
|
||||||
|
Create(name string) (io.Writer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportOptions struct {
|
||||||
|
TeamsToExport []string `json:"teams"`
|
||||||
|
ChannelsToExport []string `json:"channels"`
|
||||||
|
UsersToExport []string `json:"users"`
|
||||||
|
ExportLocalStorage bool `json:"export_local_storage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *ExportOptions) ToJson() string {
|
||||||
|
b, err := json.Marshal(options)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExportOptionsFromJson(data io.Reader) *ExportOptions {
|
||||||
|
decoder := json.NewDecoder(data)
|
||||||
|
var o ExportOptions
|
||||||
|
decoder.Decode(&o)
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExportToFile(options *ExportOptions) (link string, err *model.AppError) {
|
||||||
|
// Open file for export
|
||||||
|
if file, err := openFileWriteStream(EXPORT_PATH + EXPORT_FILENAME); err != nil {
|
||||||
|
return "", err
|
||||||
|
} else {
|
||||||
|
defer closeFileWriteStream(file)
|
||||||
|
ExportToWriter(file, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/api/v1/files/get_export", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExportToWriter(w io.Writer, options *ExportOptions) *model.AppError {
|
||||||
|
// Open a writer to write to zip file
|
||||||
|
zipWriter := zip.NewWriter(w)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
// Write our options to file
|
||||||
|
if optionsFile, err := zipWriter.Create(EXPORT_OPTIONS_FILE); err != nil {
|
||||||
|
return model.NewAppError("ExportToWriter", "Unable to create options file", err.Error())
|
||||||
|
} else {
|
||||||
|
if _, err := optionsFile.Write([]byte(options.ToJson())); err != nil {
|
||||||
|
return model.NewAppError("ExportToWriter", "Unable to write to options file", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export Teams
|
||||||
|
ExportTeams(zipWriter, options)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExportTeams(writer ExportWriter, options *ExportOptions) *model.AppError {
|
||||||
|
// Get the teams
|
||||||
|
var teams []*model.Team
|
||||||
|
if len(options.TeamsToExport) == 0 {
|
||||||
|
if result := <-Srv.Store.Team().GetForExport(); result.Err != nil {
|
||||||
|
return result.Err
|
||||||
|
} else {
|
||||||
|
teams = result.Data.([]*model.Team)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, teamId := range options.TeamsToExport {
|
||||||
|
if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
|
||||||
|
return result.Err
|
||||||
|
} else {
|
||||||
|
team := result.Data.(*model.Team)
|
||||||
|
teams = append(teams, team)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the teams
|
||||||
|
for i := range teams {
|
||||||
|
// Sanitize
|
||||||
|
teams[i].PreExport()
|
||||||
|
|
||||||
|
if teamFile, err := writer.Create(EXPORT_TEAMS_FOLDER + "/" + teams[i].Name + ".json"); err != nil {
|
||||||
|
return model.NewAppError("ExportTeams", "Unable to open file for export", err.Error())
|
||||||
|
} else {
|
||||||
|
if _, err := teamFile.Write([]byte(teams[i].ToJson())); err != nil {
|
||||||
|
return model.NewAppError("ExportTeams", "Unable to write to team export file", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the channels, local storage and users
|
||||||
|
for _, team := range teams {
|
||||||
|
if err := ExportChannels(writer, options, team.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ExportUsers(writer, options, team.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ExportLocalStorage(writer, options, team.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExportChannels(writer ExportWriter, options *ExportOptions, teamId string) *model.AppError {
|
||||||
|
// Get the channels
|
||||||
|
var channels []*model.Channel
|
||||||
|
if len(options.ChannelsToExport) == 0 {
|
||||||
|
if result := <-Srv.Store.Channel().GetForExport(teamId); result.Err != nil {
|
||||||
|
return result.Err
|
||||||
|
} else {
|
||||||
|
channels = result.Data.([]*model.Channel)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, channelId := range options.ChannelsToExport {
|
||||||
|
if result := <-Srv.Store.Channel().Get(channelId); result.Err != nil {
|
||||||
|
return result.Err
|
||||||
|
} else {
|
||||||
|
channel := result.Data.(*model.Channel)
|
||||||
|
channels = append(channels, channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range channels {
|
||||||
|
// Get members
|
||||||
|
mchan := Srv.Store.Channel().GetMembers(channels[i].Id)
|
||||||
|
|
||||||
|
// Sanitize
|
||||||
|
channels[i].PreExport()
|
||||||
|
|
||||||
|
if channelFile, err := writer.Create(EXPORT_CHANNELS_FOLDER + "/" + channels[i].Id + ".json"); err != nil {
|
||||||
|
return model.NewAppError("ExportChannels", "Unable to open file for export", err.Error())
|
||||||
|
} else {
|
||||||
|
if _, err := channelFile.Write([]byte(channels[i].ToJson())); err != nil {
|
||||||
|
return model.NewAppError("ExportChannels", "Unable to write to export file", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var members []model.ChannelMember
|
||||||
|
if result := <-mchan; result.Err != nil {
|
||||||
|
return result.Err
|
||||||
|
} else {
|
||||||
|
members = result.Data.([]model.ChannelMember)
|
||||||
|
}
|
||||||
|
|
||||||
|
if membersFile, err := writer.Create(EXPORT_CHANNELS_FOLDER + "/" + channels[i].Id + "_members.json"); err != nil {
|
||||||
|
return model.NewAppError("ExportChannels", "Unable to open file for export", err.Error())
|
||||||
|
} else {
|
||||||
|
result, err2 := json.Marshal(members)
|
||||||
|
if err2 != nil {
|
||||||
|
return model.NewAppError("ExportChannels", "Unable to convert to json", err.Error())
|
||||||
|
}
|
||||||
|
if _, err3 := membersFile.Write([]byte(result)); err3 != nil {
|
||||||
|
return model.NewAppError("ExportChannels", "Unable to write to export file", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, channel := range channels {
|
||||||
|
if err := ExportPosts(writer, options, channel.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExportPosts(writer ExportWriter, options *ExportOptions, channelId string) *model.AppError {
|
||||||
|
// Get the posts
|
||||||
|
var posts []*model.Post
|
||||||
|
if result := <-Srv.Store.Post().GetForExport(channelId); result.Err != nil {
|
||||||
|
return result.Err
|
||||||
|
} else {
|
||||||
|
posts = result.Data.([]*model.Post)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the posts
|
||||||
|
if postsFile, err := writer.Create(EXPORT_POSTS_FOLDER + "/" + channelId + "_posts.json"); err != nil {
|
||||||
|
return model.NewAppError("ExportPosts", "Unable to open file for export", err.Error())
|
||||||
|
} else {
|
||||||
|
result, err2 := json.Marshal(posts)
|
||||||
|
if err2 != nil {
|
||||||
|
return model.NewAppError("ExportPosts", "Unable to convert to json", err.Error())
|
||||||
|
}
|
||||||
|
if _, err3 := postsFile.Write([]byte(result)); err3 != nil {
|
||||||
|
return model.NewAppError("ExportPosts", "Unable to write to export file", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExportUsers(writer ExportWriter, options *ExportOptions, teamId string) *model.AppError {
|
||||||
|
// Get the users
|
||||||
|
var users []*model.User
|
||||||
|
if result := <-Srv.Store.User().GetForExport(teamId); result.Err != nil {
|
||||||
|
return result.Err
|
||||||
|
} else {
|
||||||
|
users = result.Data.([]*model.User)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the users
|
||||||
|
if usersFile, err := writer.Create(EXPORT_USERS_FOLDER + "/" + teamId + "_users.json"); err != nil {
|
||||||
|
return model.NewAppError("ExportUsers", "Unable to open file for export", err.Error())
|
||||||
|
} else {
|
||||||
|
result, err2 := json.Marshal(users)
|
||||||
|
if err2 != nil {
|
||||||
|
return model.NewAppError("ExportUsers", "Unable to convert to json", err.Error())
|
||||||
|
}
|
||||||
|
if _, err3 := usersFile.Write([]byte(result)); err3 != nil {
|
||||||
|
return model.NewAppError("ExportUsers", "Unable to write to export file", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyDirToExportWriter(writer ExportWriter, inPath string, outPath string) *model.AppError {
|
||||||
|
dir, err := os.Open(inPath)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewAppError("copyDirToExportWriter", "Unable to open directory", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfoList, err := dir.Readdir(0)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewAppError("copyDirToExportWriter", "Unable to read directory", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fileInfo := range fileInfoList {
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
copyDirToExportWriter(writer, inPath+"/"+fileInfo.Name(), outPath+"/"+fileInfo.Name())
|
||||||
|
} else {
|
||||||
|
if toFile, err := writer.Create(outPath + "/" + fileInfo.Name()); err != nil {
|
||||||
|
return model.NewAppError("copyDirToExportWriter", "Unable to open file for export", err.Error())
|
||||||
|
} else {
|
||||||
|
fromFile, err := os.Open(inPath + "/" + fileInfo.Name())
|
||||||
|
if err != nil {
|
||||||
|
return model.NewAppError("copyDirToExportWriter", "Unable to open file", err.Error())
|
||||||
|
}
|
||||||
|
io.Copy(toFile, fromFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExportLocalStorage(writer ExportWriter, options *ExportOptions, teamId string) *model.AppError {
|
||||||
|
teamDir := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + teamId
|
||||||
|
|
||||||
|
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
|
||||||
|
return model.NewAppError("ExportLocalStorage", "S3 is not supported for local storage export.", "")
|
||||||
|
} else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 {
|
||||||
|
if err := copyDirToExportWriter(writer, teamDir, EXPORT_LOCAL_STORAGE_FOLDER); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
42
api/file.go
42
api/file.go
@@ -40,6 +40,7 @@ func InitFile(r *mux.Router) {
|
|||||||
sr.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandler(getFile)).Methods("GET")
|
sr.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandler(getFile)).Methods("GET")
|
||||||
sr.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,})?}", ApiAppHandler(getFileInfo)).Methods("GET")
|
sr.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,})?}", ApiAppHandler(getFileInfo)).Methods("GET")
|
||||||
sr.Handle("/get_public_link", ApiUserRequired(getPublicLink)).Methods("POST")
|
sr.Handle("/get_public_link", ApiUserRequired(getPublicLink)).Methods("POST")
|
||||||
|
sr.Handle("/get_export", ApiUserRequired(getExport)).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -414,6 +415,23 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(model.MapToJson(rData)))
|
w.Write([]byte(model.MapToJson(rData)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getExport(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !c.HasPermissionsToTeam(c.Session.TeamId, "export") || !c.IsTeamAdmin(c.Session.UserId) {
|
||||||
|
c.Err = model.NewAppError("getExport", "Only a team admin can retrieve exported data.", "userId="+c.Session.UserId)
|
||||||
|
c.Err.StatusCode = http.StatusForbidden
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := readFile(EXPORT_PATH + EXPORT_FILENAME)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError("getExport", "Unable to retrieve exported file. Please re-export", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename="+EXPORT_FILENAME)
|
||||||
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
func writeFile(f []byte, path string) *model.AppError {
|
func writeFile(f []byte, path string) *model.AppError {
|
||||||
|
|
||||||
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
|
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
|
||||||
@@ -488,3 +506,27 @@ func readFile(path string) ([]byte, *model.AppError) {
|
|||||||
return nil, model.NewAppError("readFile", "File storage not configured properly. Please configure for either S3 or local server file storage.", "")
|
return nil, model.NewAppError("readFile", "File storage not configured properly. Please configure for either S3 or local server file storage.", "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openFileWriteStream(path string) (io.Writer, *model.AppError) {
|
||||||
|
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
|
||||||
|
return nil, model.NewAppError("openFileWriteStream", "S3 is not supported.", "")
|
||||||
|
} else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(utils.Cfg.ServiceSettings.StorageDirectory+path), 0774); err != nil {
|
||||||
|
return nil, model.NewAppError("openFileWriteStream", "Encountered an error creating the directory for the new file", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileHandle, err := os.Create(utils.Cfg.ServiceSettings.StorageDirectory + path); err != nil {
|
||||||
|
return nil, model.NewAppError("openFileWriteStream", "Encountered an error writing to local server storage", err.Error())
|
||||||
|
} else {
|
||||||
|
fileHandle.Chmod(0644)
|
||||||
|
return fileHandle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, model.NewAppError("openFileWriteStream", "File storage not configured properly. Please configure for either S3 or local server file storage.", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeFileWriteStream(file io.Writer) {
|
||||||
|
file.(*os.File).Close()
|
||||||
|
}
|
||||||
|
|||||||
21
api/team.go
21
api/team.go
@@ -32,7 +32,9 @@ func InitTeam(r *mux.Router) {
|
|||||||
sr.Handle("/update_name", ApiUserRequired(updateTeamDisplayName)).Methods("POST")
|
sr.Handle("/update_name", ApiUserRequired(updateTeamDisplayName)).Methods("POST")
|
||||||
sr.Handle("/update_valet_feature", ApiUserRequired(updateValetFeature)).Methods("POST")
|
sr.Handle("/update_valet_feature", ApiUserRequired(updateValetFeature)).Methods("POST")
|
||||||
sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET")
|
sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET")
|
||||||
|
// These should be moved to the global admain console
|
||||||
sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST")
|
sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST")
|
||||||
|
sr.Handle("/export_team", ApiUserRequired(exportTeam)).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -675,3 +677,22 @@ func importTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
http.ServeContent(w, r, "MattermostImportLog.txt", time.Now(), bytes.NewReader(log.Bytes()))
|
http.ServeContent(w, r, "MattermostImportLog.txt", time.Now(), bytes.NewReader(log.Bytes()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func exportTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !c.HasPermissionsToTeam(c.Session.TeamId, "export") || !c.IsTeamAdmin(c.Session.UserId) {
|
||||||
|
c.Err = model.NewAppError("exportTeam", "Only a team admin can export data.", "userId="+c.Session.UserId)
|
||||||
|
c.Err.StatusCode = http.StatusForbidden
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options := ExportOptionsFromJson(r.Body)
|
||||||
|
|
||||||
|
if link, err := ExportToFile(options); err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
result := map[string]string{}
|
||||||
|
result["link"] = link
|
||||||
|
w.Write([]byte(model.MapToJson(result)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -117,3 +117,6 @@ func (o *Channel) PreUpdate() {
|
|||||||
func (o *Channel) ExtraUpdated() {
|
func (o *Channel) ExtraUpdated() {
|
||||||
o.ExtraUpdateAt = GetMillis()
|
o.ExtraUpdateAt = GetMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Channel) PreExport() {
|
||||||
|
}
|
||||||
|
|||||||
@@ -147,3 +147,6 @@ func (o *Post) AddProp(key string, value string) {
|
|||||||
|
|
||||||
o.Props[key] = value
|
o.Props[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Post) PreExport() {
|
||||||
|
}
|
||||||
|
|||||||
@@ -197,3 +197,6 @@ func CleanTeamName(s string) string {
|
|||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Team) PreExport() {
|
||||||
|
}
|
||||||
|
|||||||
@@ -272,6 +272,16 @@ func (u *User) GetDisplayName() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) PreExport() {
|
||||||
|
u.Password = ""
|
||||||
|
u.AuthData = ""
|
||||||
|
u.LastActivityAt = 0
|
||||||
|
u.LastPingAt = 0
|
||||||
|
u.LastPasswordUpdate = 0
|
||||||
|
u.LastPictureUpdate = 0
|
||||||
|
u.FailedAttempts = 0
|
||||||
|
}
|
||||||
|
|
||||||
// UserFromJson will decode the input and return a User
|
// UserFromJson will decode the input and return a User
|
||||||
func UserFromJson(data io.Reader) *User {
|
func UserFromJson(data io.Reader) *User {
|
||||||
decoder := json.NewDecoder(data)
|
decoder := json.NewDecoder(data)
|
||||||
|
|||||||
@@ -678,3 +678,25 @@ func (s SqlChannelStore) UpdateNotifyLevel(channelId, userId, notifyLevel string
|
|||||||
|
|
||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlChannelStore) GetForExport(teamId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
var data []*model.Channel
|
||||||
|
_, err := s.GetReplica().Select(&data, "SELECT * FROM Channels WHERE TeamId = :TeamId AND DeleteAt = 0 AND Type = 'O'", map[string]interface{}{"TeamId": teamId})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlChannelStore.GetAllChannels", "We couldn't get all the channels", "teamId="+teamId+", err="+err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|||||||
@@ -506,3 +506,27 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
|
|||||||
|
|
||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlPostStore) GetForExport(channelId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
var posts []*model.Post
|
||||||
|
_, err := s.GetReplica().Select(
|
||||||
|
&posts,
|
||||||
|
"SELECT * FROM Posts WHERE ChannelId = :ChannelId AND DeleteAt = 0",
|
||||||
|
map[string]interface{}{"ChannelId": channelId})
|
||||||
|
if err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlPostStore.GetForExport", "We couldn't get the posts for the channel", "channelId="+channelId+err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = posts
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|||||||
@@ -193,3 +193,23 @@ func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel {
|
|||||||
|
|
||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlTeamStore) GetForExport() StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
var data []*model.Team
|
||||||
|
if _, err := s.GetReplica().Select(&data, "SELECT * FROM Teams"); err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlTeamStore.GetForExport", "We could not get all teams", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Data = data
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|||||||
@@ -452,3 +452,30 @@ func (us SqlUserStore) VerifyEmail(userId string) StoreChannel {
|
|||||||
|
|
||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (us SqlUserStore) GetForExport(teamId string) StoreChannel {
|
||||||
|
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
var users []*model.User
|
||||||
|
|
||||||
|
if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}); err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlUserStore.GetProfiles", "We encounted an error while finding user profiles", err.Error())
|
||||||
|
} else {
|
||||||
|
for _, u := range users {
|
||||||
|
u.Password = ""
|
||||||
|
u.AuthData = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Data = users
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type TeamStore interface {
|
|||||||
Get(id string) StoreChannel
|
Get(id string) StoreChannel
|
||||||
GetByName(name string) StoreChannel
|
GetByName(name string) StoreChannel
|
||||||
GetTeamsForEmail(domain string) StoreChannel
|
GetTeamsForEmail(domain string) StoreChannel
|
||||||
|
GetForExport() StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChannelStore interface {
|
type ChannelStore interface {
|
||||||
@@ -55,6 +56,7 @@ type ChannelStore interface {
|
|||||||
GetChannels(teamId string, userId string) StoreChannel
|
GetChannels(teamId string, userId string) StoreChannel
|
||||||
GetMoreChannels(teamId string, userId string) StoreChannel
|
GetMoreChannels(teamId string, userId string) StoreChannel
|
||||||
GetChannelCounts(teamId string, userId string) StoreChannel
|
GetChannelCounts(teamId string, userId string) StoreChannel
|
||||||
|
GetForExport(teamId string) StoreChannel
|
||||||
|
|
||||||
SaveMember(member *model.ChannelMember) StoreChannel
|
SaveMember(member *model.ChannelMember) StoreChannel
|
||||||
GetMembers(channelId string) StoreChannel
|
GetMembers(channelId string) StoreChannel
|
||||||
@@ -78,6 +80,7 @@ type PostStore interface {
|
|||||||
GetPostsSince(channelId string, time int64) StoreChannel
|
GetPostsSince(channelId string, time int64) StoreChannel
|
||||||
GetEtag(channelId string) StoreChannel
|
GetEtag(channelId string) StoreChannel
|
||||||
Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel
|
Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel
|
||||||
|
GetForExport(channelId string) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserStore interface {
|
type UserStore interface {
|
||||||
@@ -96,6 +99,7 @@ type UserStore interface {
|
|||||||
VerifyEmail(userId string) StoreChannel
|
VerifyEmail(userId string) StoreChannel
|
||||||
GetEtagForProfiles(teamId string) StoreChannel
|
GetEtagForProfiles(teamId string) StoreChannel
|
||||||
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
|
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
|
||||||
|
GetForExport(teamId string) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionStore interface {
|
type SessionStore interface {
|
||||||
|
|||||||
96
web/react/components/team_export_tab.jsx
Обычный файл
96
web/react/components/team_export_tab.jsx
Обычный файл
@@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
var Client = require('../utils/client.jsx');
|
||||||
|
|
||||||
|
export default class TeamExportTab extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {status: 'request', link: '', err: ''};
|
||||||
|
|
||||||
|
this.onExportSuccess = this.onExportSuccess.bind(this);
|
||||||
|
this.onExportFailure = this.onExportFailure.bind(this);
|
||||||
|
this.doExport = this.doExport.bind(this);
|
||||||
|
}
|
||||||
|
onExportSuccess(data) {
|
||||||
|
this.setState({status: 'ready', link: data.link, err: ''});
|
||||||
|
}
|
||||||
|
onExportFailure(e) {
|
||||||
|
this.setState({status: 'failure', link: '', err: e.message});
|
||||||
|
}
|
||||||
|
doExport() {
|
||||||
|
if (this.state.status === 'in-progress') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({status: 'in-progress'});
|
||||||
|
Client.exportTeam(this.onExportSuccess, this.onExportFailure);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
var messageSection = '';
|
||||||
|
switch (this.state.status) {
|
||||||
|
case 'request':
|
||||||
|
messageSection = '';
|
||||||
|
break;
|
||||||
|
case 'in-progress':
|
||||||
|
messageSection = (
|
||||||
|
<p className='confirm-import alert alert-warning'>
|
||||||
|
<i className='fa fa-spinner fa-pulse' />
|
||||||
|
{' Exporting...'}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'ready':
|
||||||
|
messageSection = (
|
||||||
|
<p className='confirm-import alert alert-success'>
|
||||||
|
<i className='fa fa-check' />
|
||||||
|
{' Ready for '}
|
||||||
|
<a
|
||||||
|
href={this.state.link}
|
||||||
|
download={true}
|
||||||
|
>
|
||||||
|
{'download'}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'failure':
|
||||||
|
messageSection = (
|
||||||
|
<p className='confirm-import alert alert-warning'>
|
||||||
|
<i className='fa fa-warning' />
|
||||||
|
{' Unable to export: ' + this.state.err}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref='wrapper'
|
||||||
|
className='user-settings'
|
||||||
|
>
|
||||||
|
<h3 className='tab-header'>{'Export'}</h3>
|
||||||
|
<div className='divider-dark first'/>
|
||||||
|
<ul className='section-max'>
|
||||||
|
<li className='col-xs-12 section-title'>{'Export your team'}</li>
|
||||||
|
<li className='col-xs-offset-3 col-xs-8'>
|
||||||
|
<ul className='setting-list'>
|
||||||
|
<li className='setting-list-item'>
|
||||||
|
<span className='btn btn-sm btn-primary btn-file sel-btn'>
|
||||||
|
<a
|
||||||
|
className='btn btn-sm btn-primary'
|
||||||
|
href='#'
|
||||||
|
onClick={this.doExport}
|
||||||
|
>
|
||||||
|
{'Export'}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div className='divider-dark'/>
|
||||||
|
{messageSection}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
var TeamStore = require('../stores/team_store.jsx');
|
var TeamStore = require('../stores/team_store.jsx');
|
||||||
var ImportTab = require('./team_import_tab.jsx');
|
var ImportTab = require('./team_import_tab.jsx');
|
||||||
|
var ExportTab = require('./team_export_tab.jsx');
|
||||||
var FeatureTab = require('./team_feature_tab.jsx');
|
var FeatureTab = require('./team_feature_tab.jsx');
|
||||||
var GeneralTab = require('./team_general_tab.jsx');
|
var GeneralTab = require('./team_general_tab.jsx');
|
||||||
var Utils = require('../utils/utils.jsx');
|
var Utils = require('../utils/utils.jsx');
|
||||||
@@ -64,6 +65,13 @@ export default class TeamSettings extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'export':
|
||||||
|
result = (
|
||||||
|
<div>
|
||||||
|
<ExportTab />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
result = (
|
result = (
|
||||||
<div/>
|
<div/>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export default class TeamSettingsModal extends React.Component {
|
|||||||
let tabs = [];
|
let tabs = [];
|
||||||
tabs.push({name: 'general', uiName: 'General', icon: 'glyphicon glyphicon-cog'});
|
tabs.push({name: 'general', uiName: 'General', icon: 'glyphicon glyphicon-cog'});
|
||||||
tabs.push({name: 'import', uiName: 'Import', icon: 'glyphicon glyphicon-upload'});
|
tabs.push({name: 'import', uiName: 'Import', icon: 'glyphicon glyphicon-upload'});
|
||||||
|
tabs.push({name: 'export', uiName: 'Export', icon: 'glyphicon glyphicon-download'});
|
||||||
tabs.push({name: 'feature', uiName: 'Advanced', icon: 'glyphicon glyphicon-wrench'});
|
tabs.push({name: 'feature', uiName: 'Advanced', icon: 'glyphicon glyphicon-wrench'});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -919,6 +919,19 @@ export function importSlack(fileData, success, error) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function exportTeam(success, error) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/v1/teams/export_team',
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
success: success,
|
||||||
|
error: function onError(xhr, status, err) {
|
||||||
|
var e = handleError('exportTeam', xhr, status, err);
|
||||||
|
error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getStatuses(success, error) {
|
export function getStatuses(success, error) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/v1/users/status',
|
url: '/api/v1/users/status',
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user