aboutsummaryrefslogtreecommitdiff
path: root/internal/fusefrontend/file_allocate_truncate.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/fusefrontend/file_allocate_truncate.go')
-rw-r--r--internal/fusefrontend/file_allocate_truncate.go159
1 files changed, 122 insertions, 37 deletions
diff --git a/internal/fusefrontend/file_allocate_truncate.go b/internal/fusefrontend/file_allocate_truncate.go
index 5be7df4..65d6df6 100644
--- a/internal/fusefrontend/file_allocate_truncate.go
+++ b/internal/fusefrontend/file_allocate_truncate.go
@@ -4,6 +4,7 @@ package fusefrontend
// i.e. ftruncate and fallocate
import (
+ "log"
"sync"
"syscall"
@@ -12,17 +13,78 @@ import (
"github.com/rfjakob/gocryptfs/internal/tlog"
)
+const FALLOC_DEFAULT = 0x00
+const FALLOC_FL_KEEP_SIZE = 0x01
+
// Only warn once
var allocateWarnOnce sync.Once
-// Allocate - FUSE call, fallocate(2)
-// This is not implemented yet in gocryptfs, but it is neither in EncFS. This
-// suggests that the user demand is low.
+// Allocate - FUSE call for fallocate(2)
+//
+// mode=FALLOC_FL_KEEP_SIZE is implemented directly.
+//
+// mode=FALLOC_DEFAULT is implemented as a two-step process:
+//
+// (1) Allocate the space using FALLOC_FL_KEEP_SIZE
+// (2) Set the file size using ftruncate (via truncateGrowFile)
+//
+// This allows us to reuse the file grow mechanics from Truncate as they are
+// complicated and hard to get right.
+//
+// Other modes (hole punching, zeroing) are not supported.
func (f *file) Allocate(off uint64, sz uint64, mode uint32) fuse.Status {
- allocateWarnOnce.Do(func() {
- tlog.Warn.Printf("fallocate(2) is not supported, returning ENOSYS - see https://github.com/rfjakob/gocryptfs/issues/1")
- })
- return fuse.ENOSYS
+ if mode != FALLOC_DEFAULT && mode != FALLOC_FL_KEEP_SIZE {
+ f := func() {
+ tlog.Warn.Print("fallocate: only mode 0 (default) and 1 (keep size) are supported")
+ }
+ allocateWarnOnce.Do(f)
+ return fuse.Status(syscall.EOPNOTSUPP)
+ }
+
+ f.fdLock.RLock()
+ defer f.fdLock.RUnlock()
+ if f.released {
+ return fuse.EBADF
+ }
+ wlock.lock(f.ino)
+ defer wlock.unlock(f.ino)
+
+ blocks := f.contentEnc.ExplodePlainRange(off, sz)
+ firstBlock := blocks[0]
+ lastBlock := blocks[len(blocks)-1]
+
+ // Step (1): Allocate the space the user wants using FALLOC_FL_KEEP_SIZE.
+ // This will fill file holes and/or allocate additional space past the end of
+ // the file.
+ cipherOff := firstBlock.BlockCipherOff()
+ cipherSz := lastBlock.BlockCipherOff() - cipherOff +
+ f.contentEnc.PlainSizeToCipherSize(lastBlock.Skip+lastBlock.Length)
+ err := syscall.Fallocate(f.intFd(), FALLOC_FL_KEEP_SIZE, int64(cipherOff), int64(cipherSz))
+ tlog.Debug.Printf("Allocate off=%d sz=%d mode=%x cipherOff=%d cipherSz=%d\n",
+ off, sz, mode, cipherOff, cipherSz)
+ if err != nil {
+ return fuse.ToStatus(err)
+ }
+ if mode == FALLOC_FL_KEEP_SIZE {
+ // The user did not want to change the apparent size. We are done.
+ return fuse.OK
+ }
+ // Step (2): Grow the apparent file size
+ // We need the old file size to determine if we are growing the file at all.
+ newPlainSz := off + sz
+ oldPlainSz, err := f.statPlainSize()
+ if err != nil {
+ return fuse.ToStatus(err)
+ }
+ if newPlainSz <= oldPlainSz {
+ // The new size is smaller (or equal). Fallocate with mode = 0 never
+ // truncates a file, so we are done.
+ return fuse.OK
+ }
+ // The file grows. The space has already been allocated in (1), so what is
+ // left to do is to pad the first and last block and call truncate.
+ // truncateGrowFile does just that.
+ return f.truncateGrowFile(oldPlainSz, newPlainSz)
}
// Truncate - FUSE call
@@ -50,13 +112,10 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
}
// We need the old file size to determine if we are growing or shrinking
// the file
- fi, err := f.fd.Stat()
+ oldSize, err := f.statPlainSize()
if err != nil {
- tlog.Warn.Printf("ino%d fh%d: Truncate: Fstat failed: %v", f.ino, f.intFd(), err)
return fuse.ToStatus(err)
- }
- oldSize := f.contentEnc.CipherSizeToPlainSize(uint64(fi.Size()))
- {
+ } else {
oldB := float32(oldSize) / float32(f.contentEnc.PlainBS())
newB := float32(newSize) / float32(f.contentEnc.PlainBS())
tlog.Debug.Printf("ino%d: FUSE Truncate from %.2f to %.2f blocks (%d to %d bytes)", f.ino, oldB, newB, oldSize, newSize)
@@ -67,31 +126,7 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
}
// File grows
if newSize > oldSize {
- // File was empty, create new header
- if oldSize == 0 {
- err = f.createHeader()
- if err != nil {
- return fuse.ToStatus(err)
- }
- }
- // New blocks to add
- addBlocks := f.contentEnc.ExplodePlainRange(oldSize, newSize-oldSize)
- if len(addBlocks) >= 2 {
- f.zeroPad(oldSize)
- }
- lastBlock := addBlocks[len(addBlocks)-1]
- if lastBlock.IsPartial() {
- off := lastBlock.BlockPlainOff()
- _, status := f.doWrite(make([]byte, lastBlock.Length), int64(off+lastBlock.Skip))
- return status
- } else {
- off := lastBlock.BlockCipherOff()
- err = syscall.Ftruncate(f.intFd(), int64(off+f.contentEnc.CipherBS()))
- if err != nil {
- tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err)
- }
- return fuse.ToStatus(err)
- }
+ return f.truncateGrowFile(oldSize, newSize)
} else {
// File shrinks
blockNo := f.contentEnc.PlainOffToBlockNo(newSize)
@@ -121,3 +156,53 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
return fuse.OK
}
}
+
+// statPlainSize stats the file and returns the plaintext size
+func (f *file) statPlainSize() (uint64, error) {
+ fi, err := f.fd.Stat()
+ if err != nil {
+ tlog.Warn.Printf("ino%d fh%d: statPlainSize: %v", f.ino, f.intFd(), err)
+ return 0, err
+ }
+ cipherSz := uint64(fi.Size())
+ plainSz := uint64(f.contentEnc.CipherSizeToPlainSize(cipherSz))
+ return plainSz, nil
+}
+
+// truncateGrowFile extends a file using seeking or ftruncate performing RMW on
+// the first and last block as neccessary. New blocks in the middle become
+// file holes unless they have been fallocate()'d beforehand.
+func (f *file) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) fuse.Status {
+ if newPlainSz <= oldPlainSz {
+ log.Panicf("BUG: newSize=%d <= oldSize=%d", newPlainSz, oldPlainSz)
+ }
+ var err error
+ // File was empty, create new header
+ if oldPlainSz == 0 {
+ err = f.createHeader()
+ if err != nil {
+ return fuse.ToStatus(err)
+ }
+ }
+ // New blocks to add
+ addBlocks := f.contentEnc.ExplodePlainRange(oldPlainSz, newPlainSz-oldPlainSz)
+ if oldPlainSz > 0 && len(addBlocks) >= 2 {
+ // Zero-pad the first block (unless the first block is also the last block)
+ f.zeroPad(oldPlainSz)
+ }
+ lastBlock := addBlocks[len(addBlocks)-1]
+ if lastBlock.IsPartial() {
+ // Write at the new end of the file. The seek implicitly grows the file
+ // (creates a file hole) and doWrite() takes care of RMW.
+ off := lastBlock.BlockPlainOff()
+ _, status := f.doWrite(make([]byte, lastBlock.Length), int64(off+lastBlock.Skip))
+ return status
+ } else {
+ off := lastBlock.BlockCipherOff()
+ err = syscall.Ftruncate(f.intFd(), int64(off+f.contentEnc.CipherBS()))
+ if err != nil {
+ tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err)
+ }
+ return fuse.ToStatus(err)
+ }
+}