From cb2247c9da68ea06d5bf48d0b9eb54078178e55d Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 25 Dec 2025 16:44:30 +0900 Subject: [PATCH] implement blossom mirror --- blossom.go | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 3 deletions(-) diff --git a/blossom.go b/blossom.go index 45a5eaa..7df0015 100644 --- a/blossom.go +++ b/blossom.go @@ -3,10 +3,15 @@ package main import ( "bytes" "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" "fmt" "io" + "net/http" "os" + "fiatjaf.com/nostr" "fiatjaf.com/nostr/keyer" "fiatjaf.com/nostr/nipb0/blossom" "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", - Usage: "", - Description: ``, + Usage: "mirrors blobs from source server to target server", + 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, - ArgsUsage: "", 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 }, }, @@ -248,3 +288,82 @@ func getBlossomClient(ctx context.Context, c *cli.Command) (*blossom.Client, err } 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 +}