1
0
mirror of https://github.com/fiatjaf/nak.git synced 2025-08-07 12:18:27 -04:00
Files
nak/fs.go
2025-03-08 10:51:44 -03:00

223 lines
4.9 KiB
Go

package main
import (
"context"
"fmt"
"os"
"os/signal"
"strings"
"sync/atomic"
"syscall"
"time"
"github.com/colduction/nocopy"
"github.com/fatih/color"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/urfave/cli/v3"
)
var fsCmd = &cli.Command{
Name: "fs",
Usage: "mount a FUSE filesystem that exposes Nostr events as files.",
Description: `(experimental)`,
ArgsUsage: "<mountpoint>",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "pubkey",
Usage: "public key from where to to prepopulate directories",
Validator: func(pk string) error {
if nostr.IsValidPublicKey(pk) {
return nil
}
return fmt.Errorf("invalid public key '%s'", pk)
},
},
},
DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, c *cli.Command) error {
mountpoint := c.Args().First()
if mountpoint == "" {
return fmt.Errorf("must be called with a directory path to serve as the mountpoint as an argument")
}
root := &NostrRoot{ctx: ctx, rootPubKey: c.String("pubkey")}
// create the server
log("- mounting at %s... ", color.HiCyanString(mountpoint))
timeout := time.Second * 120
server, err := fs.Mount(mountpoint, root, &fs.Options{
MountOptions: fuse.MountOptions{
Debug: isVerbose,
Name: "nak",
},
AttrTimeout: &timeout,
EntryTimeout: &timeout,
Logger: nostr.DebugLogger,
})
if err != nil {
return fmt.Errorf("mount failed: %w", err)
}
log("ok\n")
// setup signal handling for clean unmount
ch := make(chan os.Signal, 1)
chErr := make(chan error)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-ch
log("- unmounting... ")
err := server.Unmount()
if err != nil {
chErr <- fmt.Errorf("unmount failed: %w", err)
} else {
log("ok\n")
chErr <- nil
}
}()
// serve the filesystem until unmounted
server.Wait()
return <-chErr
},
}
type NostrRoot struct {
fs.Inode
rootPubKey string
ctx context.Context
}
var _ = (fs.NodeOnAdder)((*NostrRoot)(nil))
func (r *NostrRoot) OnAdd(context.Context) {
if r.rootPubKey == "" {
return
}
fl := sys.FetchFollowList(r.ctx, r.rootPubKey)
for _, f := range fl.Items {
h := r.NewPersistentInode(
r.ctx,
&NpubDir{pointer: nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}}},
fs.StableAttr{Mode: syscall.S_IFDIR},
)
npub, _ := nip19.EncodePublicKey(f.Pubkey)
r.AddChild(npub, h, true)
}
}
func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
// check if we already have this npub
child := r.GetChild(name)
if child != nil {
return child, fs.OK
}
// if the name starts with "npub1" or "nprofile1", create a new npub directory
if strings.HasPrefix(name, "npub1") || strings.HasPrefix(name, "nprofile1") {
npubdir, err := NewNpubDir(name)
if err != nil {
return nil, syscall.ENOENT
}
return r.NewPersistentInode(
ctx,
npubdir,
fs.StableAttr{Mode: syscall.S_IFDIR},
), 0
}
return nil, syscall.ENOENT
}
type NpubDir struct {
fs.Inode
pointer nostr.ProfilePointer
ctx context.Context
fetched atomic.Bool
}
func NewNpubDir(npub string) (*NpubDir, error) {
pointer, err := nip19.ToPointer(npub)
if err != nil {
return nil, err
}
pp, ok := pointer.(nostr.ProfilePointer)
if !ok {
return nil, fmt.Errorf("directory must be npub or nprofile")
}
return &NpubDir{pointer: pp}, nil
}
var _ = (fs.NodeOpendirer)((*NpubDir)(nil))
func (n *NpubDir) Opendir(ctx context.Context) syscall.Errno {
if n.fetched.CompareAndSwap(true, true) {
return fs.OK
}
for ie := range sys.Pool.FetchMany(ctx, sys.FetchOutboxRelays(ctx, n.pointer.PublicKey, 2), nostr.Filter{
Kinds: []int{1},
Authors: []string{n.pointer.PublicKey},
}, nostr.WithLabel("nak-fs-feed")) {
h := n.NewPersistentInode(
ctx,
&EventFile{ctx: ctx, evt: *ie.Event},
fs.StableAttr{
Mode: syscall.S_IFREG,
Ino: hexToUint64(ie.Event.ID),
},
)
n.AddChild(ie.Event.ID, h, true)
}
return fs.OK
}
type EventFile struct {
fs.Inode
ctx context.Context
evt nostr.Event
}
var (
_ = (fs.NodeOpener)((*EventFile)(nil))
_ = (fs.NodeGetattrer)((*EventFile)(nil))
)
func (c *EventFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
out.Mode = 0444
out.Size = uint64(len(c.evt.String()))
ts := uint64(c.evt.CreatedAt)
out.Atime = ts
out.Mtime = ts
out.Ctime = ts
return fs.OK
}
func (c *EventFile) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
return nil, fuse.FOPEN_KEEP_CACHE, 0
}
func (c *EventFile) Read(
ctx context.Context,
fh fs.FileHandle,
dest []byte,
off int64,
) (fuse.ReadResult, syscall.Errno) {
buf := c.evt.String()
end := int(off) + len(dest)
if end > len(buf) {
end = len(c.evt.Content)
}
return fuse.ReadResultData(nocopy.StringToByteSlice(c.evt.Content[off:end])), fs.OK
}