1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
|
package dynamicfs
import (
"context"
"fmt"
"io"
"log"
"os"
"strings"
"sync"
"time"
"github.com/jacobsa/fuse"
"github.com/jacobsa/fuse/fuseops"
"github.com/jacobsa/fuse/fuseutil"
"github.com/jacobsa/timeutil"
)
// Create a file system that contains 2 files (`age` and `weekday`) and no
// directories. Every time the `age` file is opened, its contents are refreshed
// to show the number of seconds elapsed since the file system was created (as
// opposed to mounted). Every time the `weekday` file is opened, its contents
// are refreshed to reflect the current weekday.
//
// The contents of both of these files is updated within the filesystem itself,
// i.e., these changes do not go through the kernel. Additionally, file access
// times are not updated and file size is not known in advance and is set to 0.
// This simulates a filesystem that is backed by a dynamic data source where
// file metadata is not necessarily known before the file is read. For example,
// a filesystem backed by an expensive RPC or by a stream that's generated on
// the fly might not know data size ahead of time.
//
// This implementation depends on direct IO in fuse. Without it, all read
// operations are suppressed because the kernel detects that they read beyond
// the end of the files.
func NewDynamicFS(clock timeutil.Clock) (fuse.Server, error) {
createTime := clock.Now()
fs := &dynamicFS{
clock: clock,
createTime: createTime,
fileHandles: make(map[fuseops.HandleID]string),
}
return fuseutil.NewFileSystemServer(fs), nil
}
type dynamicFS struct {
fuseutil.NotImplementedFileSystem
mu sync.Mutex
clock timeutil.Clock
createTime time.Time
nextHandle fuseops.HandleID
fileHandles map[fuseops.HandleID]string
}
const (
rootInode fuseops.InodeID = fuseops.RootInodeID + iota
ageInode
weekdayInode
)
type inodeInfo struct {
attributes fuseops.InodeAttributes
// File or directory?
dir bool
// For directories, children.
children []fuseutil.Dirent
}
// We have a fixed directory structure.
var gInodeInfo = map[fuseops.InodeID]inodeInfo{
// root
rootInode: {
attributes: fuseops.InodeAttributes{
Nlink: 1,
Mode: 0555 | os.ModeDir,
},
dir: true,
children: []fuseutil.Dirent{
{
Offset: 1,
Inode: ageInode,
Name: "age",
Type: fuseutil.DT_File,
},
{
Offset: 2,
Inode: weekdayInode,
Name: "weekday",
Type: fuseutil.DT_File,
},
},
},
// age
ageInode: {
attributes: fuseops.InodeAttributes{
Nlink: 1,
Mode: 0444,
},
},
// weekday
weekdayInode: {
attributes: fuseops.InodeAttributes{
Nlink: 1,
Mode: 0444,
// Size left at 0.
},
},
}
func findChildInode(
name string,
children []fuseutil.Dirent) (fuseops.InodeID, error) {
for _, e := range children {
if e.Name == name {
return e.Inode, nil
}
}
return 0, fuse.ENOENT
}
func (fs *dynamicFS) findUnusedHandle() fuseops.HandleID {
// TODO: Mutex annotation?
handle := fs.nextHandle
for _, exists := fs.fileHandles[handle]; exists; _, exists = fs.fileHandles[handle] {
handle++
}
fs.nextHandle = handle + 1
return handle
}
func (fs *dynamicFS) GetInodeAttributes(
ctx context.Context,
op *fuseops.GetInodeAttributesOp) error {
// Find the info for this inode.
info, ok := gInodeInfo[op.Inode]
if !ok {
return fuse.ENOENT
}
// Copy over its attributes.
op.Attributes = info.attributes
return nil
}
func (fs *dynamicFS) LookUpInode(
ctx context.Context,
op *fuseops.LookUpInodeOp) error {
// Find the info for the parent.
parentInfo, ok := gInodeInfo[op.Parent]
if !ok {
return fuse.ENOENT
}
// Find the child within the parent.
childInode, err := findChildInode(op.Name, parentInfo.children)
if err != nil {
return err
}
// Copy over information.
op.Entry.Child = childInode
op.Entry.Attributes = gInodeInfo[childInode].attributes
return nil
}
func (fs *dynamicFS) OpenDir(
ctx context.Context,
op *fuseops.OpenDirOp) error {
// Allow opening directory.
return nil
}
func (fs *dynamicFS) ReadDir(
ctx context.Context,
op *fuseops.ReadDirOp) error {
// Find the info for this inode.
info, ok := gInodeInfo[op.Inode]
if !ok {
return fuse.ENOENT
}
if !info.dir {
return fuse.EIO
}
entries := info.children
// Grab the range of interest.
if op.Offset > fuseops.DirOffset(len(entries)) {
return fuse.EIO
}
entries = entries[op.Offset:]
// Resume at the specified offset into the array.
for _, e := range entries {
n := fuseutil.WriteDirent(op.Dst[op.BytesRead:], e)
if n == 0 {
break
}
op.BytesRead += n
}
return nil
}
func (fs *dynamicFS) OpenFile(
ctx context.Context,
op *fuseops.OpenFileOp) error {
fs.mu.Lock()
defer fs.mu.Unlock()
var contents string
// Update file contents on (and only on) open.
switch op.Inode {
case ageInode:
now := fs.clock.Now()
ageInSeconds := int(now.Sub(fs.createTime).Seconds())
contents = fmt.Sprintf("This filesystem is %d seconds old.", ageInSeconds)
case weekdayInode:
contents = fmt.Sprintf("Today is %s.", fs.clock.Now().Weekday())
default:
return fuse.EINVAL
}
handle := fs.findUnusedHandle()
fs.fileHandles[handle] = contents
op.UseDirectIO = true
op.Handle = handle
return nil
}
func (fs *dynamicFS) ReadFile(
ctx context.Context,
op *fuseops.ReadFileOp) error {
fs.mu.Lock()
defer fs.mu.Unlock()
contents, ok := fs.fileHandles[op.Handle]
if !ok {
log.Printf("ReadFile: no open file handle: %d", op.Handle)
return fuse.EIO
}
reader := strings.NewReader(contents)
var err error
op.BytesRead, err = reader.ReadAt(op.Dst, op.Offset)
if err == io.EOF {
return nil
}
return err
}
func (fs *dynamicFS) ReleaseFileHandle(
ctx context.Context,
op *fuseops.ReleaseFileHandleOp) error {
fs.mu.Lock()
defer fs.mu.Unlock()
_, ok := fs.fileHandles[op.Handle]
if !ok {
log.Printf("ReleaseFileHandle: bad handle: %d", op.Handle)
return fuse.EIO
}
delete(fs.fileHandles, op.Handle)
return nil
}
func (fs *dynamicFS) StatFS(ctx context.Context,
op *fuseops.StatFSOp) error {
return nil
}
|