diff --git a/fs.go b/fs.go index b5845e4..494c2a3 100644 --- a/fs.go +++ b/fs.go @@ -5,17 +5,15 @@ import ( "fmt" "os" "os/signal" - "strings" - "sync/atomic" "syscall" "time" - "github.com/colduction/nocopy" "github.com/fatih/color" + "github.com/fiatjaf/nak/nostrfs" "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/nbd-wtf/go-nostr/keyer" "github.com/urfave/cli/v3" ) @@ -43,7 +41,7 @@ var fsCmd = &cli.Command{ 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")} + root := nostrfs.NewNostrRoot(ctx, sys, keyer.NewReadOnlyUser(c.String("pubkey"))) // create the server log("- mounting at %s... ", color.HiCyanString(mountpoint)) @@ -83,140 +81,3 @@ var fsCmd = &cli.Command{ 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 -} diff --git a/go.mod b/go.mod index a2f633d..35ac8c8 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e - github.com/colduction/nocopy v0.2.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 github.com/fiatjaf/eventstore v0.15.0 @@ -37,6 +36,7 @@ require ( github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/coder/websocket v1.8.12 // indirect + github.com/colduction/nocopy v0.2.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/dgraph-io/ristretto v1.0.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -77,3 +77,5 @@ require ( golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.21.0 // indirect ) + +replace github.com/nbd-wtf/go-nostr => ../go-nostr diff --git a/go.sum b/go.sum index 0fe7b5f..5c67fd5 100644 --- a/go.sum +++ b/go.sum @@ -149,8 +149,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/nbd-wtf/go-nostr v0.50.5 h1:JOLrozw6nzWMD7CKhEGB5Ys7zXrTV82YjItBBnI5nw8= -github.com/nbd-wtf/go-nostr v0.50.5/go.mod h1:s7XMBrnFUTX+ylEekIAmdvkGxMjllLZeic93TmAi6hU= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/helpers.go b/helpers.go index 63f43f6..d0c8476 100644 --- a/helpers.go +++ b/helpers.go @@ -11,7 +11,6 @@ import ( "net/url" "os" "slices" - "strconv" "strings" "time" @@ -235,11 +234,6 @@ func leftPadKey(k string) string { return strings.Repeat("0", 64-len(k)) + k } -func hexToUint64(hexStr string) uint64 { - v, _ := strconv.ParseUint(hexStr[0:16], 16, 64) - return v -} - var colors = struct { reset func(...any) (int, error) italic func(...any) string diff --git a/nostrfs/eventdir.go b/nostrfs/eventdir.go new file mode 100644 index 0000000..c3ea83d --- /dev/null +++ b/nostrfs/eventdir.go @@ -0,0 +1,68 @@ +package nostrfs + +import ( + "context" + "fmt" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/mailru/easyjson" + "github.com/nbd-wtf/go-nostr" + sdk "github.com/nbd-wtf/go-nostr/sdk" +) + +type EventDir struct { + fs.Inode + ctx context.Context + evt *nostr.Event +} + +func FetchAndCreateEventDir( + ctx context.Context, + parent fs.InodeEmbedder, + sys *sdk.System, + pointer nostr.EventPointer, +) (*fs.Inode, error) { + event, _, err := sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{ + WithRelays: false, + }) + if err != nil { + return nil, fmt.Errorf("failed to fetch: %w", err) + } + + return CreateEventDir(ctx, parent, event), nil +} + +func CreateEventDir( + ctx context.Context, + parent fs.InodeEmbedder, + event *nostr.Event, +) *fs.Inode { + h := parent.EmbeddedInode().NewPersistentInode( + ctx, + &EventDir{ctx: ctx, evt: event}, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ) + + eventj, _ := easyjson.Marshal(event) + h.AddChild("event.json", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: eventj, + Attr: fuse.Attr{Mode: 0444}, + }, + fs.StableAttr{}, + ), true) + + h.AddChild("content.txt", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: []byte(event.Content), + Attr: fuse.Attr{Mode: 0444}, + }, + fs.StableAttr{}, + ), true) + + return h +} diff --git a/nostrfs/helpers.go b/nostrfs/helpers.go new file mode 100644 index 0000000..1fd8672 --- /dev/null +++ b/nostrfs/helpers.go @@ -0,0 +1,8 @@ +package nostrfs + +import "strconv" + +func hexToUint64(hexStr string) uint64 { + v, _ := strconv.ParseUint(hexStr[0:16], 16, 64) + return v +} diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go new file mode 100644 index 0000000..8eed441 --- /dev/null +++ b/nostrfs/npubdir.go @@ -0,0 +1,46 @@ +package nostrfs + +import ( + "context" + "sync/atomic" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/nbd-wtf/go-nostr" + sdk "github.com/nbd-wtf/go-nostr/sdk" +) + +type NpubDir struct { + sys *sdk.System + fs.Inode + pointer nostr.ProfilePointer + ctx context.Context + fetched atomic.Bool +} + +func CreateNpubDir(ctx context.Context, parent fs.InodeEmbedder, pointer nostr.ProfilePointer) *fs.Inode { + npubdir := &NpubDir{pointer: pointer} + return parent.EmbeddedInode().NewPersistentInode( + ctx, + npubdir, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ) +} + +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 n.sys.Pool.FetchMany(ctx, n.sys.FetchOutboxRelays(ctx, n.pointer.PublicKey, 2), nostr.Filter{ + Kinds: []int{1}, + Authors: []string{n.pointer.PublicKey}, + }, nostr.WithLabel("nak-fs-feed")) { + e := CreateEventDir(ctx, n, ie.Event) + n.AddChild(ie.Event.ID, e, true) + } + + return fs.OK +} diff --git a/nostrfs/root.go b/nostrfs/root.go new file mode 100644 index 0000000..201ec70 --- /dev/null +++ b/nostrfs/root.go @@ -0,0 +1,80 @@ +package nostrfs + +import ( + "context" + "syscall" + + "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/nbd-wtf/go-nostr/sdk" +) + +type NostrRoot struct { + sys *sdk.System + fs.Inode + + rootPubKey string + signer nostr.Signer + ctx context.Context +} + +var _ = (fs.NodeOnAdder)((*NostrRoot)(nil)) + +func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User) *NostrRoot { + pubkey, _ := user.GetPublicKey(ctx) + signer, _ := user.(nostr.Signer) + + return &NostrRoot{ + sys: sys, + ctx: ctx, + rootPubKey: pubkey, + signer: signer, + } +} + +func (r *NostrRoot) OnAdd(context.Context) { + if r.rootPubKey == "" { + return + } + + fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey) + + for _, f := range fl.Items { + h := r.NewPersistentInode( + r.ctx, + &NpubDir{sys: r.sys, 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 + } + + pointer, err := nip19.ToPointer(name) + if err != nil { + return nil, syscall.ENOENT + } + + switch p := pointer.(type) { + case nostr.ProfilePointer: + npubdir := CreateNpubDir(ctx, r, p) + return npubdir, fs.OK + case nostr.EventPointer: + eventdir, err := FetchAndCreateEventDir(ctx, r, r.sys, p) + if err != nil { + return nil, syscall.ENOENT + } + return eventdir, fs.OK + default: + return nil, syscall.ENOENT + } +}