diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go new file mode 100644 index 0000000..c45664c --- /dev/null +++ b/nostrfs/entitydir.go @@ -0,0 +1,214 @@ +package nostrfs + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "path/filepath" + "strconv" + "syscall" + "time" + + "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/nip27" + "github.com/nbd-wtf/go-nostr/nip92" + sdk "github.com/nbd-wtf/go-nostr/sdk" +) + +type EntityDir struct { + fs.Inode + ctx context.Context + wd string + evt *nostr.Event +} + +var _ = (fs.NodeGetattrer)((*EntityDir)(nil)) + +func (e *EntityDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + publishedAt := uint64(e.evt.CreatedAt) + out.Ctime = publishedAt + + if tag := e.evt.Tags.Find("published_at"); tag != nil { + publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) + } + out.Mtime = publishedAt + + return fs.OK +} + +func FetchAndCreateEntityDir( + ctx context.Context, + parent fs.InodeEmbedder, + wd string, + extension string, + sys *sdk.System, + pointer nostr.EntityPointer, +) (*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 CreateEntityDir(ctx, parent, wd, extension, event), nil +} + +func CreateEntityDir( + ctx context.Context, + parent fs.InodeEmbedder, + wd string, + extension string, + event *nostr.Event, +) *fs.Inode { + h := parent.EmbeddedInode().NewPersistentInode( + ctx, + &EntityDir{ctx: ctx, wd: wd, evt: event}, + fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, + ) + + var publishedAt uint64 + if tag := event.Tags.Find("published_at"); tag != nil { + publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) + } + + npub, _ := nip19.EncodePublicKey(event.PubKey) + h.AddChild("@author", h.NewPersistentInode( + ctx, + &fs.MemSymlink{ + Data: []byte(wd + "/" + npub), + }, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) + + eventj, _ := json.MarshalIndent(event, "", " ") + h.AddChild("event.json", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: eventj, + Attr: fuse.Attr{ + Mode: 0444, + Ctime: uint64(event.CreatedAt), + Mtime: uint64(publishedAt), + Size: uint64(len(event.Content)), + }, + }, + fs.StableAttr{}, + ), true) + + h.AddChild("identifier", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: []byte(event.Tags.GetD()), + Attr: fuse.Attr{ + Mode: 0444, + Ctime: uint64(event.CreatedAt), + Mtime: uint64(publishedAt), + Size: uint64(len(event.Tags.GetD())), + }, + }, + fs.StableAttr{}, + ), true) + + if tag := event.Tags.Find("title"); tag != nil { + h.AddChild("title", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: []byte(tag[1]), + Attr: fuse.Attr{ + Mode: 0444, + Ctime: uint64(event.CreatedAt), + Mtime: uint64(publishedAt), + Size: uint64(len(tag[1])), + }, + }, + fs.StableAttr{}, + ), true) + } + + h.AddChild("content"+extension, h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: []byte(event.Content), + Attr: fuse.Attr{ + Mode: 0444, + Ctime: uint64(event.CreatedAt), + Mtime: uint64(publishedAt), + Size: uint64(len(event.Content)), + }, + }, + fs.StableAttr{}, + ), true) + + var refsdir *fs.Inode + i := 0 + for ref := range nip27.ParseReferences(*event) { + i++ + if refsdir == nil { + refsdir = h.NewPersistentInode(ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR}) + h.AddChild("references", refsdir, true) + } + refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode( + ctx, + &fs.MemSymlink{ + Data: []byte(wd + "/" + nip19.EncodePointer(ref.Pointer)), + }, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) + } + + var imagesdir *fs.Inode + addImage := func(url string) { + if imagesdir == nil { + in := &fs.Inode{} + imagesdir = h.NewPersistentInode(ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) + h.AddChild("images", imagesdir, true) + } + imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode( + ctx, + &AsyncFile{ + ctx: ctx, + load: func() ([]byte, nostr.Timestamp) { + ctx, cancel := context.WithTimeout(ctx, time.Second*20) + defer cancel() + r, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, 0 + } + resp, err := http.DefaultClient.Do(r) + if err != nil { + return nil, 0 + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + return nil, 0 + } + w := &bytes.Buffer{} + io.Copy(w, resp.Body) + return w.Bytes(), 0 + }, + }, + fs.StableAttr{}, + ), true) + } + + images := nip92.ParseTags(event.Tags) + for _, imeta := range images { + if imeta.URL == "" { + continue + } + addImage(imeta.URL) + } + + if tag := event.Tags.Find("image"); tag != nil { + addImage(tag[1]) + } + + return h +} diff --git a/nostrfs/eventdir.go b/nostrfs/eventdir.go index 53c6e72..253e99c 100644 --- a/nostrfs/eventdir.go +++ b/nostrfs/eventdir.go @@ -3,6 +3,7 @@ package nostrfs import ( "bytes" "context" + "encoding/json" "fmt" "io" "net/http" @@ -12,7 +13,6 @@ import ( "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" - "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip10" "github.com/nbd-wtf/go-nostr/nip19" @@ -74,7 +74,7 @@ func CreateEventDir( fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) - eventj, _ := easyjson.Marshal(event) + eventj, _ := json.MarshalIndent(event, "", " ") h.AddChild("event.json", h.NewPersistentInode( ctx, &fs.MemRegularFile{ @@ -97,7 +97,7 @@ func CreateEventDir( Mode: 0444, Ctime: uint64(event.CreatedAt), Mtime: uint64(event.CreatedAt), - Size: uint64(len(event.Content)), + Size: uint64(64), }, }, fs.StableAttr{}, diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index f509344..f2a0509 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -2,10 +2,10 @@ package nostrfs import ( "context" - "encoding/json" "sync/atomic" "syscall" + "github.com/bytedance/sonic" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" "github.com/nbd-wtf/go-nostr" @@ -42,6 +42,27 @@ func CreateNpubDir( fs.StableAttr{}, ), true) + h.AddChild( + "metadata.json", + h.NewPersistentInode( + ctx, + &AsyncFile{ + ctx: ctx, + load: func() ([]byte, nostr.Timestamp) { + pm := sys.FetchProfileMetadata(ctx, pointer.PublicKey) + jsonb, _ := sonic.ConfigFastest.MarshalIndent(pm, "", " ") + var ts nostr.Timestamp + if pm.Event != nil { + ts = pm.Event.CreatedAt + } + return jsonb, ts + }, + }, + fs.StableAttr{}, + ), + true, + ) + h.AddChild( "notes", h.NewPersistentInode( @@ -54,7 +75,11 @@ func CreateNpubDir( Kinds: []int{1}, Authors: []string{pointer.PublicKey}, }, - relays: relays, + paginate: true, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, CreateEventDir(ctx, n, n.wd, event) + }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -73,7 +98,11 @@ func CreateNpubDir( Kinds: []int{1111}, Authors: []string{pointer.PublicKey}, }, - relays: relays, + paginate: true, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, CreateEventDir(ctx, n, n.wd, event) + }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -92,7 +121,11 @@ func CreateNpubDir( Kinds: []int{20}, Authors: []string{pointer.PublicKey}, }, - relays: relays, + paginate: true, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, CreateEventDir(ctx, n, n.wd, event) + }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -111,7 +144,11 @@ func CreateNpubDir( Kinds: []int{21, 22}, Authors: []string{pointer.PublicKey}, }, - relays: relays, + paginate: true, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, CreateEventDir(ctx, n, n.wd, event) + }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -130,7 +167,11 @@ func CreateNpubDir( Kinds: []int{9802}, Authors: []string{pointer.PublicKey}, }, - relays: relays, + paginate: true, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, CreateEventDir(ctx, n, n.wd, event) + }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -138,22 +179,47 @@ func CreateNpubDir( ) h.AddChild( - "metadata.json", + "articles", h.NewPersistentInode( ctx, - &AsyncFile{ + &ViewDir{ ctx: ctx, - load: func() ([]byte, nostr.Timestamp) { - pm := sys.FetchProfileMetadata(ctx, pointer.PublicKey) - jsonb, _ := json.MarshalIndent(pm.Event, "", " ") - var ts nostr.Timestamp - if pm.Event != nil { - ts = pm.Event.CreatedAt - } - return jsonb, ts + sys: sys, + wd: wd, + filter: nostr.Filter{ + Kinds: []int{30023}, + Authors: []string{pointer.PublicKey}, + }, + paginate: false, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.Tags.GetD(), CreateEntityDir(ctx, n, n.wd, ".md", event) }, }, - fs.StableAttr{}, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + + h.AddChild( + "wiki", + h.NewPersistentInode( + ctx, + &ViewDir{ + ctx: ctx, + sys: sys, + wd: wd, + filter: nostr.Filter{ + Kinds: []int{30818}, + Authors: []string{pointer.PublicKey}, + }, + paginate: false, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.Tags.GetD(), CreateEntityDir(ctx, n, n.wd, ".adoc", event) + }, + }, + fs.StableAttr{Mode: syscall.S_IFDIR}, ), true, ) diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index 04ce2eb..ddc6979 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -13,12 +13,14 @@ import ( type ViewDir struct { fs.Inode - ctx context.Context - sys *sdk.System - wd string - fetched atomic.Bool - filter nostr.Filter - relays []string + ctx context.Context + sys *sdk.System + wd string + fetched atomic.Bool + filter nostr.Filter + paginate bool + relays []string + create func(context.Context, *ViewDir, *nostr.Event) (string, *fs.Inode) } var ( @@ -33,6 +35,7 @@ func (n *ViewDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOu } aMonthAgo := now - 30*24*60*60 out.Mtime = uint64(aMonthAgo) + return fs.OK } @@ -41,32 +44,39 @@ func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno { return fs.OK } - now := nostr.Now() - if n.filter.Until != nil { - now = *n.filter.Until + if n.paginate { + now := nostr.Now() + if n.filter.Until != nil { + now = *n.filter.Until + } + aMonthAgo := now - 30*24*60*60 + n.filter.Since = &aMonthAgo + + for ie := range n.sys.Pool.FetchMany(ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { + basename, inode := n.create(ctx, n, ie.Event) + n.AddChild(basename, inode, true) + } + + filter := n.filter + filter.Until = &aMonthAgo + + n.AddChild("@previous", n.NewPersistentInode( + ctx, + &ViewDir{ + ctx: n.ctx, + sys: n.sys, + filter: filter, + wd: n.wd, + relays: n.relays, + }, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), true) + } else { + for ie := range n.sys.Pool.FetchMany(ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { + basename, inode := n.create(ctx, n, ie.Event) + n.AddChild(basename, inode, true) + } } - aMonthAgo := now - 30*24*60*60 - n.filter.Since = &aMonthAgo - - for ie := range n.sys.Pool.FetchMany(ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { - e := CreateEventDir(ctx, n, n.wd, ie.Event) - n.AddChild(ie.Event.ID, e, true) - } - - filter := n.filter - filter.Until = &aMonthAgo - - n.AddChild("@previous", n.NewPersistentInode( - ctx, - &ViewDir{ - ctx: n.ctx, - sys: n.sys, - filter: filter, - wd: n.wd, - relays: n.relays, - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), true) return fs.OK }