1
0
mirror of https://github.com/fiatjaf/nak.git synced 2025-07-30 16:58:29 -04:00

fs: creating articles (and presumably wikis); fixes and improvements to editing articles.

This commit is contained in:
fiatjaf
2025-03-13 01:13:34 -03:00
parent 931da4b0ae
commit 4b8c067e00
7 changed files with 357 additions and 236 deletions

25
fs.go

@@ -33,6 +33,11 @@ var fsCmd = &cli.Command{
return fmt.Errorf("invalid public key '%s'", pk) 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, DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
@@ -49,10 +54,19 @@ var fsCmd = &cli.Command{
} }
root := nostrfs.NewNostrRoot( root := nostrfs.NewNostrRoot(
context.WithValue(ctx, "log", log), context.WithValue(
context.WithValue(
ctx,
"log", log,
),
"logverbose", logverbose,
),
sys, sys,
kr, kr,
mountpoint, mountpoint,
nostrfs.Options{
AutoPublishTimeout: c.Duration("auto-publish"),
},
) )
// create the server // create the server
@@ -60,9 +74,10 @@ var fsCmd = &cli.Command{
timeout := time.Second * 120 timeout := time.Second * 120
server, err := fs.Mount(mountpoint, root, &fs.Options{ server, err := fs.Mount(mountpoint, root, &fs.Options{
MountOptions: fuse.MountOptions{ MountOptions: fuse.MountOptions{
Debug: isVerbose, Debug: isVerbose,
Name: "nak", Name: "nak",
FsName: "nak", FsName: "nak",
RememberInodes: true,
}, },
AttrTimeout: &timeout, AttrTimeout: &timeout,
EntryTimeout: &timeout, EntryTimeout: &timeout,
@@ -71,7 +86,7 @@ var fsCmd = &cli.Command{
if err != nil { if err != nil {
return fmt.Errorf("mount failed: %w", err) return fmt.Errorf("mount failed: %w", err)
} }
log("ok\n") log("ok.\n")
// setup signal handling for clean unmount // setup signal handling for clean unmount
ch := make(chan os.Signal, 1) ch := make(chan os.Signal, 1)

@@ -30,30 +30,29 @@ type EntityDir struct {
root *NostrRoot root *NostrRoot
publisher *debouncer.Debouncer publisher *debouncer.Debouncer
extension string
event *nostr.Event event *nostr.Event
updating struct { updating struct {
title string title string
content string content string
publishedAt uint64
} }
} }
var ( var (
_ = (fs.NodeOnAdder)((*EntityDir)(nil)) _ = (fs.NodeOnAdder)((*EntityDir)(nil))
_ = (fs.NodeGetattrer)((*EntityDir)(nil)) _ = (fs.NodeGetattrer)((*EntityDir)(nil))
_ = (fs.NodeSetattrer)((*EntityDir)(nil))
_ = (fs.NodeCreater)((*EntityDir)(nil)) _ = (fs.NodeCreater)((*EntityDir)(nil))
_ = (fs.NodeUnlinker)((*EntityDir)(nil)) _ = (fs.NodeUnlinker)((*EntityDir)(nil))
) )
func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
publishedAt := uint64(e.event.CreatedAt) out.Ctime = uint64(e.event.CreatedAt)
out.Ctime = publishedAt if e.updating.publishedAt != 0 {
out.Mtime = e.updating.publishedAt
if tag := e.event.Tags.Find("published_at"); tag != nil { } else {
publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) out.Mtime = e.PublishedAt()
} }
out.Mtime = publishedAt
return fs.OK return fs.OK
} }
@@ -64,8 +63,10 @@ func (e *EntityDir) Create(
mode uint32, mode uint32,
out *fuse.EntryOut, out *fuse.EntryOut,
) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { ) (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 // 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() e.publisher.Flush()
return nil, nil, 0, syscall.ENOTDIR 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 { func (e *EntityDir) Unlink(ctx context.Context, name string) syscall.Errno {
switch name { switch name {
case "content" + e.extension: case "content" + kindToExtension(e.event.Kind):
e.updating.content = e.event.Content e.updating.content = e.event.Content
return syscall.ENOTDIR return syscall.ENOTDIR
case "title": case "title":
e.updating.title = "" e.updating.title = e.Title()
if titleTag := e.event.Tags.Find("title"); titleTag != nil {
e.updating.title = titleTag[1]
}
return syscall.ENOTDIR return syscall.ENOTDIR
default: default:
return syscall.EINTR 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) { func (e *EntityDir) OnAdd(_ context.Context) {
log := e.root.ctx.Value("log").(func(msg string, args ...any)) 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) npub, _ := nip19.EncodePublicKey(e.event.PubKey)
e.AddChild("@author", e.NewPersistentInode( e.AddChild("@author", e.NewPersistentInode(
@@ -132,42 +131,35 @@ func (e *EntityDir) OnAdd(_ context.Context) {
fs.StableAttr{}, fs.StableAttr{},
), true) ), true)
if e.root.signer == nil { if e.root.signer == nil || e.root.rootPubKey != e.event.PubKey {
// read-only // read-only
e.AddChild("title", e.NewPersistentInode( e.AddChild("title", e.NewPersistentInode(
e.root.ctx, e.root.ctx,
&DeterministicFile{ &DeterministicFile{
get: func() (ctime uint64, mtime uint64, data string) { get: func() (ctime uint64, mtime uint64, data string) {
var title string return uint64(e.event.CreatedAt), e.PublishedAt(), e.Title()
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
}, },
}, },
fs.StableAttr{}, fs.StableAttr{},
), true) ), true)
e.AddChild("content."+e.extension, e.NewPersistentInode( e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode(
e.root.ctx, e.root.ctx,
&DeterministicFile{ &DeterministicFile{
get: func() (ctime uint64, mtime uint64, data string) { 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{}, fs.StableAttr{},
), true) ), true)
} else { } else {
// writeable // writeable
if tag := e.event.Tags.Find("title"); tag != nil { e.updating.title = e.Title()
e.updating.title = tag[1] e.updating.publishedAt = e.PublishedAt()
}
e.updating.content = e.event.Content e.updating.content = e.event.Content
e.AddChild("title", e.NewPersistentInode( e.AddChild("title", e.NewPersistentInode(
e.root.ctx, 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") log("title updated")
e.updating.title = strings.TrimSpace(s) e.updating.title = strings.TrimSpace(s)
e.handleWrite() e.handleWrite()
@@ -175,9 +167,9 @@ func (e *EntityDir) OnAdd(_ context.Context) {
fs.StableAttr{}, fs.StableAttr{},
), true) ), true)
e.AddChild("content."+e.extension, e.NewPersistentInode( e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode(
e.root.ctx, 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") log("content updated")
e.updating.content = strings.TrimSpace(s) e.updating.content = strings.TrimSpace(s)
e.handleWrite() 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() { func (e *EntityDir) handleWrite() {
log := e.root.ctx.Value("log").(func(msg string, args ...any)) 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() { if e.root.opts.AutoPublishTimeout.Hours() < 24*365 {
log(", timer reset") 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() { if !e.publisher.IsRunning() {
log("- `touch publish` to publish immediately\n") 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() { 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 { if e.Title() == e.updating.title && e.event.Content == e.updating.content {
log("back into the previous state, not publishing.\n") log("not modified, publish canceled.\n")
return return
} }
@@ -278,7 +300,7 @@ func (e *EntityDir) handleWrite() {
Tags: make(nostr.Tags, len(e.event.Tags)), Tags: make(nostr.Tags, len(e.event.Tags)),
CreatedAt: nostr.Now(), 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 e.updating.title != "" {
if titleTag := evt.Tags.Find("title"); titleTag != nil { if titleTag := evt.Tags.Find("title"); titleTag != nil {
titleTag[1] = e.updating.title titleTag[1] = e.updating.title
@@ -286,24 +308,42 @@ func (e *EntityDir) handleWrite() {
evt.Tags = append(evt.Tags, nostr.Tag{"title", e.updating.title}) 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" tag
"published_at", publishedAtStr := strconv.FormatUint(e.updating.publishedAt, 10)
strconv.FormatInt(int64(e.event.CreatedAt), 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) { for ref := range nip27.ParseReferences(evt) {
tag := ref.Pointer.AsTag() 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) evt.Tags = append(evt.Tags, tag)
} }
} }
// sign and publish
if err := e.root.signer.SignEvent(e.root.ctx, &evt); err != nil { if err := e.root.signer.SignEvent(e.root.ctx, &evt); err != nil {
log("failed to sign: '%s'.\n", err) log("failed to sign: '%s'.\n", err)
return return
} }
logverbose("%s\n", evt)
relays := e.root.sys.FetchWriteRelays(e.root.ctx, evt.PubKey, 8) 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)) log("publishing to %d relays... ", len(relays))
success := false success := false
first := true first := true
@@ -330,6 +370,7 @@ func (e *EntityDir) handleWrite() {
if success { if success {
e.event = &evt e.event = &evt
log("event updated locally.\n") log("event updated locally.\n")
e.updating.publishedAt = uint64(evt.CreatedAt) // set this so subsequent edits get the correct value
} else { } else {
log("failed.\n") log("failed.\n")
} }
@@ -348,17 +389,16 @@ func (r *NostrRoot) FetchAndCreateEntityDir(
return nil, fmt.Errorf("failed to fetch: %w", err) 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( func (r *NostrRoot) CreateEntityDir(
parent fs.InodeEmbedder, parent fs.InodeEmbedder,
extension string,
event *nostr.Event, event *nostr.Event,
) *fs.Inode { ) *fs.Inode {
return parent.EmbeddedInode().NewPersistentInode( return parent.EmbeddedInode().NewPersistentInode(
r.ctx, r.ctx,
&EntityDir{root: r, event: event, publisher: debouncer.New(time.Second * 30), extension: extension}, &EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishTimeout)},
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, fs.StableAttr{Mode: syscall.S_IFDIR},
) )
} }

@@ -2,6 +2,17 @@ package nostrfs
import "strconv" 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 { func hexToUint64(hexStr string) uint64 {
v, _ := strconv.ParseUint(hexStr[16:32], 16, 64) v, _ := strconv.ParseUint(hexStr[16:32], 16, 64)
return v return v

@@ -10,10 +10,12 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/fatih/color"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/liamg/magic" "github.com/liamg/magic"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
) )
type NpubDir struct { type NpubDir struct {
@@ -23,28 +25,36 @@ type NpubDir struct {
fetched atomic.Bool fetched atomic.Bool
} }
var _ = (fs.NodeOnAdder)((*NpubDir)(nil))
func (r *NostrRoot) CreateNpubDir( func (r *NostrRoot) CreateNpubDir(
parent fs.InodeEmbedder, parent fs.InodeEmbedder,
pointer nostr.ProfilePointer, pointer nostr.ProfilePointer,
signer nostr.Signer, signer nostr.Signer,
) *fs.Inode { ) *fs.Inode {
npubdir := &NpubDir{root: r, pointer: pointer} npubdir := &NpubDir{root: r, pointer: pointer}
h := parent.EmbeddedInode().NewPersistentInode( return parent.EmbeddedInode().NewPersistentInode(
r.ctx, r.ctx,
npubdir, npubdir,
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(pointer.PublicKey)}, 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( h.AddChild("pubkey", h.NewPersistentInode(
r.ctx, h.root.ctx,
&fs.MemRegularFile{Data: []byte(pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}}, &fs.MemRegularFile{Data: []byte(h.pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}},
fs.StableAttr{}, fs.StableAttr{},
), true) ), true)
go func() { 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 { if pm.Event == nil {
return return
} }
@@ -53,7 +63,7 @@ func (r *NostrRoot) CreateNpubDir(
h.AddChild( h.AddChild(
"metadata.json", "metadata.json",
h.NewPersistentInode( h.NewPersistentInode(
r.ctx, h.root.ctx,
&fs.MemRegularFile{ &fs.MemRegularFile{
Data: metadataj, Data: metadataj,
Attr: fuse.Attr{ Attr: fuse.Attr{
@@ -66,11 +76,11 @@ func (r *NostrRoot) CreateNpubDir(
true, true,
) )
ctx, cancel := context.WithTimeout(r.ctx, time.Second*20) ctx, cancel := context.WithTimeout(h.root.ctx, time.Second*20)
defer cancel() defer cancel()
r, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil) req, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil)
if err == nil { if err == nil {
resp, err := http.DefaultClient.Do(r) resp, err := http.DefaultClient.Do(req)
if err == nil { if err == nil {
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < 300 { if resp.StatusCode < 300 {
@@ -98,145 +108,152 @@ func (r *NostrRoot) CreateNpubDir(
} }
}() }()
h.AddChild( if h.GetChild("notes") == nil {
"notes", h.AddChild(
h.NewPersistentInode( "notes",
r.ctx, h.NewPersistentInode(
&ViewDir{ h.root.ctx,
root: r, &ViewDir{
filter: nostr.Filter{ root: h.root,
Kinds: []int{1}, filter: nostr.Filter{
Authors: []string{pointer.PublicKey}, Kinds: []int{1},
Authors: []string{h.pointer.PublicKey},
},
paginate: true,
relays: relays,
replaceable: false,
}, },
paginate: true, fs.StableAttr{Mode: syscall.S_IFDIR},
relays: relays, ),
replaceable: false, true,
extension: "txt", )
}, }
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
h.AddChild( if h.GetChild("comments") == nil {
"comments", h.AddChild(
h.NewPersistentInode( "comments",
r.ctx, h.NewPersistentInode(
&ViewDir{ h.root.ctx,
root: r, &ViewDir{
filter: nostr.Filter{ root: h.root,
Kinds: []int{1111}, filter: nostr.Filter{
Authors: []string{pointer.PublicKey}, Kinds: []int{1111},
Authors: []string{h.pointer.PublicKey},
},
paginate: true,
relays: relays,
replaceable: false,
}, },
paginate: true, fs.StableAttr{Mode: syscall.S_IFDIR},
relays: relays, ),
replaceable: false, true,
extension: "txt", )
}, }
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
h.AddChild( if h.GetChild("photos") == nil {
"photos", h.AddChild(
h.NewPersistentInode( "photos",
r.ctx, h.NewPersistentInode(
&ViewDir{ h.root.ctx,
root: r, &ViewDir{
filter: nostr.Filter{ root: h.root,
Kinds: []int{20}, filter: nostr.Filter{
Authors: []string{pointer.PublicKey}, Kinds: []int{20},
Authors: []string{h.pointer.PublicKey},
},
paginate: true,
relays: relays,
replaceable: false,
}, },
paginate: true, fs.StableAttr{Mode: syscall.S_IFDIR},
relays: relays, ),
replaceable: false, true,
extension: "txt", )
}, }
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
h.AddChild( if h.GetChild("videos") == nil {
"videos", h.AddChild(
h.NewPersistentInode( "videos",
r.ctx, h.NewPersistentInode(
&ViewDir{ h.root.ctx,
root: r, &ViewDir{
filter: nostr.Filter{ root: h.root,
Kinds: []int{21, 22}, filter: nostr.Filter{
Authors: []string{pointer.PublicKey}, Kinds: []int{21, 22},
Authors: []string{h.pointer.PublicKey},
},
paginate: false,
relays: relays,
replaceable: false,
}, },
paginate: false, fs.StableAttr{Mode: syscall.S_IFDIR},
relays: relays, ),
replaceable: false, true,
extension: "txt", )
}, }
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
h.AddChild( if h.GetChild("highlights") == nil {
"highlights", h.AddChild(
h.NewPersistentInode( "highlights",
r.ctx, h.NewPersistentInode(
&ViewDir{ h.root.ctx,
root: r, &ViewDir{
filter: nostr.Filter{ root: h.root,
Kinds: []int{9802}, filter: nostr.Filter{
Authors: []string{pointer.PublicKey}, Kinds: []int{9802},
Authors: []string{h.pointer.PublicKey},
},
paginate: false,
relays: relays,
replaceable: false,
}, },
paginate: false, fs.StableAttr{Mode: syscall.S_IFDIR},
relays: relays, ),
replaceable: false, true,
extension: "txt", )
}, }
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
h.AddChild( if h.GetChild("articles") == nil {
"articles", h.AddChild(
h.NewPersistentInode( "articles",
r.ctx, h.NewPersistentInode(
&ViewDir{ h.root.ctx,
root: r, &ViewDir{
filter: nostr.Filter{ root: h.root,
Kinds: []int{30023}, filter: nostr.Filter{
Authors: []string{pointer.PublicKey}, Kinds: []int{30023},
Authors: []string{h.pointer.PublicKey},
},
paginate: false,
relays: relays,
replaceable: true,
createable: true,
}, },
paginate: false, fs.StableAttr{Mode: syscall.S_IFDIR},
relays: relays, ),
replaceable: true, true,
extension: "md", )
}, }
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
h.AddChild( if h.GetChild("wiki") == nil {
"wiki", h.AddChild(
h.NewPersistentInode( "wiki",
r.ctx, h.NewPersistentInode(
&ViewDir{ h.root.ctx,
root: r, &ViewDir{
filter: nostr.Filter{ root: h.root,
Kinds: []int{30818}, filter: nostr.Filter{
Authors: []string{pointer.PublicKey}, Kinds: []int{30818},
Authors: []string{h.pointer.PublicKey},
},
paginate: false,
relays: relays,
replaceable: true,
createable: true,
}, },
paginate: false, fs.StableAttr{Mode: syscall.S_IFDIR},
relays: relays, ),
replaceable: true, true,
extension: "adoc", )
}, }
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
return h
} }

@@ -14,6 +14,10 @@ import (
"github.com/nbd-wtf/go-nostr/sdk" "github.com/nbd-wtf/go-nostr/sdk"
) )
type Options struct {
AutoPublishTimeout time.Duration // a negative number means do not publish
}
type NostrRoot struct { type NostrRoot struct {
fs.Inode fs.Inode
@@ -22,11 +26,13 @@ type NostrRoot struct {
sys *sdk.System sys *sdk.System
rootPubKey string rootPubKey string
signer nostr.Signer signer nostr.Signer
opts Options
} }
var _ = (fs.NodeOnAdder)((*NostrRoot)(nil)) 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) pubkey, _ := user.GetPublicKey(ctx)
abs, _ := filepath.Abs(mountpoint) abs, _ := filepath.Abs(mountpoint)
@@ -41,6 +47,8 @@ func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpo
rootPubKey: pubkey, rootPubKey: pubkey,
signer: signer, signer: signer,
wd: abs, wd: abs,
opts: o,
} }
} }
@@ -49,36 +57,40 @@ func (r *NostrRoot) OnAdd(_ context.Context) {
return return
} }
// add our contacts go func() {
fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey) time.Sleep(time.Millisecond * 100)
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,
)
}
// add ourselves // add our contacts
npub, _ := nip19.EncodePublicKey(r.rootPubKey) fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey)
if r.GetChild(npub) == nil { for _, f := range fl.Items {
pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey} 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( // add ourselves
npub, npub, _ := nip19.EncodePublicKey(r.rootPubKey)
r.CreateNpubDir(r, pointer, r.signer), if r.GetChild(npub) == nil {
true, pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey}
)
}
// add a link to ourselves r.AddChild(
r.AddChild("@me", r.NewPersistentInode( npub,
r.ctx, r.CreateNpubDir(r, pointer, r.signer),
&fs.MemSymlink{Data: []byte(r.wd + "/" + npub)}, true,
fs.StableAttr{Mode: syscall.S_IFLNK}, )
), 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) { func (r *NostrRoot) Lookup(_ context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {

@@ -18,12 +18,13 @@ type ViewDir struct {
paginate bool paginate bool
relays []string relays []string
replaceable bool replaceable bool
extension string createable bool
} }
var ( var (
_ = (fs.NodeOpendirer)((*ViewDir)(nil)) _ = (fs.NodeOpendirer)((*ViewDir)(nil))
_ = (fs.NodeGetattrer)((*ViewDir)(nil)) _ = (fs.NodeGetattrer)((*ViewDir)(nil))
_ = (fs.NodeMkdirer)((*ViewDir)(nil))
) )
func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { 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 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) { if n.fetched.CompareAndSwap(true, true) {
return fs.OK return fs.OK
} }
@@ -59,7 +60,6 @@ func (n *ViewDir) Opendir(_ context.Context) syscall.Errno {
root: n.root, root: n.root,
filter: filter, filter: filter,
relays: n.relays, relays: n.relays,
extension: n.extension,
replaceable: n.replaceable, replaceable: n.replaceable,
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
@@ -74,15 +74,39 @@ func (n *ViewDir) Opendir(_ context.Context) syscall.Errno {
if name == "" { if name == "" {
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 { } else {
for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter,
nostr.WithLabel("nakfs"), 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 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
}

@@ -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) { func (f *WriteableFile) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
end := int64(len(data)) + off
if int64(len(f.data)) < end { offset := int(off)
n := make([]byte, end) end := offset + len(data)
copy(n, f.data) if len(f.data) < end {
f.data = n 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)) f.onWrite(string(f.data))
return uint32(len(data)), fs.OK 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 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 return fs.OK
} }