16a334020f
This change adds the ability to download byte ranges to uplinkng. Extended the uplinkng Filesystem interface with Stat method and an OpenOptions struct as parameter for the Open method. Also added a few tests for the ranged download Change-Id: I89a7276a75c51a4b22d7a450f15b3eb18ba838d4
203 lines
4.4 KiB
Go
203 lines
4.4 KiB
Go
// Copyright (C) 2021 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package ulfs
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/zeebo/errs"
|
|
|
|
"storj.io/storj/cmd/uplinkng/ulloc"
|
|
)
|
|
|
|
// Local implements something close to a filesystem but backed by the local disk.
|
|
type Local struct{}
|
|
|
|
// NewLocal constructs a Local filesystem.
|
|
func NewLocal() *Local {
|
|
return &Local{}
|
|
}
|
|
|
|
// Open returns a read ReadHandle for the given local path.
|
|
func (l *Local) Open(ctx context.Context, path string, opts *OpenOptions) (ReadHandle, error) {
|
|
fh, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
|
|
if opts != nil {
|
|
fr := io.NewSectionReader(fh, opts.Offset, opts.Length)
|
|
return newGenericReadHandle(fr), nil
|
|
}
|
|
|
|
return newOSReadHandle(fh)
|
|
}
|
|
|
|
// Create makes any directories necessary to create a file at path and returns a WriteHandle.
|
|
func (l *Local) Create(ctx context.Context, path string) (WriteHandle, error) {
|
|
fi, err := os.Stat(path)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return nil, errs.Wrap(err)
|
|
} else if err == nil && fi.IsDir() {
|
|
return nil, errs.New("path exists as a directory already")
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
|
|
// TODO: atomic rename
|
|
fh, err := os.Create(path)
|
|
if err != nil {
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
return newOSWriteHandle(fh), nil
|
|
}
|
|
|
|
// Remove unlinks the file at the path. It is not an error if the file does not exist.
|
|
func (l *Local) Remove(ctx context.Context, path string, opts *RemoveOptions) error {
|
|
if opts.isPending() {
|
|
return nil
|
|
}
|
|
|
|
if err := os.Remove(path); os.IsNotExist(err) {
|
|
return nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// List returns an ObjectIterator listing files and directories that have string prefix
|
|
// with the provided path.
|
|
func (l *Local) List(ctx context.Context, path string, opts *ListOptions) (ObjectIterator, error) {
|
|
if opts.isPending() {
|
|
return emptyObjectIterator{}, nil
|
|
}
|
|
|
|
prefix := path
|
|
if idx := strings.LastIndex(path, "/"); idx >= 0 {
|
|
prefix = path[:idx+1]
|
|
}
|
|
|
|
prefix, err := filepath.Abs(prefix)
|
|
if err != nil {
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
prefix += string(filepath.Separator)
|
|
|
|
var files []os.FileInfo
|
|
if opts.isRecursive() {
|
|
err = filepath.Walk(prefix, func(path string, info os.FileInfo, err error) error {
|
|
if err == nil && !info.IsDir() {
|
|
rel, err := filepath.Rel(prefix, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files = append(files, &namedFileInfo{
|
|
FileInfo: info,
|
|
name: rel,
|
|
})
|
|
}
|
|
return nil
|
|
})
|
|
} else {
|
|
files, err = ioutil.ReadDir(prefix)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sort.Slice(files, func(i, j int) bool {
|
|
if files[i].IsDir() && files[j].IsDir() {
|
|
return files[i].Name() < files[j].Name()
|
|
} else if files[i].IsDir() {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
})
|
|
|
|
var trim ulloc.Location
|
|
if !opts.isRecursive() {
|
|
trim = ulloc.NewLocal(prefix)
|
|
}
|
|
|
|
return &filteredObjectIterator{
|
|
trim: trim,
|
|
filter: ulloc.NewLocal(prefix),
|
|
iter: &fileinfoObjectIterator{
|
|
base: prefix,
|
|
files: files,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// IsLocalDir returns true if the path is a directory.
|
|
func (l *Local) IsLocalDir(ctx context.Context, path string) bool {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return fi.IsDir()
|
|
}
|
|
|
|
// Stat returns an ObjectInfo describing the provided path.
|
|
func (l *Local) Stat(ctx context.Context, path string) (*ObjectInfo, error) {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ObjectInfo{
|
|
Loc: ulloc.NewLocal(path),
|
|
Created: fi.ModTime(),
|
|
ContentLength: fi.Size(),
|
|
}, nil
|
|
}
|
|
|
|
type namedFileInfo struct {
|
|
os.FileInfo
|
|
name string
|
|
}
|
|
|
|
func (n *namedFileInfo) Name() string { return n.name }
|
|
|
|
type fileinfoObjectIterator struct {
|
|
base string
|
|
files []os.FileInfo
|
|
current os.FileInfo
|
|
}
|
|
|
|
func (fi *fileinfoObjectIterator) Next() bool {
|
|
if len(fi.files) == 0 {
|
|
return false
|
|
}
|
|
fi.current, fi.files = fi.files[0], fi.files[1:]
|
|
return true
|
|
}
|
|
|
|
func (fi *fileinfoObjectIterator) Err() error { return nil }
|
|
|
|
func (fi *fileinfoObjectIterator) Item() ObjectInfo {
|
|
name := filepath.Join(fi.base, fi.current.Name())
|
|
isDir := fi.current.IsDir()
|
|
if isDir {
|
|
name += string(filepath.Separator)
|
|
}
|
|
return ObjectInfo{
|
|
Loc: ulloc.NewLocal(name),
|
|
IsPrefix: isDir,
|
|
Created: fi.current.ModTime(), // TODO: use real crtime
|
|
ContentLength: fi.current.Size(),
|
|
}
|
|
}
|