2019-01-24 20:15:10 +00:00
|
|
|
// Copyright (C) 2019 Storj Labs, Inc.
|
2018-09-28 07:59:27 +01:00
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
|
|
|
package filestore
|
|
|
|
|
|
|
|
import (
|
2019-06-05 14:06:06 +01:00
|
|
|
"context"
|
2019-03-11 08:06:56 +00:00
|
|
|
"encoding/base32"
|
2018-09-28 07:59:27 +01:00
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"math"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"sync"
|
|
|
|
|
2018-12-21 10:54:20 +00:00
|
|
|
"github.com/zeebo/errs"
|
|
|
|
|
2018-09-28 07:59:27 +01:00
|
|
|
"storj.io/storj/storage"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
blobPermission = 0600
|
|
|
|
dirPermission = 0700
|
|
|
|
)
|
|
|
|
|
2019-03-11 08:06:56 +00:00
|
|
|
var pathEncoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding)
|
|
|
|
|
2018-09-28 07:59:27 +01:00
|
|
|
// Dir represents single folder for storing blobs
|
|
|
|
type Dir struct {
|
|
|
|
path string
|
|
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
deleteQueue []string
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewDir returns folder for storing blobs
|
|
|
|
func NewDir(path string) (*Dir, error) {
|
|
|
|
dir := &Dir{
|
|
|
|
path: path,
|
|
|
|
}
|
|
|
|
|
2018-12-21 10:54:20 +00:00
|
|
|
return dir, errs.Combine(
|
2019-07-16 17:31:29 +01:00
|
|
|
os.MkdirAll(dir.blobsdir(), dirPermission),
|
2018-09-28 07:59:27 +01:00
|
|
|
os.MkdirAll(dir.tempdir(), dirPermission),
|
2019-07-16 17:31:29 +01:00
|
|
|
os.MkdirAll(dir.garbagedir(), dirPermission),
|
2018-09-28 07:59:27 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Path returns the directory path
|
|
|
|
func (dir *Dir) Path() string { return dir.path }
|
|
|
|
|
2019-07-16 17:31:29 +01:00
|
|
|
func (dir *Dir) blobsdir() string { return filepath.Join(dir.path, "blobs") }
|
|
|
|
func (dir *Dir) tempdir() string { return filepath.Join(dir.path, "temp") }
|
|
|
|
func (dir *Dir) garbagedir() string { return filepath.Join(dir.path, "garbage") }
|
2018-09-28 07:59:27 +01:00
|
|
|
|
|
|
|
// CreateTemporaryFile creates a preallocated temporary file in the temp directory
|
|
|
|
// prealloc preallocates file to make writing faster
|
2019-06-05 14:06:06 +01:00
|
|
|
func (dir *Dir) CreateTemporaryFile(ctx context.Context, prealloc int64) (_ *os.File, err error) {
|
2018-09-28 07:59:27 +01:00
|
|
|
const preallocLimit = 5 << 20 // 5 MB
|
|
|
|
if prealloc > preallocLimit {
|
|
|
|
prealloc = preallocLimit
|
|
|
|
}
|
|
|
|
|
|
|
|
file, err := ioutil.TempFile(dir.tempdir(), "blob-*.partial")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if prealloc >= 0 {
|
|
|
|
if err := file.Truncate(prealloc); err != nil {
|
2018-12-21 10:54:20 +00:00
|
|
|
return nil, errs.Combine(err, file.Close())
|
2018-09-28 07:59:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return file, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteTemporary deletes a temporary file
|
2019-06-05 14:06:06 +01:00
|
|
|
func (dir *Dir) DeleteTemporary(ctx context.Context, file *os.File) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2018-09-28 07:59:27 +01:00
|
|
|
closeErr := file.Close()
|
2018-12-21 10:54:20 +00:00
|
|
|
return errs.Combine(closeErr, os.Remove(file.Name()))
|
2018-09-28 07:59:27 +01:00
|
|
|
}
|
|
|
|
|
2019-03-11 08:06:56 +00:00
|
|
|
// blobToPath converts blob reference to a filepath in permanent storage
|
|
|
|
func (dir *Dir) blobToPath(ref storage.BlobRef) (string, error) {
|
|
|
|
if !ref.IsValid() {
|
|
|
|
return "", storage.ErrInvalidBlobRef.New("")
|
|
|
|
}
|
|
|
|
|
|
|
|
namespace := pathEncoding.EncodeToString(ref.Namespace)
|
|
|
|
key := pathEncoding.EncodeToString(ref.Key)
|
|
|
|
if len(key) < 3 {
|
|
|
|
// ensure we always have at least
|
|
|
|
key = "11" + key
|
|
|
|
}
|
2019-07-16 17:31:29 +01:00
|
|
|
return filepath.Join(dir.blobsdir(), namespace, key[:2], key[2:]), nil
|
2019-03-11 08:06:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// blobToTrashPath converts blob reference to a filepath in transient storage
|
|
|
|
// the files in trash are deleted in an interval (in case the initial deletion didn't work for some reason)
|
|
|
|
func (dir *Dir) blobToTrashPath(ref storage.BlobRef) string {
|
2019-07-16 17:31:29 +01:00
|
|
|
var name []byte
|
2019-03-11 08:06:56 +00:00
|
|
|
name = append(name, ref.Namespace...)
|
|
|
|
name = append(name, ref.Key...)
|
2019-07-16 17:31:29 +01:00
|
|
|
return filepath.Join(dir.garbagedir(), pathEncoding.EncodeToString(name))
|
2018-09-28 07:59:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Commit commits temporary file to the permanent storage
|
2019-06-05 14:06:06 +01:00
|
|
|
func (dir *Dir) Commit(ctx context.Context, file *os.File, ref storage.BlobRef) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2018-09-28 07:59:27 +01:00
|
|
|
position, seekErr := file.Seek(0, io.SeekCurrent)
|
|
|
|
truncErr := file.Truncate(position)
|
|
|
|
syncErr := file.Sync()
|
|
|
|
chmodErr := os.Chmod(file.Name(), blobPermission)
|
|
|
|
closeErr := file.Close()
|
|
|
|
|
|
|
|
if seekErr != nil || truncErr != nil || syncErr != nil || chmodErr != nil || closeErr != nil {
|
|
|
|
removeErr := os.Remove(file.Name())
|
2018-12-21 10:54:20 +00:00
|
|
|
return errs.Combine(seekErr, truncErr, syncErr, chmodErr, closeErr, removeErr)
|
2018-09-28 07:59:27 +01:00
|
|
|
}
|
|
|
|
|
2019-03-11 08:06:56 +00:00
|
|
|
path, err := dir.blobToPath(ref)
|
|
|
|
if err != nil {
|
|
|
|
removeErr := os.Remove(file.Name())
|
|
|
|
return errs.Combine(err, removeErr)
|
|
|
|
}
|
|
|
|
|
2018-09-28 07:59:27 +01:00
|
|
|
mkdirErr := os.MkdirAll(filepath.Dir(path), dirPermission)
|
|
|
|
if os.IsExist(mkdirErr) {
|
|
|
|
mkdirErr = nil
|
|
|
|
}
|
2019-03-11 08:06:56 +00:00
|
|
|
|
2018-09-28 07:59:27 +01:00
|
|
|
if mkdirErr != nil {
|
|
|
|
removeErr := os.Remove(file.Name())
|
2018-12-21 10:54:20 +00:00
|
|
|
return errs.Combine(mkdirErr, removeErr)
|
2018-09-28 07:59:27 +01:00
|
|
|
}
|
|
|
|
|
2019-03-11 08:06:56 +00:00
|
|
|
renameErr := rename(file.Name(), path)
|
2018-09-28 07:59:27 +01:00
|
|
|
if renameErr != nil {
|
|
|
|
removeErr := os.Remove(file.Name())
|
2018-12-21 10:54:20 +00:00
|
|
|
return errs.Combine(renameErr, removeErr)
|
2018-09-28 07:59:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Open opens the file with the specified ref
|
2019-06-05 14:06:06 +01:00
|
|
|
func (dir *Dir) Open(ctx context.Context, ref storage.BlobRef) (_ *os.File, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-03-11 08:06:56 +00:00
|
|
|
path, err := dir.blobToPath(ref)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-03-18 10:55:06 +00:00
|
|
|
file, err := openFileReadOnly(path, blobPermission)
|
|
|
|
if err != nil {
|
2019-06-03 10:17:09 +01:00
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-03-18 10:55:06 +00:00
|
|
|
return nil, Error.New("unable to open %q: %v", path, err)
|
|
|
|
}
|
|
|
|
return file, nil
|
2018-09-28 07:59:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Delete deletes file with the specified ref
|
2019-06-05 14:06:06 +01:00
|
|
|
func (dir *Dir) Delete(ctx context.Context, ref storage.BlobRef) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-03-11 08:06:56 +00:00
|
|
|
path, err := dir.blobToPath(ref)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
trashPath := dir.blobToTrashPath(ref)
|
2018-09-28 07:59:27 +01:00
|
|
|
|
|
|
|
// move to trash folder, this is allowed for some OS-es
|
2019-03-11 08:06:56 +00:00
|
|
|
moveErr := rename(path, trashPath)
|
2018-09-28 07:59:27 +01:00
|
|
|
|
|
|
|
// ignore concurrent delete
|
|
|
|
if os.IsNotExist(moveErr) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if moveErr != nil {
|
|
|
|
trashPath = path
|
|
|
|
}
|
|
|
|
|
|
|
|
// try removing the file
|
2019-03-11 08:06:56 +00:00
|
|
|
err = os.Remove(trashPath)
|
2018-09-28 07:59:27 +01:00
|
|
|
|
|
|
|
// ignore concurrent deletes
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// this may fail, because someone might be still reading it
|
|
|
|
if err != nil {
|
|
|
|
dir.mu.Lock()
|
|
|
|
dir.deleteQueue = append(dir.deleteQueue, trashPath)
|
|
|
|
dir.mu.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
// ignore is busy errors, they are still in the queue
|
|
|
|
// but no need to notify
|
|
|
|
if isBusy(err) {
|
|
|
|
err = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// GarbageCollect collects files that are pending deletion
|
2019-06-05 14:06:06 +01:00
|
|
|
func (dir *Dir) GarbageCollect(ctx context.Context) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2018-09-28 07:59:27 +01:00
|
|
|
offset := int(math.MaxInt32)
|
|
|
|
// limited deletion loop to avoid blocking `Delete` for too long
|
|
|
|
for offset >= 0 {
|
|
|
|
dir.mu.Lock()
|
|
|
|
limit := 100
|
|
|
|
if offset >= len(dir.deleteQueue) {
|
|
|
|
offset = len(dir.deleteQueue) - 1
|
|
|
|
}
|
|
|
|
for offset >= 0 && limit > 0 {
|
|
|
|
path := dir.deleteQueue[offset]
|
|
|
|
err := os.Remove(path)
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
err = nil
|
|
|
|
}
|
|
|
|
if err == nil {
|
|
|
|
dir.deleteQueue = append(dir.deleteQueue[:offset], dir.deleteQueue[offset+1:]...)
|
|
|
|
}
|
|
|
|
|
|
|
|
offset--
|
|
|
|
limit--
|
|
|
|
}
|
|
|
|
dir.mu.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove anything left in the trashdir
|
2019-07-16 17:31:29 +01:00
|
|
|
_ = removeAllContent(ctx, dir.garbagedir())
|
2018-09-28 07:59:27 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// removeAllContent deletes everything in the folder
|
2019-06-05 14:06:06 +01:00
|
|
|
func removeAllContent(ctx context.Context, path string) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2018-09-28 07:59:27 +01:00
|
|
|
dir, err := os.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
files, err := dir.Readdirnames(100)
|
|
|
|
for _, file := range files {
|
|
|
|
// the file might be still in use, so ignore the error
|
|
|
|
_ = os.RemoveAll(filepath.Join(path, file))
|
|
|
|
}
|
|
|
|
if err == io.EOF || len(files) == 0 {
|
|
|
|
return dir.Close()
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// DiskInfo contains statistics about this dir
|
|
|
|
type DiskInfo struct {
|
|
|
|
ID string
|
|
|
|
AvailableSpace int64
|
|
|
|
}
|
|
|
|
|
|
|
|
// Info returns information about the current state of the dir
|
|
|
|
func (dir *Dir) Info() (DiskInfo, error) {
|
2019-03-18 10:55:06 +00:00
|
|
|
path, err := filepath.Abs(dir.path)
|
|
|
|
if err != nil {
|
|
|
|
return DiskInfo{}, err
|
|
|
|
}
|
|
|
|
return diskInfoFromPath(path)
|
2018-09-28 07:59:27 +01:00
|
|
|
}
|