├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── dir.go ├── encode.go ├── encode_test.go ├── file.go ├── fs.go ├── go.mod ├── go.sum ├── main.go ├── mount.go └── mount_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go filter=gofmt 2 | *.cgo filter=gofmt 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bolt-mount 2 | *.test 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2019 Tommi Virtanen. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mount a Bolt database as a FUSE filesystem 2 | 3 | [Bolt](https://github.com/boltdb/bolt) is a key-value store that also 4 | supports nested buckets. This makes it look a little bit like a file 5 | system tree. 6 | 7 | `bolt-mount` exposes a Bolt database as a FUSE file system. 8 | 9 | ``` console 10 | $ go get bazil.org/bolt-mount 11 | # assuming $GOPATH/bin is in $PATH 12 | $ mkdir mnt 13 | $ bolt-mount mydatabase.bolt mnt & 14 | $ cd mnt 15 | $ mkdir bucket 16 | $ mkdir bucket/sub 17 | $ echo Hello, world >bucket/sub/greeting 18 | $ ls -l bucket 19 | total 0 20 | drwxr-xr-x 1 root root 0 Apr 25 18:00 sub/ 21 | $ ls -l bucket/sub 22 | total 0 23 | -rw-r--r-- 1 root root 0 Apr 25 18:00 greeting 24 | $ cat bucket/sub/greeting 25 | Hello, world 26 | $ cd .. 27 | # for Linux 28 | $ fusermount -u mnt 29 | # for OS X 30 | $ umount mnt 31 | [1]+ Done bolt-mount mydatabase.bolt mnt 32 | ``` 33 | 34 | ## Encoding keys to file names 35 | 36 | As Bolt keys can contain arbitrary bytes, but file names cannot, the 37 | keys are encoded. 38 | 39 | First, we define *safe* as: 40 | 41 | - ASCII letters and numbers 42 | - the characters ".", "," "-", "_" (period/dot, comma, dash, underscore) 43 | 44 | A name consisting completely of *safe* characters, and not starting 45 | with a dot, is unaltered. Everything else is hex-encoded. Hex encoding 46 | looks like `@xx[xx..]` where `xx` are lower case hex digits. 47 | 48 | Additionally, any *safe* prefixes (not starting with a dot) and 49 | suffixes longer than than a noise threshold remain unaltered. They are 50 | separated from the hex encoded middle part by a semicolon, as in 51 | `[PREFIX:]MIDDLE[:SUFFIX]`. 52 | 53 | For example: 54 | 55 | A Bolt key packing two little-endian `uint16` values 42 and 10000 and the string 56 | "test" is encoded as filename `@002a2710:test`. 57 | -------------------------------------------------------------------------------- /dir.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "bazil.org/fuse" 8 | "bazil.org/fuse/fs" 9 | "github.com/boltdb/bolt" 10 | "golang.org/x/net/context" 11 | ) 12 | 13 | type Dir struct { 14 | fs *FS 15 | // path from Bolt database root to this bucket; empty for root bucket 16 | buckets [][]byte 17 | } 18 | 19 | var _ = fs.Node(&Dir{}) 20 | 21 | func (d *Dir) Attr(ctx context.Context, a *fuse.Attr) error { 22 | a.Mode = os.ModeDir | 0755 23 | return nil 24 | } 25 | 26 | var _ = fs.HandleReadDirAller(&Dir{}) 27 | 28 | type BucketLike interface { 29 | Bucket(name []byte) *bolt.Bucket 30 | CreateBucket(name []byte) (*bolt.Bucket, error) 31 | DeleteBucket(name []byte) error 32 | Cursor() *bolt.Cursor 33 | Get(key []byte) []byte 34 | Put(key []byte, value []byte) error 35 | Delete(key []byte) error 36 | } 37 | 38 | // root bucket is special because it cannot contain keys, and doesn't 39 | // really have a *bolt.Bucket exposed in the Bolt API. 40 | type fakeBucket struct { 41 | *bolt.Tx 42 | } 43 | 44 | func (fakeBucket) Get(key []byte) []byte { 45 | return nil 46 | } 47 | 48 | func (fakeBucket) Put(key []byte, value []byte) error { 49 | return fuse.EPERM 50 | } 51 | 52 | func (fakeBucket) Delete(key []byte) error { 53 | return fuse.EPERM 54 | } 55 | 56 | // bucket returns a BucketLike value (either a *bolt.Bucket or a 57 | // *bolt.Tx wrapped in a fakeBucket, to provide Get/Put/Delete 58 | // methods). 59 | // 60 | // It never returns a nil value in a non-nil interface. 61 | func (d *Dir) bucket(tx *bolt.Tx) BucketLike { 62 | if len(d.buckets) == 0 { 63 | return fakeBucket{tx} 64 | } 65 | b := tx.Bucket(d.buckets[0]) 66 | if b == nil { 67 | return nil 68 | } 69 | for _, name := range d.buckets[1:] { 70 | b = b.Bucket(name) 71 | if b == nil { 72 | return nil 73 | } 74 | } 75 | return b 76 | } 77 | 78 | func (d *Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { 79 | var res []fuse.Dirent 80 | err := d.fs.db.View(func(tx *bolt.Tx) error { 81 | b := d.bucket(tx) 82 | if b == nil { 83 | return errors.New("bucket no longer exists") 84 | } 85 | c := b.Cursor() 86 | for k, v := c.First(); k != nil; k, v = c.Next() { 87 | de := fuse.Dirent{ 88 | Name: EncodeKey(k), 89 | } 90 | if v == nil { 91 | de.Type = fuse.DT_Dir 92 | } else { 93 | de.Type = fuse.DT_File 94 | } 95 | res = append(res, de) 96 | } 97 | return nil 98 | }) 99 | return res, err 100 | } 101 | 102 | var _ = fs.NodeStringLookuper(&Dir{}) 103 | 104 | func (d *Dir) Lookup(ctx context.Context, name string) (fs.Node, error) { 105 | var n fs.Node 106 | err := d.fs.db.View(func(tx *bolt.Tx) error { 107 | b := d.bucket(tx) 108 | if b == nil { 109 | return errors.New("bucket no longer exists") 110 | } 111 | nameRaw, err := DecodeKey(name) 112 | if err != nil { 113 | return fuse.ENOENT 114 | } 115 | if child := b.Bucket(nameRaw); child != nil { 116 | // directory 117 | buckets := make([][]byte, 0, len(d.buckets)+1) 118 | buckets = append(buckets, d.buckets...) 119 | buckets = append(buckets, nameRaw) 120 | n = &Dir{ 121 | fs: d.fs, 122 | buckets: buckets, 123 | } 124 | return nil 125 | } 126 | if child := b.Get(nameRaw); child != nil { 127 | // file 128 | n = &File{ 129 | dir: d, 130 | name: nameRaw, 131 | } 132 | return nil 133 | } 134 | return fuse.ENOENT 135 | }) 136 | if err != nil { 137 | return nil, err 138 | } 139 | return n, nil 140 | } 141 | 142 | var _ = fs.NodeMkdirer(&Dir{}) 143 | 144 | func (d *Dir) Mkdir(ctx context.Context, req *fuse.MkdirRequest) (fs.Node, error) { 145 | name, err := DecodeKey(req.Name) 146 | if err != nil { 147 | return nil, fuse.EPERM 148 | } 149 | err = d.fs.db.Update(func(tx *bolt.Tx) error { 150 | b := d.bucket(tx) 151 | if b == nil { 152 | return errors.New("bucket no longer exists") 153 | } 154 | if child := b.Bucket(name); child != nil { 155 | return fuse.EEXIST 156 | } 157 | if _, err := b.CreateBucket(name); err != nil { 158 | return err 159 | } 160 | return nil 161 | }) 162 | if err != nil { 163 | return nil, err 164 | } 165 | buckets := make([][]byte, 0, len(d.buckets)+1) 166 | buckets = append(buckets, d.buckets...) 167 | buckets = append(buckets, name) 168 | n := &Dir{ 169 | fs: d.fs, 170 | buckets: buckets, 171 | } 172 | return n, nil 173 | } 174 | 175 | var _ = fs.NodeCreater(&Dir{}) 176 | 177 | func (d *Dir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) { 178 | if len(d.buckets) == 0 { 179 | // only buckets go in root bucket 180 | return nil, nil, fuse.EPERM 181 | } 182 | nameRaw, err := DecodeKey(req.Name) 183 | if err != nil { 184 | return nil, nil, fuse.EPERM 185 | } 186 | f := &File{ 187 | dir: d, 188 | name: nameRaw, 189 | writers: 1, 190 | // file is empty at Create time, no need to set data 191 | } 192 | return f, f, nil 193 | } 194 | 195 | var _ = fs.NodeRemover(&Dir{}) 196 | 197 | func (d *Dir) Remove(ctx context.Context, req *fuse.RemoveRequest) error { 198 | nameRaw, err := DecodeKey(req.Name) 199 | if err != nil { 200 | return fuse.ENOENT 201 | } 202 | fn := func(tx *bolt.Tx) error { 203 | b := d.bucket(tx) 204 | if b == nil { 205 | return errors.New("bucket no longer exists") 206 | } 207 | 208 | switch req.Dir { 209 | case true: 210 | if b.Bucket(nameRaw) == nil { 211 | return fuse.ENOENT 212 | } 213 | if err := b.DeleteBucket(nameRaw); err != nil { 214 | return err 215 | } 216 | 217 | case false: 218 | if b.Get(nameRaw) == nil { 219 | return fuse.ENOENT 220 | } 221 | if err := b.Delete(nameRaw); err != nil { 222 | return err 223 | } 224 | } 225 | return nil 226 | } 227 | return d.fs.db.Update(fn) 228 | } 229 | -------------------------------------------------------------------------------- /encode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | "strings" 8 | "unicode" 9 | ) 10 | 11 | const FragSeparator = ':' 12 | 13 | func DecodeKey(quoted string) ([]byte, error) { 14 | var key []byte 15 | for _, frag := range strings.Split(quoted, string(FragSeparator)) { 16 | if frag == "" { 17 | return nil, fmt.Errorf("quoted key cannot have empty fragment: %s", quoted) 18 | } 19 | switch { 20 | case strings.HasPrefix(frag, "@"): 21 | f, err := hex.DecodeString(frag[1:]) 22 | if err != nil { 23 | return nil, err 24 | } 25 | key = append(key, f...) 26 | default: 27 | key = append(key, frag...) 28 | } 29 | } 30 | return key, nil 31 | } 32 | 33 | func isSafe(r rune) bool { 34 | if r > unicode.MaxASCII { 35 | return false 36 | } 37 | if unicode.IsLetter(r) || unicode.IsNumber(r) { 38 | return true 39 | } 40 | switch r { 41 | case FragSeparator: 42 | return false 43 | case '.', ',', '-', '_': 44 | return true 45 | } 46 | return false 47 | } 48 | 49 | const prettyTheshold = 2 50 | 51 | func EncodeKey(key []byte) string { 52 | // we do sloppy work and process safe bytes only at the beginning 53 | // and end; this avoids many false positives in large binary data 54 | 55 | var left, right []byte 56 | var middle string 57 | 58 | if key[0] != '.' { 59 | mid := bytes.TrimLeftFunc(key, isSafe) 60 | if len(key)-len(mid) > prettyTheshold { 61 | left = key[:len(key)-len(mid)] 62 | key = mid 63 | } 64 | } 65 | 66 | { 67 | mid := bytes.TrimRightFunc(key, isSafe) 68 | if len(mid) == 0 && len(key) > 0 && key[0] == '.' { 69 | // don't let right safe zone reach all the way to leading dot 70 | mid = key[:1] 71 | } 72 | if len(key)-len(mid) > prettyTheshold { 73 | right = key[len(mid):] 74 | key = mid 75 | } 76 | } 77 | 78 | if len(key) > 0 { 79 | middle = "@" + hex.EncodeToString(key) 80 | } 81 | 82 | return strings.Trim( 83 | string(left)+string(FragSeparator)+middle+string(FragSeparator)+string(right), 84 | string(FragSeparator), 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /encode_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | func ExampleEncodeKey() { 11 | type T struct { 12 | X uint16 13 | Y uint16 14 | } 15 | var buf bytes.Buffer 16 | if err := binary.Write(&buf, binary.BigEndian, T{X: 42, Y: 10000}); err != nil { 17 | fmt.Printf("error: %v", err) 18 | return 19 | } 20 | buf.WriteString("test") 21 | key := buf.Bytes() 22 | filename := EncodeKey(key) 23 | fmt.Println(filename) 24 | // Output: 25 | // @002a2710:test 26 | } 27 | 28 | func TestEncodeKeyLeadingDot(t *testing.T) { 29 | key := []byte(".evil") 30 | filename := EncodeKey(key) 31 | if g, e := filename, "@2e:evil"; g != e { 32 | t.Errorf("leading dot not encoded: %q != %q", g, e) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "syscall" 7 | 8 | "bazil.org/fuse" 9 | "bazil.org/fuse/fs" 10 | "bazil.org/fuse/fuseutil" 11 | "github.com/boltdb/bolt" 12 | "golang.org/x/net/context" 13 | ) 14 | 15 | type File struct { 16 | dir *Dir 17 | name []byte 18 | 19 | mu sync.Mutex 20 | // number of write-capable handles currently open 21 | writers uint 22 | // only valid if writers > 0 23 | data []byte 24 | } 25 | 26 | var _ = fs.Node(&File{}) 27 | var _ = fs.Handle(&File{}) 28 | 29 | // load calls fn inside a View with the contents of the file. Caller 30 | // must make a copy of the data if needed, because once we're out of 31 | // the transaction, bolt might reuse the db page. 32 | func (f *File) load(fn func([]byte)) error { 33 | err := f.dir.fs.db.View(func(tx *bolt.Tx) error { 34 | b := f.dir.bucket(tx) 35 | if b == nil { 36 | return errors.New("bucket no longer exists") 37 | } 38 | v := b.Get(f.name) 39 | if v == nil { 40 | return fuse.ESTALE 41 | } 42 | fn(v) 43 | return nil 44 | }) 45 | return err 46 | } 47 | 48 | func (f *File) Attr(ctx context.Context, a *fuse.Attr) error { 49 | f.mu.Lock() 50 | defer f.mu.Unlock() 51 | 52 | a.Mode = 0644 53 | a.Size = uint64(len(f.data)) 54 | if f.writers == 0 { 55 | // not in memory, fetch correct size. 56 | // Attr can't fail, so ignore errors 57 | _ = f.load(func(b []byte) { a.Size = uint64(len(b)) }) 58 | } 59 | return nil 60 | } 61 | 62 | var _ = fs.NodeOpener(&File{}) 63 | 64 | func (f *File) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) { 65 | if req.Flags.IsReadOnly() { 66 | // we don't need to track read-only handles 67 | return f, nil 68 | } 69 | 70 | f.mu.Lock() 71 | defer f.mu.Unlock() 72 | 73 | if f.writers == 0 { 74 | // load data 75 | fn := func(b []byte) { 76 | f.data = append([]byte(nil), b...) 77 | } 78 | if err := f.load(fn); err != nil { 79 | return nil, err 80 | } 81 | } 82 | 83 | f.writers++ 84 | return f, nil 85 | } 86 | 87 | var _ = fs.HandleReleaser(&File{}) 88 | 89 | func (f *File) Release(ctx context.Context, req *fuse.ReleaseRequest) error { 90 | if req.Flags.IsReadOnly() { 91 | // we don't need to track read-only handles 92 | return nil 93 | } 94 | 95 | f.mu.Lock() 96 | defer f.mu.Unlock() 97 | 98 | f.writers-- 99 | if f.writers == 0 { 100 | f.data = nil 101 | } 102 | return nil 103 | } 104 | 105 | var _ = fs.HandleReader(&File{}) 106 | 107 | func (f *File) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { 108 | f.mu.Lock() 109 | defer f.mu.Unlock() 110 | 111 | fn := func(b []byte) { 112 | fuseutil.HandleRead(req, resp, b) 113 | } 114 | if f.writers == 0 { 115 | f.load(fn) 116 | } else { 117 | fn(f.data) 118 | } 119 | return nil 120 | } 121 | 122 | var _ = fs.HandleWriter(&File{}) 123 | 124 | const maxInt = int(^uint(0) >> 1) 125 | 126 | func (f *File) Write(ctx context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) error { 127 | f.mu.Lock() 128 | defer f.mu.Unlock() 129 | 130 | // expand the buffer if necessary 131 | newLen := req.Offset + int64(len(req.Data)) 132 | if newLen > int64(maxInt) { 133 | return fuse.Errno(syscall.EFBIG) 134 | } 135 | if newLen := int(newLen); newLen > len(f.data) { 136 | f.data = append(f.data, make([]byte, newLen-len(f.data))...) 137 | } 138 | 139 | n := copy(f.data[req.Offset:], req.Data) 140 | resp.Size = n 141 | return nil 142 | } 143 | 144 | var _ = fs.HandleFlusher(&File{}) 145 | 146 | func (f *File) Flush(ctx context.Context, req *fuse.FlushRequest) error { 147 | f.mu.Lock() 148 | defer f.mu.Unlock() 149 | 150 | if f.writers == 0 { 151 | // Read-only handles also get flushes. Make sure we don't 152 | // overwrite valid file contents with a nil buffer. 153 | return nil 154 | } 155 | 156 | err := f.dir.fs.db.Update(func(tx *bolt.Tx) error { 157 | b := f.dir.bucket(tx) 158 | if b == nil { 159 | return fuse.ESTALE 160 | } 161 | return b.Put(f.name, f.data) 162 | }) 163 | if err != nil { 164 | return err 165 | } 166 | return nil 167 | } 168 | 169 | var _ = fs.NodeSetattrer(&File{}) 170 | 171 | func (f *File) Setattr(ctx context.Context, req *fuse.SetattrRequest, resp *fuse.SetattrResponse) error { 172 | f.mu.Lock() 173 | defer f.mu.Unlock() 174 | 175 | if req.Valid.Size() { 176 | if req.Size > uint64(maxInt) { 177 | return fuse.Errno(syscall.EFBIG) 178 | } 179 | newLen := int(req.Size) 180 | switch { 181 | case newLen > len(f.data): 182 | f.data = append(f.data, make([]byte, newLen-len(f.data))...) 183 | case newLen < len(f.data): 184 | f.data = f.data[:newLen] 185 | } 186 | } 187 | return nil 188 | } 189 | -------------------------------------------------------------------------------- /fs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bazil.org/fuse/fs" 5 | "github.com/boltdb/bolt" 6 | ) 7 | 8 | type FS struct { 9 | db *bolt.DB 10 | } 11 | 12 | var _ = fs.FS(&FS{}) 13 | 14 | func (f *FS) Root() (fs.Node, error) { 15 | n := &Dir{ 16 | fs: f, 17 | } 18 | return n, nil 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module bazil.org/bolt-mount 2 | 3 | go 1.13 4 | 5 | require ( 6 | bazil.org/fuse v0.0.0-20191221031930-2713cb0db94b 7 | github.com/boltdb/bolt v1.3.1 8 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | bazil.org/fuse v0.0.0-20191221031930-2713cb0db94b h1:/XFU3ufWdGEfQCKvQBzyhhOpf7KdFHbEyRXA8fHSbZY= 2 | bazil.org/fuse v0.0.0-20191221031930-2713cb0db94b/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM= 3 | github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= 4 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 5 | github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ= 6 | github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= 7 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 8 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= 9 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 10 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 11 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 12 | golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ= 13 | golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | var progName = filepath.Base(os.Args[0]) 12 | 13 | func usage() { 14 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", progName) 15 | fmt.Fprintf(os.Stderr, " %s DBPATH MOUNTPOINT\n", progName) 16 | fmt.Fprintf(os.Stderr, "\n") 17 | fmt.Fprintf(os.Stderr, " DBPATH will be created if it does not exist.\n") 18 | fmt.Fprintf(os.Stderr, "\n") 19 | flag.PrintDefaults() 20 | } 21 | 22 | func main() { 23 | log.SetFlags(0) 24 | log.SetPrefix(progName + ": ") 25 | 26 | flag.Usage = usage 27 | flag.Parse() 28 | 29 | if flag.NArg() != 2 { 30 | usage() 31 | os.Exit(2) 32 | } 33 | 34 | err := mount(flag.Arg(0), flag.Arg(1)) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /mount.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bazil.org/fuse" 5 | "bazil.org/fuse/fs" 6 | "github.com/boltdb/bolt" 7 | ) 8 | 9 | func mount(dbpath, mountpoint string) error { 10 | db, err := bolt.Open(dbpath, 0600, nil) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | c, err := fuse.Mount(mountpoint) 16 | if err != nil { 17 | return err 18 | } 19 | defer c.Close() 20 | 21 | filesys := &FS{ 22 | db: db, 23 | } 24 | if err := fs.Serve(c, filesys); err != nil { 25 | return err 26 | } 27 | 28 | // check if the mount process has an error to report 29 | <-c.Ready 30 | if err := c.MountError; err != nil { 31 | return err 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /mount_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "bazil.org/fuse/fs/fstestutil" 10 | "github.com/boltdb/bolt" 11 | ) 12 | 13 | func withDB(t testing.TB, fn func(*bolt.DB)) { 14 | tmp, err := ioutil.TempFile("", "bolt-mount-test-") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | dbpath := tmp.Name() 19 | defer func() { 20 | _ = os.Remove(dbpath) 21 | }() 22 | _ = tmp.Close() 23 | db, err := bolt.Open(dbpath, 0600, nil) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | fn(db) 28 | } 29 | 30 | func withMount(t testing.TB, db *bolt.DB, fn func(mntpath string)) { 31 | filesys := &FS{ 32 | db: db, 33 | } 34 | mnt, err := fstestutil.MountedT(t, filesys, nil) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | defer mnt.Close() 39 | fn(mnt.Dir) 40 | } 41 | 42 | type fileInfo struct { 43 | name string 44 | size int64 45 | mode os.FileMode 46 | } 47 | 48 | func checkFI(t testing.TB, got os.FileInfo, expected fileInfo) { 49 | if g, e := got.Name(), expected.name; g != e { 50 | t.Errorf("file info has bad name: %q != %q", g, e) 51 | } 52 | if g, e := got.Size(), expected.size; g != e { 53 | t.Errorf("file info has bad size: %v != %v", g, e) 54 | } 55 | if g, e := got.Mode(), expected.mode; g != e { 56 | t.Errorf("file info has bad mode: %v != %v", g, e) 57 | } 58 | } 59 | 60 | func TestRootReaddir(t *testing.T) { 61 | withDB(t, func(db *bolt.DB) { 62 | err := db.Update(func(tx *bolt.Tx) error { 63 | if _, err := tx.CreateBucket([]byte("one")); err != nil { 64 | return err 65 | } 66 | if _, err := tx.CreateBucket([]byte("two")); err != nil { 67 | return err 68 | } 69 | return nil 70 | }) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | withMount(t, db, func(mntpath string) { 75 | fis, err := ioutil.ReadDir(mntpath) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | if g, e := len(fis), 2; g != e { 80 | t.Fatalf("wrong readdir results: got %v", fis) 81 | } 82 | checkFI(t, fis[0], fileInfo{name: "one", size: 0, mode: 0755 | os.ModeDir}) 83 | checkFI(t, fis[1], fileInfo{name: "two", size: 0, mode: 0755 | os.ModeDir}) 84 | }) 85 | }) 86 | } 87 | 88 | func TestRootMkdir(t *testing.T) { 89 | withDB(t, func(db *bolt.DB) { 90 | withMount(t, db, func(mntpath string) { 91 | if err := os.Mkdir(filepath.Join(mntpath, "one"), 0700); err != nil { 92 | t.Error(err) 93 | } 94 | }) 95 | check := func(tx *bolt.Tx) error { 96 | b := tx.Bucket([]byte("one")) 97 | if b == nil { 98 | t.Error("expected to see bucket 'one'") 99 | } 100 | return nil 101 | } 102 | if err := db.View(check); err != nil { 103 | t.Fatal(err) 104 | } 105 | }) 106 | } 107 | 108 | func TestRootRmdir(t *testing.T) { 109 | withDB(t, func(db *bolt.DB) { 110 | prep := func(tx *bolt.Tx) error { 111 | if _, err := tx.CreateBucket([]byte("one")); err != nil { 112 | return err 113 | } 114 | return nil 115 | } 116 | if err := db.Update(prep); err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | withMount(t, db, func(mntpath string) { 121 | if err := os.Remove(filepath.Join(mntpath, "one")); err != nil { 122 | t.Error(err) 123 | } 124 | }) 125 | check := func(tx *bolt.Tx) error { 126 | b := tx.Bucket([]byte("one")) 127 | if b != nil { 128 | t.Error("bucket 'one' is still there") 129 | } 130 | return nil 131 | } 132 | if err := db.View(check); err != nil { 133 | t.Fatal(err) 134 | } 135 | }) 136 | } 137 | 138 | func TestBucketReaddir(t *testing.T) { 139 | withDB(t, func(db *bolt.DB) { 140 | prep := func(tx *bolt.Tx) error { 141 | b, err := tx.CreateBucket([]byte("bukkit")) 142 | if err != nil { 143 | return err 144 | } 145 | if _, err := b.CreateBucket([]byte("one")); err != nil { 146 | return err 147 | } 148 | if err := b.Put([]byte("two"), []byte("hello")); err != nil { 149 | return err 150 | } 151 | return nil 152 | } 153 | if err := db.Update(prep); err != nil { 154 | t.Fatal(err) 155 | } 156 | withMount(t, db, func(mntpath string) { 157 | fis, err := ioutil.ReadDir(filepath.Join(mntpath, "bukkit")) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | if g, e := len(fis), 2; g != e { 162 | t.Fatalf("wrong readdir results: got %v", fis) 163 | } 164 | checkFI(t, fis[0], fileInfo{name: "one", size: 0, mode: 0755 | os.ModeDir}) 165 | checkFI(t, fis[1], fileInfo{name: "two", size: 5, mode: 0644}) 166 | }) 167 | }) 168 | } 169 | 170 | func TestBucketMkdir(t *testing.T) { 171 | withDB(t, func(db *bolt.DB) { 172 | prep := func(tx *bolt.Tx) error { 173 | _, err := tx.CreateBucket([]byte("bukkit")) 174 | if err != nil { 175 | return err 176 | } 177 | return nil 178 | } 179 | if err := db.Update(prep); err != nil { 180 | t.Fatal(err) 181 | } 182 | withMount(t, db, func(mntpath string) { 183 | if err := os.Mkdir(filepath.Join(mntpath, "bukkit", "sub"), 0700); err != nil { 184 | t.Error(err) 185 | } 186 | }) 187 | check := func(tx *bolt.Tx) error { 188 | b := tx.Bucket([]byte("bukkit")) 189 | if b == nil { 190 | t.Error("expected to see bucket 'bukkit'") 191 | } 192 | b = b.Bucket([]byte("sub")) 193 | if b == nil { 194 | t.Error("expected to see bucket 'bukkit' 'sub'") 195 | } 196 | return nil 197 | } 198 | if err := db.View(check); err != nil { 199 | t.Fatal(err) 200 | } 201 | }) 202 | } 203 | 204 | func TestBucketRmdir(t *testing.T) { 205 | withDB(t, func(db *bolt.DB) { 206 | prep := func(tx *bolt.Tx) error { 207 | b, err := tx.CreateBucket([]byte("bukkit")) 208 | if err != nil { 209 | return err 210 | } 211 | if _, err := b.CreateBucket([]byte("sub")); err != nil { 212 | return err 213 | } 214 | return nil 215 | } 216 | if err := db.Update(prep); err != nil { 217 | t.Fatal(err) 218 | } 219 | withMount(t, db, func(mntpath string) { 220 | if err := os.Remove(filepath.Join(mntpath, "bukkit", "sub")); err != nil { 221 | t.Error(err) 222 | } 223 | }) 224 | check := func(tx *bolt.Tx) error { 225 | b := tx.Bucket([]byte("bukkit")) 226 | if b == nil { 227 | t.Error("expected to see bucket 'bukkit'") 228 | } 229 | b = b.Bucket([]byte("sub")) 230 | if b != nil { 231 | t.Error("bucket 'one' is still there") 232 | } 233 | return nil 234 | } 235 | if err := db.View(check); err != nil { 236 | t.Fatal(err) 237 | } 238 | }) 239 | } 240 | 241 | func TestRead(t *testing.T) { 242 | withDB(t, func(db *bolt.DB) { 243 | prep := func(tx *bolt.Tx) error { 244 | b, err := tx.CreateBucket([]byte("bukkit")) 245 | if err != nil { 246 | return err 247 | } 248 | if err := b.Put([]byte("greeting"), []byte("hello")); err != nil { 249 | return err 250 | } 251 | return nil 252 | } 253 | if err := db.Update(prep); err != nil { 254 | t.Fatal(err) 255 | } 256 | withMount(t, db, func(mntpath string) { 257 | data, err := ioutil.ReadFile(filepath.Join(mntpath, "bukkit", "greeting")) 258 | if err != nil { 259 | t.Fatal(err) 260 | } 261 | if g, e := string(data), "hello"; g != e { 262 | t.Fatalf("wrong read results: %q != %q", g, e) 263 | } 264 | }) 265 | }) 266 | } 267 | 268 | func TestWrite(t *testing.T) { 269 | withDB(t, func(db *bolt.DB) { 270 | prep := func(tx *bolt.Tx) error { 271 | _, err := tx.CreateBucket([]byte("bukkit")) 272 | if err != nil { 273 | return err 274 | } 275 | return nil 276 | } 277 | if err := db.Update(prep); err != nil { 278 | t.Fatal(err) 279 | } 280 | withMount(t, db, func(mntpath string) { 281 | if err := ioutil.WriteFile( 282 | filepath.Join(mntpath, "bukkit", "greeting"), 283 | []byte("hello"), 284 | 0600, 285 | ); err != nil { 286 | t.Fatal(err) 287 | } 288 | }) 289 | check := func(tx *bolt.Tx) error { 290 | b := tx.Bucket([]byte("bukkit")) 291 | if b == nil { 292 | t.Fatalf("bukkit disappeared") 293 | } 294 | v := b.Get([]byte("greeting")) 295 | if g, e := string(v), "hello"; g != e { 296 | t.Fatalf("wrong write content: %q != %q", g, e) 297 | } 298 | return nil 299 | } 300 | if err := db.View(check); err != nil { 301 | t.Fatal(err) 302 | } 303 | }) 304 | } 305 | 306 | func TestReadNested(t *testing.T) { 307 | withDB(t, func(db *bolt.DB) { 308 | prep := func(tx *bolt.Tx) error { 309 | b, err := tx.CreateBucket([]byte("bukkit")) 310 | if err != nil { 311 | return err 312 | } 313 | b, err = b.CreateBucket([]byte("sub")) 314 | if err != nil { 315 | return err 316 | } 317 | if err := b.Put([]byte("greeting"), []byte("hello")); err != nil { 318 | return err 319 | } 320 | return nil 321 | } 322 | if err := db.Update(prep); err != nil { 323 | t.Fatal(err) 324 | } 325 | withMount(t, db, func(mntpath string) { 326 | data, err := ioutil.ReadFile(filepath.Join(mntpath, "bukkit", "sub", "greeting")) 327 | if err != nil { 328 | t.Fatal(err) 329 | } 330 | if g, e := string(data), "hello"; g != e { 331 | t.Fatalf("wrong read results: %q != %q", g, e) 332 | } 333 | }) 334 | }) 335 | } 336 | 337 | func TestWriteNested(t *testing.T) { 338 | withDB(t, func(db *bolt.DB) { 339 | withMount(t, db, func(mntpath string) { 340 | if err := os.Mkdir(filepath.Join(mntpath, "bukkit"), 0755); err != nil { 341 | t.Fatal(err) 342 | } 343 | if err := os.Mkdir(filepath.Join(mntpath, "bukkit", "sub"), 0755); err != nil { 344 | t.Fatal(err) 345 | } 346 | if err := ioutil.WriteFile( 347 | filepath.Join(mntpath, "bukkit", "sub", "greeting"), 348 | []byte("hello"), 349 | 0600, 350 | ); err != nil { 351 | t.Fatal(err) 352 | } 353 | }) 354 | check := func(tx *bolt.Tx) error { 355 | b := tx.Bucket([]byte("bukkit")) 356 | if b == nil { 357 | t.Fatalf("bukkit disappeared") 358 | } 359 | b = b.Bucket([]byte("sub")) 360 | if b == nil { 361 | t.Fatalf("sub-bukkit disappeared") 362 | } 363 | v := b.Get([]byte("greeting")) 364 | if g, e := string(v), "hello"; g != e { 365 | t.Fatalf("wrong write content: %q != %q", g, e) 366 | } 367 | return nil 368 | } 369 | if err := db.View(check); err != nil { 370 | t.Fatal(err) 371 | } 372 | }) 373 | } 374 | 375 | func TestRemove(t *testing.T) { 376 | withDB(t, func(db *bolt.DB) { 377 | prep := func(tx *bolt.Tx) error { 378 | b, err := tx.CreateBucket([]byte("bukkit")) 379 | if err != nil { 380 | return err 381 | } 382 | if err := b.Put([]byte("greeting"), []byte("hello")); err != nil { 383 | return err 384 | } 385 | return nil 386 | } 387 | if err := db.Update(prep); err != nil { 388 | t.Fatal(err) 389 | } 390 | withMount(t, db, func(mntpath string) { 391 | if err := os.Remove(filepath.Join(mntpath, "bukkit", "greeting")); err != nil { 392 | t.Fatal(err) 393 | } 394 | }) 395 | check := func(tx *bolt.Tx) error { 396 | b := tx.Bucket([]byte("bukkit")) 397 | if b == nil { 398 | t.Fatalf("bukkit disappeared") 399 | } 400 | v := b.Get([]byte("greeting")) 401 | if v != nil { 402 | t.Errorf("greeting is still there: %q", v) 403 | } 404 | return nil 405 | } 406 | if err := db.View(check); err != nil { 407 | t.Fatal(err) 408 | } 409 | }) 410 | } 411 | 412 | func TestBinary(t *testing.T) { 413 | withDB(t, func(db *bolt.DB) { 414 | err := db.Update(func(tx *bolt.Tx) error { 415 | b, err := tx.CreateBucket([]byte("evil\x00lol/mwahaha")) 416 | if err != nil { 417 | return err 418 | } 419 | if err := b.Put([]byte("\x01\x02foobar"), []byte("hello")); err != nil { 420 | return err 421 | } 422 | return nil 423 | }) 424 | if err != nil { 425 | t.Fatal(err) 426 | } 427 | withMount(t, db, func(mntpath string) { 428 | fis, err := ioutil.ReadDir(mntpath) 429 | if err != nil { 430 | t.Fatal(err) 431 | } 432 | if g, e := len(fis), 1; g != e { 433 | t.Fatalf("wrong readdir results: got %v", fis) 434 | } 435 | checkFI(t, fis[0], fileInfo{name: "evil:@006c6f6c2f:mwahaha", size: 0, mode: 0755 | os.ModeDir}) 436 | 437 | fis, err = ioutil.ReadDir(filepath.Join(mntpath, "evil:@006c6f6c2f:mwahaha")) 438 | if err != nil { 439 | t.Fatal(err) 440 | } 441 | if g, e := len(fis), 1; g != e { 442 | t.Fatalf("wrong readdir results: got %v", fis) 443 | } 444 | checkFI(t, fis[0], fileInfo{name: "@0102:foobar", size: 5, mode: 0644}) 445 | }) 446 | }) 447 | } 448 | 449 | func TestRoundtrip(t *testing.T) { 450 | withDB(t, func(db *bolt.DB) { 451 | prep := func(tx *bolt.Tx) error { 452 | _, err := tx.CreateBucket([]byte("bukkit")) 453 | if err != nil { 454 | return err 455 | } 456 | return nil 457 | } 458 | if err := db.Update(prep); err != nil { 459 | t.Fatal(err) 460 | } 461 | withMount(t, db, func(mntpath string) { 462 | if err := ioutil.WriteFile( 463 | filepath.Join(mntpath, "bukkit", "greeting"), 464 | []byte("hello"), 465 | 0600, 466 | ); err != nil { 467 | t.Fatal(err) 468 | } 469 | 470 | data, err := ioutil.ReadFile(filepath.Join(mntpath, "bukkit", "greeting")) 471 | if err != nil { 472 | t.Fatal(err) 473 | } 474 | if g, e := string(data), "hello"; g != e { 475 | t.Fatalf("wrong read results: %q != %q", g, e) 476 | } 477 | }) 478 | }) 479 | } 480 | 481 | func TestUnifiedBuffer(t *testing.T) { 482 | withDB(t, func(db *bolt.DB) { 483 | prep := func(tx *bolt.Tx) error { 484 | _, err := tx.CreateBucket([]byte("bukkit")) 485 | if err != nil { 486 | return err 487 | } 488 | return nil 489 | } 490 | if err := db.Update(prep); err != nil { 491 | t.Fatal(err) 492 | } 493 | withMount(t, db, func(mntpath string) { 494 | f, err := os.Create(filepath.Join(mntpath, "bukkit", "greeting")) 495 | if err != nil { 496 | t.Fatal(err) 497 | } 498 | defer f.Close() 499 | if _, err := f.Write([]byte("hello")); err != nil { 500 | t.Fatal(err) 501 | } 502 | data, err := ioutil.ReadFile(filepath.Join(mntpath, "bukkit", "greeting")) 503 | if err != nil { 504 | t.Fatal(err) 505 | } 506 | if g, e := string(data), "hello"; g != e { 507 | t.Fatalf("wrong read results: %q != %q", g, e) 508 | } 509 | }) 510 | }) 511 | } 512 | 513 | func TestReadTwice(t *testing.T) { 514 | // catches a bug where Flush on a read-only handle emptied the 515 | // file 516 | withDB(t, func(db *bolt.DB) { 517 | prep := func(tx *bolt.Tx) error { 518 | b, err := tx.CreateBucket([]byte("bukkit")) 519 | if err != nil { 520 | return err 521 | } 522 | if err := b.Put([]byte("greeting"), []byte("hello")); err != nil { 523 | return err 524 | } 525 | return nil 526 | } 527 | if err := db.Update(prep); err != nil { 528 | t.Fatal(err) 529 | } 530 | withMount(t, db, func(mntpath string) { 531 | data, err := ioutil.ReadFile(filepath.Join(mntpath, "bukkit", "greeting")) 532 | if err != nil { 533 | t.Fatal(err) 534 | } 535 | if g, e := string(data), "hello"; g != e { 536 | t.Fatalf("wrong read results: %q != %q", g, e) 537 | } 538 | }) 539 | check := func(tx *bolt.Tx) error { 540 | b := tx.Bucket([]byte("bukkit")) 541 | if b == nil { 542 | t.Fatalf("bukkit disappeared") 543 | } 544 | v := b.Get([]byte("greeting")) 545 | if g, e := string(v), "hello"; g != e { 546 | t.Fatalf("wrong write content: %q != %q", g, e) 547 | } 548 | return nil 549 | } 550 | if err := db.View(check); err != nil { 551 | t.Fatal(err) 552 | } 553 | }) 554 | } 555 | 556 | func TestSeekAndWrite(t *testing.T) { 557 | withDB(t, func(db *bolt.DB) { 558 | prep := func(tx *bolt.Tx) error { 559 | _, err := tx.CreateBucket([]byte("bukkit")) 560 | if err != nil { 561 | return err 562 | } 563 | return nil 564 | } 565 | if err := db.Update(prep); err != nil { 566 | t.Fatal(err) 567 | } 568 | withMount(t, db, func(mntpath string) { 569 | f, err := os.Create( 570 | filepath.Join(mntpath, "bukkit", "greeting"), 571 | ) 572 | if err != nil { 573 | t.Fatal(err) 574 | } 575 | defer f.Close() 576 | n, err := f.WriteAt([]byte("offset"), 3) 577 | if err != nil { 578 | t.Fatal(err) 579 | } 580 | if g, e := n, 6; g != e { 581 | t.Errorf("bad write length: %d != %d", g, e) 582 | } 583 | }) 584 | check := func(tx *bolt.Tx) error { 585 | b := tx.Bucket([]byte("bukkit")) 586 | if b == nil { 587 | t.Fatalf("bukkit disappeared") 588 | } 589 | v := b.Get([]byte("greeting")) 590 | if g, e := string(v), "\x00\x00\x00offset"; g != e { 591 | t.Fatalf("wrong write content: %q != %q", g, e) 592 | } 593 | return nil 594 | } 595 | if err := db.View(check); err != nil { 596 | t.Fatal(err) 597 | } 598 | }) 599 | } 600 | 601 | func TestTruncate(t *testing.T) { 602 | withDB(t, func(db *bolt.DB) { 603 | prep := func(tx *bolt.Tx) error { 604 | _, err := tx.CreateBucket([]byte("bukkit")) 605 | if err != nil { 606 | return err 607 | } 608 | return nil 609 | } 610 | if err := db.Update(prep); err != nil { 611 | t.Fatal(err) 612 | } 613 | withMount(t, db, func(mntpath string) { 614 | f, err := os.Create( 615 | filepath.Join(mntpath, "bukkit", "greeting"), 616 | ) 617 | if err != nil { 618 | t.Fatal(err) 619 | } 620 | defer f.Close() 621 | if err := f.Truncate(3); err != nil { 622 | t.Fatal(err) 623 | } 624 | }) 625 | check := func(tx *bolt.Tx) error { 626 | b := tx.Bucket([]byte("bukkit")) 627 | if b == nil { 628 | t.Fatalf("bukkit disappeared") 629 | } 630 | v := b.Get([]byte("greeting")) 631 | if g, e := string(v), "\x00\x00\x00"; g != e { 632 | t.Fatalf("wrong write content: %q != %q", g, e) 633 | } 634 | return nil 635 | } 636 | if err := db.View(check); err != nil { 637 | t.Fatal(err) 638 | } 639 | }) 640 | } 641 | --------------------------------------------------------------------------------