Compare commits

..

11 Commits

12 changed files with 1065 additions and 426 deletions

View File

@@ -25,7 +25,7 @@ jobs:
strategy:
matrix:
goos: [linux, freebsd, darwin, windows]
goarch: [arm, amd64, arm64, riscv64]
goarch: [amd64, arm64, riscv64]
exclude:
- goarch: arm64
goos: windows
@@ -33,11 +33,7 @@ jobs:
goos: windows
- goarch: riscv64
goos: darwin
- goarch: arm
goos: windows
- goarch: arm
goos: darwin
- goarch: arm
- goarch: arm64
goos: freebsd
steps:
- uses: actions/checkout@v3

54
fs.go
View File

@@ -22,7 +22,7 @@ var fsCmd = &cli.Command{
Usage: "mount a FUSE filesystem that exposes Nostr events as files.",
Description: `(experimental)`,
ArgsUsage: "<mountpoint>",
Flags: []cli.Flag{
Flags: append(defaultKeyFlags,
&cli.StringFlag{
Name: "pubkey",
Usage: "public key from where to to prepopulate directories",
@@ -33,7 +33,18 @@ var fsCmd = &cli.Command{
return fmt.Errorf("invalid public key '%s'", pk)
},
},
},
&cli.DurationFlag{
Name: "auto-publish-notes",
Usage: "delay after which new notes will be auto-published, set to -1 to not publish.",
Value: time.Second * 30,
},
&cli.DurationFlag{
Name: "auto-publish-articles",
Usage: "delay after which edited articles will be auto-published.",
Value: time.Hour * 24 * 365 * 2,
DefaultText: "basically infinite",
},
),
DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, c *cli.Command) error {
mountpoint := c.Args().First()
@@ -41,11 +52,37 @@ var fsCmd = &cli.Command{
return fmt.Errorf("must be called with a directory path to serve as the mountpoint as an argument")
}
var kr nostr.User
if signer, _, err := gatherKeyerFromArguments(ctx, c); err == nil {
kr = signer
} else {
kr = keyer.NewReadOnlyUser(c.String("pubkey"))
}
apnt := c.Duration("auto-publish-notes")
if apnt < 0 {
apnt = time.Hour * 24 * 365 * 3
}
apat := c.Duration("auto-publish-articles")
if apat < 0 {
apat = time.Hour * 24 * 365 * 3
}
root := nostrfs.NewNostrRoot(
ctx,
context.WithValue(
context.WithValue(
ctx,
"log", log,
),
"logverbose", logverbose,
),
sys,
keyer.NewReadOnlyUser(c.String("pubkey")),
kr,
mountpoint,
nostrfs.Options{
AutoPublishNotesTimeout: apnt,
AutoPublishArticlesTimeout: apat,
},
)
// create the server
@@ -53,9 +90,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,
@@ -64,7 +102,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)

27
go.mod
View File

@@ -1,29 +1,28 @@
module github.com/fiatjaf/nak
go 1.23.3
toolchain go1.23.4
go 1.24.1
require (
fiatjaf.com/lib v0.3.1
github.com/bep/debounce v1.2.1
github.com/btcsuite/btcd/btcec/v2 v2.3.4
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
github.com/fatih/color v1.16.0
github.com/fiatjaf/eventstore v0.15.0
github.com/fiatjaf/khatru v0.16.0
github.com/fiatjaf/eventstore v0.16.2
github.com/fiatjaf/khatru v0.17.4
github.com/hanwen/go-fuse/v2 v2.7.2
github.com/json-iterator/go v1.1.12
github.com/liamg/magic v0.0.1
github.com/mailru/easyjson v0.9.0
github.com/mark3labs/mcp-go v0.8.3
github.com/markusmobius/go-dateparser v1.2.3
github.com/nbd-wtf/go-nostr v0.51.0
github.com/nbd-wtf/go-nostr v0.51.6
github.com/urfave/cli/v3 v3.0.0-beta1
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
)
require (
fiatjaf.com/lib v0.2.0 // indirect
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/btcsuite/btcd v0.24.2 // indirect
@@ -58,7 +57,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
github.com/tetratelabs/wazero v1.8.0 // indirect
@@ -67,12 +66,12 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.58.0 // indirect
github.com/valyala/fasthttp v1.59.0 // indirect
github.com/wasilibs/go-re2 v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/text v0.23.0 // indirect
)

50
go.sum
View File

@@ -1,5 +1,5 @@
fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs=
fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
@@ -59,8 +59,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84=
github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
@@ -77,10 +77,10 @@ github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOU
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fiatjaf/eventstore v0.15.0 h1:5UXe0+vIb30/cYcOWipks8nR3g+X8W224TFy5yPzivk=
github.com/fiatjaf/eventstore v0.15.0/go.mod h1:KAsld5BhkmSck48aF11Txu8X+OGNmoabw4TlYVWqInc=
github.com/fiatjaf/khatru v0.16.0 h1:xgGwnnOqE3989wEWm7c/z6Y6g4X92BFe/Xp1UWQ3Zmc=
github.com/fiatjaf/khatru v0.16.0/go.mod h1:TLcMgPy3IAPh40VGYq6m+gxEMpDKHj+sumqcuvbSogc=
github.com/fiatjaf/eventstore v0.16.2 h1:h4rHwSwPcqAKqWUsAbYWUhDeSgm2Kp+PBkJc3FgBYu4=
github.com/fiatjaf/eventstore v0.16.2/go.mod h1:0gU8fzYO/bG+NQAVlHtJWOlt3JKKFefh5Xjj2d1dLIs=
github.com/fiatjaf/khatru v0.17.4 h1:VzcLUyBKMlP/CAG4iHJbDJmnZgzhbGLKLxJAUuLRogg=
github.com/fiatjaf/khatru v0.17.4/go.mod h1:VYQ7ZNhs3C1+E4gBnx+DtEgU0BrPdrl3XYF3H+mq6fg=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
@@ -96,6 +96,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -127,6 +129,8 @@ github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQe
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM=
github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
@@ -147,8 +151,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nbd-wtf/go-nostr v0.51.0 h1:Z6gir3lQmlbQGYkccEPbvHlfCydMWXD6bIqukR4DZqU=
github.com/nbd-wtf/go-nostr v0.51.0/go.mod h1:9PcGOZ+e1VOaLvcK0peT4dbip+/eS+eTWXR3HuexQrA=
github.com/nbd-wtf/go-nostr v0.51.6 h1:H51l39mp4dJztvtxjTNfNqNIxQyMoJMLSKt+1aGq3UU=
github.com/nbd-wtf/go-nostr v0.51.6/go.mod h1:so45r53GkZq+8vkdIW2jBlKEjdHyxEMrl/1g9tEoSFQ=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -162,8 +166,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
@@ -195,8 +199,8 @@ github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjc
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw=
github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg=
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
@@ -210,17 +214,17 @@ golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -238,8 +242,8 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -258,4 +262,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@@ -0,0 +1,50 @@
package nostrfs
import (
"context"
"syscall"
"unsafe"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type DeterministicFile struct {
fs.Inode
get func() (ctime, mtime uint64, data string)
}
var (
_ = (fs.NodeOpener)((*DeterministicFile)(nil))
_ = (fs.NodeReader)((*DeterministicFile)(nil))
_ = (fs.NodeGetattrer)((*DeterministicFile)(nil))
)
func (r *NostrRoot) NewDeterministicFile(get func() (ctime, mtime uint64, data string)) *DeterministicFile {
return &DeterministicFile{
get: get,
}
}
func (f *DeterministicFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
return nil, fuse.FOPEN_KEEP_CACHE, fs.OK
}
func (f *DeterministicFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
var content string
out.Mode = 0444
out.Ctime, out.Mtime, content = f.get()
out.Size = uint64(len(content))
return fs.OK
}
func (f *DeterministicFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
_, _, content := f.get()
data := unsafe.Slice(unsafe.StringData(content), len(content))
end := int(off) + len(dest)
if end > len(data) {
end = len(data)
}
return fuse.ReadResultData(data[off:end]), fs.OK
}

View File

@@ -9,9 +9,13 @@ import (
"net/http"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"unsafe"
"fiatjaf.com/lib/debouncer"
"github.com/fatih/color"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/nbd-wtf/go-nostr"
@@ -23,141 +27,169 @@ import (
type EntityDir struct {
fs.Inode
ctx context.Context
wd string
evt *nostr.Event
root *NostrRoot
publisher *debouncer.Debouncer
event *nostr.Event
updating struct {
title string
content string
publishedAt uint64
}
}
var _ = (fs.NodeGetattrer)((*EntityDir)(nil))
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(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)
func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
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
}
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)
func (e *EntityDir) Create(
_ context.Context,
name string,
flags uint32,
mode uint32,
out *fuse.EntryOut,
) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
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
}
return CreateEntityDir(ctx, parent, wd, extension, event), nil
return nil, nil, 0, syscall.ENOTSUP
}
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)
func (e *EntityDir) Unlink(ctx context.Context, name string) syscall.Errno {
switch name {
case "content" + kindToExtension(e.event.Kind):
e.updating.content = e.event.Content
return syscall.ENOTDIR
case "title":
e.updating.title = e.Title()
return syscall.ENOTDIR
default:
return syscall.EINTR
}
}
npub, _ := nip19.EncodePublicKey(event.PubKey)
h.AddChild("@author", h.NewPersistentInode(
ctx,
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))
npub, _ := nip19.EncodePublicKey(e.event.PubKey)
e.AddChild("@author", e.NewPersistentInode(
e.root.ctx,
&fs.MemSymlink{
Data: []byte(wd + "/" + npub),
Data: []byte(e.root.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)),
e.AddChild("event.json", e.NewPersistentInode(
e.root.ctx,
&DeterministicFile{
get: func() (ctime uint64, mtime uint64, data string) {
eventj, _ := json.MarshalIndent(e.event, "", " ")
return uint64(e.event.CreatedAt),
uint64(e.event.CreatedAt),
unsafe.String(unsafe.SliceData(eventj), len(eventj))
},
},
fs.StableAttr{},
), true)
h.AddChild("identifier", h.NewPersistentInode(
ctx,
e.AddChild("identifier", e.NewPersistentInode(
e.root.ctx,
&fs.MemRegularFile{
Data: []byte(event.Tags.GetD()),
Data: []byte(e.event.Tags.GetD()),
Attr: fuse.Attr{
Mode: 0444,
Ctime: uint64(event.CreatedAt),
Mtime: uint64(publishedAt),
Size: uint64(len(event.Tags.GetD())),
Ctime: uint64(e.event.CreatedAt),
Mtime: uint64(e.event.CreatedAt),
Size: uint64(len(e.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])),
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) {
return uint64(e.event.CreatedAt), e.PublishedAt(), e.Title()
},
},
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)),
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), e.PublishedAt(), e.event.Content
},
},
},
fs.StableAttr{},
), true)
fs.StableAttr{},
), true)
} else {
// writeable
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), e.updating.publishedAt, func(s string) {
log("title updated")
e.updating.title = strings.TrimSpace(s)
e.handleWrite()
}),
fs.StableAttr{},
), true)
e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode(
e.root.ctx,
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()
}),
fs.StableAttr{},
), true)
}
var refsdir *fs.Inode
i := 0
for ref := range nip27.ParseReferences(*event) {
for ref := range nip27.ParseReferences(*e.event) {
i++
if refsdir == nil {
refsdir = h.NewPersistentInode(ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
h.AddChild("references", refsdir, true)
refsdir = e.NewPersistentInode(e.root.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
e.root.AddChild("references", refsdir, true)
}
refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode(
ctx,
e.root.ctx,
&fs.MemSymlink{
Data: []byte(wd + "/" + nip19.EncodePointer(ref.Pointer)),
Data: []byte(e.root.wd + "/" + nip19.EncodePointer(ref.Pointer)),
},
fs.StableAttr{Mode: syscall.S_IFLNK},
), true)
@@ -167,26 +199,29 @@ func CreateEntityDir(
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 = e.NewPersistentInode(e.root.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR})
e.AddChild("images", imagesdir, true)
}
imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode(
ctx,
e.root.ctx,
&AsyncFile{
ctx: ctx,
ctx: e.root.ctx,
load: func() ([]byte, nostr.Timestamp) {
ctx, cancel := context.WithTimeout(ctx, time.Second*20)
ctx, cancel := context.WithTimeout(e.root.ctx, time.Second*20)
defer cancel()
r, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
log("failed to load image %s: %s\n", url, err)
return nil, 0
}
resp, err := http.DefaultClient.Do(r)
if err != nil {
log("failed to load image %s: %s\n", url, err)
return nil, 0
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
log("failed to load image %s: %s\n", url, err)
return nil, 0
}
w := &bytes.Buffer{}
@@ -198,7 +233,7 @@ func CreateEntityDir(
), true)
}
images := nip92.ParseTags(event.Tags)
images := nip92.ParseTags(e.event.Tags)
for _, imeta := range images {
if imeta.URL == "" {
continue
@@ -206,9 +241,164 @@ func CreateEntityDir(
addImage(imeta.URL)
}
if tag := event.Tags.Find("image"); tag != nil {
if tag := e.event.Tags.Find("image"); tag != nil {
addImage(tag[1])
}
return h
}
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.root.opts.AutoPublishArticlesTimeout.Hours() < 24*365 {
if e.publisher.IsRunning() {
log(", timer reset")
}
log(", publishing the ")
if e.IsNew() {
log("new")
} else {
log("updated")
}
log(" event in %d seconds...\n", int(e.root.opts.AutoPublishArticlesTimeout.Seconds()))
} else {
log(".\n")
}
if !e.publisher.IsRunning() {
log("- `touch publish` to publish immediately\n")
log("- `rm title content." + kindToExtension(e.event.Kind) + "` to erase and cancel the edits\n")
}
e.publisher.Call(func() {
if e.Title() == e.updating.title && e.event.Content == e.updating.content {
log("not modified, publish canceled.\n")
return
}
evt := nostr.Event{
Kind: e.event.Kind,
Content: e.updating.content,
Tags: make(nostr.Tags, len(e.event.Tags)),
CreatedAt: nostr.Now(),
}
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
} else {
evt.Tags = append(evt.Tags, nostr.Tag{"title", e.updating.title})
}
}
// "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()
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, e.root.rootPubKey, 8)
if len(relays) == 0 {
relays = e.root.sys.FetchOutboxRelays(e.root.ctx, e.root.rootPubKey, 6)
}
log("publishing to %d relays... ", len(relays))
success := false
first := true
for res := range e.root.sys.Pool.PublishMany(e.root.ctx, relays, evt) {
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
if !ok {
cleanUrl = res.RelayURL
}
if !first {
log(", ")
}
first = false
if res.Error != nil {
log("%s: %s", color.RedString(cleanUrl), res.Error)
} else {
success = true
log("%s: ok", color.GreenString(cleanUrl))
}
}
log("\n")
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")
}
})
}
func (r *NostrRoot) FetchAndCreateEntityDir(
parent fs.InodeEmbedder,
extension string,
pointer nostr.EntityPointer,
) (*fs.Inode, error) {
event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{
WithRelays: false,
})
if err != nil {
return nil, fmt.Errorf("failed to fetch: %w", err)
}
return r.CreateEntityDir(parent, event), nil
}
func (r *NostrRoot) CreateEntityDir(
parent fs.InodeEmbedder,
event *nostr.Event,
) *fs.Inode {
return parent.EmbeddedInode().NewPersistentInode(
r.ctx,
&EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishArticlesTimeout)},
fs.StableAttr{Mode: syscall.S_IFDIR},
)
}

View File

@@ -31,52 +31,47 @@ type EventDir struct {
var _ = (fs.NodeGetattrer)((*EventDir)(nil))
func (e *EventDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
func (e *EventDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
out.Mtime = uint64(e.evt.CreatedAt)
return fs.OK
}
func FetchAndCreateEventDir(
ctx context.Context,
func (r *NostrRoot) FetchAndCreateEventDir(
parent fs.InodeEmbedder,
wd string,
sys *sdk.System,
pointer nostr.EventPointer,
) (*fs.Inode, error) {
event, _, err := sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{
event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{
WithRelays: false,
})
if err != nil {
return nil, fmt.Errorf("failed to fetch: %w", err)
}
return CreateEventDir(ctx, parent, wd, event), nil
return r.CreateEventDir(parent, event), nil
}
func CreateEventDir(
ctx context.Context,
func (r *NostrRoot) CreateEventDir(
parent fs.InodeEmbedder,
wd string,
event *nostr.Event,
) *fs.Inode {
h := parent.EmbeddedInode().NewPersistentInode(
ctx,
&EventDir{ctx: ctx, wd: wd, evt: event},
r.ctx,
&EventDir{ctx: r.ctx, wd: r.wd, evt: event},
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)},
)
npub, _ := nip19.EncodePublicKey(event.PubKey)
h.AddChild("@author", h.NewPersistentInode(
ctx,
r.ctx,
&fs.MemSymlink{
Data: []byte(wd + "/" + npub),
Data: []byte(r.wd + "/" + npub),
},
fs.StableAttr{Mode: syscall.S_IFLNK},
), true)
eventj, _ := json.MarshalIndent(event, "", " ")
h.AddChild("event.json", h.NewPersistentInode(
ctx,
r.ctx,
&fs.MemRegularFile{
Data: eventj,
Attr: fuse.Attr{
@@ -90,7 +85,7 @@ func CreateEventDir(
), true)
h.AddChild("id", h.NewPersistentInode(
ctx,
r.ctx,
&fs.MemRegularFile{
Data: []byte(event.ID),
Attr: fuse.Attr{
@@ -104,7 +99,7 @@ func CreateEventDir(
), true)
h.AddChild("content.txt", h.NewPersistentInode(
ctx,
r.ctx,
&fs.MemRegularFile{
Data: []byte(event.Content),
Attr: fuse.Attr{
@@ -122,13 +117,13 @@ func CreateEventDir(
for ref := range nip27.ParseReferences(*event) {
i++
if refsdir == nil {
refsdir = h.NewPersistentInode(ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
refsdir = h.NewPersistentInode(r.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
h.AddChild("references", refsdir, true)
}
refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode(
ctx,
r.ctx,
&fs.MemSymlink{
Data: []byte(wd + "/" + nip19.EncodePointer(ref.Pointer)),
Data: []byte(r.wd + "/" + nip19.EncodePointer(ref.Pointer)),
},
fs.StableAttr{Mode: syscall.S_IFLNK},
), true)
@@ -142,15 +137,15 @@ func CreateEventDir(
}
if imagesdir == nil {
in := &fs.Inode{}
imagesdir = h.NewPersistentInode(ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR})
imagesdir = h.NewPersistentInode(r.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR})
h.AddChild("images", imagesdir, true)
}
imagesdir.AddChild(filepath.Base(imeta.URL), imagesdir.NewPersistentInode(
ctx,
r.ctx,
&AsyncFile{
ctx: ctx,
ctx: r.ctx,
load: func() ([]byte, nostr.Timestamp) {
ctx, cancel := context.WithTimeout(ctx, time.Second*20)
ctx, cancel := context.WithTimeout(r.ctx, time.Second*20)
defer cancel()
r, err := http.NewRequestWithContext(ctx, "GET", imeta.URL, nil)
if err != nil {
@@ -177,9 +172,9 @@ func CreateEventDir(
if pointer := nip10.GetThreadRoot(event.Tags); pointer != nil {
nevent := nip19.EncodePointer(*pointer)
h.AddChild("@root", h.NewPersistentInode(
ctx,
r.ctx,
&fs.MemSymlink{
Data: []byte(wd + "/" + nevent),
Data: []byte(r.wd + "/" + nevent),
},
fs.StableAttr{Mode: syscall.S_IFLNK},
), true)
@@ -187,9 +182,9 @@ func CreateEventDir(
if pointer := nip10.GetImmediateParent(event.Tags); pointer != nil {
nevent := nip19.EncodePointer(*pointer)
h.AddChild("@parent", h.NewPersistentInode(
ctx,
r.ctx,
&fs.MemSymlink{
Data: []byte(wd + "/" + nevent),
Data: []byte(r.wd + "/" + nevent),
},
fs.StableAttr{Mode: syscall.S_IFLNK},
), true)
@@ -198,7 +193,7 @@ func CreateEventDir(
if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil {
if xp, ok := pointer.(nostr.ExternalPointer); ok {
h.AddChild("@root", h.NewPersistentInode(
ctx,
r.ctx,
&fs.MemRegularFile{
Data: []byte(`<!doctype html><meta http-equiv="refresh" content="0; url=` + xp.Thing + `" />`),
},
@@ -207,9 +202,9 @@ func CreateEventDir(
} else {
nevent := nip19.EncodePointer(pointer)
h.AddChild("@parent", h.NewPersistentInode(
ctx,
r.ctx,
&fs.MemSymlink{
Data: []byte(wd + "/" + nevent),
Data: []byte(r.wd + "/" + nevent),
},
fs.StableAttr{Mode: syscall.S_IFLNK},
), true)
@@ -218,7 +213,7 @@ func CreateEventDir(
if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil {
if xp, ok := pointer.(nostr.ExternalPointer); ok {
h.AddChild("@parent", h.NewPersistentInode(
ctx,
r.ctx,
&fs.MemRegularFile{
Data: []byte(`<!doctype html><meta http-equiv="refresh" content="0; url=` + xp.Thing + `" />`),
},
@@ -227,9 +222,9 @@ func CreateEventDir(
} else {
nevent := nip19.EncodePointer(pointer)
h.AddChild("@parent", h.NewPersistentInode(
ctx,
r.ctx,
&fs.MemSymlink{
Data: []byte(wd + "/" + nevent),
Data: []byte(r.wd + "/" + nevent),
},
fs.StableAttr{Mode: syscall.S_IFLNK},
), true)

View File

@@ -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

View File

@@ -1,228 +1,260 @@
package nostrfs
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"sync/atomic"
"syscall"
"time"
"github.com/bytedance/sonic"
"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"
sdk "github.com/nbd-wtf/go-nostr/sdk"
"github.com/nbd-wtf/go-nostr/nip19"
)
type NpubDir struct {
sys *sdk.System
fs.Inode
root *NostrRoot
pointer nostr.ProfilePointer
ctx context.Context
fetched atomic.Bool
}
func CreateNpubDir(
ctx context.Context,
sys *sdk.System,
var _ = (fs.NodeOnAdder)((*NpubDir)(nil))
func (r *NostrRoot) CreateNpubDir(
parent fs.InodeEmbedder,
wd string,
pointer nostr.ProfilePointer,
signer nostr.Signer,
) *fs.Inode {
npubdir := &NpubDir{ctx: ctx, sys: sys, pointer: pointer}
h := parent.EmbeddedInode().NewPersistentInode(
ctx,
npubdir := &NpubDir{root: r, pointer: pointer}
return parent.EmbeddedInode().NewPersistentInode(
r.ctx,
npubdir,
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(pointer.PublicKey)},
)
}
relays := sys.FetchOutboxRelays(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(
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)
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
go func() {
pm := h.root.sys.FetchProfileMetadata(h.root.ctx, h.pointer.PublicKey)
if pm.Event == nil {
return
}
metadataj, _ := json.MarshalIndent(pm, "", " ")
h.AddChild(
"metadata.json",
h.NewPersistentInode(
h.root.ctx,
&fs.MemRegularFile{
Data: metadataj,
Attr: fuse.Attr{
Mtime: uint64(pm.Event.CreatedAt),
Mode: 0444,
},
},
fs.StableAttr{},
),
true,
)
ctx, cancel := context.WithTimeout(h.root.ctx, time.Second*20)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil)
if err == nil {
resp, err := http.DefaultClient.Do(req)
if err == nil {
defer resp.Body.Close()
if resp.StatusCode < 300 {
b := &bytes.Buffer{}
io.Copy(b, resp.Body)
ext := "png"
if ft, err := magic.Lookup(b.Bytes()); err == nil {
ext = ft.Extension
}
return jsonb, ts
},
},
fs.StableAttr{},
),
true,
)
h.AddChild(
"notes",
h.NewPersistentInode(
ctx,
&ViewDir{
ctx: ctx,
sys: sys,
wd: wd,
filter: nostr.Filter{
Kinds: []int{1},
Authors: []string{pointer.PublicKey},
},
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},
),
true,
)
h.AddChild("picture."+ext, h.NewPersistentInode(
ctx,
&fs.MemRegularFile{
Data: b.Bytes(),
Attr: fuse.Attr{
Mtime: uint64(pm.Event.CreatedAt),
Mode: 0444,
},
},
fs.StableAttr{},
), true)
}
}
}
}()
h.AddChild(
"comments",
h.NewPersistentInode(
ctx,
&ViewDir{
ctx: ctx,
sys: sys,
wd: wd,
filter: nostr.Filter{
Kinds: []int{1111},
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,
createable: true,
},
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},
),
true,
)
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
}
h.AddChild(
"pictures",
h.NewPersistentInode(
ctx,
&ViewDir{
ctx: ctx,
sys: sys,
wd: wd,
filter: nostr.Filter{
Kinds: []int{20},
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,
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},
),
true,
)
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
}
h.AddChild(
"videos",
h.NewPersistentInode(
ctx,
&ViewDir{
ctx: ctx,
sys: sys,
wd: wd,
filter: nostr.Filter{
Kinds: []int{21, 22},
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,
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},
),
true,
)
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
}
h.AddChild(
"highlights",
h.NewPersistentInode(
ctx,
&ViewDir{
ctx: ctx,
sys: sys,
wd: wd,
filter: nostr.Filter{
Kinds: []int{9802},
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: 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},
),
true,
)
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
}
h.AddChild(
"articles",
h.NewPersistentInode(
ctx,
&ViewDir{
ctx: ctx,
sys: sys,
wd: wd,
filter: nostr.Filter{
Kinds: []int{30023},
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,
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{Mode: syscall.S_IFDIR},
),
true,
)
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},
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,
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,
)
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
}
return h
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,
},
fs.StableAttr{Mode: syscall.S_IFDIR},
),
true,
)
}
}

View File

@@ -14,6 +14,11 @@ import (
"github.com/nbd-wtf/go-nostr/sdk"
)
type Options struct {
AutoPublishNotesTimeout time.Duration
AutoPublishArticlesTimeout time.Duration
}
type NostrRoot struct {
fs.Inode
@@ -22,61 +27,74 @@ 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)
signer, _ := user.(nostr.Signer)
abs, _ := filepath.Abs(mountpoint)
var signer nostr.Signer
if user != nil {
signer, _ = user.(nostr.Signer)
}
return &NostrRoot{
ctx: ctx,
sys: sys,
rootPubKey: pubkey,
signer: signer,
wd: abs,
opts: o,
}
}
func (r *NostrRoot) OnAdd(context.Context) {
func (r *NostrRoot) OnAdd(_ context.Context) {
if r.rootPubKey == "" {
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,
CreateNpubDir(r.ctx, r.sys, r, r.wd, pointer),
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}
r.AddChild(
npub,
CreateNpubDir(r.ctx, r.sys, r, r.wd, pointer),
true,
)
}
// 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,
)
}
// 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)
// add ourselves
npub, _ := nip19.EncodePublicKey(r.rootPubKey)
if r.GetChild(npub) == nil {
pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey}
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(ctx 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) {
out.SetEntryTimeout(time.Minute * 5)
child := r.GetChild(name)
@@ -84,9 +102,12 @@ func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut)
return child, fs.OK
}
if pp, err := nip05.QueryIdentifier(ctx, name); err == nil {
npubdir := CreateNpubDir(ctx, r.sys, r, r.wd, *pp)
return npubdir, fs.OK
if pp, err := nip05.QueryIdentifier(r.ctx, name); err == nil {
return r.NewPersistentInode(
r.ctx,
&fs.MemSymlink{Data: []byte(r.wd + "/" + nip19.EncodePointer(*pp))},
fs.StableAttr{Mode: syscall.S_IFLNK},
), fs.OK
}
pointer, err := nip19.ToPointer(name)
@@ -96,10 +117,10 @@ func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut)
switch p := pointer.(type) {
case nostr.ProfilePointer:
npubdir := CreateNpubDir(ctx, r.sys, r, r.wd, p)
npubdir := r.CreateNpubDir(r, p, nil)
return npubdir, fs.OK
case nostr.EventPointer:
eventdir, err := FetchAndCreateEventDir(ctx, r, r.wd, r.sys, p)
eventdir, err := r.FetchAndCreateEventDir(r, p)
if err != nil {
return nil, syscall.ENOENT
}

View File

@@ -2,33 +2,210 @@ package nostrfs
import (
"context"
"slices"
"strings"
"sync/atomic"
"syscall"
"fiatjaf.com/lib/debouncer"
"github.com/fatih/color"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/nbd-wtf/go-nostr"
sdk "github.com/nbd-wtf/go-nostr/sdk"
"github.com/nbd-wtf/go-nostr/nip27"
)
type ViewDir struct {
fs.Inode
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)
root *NostrRoot
fetched atomic.Bool
filter nostr.Filter
paginate bool
relays []string
replaceable bool
createable bool
publisher *debouncer.Debouncer
publishing struct {
note string
}
}
var (
_ = (fs.NodeOpendirer)((*ViewDir)(nil))
_ = (fs.NodeGetattrer)((*ViewDir)(nil))
_ = (fs.NodeMkdirer)((*ViewDir)(nil))
_ = (fs.NodeSetattrer)((*ViewDir)(nil))
_ = (fs.NodeCreater)((*ViewDir)(nil))
_ = (fs.NodeUnlinker)((*ViewDir)(nil))
)
func (n *ViewDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
func (f *ViewDir) Setattr(_ context.Context, _ fs.FileHandle, _ *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno {
return fs.OK
}
func (n *ViewDir) Create(
_ context.Context,
name string,
flags uint32,
mode uint32,
out *fuse.EntryOut,
) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
if !n.createable || n.root.rootPubKey != n.filter.Authors[0] {
return nil, nil, 0, syscall.EPERM
}
if n.publisher == nil {
n.publisher = debouncer.New(n.root.opts.AutoPublishNotesTimeout)
}
if n.filter.Kinds[0] != 1 {
return nil, nil, 0, syscall.ENOTSUP
}
switch name {
case "new":
log := n.root.ctx.Value("log").(func(msg string, args ...any))
if n.publisher.IsRunning() {
log("pending note updated, timer reset.")
} else {
log("new note detected")
if n.root.opts.AutoPublishNotesTimeout.Hours() < 24*365 {
log(", publishing it in %d seconds...\n", int(n.root.opts.AutoPublishNotesTimeout.Seconds()))
} else {
log(".\n")
}
log("- `touch publish` to publish immediately\n")
log("- `rm new` to erase and cancel the publication.\n")
}
n.publisher.Call(n.publishNote)
first := true
return n.NewPersistentInode(
n.root.ctx,
n.root.NewWriteableFile(n.publishing.note, uint64(nostr.Now()), uint64(nostr.Now()), func(s string) {
if !first {
log("pending note updated, timer reset.\n")
}
first = false
n.publishing.note = strings.TrimSpace(s)
n.publisher.Call(n.publishNote)
}),
fs.StableAttr{},
), nil, 0, fs.OK
case "publish":
if n.publisher.IsRunning() {
// this causes the publish process to be triggered faster
log := n.root.ctx.Value("log").(func(msg string, args ...any))
log("publishing now!\n")
n.publisher.Flush()
return nil, nil, 0, syscall.ENOTDIR
}
}
return nil, nil, 0, syscall.ENOTSUP
}
func (n *ViewDir) Unlink(ctx context.Context, name string) syscall.Errno {
if !n.createable || n.root.rootPubKey != n.filter.Authors[0] {
return syscall.EPERM
}
if n.publisher == nil {
n.publisher = debouncer.New(n.root.opts.AutoPublishNotesTimeout)
}
if n.filter.Kinds[0] != 1 {
return syscall.ENOTSUP
}
switch name {
case "new":
log := n.root.ctx.Value("log").(func(msg string, args ...any))
log("publishing canceled.\n")
n.publisher.Stop()
n.publishing.note = ""
return fs.OK
}
return syscall.ENOTSUP
}
func (n *ViewDir) publishNote() {
log := n.root.ctx.Value("log").(func(msg string, args ...any))
log("publishing note...\n")
evt := nostr.Event{
Kind: 1,
CreatedAt: nostr.Now(),
Content: n.publishing.note,
Tags: make(nostr.Tags, 0, 2),
}
// our write relays
relays := n.root.sys.FetchWriteRelays(n.root.ctx, n.root.rootPubKey, 8)
if len(relays) == 0 {
relays = n.root.sys.FetchOutboxRelays(n.root.ctx, n.root.rootPubKey, 6)
}
// add "p" tags from people mentioned and "q" tags from events mentioned
for ref := range nip27.ParseReferences(evt) {
tag := ref.Pointer.AsTag()
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)
// add their "read" relays
if key == "p" {
for _, r := range n.root.sys.FetchInboxRelays(n.root.ctx, val, 4) {
if !slices.Contains(relays, r) {
relays = append(relays, r)
}
}
}
}
}
// sign and publish
if err := n.root.signer.SignEvent(n.root.ctx, &evt); err != nil {
log("failed to sign: %s\n", err)
return
}
log(evt.String() + "\n")
log("publishing to %d relays... ", len(relays))
success := false
first := true
for res := range n.root.sys.Pool.PublishMany(n.root.ctx, relays, evt) {
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
if !ok {
cleanUrl = res.RelayURL
}
if !first {
log(", ")
}
first = false
if res.Error != nil {
log("%s: %s", color.RedString(cleanUrl), res.Error)
} else {
success = true
log("%s: ok", color.GreenString(cleanUrl))
}
}
log("\n")
if success {
n.RmChild("new")
n.AddChild(evt.ID, n.root.CreateEventDir(n, &evt), true)
log("event published as %s and updated locally.\n", color.BlueString(evt.ID))
}
}
func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
now := nostr.Now()
if n.filter.Until != nil {
now = *n.filter.Until
@@ -52,31 +229,62 @@ func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno {
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,
n.root.ctx,
&ViewDir{
ctx: n.ctx,
sys: n.sys,
filter: filter,
wd: n.wd,
relays: n.relays,
root: n.root,
filter: filter,
relays: n.relays,
replaceable: n.replaceable,
},
fs.StableAttr{Mode: syscall.S_IFDIR},
), true)
}
if n.replaceable {
for rkey, evt := range n.root.sys.Pool.FetchManyReplaceable(n.root.ctx, n.relays, n.filter,
nostr.WithLabel("nakfs"),
).Range {
name := rkey.D
if name == "" {
name = "_"
}
if n.GetChild(name) == nil {
n.AddChild(name, n.root.CreateEntityDir(n, evt), 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)
for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter,
nostr.WithLabel("nakfs"),
) {
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
}

93
nostrfs/writeablefile.go Normal file
View File

@@ -0,0 +1,93 @@
package nostrfs
import (
"context"
"sync"
"syscall"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type WriteableFile struct {
fs.Inode
root *NostrRoot
mu sync.Mutex
data []byte
attr fuse.Attr
onWrite func(string)
}
var (
_ = (fs.NodeOpener)((*WriteableFile)(nil))
_ = (fs.NodeReader)((*WriteableFile)(nil))
_ = (fs.NodeWriter)((*WriteableFile)(nil))
_ = (fs.NodeGetattrer)((*WriteableFile)(nil))
_ = (fs.NodeSetattrer)((*WriteableFile)(nil))
_ = (fs.NodeFlusher)((*WriteableFile)(nil))
)
func (r *NostrRoot) NewWriteableFile(data string, ctime, mtime uint64, onWrite func(string)) *WriteableFile {
return &WriteableFile{
root: r,
data: []byte(data),
attr: fuse.Attr{
Mode: 0666,
Ctime: ctime,
Mtime: mtime,
Size: uint64(len(data)),
},
onWrite: onWrite,
}
}
func (f *WriteableFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
return nil, fuse.FOPEN_KEEP_CACHE, fs.OK
}
func (f *WriteableFile) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) {
f.mu.Lock()
defer f.mu.Unlock()
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[offset:], data)
f.data = f.data[0:end]
f.onWrite(string(f.data))
return uint32(len(data)), fs.OK
}
func (f *WriteableFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
f.mu.Lock()
defer f.mu.Unlock()
out.Attr = f.attr
out.Attr.Size = uint64(len(f.data))
return fs.OK
}
func (f *WriteableFile) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno {
f.attr.Mtime = in.Mtime
f.attr.Atime = in.Atime
f.attr.Ctime = in.Ctime
return fs.OK
}
func (f *WriteableFile) Flush(ctx context.Context, fh fs.FileHandle) syscall.Errno {
return fs.OK
}
func (f *WriteableFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
f.mu.Lock()
defer f.mu.Unlock()
end := int(off) + len(dest)
if end > len(f.data) {
end = len(f.data)
}
return fuse.ReadResultData(f.data[off:end]), fs.OK
}