MM-60115, MM-62436, MM-62493: Compliance export downloads from local and s3; e2e tests (#29806)

* add ZipReader method to filestore and file/s3 backends

to merge

to merge

* add WriteStreamResponse as an alternative to WriteFileResponse

* add generated layers

* enable download link; download job endpoint now streams export dir zips

* fix MM-62493

* re-enable e2e tests--we have download links, folks

* Add tests for ZipReader in filestore and s3store

* remove unnecessary error return on ZipReader

* little cleanup

* improve tests; some refactoring: s.Nil(err) -> s.NoError(err)

* blank commit

* backwards compatability for pre-10.5 job downloads

* compress file response; better errors; better comments; PR comments

* update generated app layers

* improve/widen tests; improve comments; simplify localstore ZipReader

* regenerate layers

* follow GoDoc conventions

* update generated layers

* remove unnecessary comment

* in jobs/job-id/download, clean exportDir before sending to ZipReader

* better comments; add an error return on ZipReader

* improve file permissions

* adjust tests for new error returns

* linting

* i18n
Этот коммит содержится в:
Christopher Poile
2025-01-26 22:58:07 -05:00
коммит произвёл GitHub
родитель 396ee06dcb
Коммит 737bed311c
21 изменённых файлов: 753 добавлений и 110 удалений

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

@@ -54,10 +54,6 @@ type AppIface interface {
AddPublicKey(name string, key io.Reader) *model.AppError
// AddUserToChannel adds a user to a given channel.
AddUserToChannel(c request.CTX, user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError)
// Caller must close the first return value
ExportFileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
// Caller must close the first return value
FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
// ChannelMembersMinusGroupMembers returns the set of users in the given channel minus the set of users in the given
// groups.
//
@@ -105,8 +101,6 @@ type AppIface interface {
// CreateUser creates a user and sets several fields of the returned User struct to
// their zero values.
CreateUser(c request.CTX, user *model.User) (*model.User, *model.AppError)
// Creates and stores FileInfos for a post created before the FileInfos table existed.
MigrateFilenamesToFileInfos(rctx request.CTX, post *model.Post) []*model.FileInfo
// DefaultChannelNames returns the list of system-wide default channel names.
//
// By default the list will be (not necessarily in this order):
@@ -153,10 +147,18 @@ type AppIface interface {
// attributes of the attachment structure. The Slack attachment structure is
// documented here: https://api.slack.com/docs/attachments
ProcessSlackAttachments(attachments []*model.SlackAttachment) []*model.SlackAttachment
// ExportFileReader returns a ReadCloseSeeker for path from the ExportFileBackend.
//
// The caller is responsible for closing the returned ReadCloseSeeker.
ExportFileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
// ExtendSessionExpiryIfNeeded extends Session.ExpiresAt based on session lengths in config.
// A new ExpiresAt is only written if enough time has elapsed since last update.
// Returns true only if the session was extended.
ExtendSessionExpiryIfNeeded(rctx request.CTX, session *model.Session) bool
// FileReader returns a ReadCloseSeeker for path from the FileBackend.
//
// The caller is responsible for closing the returned ReadCloseSeeker.
FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
// FillInPostProps should be invoked before saving posts to fill in properties such as
// channel_mentions.
//
@@ -270,6 +272,8 @@ type AppIface interface {
// MentionsToTeamMembers returns all the @ mentions found in message that
// belong to users in the specified team, linking them to their users
MentionsToTeamMembers(c request.CTX, message, teamID string) model.UserMentionMap
// MigrateFilenamesToFileInfos creates and stores FileInfos for a post created before the FileInfos table existed.
MigrateFilenamesToFileInfos(rctx request.CTX, post *model.Post) []*model.FileInfo
// MoveChannel method is prone to data races if someone joins to channel during the move process. However this
// function is only exposed to sysadmins and the possibility of this edge case is relatively small.
MoveChannel(c request.CTX, team *model.Team, channel *model.Channel, user *model.User) *model.AppError
@@ -422,6 +426,10 @@ type AppIface interface {
ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string
// VerifyPlugin checks that the given signature corresponds to the given plugin and matches a trusted certificate.
VerifyPlugin(plugin, signature io.ReadSeeker) *model.AppError
// ZipReader returns a ReadCloser for path. If deflate is true, the zip will use compression.
//
// The caller is responsible for closing the returned ReadCloser.
ZipReader(path string, deflate bool) (io.ReadCloser, *model.AppError)
// validateMoveOrCopy performs validation on a provided post list to determine
// if all permissions are in place to allow the for the posts to be moved or
// copied.

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

@@ -124,20 +124,43 @@ func fileReader(backend filestore.FileBackend, path string) (filestore.ReadClose
return result, nil
}
func zipReader(backend filestore.FileBackend, path string, deflate bool) (io.ReadCloser, *model.AppError) {
result, err := backend.ZipReader(path, deflate)
if err != nil {
return nil, model.NewAppError("ZipReader", "api.file.zip_file_reader.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return result, nil
}
func (s *Server) fileReader(path string) (filestore.ReadCloseSeeker, *model.AppError) {
return fileReader(s.FileBackend(), path)
}
func (s *Server) zipReader(path string, deflate bool) (io.ReadCloser, *model.AppError) {
return zipReader(s.FileBackend(), path, deflate)
}
func (s *Server) exportFileReader(path string) (filestore.ReadCloseSeeker, *model.AppError) {
return fileReader(s.ExportFileBackend(), path)
}
// Caller must close the first return value
// FileReader returns a ReadCloseSeeker for path from the FileBackend.
//
// The caller is responsible for closing the returned ReadCloseSeeker.
func (a *App) FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError) {
return a.Srv().fileReader(path)
}
// Caller must close the first return value
// ZipReader returns a ReadCloser for path. If deflate is true, the zip will use compression.
//
// The caller is responsible for closing the returned ReadCloser.
func (a *App) ZipReader(path string, deflate bool) (io.ReadCloser, *model.AppError) {
return a.Srv().zipReader(path, deflate)
}
// ExportFileReader returns a ReadCloseSeeker for path from the ExportFileBackend.
//
// The caller is responsible for closing the returned ReadCloseSeeker.
func (a *App) ExportFileReader(path string) (filestore.ReadCloseSeeker, *model.AppError) {
return a.Srv().exportFileReader(path)
}
@@ -424,7 +447,7 @@ func parseOldFilenames(rctx request.CTX, filenames []string, channelID, userID s
return parsed
}
// Creates and stores FileInfos for a post created before the FileInfos table existed.
// MigrateFilenamesToFileInfos creates and stores FileInfos for a post created before the FileInfos table existed.
func (a *App) MigrateFilenamesToFileInfos(rctx request.CTX, post *model.Post) []*model.FileInfo {
if len(post.Filenames) == 0 {
rctx.Logger().Warn("Unable to migrate post to use FileInfos with an empty Filenames field", mlog.String("post_id", post.Id))

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

@@ -20061,6 +20061,28 @@ func (a *OpenTracingAppLayer) WriteFileContext(ctx context.Context, fr io.Reader
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ZipReader(path string, deflate bool) (io.ReadCloser, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ZipReader")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ZipReader(path, deflate)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func NewOpenTracingAppLayer(childApp app.AppIface, ctx context.Context) *OpenTracingAppLayer {
newApp := OpenTracingAppLayer{
app: childApp,