Import source: handler/fs/fs

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:18:55 +03:30
parent 0d61b36cb3
commit 055d864490
2 changed files with 214 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
package fs
import (
"io"
"path"
"reactor-crw"
)
// PathResolver defines a simple interface to resolve path for content
// before saving it.
type pathResolver interface {
// CreateFolder creates a new folder in the "current" dir. The current
// directory is specified in Config.SavePath provided by a user. If dir
// with such name already exists it'll skip creation step.
CreateFolder(name string) error
// CreateFile creates a new file for downloaded content in the directory
// created by the CreateFolder function and returns the corresponding
// io.WriteCloser. If a file with such a name already exists then it'll
// be overwritten.
CreateFile(name string) (io.WriteCloser, error)
// Remove removes a file or folder by its name. If provided path does not
// exist it'll be skipped.
Remove(name string)
}
// FileSaver defines ContentHandler implementation that will download content
// and save it to the host's file system. It resolves the corresponding file
// path with PathResolver.
type FileSaver struct {
pr pathResolver
t reactor_crw.Transport
}
// NewFileSaver creates a new FileSaver instance along with a new folder that
// will act as a context for a new instance. All files will be processed within
// this new folder.
func NewFileSaver(pr pathResolver, t reactor_crw.Transport, baseFolder string) (*FileSaver, error) {
err := pr.CreateFolder(baseFolder)
if err != nil {
return nil, err
}
return &FileSaver{
pr: pr,
t: t,
}, nil
}
// Process downloads content by the corresponding URL by making an HTTP request
// and saves the result to the file system. In case of error during saving the
// content the corresponding file will be deleted from the file system.
func (f *FileSaver) Process(url string, progress chan<- int, e chan<- error) {
defer func() {
progress <- 1
}()
data, err := f.t.FetchData(url)
if err != nil {
e <- err
return
}
defer func(b io.ReadCloser) {
_ = b.Close()
}(data)
file, err := f.pr.CreateFile(path.Base(url))
if err != nil {
e <- err
return
}
defer func(f io.WriteCloser) {
_ = f.Close()
}(file)
_, err = io.Copy(file, data)
if err != nil {
f.pr.Remove(path.Base(url))
e <- err
}
}
+130
View File
@@ -0,0 +1,130 @@
//go:build unit
// +build unit
package fs_test
import (
"errors"
"io"
"io/ioutil"
"os"
"reactor-crw/handler/fs"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type pathResolverMock struct {
mock.Mock
}
func (m *pathResolverMock) CreateFolder(name string) error {
args := m.Called(name)
return args.Error(0)
}
func (m *pathResolverMock) CreateFile(name string) (io.WriteCloser, error) {
args := m.Called(name)
return args.Get(0).(io.WriteCloser), args.Error(1)
}
func (m *pathResolverMock) Remove(name string) {
_ = m.Called(name)
}
type transportMock struct {
mock.Mock
}
func (m *transportMock) FetchData(url string) (io.ReadCloser, error) {
args := m.Called(url)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
func TestNewFileSaver(t *testing.T) {
pr := pathResolverMock{}
trp := transportMock{}
t.Log("Given the need to create a file saver handler.")
{
t.Log("When baseFolder is incorrect.")
{
pr.On("CreateFolder", "baseFolder").Return(errors.New("error"))
_, err := fs.NewFileSaver(&pr, &trp, "baseFolder")
require.Error(t, err, "Expected an error during create folder")
pr.ExpectedCalls = []*mock.Call{}
}
t.Log("When all data correct.")
{
pr.On("CreateFolder", "baseFolder").Return(nil)
_, err := fs.NewFileSaver(&pr, &trp, "baseFolder")
require.NoError(t, err, "Wasn't expected an error on new creating file saver")
}
}
}
func TestFileSaver_Process(t *testing.T) {
tmlFile, _ := ioutil.TempFile(os.TempDir(), "file-title.txt")
pr := pathResolverMock{}
pr.On("CreateFolder", "baseFolder").Return(nil)
pr.On("Remove", mock.Anything)
trp := transportMock{}
p := make(chan int, 1)
e := make(chan error, 1)
fileSaver, _ := fs.NewFileSaver(&pr, &trp, "baseFolder")
t.Log("Given the need to process data.")
{
t.Log("When transport returns an error.")
{
trp.On("FetchData", "file-title.txt").Return(tmlFile, errors.New("error")).Once()
fileSaver.Process("file-title.txt", p, e)
require.Error(t, <-e, "Expected an error during create file")
<-p
}
t.Log("When path resolver returns an error.")
{
trp.On("FetchData", "file-title.txt").Return(tmlFile, nil).Once()
pr.On("CreateFile", "file-title.txt").Return(tmlFile, errors.New("error")).Once()
fileSaver.Process("file-title.txt", p, e)
require.Error(t, <-e, "Expected an error during create file")
<-p
}
t.Log("When cannot save the data.")
{
trp.On("FetchData", "file-title.txt").Return(tmlFile, nil).Once()
pr.On("CreateFile", "file-title.txt").Return(tmlFile, nil).Once()
pr.On("Remove", mock.Anything)
fileSaver.Process("file-title.txt", p, e)
require.Error(t, <-e, "Expected an error file copying")
<-p
}
t.Log("When all data correct.")
{
tmlFile, _ = ioutil.TempFile(os.TempDir(), "new-file-title.txt")
trp.On("FetchData", "new-file-title.txt").Return(tmlFile, nil).Once()
pr.On("CreateFile", "new-file-title.txt").Return(tmlFile, nil).Once()
fileSaver.Process("new-file-title.txt", p, e)
select {
case err := <-e:
require.NoError(t, err, "Wasn't expected an error on new file process")
case <-p:
return
}
}
}
}