mirror of
https://github.com/fiatjaf/nak.git
synced 2026-01-31 06:28:52 +00:00
Compare commits
11 Commits
38775e0d93
...
v0.17.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e6a6d7506 | ||
|
|
cff60b2f9f | ||
|
|
cb2247c9da | ||
|
|
686d960f62 | ||
|
|
af04838153 | ||
|
|
c6da13649d | ||
|
|
acd6227dd0 | ||
|
|
00fbda9af7 | ||
|
|
e838de9b72 | ||
|
|
6dfbed4413 | ||
|
|
0e283368ed |
5
.github/workflows/release-cli.yml
vendored
5
.github/workflows/release-cli.yml
vendored
@@ -47,6 +47,7 @@ jobs:
|
|||||||
md5sum: false
|
md5sum: false
|
||||||
sha256sum: false
|
sha256sum: false
|
||||||
compress_assets: false
|
compress_assets: false
|
||||||
|
|
||||||
smoke-test-linux-amd64:
|
smoke-test-linux-amd64:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
@@ -102,7 +103,7 @@ jobs:
|
|||||||
# test NIP-49 key encryption/decryption
|
# test NIP-49 key encryption/decryption
|
||||||
echo "testing NIP-49 key encryption/decryption..."
|
echo "testing NIP-49 key encryption/decryption..."
|
||||||
ENCRYPTED_KEY=$(./nak key encrypt $SECRET_KEY "testpassword")
|
ENCRYPTED_KEY=$(./nak key encrypt $SECRET_KEY "testpassword")
|
||||||
echo "encrypted key: ${ENCRYPTED_KEY:0:20}..."
|
echo "encrypted key: ${ENCRYPTED_KEY: 0:20}..."
|
||||||
DECRYPTED_KEY=$(./nak key decrypt $ENCRYPTED_KEY "testpassword")
|
DECRYPTED_KEY=$(./nak key decrypt $ENCRYPTED_KEY "testpassword")
|
||||||
if [ "$DECRYPTED_KEY" != "$SECRET_KEY" ]; then
|
if [ "$DECRYPTED_KEY" != "$SECRET_KEY" ]; then
|
||||||
echo "nip-49 encryption/decryption test failed!"
|
echo "nip-49 encryption/decryption test failed!"
|
||||||
@@ -116,7 +117,7 @@ jobs:
|
|||||||
# test relay operations (with a public relay)
|
# test relay operations (with a public relay)
|
||||||
echo "testing publishing..."
|
echo "testing publishing..."
|
||||||
# publish a simple event to a public relay
|
# publish a simple event to a public relay
|
||||||
EVENT_JSON=$(./nak event --sec $SECRET_KEY -c "test from nak smoke test" nos.lol)
|
EVENT_JSON=$(./nak event --sec $SECRET_KEY -c "test from nak smoke test" nos.lol < /dev/null)
|
||||||
EVENT_ID=$(echo $EVENT_JSON | jq -r .id)
|
EVENT_ID=$(echo $EVENT_JSON | jq -r .id)
|
||||||
echo "published event ID: $EVENT_ID"
|
echo "published event ID: $EVENT_ID"
|
||||||
|
|
||||||
|
|||||||
125
blossom.go
125
blossom.go
@@ -3,10 +3,15 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/keyer"
|
"fiatjaf.com/nostr/keyer"
|
||||||
"fiatjaf.com/nostr/nipb0/blossom"
|
"fiatjaf.com/nostr/nipb0/blossom"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
@@ -230,11 +235,46 @@ if any of the files are not found the command will fail, otherwise it will succe
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "mirror",
|
Name: "mirror",
|
||||||
Usage: "",
|
Usage: "mirrors blobs from source server to target server",
|
||||||
Description: ``,
|
Description: `lists all blobs from the source server and mirrors them to the target server using BUD-04. requires --sec to sign the authorization event.`,
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
ArgsUsage: "",
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
targetClient, err := getBlossomClient(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create client for source server
|
||||||
|
sourceServer := c.Args().First()
|
||||||
|
keyer, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sourceClient := blossom.NewClient(sourceServer, keyer)
|
||||||
|
|
||||||
|
// Get list of blobs from source server
|
||||||
|
bds, err := sourceClient.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list blobs from source server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirror each blob to target server
|
||||||
|
hasError := false
|
||||||
|
for _, bd := range bds {
|
||||||
|
mirrored, err := mirrorBlob(ctx, targetClient, bd.URL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to mirror %s: %s\n", bd.SHA256, err)
|
||||||
|
hasError = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
j, _ := json.Marshal(mirrored)
|
||||||
|
stdout(string(j))
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasError {
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -248,3 +288,82 @@ func getBlossomClient(ctx context.Context, c *cli.Command) (*blossom.Client, err
|
|||||||
}
|
}
|
||||||
return blossom.NewClient(c.String("server"), keyer), nil
|
return blossom.NewClient(c.String("server"), keyer), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mirrorBlob mirrors a blob from a URL to the mediaserver using BUD-04
|
||||||
|
func mirrorBlob(ctx context.Context, client *blossom.Client, url string) (*blossom.BlobDescriptor, error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to download blob from URL: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to download blob: HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read blob content: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := sha256.Sum256(data)
|
||||||
|
hashHex := hex.EncodeToString(hash[:])
|
||||||
|
|
||||||
|
signer := client.GetSigner()
|
||||||
|
pubkey, _ := signer.GetPublicKey(ctx)
|
||||||
|
|
||||||
|
evt := nostr.Event{
|
||||||
|
Kind: 24242,
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
Tags: nostr.Tags{
|
||||||
|
{"t", "upload"},
|
||||||
|
{"x", hashHex},
|
||||||
|
{"expiration", fmt.Sprintf("%d", nostr.Now()+60)},
|
||||||
|
},
|
||||||
|
Content: "blossom stuff",
|
||||||
|
PubKey: pubkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := signer.SignEvent(ctx, &evt); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to sign authorization event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
evtj, err := json.Marshal(evt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal authorization event: %w", err)
|
||||||
|
}
|
||||||
|
auth := base64.StdEncoding.EncodeToString(evtj)
|
||||||
|
|
||||||
|
mediaserver := client.GetMediaServer()
|
||||||
|
mirrorURL := mediaserver + "mirror"
|
||||||
|
|
||||||
|
requestBody := map[string]string{"url": url}
|
||||||
|
requestJSON, _ := json.Marshal(requestBody)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "PUT", mirrorURL, bytes.NewReader(requestJSON))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create mirror request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Nostr "+auth)
|
||||||
|
|
||||||
|
httpClient := &http.Client{}
|
||||||
|
mirrorResp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to send mirror request: %w", err)
|
||||||
|
}
|
||||||
|
defer mirrorResp.Body.Close()
|
||||||
|
|
||||||
|
if mirrorResp.StatusCode < 200 || mirrorResp.StatusCode >= 300 {
|
||||||
|
body, _ := io.ReadAll(mirrorResp.Body)
|
||||||
|
return nil, fmt.Errorf("mirror request failed with HTTP %d: %s", mirrorResp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var bd blossom.BlobDescriptor
|
||||||
|
if err := json.NewDecoder(mirrorResp.Body).Decode(&bd); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode blob descriptor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &bd, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -329,6 +329,10 @@ var bunker = &cli.Command{
|
|||||||
|
|
||||||
// asking user for authorization
|
// asking user for authorization
|
||||||
signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool {
|
signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool {
|
||||||
|
if slices.Contains(config.AuthorizedKeys, from) || slices.Contains(authorizedSecrets, secret) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if secret == newSecret {
|
if secret == newSecret {
|
||||||
// store this key
|
// store this key
|
||||||
config.AuthorizedKeys = appendUnique(config.AuthorizedKeys, from)
|
config.AuthorizedKeys = appendUnique(config.AuthorizedKeys, from)
|
||||||
@@ -343,9 +347,11 @@ var bunker = &cli.Command{
|
|||||||
if persist != nil {
|
if persist != nil {
|
||||||
persist()
|
persist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return slices.Contains(config.AuthorizedKeys, from) || slices.Contains(authorizedSecrets, secret)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for ie := range events {
|
for ie := range events {
|
||||||
|
|||||||
1
event.go
1
event.go
@@ -155,6 +155,7 @@ example:
|
|||||||
os.Exit(3)
|
os.Exit(3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kr, sec, err := gatherKeyerFromArguments(ctx, c)
|
kr, sec, err := gatherKeyerFromArguments(ctx, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
45
fs.go
45
fs.go
@@ -14,8 +14,9 @@ import (
|
|||||||
"fiatjaf.com/nostr/keyer"
|
"fiatjaf.com/nostr/keyer"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/fiatjaf/nak/nostrfs"
|
"github.com/fiatjaf/nak/nostrfs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/winfsp/cgofuse/fuse"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var fsCmd = &cli.Command{
|
var fsCmd = &cli.Command{
|
||||||
@@ -82,22 +83,21 @@ var fsCmd = &cli.Command{
|
|||||||
|
|
||||||
// create the server
|
// create the server
|
||||||
log("- mounting at %s... ", color.HiCyanString(mountpoint))
|
log("- mounting at %s... ", color.HiCyanString(mountpoint))
|
||||||
|
timeout := time.Second * 120
|
||||||
// Create cgofuse host
|
server, err := fs.Mount(mountpoint, root, &fs.Options{
|
||||||
host := fuse.NewFileSystemHost(root)
|
MountOptions: fuse.MountOptions{
|
||||||
host.SetCapReaddirPlus(true)
|
Debug: isVerbose,
|
||||||
host.SetUseIno(true)
|
Name: "nak",
|
||||||
|
FsName: "nak",
|
||||||
// Mount the filesystem
|
RememberInodes: true,
|
||||||
mountArgs := []string{"-s", mountpoint}
|
},
|
||||||
if isVerbose {
|
AttrTimeout: &timeout,
|
||||||
mountArgs = append([]string{"-d"}, mountArgs...)
|
EntryTimeout: &timeout,
|
||||||
|
Logger: nostr.DebugLogger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mount failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
|
||||||
host.Mount("", mountArgs)
|
|
||||||
}()
|
|
||||||
|
|
||||||
log("ok.\n")
|
log("ok.\n")
|
||||||
|
|
||||||
// setup signal handling for clean unmount
|
// setup signal handling for clean unmount
|
||||||
@@ -107,12 +107,17 @@ var fsCmd = &cli.Command{
|
|||||||
go func() {
|
go func() {
|
||||||
<-ch
|
<-ch
|
||||||
log("- unmounting... ")
|
log("- unmounting... ")
|
||||||
// cgofuse doesn't have explicit unmount, it unmounts on process exit
|
err := server.Unmount()
|
||||||
log("ok\n")
|
if err != nil {
|
||||||
chErr <- nil
|
chErr <- fmt.Errorf("unmount failed: %w", err)
|
||||||
|
} else {
|
||||||
|
log("ok\n")
|
||||||
|
chErr <- nil
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// wait for signals
|
// serve the filesystem until unmounted
|
||||||
|
server.Wait()
|
||||||
return <-chErr
|
return <-chErr
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ var fsCmd = &cli.Command{
|
|||||||
Description: `doesn't work on Windows and OpenBSD.`,
|
Description: `doesn't work on Windows and OpenBSD.`,
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
return fmt.Errorf("this doesn't work on Windows and OpenBSD.")
|
return fmt.Errorf("this doesn't work on OpenBSD.")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package nostrfs
|
//go:build !openbsd
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
stdjson "encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -17,27 +19,27 @@ import (
|
|||||||
"github.com/winfsp/cgofuse/fuse"
|
"github.com/winfsp/cgofuse/fuse"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type FSOptions struct {
|
||||||
AutoPublishNotesTimeout time.Duration
|
AutoPublishNotesTimeout time.Duration
|
||||||
AutoPublishArticlesTimeout time.Duration
|
AutoPublishArticlesTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type NostrRoot struct {
|
type FSRoot struct {
|
||||||
fuse.FileSystemBase
|
fuse.FileSystemBase
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
sys *sdk.System
|
sys *sdk.System
|
||||||
rootPubKey nostr.PubKey
|
rootPubKey nostr.PubKey
|
||||||
signer nostr.Signer
|
signer nostr.Signer
|
||||||
opts Options
|
opts FSOptions
|
||||||
mountpoint string
|
mountpoint string
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
nodes map[string]*Node // path -> node
|
nodes map[string]*FSNode // path -> node
|
||||||
nextIno uint64
|
nextIno uint64
|
||||||
pendingNotes map[string]*time.Timer // path -> auto-publish timer
|
pendingNotes map[string]*time.Timer // path -> auto-publish timer
|
||||||
}
|
}
|
||||||
|
|
||||||
type Node struct {
|
type FSNode struct {
|
||||||
ino uint64
|
ino uint64
|
||||||
path string
|
path string
|
||||||
name string
|
name string
|
||||||
@@ -46,14 +48,14 @@ type Node struct {
|
|||||||
mode uint32
|
mode uint32
|
||||||
mtime time.Time
|
mtime time.Time
|
||||||
data []byte
|
data []byte
|
||||||
children map[string]*Node
|
children map[string]*FSNode
|
||||||
loadFunc func() ([]byte, error) // for lazy loading
|
loadFunc func() ([]byte, error) // for lazy loading
|
||||||
loaded bool
|
loaded bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ fuse.FileSystemInterface = (*NostrRoot)(nil)
|
var _ fuse.FileSystemInterface = (*FSRoot)(nil)
|
||||||
|
|
||||||
func NewNostrRoot(ctx context.Context, sys interface{}, user interface{}, mountpoint string, o Options) *NostrRoot {
|
func NewFSRoot(ctx context.Context, sys interface{}, user interface{}, mountpoint string, o FSOptions) *FSRoot {
|
||||||
var system *sdk.System
|
var system *sdk.System
|
||||||
if sys != nil {
|
if sys != nil {
|
||||||
system = sys.(*sdk.System)
|
system = sys.(*sdk.System)
|
||||||
@@ -71,37 +73,37 @@ func NewNostrRoot(ctx context.Context, sys interface{}, user interface{}, mountp
|
|||||||
|
|
||||||
abs, _ := filepath.Abs(mountpoint)
|
abs, _ := filepath.Abs(mountpoint)
|
||||||
|
|
||||||
root := &NostrRoot{
|
root := &FSRoot{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
sys: system,
|
sys: system,
|
||||||
rootPubKey: pubkey,
|
rootPubKey: pubkey,
|
||||||
signer: signer,
|
signer: signer,
|
||||||
opts: o,
|
opts: o,
|
||||||
mountpoint: abs,
|
mountpoint: abs,
|
||||||
nodes: make(map[string]*Node),
|
nodes: make(map[string]*FSNode),
|
||||||
nextIno: 2, // 1 is reserved for root
|
nextIno: 2, // 1 is reserved for root
|
||||||
pendingNotes: make(map[string]*time.Timer),
|
pendingNotes: make(map[string]*time.Timer),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize root directory
|
// initialize root directory
|
||||||
rootNode := &Node{
|
rootNode := &FSNode{
|
||||||
ino: 1,
|
ino: 1,
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "",
|
name: "",
|
||||||
isDir: true,
|
isDir: true,
|
||||||
mode: fuse.S_IFDIR | 0755,
|
mode: fuse.S_IFDIR | 0755,
|
||||||
mtime: time.Now(),
|
mtime: time.Now(),
|
||||||
children: make(map[string]*Node),
|
children: make(map[string]*FSNode),
|
||||||
}
|
}
|
||||||
root.nodes["/"] = rootNode
|
root.nodes["/"] = rootNode
|
||||||
|
|
||||||
// Start async initialization
|
// start async initialization
|
||||||
go root.initialize()
|
go root.initialize()
|
||||||
|
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) initialize() {
|
func (r *FSRoot) initialize() {
|
||||||
if r.rootPubKey == nostr.ZeroPK {
|
if r.rootPubKey == nostr.ZeroPK {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -109,14 +111,14 @@ func (r *NostrRoot) initialize() {
|
|||||||
log := r.getLog()
|
log := r.getLog()
|
||||||
time.Sleep(time.Millisecond * 100)
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
|
||||||
// Fetch follow list
|
// fetch follow list
|
||||||
fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey)
|
fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey)
|
||||||
log("- fetched %d contacts\n", len(fl.Items))
|
log("- fetched %d contacts\n", len(fl.Items))
|
||||||
|
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
// Add our contacts
|
// add our contacts
|
||||||
for _, f := range fl.Items {
|
for _, f := range fl.Items {
|
||||||
npub := nip19.EncodeNpub(f.Pubkey)
|
npub := nip19.EncodeNpub(f.Pubkey)
|
||||||
if _, exists := r.nodes["/"+npub]; !exists {
|
if _, exists := r.nodes["/"+npub]; !exists {
|
||||||
@@ -124,14 +126,14 @@ func (r *NostrRoot) initialize() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add ourselves
|
// add ourselves
|
||||||
npub := nip19.EncodeNpub(r.rootPubKey)
|
npub := nip19.EncodeNpub(r.rootPubKey)
|
||||||
if _, exists := r.nodes["/"+npub]; !exists {
|
if _, exists := r.nodes["/"+npub]; !exists {
|
||||||
r.createNpubDirLocked(npub, r.rootPubKey, r.signer)
|
r.createNpubDirLocked(npub, r.rootPubKey, r.signer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add @me symlink (for now, just create a text file pointing to our npub)
|
// add @me symlink (for now, just create a text file pointing to our npub)
|
||||||
meNode := &Node{
|
meNode := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: "/@me",
|
path: "/@me",
|
||||||
name: "@me",
|
name: "@me",
|
||||||
@@ -146,19 +148,19 @@ func (r *NostrRoot) initialize() {
|
|||||||
r.nodes["/"].children["@me"] = meNode
|
r.nodes["/"].children["@me"] = meNode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) fetchMetadata(dirPath string, pubkey nostr.PubKey) {
|
func (r *FSRoot) fetchMetadata(dirPath string, pubkey nostr.PubKey) {
|
||||||
pm := r.sys.FetchProfileMetadata(r.ctx, pubkey)
|
pm := r.sys.FetchProfileMetadata(r.ctx, pubkey)
|
||||||
if pm.Event == nil {
|
if pm.Event == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the content field which contains the actual profile JSON
|
// use the content field which contains the actual profile JSON
|
||||||
metadataJ := []byte(pm.Event.Content)
|
metadataJ := []byte(pm.Event.Content)
|
||||||
|
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
metadataNode := &Node{
|
metadataNode := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: dirPath + "/metadata.json",
|
path: dirPath + "/metadata.json",
|
||||||
name: "metadata.json",
|
name: "metadata.json",
|
||||||
@@ -175,13 +177,13 @@ func (r *NostrRoot) fetchMetadata(dirPath string, pubkey nostr.PubKey) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) {
|
func (r *FSRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) {
|
||||||
pm := r.sys.FetchProfileMetadata(r.ctx, pubkey)
|
pm := r.sys.FetchProfileMetadata(r.ctx, pubkey)
|
||||||
if pm.Event == nil || pm.Picture == "" {
|
if pm.Event == nil || pm.Picture == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download picture
|
// download picture
|
||||||
ctx, cancel := context.WithTimeout(r.ctx, time.Second*20)
|
ctx, cancel := context.WithTimeout(r.ctx, time.Second*20)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -200,7 +202,7 @@ func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read image data
|
// read image data
|
||||||
imageData := make([]byte, 0, 1024*1024) // 1MB initial capacity
|
imageData := make([]byte, 0, 1024*1024) // 1MB initial capacity
|
||||||
buf := make([]byte, 32*1024)
|
buf := make([]byte, 32*1024)
|
||||||
for {
|
for {
|
||||||
@@ -220,7 +222,7 @@ func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect file extension from content-type or URL
|
// detect file extension from content-type or URL
|
||||||
ext := "png"
|
ext := "png"
|
||||||
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
||||||
switch ct {
|
switch ct {
|
||||||
@@ -239,7 +241,7 @@ func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) {
|
|||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
picturePath := dirPath + "/picture." + ext
|
picturePath := dirPath + "/picture." + ext
|
||||||
pictureNode := &Node{
|
pictureNode := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: picturePath,
|
path: picturePath,
|
||||||
name: "picture." + ext,
|
name: "picture." + ext,
|
||||||
@@ -256,11 +258,11 @@ func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) {
|
func (r *FSRoot) fetchEvents(dirPath string, filter nostr.Filter) {
|
||||||
ctx, cancel := context.WithTimeout(r.ctx, time.Second*10)
|
ctx, cancel := context.WithTimeout(r.ctx, time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Get relays for authors
|
// get relays for authors
|
||||||
var relays []string
|
var relays []string
|
||||||
if len(filter.Authors) > 0 {
|
if len(filter.Authors) > 0 {
|
||||||
relays = r.sys.FetchOutboxRelays(ctx, filter.Authors[0], 3)
|
relays = r.sys.FetchOutboxRelays(ctx, filter.Authors[0], 3)
|
||||||
@@ -272,12 +274,12 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) {
|
|||||||
log := r.getLog()
|
log := r.getLog()
|
||||||
log("- fetching events for %s from %v\n", dirPath, relays)
|
log("- fetching events for %s from %v\n", dirPath, relays)
|
||||||
|
|
||||||
// Fetch events
|
// fetch events
|
||||||
events := make([]*nostr.Event, 0)
|
events := make([]*nostr.Event, 0)
|
||||||
for ie := range r.sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{
|
for ie := range r.sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{
|
||||||
Label: "nak-fs",
|
Label: "nak-fs",
|
||||||
}) {
|
}) {
|
||||||
// Make a copy to avoid pointer issues with loop variable
|
// make a copy to avoid pointer issues with loop variable
|
||||||
evt := ie.Event
|
evt := ie.Event
|
||||||
events = append(events, &evt)
|
events = append(events, &evt)
|
||||||
if len(events) >= int(filter.Limit) {
|
if len(events) >= int(filter.Limit) {
|
||||||
@@ -295,14 +297,14 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track oldest timestamp for pagination
|
// track oldest timestamp for pagination
|
||||||
var oldestTimestamp nostr.Timestamp
|
var oldestTimestamp nostr.Timestamp
|
||||||
if len(events) > 0 {
|
if len(events) > 0 {
|
||||||
oldestTimestamp = events[len(events)-1].CreatedAt
|
oldestTimestamp = events[len(events)-1].CreatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, evt := range events {
|
for _, evt := range events {
|
||||||
// Create filename based on event
|
// create filename based on event
|
||||||
filename := r.eventToFilename(evt)
|
filename := r.eventToFilename(evt)
|
||||||
filePath := dirPath + "/" + filename
|
filePath := dirPath + "/" + filename
|
||||||
|
|
||||||
@@ -315,7 +317,7 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) {
|
|||||||
content = "(empty)"
|
content = "(empty)"
|
||||||
}
|
}
|
||||||
|
|
||||||
fileNode := &Node{
|
fileNode := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: filePath,
|
path: filePath,
|
||||||
name: filename,
|
name: filename,
|
||||||
@@ -330,9 +332,9 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) {
|
|||||||
dir.children[filename] = fileNode
|
dir.children[filename] = fileNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add "more" file for pagination if we got a full page
|
// add "more" file for pagination if we got a full page
|
||||||
if len(events) >= int(filter.Limit) {
|
if len(events) >= int(filter.Limit) {
|
||||||
moreFile := &Node{
|
moreFile := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: dirPath + "/.more",
|
path: dirPath + "/.more",
|
||||||
name: ".more",
|
name: ".more",
|
||||||
@@ -342,7 +344,7 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) {
|
|||||||
data: []byte(fmt.Sprintf("Read this file to load more events (until: %d)\n", oldestTimestamp)),
|
data: []byte(fmt.Sprintf("Read this file to load more events (until: %d)\n", oldestTimestamp)),
|
||||||
size: int64(len(fmt.Sprintf("Read this file to load more events (until: %d)\n", oldestTimestamp))),
|
size: int64(len(fmt.Sprintf("Read this file to load more events (until: %d)\n", oldestTimestamp))),
|
||||||
loadFunc: func() ([]byte, error) {
|
loadFunc: func() ([]byte, error) {
|
||||||
// When .more is read, fetch next page
|
// when .more is read, fetch next page
|
||||||
newFilter := filter
|
newFilter := filter
|
||||||
newFilter.Until = oldestTimestamp
|
newFilter.Until = oldestTimestamp
|
||||||
go r.fetchEvents(dirPath, newFilter)
|
go r.fetchEvents(dirPath, newFilter)
|
||||||
@@ -355,24 +357,24 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) eventToFilename(evt *nostr.Event) string {
|
func (r *FSRoot) eventToFilename(evt *nostr.Event) string {
|
||||||
// Use event ID first 8 chars + extension based on kind
|
// use event ID first 8 chars + extension based on kind
|
||||||
ext := kindToExtension(evt.Kind)
|
ext := kindToExtension(evt.Kind)
|
||||||
|
|
||||||
// Get hex representation of event ID
|
// get hex representation of event ID
|
||||||
// evt.ID.String() may return format like ":1234abcd" so use Hex() or remove colons
|
// evt.ID.String() may return format like ":1234abcd" so use Hex() or remove colons
|
||||||
idHex := evt.ID.Hex()
|
idHex := evt.ID.Hex()
|
||||||
if len(idHex) > 8 {
|
if len(idHex) > 8 {
|
||||||
idHex = idHex[:8]
|
idHex = idHex[:8]
|
||||||
}
|
}
|
||||||
|
|
||||||
// For articles, try to use title
|
// for articles, try to use title
|
||||||
if evt.Kind == 30023 || evt.Kind == 30818 {
|
if evt.Kind == 30023 || evt.Kind == 30818 {
|
||||||
for _, tag := range evt.Tags {
|
for _, tag := range evt.Tags {
|
||||||
if len(tag) >= 2 && tag[0] == "title" {
|
if len(tag) >= 2 && tag[0] == "title" {
|
||||||
titleStr := tag[1]
|
titleStr := tag[1]
|
||||||
if titleStr != "" {
|
if titleStr != "" {
|
||||||
// Sanitize title for filename
|
// sanitize title for filename
|
||||||
name := strings.Map(func(r rune) rune {
|
name := strings.Map(func(r rune) rune {
|
||||||
if r == '/' || r == '\\' || r == ':' || r == '*' || r == '?' || r == '"' || r == '<' || r == '>' || r == '|' {
|
if r == '/' || r == '\\' || r == ':' || r == '*' || r == '?' || r == '"' || r == '<' || r == '>' || r == '|' {
|
||||||
return '-'
|
return '-'
|
||||||
@@ -391,35 +393,35 @@ func (r *NostrRoot) eventToFilename(evt *nostr.Event) string {
|
|||||||
return fmt.Sprintf("%s.%s", idHex, ext)
|
return fmt.Sprintf("%s.%s", idHex, ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) getLog() func(string, ...interface{}) {
|
func (r *FSRoot) getLog() func(string, ...interface{}) {
|
||||||
if log := r.ctx.Value("log"); log != nil {
|
if log := r.ctx.Value("log"); log != nil {
|
||||||
return log.(func(string, ...interface{}))
|
return log.(func(string, ...interface{}))
|
||||||
}
|
}
|
||||||
return func(string, ...interface{}) {}
|
return func(string, ...interface{}) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) getNode(path string) *Node {
|
func (r *FSRoot) getNode(path string) *FSNode {
|
||||||
originalPath := path
|
originalPath := path
|
||||||
|
|
||||||
// Normalize path
|
// normalize path
|
||||||
if path == "" {
|
if path == "" {
|
||||||
path = "/"
|
path = "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert Windows backslashes to forward slashes
|
// convert Windows backslashes to forward slashes
|
||||||
path = strings.ReplaceAll(path, "\\", "/")
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
|
|
||||||
// Ensure path starts with /
|
// ensure path starts with /
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
path = "/" + path
|
path = "/" + path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove trailing slash except for root
|
// remove trailing slash except for root
|
||||||
if path != "/" && strings.HasSuffix(path, "/") {
|
if path != "/" && strings.HasSuffix(path, "/") {
|
||||||
path = strings.TrimSuffix(path, "/")
|
path = strings.TrimSuffix(path, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logging
|
// debug logging
|
||||||
if r.ctx.Value("logverbose") != nil {
|
if r.ctx.Value("logverbose") != nil {
|
||||||
logv := r.ctx.Value("logverbose").(func(string, ...interface{}))
|
logv := r.ctx.Value("logverbose").(func(string, ...interface{}))
|
||||||
logv("getNode: original='%s' normalized='%s'\n", originalPath, path)
|
logv("getNode: original='%s' normalized='%s'\n", originalPath, path)
|
||||||
@@ -430,7 +432,7 @@ func (r *NostrRoot) getNode(path string) *Node {
|
|||||||
|
|
||||||
node := r.nodes[path]
|
node := r.nodes[path]
|
||||||
|
|
||||||
// Debug: if not found, show similar paths
|
// debug: if not found, show similar paths
|
||||||
if node == nil && r.ctx.Value("logverbose") != nil {
|
if node == nil && r.ctx.Value("logverbose") != nil {
|
||||||
logv := r.ctx.Value("logverbose").(func(string, ...interface{}))
|
logv := r.ctx.Value("logverbose").(func(string, ...interface{}))
|
||||||
logv("getNode: NOT FOUND '%s'\n", path)
|
logv("getNode: NOT FOUND '%s'\n", path)
|
||||||
@@ -451,11 +453,11 @@ func (r *NostrRoot) getNode(path string) *Node {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Getattr(path string, stat *fuse.Stat_t, fh uint64) int {
|
func (r *FSRoot) Getattr(path string, stat *fuse.Stat_t, fh uint64) int {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
|
|
||||||
// If node doesn't exist, try dynamic lookup
|
// if node doesn't exist, try dynamic lookup
|
||||||
// But skip for special files starting with @ or .
|
// but skip for special files starting with @ or .
|
||||||
if node == nil {
|
if node == nil {
|
||||||
basename := filepath.Base(path)
|
basename := filepath.Base(path)
|
||||||
if !strings.HasPrefix(basename, "@") && !strings.HasPrefix(basename, ".") {
|
if !strings.HasPrefix(basename, "@") && !strings.HasPrefix(basename, ".") {
|
||||||
@@ -480,14 +482,14 @@ func (r *NostrRoot) Getattr(path string, stat *fuse.Stat_t, fh uint64) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// dynamicLookup tries to create nodes on-demand for npub/note/nevent paths
|
// dynamicLookup tries to create nodes on-demand for npub/note/nevent paths
|
||||||
func (r *NostrRoot) dynamicLookup(path string) bool {
|
func (r *FSRoot) dynamicLookup(path string) bool {
|
||||||
// Normalize path
|
// normalize path
|
||||||
path = strings.ReplaceAll(path, "\\", "/")
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
path = "/" + path
|
path = "/" + path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the first component after root
|
// get the first component after root
|
||||||
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
|
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
return false
|
return false
|
||||||
@@ -495,10 +497,10 @@ func (r *NostrRoot) dynamicLookup(path string) bool {
|
|||||||
|
|
||||||
name := parts[0]
|
name := parts[0]
|
||||||
|
|
||||||
// Try to decode as nostr pointer
|
// try to decode as nostr pointer
|
||||||
pointer, err := nip19.ToPointer(name)
|
pointer, err := nip19.ToPointer(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Try NIP-05
|
// try NIP-05
|
||||||
if strings.Contains(name, "@") && !strings.HasPrefix(name, "@") {
|
if strings.Contains(name, "@") && !strings.HasPrefix(name, "@") {
|
||||||
ctx, cancel := context.WithTimeout(r.ctx, time.Second*5)
|
ctx, cancel := context.WithTimeout(r.ctx, time.Second*5)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -515,19 +517,19 @@ func (r *NostrRoot) dynamicLookup(path string) bool {
|
|||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
// Check if already exists
|
// check if already exists
|
||||||
if _, exists := r.nodes["/"+name]; exists {
|
if _, exists := r.nodes["/"+name]; exists {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
switch p := pointer.(type) {
|
switch p := pointer.(type) {
|
||||||
case nostr.ProfilePointer:
|
case nostr.ProfilePointer:
|
||||||
// Create npub directory dynamically
|
// create npub directory dynamically
|
||||||
r.createNpubDirLocked(name, p.PublicKey, nil)
|
r.createNpubDirLocked(name, p.PublicKey, nil)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case nostr.EventPointer:
|
case nostr.EventPointer:
|
||||||
// Create event directory dynamically
|
// create event directory dynamically
|
||||||
return r.createEventDirLocked(name, p)
|
return r.createEventDirLocked(name, p)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -535,30 +537,30 @@ func (r *NostrRoot) dynamicLookup(path string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) createNpubDirLocked(npub string, pubkey nostr.PubKey, signer nostr.Signer) {
|
func (r *FSRoot) createNpubDirLocked(npub string, pubkey nostr.PubKey, signer nostr.Signer) {
|
||||||
dirPath := "/" + npub
|
dirPath := "/" + npub
|
||||||
|
|
||||||
// Check if already exists
|
// check if already exists
|
||||||
if _, exists := r.nodes[dirPath]; exists {
|
if _, exists := r.nodes[dirPath]; exists {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dirNode := &Node{
|
dirNode := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: dirPath,
|
path: dirPath,
|
||||||
name: npub,
|
name: npub,
|
||||||
isDir: true,
|
isDir: true,
|
||||||
mode: fuse.S_IFDIR | 0755,
|
mode: fuse.S_IFDIR | 0755,
|
||||||
mtime: time.Now(),
|
mtime: time.Now(),
|
||||||
children: make(map[string]*Node),
|
children: make(map[string]*FSNode),
|
||||||
}
|
}
|
||||||
r.nextIno++
|
r.nextIno++
|
||||||
r.nodes[dirPath] = dirNode
|
r.nodes[dirPath] = dirNode
|
||||||
r.nodes["/"].children[npub] = dirNode
|
r.nodes["/"].children[npub] = dirNode
|
||||||
|
|
||||||
// Add pubkey file
|
// add pubkey file
|
||||||
pubkeyData := []byte(pubkey.Hex() + "\n")
|
pubkeyData := []byte(pubkey.Hex() + "\n")
|
||||||
pubkeyNode := &Node{
|
pubkeyNode := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: dirPath + "/pubkey",
|
path: dirPath + "/pubkey",
|
||||||
name: "pubkey",
|
name: "pubkey",
|
||||||
@@ -572,78 +574,78 @@ func (r *NostrRoot) createNpubDirLocked(npub string, pubkey nostr.PubKey, signer
|
|||||||
r.nodes[dirPath+"/pubkey"] = pubkeyNode
|
r.nodes[dirPath+"/pubkey"] = pubkeyNode
|
||||||
dirNode.children["pubkey"] = pubkeyNode
|
dirNode.children["pubkey"] = pubkeyNode
|
||||||
|
|
||||||
// Fetch metadata asynchronously
|
// fetch metadata asynchronously
|
||||||
go r.fetchMetadata(dirPath, pubkey)
|
go r.fetchMetadata(dirPath, pubkey)
|
||||||
|
|
||||||
// Add notes directory
|
// add notes directory
|
||||||
r.createViewDirLocked(dirPath, "notes", nostr.Filter{
|
r.createViewDirLocked(dirPath, "notes", nostr.Filter{
|
||||||
Kinds: []nostr.Kind{1},
|
Kinds: []nostr.Kind{1},
|
||||||
Authors: []nostr.PubKey{pubkey},
|
Authors: []nostr.PubKey{pubkey},
|
||||||
Limit: 50,
|
Limit: 50,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add articles directory
|
// add articles directory
|
||||||
r.createViewDirLocked(dirPath, "articles", nostr.Filter{
|
r.createViewDirLocked(dirPath, "articles", nostr.Filter{
|
||||||
Kinds: []nostr.Kind{30023},
|
Kinds: []nostr.Kind{30023},
|
||||||
Authors: []nostr.PubKey{pubkey},
|
Authors: []nostr.PubKey{pubkey},
|
||||||
Limit: 50,
|
Limit: 50,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add comments directory
|
// add comments directory
|
||||||
r.createViewDirLocked(dirPath, "comments", nostr.Filter{
|
r.createViewDirLocked(dirPath, "comments", nostr.Filter{
|
||||||
Kinds: []nostr.Kind{1111},
|
Kinds: []nostr.Kind{1111},
|
||||||
Authors: []nostr.PubKey{pubkey},
|
Authors: []nostr.PubKey{pubkey},
|
||||||
Limit: 50,
|
Limit: 50,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add highlights directory
|
// add highlights directory
|
||||||
r.createViewDirLocked(dirPath, "highlights", nostr.Filter{
|
r.createViewDirLocked(dirPath, "highlights", nostr.Filter{
|
||||||
Kinds: []nostr.Kind{9802},
|
Kinds: []nostr.Kind{9802},
|
||||||
Authors: []nostr.PubKey{pubkey},
|
Authors: []nostr.PubKey{pubkey},
|
||||||
Limit: 50,
|
Limit: 50,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add photos directory
|
// add photos directory
|
||||||
r.createViewDirLocked(dirPath, "photos", nostr.Filter{
|
r.createViewDirLocked(dirPath, "photos", nostr.Filter{
|
||||||
Kinds: []nostr.Kind{20},
|
Kinds: []nostr.Kind{20},
|
||||||
Authors: []nostr.PubKey{pubkey},
|
Authors: []nostr.PubKey{pubkey},
|
||||||
Limit: 50,
|
Limit: 50,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add videos directory
|
// add videos directory
|
||||||
r.createViewDirLocked(dirPath, "videos", nostr.Filter{
|
r.createViewDirLocked(dirPath, "videos", nostr.Filter{
|
||||||
Kinds: []nostr.Kind{21, 22},
|
Kinds: []nostr.Kind{21, 22},
|
||||||
Authors: []nostr.PubKey{pubkey},
|
Authors: []nostr.PubKey{pubkey},
|
||||||
Limit: 50,
|
Limit: 50,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add wikis directory
|
// add wikis directory
|
||||||
r.createViewDirLocked(dirPath, "wikis", nostr.Filter{
|
r.createViewDirLocked(dirPath, "wikis", nostr.Filter{
|
||||||
Kinds: []nostr.Kind{30818},
|
Kinds: []nostr.Kind{30818},
|
||||||
Authors: []nostr.PubKey{pubkey},
|
Authors: []nostr.PubKey{pubkey},
|
||||||
Limit: 50,
|
Limit: 50,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Fetch profile picture asynchronously
|
// fetch profile picture asynchronously
|
||||||
go r.fetchProfilePicture(dirPath, pubkey)
|
go r.fetchProfilePicture(dirPath, pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) createViewDirLocked(parentPath, name string, filter nostr.Filter) {
|
func (r *FSRoot) createViewDirLocked(parentPath, name string, filter nostr.Filter) {
|
||||||
dirPath := parentPath + "/" + name
|
dirPath := parentPath + "/" + name
|
||||||
|
|
||||||
// Check if already exists
|
// check if already exists
|
||||||
if _, exists := r.nodes[dirPath]; exists {
|
if _, exists := r.nodes[dirPath]; exists {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dirNode := &Node{
|
dirNode := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: dirPath,
|
path: dirPath,
|
||||||
name: name,
|
name: name,
|
||||||
isDir: true,
|
isDir: true,
|
||||||
mode: fuse.S_IFDIR | 0755,
|
mode: fuse.S_IFDIR | 0755,
|
||||||
mtime: time.Now(),
|
mtime: time.Now(),
|
||||||
children: make(map[string]*Node),
|
children: make(map[string]*FSNode),
|
||||||
}
|
}
|
||||||
r.nextIno++
|
r.nextIno++
|
||||||
|
|
||||||
@@ -652,14 +654,14 @@ func (r *NostrRoot) createViewDirLocked(parentPath, name string, filter nostr.Fi
|
|||||||
parent.children[name] = dirNode
|
parent.children[name] = dirNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch events asynchronously
|
// fetch events asynchronously
|
||||||
go r.fetchEvents(dirPath, filter)
|
go r.fetchEvents(dirPath, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer) bool {
|
func (r *FSRoot) createEventDirLocked(name string, pointer nostr.EventPointer) bool {
|
||||||
dirPath := "/" + name
|
dirPath := "/" + name
|
||||||
|
|
||||||
// Fetch the event
|
// fetch the event
|
||||||
ctx, cancel := context.WithTimeout(r.ctx, time.Second*10)
|
ctx, cancel := context.WithTimeout(r.ctx, time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -676,7 +678,7 @@ func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer
|
|||||||
for ie := range r.sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{
|
for ie := range r.sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{
|
||||||
Label: "nak-fs-event",
|
Label: "nak-fs-event",
|
||||||
}) {
|
}) {
|
||||||
// Make a copy to avoid pointer issues
|
// make a copy to avoid pointer issues
|
||||||
evtCopy := ie.Event
|
evtCopy := ie.Event
|
||||||
evt = &evtCopy
|
evt = &evtCopy
|
||||||
break
|
break
|
||||||
@@ -686,24 +688,24 @@ func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create event directory
|
// create event directory
|
||||||
dirNode := &Node{
|
dirNode := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: dirPath,
|
path: dirPath,
|
||||||
name: name,
|
name: name,
|
||||||
isDir: true,
|
isDir: true,
|
||||||
mode: fuse.S_IFDIR | 0755,
|
mode: fuse.S_IFDIR | 0755,
|
||||||
mtime: time.Unix(int64(evt.CreatedAt), 0),
|
mtime: time.Unix(int64(evt.CreatedAt), 0),
|
||||||
children: make(map[string]*Node),
|
children: make(map[string]*FSNode),
|
||||||
}
|
}
|
||||||
r.nextIno++
|
r.nextIno++
|
||||||
r.nodes[dirPath] = dirNode
|
r.nodes[dirPath] = dirNode
|
||||||
r.nodes["/"].children[name] = dirNode
|
r.nodes["/"].children[name] = dirNode
|
||||||
|
|
||||||
// Add content file
|
// add content file
|
||||||
ext := kindToExtension(evt.Kind)
|
ext := kindToExtension(evt.Kind)
|
||||||
contentPath := dirPath + "/content." + ext
|
contentPath := dirPath + "/content." + ext
|
||||||
contentNode := &Node{
|
contentNode := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: contentPath,
|
path: contentPath,
|
||||||
name: "content." + ext,
|
name: "content." + ext,
|
||||||
@@ -717,10 +719,10 @@ func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer
|
|||||||
r.nodes[contentPath] = contentNode
|
r.nodes[contentPath] = contentNode
|
||||||
dirNode.children["content."+ext] = contentNode
|
dirNode.children["content."+ext] = contentNode
|
||||||
|
|
||||||
// Add event.json
|
// add event.json
|
||||||
eventJSON, _ := json.MarshalIndent(evt, "", " ")
|
eventJSON, _ := stdjson.MarshalIndent(evt, "", " ")
|
||||||
eventJSONPath := dirPath + "/event.json"
|
eventJSONPath := dirPath + "/event.json"
|
||||||
eventJSONNode := &Node{
|
eventJSONNode := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: eventJSONPath,
|
path: eventJSONPath,
|
||||||
name: "event.json",
|
name: "event.json",
|
||||||
@@ -737,11 +739,11 @@ func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Readdir(path string,
|
func (r *FSRoot) Readdir(path string,
|
||||||
fill func(name string, stat *fuse.Stat_t, ofst int64) bool,
|
fill func(name string, stat *fuse.Stat_t, ofst int64) bool,
|
||||||
ofst int64,
|
ofst int64,
|
||||||
fh uint64) int {
|
fh uint64,
|
||||||
|
) int {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
if node == nil || !node.isDir {
|
if node == nil || !node.isDir {
|
||||||
return -fuse.ENOENT
|
return -fuse.ENOENT
|
||||||
@@ -768,8 +770,8 @@ func (r *NostrRoot) Readdir(path string,
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Open(path string, flags int) (int, uint64) {
|
func (r *FSRoot) Open(path string, flags int) (int, uint64) {
|
||||||
// Log the open attempt
|
// log the open attempt
|
||||||
if r.ctx.Value("logverbose") != nil {
|
if r.ctx.Value("logverbose") != nil {
|
||||||
logv := r.ctx.Value("logverbose").(func(string, ...interface{}))
|
logv := r.ctx.Value("logverbose").(func(string, ...interface{}))
|
||||||
logv("Open: path='%s' flags=%d\n", path, flags)
|
logv("Open: path='%s' flags=%d\n", path, flags)
|
||||||
@@ -783,7 +785,7 @@ func (r *NostrRoot) Open(path string, flags int) (int, uint64) {
|
|||||||
return -fuse.EISDIR, ^uint64(0)
|
return -fuse.EISDIR, ^uint64(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load data if needed
|
// load data if needed
|
||||||
if node.loadFunc != nil && !node.loaded {
|
if node.loadFunc != nil && !node.loaded {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
if !node.loaded {
|
if !node.loaded {
|
||||||
@@ -799,7 +801,7 @@ func (r *NostrRoot) Open(path string, flags int) (int, uint64) {
|
|||||||
return 0, node.ino
|
return 0, node.ino
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Read(path string, buff []byte, ofst int64, fh uint64) int {
|
func (r *FSRoot) Read(path string, buff []byte, ofst int64, fh uint64) int {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
if node == nil || node.isDir {
|
if node == nil || node.isDir {
|
||||||
return -fuse.ENOENT
|
return -fuse.ENOENT
|
||||||
@@ -818,7 +820,7 @@ func (r *NostrRoot) Read(path string, buff []byte, ofst int64, fh uint64) int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Opendir(path string) (int, uint64) {
|
func (r *FSRoot) Opendir(path string) (int, uint64) {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return -fuse.ENOENT, ^uint64(0)
|
return -fuse.ENOENT, ^uint64(0)
|
||||||
@@ -829,17 +831,17 @@ func (r *NostrRoot) Opendir(path string) (int, uint64) {
|
|||||||
return 0, node.ino
|
return 0, node.ino
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Release(path string, fh uint64) int {
|
func (r *FSRoot) Release(path string, fh uint64) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Releasedir(path string, fh uint64) int {
|
func (r *FSRoot) Releasedir(path string, fh uint64) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new file
|
// Create creates a new file
|
||||||
func (r *NostrRoot) Create(path string, flags int, mode uint32) (int, uint64) {
|
func (r *FSRoot) Create(path string, flags int, mode uint32) (int, uint64) {
|
||||||
// Parse path
|
// parse path
|
||||||
path = strings.ReplaceAll(path, "\\", "/")
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
path = "/" + path
|
path = "/" + path
|
||||||
@@ -851,19 +853,19 @@ func (r *NostrRoot) Create(path string, flags int, mode uint32) (int, uint64) {
|
|||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
// Check if parent directory exists
|
// check if parent directory exists
|
||||||
parent, ok := r.nodes[dir]
|
parent, ok := r.nodes[dir]
|
||||||
if !ok || !parent.isDir {
|
if !ok || !parent.isDir {
|
||||||
return -fuse.ENOENT, ^uint64(0)
|
return -fuse.ENOENT, ^uint64(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if file already exists
|
// check if file already exists
|
||||||
if _, exists := r.nodes[path]; exists {
|
if _, exists := r.nodes[path]; exists {
|
||||||
return -fuse.EEXIST, ^uint64(0)
|
return -fuse.EEXIST, ^uint64(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new file node
|
// create new file node
|
||||||
fileNode := &Node{
|
fileNode := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: path,
|
path: path,
|
||||||
name: name,
|
name: name,
|
||||||
@@ -882,7 +884,7 @@ func (r *NostrRoot) Create(path string, flags int, mode uint32) (int, uint64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Truncate truncates a file
|
// Truncate truncates a file
|
||||||
func (r *NostrRoot) Truncate(path string, size int64, fh uint64) int {
|
func (r *FSRoot) Truncate(path string, size int64, fh uint64) int {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return -fuse.ENOENT
|
return -fuse.ENOENT
|
||||||
@@ -899,7 +901,7 @@ func (r *NostrRoot) Truncate(path string, size int64, fh uint64) int {
|
|||||||
} else if size < int64(len(node.data)) {
|
} else if size < int64(len(node.data)) {
|
||||||
node.data = node.data[:size]
|
node.data = node.data[:size]
|
||||||
} else {
|
} else {
|
||||||
// Extend with zeros
|
// extend with zeros
|
||||||
newData := make([]byte, size)
|
newData := make([]byte, size)
|
||||||
copy(newData, node.data)
|
copy(newData, node.data)
|
||||||
node.data = newData
|
node.data = newData
|
||||||
@@ -911,7 +913,7 @@ func (r *NostrRoot) Truncate(path string, size int64, fh uint64) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write writes data to a file
|
// Write writes data to a file
|
||||||
func (r *NostrRoot) Write(path string, buff []byte, ofst int64, fh uint64) int {
|
func (r *FSRoot) Write(path string, buff []byte, ofst int64, fh uint64) int {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return -fuse.ENOENT
|
return -fuse.ENOENT
|
||||||
@@ -925,7 +927,7 @@ func (r *NostrRoot) Write(path string, buff []byte, ofst int64, fh uint64) int {
|
|||||||
|
|
||||||
endofst := ofst + int64(len(buff))
|
endofst := ofst + int64(len(buff))
|
||||||
|
|
||||||
// Extend data if necessary
|
// extend data if necessary
|
||||||
if endofst > int64(len(node.data)) {
|
if endofst > int64(len(node.data)) {
|
||||||
newData := make([]byte, endofst)
|
newData := make([]byte, endofst)
|
||||||
copy(newData, node.data)
|
copy(newData, node.data)
|
||||||
@@ -936,14 +938,14 @@ func (r *NostrRoot) Write(path string, buff []byte, ofst int64, fh uint64) int {
|
|||||||
node.size = int64(len(node.data))
|
node.size = int64(len(node.data))
|
||||||
node.mtime = time.Now()
|
node.mtime = time.Now()
|
||||||
|
|
||||||
// Check if this is a note that should be auto-published
|
// check if this is a note that should be auto-published
|
||||||
if r.signer != nil && strings.Contains(path, "/notes/") && !strings.HasPrefix(filepath.Base(path), ".") {
|
if r.signer != nil && strings.Contains(path, "/notes/") && !strings.HasPrefix(filepath.Base(path), ".") {
|
||||||
// Cancel existing timer if any
|
// cancel existing timer if any
|
||||||
if timer, exists := r.pendingNotes[path]; exists {
|
if timer, exists := r.pendingNotes[path]; exists {
|
||||||
timer.Stop()
|
timer.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule auto-publish
|
// schedule auto-publish
|
||||||
timeout := r.opts.AutoPublishNotesTimeout
|
timeout := r.opts.AutoPublishNotesTimeout
|
||||||
if timeout > 0 && timeout < time.Hour*24*365 {
|
if timeout > 0 && timeout < time.Hour*24*365 {
|
||||||
r.pendingNotes[path] = time.AfterFunc(timeout, func() {
|
r.pendingNotes[path] = time.AfterFunc(timeout, func() {
|
||||||
@@ -955,7 +957,7 @@ func (r *NostrRoot) Write(path string, buff []byte, ofst int64, fh uint64) int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) publishNote(path string) {
|
func (r *FSRoot) publishNote(path string) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
node, ok := r.nodes[path]
|
node, ok := r.nodes[path]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -973,7 +975,7 @@ func (r *NostrRoot) publishNote(path string) {
|
|||||||
log := r.getLog()
|
log := r.getLog()
|
||||||
log("- auto-publishing note from %s\n", path)
|
log("- auto-publishing note from %s\n", path)
|
||||||
|
|
||||||
// Create and sign event
|
// create and sign event
|
||||||
evt := &nostr.Event{
|
evt := &nostr.Event{
|
||||||
CreatedAt: nostr.Now(),
|
CreatedAt: nostr.Now(),
|
||||||
Kind: 1,
|
Kind: 1,
|
||||||
@@ -986,7 +988,7 @@ func (r *NostrRoot) publishNote(path string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish to relays
|
// publish to relays
|
||||||
ctx, cancel := context.WithTimeout(r.ctx, time.Second*10)
|
ctx, cancel := context.WithTimeout(r.ctx, time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -1005,7 +1007,7 @@ func (r *NostrRoot) publishNote(path string) {
|
|||||||
|
|
||||||
log("- published note %s to %d relays\n", evt.ID.Hex()[:8], len(relays))
|
log("- published note %s to %d relays\n", evt.ID.Hex()[:8], len(relays))
|
||||||
|
|
||||||
// Update filename to include event ID
|
// update filename to include event ID
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
@@ -1015,7 +1017,7 @@ func (r *NostrRoot) publishNote(path string) {
|
|||||||
newName := evt.ID.Hex()[:8] + ext
|
newName := evt.ID.Hex()[:8] + ext
|
||||||
newPath := dir + "/" + newName
|
newPath := dir + "/" + newName
|
||||||
|
|
||||||
// Rename node
|
// rename node
|
||||||
if _, exists := r.nodes[newPath]; !exists {
|
if _, exists := r.nodes[newPath]; !exists {
|
||||||
node.path = newPath
|
node.path = newPath
|
||||||
node.name = newName
|
node.name = newName
|
||||||
@@ -1032,7 +1034,7 @@ func (r *NostrRoot) publishNote(path string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unlink deletes a file
|
// Unlink deletes a file
|
||||||
func (r *NostrRoot) Unlink(path string) int {
|
func (r *FSRoot) Unlink(path string) int {
|
||||||
path = strings.ReplaceAll(path, "\\", "/")
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
path = "/" + path
|
path = "/" + path
|
||||||
@@ -1044,7 +1046,7 @@ func (r *NostrRoot) Unlink(path string) int {
|
|||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
// Check if file exists
|
// check if file exists
|
||||||
node, ok := r.nodes[path]
|
node, ok := r.nodes[path]
|
||||||
if !ok {
|
if !ok {
|
||||||
return -fuse.ENOENT
|
return -fuse.ENOENT
|
||||||
@@ -1053,19 +1055,19 @@ func (r *NostrRoot) Unlink(path string) int {
|
|||||||
return -fuse.EISDIR
|
return -fuse.EISDIR
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from parent
|
// remove from parent
|
||||||
if parent, ok := r.nodes[dir]; ok {
|
if parent, ok := r.nodes[dir]; ok {
|
||||||
delete(parent.children, name)
|
delete(parent.children, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from nodes map
|
// remove from nodes map
|
||||||
delete(r.nodes, path)
|
delete(r.nodes, path)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mkdir creates a new directory
|
// Mkdir creates a new directory
|
||||||
func (r *NostrRoot) Mkdir(path string, mode uint32) int {
|
func (r *FSRoot) Mkdir(path string, mode uint32) int {
|
||||||
path = strings.ReplaceAll(path, "\\", "/")
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
path = "/" + path
|
path = "/" + path
|
||||||
@@ -1077,26 +1079,26 @@ func (r *NostrRoot) Mkdir(path string, mode uint32) int {
|
|||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
// Check if parent directory exists
|
// check if parent directory exists
|
||||||
parent, ok := r.nodes[dir]
|
parent, ok := r.nodes[dir]
|
||||||
if !ok || !parent.isDir {
|
if !ok || !parent.isDir {
|
||||||
return -fuse.ENOENT
|
return -fuse.ENOENT
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if directory already exists
|
// check if directory already exists
|
||||||
if _, exists := r.nodes[path]; exists {
|
if _, exists := r.nodes[path]; exists {
|
||||||
return -fuse.EEXIST
|
return -fuse.EEXIST
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new directory node
|
// create new directory node
|
||||||
dirNode := &Node{
|
dirNode := &FSNode{
|
||||||
ino: r.nextIno,
|
ino: r.nextIno,
|
||||||
path: path,
|
path: path,
|
||||||
name: name,
|
name: name,
|
||||||
isDir: true,
|
isDir: true,
|
||||||
mode: fuse.S_IFDIR | 0755,
|
mode: fuse.S_IFDIR | 0755,
|
||||||
mtime: time.Now(),
|
mtime: time.Now(),
|
||||||
children: make(map[string]*Node),
|
children: make(map[string]*FSNode),
|
||||||
}
|
}
|
||||||
r.nextIno++
|
r.nextIno++
|
||||||
|
|
||||||
@@ -1107,7 +1109,7 @@ func (r *NostrRoot) Mkdir(path string, mode uint32) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rmdir removes a directory
|
// Rmdir removes a directory
|
||||||
func (r *NostrRoot) Rmdir(path string) int {
|
func (r *FSRoot) Rmdir(path string) int {
|
||||||
path = strings.ReplaceAll(path, "\\", "/")
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
path = "/" + path
|
path = "/" + path
|
||||||
@@ -1123,7 +1125,7 @@ func (r *NostrRoot) Rmdir(path string) int {
|
|||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
// Check if directory exists
|
// check if directory exists
|
||||||
node, ok := r.nodes[path]
|
node, ok := r.nodes[path]
|
||||||
if !ok {
|
if !ok {
|
||||||
return -fuse.ENOENT
|
return -fuse.ENOENT
|
||||||
@@ -1132,24 +1134,24 @@ func (r *NostrRoot) Rmdir(path string) int {
|
|||||||
return -fuse.ENOTDIR
|
return -fuse.ENOTDIR
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if directory is empty
|
// check if directory is empty
|
||||||
if len(node.children) > 0 {
|
if len(node.children) > 0 {
|
||||||
return -fuse.ENOTEMPTY
|
return -fuse.ENOTEMPTY
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from parent
|
// remove from parent
|
||||||
if parent, ok := r.nodes[dir]; ok {
|
if parent, ok := r.nodes[dir]; ok {
|
||||||
delete(parent.children, name)
|
delete(parent.children, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from nodes map
|
// remove from nodes map
|
||||||
delete(r.nodes, path)
|
delete(r.nodes, path)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utimens updates file timestamps
|
// Utimens updates file timestamps
|
||||||
func (r *NostrRoot) Utimens(path string, tmsp []fuse.Timespec) int {
|
func (r *FSRoot) Utimens(path string, tmsp []fuse.Timespec) int {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return -fuse.ENOENT
|
return -fuse.ENOENT
|
||||||
@@ -1164,3 +1166,14 @@ func (r *NostrRoot) Utimens(path string, tmsp []fuse.Timespec) int {
|
|||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func kindToExtension(kind nostr.Kind) string {
|
||||||
|
switch kind {
|
||||||
|
case 30023:
|
||||||
|
return "md"
|
||||||
|
case 30818:
|
||||||
|
return "djot"
|
||||||
|
default:
|
||||||
|
return "txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/keyer"
|
"fiatjaf.com/nostr/keyer"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/fiatjaf/nak/nostrfs"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/winfsp/cgofuse/fuse"
|
"github.com/winfsp/cgofuse/fuse"
|
||||||
)
|
)
|
||||||
@@ -62,7 +61,7 @@ var fsCmd = &cli.Command{
|
|||||||
apat = time.Hour * 24 * 365 * 3
|
apat = time.Hour * 24 * 365 * 3
|
||||||
}
|
}
|
||||||
|
|
||||||
root := nostrfs.NewNostrRoot(
|
root := NewFSRoot(
|
||||||
context.WithValue(
|
context.WithValue(
|
||||||
context.WithValue(
|
context.WithValue(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -73,7 +72,7 @@ var fsCmd = &cli.Command{
|
|||||||
sys,
|
sys,
|
||||||
kr,
|
kr,
|
||||||
mountpoint,
|
mountpoint,
|
||||||
nostrfs.Options{
|
FSOptions{
|
||||||
AutoPublishNotesTimeout: apnt,
|
AutoPublishNotesTimeout: apnt,
|
||||||
AutoPublishArticlesTimeout: apat,
|
AutoPublishArticlesTimeout: apat,
|
||||||
},
|
},
|
||||||
@@ -82,37 +81,37 @@ var fsCmd = &cli.Command{
|
|||||||
// create the server
|
// create the server
|
||||||
log("- mounting at %s... ", color.HiCyanString(mountpoint))
|
log("- mounting at %s... ", color.HiCyanString(mountpoint))
|
||||||
|
|
||||||
// Create cgofuse host
|
// create cgofuse host
|
||||||
host := fuse.NewFileSystemHost(root)
|
host := fuse.NewFileSystemHost(root)
|
||||||
host.SetCapReaddirPlus(true)
|
host.SetCapReaddirPlus(true)
|
||||||
host.SetUseIno(true)
|
host.SetUseIno(true)
|
||||||
|
|
||||||
// Mount the filesystem - Windows/WinFsp version
|
// mount the filesystem - Windows/WinFsp version
|
||||||
// Based on rclone cmount implementation
|
// based on rclone cmount implementation
|
||||||
mountArgs := []string{
|
mountArgs := []string{
|
||||||
"-o", "uid=-1",
|
"-o", "uid=-1",
|
||||||
"-o", "gid=-1",
|
"-o", "gid=-1",
|
||||||
"--FileSystemName=nak",
|
"--FileSystemName=nak",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if mountpoint is a drive letter or directory
|
// check if mountpoint is a drive letter or directory
|
||||||
isDriveLetter := len(mountpoint) == 2 && mountpoint[1] == ':'
|
isDriveLetter := len(mountpoint) == 2 && mountpoint[1] == ':'
|
||||||
|
|
||||||
if !isDriveLetter {
|
if !isDriveLetter {
|
||||||
// WinFsp primarily supports drive letters on Windows
|
// winFsp primarily supports drive letters on Windows
|
||||||
// Directory mounting may not work reliably
|
// directory mounting may not work reliably
|
||||||
log("WARNING: directory mounting may not work on Windows (WinFsp limitation)\n")
|
log("WARNING: directory mounting may not work on Windows (WinFsp limitation)\n")
|
||||||
log(" consider using a drive letter instead (e.g., 'nak fs Z:')\n")
|
log(" consider using a drive letter instead (e.g., 'nak fs Z:')\n")
|
||||||
|
|
||||||
// For directory mounts, follow rclone's approach:
|
// for directory mounts, follow rclone's approach:
|
||||||
// 1. Check that mountpoint doesn't already exist
|
// 1. check that mountpoint doesn't already exist
|
||||||
if _, err := os.Stat(mountpoint); err == nil {
|
if _, err := os.Stat(mountpoint); err == nil {
|
||||||
return fmt.Errorf("mountpoint path already exists: %s (must not exist before mounting)", mountpoint)
|
return fmt.Errorf("mountpoint path already exists: %s (must not exist before mounting)", mountpoint)
|
||||||
} else if !os.IsNotExist(err) {
|
} else if !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("failed to check mountpoint: %w", err)
|
return fmt.Errorf("failed to check mountpoint: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check that parent directory exists
|
// 2. check that parent directory exists
|
||||||
parent := filepath.Join(mountpoint, "..")
|
parent := filepath.Join(mountpoint, "..")
|
||||||
if _, err := os.Stat(parent); err != nil {
|
if _, err := os.Stat(parent); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -121,7 +120,7 @@ var fsCmd = &cli.Command{
|
|||||||
return fmt.Errorf("failed to check parent directory: %w", err)
|
return fmt.Errorf("failed to check parent directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Use network mode for directory mounts
|
// 3. use network mode for directory mounts
|
||||||
mountArgs = append(mountArgs, "--VolumePrefix=\\nak\\"+filepath.Base(mountpoint))
|
mountArgs = append(mountArgs, "--VolumePrefix=\\nak\\"+filepath.Base(mountpoint))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +131,6 @@ var fsCmd = &cli.Command{
|
|||||||
|
|
||||||
log("ok.\n")
|
log("ok.\n")
|
||||||
|
|
||||||
// Mount in main thread like hellofs
|
|
||||||
if !host.Mount("", mountArgs) {
|
if !host.Mount("", mountArgs) {
|
||||||
return fmt.Errorf("failed to mount filesystem")
|
return fmt.Errorf("failed to mount filesystem")
|
||||||
}
|
}
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -3,7 +3,6 @@ module github.com/fiatjaf/nak
|
|||||||
go 1.25
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
fiatjaf.com/lib v0.3.1
|
|
||||||
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6
|
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||||
github.com/bep/debounce v1.2.1
|
github.com/bep/debounce v1.2.1
|
||||||
@@ -12,7 +11,7 @@ require (
|
|||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
|
||||||
github.com/fatih/color v1.16.0
|
github.com/fatih/color v1.16.0
|
||||||
|
github.com/hanwen/go-fuse/v2 v2.7.2
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/liamg/magic v0.0.1
|
github.com/liamg/magic v0.0.1
|
||||||
github.com/mailru/easyjson v0.9.1
|
github.com/mailru/easyjson v0.9.1
|
||||||
@@ -30,6 +29,8 @@ require (
|
|||||||
golang.org/x/term v0.32.0
|
golang.org/x/term v0.32.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require fiatjaf.com/lib v0.3.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/FastFilter/xorfilter v0.2.1 // indirect
|
github.com/FastFilter/xorfilter v0.2.1 // indirect
|
||||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -1,5 +1,5 @@
|
|||||||
fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
|
fiatjaf.com/lib v0.3.2 h1:RBS41z70d8Rp8e2nemQsbPY1NLLnEGShiY2c+Bom3+Q=
|
||||||
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
|
fiatjaf.com/lib v0.3.2/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
|
||||||
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6 h1:yH+cU9ZNgUdMCRa5eS3pmqTPP/QdZtSmQAIrN/U5nEc=
|
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6 h1:yH+cU9ZNgUdMCRa5eS3pmqTPP/QdZtSmQAIrN/U5nEc=
|
||||||
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||||
@@ -144,8 +144,8 @@ github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnm
|
|||||||
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
||||||
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
||||||
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
||||||
github.com/hanwen/go-fuse/v2 v2.9.0 h1:0AOGUkHtbOVeyGLr0tXupiid1Vg7QB7M6YUcdmVdC58=
|
github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw=
|
||||||
github.com/hanwen/go-fuse/v2 v2.9.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
|
github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||||
@@ -169,8 +169,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||||
github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM=
|
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/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
@@ -200,8 +200,8 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f
|
|||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
|||||||
12
helpers.go
12
helpers.go
@@ -46,8 +46,14 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func isPiped() bool {
|
func isPiped() bool {
|
||||||
stat, _ := os.Stdin.Stat()
|
stat, err := os.Stdin.Stat()
|
||||||
return stat.Mode()&os.ModeCharDevice == 0
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := stat.Mode()
|
||||||
|
is := mode&os.ModeCharDevice == 0
|
||||||
|
return is
|
||||||
}
|
}
|
||||||
|
|
||||||
func getJsonsOrBlank() iter.Seq[string] {
|
func getJsonsOrBlank() iter.Seq[string] {
|
||||||
@@ -76,7 +82,7 @@ func getJsonsOrBlank() iter.Seq[string] {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if !hasStdin && !isPiped() {
|
if !hasStdin {
|
||||||
yield("{}")
|
yield("{}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package nostrfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func kindToExtension(kind nostr.Kind) string {
|
|
||||||
switch kind {
|
|
||||||
case 30023:
|
|
||||||
return "md"
|
|
||||||
case 30818:
|
|
||||||
return "adoc"
|
|
||||||
default:
|
|
||||||
return "txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user