mirror of
https://github.com/fiatjaf/nak.git
synced 2025-05-05 13:59:57 -04:00
fs: creating articles (and presumably wikis); fixes and improvements to editing articles.
This commit is contained in:
parent
931da4b0ae
commit
4b8c067e00
25
fs.go
25
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)
|
||||
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user