//go:build !windows package main import ( "context" "fmt" "os" "os/signal" "syscall" "time" "github.com/fatih/color" "github.com/fiatjaf/nak/nostrfs" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/keyer" "github.com/urfave/cli/v3" ) var fsCmd = &cli.Command{ Name: "fs", Usage: "mount a FUSE filesystem that exposes Nostr events as files.", Description: `(experimental)`, ArgsUsage: "<mountpoint>", Flags: append(defaultKeyFlags, &cli.StringFlag{ Name: "pubkey", Usage: "public key from where to to prepopulate directories", Validator: func(pk string) error { if nostr.IsValidPublicKey(pk) { return nil } 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() if mountpoint == "" { 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( context.WithValue( context.WithValue( ctx, "log", log, ), "logverbose", logverbose, ), sys, kr, mountpoint, nostrfs.Options{ AutoPublishNotesTimeout: apnt, AutoPublishArticlesTimeout: apat, }, ) // create the server log("- mounting at %s... ", color.HiCyanString(mountpoint)) timeout := time.Second * 120 server, err := fs.Mount(mountpoint, root, &fs.Options{ MountOptions: fuse.MountOptions{ Debug: isVerbose, Name: "nak", FsName: "nak", RememberInodes: true, }, AttrTimeout: &timeout, EntryTimeout: &timeout, Logger: nostr.DebugLogger, }) if err != nil { return fmt.Errorf("mount failed: %w", err) } log("ok.\n") // setup signal handling for clean unmount ch := make(chan os.Signal, 1) chErr := make(chan error) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) go func() { <-ch log("- unmounting... ") err := server.Unmount() if err != nil { chErr <- fmt.Errorf("unmount failed: %w", err) } else { log("ok\n") chErr <- nil } }() // serve the filesystem until unmounted server.Wait() return <-chErr }, }