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