diff --git a/fs.go b/fs.go index 796c821..1d8d048 100644 --- a/fs.go +++ b/fs.go @@ -33,6 +33,11 @@ var fsCmd = &cli.Command{ return fmt.Errorf("invalid public key '%s'", pk) }, }, + &cli.DurationFlag{ + Name: "auto-publish", + Usage: "delay after which edited articles will be auto-published.", + Value: time.Hour * 24 * 365 * 2, + }, ), DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { @@ -49,10 +54,19 @@ var fsCmd = &cli.Command{ } root := nostrfs.NewNostrRoot( - context.WithValue(ctx, "log", log), + context.WithValue( + context.WithValue( + ctx, + "log", log, + ), + "logverbose", logverbose, + ), sys, kr, mountpoint, + nostrfs.Options{ + AutoPublishTimeout: c.Duration("auto-publish"), + }, ) // create the server @@ -60,9 +74,10 @@ var fsCmd = &cli.Command{ timeout := time.Second * 120 server, err := fs.Mount(mountpoint, root, &fs.Options{ MountOptions: fuse.MountOptions{ - Debug: isVerbose, - Name: "nak", - FsName: "nak", + Debug: isVerbose, + Name: "nak", + FsName: "nak", + RememberInodes: true, }, AttrTimeout: &timeout, EntryTimeout: &timeout, @@ -71,7 +86,7 @@ var fsCmd = &cli.Command{ if err != nil { return fmt.Errorf("mount failed: %w", err) } - log("ok\n") + log("ok.\n") // setup signal handling for clean unmount ch := make(chan os.Signal, 1) diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go index 9a242aa..bfdd275 100644 --- a/nostrfs/entitydir.go +++ b/nostrfs/entitydir.go @@ -30,30 +30,29 @@ type EntityDir struct { root *NostrRoot publisher *debouncer.Debouncer - extension string event *nostr.Event updating struct { - title string - content string + title string + content string + publishedAt uint64 } } var ( _ = (fs.NodeOnAdder)((*EntityDir)(nil)) _ = (fs.NodeGetattrer)((*EntityDir)(nil)) + _ = (fs.NodeSetattrer)((*EntityDir)(nil)) _ = (fs.NodeCreater)((*EntityDir)(nil)) _ = (fs.NodeUnlinker)((*EntityDir)(nil)) ) func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - publishedAt := uint64(e.event.CreatedAt) - out.Ctime = publishedAt - - if tag := e.event.Tags.Find("published_at"); tag != nil { - publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) + out.Ctime = uint64(e.event.CreatedAt) + if e.updating.publishedAt != 0 { + out.Mtime = e.updating.publishedAt + } else { + out.Mtime = e.PublishedAt() } - out.Mtime = publishedAt - return fs.OK } @@ -64,8 +63,10 @@ func (e *EntityDir) Create( mode uint32, out *fuse.EntryOut, ) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { - if name == "publish" { + if name == "publish" && e.publisher.IsRunning() { // this causes the publish process to be triggered faster + log := e.root.ctx.Value("log").(func(msg string, args ...any)) + log("publishing now!\n") e.publisher.Flush() return nil, nil, 0, syscall.ENOTDIR } @@ -75,26 +76,24 @@ func (e *EntityDir) Create( func (e *EntityDir) Unlink(ctx context.Context, name string) syscall.Errno { switch name { - case "content" + e.extension: + case "content" + kindToExtension(e.event.Kind): e.updating.content = e.event.Content return syscall.ENOTDIR case "title": - e.updating.title = "" - if titleTag := e.event.Tags.Find("title"); titleTag != nil { - e.updating.title = titleTag[1] - } + e.updating.title = e.Title() return syscall.ENOTDIR default: return syscall.EINTR } } +func (e *EntityDir) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno { + e.updating.publishedAt = in.Mtime + return fs.OK +} + func (e *EntityDir) OnAdd(_ context.Context) { log := e.root.ctx.Value("log").(func(msg string, args ...any)) - publishedAt := uint64(e.event.CreatedAt) - if tag := e.event.Tags.Find("published_at"); tag != nil { - publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) - } npub, _ := nip19.EncodePublicKey(e.event.PubKey) e.AddChild("@author", e.NewPersistentInode( @@ -132,42 +131,35 @@ func (e *EntityDir) OnAdd(_ context.Context) { fs.StableAttr{}, ), true) - if e.root.signer == nil { + if e.root.signer == nil || e.root.rootPubKey != e.event.PubKey { // read-only e.AddChild("title", e.NewPersistentInode( e.root.ctx, &DeterministicFile{ get: func() (ctime uint64, mtime uint64, data string) { - var title string - if tag := e.event.Tags.Find("title"); tag != nil { - title = tag[1] - } else { - title = e.event.Tags.GetD() - } - return uint64(e.event.CreatedAt), publishedAt, title + return uint64(e.event.CreatedAt), e.PublishedAt(), e.Title() }, }, fs.StableAttr{}, ), true) - e.AddChild("content."+e.extension, e.NewPersistentInode( + e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode( e.root.ctx, &DeterministicFile{ get: func() (ctime uint64, mtime uint64, data string) { - return uint64(e.event.CreatedAt), publishedAt, e.event.Content + return uint64(e.event.CreatedAt), e.PublishedAt(), e.event.Content }, }, fs.StableAttr{}, ), true) } else { // writeable - if tag := e.event.Tags.Find("title"); tag != nil { - e.updating.title = tag[1] - } + e.updating.title = e.Title() + e.updating.publishedAt = e.PublishedAt() e.updating.content = e.event.Content e.AddChild("title", e.NewPersistentInode( e.root.ctx, - e.root.NewWriteableFile(e.updating.title, uint64(e.event.CreatedAt), publishedAt, func(s string) { + e.root.NewWriteableFile(e.updating.title, uint64(e.event.CreatedAt), e.updating.publishedAt, func(s string) { log("title updated") e.updating.title = strings.TrimSpace(s) e.handleWrite() @@ -175,9 +167,9 @@ func (e *EntityDir) OnAdd(_ context.Context) { fs.StableAttr{}, ), true) - e.AddChild("content."+e.extension, e.NewPersistentInode( + e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode( e.root.ctx, - e.root.NewWriteableFile(e.updating.content, uint64(e.event.CreatedAt), publishedAt, func(s string) { + e.root.NewWriteableFile(e.updating.content, uint64(e.event.CreatedAt), e.updating.publishedAt, func(s string) { log("content updated") e.updating.content = strings.TrimSpace(s) e.handleWrite() @@ -254,21 +246,51 @@ func (e *EntityDir) OnAdd(_ context.Context) { } } +func (e *EntityDir) IsNew() bool { + return e.event.CreatedAt == 0 +} + +func (e *EntityDir) PublishedAt() uint64 { + if tag := e.event.Tags.Find("published_at"); tag != nil { + publishedAt, _ := strconv.ParseUint(tag[1], 10, 64) + return publishedAt + } + return uint64(e.event.CreatedAt) +} + +func (e *EntityDir) Title() string { + if tag := e.event.Tags.Find("title"); tag != nil { + return tag[1] + } + return "" +} + func (e *EntityDir) handleWrite() { log := e.root.ctx.Value("log").(func(msg string, args ...any)) + logverbose := e.root.ctx.Value("logverbose").(func(msg string, args ...any)) - if e.publisher.IsRunning() { - log(", timer reset") + if e.root.opts.AutoPublishTimeout.Hours() < 24*365 { + if e.publisher.IsRunning() { + log(", timer reset") + } + log(", will publish the ") + if e.IsNew() { + log("new") + } else { + log("updated") + } + log(" event in %d seconds...\n", e.root.opts.AutoPublishTimeout.Seconds()) + } else { + log(".\n") } - log(", will publish the updated event in 30 seconds...\n") if !e.publisher.IsRunning() { log("- `touch publish` to publish immediately\n") - log("- `rm title content." + e.extension + "` to erase and cancel the edits\n") + log("- `rm title content." + kindToExtension(e.event.Kind) + "` to erase and cancel the edits\n") } e.publisher.Call(func() { - if currentTitle := e.event.Tags.Find("title"); (currentTitle != nil && currentTitle[1] == e.updating.title) || (currentTitle == nil && e.updating.title == "") && e.updating.content == e.event.Content { - log("back into the previous state, not publishing.\n") + if e.Title() == e.updating.title && e.event.Content == e.updating.content { + log("not modified, publish canceled.\n") return } @@ -278,7 +300,7 @@ func (e *EntityDir) handleWrite() { Tags: make(nostr.Tags, len(e.event.Tags)), CreatedAt: nostr.Now(), } - copy(evt.Tags, e.event.Tags) + copy(evt.Tags, e.event.Tags) // copy tags because that's the rule if e.updating.title != "" { if titleTag := evt.Tags.Find("title"); titleTag != nil { titleTag[1] = e.updating.title @@ -286,24 +308,42 @@ func (e *EntityDir) handleWrite() { evt.Tags = append(evt.Tags, nostr.Tag{"title", e.updating.title}) } } - if publishedAtTag := evt.Tags.Find("published_at"); publishedAtTag == nil { - evt.Tags = append(evt.Tags, nostr.Tag{ - "published_at", - strconv.FormatInt(int64(e.event.CreatedAt), 10), - }) + + // "published_at" tag + publishedAtStr := strconv.FormatUint(e.updating.publishedAt, 10) + if publishedAtStr != "0" { + if publishedAtTag := evt.Tags.Find("published_at"); publishedAtTag != nil { + publishedAtTag[1] = publishedAtStr + } else { + evt.Tags = append(evt.Tags, nostr.Tag{"published_at", publishedAtStr}) + } } + + // add "p" tags from people mentioned and "q" tags from events mentioned for ref := range nip27.ParseReferences(evt) { tag := ref.Pointer.AsTag() - if existing := evt.Tags.FindWithValue(tag[0], tag[1]); existing == nil { + key := tag[0] + val := tag[1] + if key == "e" || key == "a" { + key = "q" + } + if existing := evt.Tags.FindWithValue(key, val); existing == nil { evt.Tags = append(evt.Tags, tag) } } + + // sign and publish if err := e.root.signer.SignEvent(e.root.ctx, &evt); err != nil { log("failed to sign: '%s'.\n", err) return } + logverbose("%s\n", evt) relays := e.root.sys.FetchWriteRelays(e.root.ctx, evt.PubKey, 8) + if len(relays) == 0 { + relays = e.root.sys.FetchOutboxRelays(e.root.ctx, evt.PubKey, 6) + } + log("publishing to %d relays... ", len(relays)) success := false first := true @@ -330,6 +370,7 @@ func (e *EntityDir) handleWrite() { if success { e.event = &evt log("event updated locally.\n") + e.updating.publishedAt = uint64(evt.CreatedAt) // set this so subsequent edits get the correct value } else { log("failed.\n") } @@ -348,17 +389,16 @@ func (r *NostrRoot) FetchAndCreateEntityDir( return nil, fmt.Errorf("failed to fetch: %w", err) } - return r.CreateEntityDir(parent, extension, event), nil + return r.CreateEntityDir(parent, event), nil } func (r *NostrRoot) CreateEntityDir( parent fs.InodeEmbedder, - extension string, event *nostr.Event, ) *fs.Inode { return parent.EmbeddedInode().NewPersistentInode( r.ctx, - &EntityDir{root: r, event: event, publisher: debouncer.New(time.Second * 30), extension: extension}, - fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, + &EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishTimeout)}, + fs.StableAttr{Mode: syscall.S_IFDIR}, ) } diff --git a/nostrfs/helpers.go b/nostrfs/helpers.go index 9cd82e5..e067e6b 100644 --- a/nostrfs/helpers.go +++ b/nostrfs/helpers.go @@ -2,6 +2,17 @@ package nostrfs import "strconv" +func kindToExtension(kind int) string { + switch kind { + case 30023: + return "md" + case 30818: + return "adoc" + default: + return "txt" + } +} + func hexToUint64(hexStr string) uint64 { v, _ := strconv.ParseUint(hexStr[16:32], 16, 64) return v diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index 633137e..b721002 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -10,10 +10,12 @@ import ( "syscall" "time" + "github.com/fatih/color" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" "github.com/liamg/magic" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" ) type NpubDir struct { @@ -23,28 +25,36 @@ type NpubDir struct { fetched atomic.Bool } +var _ = (fs.NodeOnAdder)((*NpubDir)(nil)) + func (r *NostrRoot) CreateNpubDir( parent fs.InodeEmbedder, pointer nostr.ProfilePointer, signer nostr.Signer, ) *fs.Inode { npubdir := &NpubDir{root: r, pointer: pointer} - h := parent.EmbeddedInode().NewPersistentInode( + return parent.EmbeddedInode().NewPersistentInode( r.ctx, npubdir, fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(pointer.PublicKey)}, ) +} - relays := r.sys.FetchOutboxRelays(r.ctx, pointer.PublicKey, 2) +func (h *NpubDir) OnAdd(_ context.Context) { + log := h.root.ctx.Value("log").(func(msg string, args ...any)) + + relays := h.root.sys.FetchOutboxRelays(h.root.ctx, h.pointer.PublicKey, 2) + log("- adding folder for %s with relays %s\n", + color.HiYellowString(nip19.EncodePointer(h.pointer)), color.HiGreenString("%v", relays)) h.AddChild("pubkey", h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{Data: []byte(pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}}, + h.root.ctx, + &fs.MemRegularFile{Data: []byte(h.pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}}, fs.StableAttr{}, ), true) go func() { - pm := r.sys.FetchProfileMetadata(r.ctx, pointer.PublicKey) + pm := h.root.sys.FetchProfileMetadata(h.root.ctx, h.pointer.PublicKey) if pm.Event == nil { return } @@ -53,7 +63,7 @@ func (r *NostrRoot) CreateNpubDir( h.AddChild( "metadata.json", h.NewPersistentInode( - r.ctx, + h.root.ctx, &fs.MemRegularFile{ Data: metadataj, Attr: fuse.Attr{ @@ -66,11 +76,11 @@ func (r *NostrRoot) CreateNpubDir( true, ) - ctx, cancel := context.WithTimeout(r.ctx, time.Second*20) + ctx, cancel := context.WithTimeout(h.root.ctx, time.Second*20) defer cancel() - r, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil) + req, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil) if err == nil { - resp, err := http.DefaultClient.Do(r) + resp, err := http.DefaultClient.Do(req) if err == nil { defer resp.Body.Close() if resp.StatusCode < 300 { @@ -98,145 +108,152 @@ func (r *NostrRoot) CreateNpubDir( } }() - h.AddChild( - "notes", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{1}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("notes") == nil { + h.AddChild( + "notes", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{1}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: true, + relays: relays, + replaceable: false, }, - paginate: true, - relays: relays, - replaceable: false, - extension: "txt", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } - h.AddChild( - "comments", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{1111}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("comments") == nil { + h.AddChild( + "comments", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{1111}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: true, + relays: relays, + replaceable: false, }, - paginate: true, - relays: relays, - replaceable: false, - extension: "txt", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } - h.AddChild( - "photos", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{20}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("photos") == nil { + h.AddChild( + "photos", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{20}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: true, + relays: relays, + replaceable: false, }, - paginate: true, - relays: relays, - replaceable: false, - extension: "txt", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } - h.AddChild( - "videos", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{21, 22}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("videos") == nil { + h.AddChild( + "videos", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{21, 22}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: false, + relays: relays, + replaceable: false, }, - paginate: false, - relays: relays, - replaceable: false, - extension: "txt", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } - h.AddChild( - "highlights", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{9802}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("highlights") == nil { + h.AddChild( + "highlights", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{9802}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: false, + relays: relays, + replaceable: false, }, - paginate: false, - relays: relays, - replaceable: false, - extension: "txt", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } - h.AddChild( - "articles", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{30023}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("articles") == nil { + h.AddChild( + "articles", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{30023}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: false, + relays: relays, + replaceable: true, + createable: true, }, - paginate: false, - relays: relays, - replaceable: true, - extension: "md", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } - h.AddChild( - "wiki", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{30818}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("wiki") == nil { + h.AddChild( + "wiki", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{30818}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: false, + relays: relays, + replaceable: true, + createable: true, }, - paginate: false, - relays: relays, - replaceable: true, - extension: "adoc", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) - - return h + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } } diff --git a/nostrfs/root.go b/nostrfs/root.go index c36ec1a..510969c 100644 --- a/nostrfs/root.go +++ b/nostrfs/root.go @@ -14,6 +14,10 @@ import ( "github.com/nbd-wtf/go-nostr/sdk" ) +type Options struct { + AutoPublishTimeout time.Duration // a negative number means do not publish +} + type NostrRoot struct { fs.Inode @@ -22,11 +26,13 @@ type NostrRoot struct { sys *sdk.System rootPubKey string signer nostr.Signer + + opts Options } var _ = (fs.NodeOnAdder)((*NostrRoot)(nil)) -func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpoint string) *NostrRoot { +func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpoint string, o Options) *NostrRoot { pubkey, _ := user.GetPublicKey(ctx) abs, _ := filepath.Abs(mountpoint) @@ -41,6 +47,8 @@ func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpo rootPubKey: pubkey, signer: signer, wd: abs, + + opts: o, } } @@ -49,36 +57,40 @@ func (r *NostrRoot) OnAdd(_ context.Context) { return } - // add our contacts - fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey) - for _, f := range fl.Items { - pointer := nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}} - npub, _ := nip19.EncodePublicKey(f.Pubkey) - r.AddChild( - npub, - r.CreateNpubDir(r, pointer, nil), - true, - ) - } + go func() { + time.Sleep(time.Millisecond * 100) - // add ourselves - npub, _ := nip19.EncodePublicKey(r.rootPubKey) - if r.GetChild(npub) == nil { - pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey} + // add our contacts + fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey) + for _, f := range fl.Items { + pointer := nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}} + npub, _ := nip19.EncodePublicKey(f.Pubkey) + r.AddChild( + npub, + r.CreateNpubDir(r, pointer, nil), + true, + ) + } - r.AddChild( - npub, - r.CreateNpubDir(r, pointer, r.signer), - true, - ) - } + // add ourselves + npub, _ := nip19.EncodePublicKey(r.rootPubKey) + if r.GetChild(npub) == nil { + pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey} - // add a link to ourselves - r.AddChild("@me", r.NewPersistentInode( - r.ctx, - &fs.MemSymlink{Data: []byte(r.wd + "/" + npub)}, - fs.StableAttr{Mode: syscall.S_IFLNK}, - ), true) + r.AddChild( + npub, + r.CreateNpubDir(r, pointer, r.signer), + true, + ) + } + + // add a link to ourselves + r.AddChild("@me", r.NewPersistentInode( + r.ctx, + &fs.MemSymlink{Data: []byte(r.wd + "/" + npub)}, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) + }() } func (r *NostrRoot) Lookup(_ context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index b4b94a1..838ab05 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -18,12 +18,13 @@ type ViewDir struct { paginate bool relays []string replaceable bool - extension string + createable bool } var ( _ = (fs.NodeOpendirer)((*ViewDir)(nil)) _ = (fs.NodeGetattrer)((*ViewDir)(nil)) + _ = (fs.NodeMkdirer)((*ViewDir)(nil)) ) func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { @@ -37,7 +38,7 @@ func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) return fs.OK } -func (n *ViewDir) Opendir(_ context.Context) syscall.Errno { +func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno { if n.fetched.CompareAndSwap(true, true) { return fs.OK } @@ -59,7 +60,6 @@ func (n *ViewDir) Opendir(_ context.Context) syscall.Errno { root: n.root, filter: filter, relays: n.relays, - extension: n.extension, replaceable: n.replaceable, }, fs.StableAttr{Mode: syscall.S_IFDIR}, @@ -74,15 +74,39 @@ func (n *ViewDir) Opendir(_ context.Context) syscall.Errno { if name == "" { name = "_" } - n.AddChild(name, n.root.CreateEntityDir(n, n.extension, evt), true) + if n.GetChild(name) == nil { + n.AddChild(name, n.root.CreateEntityDir(n, evt), true) + } } } else { for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, nostr.WithLabel("nakfs"), ) { - n.AddChild(ie.Event.ID, n.root.CreateEventDir(n, ie.Event), true) + if n.GetChild(ie.Event.ID) == nil { + n.AddChild(ie.Event.ID, n.root.CreateEventDir(n, ie.Event), true) + } } } return fs.OK } + +func (n *ViewDir) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { + if !n.createable || n.root.signer == nil || n.root.rootPubKey != n.filter.Authors[0] { + return nil, syscall.ENOTSUP + } + + if n.replaceable { + // create a template event that can later be modified and published as new + return n.root.CreateEntityDir(n, &nostr.Event{ + PubKey: n.root.rootPubKey, + CreatedAt: 0, + Kind: n.filter.Kinds[0], + Tags: nostr.Tags{ + nostr.Tag{"d", name}, + }, + }), fs.OK + } + + return nil, syscall.ENOTSUP +} diff --git a/nostrfs/writeablefile.go b/nostrfs/writeablefile.go index 2c897fb..c37291a 100644 --- a/nostrfs/writeablefile.go +++ b/nostrfs/writeablefile.go @@ -48,16 +48,18 @@ func (f *WriteableFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandl func (f *WriteableFile) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) { f.mu.Lock() defer f.mu.Unlock() - end := int64(len(data)) + off - if int64(len(f.data)) < end { - n := make([]byte, end) - copy(n, f.data) - f.data = n + + offset := int(off) + end := offset + len(data) + if len(f.data) < end { + newData := make([]byte, offset+len(data)) + copy(newData, f.data) + f.data = newData } - copy(f.data[off:off+int64(len(data))], data) + copy(f.data[offset:], data) + f.data = f.data[0:end] f.onWrite(string(f.data)) - return uint32(len(data)), fs.OK } @@ -69,7 +71,7 @@ func (f *WriteableFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse return fs.OK } -func (f *WriteableFile) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno { +func (f *WriteableFile) Setattr(_ context.Context, _ fs.FileHandle, _ *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno { return fs.OK }