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:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user