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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
396ee06dcb
Коммит
737bed311c
@@ -40,6 +40,7 @@ type FileBackend interface {
|
||||
ListDirectory(path string) ([]string, error)
|
||||
ListDirectoryRecursively(path string) ([]string, error)
|
||||
RemoveDirectory(path string) error
|
||||
ZipReader(path string, deflate bool) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
type FileBackendWithLinkGenerator interface {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package filestore
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
@@ -11,15 +12,17 @@ import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/xtgo/uuid"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
@@ -114,12 +117,12 @@ func (s *FileBackendTestSuite) TestReadWriteFile() {
|
||||
path := "tests/" + randomString()
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(b), path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
defer s.backend.RemoveFile(path)
|
||||
|
||||
read, err := s.backend.ReadFile(path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
|
||||
readString := string(read)
|
||||
s.EqualValues(readString, "test")
|
||||
@@ -215,12 +218,12 @@ func (s *FileBackendTestSuite) TestReadWriteFileImage() {
|
||||
path := "tests/" + randomString() + ".png"
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(b), path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
defer s.backend.RemoveFile(path)
|
||||
|
||||
read, err := s.backend.ReadFile(path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
|
||||
readString := string(read)
|
||||
s.EqualValues(readString, "testimage")
|
||||
@@ -231,15 +234,15 @@ func (s *FileBackendTestSuite) TestFileExists() {
|
||||
path := "tests/" + randomString() + ".png"
|
||||
|
||||
_, err := s.backend.WriteFile(bytes.NewReader(b), path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
defer s.backend.RemoveFile(path)
|
||||
|
||||
res, err := s.backend.FileExists(path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.True(res)
|
||||
|
||||
res, err = s.backend.FileExists("tests/idontexist.png")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.False(res)
|
||||
}
|
||||
|
||||
@@ -249,19 +252,19 @@ func (s *FileBackendTestSuite) TestCopyFile() {
|
||||
path2 := "tests/" + randomString()
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(b), path1)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
defer s.backend.RemoveFile(path1)
|
||||
|
||||
err = s.backend.CopyFile(path1, path2)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
defer s.backend.RemoveFile(path2)
|
||||
|
||||
data1, err := s.backend.ReadFile(path1)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
|
||||
data2, err := s.backend.ReadFile(path2)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
|
||||
s.Equal(b, data1)
|
||||
s.Equal(b, data2)
|
||||
@@ -273,19 +276,19 @@ func (s *FileBackendTestSuite) TestCopyFileToDirectoryThatDoesntExist() {
|
||||
path2 := "tests/newdirectory/" + randomString()
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(b), path1)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
defer s.backend.RemoveFile(path1)
|
||||
|
||||
err = s.backend.CopyFile(path1, path2)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
defer s.backend.RemoveFile(path2)
|
||||
|
||||
_, err = s.backend.ReadFile(path1)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
|
||||
_, err = s.backend.ReadFile(path2)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func (s *FileBackendTestSuite) TestMoveFile() {
|
||||
@@ -294,7 +297,7 @@ func (s *FileBackendTestSuite) TestMoveFile() {
|
||||
path2 := "tests/" + randomString()
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(b), path1)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
defer s.backend.RemoveFile(path1)
|
||||
|
||||
@@ -305,7 +308,7 @@ func (s *FileBackendTestSuite) TestMoveFile() {
|
||||
s.Error(err)
|
||||
|
||||
data, err := s.backend.ReadFile(path2)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
|
||||
s.Equal(b, data)
|
||||
}
|
||||
@@ -315,7 +318,7 @@ func (s *FileBackendTestSuite) TestRemoveFile() {
|
||||
path := "tests/" + randomString()
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(b), path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
s.Nil(s.backend.RemoveFile(path))
|
||||
|
||||
@@ -323,15 +326,15 @@ func (s *FileBackendTestSuite) TestRemoveFile() {
|
||||
s.Error(err)
|
||||
|
||||
written, err = s.backend.WriteFile(bytes.NewReader(b), "tests2/foo")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
|
||||
written, err = s.backend.WriteFile(bytes.NewReader(b), "tests2/bar")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
|
||||
written, err = s.backend.WriteFile(bytes.NewReader(b), "tests2/asdf")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
|
||||
s.Nil(s.backend.RemoveDirectory("tests2"))
|
||||
@@ -343,35 +346,35 @@ func (s *FileBackendTestSuite) TestListDirectory() {
|
||||
path2 := "19800101/" + randomString()
|
||||
|
||||
paths, err := s.backend.ListDirectory("19700101")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.Len(paths, 0)
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(b), path1)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
|
||||
written, err = s.backend.WriteFile(bytes.NewReader(b), path2)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
|
||||
paths, err = s.backend.ListDirectory("19700101")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.Len(paths, 1)
|
||||
s.Equal(path1, (paths)[0])
|
||||
|
||||
paths, err = s.backend.ListDirectory("19800101/")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.Len(paths, 1)
|
||||
s.Equal(path2, (paths)[0])
|
||||
|
||||
if s.settings.DriverName == driverLocal {
|
||||
paths, err = s.backend.ListDirectory("19800102")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.Len(paths, 0)
|
||||
}
|
||||
|
||||
paths, err = s.backend.ListDirectory("")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
found1 := false
|
||||
found2 := false
|
||||
for _, path := range paths {
|
||||
@@ -395,39 +398,39 @@ func (s *FileBackendTestSuite) TestListDirectoryRecursively() {
|
||||
longPath := "19800102" + strings.Repeat("/toomuch", MaxRecursionDepth+1) + randomString()
|
||||
|
||||
paths, err := s.backend.ListDirectoryRecursively("19700101")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.Len(paths, 0)
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(b), path1)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
|
||||
written, err = s.backend.WriteFile(bytes.NewReader(b), path2)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
|
||||
written, err = s.backend.WriteFile(bytes.NewReader(b), longPath)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
|
||||
paths, err = s.backend.ListDirectoryRecursively("19700101")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.Len(paths, 1)
|
||||
s.Equal(path1, (paths)[0])
|
||||
|
||||
paths, err = s.backend.ListDirectoryRecursively("19800101/")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.Len(paths, 1)
|
||||
s.Equal(path2, (paths)[0])
|
||||
|
||||
if s.settings.DriverName == driverLocal {
|
||||
paths, err = s.backend.ListDirectory("19800102")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.Len(paths, 1)
|
||||
}
|
||||
|
||||
paths, err = s.backend.ListDirectoryRecursively("")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
found1 := false
|
||||
found2 := false
|
||||
found3 := false
|
||||
@@ -455,15 +458,15 @@ func (s *FileBackendTestSuite) TestRemoveDirectory() {
|
||||
b := []byte("test")
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(b), "tests2/foo")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
|
||||
written, err = s.backend.WriteFile(bytes.NewReader(b), "tests2/bar")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
|
||||
written, err = s.backend.WriteFile(bytes.NewReader(b), "tests2/aaa")
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written, "expected given number of bytes to have been written")
|
||||
|
||||
s.Nil(s.backend.RemoveDirectory("tests2"))
|
||||
@@ -492,7 +495,7 @@ func (s *FileBackendTestSuite) TestAppendFile() {
|
||||
path := "tests/" + randomString()
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(b), path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written)
|
||||
defer s.backend.RemoveFile(path)
|
||||
|
||||
@@ -502,11 +505,11 @@ func (s *FileBackendTestSuite) TestAppendFile() {
|
||||
}
|
||||
|
||||
written, err = s.backend.AppendFile(bytes.NewReader(b2), path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(int64(len(b2)), written)
|
||||
|
||||
read, err := s.backend.ReadFile(path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b)+len(b2), len(read))
|
||||
s.True(bytes.Equal(append(b, b2...), read))
|
||||
|
||||
@@ -516,11 +519,11 @@ func (s *FileBackendTestSuite) TestAppendFile() {
|
||||
}
|
||||
|
||||
written, err = s.backend.AppendFile(bytes.NewReader(b3), path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(int64(len(b3)), written)
|
||||
|
||||
read, err = s.backend.ReadFile(path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b)+len(b2)+len(b3), len(read))
|
||||
s.True(bytes.Equal(append(append(b, b2...), b3...), read))
|
||||
})
|
||||
@@ -538,12 +541,12 @@ func (s *FileBackendTestSuite) TestFileSize() {
|
||||
path := "tests/" + randomString()
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(data), path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(data), written)
|
||||
defer s.backend.RemoveFile(path)
|
||||
|
||||
size, err := s.backend.FileSize(path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.Equal(int64(len(data)), size)
|
||||
})
|
||||
}
|
||||
@@ -560,12 +563,12 @@ func (s *FileBackendTestSuite) TestFileModTime() {
|
||||
data := []byte("some data")
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(data), path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(data), written)
|
||||
defer s.backend.RemoveFile(path)
|
||||
|
||||
modTime, err := s.backend.FileModTime(path)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.NotEmpty(modTime)
|
||||
|
||||
// We wait 1 second so that the times will differ enough to be testable.
|
||||
@@ -573,12 +576,12 @@ func (s *FileBackendTestSuite) TestFileModTime() {
|
||||
|
||||
path2 := "tests/" + randomString()
|
||||
written, err = s.backend.WriteFile(bytes.NewReader(data), path2)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(data), written)
|
||||
defer s.backend.RemoveFile(path2)
|
||||
|
||||
modTime2, err := s.backend.FileModTime(path2)
|
||||
s.Nil(err)
|
||||
s.NoError(err)
|
||||
s.NotEmpty(modTime2)
|
||||
s.True(modTime2.After(modTime))
|
||||
})
|
||||
@@ -809,3 +812,268 @@ func TestNewExportFileBackendSettingsFromConfig(t *testing.T) {
|
||||
require.Equal(t, expected, actual)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileBackendTestSuite) TestZipReaderSingleFile() {
|
||||
// Test zipping a single file (but not its neighbours)
|
||||
b := []byte("testdata")
|
||||
path := "tests/" + randomString() + ".txt"
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(b), path)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written)
|
||||
defer s.backend.RemoveFile(path)
|
||||
|
||||
// neighbour, not included
|
||||
b2 := []byte("testdata2")
|
||||
path2 := "tests/" + randomString() + ".txt"
|
||||
|
||||
written, err = s.backend.WriteFile(bytes.NewReader(b2), path2)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b2), written)
|
||||
defer s.backend.RemoveFile(path2)
|
||||
|
||||
// Test without compression
|
||||
reader, err := s.backend.ZipReader(path, false)
|
||||
s.NoError(err)
|
||||
defer reader.Close()
|
||||
|
||||
// Read the zip file
|
||||
zipBytes, err := io.ReadAll(reader)
|
||||
s.NoError(err)
|
||||
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
|
||||
s.NoError(err)
|
||||
s.Len(zipReader.File, 1)
|
||||
|
||||
// Verify file contents
|
||||
zf := zipReader.File[0]
|
||||
s.Equal(filepath.Base(path), zf.Name)
|
||||
s.Equal(zip.Store, zf.Method)
|
||||
|
||||
rc, err := zf.Open()
|
||||
s.NoError(err)
|
||||
defer rc.Close()
|
||||
|
||||
content, err := io.ReadAll(rc)
|
||||
s.NoError(err)
|
||||
s.Equal(b, content)
|
||||
}
|
||||
|
||||
func (s *FileBackendTestSuite) TestZipReaderSingleFileCompressed() {
|
||||
// Test zipping a single file (but not its neighbours) with compression
|
||||
b := []byte("testdata")
|
||||
path := "tests/" + randomString() + ".txt"
|
||||
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(b), path)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b), written)
|
||||
defer s.backend.RemoveFile(path)
|
||||
|
||||
// neighbour, not included
|
||||
b2 := []byte("testdata2")
|
||||
path2 := "tests/" + randomString() + ".txt"
|
||||
|
||||
written, err = s.backend.WriteFile(bytes.NewReader(b2), path2)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(b2), written)
|
||||
defer s.backend.RemoveFile(path2)
|
||||
|
||||
reader, err := s.backend.ZipReader(path, true)
|
||||
s.NoError(err)
|
||||
defer reader.Close()
|
||||
|
||||
zipBytes, err := io.ReadAll(reader)
|
||||
s.NoError(err)
|
||||
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
|
||||
s.NoError(err)
|
||||
s.Len(zipReader.File, 1)
|
||||
|
||||
zf := zipReader.File[0]
|
||||
s.Equal(filepath.Base(path), zf.Name)
|
||||
s.Equal(zip.Deflate, zf.Method)
|
||||
|
||||
rc, err := zf.Open()
|
||||
s.NoError(err)
|
||||
defer rc.Close()
|
||||
|
||||
content, err := io.ReadAll(rc)
|
||||
s.NoError(err)
|
||||
s.Equal(b, content)
|
||||
}
|
||||
|
||||
func (s *FileBackendTestSuite) TestZipReaderDirectory() {
|
||||
// Create test directory structure
|
||||
dirPath := "tests/zip_test_" + randomString()
|
||||
files := map[string][]byte{
|
||||
"file1.txt": []byte("file1 content"),
|
||||
"file2.png": []byte("file2 content"),
|
||||
"subdir/file3.txt": []byte("file3 content"),
|
||||
"subdir2/file4.json": []byte("file4 content"),
|
||||
}
|
||||
|
||||
for path, content := range files {
|
||||
fullPath := filepath.Join(dirPath, path)
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(content), fullPath)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(content), written)
|
||||
defer s.backend.RemoveFile(fullPath)
|
||||
}
|
||||
|
||||
// Test without compression
|
||||
reader, err := s.backend.ZipReader(dirPath, false)
|
||||
s.NoError(err)
|
||||
defer reader.Close()
|
||||
|
||||
// Read and verify zip contents
|
||||
zipBytes, err := io.ReadAll(reader)
|
||||
s.NoError(err)
|
||||
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
|
||||
s.NoError(err)
|
||||
|
||||
// Verify each file
|
||||
foundFiles := make(map[string]bool)
|
||||
for _, zf := range zipReader.File {
|
||||
s.Contains(files, zf.Name)
|
||||
expectedContent := files[zf.Name]
|
||||
delete(files, zf.Name)
|
||||
|
||||
rc, err := zf.Open()
|
||||
s.NoError(err)
|
||||
|
||||
content, err := io.ReadAll(rc)
|
||||
s.NoError(err)
|
||||
rc.Close()
|
||||
|
||||
s.Equal(expectedContent, content)
|
||||
foundFiles[zf.Name] = true
|
||||
}
|
||||
|
||||
// Verify we found all files
|
||||
s.Len(foundFiles, 4)
|
||||
s.Empty(files)
|
||||
}
|
||||
|
||||
func (s *FileBackendTestSuite) TestZipReaderDirectoryCompressed() {
|
||||
// Create test directory structure
|
||||
dirPath := "tests/zip_test_" + randomString()
|
||||
files := map[string][]byte{
|
||||
"file1.txt": []byte("file1 content"),
|
||||
"file2.png": []byte("file2 content"),
|
||||
"subdir/file3.txt": []byte("file3 content"),
|
||||
"subdir2/file4.json": []byte("file4 content"),
|
||||
}
|
||||
|
||||
for path, content := range files {
|
||||
fullPath := filepath.Join(dirPath, path)
|
||||
written, err := s.backend.WriteFile(bytes.NewReader(content), fullPath)
|
||||
s.NoError(err)
|
||||
s.EqualValues(len(content), written)
|
||||
defer s.backend.RemoveFile(fullPath)
|
||||
}
|
||||
|
||||
// Test with compression
|
||||
reader, err := s.backend.ZipReader(dirPath, true)
|
||||
s.NoError(err)
|
||||
defer reader.Close()
|
||||
|
||||
// Read and verify zip contents
|
||||
zipBytes, err := io.ReadAll(reader)
|
||||
s.NoError(err)
|
||||
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
|
||||
s.NoError(err)
|
||||
|
||||
// Verify each file
|
||||
foundFiles := make(map[string]bool)
|
||||
for _, zf := range zipReader.File {
|
||||
s.Contains(files, zf.Name)
|
||||
s.Equal(zip.Deflate, zf.Method)
|
||||
expectedContent := files[zf.Name]
|
||||
delete(files, zf.Name)
|
||||
|
||||
rc, err := zf.Open()
|
||||
s.NoError(err)
|
||||
|
||||
content, err := io.ReadAll(rc)
|
||||
s.NoError(err)
|
||||
rc.Close()
|
||||
|
||||
s.Equal(expectedContent, content)
|
||||
foundFiles[zf.Name] = true
|
||||
}
|
||||
|
||||
// Verify we found all files
|
||||
s.Len(foundFiles, 4)
|
||||
s.Empty(files)
|
||||
}
|
||||
|
||||
func (s *FileBackendTestSuite) TestZipReaderErrors() {
|
||||
// Test non-existent path
|
||||
reader, err := s.backend.ZipReader("path/to/nonexistent.txt", false)
|
||||
if s.settings.DriverName == driverLocal {
|
||||
// Only local will return the error immediately.
|
||||
s.Error(err)
|
||||
s.Nil(reader)
|
||||
} else {
|
||||
s.NoError(err)
|
||||
defer reader.Close()
|
||||
var content []byte
|
||||
content, err = io.ReadAll(reader)
|
||||
s.NoError(err)
|
||||
s.assertEmptyZip(content)
|
||||
}
|
||||
|
||||
// Test empty directory
|
||||
emptyDir := "tests/empty_" + randomString()
|
||||
err = os.MkdirAll(filepath.Join(s.settings.Directory, emptyDir), 0750)
|
||||
s.NoError(err)
|
||||
defer os.RemoveAll(filepath.Join(s.settings.Directory, emptyDir))
|
||||
|
||||
reader, err = s.backend.ZipReader(emptyDir, false)
|
||||
s.NoError(err)
|
||||
defer reader.Close()
|
||||
content, err := io.ReadAll(reader)
|
||||
s.NoError(err)
|
||||
s.assertEmptyZip(content)
|
||||
}
|
||||
|
||||
func (s *FileBackendTestSuite) TestZipReaderErrorsCompressed() {
|
||||
// Test non-existent path with compression
|
||||
reader, err := s.backend.ZipReader("path/to/nonexistent.txt", true)
|
||||
if s.settings.DriverName == driverLocal {
|
||||
// Only local will return the error immediately.
|
||||
s.Error(err)
|
||||
s.Nil(reader)
|
||||
} else {
|
||||
s.NoError(err)
|
||||
defer reader.Close()
|
||||
var content []byte
|
||||
content, err = io.ReadAll(reader)
|
||||
s.NoError(err)
|
||||
s.assertEmptyZip(content)
|
||||
}
|
||||
|
||||
// Test empty directory with compression
|
||||
emptyDir := "tests/empty_" + randomString()
|
||||
err = os.MkdirAll(filepath.Join(s.settings.Directory, emptyDir), 0750)
|
||||
s.NoError(err)
|
||||
defer os.RemoveAll(filepath.Join(s.settings.Directory, emptyDir))
|
||||
|
||||
reader, err = s.backend.ZipReader(emptyDir, true)
|
||||
s.NoError(err)
|
||||
defer reader.Close()
|
||||
content, err := io.ReadAll(reader)
|
||||
s.NoError(err)
|
||||
s.assertEmptyZip(content)
|
||||
}
|
||||
|
||||
func (s *FileBackendTestSuite) assertEmptyZip(content []byte) {
|
||||
s.NotNil(content)
|
||||
|
||||
// Verify it's a valid but empty zip
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(content), int64(len(content)))
|
||||
s.NoError(err)
|
||||
s.Len(zipReader.File, 0)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package filestore
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
@@ -257,3 +258,90 @@ func (b *LocalFileBackend) RemoveDirectory(path string) error {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -387,6 +387,36 @@ func (_m *FileBackend) WriteFile(fr io.Reader, path string) (int64, error) {
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ZipReader provides a mock function with given fields: path, deflate
|
||||
func (_m *FileBackend) ZipReader(path string, deflate bool) (io.ReadCloser, error) {
|
||||
ret := _m.Called(path, deflate)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ZipReader")
|
||||
}
|
||||
|
||||
var r0 io.ReadCloser
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, bool) (io.ReadCloser, error)); ok {
|
||||
return rf(path, deflate)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, bool) io.ReadCloser); ok {
|
||||
r0 = rf(path, deflate)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(io.ReadCloser)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
|
||||
r1 = rf(path, deflate)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewFileBackend creates a new instance of FileBackend. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewFileBackend(t interface {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package filestore
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
@@ -705,6 +706,103 @@ func (b *S3FileBackend) RemoveDirectory(path string) error {
|
||||
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 *S3FileBackend) ZipReader(path string, deflate bool) (io.ReadCloser, error) {
|
||||
deflateMethod := zip.Store
|
||||
if deflate {
|
||||
deflateMethod = zip.Deflate
|
||||
}
|
||||
|
||||
path, err := b.prefixedPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
go func() {
|
||||
defer pw.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(pw)
|
||||
defer zipWriter.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
|
||||
defer cancel()
|
||||
|
||||
// Is path a single file?
|
||||
object, err := b.client.StatObject(ctx, b.bucket, path, s3.StatObjectOptions{})
|
||||
if err == nil {
|
||||
// We want the zipped file to be at the root of the zip. E.g., given a path of
|
||||
// "path/to/file.sh" we want the zip to have one file: "file.sh", not "path/to/file.sh".
|
||||
stripPath := filepath.Dir(path)
|
||||
if stripPath != "" {
|
||||
stripPath += "/"
|
||||
}
|
||||
if err = b._copyObjectToZipWriter(zipWriter, object, stripPath, deflateMethod); err != nil {
|
||||
pw.CloseWithError(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Is path a directory?
|
||||
path = path + "/"
|
||||
opts := s3.ListObjectsOptions{
|
||||
Prefix: path,
|
||||
Recursive: true,
|
||||
}
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), b.timeout)
|
||||
defer cancel2()
|
||||
|
||||
for object := range b.client.ListObjects(ctx2, b.bucket, opts) {
|
||||
if object.Err != nil {
|
||||
pw.CloseWithError(errors.Wrapf(object.Err, "unable to list the directory %s", path))
|
||||
return
|
||||
}
|
||||
|
||||
if err = b._copyObjectToZipWriter(zipWriter, object, path, deflateMethod); err != nil {
|
||||
pw.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
func (b *S3FileBackend) _copyObjectToZipWriter(zipWriter *zip.Writer, object s3.ObjectInfo, stripPath string, deflateMethod uint16) error {
|
||||
// We strip the path prefix that gets applied,
|
||||
// so that it remains transparent to the application.
|
||||
object.Key = strings.TrimPrefix(object.Key, b.pathPrefix)
|
||||
|
||||
// We strip the path prefix + path so the zip file is relative to the root of the requested path
|
||||
relPath := strings.TrimPrefix(object.Key, stripPath)
|
||||
header := &zip.FileHeader{
|
||||
Name: relPath,
|
||||
Method: deflateMethod,
|
||||
Modified: object.LastModified,
|
||||
}
|
||||
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", object.Key)
|
||||
}
|
||||
|
||||
reader, err := b.Reader(object.Key)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to create reader for %s", object.Key)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
_, err = io.Copy(writer, reader)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to copy content for %s", object.Key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *S3FileBackend) GeneratePublicLink(path string) (string, time.Duration, error) {
|
||||
path, err := b.prefixedPath(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var UnsafeContentTypes = [...]string{
|
||||
@@ -35,9 +37,10 @@ var MediaContentTypes = [...]string{
|
||||
"audio/wav",
|
||||
}
|
||||
|
||||
// WriteFileResponse copies the io.ReadSeeker `fileReader` to the ResponseWriter `w`. Use this when you have a
|
||||
// ReadSeeker.
|
||||
func WriteFileResponse(filename string, contentType string, contentSize int64, lastModification time.Time, webserverMode string, fileReader io.ReadSeeker, forceDownload bool, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "private, max-age=86400")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
setHeaders(w, contentType, forceDownload, filename)
|
||||
|
||||
if contentSize > 0 {
|
||||
contentSizeStr := strconv.Itoa(int(contentSize))
|
||||
@@ -48,6 +51,25 @@ func WriteFileResponse(filename string, contentType string, contentSize int64, l
|
||||
}
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, filename, lastModification, fileReader)
|
||||
}
|
||||
|
||||
// WriteStreamResponse copies the ReadCloser `r` to the ResponseWriter `w`. Use this when you need to stream a response
|
||||
// to the client that will appear as a file `filename` of type `contentType`.
|
||||
func WriteStreamResponse(w http.ResponseWriter, r io.ReadCloser, filename string, contentType string, forceDownload bool) error {
|
||||
setHeaders(w, contentType, forceDownload, filename)
|
||||
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
return errors.Wrap(err, "error streaming ReadCloser")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setHeaders(w http.ResponseWriter, contentType string, forceDownload bool, filename string) {
|
||||
w.Header().Set("Cache-Control", "private, max-age=86400")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
} else {
|
||||
@@ -66,14 +88,12 @@ func WriteFileResponse(filename string, contentType string, contentSize int64, l
|
||||
toDownload = true
|
||||
} else {
|
||||
isMediaType := false
|
||||
|
||||
for _, mediaContentType := range MediaContentTypes {
|
||||
if strings.HasPrefix(contentType, mediaContentType) {
|
||||
isMediaType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
toDownload = !isMediaType
|
||||
}
|
||||
|
||||
@@ -88,6 +108,4 @@ func WriteFileResponse(filename string, contentType string, contentSize int64, l
|
||||
// prevent file links from being embedded in iframes
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Content-Security-Policy", "Frame-ancestors 'none'")
|
||||
|
||||
http.ServeContent(w, r, filename, lastModification, fileReader)
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user