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 Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
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
|
|
}
|