Skip to content
This repository has been archived by the owner on Apr 30, 2021. It is now read-only.

Commit

Permalink
Obey os.File flags
Browse files Browse the repository at this point in the history
Support:
  O_RDONLY int = syscall.O_RDONLY // open the file read-only.
  O_WRONLY int = syscall.O_WRONLY // open the file write-only.
  O_RDWR   int = syscall.O_RDWR   // open the file read-write.
  O_APPEND int = syscall.O_APPEND // append data to the file when writing.
  O_CREATE int = syscall.O_CREAT  // create a new file if none exists.
  O_TRUNC  int = syscall.O_TRUNC  // if possible, truncate file when opened.
  • Loading branch information
Svetlin Ralchev committed Apr 19, 2018
1 parent 96edba6 commit 727a09e
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 24 deletions.
120 changes: 105 additions & 15 deletions manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"time"
)

var (
// ErrReadOnly is returned if the file is read-only and write operations are disabled.
ErrReadOnly = errors.New("File is read-only")
// ErrWriteOnly is returned if the file is write-only and read operations are disabled.
ErrWriteOnly = errors.New("File is write-only")
// ErrIsDirectory is returned if the file under operation is not a regular file but a directory.
ErrIsDirectory = errors.New("Is directory")
)

var _ FileSystem = &Manager{}
Expand Down Expand Up @@ -65,7 +76,7 @@ func (m *Manager) uncompress(reader *tar.Reader) error {

// Root returns a sub-manager for given path
func (m *Manager) Root(name string) (*Manager, error) {
if node := find(split(name), m.root); node != nil {
if _, node := find(split(name), nil, m.root); node != nil {
if node.IsDir {
return &Manager{root: node}, nil
}
Expand All @@ -76,22 +87,77 @@ func (m *Manager) Root(name string) (*Manager, error) {

// Open opens an embedded resource for read
func (m *Manager) Open(name string) (ReadOnlyFile, error) {
return m.OpenFile(name, 0, 0)
return m.OpenFile(name, os.O_RDONLY, 0)
}

// OpenFile is the generalized open call; most users will use Open
func (m *Manager) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
if node := find(split(name), m.root); node != nil {
return NewResourceFile(node), nil
path := split(name)
parent, node := find(path, nil, m.root)
if parent == nil {
return nil, fmt.Errorf("Directory does not exist")
}

return nil, fmt.Errorf("File '%s' not found", name)
if hasFlag(os.O_CREATE, flag) {
if node != nil {
if !hasFlag(os.O_TRUNC, flag) {
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrExist}
}
}

base := path[len(path)-1]
node = &Node{
Name: base,
IsDir: false,
ModTime: time.Now(),
}

parent.Children = append(parent.Children, node)
} else {
if node == nil {
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
}
if node.IsDir {
return nil, &os.PathError{Op: "open", Path: name, Err: ErrIsDirectory}
}
}

if hasFlag(os.O_WRONLY, flag) ||
hasFlag(os.O_RDWR, flag) ||
hasFlag(os.O_APPEND, flag) {
node.ModTime = time.Now()
}

return createResourceFile(node, flag)
}

func createResourceFile(node *Node, flag int) (File, error) {
if node.Content == nil || hasFlag(os.O_TRUNC, flag) {
buf := make([]byte, 0)
node.Content = &buf
node.Mutex = &sync.RWMutex{}
}

f := NewResourceFile(node)

if hasFlag(os.O_APPEND, flag) {
_, _ = f.Seek(0, os.SEEK_END)
}

if hasFlag(os.O_RDWR, flag) {
return f, nil
}
if hasFlag(os.O_WRONLY, flag) {
return &woFile{f}, nil
}

return &roFile{f}, nil
}

// Walk walks the file tree rooted at root, calling walkFn for each file or
// directory in the tree, including root.
func (m *Manager) Walk(dir string, fn filepath.WalkFunc) error {
if node := find(split(dir), m.root); node != nil {
if _, node := find(split(dir), nil, m.root); node != nil {
return walk(dir, node, fn)
}

Expand All @@ -116,9 +182,10 @@ func add(path []string, node *Node) *Node {
}

child := &Node{
Mutex: &sync.RWMutex{},
Name: name,
IsDir: true,
Mutex: &sync.RWMutex{},
Name: name,
IsDir: true,
ModTime: time.Now(),
}

node.Children = append(node.Children, child)
Expand All @@ -137,21 +204,21 @@ func split(path string) []string {
return parts
}

func find(path []string, node *Node) *Node {
if len(path) == 0 {
return node
func find(path []string, parent, node *Node) (*Node, *Node) {
if len(path) == 0 || node == nil {
return parent, node
}

for _, child := range node.Children {
if path[0] == child.Name {
if len(path) == 1 {
return child
return node, child
}
return find(path[1:], child)
return find(path[1:], node, child)
}
}

return nil
return parent, nil
}

func walk(path string, node *Node, fn filepath.WalkFunc) error {
Expand All @@ -167,3 +234,26 @@ func walk(path string, node *Node, fn filepath.WalkFunc) error {

return nil
}

func hasFlag(flag int, flags int) bool {
return flags&flag == flag
}

type roFile struct {
*ResourceFile
}

// Write is disabled and returns ErrorReadOnly
func (f *roFile) Write(p []byte) (n int, err error) {
return 0, ErrReadOnly
}

// woFile wraps the given file and disables Read(..) operation.
type woFile struct {
*ResourceFile
}

// Read is disabled and returns ErrorWroteOnly
func (f *woFile) Read(p []byte) (n int, err error) {
return 0, ErrWriteOnly
}
126 changes: 117 additions & 9 deletions manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package parcello_test

import (
"fmt"
"io"
"io/ioutil"
"os"
"time"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -103,17 +105,25 @@ var _ = Describe("Manager", func() {
Describe("Open", func() {
Context("when the resource is empty", func() {
It("returns an error", func() {
file, err := manager.Open("migration.sql")
file, err := manager.Open("/migration.sql")
Expect(file).To(BeNil())
Expect(err).To(MatchError("File 'migration.sql' not found"))
Expect(err).To(MatchError("Directory does not exist"))
})
})

Context("when the file is directory", func() {
It("returns an error", func() {
file, err := manager.Open("/resource/reports")
Expect(file).To(BeNil())
Expect(err).To(MatchError("open /resource/reports: Is directory"))
})
})

Context("when the global resource is empty", func() {
It("returns an error", func() {
file, err := parcello.Open("migration.sql")
Expect(file).To(BeNil())
Expect(err).To(MatchError("File 'migration.sql' not found"))
Expect(err).To(MatchError("Directory does not exist"))
})
})

Expand All @@ -127,19 +137,117 @@ var _ = Describe("Manager", func() {
Expect(string(data)).To(Equal("Report 2018\n"))
})

Context("when is trying to open a directory", func() {
It("returns an error", func() {
file, err := manager.Open("/resource/reports/")
Context("when the file is open more than once for read", func() {
It("does not change the mod time", func() {
file, err := manager.Open("/resource/reports/2018.txt")
Expect(file).NotTo(BeNil())
Expect(err).NotTo(HaveOccurred())

info, err := file.Stat()
Expect(err).NotTo(HaveOccurred())

file, err = manager.Open("/resource/reports/2018.txt")
Expect(file).NotTo(BeNil())
Expect(err).To(BeNil())
Expect(err).NotTo(HaveOccurred())

info2, err := file.Stat()
Expect(err).NotTo(HaveOccurred())

Expect(info.ModTime()).To(Equal(info2.ModTime()))
})
})

It("returns a readonly resource", func() {
file, err := manager.Open("/resource/reports/2018.txt")
Expect(file).NotTo(BeNil())
Expect(err).NotTo(HaveOccurred())

_, err = fmt.Fprintln(file.(io.Writer), "hello")
Expect(err).To(MatchError("File is read-only"))
})

Context("when the file with the requested name does not exist", func() {
It("returns an error", func() {
file, err := manager.Open("/home/root/migration.sql")
file, err := manager.Open("/resource/migration.sql")
Expect(file).To(BeNil())
Expect(err).To(MatchError("File '/home/root/migration.sql' not found"))
Expect(err).To(MatchError("open /resource/migration.sql: file does not exist"))
})
})
})

Describe("OpenFile", func() {
Context("when the file does not exist", func() {
It("creates the file", func() {
file, err := manager.OpenFile("/resource/secrets.txt", os.O_CREATE, 0600)
Expect(file).NotTo(BeNil())
Expect(err).NotTo(HaveOccurred())
})
})

Context("when the file exists", func() {
It("truncs the file content", func() {
file, err := manager.OpenFile("/resource/reports/2018.txt", os.O_CREATE|os.O_TRUNC, 0600)
Expect(file).NotTo(BeNil())
Expect(err).NotTo(HaveOccurred())

data, err := ioutil.ReadAll(file)
Expect(err).NotTo(HaveOccurred())
Expect(data).To(BeEmpty())
})

Context("when the file is open more than once for write", func() {
It("does not change the mod time", func() {
start := time.Now()

file, err := manager.OpenFile("/resource/reports/2018.txt", os.O_WRONLY, 0600)
Expect(file).NotTo(BeNil())
Expect(err).NotTo(HaveOccurred())

info, err := file.Stat()
Expect(err).NotTo(HaveOccurred())
modTime := info.ModTime()

Expect(modTime.After(start)).To(BeTrue())
})
})

Context("when the os.O_TRUNC flag is not provided", func() {
It("returns an error", func() {
file, err := manager.OpenFile("/resource/reports/2018.txt", os.O_CREATE, 0600)
Expect(file).To(BeNil())
Expect(err).To(MatchError("open /resource/reports/2018.txt: file already exists"))
})
})

Context("when the file is open for append", func() {
It("appends content successfully", func() {
file, err := manager.OpenFile("/resource/reports/2018.txt", os.O_RDWR|os.O_APPEND, 0600)
Expect(file).NotTo(BeNil())
Expect(err).NotTo(HaveOccurred())

_, err = fmt.Fprint(file, "hello")
Expect(err).NotTo(HaveOccurred())

_, err = file.Seek(0, os.SEEK_SET)
Expect(err).NotTo(HaveOccurred())

data, err := ioutil.ReadAll(file)
Expect(err).NotTo(HaveOccurred())
Expect(string(data)).To(Equal("Report 2018\nhello"))
})
})

Context("when the file is open for WRITE only", func() {
Context("when we try to read", func() {
It("returns an error", func() {
file, err := manager.OpenFile("/resource/reports/2018.txt", os.O_WRONLY, 0600)
Expect(file).NotTo(BeNil())
Expect(err).NotTo(HaveOccurred())

_, err = ioutil.ReadAll(file)
Expect(err).To(MatchError("File is write-only"))
})
})
})
})
})
Expand Down

0 comments on commit 727a09e

Please sign in to comment.