diff --git a/cmd/piecestore/main.go b/cmd/piecestore/main.go new file mode 100644 index 000000000..983fdda46 --- /dev/null +++ b/cmd/piecestore/main.go @@ -0,0 +1,125 @@ +// Copyright (C) 2018 Storj Labs, Inc. +// See LICENSE for copying information. + +package main + +import ( + "fmt" + "log" + "os" + "sort" + + "github.com/urfave/cli" + "github.com/zeebo/errs" + "storj.io/storj/pkg/piecestore" +) + +var argError = errs.Class("argError") + +func main() { + app := cli.NewApp() + + app.Name = "Piece Store CLI" + app.Usage = "Store data in hash folder structure" + app.Version = "1.0.0" + + app.Flags = []cli.Flag{} + + app.Commands = []cli.Command{ + { + Name: "store", + Aliases: []string{"s"}, + Usage: "Store data by hash", + ArgsUsage: "[hash] [dataPath] [storeDir]", + Action: func(c *cli.Context) error { + if c.Args().Get(0) == "" { + return argError.New("Missing data Hash") + } + + if c.Args().Get(1) == "" { + return argError.New("No input file specified") + } + + if c.Args().Get(2) == "" { + return argError.New("No output directory specified") + } + + file, err := os.Open(c.Args().Get(1)) + if err != nil { + return err + } + + // Close the file when we are done + defer file.Close() + + fileInfo, err := os.Stat(c.Args().Get(1)) + if err != nil { + return err + } + + if fileInfo.IsDir() { + return argError.New(fmt.Sprintf("Path (%s) is a directory, not a file", c.Args().Get(1))) + } + + _, err = pstore.Store(c.Args().Get(0), file, int64(fileInfo.Size()), 0, c.Args().Get(2)) + + return err + }, + }, + { + Name: "retrieve", + Aliases: []string{"r"}, + Usage: "Retrieve data by hash and print to Stdout", + ArgsUsage: "[hash] [storeDir]", + Action: func(c *cli.Context) error { + if c.Args().Get(0) == "" { + return argError.New("Missing data Hash") + } + if c.Args().Get(1) == "" { + return argError.New("Missing file path") + } + fileInfo, err := os.Stat(c.Args().Get(1)) + if err != nil { + return err + } + + if fileInfo.IsDir() != true { + return argError.New(fmt.Sprintf("Path (%s) is a file, not a directory", c.Args().Get(1))) + } + + _, err = pstore.Retrieve(c.Args().Get(0), os.Stdout, -1, 0, c.Args().Get(1)) + if err != nil { + + return err + } + + return nil + }, + }, + { + Name: "delete", + Aliases: []string{"d"}, + Usage: "Delete data by hash", + ArgsUsage: "[hash] [storeDir]", + Action: func(c *cli.Context) error { + if c.Args().Get(0) == "" { + return argError.New("Missing data Hash") + } + if c.Args().Get(1) == "" { + return argError.New("No directory specified") + } + err := pstore.Delete(c.Args().Get(0), c.Args().Get(1)) + + return err + }, + }, + } + + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod index 2cd5726e0..2f252b62a 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,30 @@ -module storj.io/storj +module "storj.io/storj" require ( - github.com/fsnotify/fsnotify v1.4.7 - github.com/go-redis/redis v0.0.0-20180417061816-9ccc23344a52 - github.com/gogo/protobuf v1.0.0 - github.com/golang/protobuf v1.0.0 - github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce - github.com/magiconair/properties v1.7.6 - github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 - github.com/pelletier/go-toml v1.1.0 - github.com/spacemonkeygo/errors v0.0.0-20171212215202-9064522e9fd1 - github.com/spf13/afero v1.1.0 - github.com/spf13/cast v1.2.0 - github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec - github.com/spf13/pflag v1.0.1 - github.com/spf13/viper v1.0.2 - github.com/tyler-smith/go-bip39 v0.0.0-20160629163856-8e7a99b3e716 - github.com/urfave/cli v1.20.0 - github.com/vivint/infectious v0.0.0-20180418194855-57d6abddc3d4 - github.com/zeebo/errs v0.1.0 - golang.org/x/crypto v0.0.0-20180410182641-f70185d77e82 - golang.org/x/net v0.0.0-20180420171651-5f9ae10d9af5 - golang.org/x/sys v0.0.0-20180430173509-4adea008a5e5 - golang.org/x/text v0.3.0 - google.golang.org/genproto v0.0.0-20180427144745-86e600f69ee4 - google.golang.org/grpc v1.11.3 - gopkg.in/yaml.v2 v2.2.1 + "github.com/aleitner/FilePiece" v0.0.0-20180516062859-b020bc25bf96 + "github.com/fsnotify/fsnotify" v1.4.7 + "github.com/go-redis/redis" v0.0.0-20180417061816-9ccc23344a52 + "github.com/gogo/protobuf" v1.0.0 + "github.com/golang/protobuf" v1.0.0 + "github.com/hashicorp/hcl" v0.0.0-20180404174102-ef8a98b0bbce + "github.com/magiconair/properties" v1.7.6 + "github.com/mitchellh/mapstructure" v0.0.0-20180220230111-00c29f56e238 + "github.com/pelletier/go-toml" v1.1.0 + "github.com/spacemonkeygo/errors" v0.0.0-20171212215202-9064522e9fd1 + "github.com/spf13/afero" v1.1.0 + "github.com/spf13/cast" v1.2.0 + "github.com/spf13/jWalterWeatherman" v0.0.0-20180109140146-7c0cea34c8ec + "github.com/spf13/pflag" v1.0.1 + "github.com/spf13/viper" v1.0.2 + "github.com/tyler-smith/go-bip39" v0.0.0-20160629163856-8e7a99b3e716 + "github.com/urfave/cli" v1.20.0 + "github.com/vivint/infectious" v0.0.0-20180418194855-57d6abddc3d4 + "github.com/zeebo/errs" v0.1.0 + "golang.org/x/crypto" v0.0.0-20180410182641-f70185d77e82 + "golang.org/x/net" v0.0.0-20180420171651-5f9ae10d9af5 + "golang.org/x/sys" v0.0.0-20180430173509-4adea008a5e5 + "golang.org/x/text" v0.3.0 + "google.golang.org/genproto" v0.0.0-20180427144745-86e600f69ee4 + "google.golang.org/grpc" v1.11.3 + "gopkg.in/yaml.v2" v2.2.1 ) diff --git a/pkg/piecestore/pstore.go b/pkg/piecestore/pstore.go new file mode 100644 index 000000000..53cc1613d --- /dev/null +++ b/pkg/piecestore/pstore.go @@ -0,0 +1,152 @@ +// Copyright (C) 2018 Storj Labs, Inc. +// See LICENSE for copying information. + +package pstore + +import ( + "io" + "os" + "path" + "path/filepath" + + "github.com/aleitner/FilePiece" + "github.com/zeebo/errs" +) + +// Errors +var ( + ArgError = errs.Class("argError") + FSError = errs.Class("fsError") +) + +// PathByHash -- creates datapath from hash and dir +func PathByHash(hash, dir string) (string, error) { + if len(hash) < 20 { + return "", ArgError.New("Invalid hash length") + } + + folder1 := string(hash[0:2]) + folder2 := string(hash[2:4]) + fileName := string(hash[4:]) + + return path.Join(dir, folder1, folder2, fileName), nil +} + +// Store -- Store data into piece store +// hash (string) Hash of the data to be stored +// r (io.Reader) File/Stream that contains the contents of the data to be stored +// length (length) Size of the data to be stored +// psFileOffset (offset) Offset of the data that you are writing. Useful for multiple connections to split the data transfer +// dir (string) pstore directory containing all other data stored +// returns (error) error if failed and nil if successful +func Store(hash string, r io.Reader, length int64, psFileOffset int64, dir string) (int64, error) { + if psFileOffset < 0 { + return 0, ArgError.New("Offset is less than 0. Must be greater than or equal to 0") + } + if length < 0 { + return 0, ArgError.New("Length is less than 0. Must be greater than or equal to 0") + } + if dir == "" { + return 0, ArgError.New("No path provided") + } + + dataPath, err := PathByHash(hash, dir) + if err != nil { + return 0, err + } + + // Create directory path on file system + if err = os.MkdirAll(filepath.Dir(dataPath), 0700); err != nil { + return 0, err + } + + // Create File on file system + dataFile, err := os.OpenFile(dataPath, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + return 0, err + } + + dataFileChunk := fpiece.NewChunk(dataFile, psFileOffset, length) + + // Close when finished + defer dataFile.Close() + + return io.CopyN(dataFileChunk, r, length) +} + +// Retrieve -- Retrieve data from pstore directory +// hash (string) Hash of the stored data +// w (io.Writer) Stream that recieves the stored data +// length (length) Amount of data to read. Read all data if -1 +// readPosOffset (offset) Offset of the data that you are reading. Useful for multiple connections to split the data transfer +// dir (string) pstore directory containing all other data stored +// returns (int64, error) returns err if failed and the number of bytes retrieved if successful +func Retrieve(hash string, w io.Writer, length int64, readPosOffset int64, dir string) (int64, error) { + if dir == "" { + return 0, ArgError.New("No path provided") + } + + dataPath, err := PathByHash(hash, dir) + if err != nil { + return 0, err + } + + fileInfo, err := os.Stat(dataPath) + if err != nil { + return 0, err + } + + // If offset is greater than file size return + if readPosOffset >= fileInfo.Size() || readPosOffset < 0 { + return 0, ArgError.New("Invalid offset: %v", readPosOffset) + } + + // If length less than 0 read the entire file + if length <= -1 { + length = fileInfo.Size() + } + + // If trying to read past the end of the file, just read to the end + if fileInfo.Size() < readPosOffset+length { + length = fileInfo.Size() - readPosOffset + } + + dataFile, err := os.OpenFile(dataPath, os.O_RDONLY, 0755) + if err != nil { + return 0, err + } + // Close when finished + defer dataFile.Close() + + // Created a section reader so that we can concurrently retrieve the same file. + dataFileChunk := fpiece.NewChunk(dataFile, readPosOffset, length) + + total, err := io.CopyN(w, dataFileChunk, length) + + return total, err +} + +// Delete -- Delete data from farmer +// hash (string) Hash of the data to be stored +// dir (string) pstore directory containing all other data stored +// returns (error) if failed and nil if successful +func Delete(hash string, dir string) error { + if dir == "" { + return ArgError.New("No path provided") + } + + dataPath, err := PathByHash(hash, dir) + if err != nil { + return err + } + + if _, err = os.Stat(dataPath); os.IsNotExist(err) { + return nil + } + + if err = os.Remove(dataPath); err != nil { + return err + } + + return nil +} diff --git a/pkg/piecestore/pstore_test.go b/pkg/piecestore/pstore_test.go new file mode 100644 index 000000000..f50e0a56e --- /dev/null +++ b/pkg/piecestore/pstore_test.go @@ -0,0 +1,556 @@ +// Copyright (C) 2018 Storj Labs, Inc. +// See LICENSE for copying information. + +package pstore + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" +) + +var tmpfile string + +func TestStore(t *testing.T) { + t.Run("it stores data successfully", func(t *testing.T) { + file, err := os.Open(tmpfile) + + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + fi, err := file.Stat() + if err != nil { + t.Errorf("Could not stat test file: %s", err.Error()) + return + } + + defer file.Close() + + hash := "0123456789ABCDEFGHIJ" + Store(hash, file, int64(fi.Size()), 0, os.TempDir()) + + folder1 := string(hash[0:2]) + folder2 := string(hash[2:4]) + fileName := string(hash[4:]) + + createdFilePath := path.Join(os.TempDir(), folder1, folder2, fileName) + defer os.RemoveAll(path.Join(os.TempDir(), folder1)) + _, lStatErr := os.Lstat(createdFilePath) + if lStatErr != nil { + t.Errorf("No file was created from Store(): %s", lStatErr.Error()) + return + } + + createdFile, openCreatedError := os.Open(createdFilePath) + if openCreatedError != nil { + t.Errorf("Error: %s opening created file %s", openCreatedError.Error(), createdFilePath) + } + defer createdFile.Close() + + buffer := make([]byte, 5) + createdFile.Seek(0, 0) + _, _ = createdFile.Read(buffer) + + if string(buffer) != "butts" { + t.Errorf("Expected data butts does not equal Actual data %s", string(buffer)) + } + }) + + t.Run("it stores data by offset successfully", func(t *testing.T) { + file, err := os.Open(tmpfile) + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + fi, err := file.Stat() + if err != nil { + t.Errorf("Could not stat test file: %s", err.Error()) + return + } + + defer file.Close() + + hash := "0123456789ABCDEFGHIJ" + Store(hash, file, int64(fi.Size()), 2, os.TempDir()) + + folder1 := string(hash[0:2]) + folder2 := string(hash[2:4]) + fileName := string(hash[4:]) + + createdFilePath := path.Join(os.TempDir(), folder1, folder2, fileName) + defer os.RemoveAll(path.Join(os.TempDir(), folder1)) + _, lStatErr := os.Lstat(createdFilePath) + if lStatErr != nil { + t.Errorf("No file was created from Store(): %s", lStatErr.Error()) + return + } + + createdFile, openCreatedError := os.Open(createdFilePath) + if openCreatedError != nil { + t.Errorf("Error: %s opening created file %s", openCreatedError.Error(), createdFilePath) + } + defer createdFile.Close() + + buffer := make([]byte, 7) + createdFile.Seek(0, 0) + _, _ = createdFile.Read(buffer) + + // \0\0butts + expected := []byte{0, 0, 98, 117, 116, 116, 115} + + if string(buffer) != string(expected) { + t.Errorf("Expected data (%v) does not equal Actual data (%v)", expected, buffer) + } + }) + + t.Run("it stores data by chunk successfully", func(t *testing.T) { + file, err := os.Open(tmpfile) + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + defer file.Close() + + hash := "0123456789ABCDEFGHIJ" + Store(hash, file, 4, 0, os.TempDir()) + + folder1 := string(hash[0:2]) + folder2 := string(hash[2:4]) + fileName := string(hash[4:]) + + createdFilePath := path.Join(os.TempDir(), folder1, folder2, fileName) + defer os.RemoveAll(path.Join(os.TempDir(), folder1)) + _, lStatErr := os.Lstat(createdFilePath) + if lStatErr != nil { + t.Errorf("No file was created from Store(): %s", lStatErr.Error()) + return + } + + createdFile, openCreatedError := os.Open(createdFilePath) + if openCreatedError != nil { + t.Errorf("Error: %s opening created file %s", openCreatedError.Error(), createdFilePath) + } + defer createdFile.Close() + + buffer := make([]byte, 4) + createdFile.Seek(0, 0) + _, _ = createdFile.Read(buffer) + + // butt + expected := []byte{98, 117, 116, 116} + + if string(buffer) != string(expected) { + t.Errorf("Expected data %s does not equal Actual data %s", string(expected), string(buffer)) + } + }) + + t.Run("it should return hash err if the hash is too short", func(t *testing.T) { + assert := assert.New(t) + file, err := os.Open(tmpfile) + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + defer file.Close() + + hash := "11111111" + + _, err = Store(hash, file, 5, 0, os.TempDir()) + assert.NotNil(err) + if err != nil { + assert.Equal(err.Error(), "argError: Invalid hash length", "They should have the same error message") + } + }) + + // Test passing in negative offset + t.Run("it should return an error when given negative offset", func(t *testing.T) { + assert := assert.New(t) + file, err := os.Open(tmpfile) + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + fi, err := file.Stat() + if err != nil { + t.Errorf("Could not stat test file: %s", err.Error()) + return + } + + defer file.Close() + + hash := "0123456789ABCDEFGHIJ" + + _, err = Store(hash, file, int64(fi.Size()), -12, os.TempDir()) + + assert.NotNil(err) + if err != nil { + assert.Equal(err.Error(), "argError: Offset is less than 0. Must be greater than or equal to 0", err.Error()) + } + }) + + // Test passing in a negative length + t.Run("it should return an error when given length less than 0", func(t *testing.T) { + assert := assert.New(t) + file, err := os.Open(tmpfile) + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + _, err = file.Stat() + if err != nil { + t.Errorf("Could not stat test file: %s", err.Error()) + return + } + + defer file.Close() + + hash := "0123456789ABCDEFGHIJ" + + _, err = Store(hash, file, -1, 0, os.TempDir()) + assert.NotNil(err) + if err != nil { + assert.Equal(err.Error(), "argError: Length is less than 0. Must be greater than or equal to 0", err.Error()) + } + }) +} + +func TestRetrieve(t *testing.T) { + t.Run("it retrieves data successfully", func(t *testing.T) { + file, err := os.Open(tmpfile) + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + fi, err := file.Stat() + if err != nil { + t.Errorf("Could not stat test file: %s", err.Error()) + return + } + defer file.Close() + + hash := "0123456789ABCDEFGHIJ" + Store(hash, file, int64(fi.Size()), 0, os.TempDir()) + + // Create file for retrieving data into + retrievalFilePath := path.Join(os.TempDir(), "retrieved.txt") + retrievalFile, err := os.OpenFile(retrievalFilePath, os.O_RDWR|os.O_CREATE, 0777) + if err != nil { + t.Errorf("Error creating file: %s", err.Error()) + return + } + defer os.RemoveAll(retrievalFilePath) + defer retrievalFile.Close() + + _, err = Retrieve(hash, retrievalFile, int64(fi.Size()), 0, os.TempDir()) + + if err != nil { + if err != io.EOF { + t.Errorf("Retrieve Error: %s", err.Error()) + } + } + + buffer := make([]byte, 5) + + retrievalFile.Seek(0, 0) + _, _ = retrievalFile.Read(buffer) + + fmt.Printf("Retrieved data: %s", string(buffer)) + + if string(buffer) != "butts" { + t.Errorf("Expected data butts does not equal Actual data %s", string(buffer)) + } + }) + + t.Run("it retrieves data by offset successfully", func(t *testing.T) { + file, err := os.Open(tmpfile) + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + fi, err := file.Stat() + if err != nil { + t.Errorf("Could not stat test file: %s", err.Error()) + return + } + + defer file.Close() + + hash := "0123456789ABCDEFGHIJ" + Store(hash, file, int64(fi.Size()), 0, os.TempDir()) + + // Create file for retrieving data into + retrievalFilePath := path.Join(os.TempDir(), "retrieved.txt") + retrievalFile, err := os.OpenFile(retrievalFilePath, os.O_RDWR|os.O_CREATE, 0777) + if err != nil { + t.Errorf("Error creating file: %s", err.Error()) + return + } + defer os.RemoveAll(retrievalFilePath) + defer retrievalFile.Close() + + _, err = Retrieve(hash, retrievalFile, int64(fi.Size()), 2, os.TempDir()) + + if err != nil { + if err != io.EOF { + t.Errorf("Retrieve Error: %s", err.Error()) + } + } + + buffer := make([]byte, 3) + + retrievalFile.Seek(0, 0) + _, _ = retrievalFile.Read(buffer) + + fmt.Printf("Retrieved data: %s", string(buffer)) + + if string(buffer) != "tts" { + t.Errorf("Expected data (tts) does not equal Actual data (%s)", string(buffer)) + } + }) + + t.Run("it retrieves data by chunk successfully", func(t *testing.T) { + file, err := os.Open(tmpfile) + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + fi, err := file.Stat() + if err != nil { + t.Errorf("Could not stat test file: %s", err.Error()) + return + } + + defer file.Close() + + hash := "0123456789ABCDEFGHIJ" + Store(hash, file, int64(fi.Size()), 0, os.TempDir()) + + // Create file for retrieving data into + retrievalFilePath := path.Join(os.TempDir(), "retrieved.txt") + retrievalFile, err := os.OpenFile(retrievalFilePath, os.O_RDWR|os.O_CREATE, 0777) + if err != nil { + t.Errorf("Error creating file: %s", err.Error()) + return + } + defer os.RemoveAll(retrievalFilePath) + defer retrievalFile.Close() + + _, err = Retrieve(hash, retrievalFile, 3, 0, os.TempDir()) + + if err != nil { + if err != io.EOF { + t.Errorf("Retrieve Error: %s", err.Error()) + } + } + + buffer := make([]byte, 3) + + retrievalFile.Seek(0, 0) + _, _ = retrievalFile.Read(buffer) + + fmt.Printf("Retrieved data: %s", string(buffer)) + + if string(buffer) != "but" { + t.Errorf("Expected data (but) does not equal Actual data (%s)", string(buffer)) + } + }) + + // Test passing in negative offset + t.Run("it should return an error when retrieving with offset less 0", func(t *testing.T) { + assert := assert.New(t) + file, err := os.Open(tmpfile) + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + fi, err := file.Stat() + if err != nil { + t.Errorf("Could not stat test file: %s", err.Error()) + return + } + + defer file.Close() + + hash := "0123456789ABCDEFGHIJ" + Store(hash, file, int64(fi.Size()), 0, os.TempDir()) + + // Create file for retrieving data into + retrievalFilePath := path.Join(os.TempDir(), "retrieved.txt") + retrievalFile, err := os.OpenFile(retrievalFilePath, os.O_RDWR|os.O_CREATE, 0777) + if err != nil { + t.Errorf("Error creating file: %s", err.Error()) + return + } + defer os.RemoveAll(retrievalFilePath) + defer retrievalFile.Close() + + _, err = Retrieve(hash, retrievalFile, int64(fi.Size()), -1, os.TempDir()) + assert.NotNil(err) + if err != nil { + assert.Equal("argError: Invalid offset: -1", err.Error(), err.Error()) + } + }) + + // Test passing in negative length + t.Run("it should return the entire file successfully when retrieving with negative length", func(t *testing.T) { + file, err := os.Open(tmpfile) + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + fi, err := file.Stat() + if err != nil { + t.Errorf("Could not stat test file: %s", err.Error()) + return + } + + defer file.Close() + + hash := "0123456789ABCDEFGHIJ" + Store(hash, file, int64(fi.Size()), 0, os.TempDir()) + + // Create file for retrieving data into + retrievalFilePath := path.Join(os.TempDir(), "retrieved.txt") + retrievalFile, err := os.OpenFile(retrievalFilePath, os.O_RDWR|os.O_CREATE, 0777) + if err != nil { + t.Errorf("Error creating file: %s", err.Error()) + return + } + defer os.RemoveAll(retrievalFilePath) + defer retrievalFile.Close() + + _, err = Retrieve(hash, retrievalFile, -1, 0, os.TempDir()) + + if err != nil { + if err != io.EOF { + t.Errorf("Retrieve Error: %s", err.Error()) + } + } + + buffer := make([]byte, 5) + + retrievalFile.Seek(0, 0) + _, _ = retrievalFile.Read(buffer) + + fmt.Printf("Retrieved data: %s", string(buffer)) + + if string(buffer) != "butts" { + t.Errorf("Expected data butts does not equal Actual data %s", string(buffer)) + } + }) + +} + +func TestDelete(t *testing.T) { + t.Run("it deletes data successfully", func(t *testing.T) { + file, err := os.Open(tmpfile) + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + fi, err := file.Stat() + if err != nil { + t.Errorf("Could not stat test file: %s", err.Error()) + return + } + + defer file.Close() + + hash := "0123456789ABCDEFGHIJ" + Store(hash, file, int64(fi.Size()), 0, os.TempDir()) + + folder1 := string(hash[0:2]) + folder2 := string(hash[2:4]) + fileName := string(hash[4:]) + + if _, err := os.Stat(path.Join(os.TempDir(), folder1, folder2, fileName)); err != nil { + t.Errorf("Failed to Store test file") + return + } + + Delete(hash, os.TempDir()) + _, err = os.Stat(path.Join(os.TempDir(), folder1, folder2, fileName)) + if err == nil { + t.Errorf("Failed to Delete test file") + return + } + }) + + // Test passing in a hash that doesn't exist + t.Run("it returns an error if hash doesn't exist", func(t *testing.T) { + assert := assert.New(t) + file, err := os.Open(tmpfile) + if err != nil { + t.Errorf("Error opening tmp file: %s", err.Error()) + return + } + + fi, err := file.Stat() + if err != nil { + t.Errorf("Could not stat test file: %s", err.Error()) + return + } + + defer file.Close() + + hash := "0123456789ABCDEFGHIJ" + Store(hash, file, int64(fi.Size()), 0, os.TempDir()) + + folder1 := string(hash[0:2]) + folder2 := string(hash[2:4]) + fileName := string(hash[4:]) + + if _, err := os.Stat(path.Join(os.TempDir(), folder1, folder2, fileName)); err != nil { + t.Errorf("Failed to Store test file") + return + } + + falseHash := "" + + err = Delete(falseHash, os.TempDir()) + assert.NotNil(err) + if err != nil { + assert.NotEqual(err.Error(), "argError: Hash folder does not exist", "They should be equal") + } + }) +} + +func TestMain(m *testing.M) { + content := []byte("butts") + tmpfilePtr, err := ioutil.TempFile("", "api_test") + if err != nil { + log.Fatal(err) + } + + tmpfile = tmpfilePtr.Name() + defer os.Remove(tmpfile) // clean up + + if _, err := tmpfilePtr.Write(content); err != nil { + log.Fatal(err) + } + + if err := tmpfilePtr.Close(); err != nil { + log.Fatal(err) + } + + m.Run() +}