main.go raw

   1  package main
   2  
   3  import (
   4  	"archive/tar"
   5  	"bytes"
   6  	"crypto/sha256"
   7  	"encoding/hex"
   8  	"flag"
   9  	"fmt"
  10  	"io"
  11  	"net/http"
  12  	"os"
  13  	"os/exec"
  14  	"path/filepath"
  15  	"strconv"
  16  	"strings"
  17  
  18  	"git.smesh.lol/orly/pkg/nostr/encoders/bech32encoding"
  19  	"git.smesh.lol/orly/pkg/nostr/interfaces/signer/p8k"
  20  )
  21  
  22  const chunkSize = 512 * 1024 // 512 KB
  23  
  24  func main() {
  25  	url := flag.String("url", "", "smesh base URL (e.g. https://smesh.lol)")
  26  	nsec := flag.String("nsec", "", "deploy nsec (or set DEPLOY_NSEC env)")
  27  	dir := flag.String("dir", "app/smesh3", "directory to bundle")
  28  	flag.Parse()
  29  
  30  	if *url == "" {
  31  		fatal("--url required")
  32  	}
  33  
  34  	nsecStr := *nsec
  35  	if nsecStr == "" {
  36  		nsecStr = os.Getenv("DEPLOY_NSEC")
  37  	}
  38  	if nsecStr == "" {
  39  		fatal("--nsec or DEPLOY_NSEC required")
  40  	}
  41  
  42  	skBytes, err := bech32encoding.NsecToBytes(nsecStr)
  43  	if err != nil {
  44  		fatal("invalid nsec: %v", err)
  45  	}
  46  
  47  	sign, err := p8k.New()
  48  	if err != nil {
  49  		fatal("signer init: %v", err)
  50  	}
  51  	if err := sign.InitSec(skBytes); err != nil {
  52  		fatal("init secret key: %v", err)
  53  	}
  54  
  55  	fmt.Printf("deploy pubkey: %x\n", sign.Pub())
  56  
  57  	bundle, err := createBundle(*dir)
  58  	if err != nil {
  59  		fatal("bundle: %v", err)
  60  	}
  61  	fmt.Printf("bundle: %d bytes (xz -9) from %s\n", len(bundle), *dir)
  62  
  63  	hash := sha256.Sum256(bundle)
  64  	hashHex := hex.EncodeToString(hash[:])
  65  	sig, err := sign.Sign(hash[:])
  66  	if err != nil {
  67  		fatal("sign: %v", err)
  68  	}
  69  
  70  	endpoint := strings.TrimRight(*url, "/") + "/__deploy"
  71  
  72  	// Split into chunks.
  73  	nParts := (len(bundle) + chunkSize - 1) / chunkSize
  74  	fmt.Printf("uploading %d parts (%d KB each)...\n", nParts, chunkSize/1024)
  75  
  76  	// Begin session.
  77  	resp := doPost(endpoint+"?action=begin&hash="+hashHex+"&parts="+strconv.Itoa(nParts), nil)
  78  	if resp != "ok" {
  79  		fatal("begin failed: %s", resp)
  80  	}
  81  
  82  	// Upload parts.
  83  	for i := 0; i < nParts; i++ {
  84  		start := i * chunkSize
  85  		end := start + chunkSize
  86  		if end > len(bundle) {
  87  			end = len(bundle)
  88  		}
  89  		chunk := bundle[start:end]
  90  
  91  		resp := doPost(
  92  			endpoint+"?action=part&hash="+hashHex+"&index="+strconv.Itoa(i),
  93  			chunk,
  94  		)
  95  		fmt.Printf("  part %d/%d (%d bytes): %s\n", i+1, nParts, len(chunk), resp)
  96  	}
  97  
  98  	// Apply with signature.
  99  	req, err := http.NewRequest(http.MethodPost, endpoint+"?action=apply&hash="+hashHex, nil)
 100  	if err != nil {
 101  		fatal("request: %v", err)
 102  	}
 103  	req.Header.Set("X-Sig", hex.EncodeToString(sig))
 104  	r, err := http.DefaultClient.Do(req)
 105  	if err != nil {
 106  		fatal("apply: %v", err)
 107  	}
 108  	defer r.Body.Close()
 109  	body, _ := io.ReadAll(r.Body)
 110  	if r.StatusCode != 200 {
 111  		fatal("apply failed (%d): %s", r.StatusCode, string(body))
 112  	}
 113  	fmt.Printf("deployed: %s", string(body))
 114  }
 115  
 116  func doPost(url string, body []byte) string {
 117  	var reader io.Reader
 118  	if body != nil {
 119  		reader = bytes.NewReader(body)
 120  	}
 121  	req, err := http.NewRequest(http.MethodPost, url, reader)
 122  	if err != nil {
 123  		fatal("request: %v", err)
 124  	}
 125  	if body != nil {
 126  		req.Header.Set("Content-Type", "application/octet-stream")
 127  	}
 128  	resp, err := http.DefaultClient.Do(req)
 129  	if err != nil {
 130  		fatal("upload: %v", err)
 131  	}
 132  	defer resp.Body.Close()
 133  	b, _ := io.ReadAll(resp.Body)
 134  	if resp.StatusCode != 200 {
 135  		fatal("failed (%d): %s", resp.StatusCode, string(b))
 136  	}
 137  	return string(b)
 138  }
 139  
 140  // createBundle creates a tar.xz archive of dir using xz -9.
 141  func createBundle(dir string) ([]byte, error) {
 142  	// Create tar first.
 143  	var tarBuf bytes.Buffer
 144  	tw := tar.NewWriter(&tarBuf)
 145  
 146  	base := filepath.Clean(dir)
 147  	err := filepath.Walk(base, func(path string, info os.FileInfo, err error) error {
 148  		if err != nil {
 149  			return err
 150  		}
 151  		rel, err := filepath.Rel(base, path)
 152  		if err != nil {
 153  			return err
 154  		}
 155  		if rel == "." {
 156  			return nil
 157  		}
 158  		hdr, err := tar.FileInfoHeader(info, "")
 159  		if err != nil {
 160  			return err
 161  		}
 162  		hdr.Name = rel
 163  		if err := tw.WriteHeader(hdr); err != nil {
 164  			return err
 165  		}
 166  		if !info.Mode().IsRegular() {
 167  			return nil
 168  		}
 169  		f, err := os.Open(path)
 170  		if err != nil {
 171  			return err
 172  		}
 173  		defer f.Close()
 174  		_, err = io.Copy(tw, f)
 175  		return err
 176  	})
 177  	if err != nil {
 178  		return nil, err
 179  	}
 180  	if err := tw.Close(); err != nil {
 181  		return nil, err
 182  	}
 183  
 184  	// Compress with xz -9.
 185  	var xzBuf bytes.Buffer
 186  	cmd := exec.Command("xz", "-9", "--stdout")
 187  	cmd.Stdin = &tarBuf
 188  	cmd.Stdout = &xzBuf
 189  	cmd.Stderr = os.Stderr
 190  	if err := cmd.Run(); err != nil {
 191  		return nil, fmt.Errorf("xz: %w", err)
 192  	}
 193  
 194  	return xzBuf.Bytes(), nil
 195  }
 196  
 197  func fatal(format string, args ...any) {
 198  	fmt.Fprintf(os.Stderr, format+"\n", args...)
 199  	os.Exit(1)
 200  }
 201