snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
259
server/public/model/file_info.go
Normal file
259
server/public/model/file_info.go
Normal file
@@ -0,0 +1,259 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
const (
|
||||
FileinfoSortByCreated = "CreateAt"
|
||||
FileinfoSortBySize = "Size"
|
||||
|
||||
// MaxFilenameLength is the maximum length, in Unicode codepoints, of a
|
||||
// sanitized FileInfo.Name. It matches the VARCHAR(256) width of the
|
||||
// fileinfo.name column.
|
||||
MaxFilenameLength = 256
|
||||
)
|
||||
|
||||
// FileDownloadType represents the type of file download or access being performed.
|
||||
type FileDownloadType string
|
||||
|
||||
const (
|
||||
// FileDownloadTypeFile represents a full file download request.
|
||||
FileDownloadTypeFile FileDownloadType = "file"
|
||||
// FileDownloadTypeThumbnail represents a thumbnail image request.
|
||||
FileDownloadTypeThumbnail FileDownloadType = "thumbnail"
|
||||
// FileDownloadTypePreview represents a preview image request.
|
||||
FileDownloadTypePreview FileDownloadType = "preview"
|
||||
// FileDownloadTypePublic represents a public link access (unauthenticated).
|
||||
FileDownloadTypePublic FileDownloadType = "public"
|
||||
)
|
||||
|
||||
// GetFileInfosOptions contains options for getting FileInfos
|
||||
type GetFileInfosOptions struct {
|
||||
// UserIds optionally limits the FileInfos to those created by the given users.
|
||||
UserIds []string `json:"user_ids"`
|
||||
// ChannelIds optionally limits the FileInfos to those created in the given channels.
|
||||
ChannelIds []string `json:"channel_ids"`
|
||||
// Since optionally limits FileInfos to those created at or after the given time, specified as Unix time in milliseconds.
|
||||
Since int64 `json:"since"`
|
||||
// IncludeDeleted if set includes deleted FileInfos.
|
||||
IncludeDeleted bool `json:"include_deleted"`
|
||||
// SortBy sorts the FileInfos by this field. The default is to sort by date created.
|
||||
SortBy string `json:"sort_by"`
|
||||
// SortDescending changes the sort direction to descending order when true.
|
||||
SortDescending bool `json:"sort_descending"`
|
||||
// OnlyEmptyContent limits results to files that have no extracted content stored.
|
||||
OnlyEmptyContent bool `json:"only_empty_content"`
|
||||
}
|
||||
|
||||
type FileInfo struct {
|
||||
Id string `json:"id" xml:"Id"`
|
||||
CreatorId string `json:"user_id" xml:"CreatorId"`
|
||||
PostId string `json:"post_id,omitempty" xml:"PostId,omitempty"`
|
||||
// ChannelId is the denormalized value from the corresponding post. Note that this value is
|
||||
// potentially distinct from the ChannelId provided when the file is first uploaded and
|
||||
// used to organize the directories in the file store, since in theory that same file
|
||||
// could be attached to a post from a different channel (or not attached to a post at all).
|
||||
ChannelId string `json:"channel_id" xml:"ChannelId"`
|
||||
CreateAt int64 `json:"create_at" xml:"CreateAt"`
|
||||
UpdateAt int64 `json:"update_at" xml:"UpdateAt"`
|
||||
DeleteAt int64 `json:"delete_at" xml:"DeleteAt"`
|
||||
Path string `json:"-" xml:"-"` // not sent back to the client
|
||||
ThumbnailPath string `json:"-" xml:"-"` // not sent back to the client
|
||||
PreviewPath string `json:"-" xml:"-"` // not sent back to the client
|
||||
Name string `json:"name" xml:"Name"`
|
||||
Extension string `json:"extension" xml:"Extension"`
|
||||
Size int64 `json:"size" xml:"Size"`
|
||||
MimeType string `json:"mime_type" xml:"MimeType"`
|
||||
Width int `json:"width,omitempty" xml:"Width,omitempty"`
|
||||
Height int `json:"height,omitempty" xml:"Height,omitempty"`
|
||||
HasPreviewImage bool `json:"has_preview_image,omitempty" xml:"HasPreviewImage,omitempty"`
|
||||
MiniPreview *[]byte `json:"mini_preview" xml:"-"` // pointer to distinguish NULL (no preview) from empty data
|
||||
Content string `json:"-" xml:"-"`
|
||||
RemoteId *string `json:"remote_id" xml:"RemoteId"`
|
||||
Archived bool `json:"archived" xml:"Archived"`
|
||||
}
|
||||
|
||||
func (fi *FileInfo) Auditable() map[string]any {
|
||||
return map[string]any{
|
||||
"id": fi.Id,
|
||||
"creator_id": fi.CreatorId,
|
||||
"post_id": fi.PostId,
|
||||
"channel_id": fi.ChannelId,
|
||||
"create_at": fi.CreateAt,
|
||||
"update_at": fi.UpdateAt,
|
||||
"delete_at": fi.DeleteAt,
|
||||
"name": fi.Name,
|
||||
"extension": fi.Extension,
|
||||
"size": fi.Size,
|
||||
}
|
||||
}
|
||||
|
||||
func (fi *FileInfo) PreSave() {
|
||||
if fi.Id == "" {
|
||||
fi.Id = NewId()
|
||||
}
|
||||
|
||||
if fi.CreateAt == 0 {
|
||||
fi.CreateAt = GetMillis()
|
||||
}
|
||||
|
||||
if fi.UpdateAt < fi.CreateAt {
|
||||
fi.UpdateAt = fi.CreateAt
|
||||
}
|
||||
|
||||
if fi.RemoteId == nil {
|
||||
fi.RemoteId = new("")
|
||||
}
|
||||
}
|
||||
|
||||
func (fi *FileInfo) IsValid() *AppError {
|
||||
if !IsValidId(fi.Id) {
|
||||
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if !IsValidId(fi.CreatorId) && (fi.CreatorId != "nouser" && fi.CreatorId != BookmarkFileOwner) {
|
||||
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.user_id.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if fi.PostId != "" && !IsValidId(fi.PostId) {
|
||||
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.post_id.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if fi.CreateAt == 0 {
|
||||
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.create_at.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if fi.UpdateAt == 0 {
|
||||
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.update_at.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if fi.Path == "" {
|
||||
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.path.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if fi.Name != "" && !IsValidFilename(fi.Name) {
|
||||
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.name.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidFilename reports whether name is acceptable as FileInfo.Name.
|
||||
// It rejects empty strings, bare "." and "..", names exceeding
|
||||
// MaxFilenameLength, path separators, and ASCII control characters.
|
||||
// The input is not mutated; see SanitizeFilename for the mutating form.
|
||||
func IsValidFilename(name string) bool {
|
||||
if name == "" || name == "." || name == ".." {
|
||||
return false
|
||||
}
|
||||
if utf8.RuneCountInString(name) > MaxFilenameLength {
|
||||
return false
|
||||
}
|
||||
if strings.ContainsAny(name, `/\`) {
|
||||
return false
|
||||
}
|
||||
return !strings.ContainsFunc(name, func(r rune) bool {
|
||||
return r < 0x20 || r == 0x7f
|
||||
})
|
||||
}
|
||||
|
||||
// SanitizeFilename returns a canonical form of name suitable for
|
||||
// FileInfo.Name. It NFC-normalizes Unicode, removes ASCII control
|
||||
// characters, collapses backslashes to forward slashes, reduces the
|
||||
// value to its final path element via filepath.Base, and truncates
|
||||
// to MaxFilenameLength codepoints to match the DB column width.
|
||||
//
|
||||
// Returns an empty string when nothing usable remains (for example
|
||||
// when the input was "", ".", "..", "/", or entirely control
|
||||
// characters); callers should treat an empty result as a failure.
|
||||
func SanitizeFilename(name string) string {
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
name = norm.NFC.String(name)
|
||||
name = strings.Map(func(r rune) rune {
|
||||
if r < 0x20 || r == 0x7f {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, name)
|
||||
name = strings.ReplaceAll(name, `\`, "/")
|
||||
name = filepath.Base(name)
|
||||
|
||||
if name == "." || name == ".." || name == string(filepath.Separator) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if runes := []rune(name); len(runes) > MaxFilenameLength {
|
||||
name = string(runes[:MaxFilenameLength])
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func (fi *FileInfo) IsImage() bool {
|
||||
return strings.HasPrefix(fi.MimeType, "image")
|
||||
}
|
||||
|
||||
func (fi *FileInfo) IsSvg() bool {
|
||||
return fi.MimeType == "image/svg+xml"
|
||||
}
|
||||
|
||||
func NewInfo(name string) *FileInfo {
|
||||
info := &FileInfo{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (fi *FileInfo) MakeContentInaccessible() {
|
||||
if fi == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fi.Archived = true
|
||||
fi.Content = ""
|
||||
fi.HasPreviewImage = false
|
||||
fi.MiniPreview = nil
|
||||
fi.Path = ""
|
||||
fi.PreviewPath = ""
|
||||
fi.ThumbnailPath = ""
|
||||
}
|
||||
Reference in New Issue
Block a user