mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-19 02:06:37 +08:00
* Use multi-level logging for shared channel and remote cluster service errors Service-specific log levels (LvlRemoteClusterServiceError, LvlSharedChannelServiceError) were hidden from the main log by default, requiring explicit configuration to see them. Switch all call sites to use LogM with multi-level combos so each log line is attributed to both the standard level (error/warn) and the service-specific level. This surfaces errors in the main log while preserving the ability to isolate them into dedicated files. Each instance was reviewed and either kept as error (DB failures, security issues, config errors, exhausted retries on critical data) or downgraded to warn (transient network failures, remote-side reported errors with retry logic, non-critical data like profile images, reactions, acknowledgements, and status).
211 lines
7.1 KiB
Go
211 lines
7.1 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package sharedchannel
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/platform/services/remotecluster"
|
|
)
|
|
|
|
// postsToAttachments returns the file attachments for a slice of posts that need to be synchronized.
|
|
func (scs *Service) shouldSyncAttachment(fi *model.FileInfo, rc *model.RemoteCluster) bool {
|
|
sca, err := scs.server.GetStore().SharedChannel().GetAttachment(fi.Id, rc.RemoteId)
|
|
if err != nil {
|
|
if _, ok := err.(errNotFound); !ok {
|
|
scs.server.Log().LogM(mlog.MlvlSharedChannelServiceError, "error fetching shared channel attachment",
|
|
mlog.String("file_id", fi.Id),
|
|
mlog.String("remote_id", rc.RemoteId),
|
|
mlog.Err(err),
|
|
)
|
|
}
|
|
// no record so sync is needed
|
|
return true
|
|
}
|
|
|
|
return sca.LastSyncAt < fi.UpdateAt
|
|
}
|
|
|
|
// sendAttachmentForRemote asynchronously sends a file attachment to a remote cluster.
|
|
func (scs *Service) sendAttachmentForRemote(fi *model.FileInfo, post *model.Post, rc *model.RemoteCluster) error {
|
|
rcs := scs.server.GetRemoteClusterService()
|
|
if rcs == nil {
|
|
return fmt.Errorf("cannot update remote cluster for remote id %s; Remote Cluster Service not enabled", rc.RemoteId)
|
|
}
|
|
|
|
if rc.IsPlugin() {
|
|
return scs.sendAttachmentToPlugin(fi, post, rc)
|
|
}
|
|
|
|
us := &model.UploadSession{
|
|
Id: model.NewId(),
|
|
Type: model.UploadTypeAttachment,
|
|
UserId: post.UserId,
|
|
ChannelId: post.ChannelId,
|
|
Filename: fi.Name,
|
|
FileSize: fi.Size,
|
|
RemoteId: rc.RemoteId,
|
|
ReqFileId: fi.Id,
|
|
}
|
|
|
|
payload, err := json.Marshal(us)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
msg := model.NewRemoteClusterMsg(TopicUploadCreate, payload)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), remotecluster.SendTimeout)
|
|
defer cancel()
|
|
|
|
var usResp model.UploadSession
|
|
var respErr error
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
// creating the upload session on the remote server needs to be done synchronously.
|
|
err = rcs.SendMsg(ctx, msg, rc, func(msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *remotecluster.Response, err error) {
|
|
defer wg.Done()
|
|
if err != nil {
|
|
respErr = err
|
|
return
|
|
}
|
|
if !resp.IsSuccess() {
|
|
respErr = errors.New(resp.Err)
|
|
return
|
|
}
|
|
respErr = json.Unmarshal(resp.Payload, &usResp)
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("error sending create upload session to remote %s for post %s: %w", rc.RemoteId, post.Id, err)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
if respErr != nil {
|
|
return fmt.Errorf("invalid create upload session response for remote %s and post %s: %w", rc.RemoteId, post.Id, respErr)
|
|
}
|
|
|
|
ctx2, cancel2 := context.WithTimeout(context.Background(), remotecluster.SendFileTimeout)
|
|
defer cancel2()
|
|
|
|
return rcs.SendFile(ctx2, &usResp, fi, rc, scs.app, func(us *model.UploadSession, rc *model.RemoteCluster, resp *remotecluster.Response, err error) {
|
|
if err != nil {
|
|
return // this means the response could not be parsed; already logged
|
|
}
|
|
|
|
if !resp.IsSuccess() {
|
|
scs.server.Log().LogM(mlog.MlvlSharedChannelServiceError, "send file failed",
|
|
mlog.String("remote", rc.DisplayName),
|
|
mlog.String("uploadId", usResp.Id),
|
|
mlog.String("err", resp.Err),
|
|
)
|
|
return
|
|
}
|
|
|
|
// response payload should be a model.FileInfo.
|
|
var fi model.FileInfo
|
|
if err2 := json.Unmarshal(resp.Payload, &fi); err2 != nil {
|
|
scs.server.Log().LogM(mlog.MlvlSharedChannelServiceWarn, "invalid file info response after send file",
|
|
mlog.String("remote", rc.DisplayName),
|
|
mlog.String("uploadId", usResp.Id),
|
|
mlog.Err(err2),
|
|
)
|
|
return
|
|
}
|
|
|
|
// save file attachment record in SharedChannelAttachments table
|
|
if err2 := scs.saveSharedAttachment(&fi, rc); err2 != nil {
|
|
scs.server.Log().LogM(mlog.MlvlSharedChannelServiceError, "error saving SharedChannelAttachment",
|
|
mlog.String("remote", rc.DisplayName),
|
|
mlog.String("uploadId", usResp.Id),
|
|
mlog.Err(err2),
|
|
)
|
|
return
|
|
}
|
|
|
|
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "send file successful",
|
|
mlog.String("remote", rc.DisplayName),
|
|
mlog.String("uploadId", usResp.Id),
|
|
)
|
|
})
|
|
}
|
|
|
|
// sendAttachmentToPlugin asynchronously sends a file attachment to a remote cluster.
|
|
func (scs *Service) sendAttachmentToPlugin(fi *model.FileInfo, post *model.Post, rc *model.RemoteCluster) error {
|
|
if err := scs.app.OnSharedChannelsAttachmentSyncMsg(fi, post, rc); err != nil {
|
|
return fmt.Errorf("cannot send attachment to plugin %s: %w", rc.PluginID, err)
|
|
}
|
|
return scs.saveSharedAttachment(fi, rc)
|
|
}
|
|
|
|
// saveSharedAttachment saves the attachment in SharedChannelAttachments table.
|
|
func (scs *Service) saveSharedAttachment(fi *model.FileInfo, rc *model.RemoteCluster) error {
|
|
sca := &model.SharedChannelAttachment{
|
|
FileId: fi.Id,
|
|
RemoteId: rc.RemoteId,
|
|
}
|
|
_, err := scs.server.GetStore().SharedChannel().UpsertAttachment(sca)
|
|
return err
|
|
}
|
|
|
|
// onReceiveUploadCreate is called when a message requesting to create an upload session is received. An upload session is
|
|
// created and the id returned in the response.
|
|
func (scs *Service) onReceiveUploadCreate(msg model.RemoteClusterMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
|
|
var us model.UploadSession
|
|
|
|
if err := json.Unmarshal(msg.Payload, &us); err != nil {
|
|
return fmt.Errorf("invalid upload session request: %w", err)
|
|
}
|
|
|
|
// make sure channel is shared for the remote sender
|
|
hasRemote, err := scs.server.GetStore().SharedChannel().HasRemote(us.ChannelId, rc.RemoteId)
|
|
if err != nil {
|
|
return fmt.Errorf("could not validate upload session for remote: %w", err)
|
|
}
|
|
if !hasRemote {
|
|
return model.NewAppError("createUpload", "api.upload.create.upload_channel_not_shared_with_remote.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
// make sure file attachments are enabled
|
|
if scs.server.Config().FileSettings.EnableFileAttachments == nil || !*scs.server.Config().FileSettings.EnableFileAttachments {
|
|
return model.NewAppError("ReceiveUploadCreate",
|
|
"api.file.attachments.disabled.app_error",
|
|
nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
// make sure the file size requested does not exceed local server config - MM server's regular
|
|
// upload code will ensure the actual bytes sent are within the upload session limit.
|
|
if scs.server.Config().FileSettings.MaxFileSize == nil || us.FileSize > *scs.server.Config().FileSettings.MaxFileSize {
|
|
return model.NewAppError("createUpload", "api.upload.create.upload_too_large.app_error",
|
|
map[string]any{"channelId": us.ChannelId}, "", http.StatusRequestEntityTooLarge)
|
|
}
|
|
|
|
// validate upload type for shared channels - only allow attachments
|
|
if us.Type != model.UploadTypeAttachment {
|
|
return model.NewAppError("onReceiveUploadCreate", "api.upload.invalid_type_for_shared_channel.app_error",
|
|
nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
us.RemoteId = rc.RemoteId // don't let remotes try to impersonate each other
|
|
|
|
// create upload session.
|
|
usSaved, appErr := scs.app.CreateUploadSession(request.EmptyContext(scs.server.Log()), &us)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
response.SetPayload(usSaved)
|
|
return nil
|
|
}
|