From 3a517135a557695525cbe46c7074db7c32b023d8 Mon Sep 17 00:00:00 2001 From: Steve Francia Date: Thu, 3 Dec 2015 22:30:35 -0500 Subject: [PATCH] Adding Afero/utils Portions ported from Go StdLib ioutils Portions ported from Hugo's helpers --- util/ioutil.go | 232 +++++++++++++++++++++++++++ util/ioutil_test.go | 116 ++++++++++++++ util/util.go | 297 ++++++++++++++++++++++++++++++++++ util/util_test.go | 381 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1026 insertions(+) create mode 100644 util/ioutil.go create mode 100644 util/ioutil_test.go create mode 100644 util/util.go create mode 100644 util/util_test.go diff --git a/util/ioutil.go b/util/ioutil.go new file mode 100644 index 0000000..ada8b12 --- /dev/null +++ b/util/ioutil.go @@ -0,0 +1,232 @@ +// Copyright ©2015 The Go Authors +// Copyright ©2015 Steve Francia +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "bytes" + "io" + "os" + "path/filepath" + "sort" + "strconv" + "sync" + "time" + + "github.com/spf13/afero" +) + +// byName implements sort.Interface. +type byName []os.FileInfo + +func (f byName) Len() int { return len(f) } +func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() } +func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] } + +// ReadDir reads the directory named by dirname and returns +// a list of sorted directory entries. +func (a AferoUtilFS) ReadDir(dirname string) ([]os.FileInfo, error) { + return ReadDir(dirname, a.fs) +} + +func ReadDir(dirname string, fs afero.Fs) ([]os.FileInfo, error) { + f, err := fs.Open(dirname) + if err != nil { + return nil, err + } + list, err := f.Readdir(-1) + f.Close() + if err != nil { + return nil, err + } + sort.Sort(byName(list)) + return list, nil +} + +// ReadFile reads the file named by filename and returns the contents. +// A successful call returns err == nil, not err == EOF. Because ReadFile +// reads the whole file, it does not treat an EOF from Read as an error +// to be reported. +func (a AferoUtilFS) ReadFile(filename string) ([]byte, error) { + return ReadFile(filename, a.fs) +} + +func ReadFile(filename string, fs afero.Fs) ([]byte, error) { + f, err := fs.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + // It's a good but not certain bet that FileInfo will tell us exactly how much to + // read, so let's try it but be prepared for the answer to be wrong. + var n int64 + + if fi, err := f.Stat(); err == nil { + // Don't preallocate a huge buffer, just in case. + if size := fi.Size(); size < 1e9 { + n = size + } + } + // As initial capacity for readAll, use n + a little extra in case Size is zero, + // and to avoid another allocation after Read has filled the buffer. The readAll + // call will read into its allocated internal buffer cheaply. If the size was + // wrong, we'll either waste some space off the end or reallocate as needed, but + // in the overwhelmingly common case we'll get it just right. + return readAll(f, n+bytes.MinRead) +} + +// readAll reads from r until an error or EOF and returns the data it read +// from the internal buffer allocated with a specified capacity. +func readAll(r io.Reader, capacity int64) (b []byte, err error) { + buf := bytes.NewBuffer(make([]byte, 0, capacity)) + // If the buffer overflows, we will get bytes.ErrTooLarge. + // Return that as an error. Any other panic remains. + defer func() { + e := recover() + if e == nil { + return + } + if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge { + err = panicErr + } else { + panic(e) + } + }() + _, err = buf.ReadFrom(r) + return buf.Bytes(), err +} + +// ReadAll reads from r until an error or EOF and returns the data it read. +// A successful call returns err == nil, not err == EOF. Because ReadAll is +// defined to read from src until EOF, it does not treat an EOF from Read +// as an error to be reported. +func ReadAll(r io.Reader) ([]byte, error) { + return readAll(r, bytes.MinRead) +} + +// WriteFile writes data to a file named by filename. +// If the file does not exist, WriteFile creates it with permissions perm; +// otherwise WriteFile truncates it before writing. +func (a AferoUtilFS) WriteFile(filename string, data []byte, perm os.FileMode) error { + return WriteFile(filename, data, perm, a.fs) +} + +func WriteFile(filename string, data []byte, perm os.FileMode, fs afero.Fs) error { + f, err := fs.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) + if err != nil { + return err + } + n, err := f.Write(data) + if err == nil && n < len(data) { + err = io.ErrShortWrite + } + if err1 := f.Close(); err == nil { + err = err1 + } + return err +} + +// Random number state. +// We generate random temporary file names so that there's a good +// chance the file doesn't exist yet - keeps the number of tries in +// TempFile to a minimum. +var rand uint32 +var randmu sync.Mutex + +func reseed() uint32 { + return uint32(time.Now().UnixNano() + int64(os.Getpid())) +} + +func nextSuffix() string { + randmu.Lock() + r := rand + if r == 0 { + r = reseed() + } + r = r*1664525 + 1013904223 // constants from Numerical Recipes + rand = r + randmu.Unlock() + return strconv.Itoa(int(1e9 + r%1e9))[1:] +} + +// TempFile creates a new temporary file in the directory dir +// with a name beginning with prefix, opens the file for reading +// and writing, and returns the resulting *afero.File. +// If dir is the empty string, TempFile uses the default directory +// for temporary files (see os.TempDir). +// Multiple programs calling TempFile simultaneously +// will not choose the same file. The caller can use f.Name() +// to find the pathname of the file. It is the caller's responsibility +// to remove the file when no longer needed. +func (a AferoUtilFS) TempFile(dir, prefix string) (f afero.File, err error) { + return TempFile(dir, prefix, a.fs) +} + +func TempFile(dir, prefix string, fs afero.Fs) (f afero.File, err error) { + if dir == "" { + dir = os.TempDir() + } + + nconflict := 0 + for i := 0; i < 10000; i++ { + name := filepath.Join(dir, prefix+nextSuffix()) + f, err = fs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) + if os.IsExist(err) { + if nconflict++; nconflict > 10 { + randmu.Lock() + rand = reseed() + randmu.Unlock() + } + continue + } + break + } + return +} + +// TempDir creates a new temporary directory in the directory dir +// with a name beginning with prefix and returns the path of the +// new directory. If dir is the empty string, TempDir uses the +// default directory for temporary files (see os.TempDir). +// Multiple programs calling TempDir simultaneously +// will not choose the same directory. It is the caller's responsibility +// to remove the directory when no longer needed. +func (a AferoUtilFS) TempDir(dir, prefix string) (name string, err error) { + return TempDir(dir, prefix, a.fs) +} +func TempDir(dir, prefix string, fs afero.Fs) (name string, err error) { + if dir == "" { + dir = os.TempDir() + } + + nconflict := 0 + for i := 0; i < 10000; i++ { + try := filepath.Join(dir, prefix+nextSuffix()) + err = fs.Mkdir(try, 0700) + if os.IsExist(err) { + if nconflict++; nconflict > 10 { + randmu.Lock() + rand = reseed() + randmu.Unlock() + } + continue + } + if err == nil { + name = try + } + break + } + return +} diff --git a/util/ioutil_test.go b/util/ioutil_test.go new file mode 100644 index 0000000..ab2efbf --- /dev/null +++ b/util/ioutil_test.go @@ -0,0 +1,116 @@ +// ©2015 The Go Authors +// Copyright ©2015 Steve Francia +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "testing" + + "github.com/spf13/afero" +) + +func checkSize(t *testing.T, path string, size int64) { + dir, err := testFS.Stat(path) + if err != nil { + t.Fatalf("Stat %q (looking for size %d): %s", path, size, err) + } + if dir.Size() != size { + t.Errorf("Stat %q: size %d want %d", path, dir.Size(), size) + } +} + +func TestReadFile(t *testing.T) { + testFS = &afero.MemMapFs{} + fsutil := &AferoUtilFS{fs: testFS} + + testFS.Create("this_exists.go") + filename := "rumpelstilzchen" + contents, err := fsutil.ReadFile(filename) + if err == nil { + t.Fatalf("ReadFile %s: error expected, none found", filename) + } + + filename = "this_exists.go" + contents, err = fsutil.ReadFile(filename) + if err != nil { + t.Fatalf("ReadFile %s: %v", filename, err) + } + + checkSize(t, filename, int64(len(contents))) +} + +func TestWriteFile(t *testing.T) { + testFS = &afero.MemMapFs{} + fsutil := &AferoUtilFS{fs: testFS} + f, err := fsutil.TempFile("", "ioutil-test") + if err != nil { + t.Fatal(err) + } + filename := f.Name() + data := "Programming today is a race between software engineers striving to " + + "build bigger and better idiot-proof programs, and the Universe trying " + + "to produce bigger and better idiots. So far, the Universe is winning." + + if err := fsutil.WriteFile(filename, []byte(data), 0644); err != nil { + t.Fatalf("WriteFile %s: %v", filename, err) + } + + contents, err := fsutil.ReadFile(filename) + if err != nil { + t.Fatalf("ReadFile %s: %v", filename, err) + } + + if string(contents) != data { + t.Fatalf("contents = %q\nexpected = %q", string(contents), data) + } + + // cleanup + f.Close() + testFS.Remove(filename) // ignore error +} + +func TestReadDir(t *testing.T) { + testFS = &afero.MemMapFs{} + testFS.Mkdir("/i-am-a-dir", 0777) + testFS.Create("/this_exists.go") + dirname := "rumpelstilzchen" + _, err := ReadDir(dirname, testFS) + if err == nil { + t.Fatalf("ReadDir %s: error expected, none found", dirname) + } + + dirname = ".." + list, err := ReadDir(dirname, testFS) + if err != nil { + t.Fatalf("ReadDir %s: %v", dirname, err) + } + + foundFile := false + foundSubDir := false + for _, dir := range list { + switch { + case !dir.IsDir() && dir.Name() == "this_exists.go": + foundFile = true + case dir.IsDir() && dir.Name() == "i-am-a-dir": + foundSubDir = true + } + } + if !foundFile { + t.Fatalf("ReadDir %s: this_exists.go file not found", dirname) + } + if !foundSubDir { + t.Fatalf("ReadDir %s: i-am-a-dir directory not found", dirname) + } +} diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..a0be68c --- /dev/null +++ b/util/util.go @@ -0,0 +1,297 @@ +// Copyright ©2015 Steve Francia +// Portions Copyright ©2015 The Hugo Authors +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "bytes" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" + "unicode" + + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" + + "github.com/spf13/afero" +) + +type AferoUtilFS struct { + fs afero.Fs +} + +// Filepath separator defined by os.Separator. +const FilePathSeparator = string(filepath.Separator) + +// Takes a reader and a path and writes the content +func (a AferoUtilFS) WriteReader(path string, r io.Reader) (err error) { + return WriteReader(path, r, a.fs) +} + +func WriteReader(path string, r io.Reader, fs afero.Fs) (err error) { + dir, _ := filepath.Split(path) + ospath := filepath.FromSlash(dir) + + if ospath != "" { + err = fs.MkdirAll(ospath, 0777) // rwx, rw, r + if err != nil { + if err != os.ErrExist { + log.Fatalln(err) + } + } + } + + file, err := fs.Create(path) + if err != nil { + return + } + defer file.Close() + + _, err = io.Copy(file, r) + return +} + +// Same as WriteReader but checks to see if file/directory already exists. +func (a AferoUtilFS) SafeWriteReader(path string, r io.Reader) (err error) { + return SafeWriteReader(path, r, a.fs) +} + +func SafeWriteReader(path string, r io.Reader, fs afero.Fs) (err error) { + dir, _ := filepath.Split(path) + ospath := filepath.FromSlash(dir) + + if ospath != "" { + err = fs.MkdirAll(ospath, 0777) // rwx, rw, r + if err != nil { + return + } + } + + exists, err := Exists(path, fs) + if err != nil { + return + } + if exists { + return fmt.Errorf("%v already exists", path) + } + + file, err := fs.Create(path) + if err != nil { + return + } + defer file.Close() + + _, err = io.Copy(file, r) + return +} + +func (a AferoUtilFS) GetTempDir(subPath string) string { + return GetTempDir(subPath, a.fs) +} + +// GetTempDir returns the default temp directory with trailing slash +// if subPath is not empty then it will be created recursively with mode 777 rwx rwx rwx +func GetTempDir(subPath string, fs afero.Fs) string { + addSlash := func(p string) string { + if FilePathSeparator != p[len(p)-1:] { + p = p + FilePathSeparator + } + return p + } + dir := addSlash(os.TempDir()) + + if subPath != "" { + // preserve windows backslash :-( + if FilePathSeparator == "\\" { + subPath = strings.Replace(subPath, "\\", "____", -1) + } + dir = dir + UnicodeSanitize((subPath)) + if FilePathSeparator == "\\" { + dir = strings.Replace(dir, "____", "\\", -1) + } + + if exists, _ := Exists(dir, fs); exists { + return addSlash(dir) + } + + err := fs.MkdirAll(dir, 0777) + if err != nil { + panic(err) + } + dir = addSlash(dir) + } + return dir +} + +// Rewrite string to remove non-standard path characters +func UnicodeSanitize(s string) string { + source := []rune(s) + target := make([]rune, 0, len(source)) + + for _, r := range source { + if unicode.IsLetter(r) || + unicode.IsDigit(r) || + unicode.IsMark(r) || + r == '.' || + r == '/' || + r == '\\' || + r == '_' || + r == '-' || + r == '%' || + r == ' ' || + r == '#' { + target = append(target, r) + } + } + + return string(target) +} + +// Transform characters with accents into plan forms +func NeuterAccents(s string) string { + t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) + result, _, _ := transform.String(t, string(s)) + + return result +} + +func isMn(r rune) bool { + return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks +} + +func (a AferoUtilFS) FileContainsBytes(filename string, subslice []byte) (bool, error) { + return FileContainsBytes(filename, subslice, a.fs) +} + +// Check if a file contains a specified string. +func FileContainsBytes(filename string, subslice []byte, fs afero.Fs) (bool, error) { + f, err := fs.Open(filename) + if err != nil { + return false, err + } + defer f.Close() + + return readerContains(f, subslice), nil +} + +// readerContains reports whether subslice is within r. +func readerContains(r io.Reader, subslice []byte) bool { + + if r == nil || len(subslice) == 0 { + return false + } + + bufflen := len(subslice) * 4 + halflen := bufflen / 2 + buff := make([]byte, bufflen) + var err error + var n, i int + + for { + i++ + if i == 1 { + n, err = io.ReadAtLeast(r, buff[:halflen], halflen) + } else { + if i != 2 { + // shift left to catch overlapping matches + copy(buff[:], buff[halflen:]) + } + n, err = io.ReadAtLeast(r, buff[halflen:], halflen) + } + + if n > 0 && bytes.Contains(buff, subslice) { + return true + } + + if err != nil { + break + } + } + return false +} + +func (a AferoUtilFS) DirExists(path string) (bool, error) { + return DirExists(path, a.fs) +} + +// DirExists checks if a path exists and is a directory. +func DirExists(path string, fs afero.Fs) (bool, error) { + fi, err := fs.Stat(path) + if err == nil && fi.IsDir() { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +func (a AferoUtilFS) IsDir(path string) (bool, error) { + return IsDir(path, a.fs) +} + +// IsDir checks if a given path is a directory. +func IsDir(path string, fs afero.Fs) (bool, error) { + fi, err := fs.Stat(path) + if err != nil { + return false, err + } + return fi.IsDir(), nil +} + +func (a AferoUtilFS) IsEmpty(path string) (bool, error) { + return IsEmpty(path, a.fs) +} + +// IsEmpty checks if a given file or directory is empty. +func IsEmpty(path string, fs afero.Fs) (bool, error) { + if b, _ := Exists(path, fs); !b { + return false, fmt.Errorf("%q path does not exist", path) + } + fi, err := fs.Stat(path) + if err != nil { + return false, err + } + if fi.IsDir() { + f, err := fs.Open(path) + defer f.Close() + if err != nil { + return false, err + } + list, err := f.Readdir(-1) + return len(list) == 0, nil + } + return fi.Size() == 0, nil +} + +func (a AferoUtilFS) Exists(path string) (bool, error) { + return Exists(path, a.fs) +} + +// Check if a file or directory exists. +func Exists(path string, fs afero.Fs) (bool, error) { + _, err := fs.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} diff --git a/util/util_test.go b/util/util_test.go new file mode 100644 index 0000000..447fdf6 --- /dev/null +++ b/util/util_test.go @@ -0,0 +1,381 @@ +// Copyright ©2015 Steve Francia +// Portions Copyright ©2015 The Hugo Authors +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package util + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + "time" + + "github.com/spf13/afero" +) + +var testFS = new(afero.MemMapFs) + +func TestDirExists(t *testing.T) { + type test struct { + input string + expected bool + } + + // First create a couple directories so there is something in the filesystem + //testFS := new(afero.MemMapFs) + testFS.MkdirAll("/foo/bar", 0777) + + data := []test{ + {".", true}, + {"./", true}, + {"..", true}, + {"../", true}, + {"./..", true}, + {"./../", true}, + {"/foo/", true}, + {"/foo", true}, + {"/foo/bar", true}, + {"/foo/bar/", true}, + {"/", true}, + {"/some-really-random-directory-name", false}, + {"/some/really/random/directory/name", false}, + {"./some-really-random-local-directory-name", false}, + {"./some/really/random/local/directory/name", false}, + } + + for i, d := range data { + exists, _ := DirExists(filepath.FromSlash(d.input), testFS) + if d.expected != exists { + t.Errorf("Test %d %q failed. Expected %t got %t", i, d.input, d.expected, exists) + } + } +} + +func TestIsDir(t *testing.T) { + testFS = new(afero.MemMapFs) + + type test struct { + input string + expected bool + } + data := []test{ + {"./", true}, + {"/", true}, + {"./this-directory-does-not-existi", false}, + {"/this-absolute-directory/does-not-exist", false}, + } + + for i, d := range data { + + exists, _ := IsDir(d.input, testFS) + if d.expected != exists { + t.Errorf("Test %d failed. Expected %t got %t", i, d.expected, exists) + } + } +} + +func TestIsEmpty(t *testing.T) { + testFS = new(afero.MemMapFs) + + zeroSizedFile, _ := createZeroSizedFileInTempDir() + defer deleteFileInTempDir(zeroSizedFile) + nonZeroSizedFile, _ := createNonZeroSizedFileInTempDir() + defer deleteFileInTempDir(nonZeroSizedFile) + emptyDirectory, _ := createEmptyTempDir() + defer deleteTempDir(emptyDirectory) + nonEmptyZeroLengthFilesDirectory, _ := createTempDirWithZeroLengthFiles() + defer deleteTempDir(nonEmptyZeroLengthFilesDirectory) + nonEmptyNonZeroLengthFilesDirectory, _ := createTempDirWithNonZeroLengthFiles() + defer deleteTempDir(nonEmptyNonZeroLengthFilesDirectory) + nonExistentFile := os.TempDir() + "/this-file-does-not-exist.txt" + nonExistentDir := os.TempDir() + "/this/direcotry/does/not/exist/" + + fileDoesNotExist := fmt.Errorf("%q path does not exist", nonExistentFile) + dirDoesNotExist := fmt.Errorf("%q path does not exist", nonExistentDir) + + type test struct { + input string + expectedResult bool + expectedErr error + } + + data := []test{ + {zeroSizedFile.Name(), true, nil}, + {nonZeroSizedFile.Name(), false, nil}, + {emptyDirectory, true, nil}, + {nonEmptyZeroLengthFilesDirectory, false, nil}, + {nonEmptyNonZeroLengthFilesDirectory, false, nil}, + {nonExistentFile, false, fileDoesNotExist}, + {nonExistentDir, false, dirDoesNotExist}, + } + for i, d := range data { + exists, err := IsEmpty(d.input, testFS) + if d.expectedResult != exists { + t.Errorf("Test %d %q failed exists. Expected result %t got %t", i, d.input, d.expectedResult, exists) + } + if d.expectedErr != nil { + if d.expectedErr.Error() != err.Error() { + t.Errorf("Test %d failed with err. Expected %q(%#v) got %q(%#v)", i, d.expectedErr, d.expectedErr, err, err) + } + } else { + if d.expectedErr != err { + t.Errorf("Test %d failed. Expected error %q(%#v) got %q(%#v)", i, d.expectedErr, d.expectedErr, err, err) + } + } + } +} + +func createZeroSizedFileInTempDir() (afero.File, error) { + filePrefix := "_path_test_" + f, e := TempFile("", filePrefix, testFS) // dir is os.TempDir() + if e != nil { + // if there was an error no file was created. + // => no requirement to delete the file + return nil, e + } + return f, nil +} + +func createNonZeroSizedFileInTempDir() (afero.File, error) { + f, err := createZeroSizedFileInTempDir() + if err != nil { + // no file ?? + } + byteString := []byte("byteString") + err = WriteFile(f.Name(), byteString, 0644, testFS) + if err != nil { + // delete the file + deleteFileInTempDir(f) + return nil, err + } + return f, nil +} + +func deleteFileInTempDir(f afero.File) { + err := testFS.Remove(f.Name()) + if err != nil { + // now what? + } +} + +func createEmptyTempDir() (string, error) { + dirPrefix := "_dir_prefix_" + d, e := TempDir("", dirPrefix, testFS) // will be in os.TempDir() + if e != nil { + // no directory to delete - it was never created + return "", e + } + return d, nil +} + +func createTempDirWithZeroLengthFiles() (string, error) { + d, dirErr := createEmptyTempDir() + if dirErr != nil { + //now what? + } + filePrefix := "_path_test_" + _, fileErr := TempFile(d, filePrefix, testFS) // dir is os.TempDir() + if fileErr != nil { + // if there was an error no file was created. + // but we need to remove the directory to clean-up + deleteTempDir(d) + return "", fileErr + } + // the dir now has one, zero length file in it + return d, nil + +} + +func createTempDirWithNonZeroLengthFiles() (string, error) { + d, dirErr := createEmptyTempDir() + if dirErr != nil { + //now what? + } + filePrefix := "_path_test_" + f, fileErr := TempFile(d, filePrefix, testFS) // dir is os.TempDir() + if fileErr != nil { + // if there was an error no file was created. + // but we need to remove the directory to clean-up + deleteTempDir(d) + return "", fileErr + } + byteString := []byte("byteString") + fileErr = WriteFile(f.Name(), byteString, 0644, testFS) + if fileErr != nil { + // delete the file + deleteFileInTempDir(f) + // also delete the directory + deleteTempDir(d) + return "", fileErr + } + + // the dir now has one, zero length file in it + return d, nil + +} + +func TestExists(t *testing.T) { + zeroSizedFile, _ := createZeroSizedFileInTempDir() + defer deleteFileInTempDir(zeroSizedFile) + nonZeroSizedFile, _ := createNonZeroSizedFileInTempDir() + defer deleteFileInTempDir(nonZeroSizedFile) + emptyDirectory, _ := createEmptyTempDir() + defer deleteTempDir(emptyDirectory) + nonExistentFile := os.TempDir() + "/this-file-does-not-exist.txt" + nonExistentDir := os.TempDir() + "/this/direcotry/does/not/exist/" + + type test struct { + input string + expectedResult bool + expectedErr error + } + + data := []test{ + {zeroSizedFile.Name(), true, nil}, + {nonZeroSizedFile.Name(), true, nil}, + {emptyDirectory, true, nil}, + {nonExistentFile, false, nil}, + {nonExistentDir, false, nil}, + } + for i, d := range data { + exists, err := Exists(d.input, testFS) + if d.expectedResult != exists { + t.Errorf("Test %d failed. Expected result %t got %t", i, d.expectedResult, exists) + } + if d.expectedErr != err { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expectedErr, err) + } + } + +} + +func TestSafeWriteToDisk(t *testing.T) { + emptyFile, _ := createZeroSizedFileInTempDir() + defer deleteFileInTempDir(emptyFile) + tmpDir, _ := createEmptyTempDir() + defer deleteTempDir(tmpDir) + + randomString := "This is a random string!" + reader := strings.NewReader(randomString) + + fileExists := fmt.Errorf("%v already exists", emptyFile.Name()) + + type test struct { + filename string + expectedErr error + } + + now := time.Now().Unix() + nowStr := strconv.FormatInt(now, 10) + data := []test{ + {emptyFile.Name(), fileExists}, + {tmpDir + "/" + nowStr, nil}, + } + + for i, d := range data { + e := SafeWriteReader(d.filename, reader, testFS) + if d.expectedErr != nil { + if d.expectedErr.Error() != e.Error() { + t.Errorf("Test %d failed. Expected error %q but got %q", i, d.expectedErr.Error(), e.Error()) + } + } else { + if d.expectedErr != e { + t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedErr, e) + } + contents, _ := ReadFile(d.filename, testFS) + if randomString != string(contents) { + t.Errorf("Test %d failed. Expected contents %q but got %q", i, randomString, string(contents)) + } + } + reader.Seek(0, 0) + } +} + +func TestWriteToDisk(t *testing.T) { + emptyFile, _ := createZeroSizedFileInTempDir() + defer deleteFileInTempDir(emptyFile) + tmpDir, _ := createEmptyTempDir() + defer deleteTempDir(tmpDir) + + randomString := "This is a random string!" + reader := strings.NewReader(randomString) + + type test struct { + filename string + expectedErr error + } + + now := time.Now().Unix() + nowStr := strconv.FormatInt(now, 10) + data := []test{ + {emptyFile.Name(), nil}, + {tmpDir + "/" + nowStr, nil}, + } + + for i, d := range data { + e := WriteReader(d.filename, reader, testFS) + if d.expectedErr != e { + t.Errorf("Test %d failed. WriteToDisk Error Expected %q but got %q", i, d.expectedErr, e) + } + contents, e := ReadFile(d.filename, testFS) + if e != nil { + t.Errorf("Test %d failed. Could not read file %s. Reason: %s\n", i, d.filename, e) + } + if randomString != string(contents) { + t.Errorf("Test %d failed. Expected contents %q but got %q", i, randomString, string(contents)) + } + reader.Seek(0, 0) + } +} + +func TestGetTempDir(t *testing.T) { + dir := os.TempDir() + if FilePathSeparator != dir[len(dir)-1:] { + dir = dir + FilePathSeparator + } + testDir := "hugoTestFolder" + FilePathSeparator + tests := []struct { + input string + expected string + }{ + {"", dir}, + {testDir + " Foo bar ", dir + testDir + " Foo bar " + FilePathSeparator}, + {testDir + "Foo.Bar/foo_Bar-Foo", dir + testDir + "Foo.Bar/foo_Bar-Foo" + FilePathSeparator}, + {testDir + "fOO,bar:foo%bAR", dir + testDir + "fOObarfoo%bAR" + FilePathSeparator}, + {testDir + "FOo/BaR.html", dir + testDir + "FOo/BaR.html" + FilePathSeparator}, + {testDir + "трям/трям", dir + testDir + "трям/трям" + FilePathSeparator}, + {testDir + "은행", dir + testDir + "은행" + FilePathSeparator}, + {testDir + "Банковский кассир", dir + testDir + "Банковский кассир" + FilePathSeparator}, + } + + for _, test := range tests { + output := GetTempDir(test.input, new(afero.MemMapFs)) + if output != test.expected { + t.Errorf("Expected %#v, got %#v\n", test.expected, output) + } + } +} + +// This function is very dangerous. Don't use it. +func deleteTempDir(d string) { + err := os.RemoveAll(d) + if err != nil { + // now what? + } +}