Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
348 lines
9.5 KiB
Go
348 lines
9.5 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package filestore
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
)
|
|
|
|
const (
|
|
TestFilePath = "/testfile"
|
|
MaxRecursionDepth = 50
|
|
)
|
|
|
|
type LocalFileBackend struct {
|
|
directory string
|
|
}
|
|
|
|
// copyFile will copy a file from src path to dst path.
|
|
// Overwrites any existing files at dst.
|
|
// Permissions are copied from file at src to the new file at dst.
|
|
func copyFile(src, dst string) (err error) {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer in.Close()
|
|
|
|
if err = os.MkdirAll(filepath.Dir(dst), os.ModePerm); err != nil {
|
|
return
|
|
}
|
|
out, err := os.Create(dst)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() {
|
|
if e := out.Close(); e != nil {
|
|
err = e
|
|
}
|
|
}()
|
|
|
|
_, err = io.Copy(out, in)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = out.Sync()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
stat, err := os.Stat(src)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = os.Chmod(dst, stat.Mode())
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (b *LocalFileBackend) DriverName() string {
|
|
return driverLocal
|
|
}
|
|
|
|
func (b *LocalFileBackend) TestConnection() error {
|
|
f := bytes.NewReader([]byte("testingwrite"))
|
|
if _, err := writeFileLocally(f, filepath.Join(b.directory, TestFilePath)); err != nil {
|
|
return errors.Wrap(err, "unable to write to the local filesystem storage")
|
|
}
|
|
os.Remove(filepath.Join(b.directory, TestFilePath))
|
|
mlog.Debug("Able to write files to local storage.")
|
|
return nil
|
|
}
|
|
|
|
func (b *LocalFileBackend) Reader(path string) (ReadCloseSeeker, error) {
|
|
f, err := os.Open(filepath.Join(b.directory, path))
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "unable to open file %s", path)
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func (b *LocalFileBackend) ReadFile(path string) ([]byte, error) {
|
|
f, err := os.ReadFile(filepath.Join(b.directory, path))
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "unable to read file %s", path)
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func (b *LocalFileBackend) FileExists(path string) (bool, error) {
|
|
_, err := os.Stat(filepath.Join(b.directory, path))
|
|
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
|
|
if err != nil {
|
|
return false, errors.Wrapf(err, "unable to know if file %s exists", path)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (b *LocalFileBackend) FileSize(path string) (int64, error) {
|
|
info, err := os.Stat(filepath.Join(b.directory, path))
|
|
if err != nil {
|
|
return 0, errors.Wrapf(err, "unable to get file size for %s", path)
|
|
}
|
|
return info.Size(), nil
|
|
}
|
|
|
|
func (b *LocalFileBackend) FileModTime(path string) (time.Time, error) {
|
|
info, err := os.Stat(filepath.Join(b.directory, path))
|
|
if err != nil {
|
|
return time.Time{}, errors.Wrapf(err, "unable to get modification time for file %s", path)
|
|
}
|
|
return info.ModTime(), nil
|
|
}
|
|
|
|
func (b *LocalFileBackend) CopyFile(oldPath, newPath string) error {
|
|
if err := copyFile(filepath.Join(b.directory, oldPath), filepath.Join(b.directory, newPath)); err != nil {
|
|
return errors.Wrapf(err, "unable to copy file from %s to %s", oldPath, newPath)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *LocalFileBackend) MoveFile(oldPath, newPath string) error {
|
|
if err := os.MkdirAll(filepath.Dir(filepath.Join(b.directory, newPath)), 0750); err != nil {
|
|
return errors.Wrapf(err, "unable to create the new destination directory %s", filepath.Dir(newPath))
|
|
}
|
|
|
|
if err := os.Rename(filepath.Join(b.directory, oldPath), filepath.Join(b.directory, newPath)); err != nil {
|
|
return errors.Wrapf(err, "unable to move the file to %s to the destination directory", newPath)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *LocalFileBackend) WriteFile(fr io.Reader, path string) (int64, error) {
|
|
return writeFileLocally(fr, filepath.Join(b.directory, path))
|
|
}
|
|
|
|
func writeFileLocally(fr io.Reader, path string) (int64, error) {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil {
|
|
directory, _ := filepath.Abs(filepath.Dir(path))
|
|
return 0, errors.Wrapf(err, "unable to create the directory %s for the file %s", directory, path)
|
|
}
|
|
fw, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return 0, errors.Wrapf(err, "unable to open the file %s to write the data", path)
|
|
}
|
|
defer fw.Close()
|
|
written, err := io.Copy(fw, fr)
|
|
if err != nil {
|
|
return written, errors.Wrapf(err, "unable write the data in the file %s", path)
|
|
}
|
|
return written, nil
|
|
}
|
|
|
|
func (b *LocalFileBackend) AppendFile(fr io.Reader, path string) (int64, error) {
|
|
fp := filepath.Join(b.directory, path)
|
|
if _, err := os.Stat(fp); err != nil {
|
|
return 0, errors.Wrapf(err, "unable to find the file %s to append the data", path)
|
|
}
|
|
fw, err := os.OpenFile(fp, os.O_WRONLY|os.O_APPEND, 0600)
|
|
if err != nil {
|
|
return 0, errors.Wrapf(err, "unable to open the file %s to append the data", path)
|
|
}
|
|
defer fw.Close()
|
|
written, err := io.Copy(fw, fr)
|
|
if err != nil {
|
|
return written, errors.Wrapf(err, "unable append the data in the file %s", path)
|
|
}
|
|
return written, nil
|
|
}
|
|
|
|
func (b *LocalFileBackend) RemoveFile(path string) error {
|
|
if err := os.Remove(filepath.Join(b.directory, path)); err != nil {
|
|
return errors.Wrapf(err, "unable to remove the file %s", path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// basePath: path to get to the file but won't be added to the end result
|
|
// path: basePath+path current directory we are looking at
|
|
// maxDepth: parameter to prevent infinite recursion, once this is reached we won't look any further
|
|
func appendRecursively(basePath, path string, maxDepth int) ([]string, error) {
|
|
results := []string{}
|
|
dirEntries, err := os.ReadDir(filepath.Join(basePath, path))
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return results, nil
|
|
}
|
|
return results, errors.Wrapf(err, "unable to list the directory %s", path)
|
|
}
|
|
for _, dirEntry := range dirEntries {
|
|
entryName := dirEntry.Name()
|
|
entryPath := filepath.Join(path, entryName)
|
|
if entryName == "." || entryName == ".." || entryPath == path {
|
|
continue
|
|
}
|
|
if dirEntry.IsDir() {
|
|
if maxDepth <= 0 {
|
|
mlog.Warn("Max depth reached, skipping any further directories", mlog.Int("depth", maxDepth), mlog.String("path", entryPath))
|
|
results = append(results, entryPath)
|
|
continue // we'll ignore it if max depth is reached.
|
|
}
|
|
nestedResults, err := appendRecursively(basePath, entryPath, maxDepth-1)
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
results = append(results, nestedResults...)
|
|
} else {
|
|
results = append(results, entryPath)
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func (b *LocalFileBackend) ListDirectory(path string) ([]string, error) {
|
|
results := []string{}
|
|
dirEntries, err := os.ReadDir(filepath.Join(b.directory, path))
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// ideally os.ErrNotExist should've been returned but to keep the
|
|
// consistency, leaving it as is before.
|
|
return results, nil
|
|
}
|
|
// same here, ideally we shouldn't return the empty slice
|
|
return results, errors.Wrapf(err, "unable to list the directory %s", path)
|
|
}
|
|
for _, dirEntry := range dirEntries {
|
|
results = append(results, filepath.Join(path, dirEntry.Name()))
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func (b *LocalFileBackend) ListDirectoryRecursively(path string) ([]string, error) {
|
|
return appendRecursively(b.directory, path, MaxRecursionDepth)
|
|
}
|
|
|
|
func (b *LocalFileBackend) RemoveDirectory(path string) error {
|
|
if err := os.RemoveAll(filepath.Join(b.directory, path)); err != nil {
|
|
return errors.Wrapf(err, "unable to remove the directory %s", path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ZipReader will create a zip of path. If path is a single file, it will zip the single file.
|
|
// If deflate is true, the contents will be compressed. It will stream the zip to io.ReadCloser.
|
|
func (b *LocalFileBackend) ZipReader(path string, deflate bool) (io.ReadCloser, error) {
|
|
deflateMethod := zip.Store
|
|
if deflate {
|
|
deflateMethod = zip.Deflate
|
|
}
|
|
|
|
fullPath := filepath.Join(b.directory, path)
|
|
baseInfo, err := os.Stat(fullPath)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "unable to stat path %s", path)
|
|
}
|
|
|
|
pr, pw := io.Pipe()
|
|
|
|
go func() {
|
|
defer pw.Close()
|
|
|
|
zipWriter := zip.NewWriter(pw)
|
|
defer zipWriter.Close()
|
|
|
|
err = filepath.Walk(fullPath, func(filePath string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Handle single file case
|
|
baseDir := fullPath
|
|
if !baseInfo.IsDir() {
|
|
baseDir = filepath.Dir(baseDir)
|
|
}
|
|
|
|
// Get the relative path from the base directory
|
|
relPath, err := filepath.Rel(baseDir, filePath)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "unable to get relative path for %s", filePath)
|
|
}
|
|
|
|
// Skip the root directory itself
|
|
if relPath == "." {
|
|
return nil
|
|
}
|
|
|
|
// Create zip header
|
|
header, err := zip.FileInfoHeader(info)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "unable to create zip header for %s", relPath)
|
|
}
|
|
|
|
// Ensure consistent forward slashes in paths
|
|
header.Name = filepath.ToSlash(relPath)
|
|
|
|
// Skip directories - we don't need to create entries for them
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
// Create file entry
|
|
header.Method = deflateMethod
|
|
header.SetMode(0644) // rw-r--r-- permissions
|
|
writer, err := zipWriter.CreateHeader(header)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "unable to create zip entry for %s", relPath)
|
|
}
|
|
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "unable to open file %s", filePath)
|
|
}
|
|
defer file.Close()
|
|
|
|
if _, err := io.Copy(writer, file); err != nil {
|
|
return errors.Wrapf(err, "unable to copy file content for %s", relPath)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
pw.CloseWithError(errors.Wrap(err, "error walking directory"))
|
|
}
|
|
}()
|
|
|
|
return pr, nil
|
|
}
|