forked from Elara6331/itd
		
	Added FUSE support (#55)
This exposes the watches' file system over FUSE. This way, we can access files on the watch without having to go through `itctl` or developing 3rd party tools. **Features** - [x] read/write access to the file system - [x] read access to momentary sensor data - [x] live access to sensor data (i.e. WatchMotion rather than Motion) - [x] configuration of mount point Co-authored-by: Yannick Ulrich <yannick.ulrich@durham.ac.uk> Reviewed-on: https://gitea.arsenm.dev/Arsen6331/itd/pulls/55 Co-authored-by: yannickulrich <yannick.ulrich@protonmail.com> Co-committed-by: yannickulrich <yannick.ulrich@protonmail.com>
This commit is contained in:
		| @@ -106,5 +106,8 @@ func setCfgDefaults() { | |||||||
| 		"notifs.ignore.body":    []string{}, | 		"notifs.ignore.body":    []string{}, | ||||||
|  |  | ||||||
| 		"music.vol.interval": 5, | 		"music.vol.interval": 5, | ||||||
|  |  | ||||||
|  | 		"fuse.enabled": false, | ||||||
|  | 		"fuse.mountpoint": "/tmp/itd/mnt", | ||||||
| 	}, "."), nil) | 	}, "."), nil) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										60
									
								
								fuse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								fuse.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | package main | ||||||
|  | import ( | ||||||
|  | 	"go.arsenm.dev/itd/internal/fusefs" | ||||||
|  | 	"os" | ||||||
|  | 	"github.com/hanwen/go-fuse/v2/fs" | ||||||
|  | 	"github.com/hanwen/go-fuse/v2/fuse" | ||||||
|  | 	"go.arsenm.dev/logger/log" | ||||||
|  | 	"context" | ||||||
|  | 	"go.arsenm.dev/infinitime" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func startFUSE(ctx context.Context, dev *infinitime.Device) error { | ||||||
|  | 	// This is where we'll mount the FS | ||||||
|  | 	err := os.MkdirAll(k.String("fuse.mountpoint"), 0755) | ||||||
|  | 	if err != nil && !os.IsExist(err) { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Ignore the error because nothing might be mounted on the mountpoint | ||||||
|  | 	_ = fusefs.Unmount(k.String("fuse.mountpoint")) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	root, err := fusefs.BuildRootNode(dev) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("Building root node failed"). | ||||||
|  | 			Err(err). | ||||||
|  | 			Send() | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	server, err := fs.Mount(k.String("fuse.mountpoint"), root, &fs.Options{ | ||||||
|  | 		MountOptions: fuse.MountOptions{ | ||||||
|  | 			// Set to true to see how the file system works. | ||||||
|  | 			Debug: false, | ||||||
|  | 			SingleThreaded: true, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("Mounting failed"). | ||||||
|  | 			Str("target", k.String("fuse.mountpoint")). | ||||||
|  | 			Err(err). | ||||||
|  | 			Send() | ||||||
|  | 			return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Info("Mounted on target"). | ||||||
|  | 		Str("target", k.String("fuse.mountpoint")). | ||||||
|  | 		Send() | ||||||
|  |  | ||||||
|  | 	fusefs.BuildProperties(dev) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Warn("Error getting BLE filesystem").Err(err).Send() | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Wait until unmount before exiting | ||||||
|  | 	go server.Serve() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @@ -45,6 +45,7 @@ require ( | |||||||
| 	github.com/google/uuid v1.3.0 // indirect | 	github.com/google/uuid v1.3.0 // indirect | ||||||
| 	github.com/gookit/color v1.5.1 // indirect | 	github.com/gookit/color v1.5.1 // indirect | ||||||
| 	github.com/gopherjs/gopherjs v1.17.2 // indirect | 	github.com/gopherjs/gopherjs v1.17.2 // indirect | ||||||
|  | 	github.com/hanwen/go-fuse/v2 v2.2.0 // indirect | ||||||
| 	github.com/hashicorp/yamux v0.1.1 // indirect | 	github.com/hashicorp/yamux v0.1.1 // indirect | ||||||
| 	github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect | 	github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect | ||||||
| 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect | 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							| @@ -257,6 +257,8 @@ github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a/go.mod h1:dy/f2gjY09hwVfI | |||||||
| github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY= | github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY= | ||||||
| github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= | ||||||
| github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= | ||||||
|  | github.com/hanwen/go-fuse/v2 v2.2.0 h1:jo5QZYmBLNcl9ovypWaQ5yXMSSV+Ch68xoC3rtZvvBM= | ||||||
|  | github.com/hanwen/go-fuse/v2 v2.2.0/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc= | ||||||
| github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= | ||||||
| github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= | github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= | ||||||
| github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= | ||||||
|   | |||||||
							
								
								
									
										572
									
								
								internal/fusefs/fuse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										572
									
								
								internal/fusefs/fuse.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,572 @@ | |||||||
|  | package fusefs | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"go.arsenm.dev/infinitime" | ||||||
|  | 	"go.arsenm.dev/infinitime/blefs" | ||||||
|  | 	"go.arsenm.dev/logger/log" | ||||||
|  | 	"context" | ||||||
|  | 	"syscall" | ||||||
|  | 	"github.com/hanwen/go-fuse/v2/fs" | ||||||
|  | 	"github.com/hanwen/go-fuse/v2/fuse" | ||||||
|  | 	"io" | ||||||
|  | 	"bytes" | ||||||
|  | 	"strconv" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type ITProperty struct { | ||||||
|  | 	name string | ||||||
|  | 	Ino uint64 | ||||||
|  | 	gen func() ([]byte, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | type DirEntry struct { | ||||||
|  | 	isDir    bool | ||||||
|  | 	modtime  uint64 | ||||||
|  | 	size     uint32 | ||||||
|  | 	path     string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ITNode struct { | ||||||
|  | 	fs.Inode | ||||||
|  | 	kind int | ||||||
|  | 	Ino uint64 | ||||||
|  |  | ||||||
|  | 	lst []DirEntry | ||||||
|  | 	self DirEntry | ||||||
|  | 	path string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var myfs *blefs.FS = nil | ||||||
|  | var inodemap map[string]uint64 = nil | ||||||
|  |  | ||||||
|  | func BuildRootNode(dev *infinitime.Device) (*ITNode, error) { | ||||||
|  | 	var err error | ||||||
|  | 	inodemap = make(map[string]uint64) | ||||||
|  | 	myfs, err = dev.FS() | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("FUSE Failed to get filesystem").Err(err).Send() | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &ITNode{kind: 0}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var properties = make([]ITProperty, 6) | ||||||
|  |  | ||||||
|  | func BuildProperties(dev *infinitime.Device) { | ||||||
|  | 	properties[0] = ITProperty{"heartrate", 2, | ||||||
|  | 		func() ([]byte, error) { | ||||||
|  | 			ans, err := dev.HeartRate() | ||||||
|  | 			return []byte(strconv.Itoa(int(ans)) + "\n"), err | ||||||
|  | 	}} | ||||||
|  | 	properties[1] = ITProperty{"battery", 3, | ||||||
|  | 		func() ([]byte, error) { | ||||||
|  | 			ans, err := dev.BatteryLevel() | ||||||
|  | 			return []byte(strconv.Itoa(int(ans)) + "\n"), err | ||||||
|  | 	}} | ||||||
|  | 	properties[2] = ITProperty{"motion", 4, | ||||||
|  | 		func() ([]byte, error) { | ||||||
|  | 			ans, err := dev.Motion() | ||||||
|  | 			return []byte(strconv.Itoa(int(ans.X)) + " " + strconv.Itoa(int(ans.Y)) + " " + strconv.Itoa(int(ans.Z)) + "\n"), err | ||||||
|  | 	}} | ||||||
|  | 	properties[3] = ITProperty{"stepcount", 6, | ||||||
|  | 		func() ([]byte, error) { | ||||||
|  | 			ans, err := dev.StepCount() | ||||||
|  | 			return []byte(strconv.Itoa(int(ans)) + "\n"), err | ||||||
|  | 	}} | ||||||
|  | 	properties[4] = ITProperty{"version", 7, | ||||||
|  | 		func() ([]byte, error) { | ||||||
|  | 			ans, err := dev.Version() | ||||||
|  | 			return []byte(ans + "\n"), err | ||||||
|  | 	}} | ||||||
|  | 	properties[5] = ITProperty{"address", 8, | ||||||
|  | 		func() ([]byte, error) { | ||||||
|  | 			ans := dev.Address() | ||||||
|  | 			return []byte(ans + "\n"), nil | ||||||
|  | 	}} | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | var _ fs.NodeReaddirer = (*ITNode)(nil) | ||||||
|  |  | ||||||
|  | // Readdir is part of the NodeReaddirer interface | ||||||
|  | func (n *ITNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { | ||||||
|  | 	switch n.kind { | ||||||
|  | 	case 0: | ||||||
|  | 		// root folder | ||||||
|  | 		r := make([]fuse.DirEntry, 2) | ||||||
|  | 		r[0] = fuse.DirEntry{ | ||||||
|  | 			Name: "info", | ||||||
|  | 			Ino:  0, | ||||||
|  | 			Mode: fuse.S_IFDIR, | ||||||
|  | 		} | ||||||
|  | 		r[1] = fuse.DirEntry{ | ||||||
|  | 			Name: "fs", | ||||||
|  | 			Ino:  1, | ||||||
|  | 			Mode: fuse.S_IFDIR, | ||||||
|  | 		} | ||||||
|  | 		return fs.NewListDirStream(r), 0 | ||||||
|  |  | ||||||
|  | 	case 1: | ||||||
|  | 		// info folder | ||||||
|  | 		r := make([]fuse.DirEntry, 6) | ||||||
|  | 		for ind, value := range properties { | ||||||
|  | 			r[ind] = fuse.DirEntry{ | ||||||
|  | 				Name: value.name, | ||||||
|  | 				Ino:  value.Ino, | ||||||
|  | 				Mode: fuse.S_IFREG, | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return fs.NewListDirStream(r), 0 | ||||||
|  |  | ||||||
|  | 	case 2: | ||||||
|  | 		// on info | ||||||
|  | 		files, err := myfs.ReadDir(n.path) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("FUSE ReadDir failed").Str("path", n.path).Err(err).Send() | ||||||
|  | 			return nil, syscallErr(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		log.Debug("FUSE ReadDir succeeded").Str("path", n.path).Int("objects", len(files)).Send() | ||||||
|  | 		r := make([]fuse.DirEntry, len(files)) | ||||||
|  | 		n.lst = make([]DirEntry, len(files)) | ||||||
|  | 		for ind, entry := range files { | ||||||
|  | 			info, err := entry.Info() | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("FUSE Info failed").Str("path", n.path).Err(err).Send() | ||||||
|  | 				return nil, syscallErr(err) | ||||||
|  | 			} | ||||||
|  | 			name := info.Name() | ||||||
|  |  | ||||||
|  | 			file := DirEntry{ | ||||||
|  | 				path: n.path + "/" + name, | ||||||
|  | 				size: uint32(info.Size()), | ||||||
|  | 				modtime: uint64(info.ModTime().Unix()), | ||||||
|  | 				isDir: info.IsDir(), | ||||||
|  | 			} | ||||||
|  | 			n.lst[ind] = file | ||||||
|  |  | ||||||
|  | 			ino := inodemap[file.path] | ||||||
|  | 			if ino == 0 { | ||||||
|  | 				ino = uint64(len(inodemap)) + 1 | ||||||
|  | 				inodemap[file.path] = ino | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if file.isDir { | ||||||
|  | 				r[ind] = fuse.DirEntry{ | ||||||
|  | 					Name: name, | ||||||
|  | 					Mode: fuse.S_IFDIR, | ||||||
|  | 					Ino : ino + 10, | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				r[ind] = fuse.DirEntry{ | ||||||
|  | 					Name: name, | ||||||
|  | 					Mode: fuse.S_IFREG, | ||||||
|  | 					Ino : ino + 10, | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return fs.NewListDirStream(r), 0 | ||||||
|  | 	} | ||||||
|  | 	r := make([]fuse.DirEntry, 0) | ||||||
|  | 	return fs.NewListDirStream(r), 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.NodeLookuper = (*ITNode)(nil) | ||||||
|  | func (n *ITNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { | ||||||
|  | 	switch n.kind { | ||||||
|  | 	case 0: | ||||||
|  | 		// root folder | ||||||
|  | 		if name == "info" { | ||||||
|  | 			stable := fs.StableAttr{ | ||||||
|  | 				Mode: fuse.S_IFDIR, | ||||||
|  | 				Ino: uint64(0), | ||||||
|  | 			} | ||||||
|  | 			operations := &ITNode{kind: 1, Ino: 0} | ||||||
|  | 			child := n.NewInode(ctx, operations, stable) | ||||||
|  | 			return child, 0 | ||||||
|  | 		} else if name == "fs" { | ||||||
|  | 			stable := fs.StableAttr{ | ||||||
|  | 				Mode: fuse.S_IFDIR, | ||||||
|  | 				Ino: uint64(1), | ||||||
|  | 			} | ||||||
|  | 			operations := &ITNode{kind: 2, Ino: 1, path : ""} | ||||||
|  | 			child := n.NewInode(ctx, operations, stable) | ||||||
|  | 			return child, 0 | ||||||
|  | 		} | ||||||
|  | 	case 1: | ||||||
|  | 		// info folder | ||||||
|  | 		for _, value := range properties { | ||||||
|  | 			if value.name == name { | ||||||
|  | 				stable := fs.StableAttr{ | ||||||
|  | 					Mode: fuse.S_IFREG, | ||||||
|  | 					Ino: uint64(value.Ino), | ||||||
|  | 				} | ||||||
|  | 				operations := &ITNode{kind: 3, Ino: value.Ino} | ||||||
|  | 				child := n.NewInode(ctx, operations, stable) | ||||||
|  | 				return child, 0 | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	case 2: | ||||||
|  | 		// FS object | ||||||
|  | 		if len(n.lst) == 0 { | ||||||
|  | 			n.Readdir(ctx) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, file := range n.lst { | ||||||
|  | 			if file.path != n.path + "/" + name { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			log.Debug("FUSE Lookup successful").Str("path", file.path).Send() | ||||||
|  |  | ||||||
|  | 			if file.isDir { | ||||||
|  | 				stable := fs.StableAttr{ | ||||||
|  | 					Mode: fuse.S_IFDIR, | ||||||
|  | 					Ino: inodemap[file.path], | ||||||
|  | 				} | ||||||
|  | 				operations := &ITNode{kind: 2, path: file.path} | ||||||
|  | 				child := n.NewInode(ctx, operations, stable) | ||||||
|  | 				return child, 0 | ||||||
|  | 			} else { | ||||||
|  | 				stable := fs.StableAttr{ | ||||||
|  | 					Mode: fuse.S_IFREG, | ||||||
|  | 					Ino: inodemap[file.path], | ||||||
|  | 				} | ||||||
|  | 				operations := &ITNode{ | ||||||
|  | 					kind: 2, path: file.path, | ||||||
|  | 					self: file, | ||||||
|  | 				} | ||||||
|  | 				child := n.NewInode(ctx, operations, stable) | ||||||
|  | 				return child, 0 | ||||||
|  | 			} | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		log.Warn("FUSE Lookup failed").Str("path", n.path + "/" + name).Send() | ||||||
|  | 	} | ||||||
|  | 	return nil, syscall.ENOENT | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type bytesFileReadHandle struct { | ||||||
|  | 	content []byte | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.FileReader = (*bytesFileReadHandle)(nil) | ||||||
|  | func (fh *bytesFileReadHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { | ||||||
|  | 	log.Debug("FUSE Executing Read").Int("size", len(fh.content)).Send() | ||||||
|  | 	end := off + int64(len(dest)) | ||||||
|  | 	if end > int64(len(fh.content)) { | ||||||
|  | 		end = int64(len(fh.content)) | ||||||
|  | 	} | ||||||
|  | 	return fuse.ReadResultData(fh.content[off:end]), 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type sensorFileReadHandle struct { | ||||||
|  | 	content []byte | ||||||
|  | } | ||||||
|  | var _ fs.FileReader = (*sensorFileReadHandle)(nil) | ||||||
|  | func (fh *sensorFileReadHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { | ||||||
|  | 	log.Info("Executing Read").Int("size", len(fh.content)).Send() | ||||||
|  | 	end := off + int64(len(dest)) | ||||||
|  | 	if end > int64(len(fh.content)) { | ||||||
|  | 		end = int64(len(fh.content)) | ||||||
|  | 	} | ||||||
|  | 	return fuse.ReadResultData(fh.content[off:end]), 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.FileFlusher = (*sensorFileReadHandle)(nil) | ||||||
|  | func (fh *sensorFileReadHandle) Flush(ctx context.Context) (errno syscall.Errno) { | ||||||
|  | 	return 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | type bytesFileWriteHandle struct { | ||||||
|  | 	content []byte | ||||||
|  | 	path string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.FileWriter = (*bytesFileWriteHandle)(nil) | ||||||
|  | func (fh *bytesFileWriteHandle) Write(ctx context.Context, data []byte, off int64) (written uint32, errno syscall.Errno) { | ||||||
|  | 	log.Info("Executing Write").Str("path", fh.path).Int("prev_size", len(fh.content)).Int("next_size", len(data)).Send() | ||||||
|  | 	if off != int64(len(fh.content)) { | ||||||
|  | 		log.Error("FUSE Write file size changed unexpectedly").Int("expect", int(off)).Int("received", len(fh.content)).Send() | ||||||
|  | 		return 0, syscall.ENXIO | ||||||
|  | 	} | ||||||
|  | 	fh.content = append(fh.content[:], data[:]...) | ||||||
|  | 	return uint32(len(data)), 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.FileFlusher = (*bytesFileWriteHandle)(nil) | ||||||
|  | func (fh *bytesFileWriteHandle) Flush(ctx context.Context) (errno syscall.Errno) { | ||||||
|  |  | ||||||
|  | 	log.Debug("FUSE Attempting flush").Str("path", fh.path).Send() | ||||||
|  | 	fp, err := myfs.Create(fh.path, uint32(len(fh.content))) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("FUSE Flush failed: create").Str("path", fh.path).Err(err).Send() | ||||||
|  | 		return syscallErr(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(fh.content) == 0 { | ||||||
|  | 		log.Debug("FUSE Flush no data to write").Str("path", fh.path).Send() | ||||||
|  | 		err = fp.Close() | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("FUSE Flush failed during close").Str("path", fh.path).Err(err).Send() | ||||||
|  | 			return syscallErr(err) | ||||||
|  | 		} | ||||||
|  | 		return 0 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	go func() { | ||||||
|  | 		// For every progress event | ||||||
|  | 		for sent := range fp.Progress() { | ||||||
|  | 			log.Debug("FUSE Flush progress").Int("bytes", int(sent)).Int("total", len(fh.content)).Send() | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	r := bytes.NewReader(fh.content) | ||||||
|  | 	nread, err := io.Copy(fp, r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("FUSE Flush failed during write").Str("path", fh.path).Err(err).Send() | ||||||
|  | 		fp.Close() | ||||||
|  | 		return syscallErr(err) | ||||||
|  | 	} | ||||||
|  | 	if int(nread) != len(fh.content) { | ||||||
|  | 		log.Error("FUSE Flush failed during write").Str("path", fh.path).Int("expect", len(fh.content)).Int("got", int(nread)).Send() | ||||||
|  | 		fp.Close() | ||||||
|  | 		return syscall.EIO | ||||||
|  | 	} | ||||||
|  | 	err = fp.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("FUSE Flush failed during close").Str("path", fh.path).Err(err).Send() | ||||||
|  | 		return syscallErr(err) | ||||||
|  | 	} | ||||||
|  | 	log.Debug("FUSE Flush done").Str("path", fh.path).Int("size", len(fh.content)).Send() | ||||||
|  |  | ||||||
|  | 	return 0 | ||||||
|  | } | ||||||
|  | var _ fs.FileFsyncer = (*bytesFileWriteHandle)(nil) | ||||||
|  | func (fh *bytesFileWriteHandle) Fsync(ctx context.Context, flags uint32) (errno syscall.Errno) { | ||||||
|  | 	return fh.Flush(ctx) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.NodeGetattrer = (*ITNode)(nil) | ||||||
|  | func (bn *ITNode) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { | ||||||
|  | 	log.Debug("FUSE getattr").Str("path", bn.path).Send() | ||||||
|  | 	out.Ino = bn.Ino | ||||||
|  | 	out.Mtime = bn.self.modtime | ||||||
|  | 	out.Ctime = bn.self.modtime | ||||||
|  | 	out.Atime = bn.self.modtime | ||||||
|  | 	out.Size = uint64(bn.self.size) | ||||||
|  | 	return 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.NodeSetattrer = (*ITNode)(nil) | ||||||
|  | func (bn *ITNode) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno { | ||||||
|  | 	log.Debug("FUSE setattr").Str("path", bn.path).Send() | ||||||
|  | 	out.Size = 0 | ||||||
|  | 	out.Mtime = 0 | ||||||
|  | 	return 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.NodeOpener = (*ITNode)(nil) | ||||||
|  | func (f *ITNode) Open(ctx context.Context, openFlags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { | ||||||
|  | 	switch f.kind { | ||||||
|  | 	case 2: | ||||||
|  | 		// FS file | ||||||
|  | 		if openFlags&syscall.O_RDWR != 0 { | ||||||
|  | 			log.Error("FUSE Open failed: RDWR").Str("path", f.path).Send() | ||||||
|  | 			return nil, 0, syscall.EROFS | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if openFlags & syscall.O_WRONLY != 0 { | ||||||
|  | 			log.Debug("FUSE Opening for write").Str("path", f.path).Send() | ||||||
|  | 			fh = &bytesFileWriteHandle{ | ||||||
|  | 				path : f.path, | ||||||
|  | 				content : make([]byte, 0), | ||||||
|  | 			} | ||||||
|  | 			return fh, fuse.FOPEN_DIRECT_IO, 0 | ||||||
|  | 		} else { | ||||||
|  | 			log.Debug("FUSE Opening for read").Str("path", f.path).Send() | ||||||
|  | 			fp, err := myfs.Open(f.path) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("FUSE: Opening failed").Str("path", f.path).Err(err).Send() | ||||||
|  | 				return nil, 0, syscallErr(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			defer fp.Close() | ||||||
|  |  | ||||||
|  | 			b := &bytes.Buffer{} | ||||||
|  |  | ||||||
|  | 			go func() { | ||||||
|  | 				// For every progress event | ||||||
|  | 				for sent := range fp.Progress() { | ||||||
|  | 					log.Debug("FUSE Read progress").Int("bytes", int(sent)).Int("total", int(f.self.size)).Send() | ||||||
|  | 				} | ||||||
|  | 			}() | ||||||
|  |  | ||||||
|  | 			_, err = io.Copy(b, fp) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("FUSE Read failed").Str("path", f.path).Err(err).Send() | ||||||
|  | 				fp.Close() | ||||||
|  | 				return nil, 0, syscallErr(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			fh = &bytesFileReadHandle{ | ||||||
|  | 				content: b.Bytes(), | ||||||
|  | 			} | ||||||
|  | 			return fh, fuse.FOPEN_DIRECT_IO, 0 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	case 3: | ||||||
|  | 		// Device file | ||||||
|  |  | ||||||
|  | 		// disallow writes | ||||||
|  | 		if openFlags&(syscall.O_RDWR|syscall.O_WRONLY) != 0 { | ||||||
|  | 			return nil, 0, syscall.EROFS | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, value := range properties { | ||||||
|  | 			if value.Ino == f.Ino { | ||||||
|  | 				ans, err := value.gen() | ||||||
|  | 				if err != nil { | ||||||
|  | 					return nil, 0, syscallErr(err) | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				fh = &sensorFileReadHandle{ | ||||||
|  | 					content : ans, | ||||||
|  | 				} | ||||||
|  | 				return fh, fuse.FOPEN_DIRECT_IO, 0 | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil, 0, syscall.EINVAL | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.NodeCreater = (*ITNode)(nil) | ||||||
|  | func (f *ITNode) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { | ||||||
|  | 	if f.kind != 2 { | ||||||
|  | 		return nil, nil, 0, syscall.EROFS | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	path := f.path + "/" + name | ||||||
|  | 	ino := uint64(len(inodemap)) + 11 | ||||||
|  | 	inodemap[path] = ino | ||||||
|  |  | ||||||
|  | 	stable := fs.StableAttr{ | ||||||
|  | 		Mode: fuse.S_IFREG, | ||||||
|  | 		Ino: ino, | ||||||
|  | 	} | ||||||
|  | 	operations := &ITNode{ | ||||||
|  | 		kind: 2, Ino: ino, | ||||||
|  | 		path : path, | ||||||
|  | 	} | ||||||
|  | 	node = f.NewInode(ctx, operations, stable) | ||||||
|  |  | ||||||
|  | 	fh = &bytesFileWriteHandle{ | ||||||
|  | 		path : path, | ||||||
|  | 		content : make([]byte, 0), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Debug("FUSE Creating file").Str("path", path).Send() | ||||||
|  |  | ||||||
|  | 	errno = 0 | ||||||
|  | 	return node, fh, fuseFlags, 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.NodeMkdirer = (*ITNode)(nil) | ||||||
|  | func (f *ITNode) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { | ||||||
|  | 	if f.kind != 2 { | ||||||
|  | 		return nil, syscall.EROFS | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	path := f.path + "/" + name | ||||||
|  | 	err := myfs.Mkdir(path) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("FUSE Mkdir failed"). | ||||||
|  | 			Str("path", path). | ||||||
|  | 			Err(err). | ||||||
|  | 			Send() | ||||||
|  | 		return nil, syscallErr(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ino := uint64(len(inodemap)) + 11 | ||||||
|  | 	inodemap[path] = ino | ||||||
|  |  | ||||||
|  | 	stable := fs.StableAttr{ | ||||||
|  | 		Mode: fuse.S_IFDIR, | ||||||
|  | 		Ino: ino, | ||||||
|  | 	} | ||||||
|  | 	operations := &ITNode{ | ||||||
|  | 		kind: 2, Ino: ino, | ||||||
|  | 		path : path, | ||||||
|  | 	} | ||||||
|  | 	node := f.NewInode(ctx, operations, stable) | ||||||
|  |  | ||||||
|  | 	log.Debug("FUSE Mkdir success"). | ||||||
|  | 		Str("path", path). | ||||||
|  | 		Int("ino", int(ino)). | ||||||
|  | 		Send() | ||||||
|  | 	return node, 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.NodeRenamer = (*ITNode)(nil) | ||||||
|  | func (f *ITNode) Rename(ctx context.Context, name string, newParent fs.InodeEmbedder, newName string, flags uint32) syscall.Errno { | ||||||
|  | 	if f.kind != 2 { | ||||||
|  | 		return syscall.EROFS | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	p1 := f.path + "/" + name | ||||||
|  | 	p2 := newParent.EmbeddedInode().Path(nil)[2:] + "/" + newName | ||||||
|  |  | ||||||
|  | 	err := myfs.Rename(p1, p2) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("FUSE Rename failed"). | ||||||
|  | 			Str("src", p1). | ||||||
|  | 			Str("dest", p2). | ||||||
|  | 			Err(err). | ||||||
|  | 			Send() | ||||||
|  |  | ||||||
|  | 		return syscallErr(err) | ||||||
|  | 	} | ||||||
|  | 	log.Debug("FUSE Rename sucess"). | ||||||
|  | 		Str("src", p1). | ||||||
|  | 		Str("dest", p2). | ||||||
|  | 		Send() | ||||||
|  |  | ||||||
|  | 	ino := inodemap[p1] | ||||||
|  | 	delete(inodemap, p1) | ||||||
|  | 	inodemap[p2] = ino | ||||||
|  |  | ||||||
|  | 	return 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.NodeUnlinker = (*ITNode)(nil) | ||||||
|  | func (f *ITNode) Unlink(ctx context.Context, name string) syscall.Errno { | ||||||
|  | 	if f.kind != 2 { | ||||||
|  | 		return syscall.EROFS | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	delete(inodemap, f.path + "/" + name) | ||||||
|  | 	err := myfs.Remove(f.path + "/" + name) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("FUSE Unlink failed"). | ||||||
|  | 			Str("file", f.path + "/" + name). | ||||||
|  | 			Err(err). | ||||||
|  | 			Send() | ||||||
|  |  | ||||||
|  | 		return syscallErr(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Debug("FUSE Unlink success"). | ||||||
|  | 		Str("file", f.path + "/" + name). | ||||||
|  | 		Send() | ||||||
|  | 	return 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ fs.NodeRmdirer = (*ITNode)(nil) | ||||||
|  | func (f *ITNode) Rmdir(ctx context.Context, name string) syscall.Errno { | ||||||
|  | 	return f.Unlink(ctx, name) | ||||||
|  | } | ||||||
							
								
								
									
										67
									
								
								internal/fusefs/syscallerr.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								internal/fusefs/syscallerr.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | package fusefs | ||||||
|  | import ( | ||||||
|  | 	"go.arsenm.dev/infinitime/blefs" | ||||||
|  | 	"syscall" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func syscallErr(err error) syscall.Errno { | ||||||
|  | 	if err == nil { | ||||||
|  | 		return 0 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch err { | ||||||
|  | 	case blefs.FSError{0x02}: // filesystem error | ||||||
|  | 		return syscall.EIO // TODO | ||||||
|  | 	case blefs.FSError{0x05}: // read-only filesystem | ||||||
|  | 		return syscall.EROFS | ||||||
|  | 	case blefs.FSError{0x03}: // no such file | ||||||
|  | 		return syscall.ENOENT | ||||||
|  | 	case blefs.FSError{0x04}: // protocol error | ||||||
|  | 		return syscall.EPROTO | ||||||
|  | 	case blefs.FSError{-5}: // input/output error | ||||||
|  | 		return syscall.EIO | ||||||
|  | 	case blefs.FSError{-84}: // filesystem is corrupted | ||||||
|  | 		return syscall.ENOTRECOVERABLE // TODO | ||||||
|  | 	case blefs.FSError{-2}: // no such directory entry | ||||||
|  | 		return syscall.ENOENT | ||||||
|  | 	case blefs.FSError{-17}: // entry already exists | ||||||
|  | 		return syscall.EEXIST | ||||||
|  | 	case blefs.FSError{-20}: // entry is not a directory | ||||||
|  | 		return syscall.ENOTDIR | ||||||
|  | 	case blefs.FSError{-39}: // directory is not empty | ||||||
|  | 		return syscall.ENOTEMPTY | ||||||
|  | 	case blefs.FSError{-9}: // bad file number | ||||||
|  | 		return syscall.EBADF | ||||||
|  | 	case blefs.FSError{-27}: // file is too large | ||||||
|  | 		return syscall.EFBIG | ||||||
|  | 	case blefs.FSError{-22}: // invalid parameter | ||||||
|  | 		return syscall.EINVAL | ||||||
|  | 	case blefs.FSError{-28}: // no space left on device | ||||||
|  | 		return syscall.ENOSPC | ||||||
|  | 	case blefs.FSError{-12}: // no more memory available | ||||||
|  | 		return syscall.ENOMEM | ||||||
|  | 	case blefs.FSError{-61}: // no attr available | ||||||
|  | 		return syscall.ENODATA // TODO | ||||||
|  | 	case blefs.FSError{-36}: // file name is too long | ||||||
|  | 		return syscall.ENAMETOOLONG | ||||||
|  | 	case blefs.ErrFileNotExists: // file does not exist | ||||||
|  | 		return syscall.ENOENT | ||||||
|  | 	case blefs.ErrFileReadOnly: // file is read only | ||||||
|  | 		return syscall.EACCES | ||||||
|  | 	case blefs.ErrFileWriteOnly: // file is write only | ||||||
|  | 		return syscall.EACCES | ||||||
|  | 	case blefs.ErrInvalidOffset: // invalid file offset | ||||||
|  | 		return syscall.EFAULT  // TODO | ||||||
|  | 	case blefs.ErrOffsetChanged: // offset has already been changed | ||||||
|  | 		return syscall.ESPIPE | ||||||
|  | 	case blefs.ErrReadOpen: // only one file can be opened for reading at a time | ||||||
|  | 		return syscall.ENFILE | ||||||
|  | 	case blefs.ErrWriteOpen: // only one file can be opened for writing at a time | ||||||
|  | 		return syscall.ENFILE | ||||||
|  | 	case blefs.ErrNoRemoveRoot: // refusing to remove root directory | ||||||
|  | 		return syscall.EPERM | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return syscall.EIO // TODO | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								internal/fusefs/unmount.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								internal/fusefs/unmount.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | package fusefs | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	_ "unsafe" | ||||||
|  | 	"github.com/hanwen/go-fuse/v2/fuse" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func Unmount(mountPoint string) error { | ||||||
|  | 	return unmount(mountPoint, &fuse.MountOptions{DirectMount: false}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Unfortunately, the FUSE library does not export its unmount function, | ||||||
|  | // so this is required until that changes | ||||||
|  | //go:linkname unmount github.com/hanwen/go-fuse/v2/fuse.unmount | ||||||
|  | func unmount(mountPoint string, opts *fuse.MountOptions) error | ||||||
							
								
								
									
										8
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								main.go
									
									
									
									
									
								
							| @@ -181,6 +181,14 @@ func main() { | |||||||
| 		log.Error("Error intializing puremaps integration").Err(err).Send() | 		log.Error("Error intializing puremaps integration").Err(err).Send() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Start fuse socket | ||||||
|  | 	if k.Bool("fuse.enabled") { | ||||||
|  | 		err = startFUSE(ctx, dev) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("Error starting fuse socket").Err(err).Send() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Start control socket | 	// Start control socket | ||||||
| 	err = startSocket(ctx, dev) | 	err = startSocket(ctx, dev) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user