package memory
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"gopkg.in/src-d/go-git.v4/utils/fs"
)
const separator = '/'
// Memory a very convenient filesystem based on memory files
type Memory struct {
base string
s *storage
tempCount int
}
//New returns a new Memory filesystem
func New() *Memory {
return &Memory{
base: "/",
s: &storage{make(map[string]*file, 0)},
}
}
func (fs *Memory) Create(filename string) (fs.File, error) {
return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0)
}
func (fs *Memory) Open(filename string) (fs.File, error) {
return fs.OpenFile(filename, os.O_RDONLY, 0)
}
func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (fs.File, error) {
fullpath := fs.Join(fs.base, filename)
f, ok := fs.s.files[fullpath]
if !ok && !isCreate(flag) {
return nil, os.ErrNotExist
}
if f == nil {
fs.s.files[fullpath] = newFile(fs.base, fullpath, flag)
return fs.s.files[fullpath], nil
}
n := newFile(fs.base, fullpath, flag)
n.content = f.content
if isAppend(flag) {
n.position = int64(n.content.Len())
}
if isTruncate(flag) {
n.content.Truncate()
}
return n, nil
}
func (fs *Memory) Stat(filename string) (fs.FileInfo, error) {
fullpath := fs.Join(fs.base, filename)
if _, ok := fs.s.files[filename]; ok {
return newFileInfo(fs.base, fullpath, fs.s.files[filename].content.Len()), nil
}
info, err := fs.ReadDir(filename)
if err == nil && len(info) != 0 {
return newFileInfo(fs.base, fullpath, len(info)), nil
}
return nil, os.ErrNotExist
}
func (fs *Memory) ReadDir(base string) (entries []fs.FileInfo, err error) {
base = fs.Join(fs.base, base)
appendedDirs := make(map[string]bool, 0)
for fullpath, f := range fs.s.files {
if !strings.HasPrefix(fullpath, base) {
continue
}
fullpath, _ = filepath.Rel(base, fullpath)
parts := strings.Split(fullpath, string(separator))
if len(parts) == 1 {
entries = append(entries, newFileInfo(fs.base, fullpath, f.content.Len()))
continue
}
if _, ok := appendedDirs[parts[0]]; ok {
continue
}
entries = append(entries, &fileInfo{name: parts[0], isDir: true})
appendedDirs[parts[0]] = true
}
return
}
var maxTempFiles = 1024 * 4
func (fs *Memory) TempFile(dir, prefix string) (fs.File, error) {
var fullpath string
for {
if fs.tempCount >= maxTempFiles {
return nil, errors.New("max. number of tempfiles reached")
}
fullpath = fs.getTempFilename(dir, prefix)
if _, ok := fs.s.files[fullpath]; !ok {
break
}
}
return fs.Create(fullpath)
}
func (fs *Memory) getTempFilename(dir, prefix string) string {
fs.tempCount++
filename := fmt.Sprintf("%s_%d_%d", prefix, fs.tempCount, time.Now().UnixNano())
return fs.Join(fs.base, dir, filename)
}
func (fs *Memory) Rename(from, to string) error {
from = fs.Join(fs.base, from)
to = fs.Join(fs.base, to)
if _, ok := fs.s.files[from]; !ok {
return os.ErrNotExist
}
fs.s.files[to] = fs.s.files[from]
fs.s.files[to].BaseFilename = to
delete(fs.s.files, from)
return nil
}
func (fs *Memory) Remove(filename string) error {
fullpath := fs.Join(fs.base, filename)
if _, ok := fs.s.files[fullpath]; !ok {
return os.ErrNotExist
}
delete(fs.s.files, fullpath)
return nil
}
func (fs *Memory) Join(elem ...string) string {
return filepath.Join(elem...)
}
func (fs *Memory) Dir(path string) fs.Filesystem {
return &Memory{
base: fs.Join(fs.base, path),
s: fs.s,
}
}
func (fs *Memory) Base() string {
return fs.base
}
type file struct {
fs.BaseFile
content *content
position int64
flag int
}
func newFile(base, fullpath string, flag int) *file {
filename, _ := filepath.Rel(base, fullpath)
return &file{
BaseFile: fs.BaseFile{BaseFilename: filename},
content: &content{},
flag: flag,
}
}
func (f *file) Read(b []byte) (int, error) {
if f.IsClosed() {
return 0, fs.ErrClosed
}
if !isReadAndWrite(f.flag) && !isReadOnly(f.flag) {
return 0, errors.New("read not supported")
}
n, err := f.content.ReadAt(b, f.position)
f.position += int64(n)
return n, err
}
func (f *file) Seek(offset int64, whence int) (int64, error) {
if f.IsClosed() {
return 0, fs.ErrClosed
}
switch whence {
case io.SeekCurrent:
f.position += offset
case io.SeekStart:
f.position = offset
case io.SeekEnd:
f.position = int64(f.content.Len()) - offset
}
return f.position, nil
}
func (f *file) Write(p []byte) (int, error) {
if f.IsClosed() {
return 0, fs.ErrClosed
}
if !isReadAndWrite(f.flag) && !isWriteOnly(f.flag) {
return 0, errors.New("write not supported")
}
n, err := f.content.WriteAt(p, f.position)
f.position += int64(n)
return n, err
}
func (f *file) Close() error {
if f.IsClosed() {
return errors.New("file already closed")
}
f.Closed = true
return nil
}
func (f *file) Open() error {
f.Closed = false
return nil
}
type fileInfo struct {
name string
size int
isDir bool
}
func newFileInfo(base, fullpath string, size int) *fileInfo {
filename, _ := filepath.Rel(base, fullpath)
return &fileInfo{
name: filename,
size: size,
}
}
func (fi *fileInfo) Name() string {
return fi.name
}
func (fi *fileInfo) Size() int64 {
return int64(fi.size)
}
func (fi *fileInfo) Mode() os.FileMode {
return os.FileMode(0)
}
func (*fileInfo) ModTime() time.Time {
return time.Now()
}
func (fi *fileInfo) IsDir() bool {
return fi.isDir
}
func (*fileInfo) Sys() interface{} {
return nil
}
type storage struct {
files map[string]*file
}
type content struct {
bytes []byte
}
func (c *content) WriteAt(p []byte, off int64) (int, error) {
prev := len(c.bytes)
c.bytes = append(c.bytes[:off], p...)
if len(c.bytes) < prev {
c.bytes = c.bytes[:prev]
}
return len(p), nil
}
func (c *content) ReadAt(b []byte, off int64) (int, error) {
size := int64(len(c.bytes))
if off >= size {
return 0, io.EOF
}
l := int64(len(b))
if off+l > size {
l = size - off
}
n := copy(b, c.bytes[off:off+l])
return n, nil
}
func (c *content) Truncate() {
c.bytes = make([]byte, 0)
}
func (c *content) Len() int {
return len(c.bytes)
}
func isCreate(flag int) bool {
return flag&os.O_CREATE != 0
}
func isAppend(flag int) bool {
return flag&os.O_APPEND != 0
}
func isTruncate(flag int) bool {
return flag&os.O_TRUNC != 0
}
func isReadAndWrite(flag int) bool {
return flag&os.O_RDWR != 0
}
func isReadOnly(flag int) bool {
return flag == os.O_RDONLY
}
func isWriteOnly(flag int) bool {
return flag&os.O_WRONLY != 0
}