|
|
|
@ -0,0 +1,384 @@ |
|
|
|
|
|
|
|
package main |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
|
|
|
"bytes" |
|
|
|
|
|
|
|
"crypto/rand" |
|
|
|
|
|
|
|
"encoding/base64" |
|
|
|
|
|
|
|
"encoding/hex" |
|
|
|
|
|
|
|
"encoding/json" |
|
|
|
|
|
|
|
"flag" |
|
|
|
|
|
|
|
"fmt" |
|
|
|
|
|
|
|
"io" |
|
|
|
|
|
|
|
"net/http" |
|
|
|
|
|
|
|
"os" |
|
|
|
|
|
|
|
"strings" |
|
|
|
|
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"git.mleku.dev/mleku/nostr/crypto/ec/schnorr" |
|
|
|
|
|
|
|
"git.mleku.dev/mleku/nostr/crypto/ec/secp256k1" |
|
|
|
|
|
|
|
"git.mleku.dev/mleku/nostr/encoders/bech32encoding" |
|
|
|
|
|
|
|
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k" |
|
|
|
|
|
|
|
"github.com/minio/sha256-simd" |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
|
|
|
// BlossomAuthKind is the Nostr event kind for Blossom authorization (BUD-01)
|
|
|
|
|
|
|
|
BlossomAuthKind = 24242 |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var ( |
|
|
|
|
|
|
|
relayURL = flag.String("url", "http://localhost:3334", "Relay base URL") |
|
|
|
|
|
|
|
nsec = flag.String("nsec", "", "Nostr private key (nsec format). If empty, generates a new key") |
|
|
|
|
|
|
|
blobSize = flag.Int("size", 1024, "Size of test blob in bytes") |
|
|
|
|
|
|
|
verbose = flag.Bool("v", false, "Verbose output") |
|
|
|
|
|
|
|
noAuth = flag.Bool("no-auth", false, "Skip authentication (test anonymous uploads)") |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// BlossomDescriptor represents a blob descriptor returned by the server
|
|
|
|
|
|
|
|
type BlossomDescriptor struct { |
|
|
|
|
|
|
|
URL string `json:"url"` |
|
|
|
|
|
|
|
SHA256 string `json:"sha256"` |
|
|
|
|
|
|
|
Size int64 `json:"size"` |
|
|
|
|
|
|
|
Type string `json:"type,omitempty"` |
|
|
|
|
|
|
|
Uploaded int64 `json:"uploaded"` |
|
|
|
|
|
|
|
PublicKey string `json:"public_key,omitempty"` |
|
|
|
|
|
|
|
Tags [][]string `json:"tags,omitempty"` |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func main() { |
|
|
|
|
|
|
|
flag.Parse() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fmt.Println("🌸 Blossom Test Tool") |
|
|
|
|
|
|
|
fmt.Println("===================\n") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get or generate keypair (only if auth is enabled)
|
|
|
|
|
|
|
|
var sec, pub []byte |
|
|
|
|
|
|
|
var err error |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if !*noAuth { |
|
|
|
|
|
|
|
sec, pub, err = getKeypair() |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error getting keypair: %v\n", err) |
|
|
|
|
|
|
|
os.Exit(1) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pubkey, _ := schnorr.ParsePubKey(pub) |
|
|
|
|
|
|
|
npubBytes, _ := bech32encoding.PublicKeyToNpub(pubkey) |
|
|
|
|
|
|
|
fmt.Printf("Using identity: %s\n", string(npubBytes)) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
fmt.Printf("Testing anonymous uploads (no authentication)\n") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
fmt.Printf("Relay URL: %s\n\n", *relayURL) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Generate random test data
|
|
|
|
|
|
|
|
testData := make([]byte, *blobSize) |
|
|
|
|
|
|
|
if _, err := rand.Read(testData); err != nil { |
|
|
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error generating test data: %v\n", err) |
|
|
|
|
|
|
|
os.Exit(1) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate SHA256
|
|
|
|
|
|
|
|
hash := sha256.Sum256(testData) |
|
|
|
|
|
|
|
hashHex := hex.EncodeToString(hash[:]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fmt.Printf("📦 Generated %d bytes of random data\n", *blobSize) |
|
|
|
|
|
|
|
fmt.Printf(" SHA256: %s\n\n", hashHex) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Step 1: Upload blob
|
|
|
|
|
|
|
|
fmt.Println("📤 Step 1: Uploading blob...") |
|
|
|
|
|
|
|
descriptor, err := uploadBlob(sec, pub, testData) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
fmt.Fprintf(os.Stderr, "❌ Upload failed: %v\n", err) |
|
|
|
|
|
|
|
os.Exit(1) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
fmt.Printf("✅ Upload successful!\n") |
|
|
|
|
|
|
|
fmt.Printf(" URL: %s\n", descriptor.URL) |
|
|
|
|
|
|
|
fmt.Printf(" SHA256: %s\n", descriptor.SHA256) |
|
|
|
|
|
|
|
fmt.Printf(" Size: %d bytes\n\n", descriptor.Size) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Step 2: Fetch blob
|
|
|
|
|
|
|
|
fmt.Println("📥 Step 2: Fetching blob...") |
|
|
|
|
|
|
|
fetchedData, err := fetchBlob(hashHex) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
fmt.Fprintf(os.Stderr, "❌ Fetch failed: %v\n", err) |
|
|
|
|
|
|
|
os.Exit(1) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
fmt.Printf("✅ Fetch successful! Retrieved %d bytes\n", len(fetchedData)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Verify data matches
|
|
|
|
|
|
|
|
if !bytes.Equal(testData, fetchedData) { |
|
|
|
|
|
|
|
fmt.Fprintf(os.Stderr, "❌ Data mismatch! Retrieved data doesn't match uploaded data\n") |
|
|
|
|
|
|
|
os.Exit(1) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
fmt.Printf("✅ Data verification passed - hashes match!\n\n") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Step 3: Delete blob
|
|
|
|
|
|
|
|
fmt.Println("🗑️ Step 3: Deleting blob...") |
|
|
|
|
|
|
|
if err := deleteBlob(sec, pub, hashHex); err != nil { |
|
|
|
|
|
|
|
fmt.Fprintf(os.Stderr, "❌ Delete failed: %v\n", err) |
|
|
|
|
|
|
|
os.Exit(1) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
fmt.Printf("✅ Delete successful!\n\n") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Step 4: Verify deletion
|
|
|
|
|
|
|
|
fmt.Println("🔍 Step 4: Verifying deletion...") |
|
|
|
|
|
|
|
if err := verifyDeleted(hashHex); err != nil { |
|
|
|
|
|
|
|
fmt.Fprintf(os.Stderr, "❌ Verification failed: %v\n", err) |
|
|
|
|
|
|
|
os.Exit(1) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
fmt.Printf("✅ Blob successfully deleted - returns 404 as expected\n\n") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fmt.Println("🎉 All tests passed! Blossom service is working correctly.") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func getKeypair() (sec, pub []byte, err error) { |
|
|
|
|
|
|
|
if *nsec != "" { |
|
|
|
|
|
|
|
// Decode provided nsec
|
|
|
|
|
|
|
|
var secKey *secp256k1.SecretKey |
|
|
|
|
|
|
|
secKey, err = bech32encoding.NsecToSecretKey(*nsec) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return nil, nil, fmt.Errorf("invalid nsec: %w", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
sec = secKey.Serialize() |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Generate new keypair
|
|
|
|
|
|
|
|
sec = make([]byte, 32) |
|
|
|
|
|
|
|
if _, err := rand.Read(sec); err != nil { |
|
|
|
|
|
|
|
return nil, nil, fmt.Errorf("failed to generate key: %w", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
fmt.Println("ℹ️ No key provided, generated new keypair") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Derive public key using p8k signer
|
|
|
|
|
|
|
|
var signer *p8k.Signer |
|
|
|
|
|
|
|
if signer, err = p8k.New(); err != nil { |
|
|
|
|
|
|
|
return nil, nil, fmt.Errorf("failed to create signer: %w", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if err = signer.InitSec(sec); err != nil { |
|
|
|
|
|
|
|
return nil, nil, fmt.Errorf("failed to initialize signer: %w", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
pub = signer.Pub() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return sec, pub, nil |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// createAuthEvent creates a Blossom authorization event (kind 24242)
|
|
|
|
|
|
|
|
func createAuthEvent(sec, pub []byte, action, hash string) (string, error) { |
|
|
|
|
|
|
|
now := time.Now().Unix() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Build tags based on action
|
|
|
|
|
|
|
|
tags := [][]string{ |
|
|
|
|
|
|
|
{"t", action}, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add x tag for DELETE and GET actions
|
|
|
|
|
|
|
|
if hash != "" && (action == "delete" || action == "get") { |
|
|
|
|
|
|
|
tags = append(tags, []string{"x", hash}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// All Blossom auth events require expiration tag (BUD-01)
|
|
|
|
|
|
|
|
expiry := now + 300 // Event expires in 5 minutes
|
|
|
|
|
|
|
|
tags = append(tags, []string{"expiration", fmt.Sprintf("%d", expiry)}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pubkeyHex := hex.EncodeToString(pub) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create event ID
|
|
|
|
|
|
|
|
eventJSON, err := json.Marshal([]interface{}{ |
|
|
|
|
|
|
|
0, |
|
|
|
|
|
|
|
pubkeyHex, |
|
|
|
|
|
|
|
now, |
|
|
|
|
|
|
|
BlossomAuthKind, |
|
|
|
|
|
|
|
tags, |
|
|
|
|
|
|
|
"", |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return "", fmt.Errorf("failed to marshal event for ID: %w", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eventHash := sha256.Sum256(eventJSON) |
|
|
|
|
|
|
|
eventID := hex.EncodeToString(eventHash[:]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Sign the event using p8k signer
|
|
|
|
|
|
|
|
signer, err := p8k.New() |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return "", fmt.Errorf("failed to create signer: %w", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if err = signer.InitSec(sec); err != nil { |
|
|
|
|
|
|
|
return "", fmt.Errorf("failed to initialize signer: %w", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sig, err := signer.Sign(eventHash[:]) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return "", fmt.Errorf("failed to sign event: %w", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
sigHex := hex.EncodeToString(sig) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create event JSON (signed)
|
|
|
|
|
|
|
|
event := map[string]interface{}{ |
|
|
|
|
|
|
|
"id": eventID, |
|
|
|
|
|
|
|
"pubkey": pubkeyHex, |
|
|
|
|
|
|
|
"created_at": now, |
|
|
|
|
|
|
|
"kind": BlossomAuthKind, |
|
|
|
|
|
|
|
"tags": tags, |
|
|
|
|
|
|
|
"content": "", |
|
|
|
|
|
|
|
"sig": sigHex, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Marshal to JSON for Authorization header
|
|
|
|
|
|
|
|
authJSON, err := json.Marshal(event) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return "", fmt.Errorf("failed to marshal auth event: %w", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if *verbose { |
|
|
|
|
|
|
|
fmt.Printf(" Auth event: %s\n", string(authJSON)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return string(authJSON), nil |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func uploadBlob(sec, pub, data []byte) (*BlossomDescriptor, error) { |
|
|
|
|
|
|
|
// Create request
|
|
|
|
|
|
|
|
url := strings.TrimSuffix(*relayURL, "/") + "/blossom/upload" |
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data)) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return nil, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Set headers
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/octet-stream") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add authorization if not disabled
|
|
|
|
|
|
|
|
if !*noAuth && sec != nil && pub != nil { |
|
|
|
|
|
|
|
authEvent, err := createAuthEvent(sec, pub, "upload", "") |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return nil, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// Base64-encode the auth event as per BUD-01
|
|
|
|
|
|
|
|
authEventB64 := base64.StdEncoding.EncodeToString([]byte(authEvent)) |
|
|
|
|
|
|
|
req.Header.Set("Authorization", "Nostr "+authEventB64) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if *verbose { |
|
|
|
|
|
|
|
fmt.Printf(" PUT %s\n", url) |
|
|
|
|
|
|
|
fmt.Printf(" Content-Length: %d\n", len(data)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Send request
|
|
|
|
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second} |
|
|
|
|
|
|
|
resp, err := client.Do(req) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return nil, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
defer resp.Body.Close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Read response
|
|
|
|
|
|
|
|
body, err := io.ReadAll(resp.Body) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return nil, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { |
|
|
|
|
|
|
|
reason := resp.Header.Get("X-Reason") |
|
|
|
|
|
|
|
if reason == "" { |
|
|
|
|
|
|
|
reason = string(body) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return nil, fmt.Errorf("server returned %d: %s", resp.StatusCode, reason) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Parse descriptor
|
|
|
|
|
|
|
|
var descriptor BlossomDescriptor |
|
|
|
|
|
|
|
if err := json.Unmarshal(body, &descriptor); err != nil { |
|
|
|
|
|
|
|
return nil, fmt.Errorf("failed to parse response: %w (body: %s)", err, string(body)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return &descriptor, nil |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func fetchBlob(hash string) ([]byte, error) { |
|
|
|
|
|
|
|
url := strings.TrimSuffix(*relayURL, "/") + "/blossom/" + hash |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if *verbose { |
|
|
|
|
|
|
|
fmt.Printf(" GET %s\n", url) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := http.Get(url) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return nil, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
defer resp.Body.Close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK { |
|
|
|
|
|
|
|
body, _ := io.ReadAll(resp.Body) |
|
|
|
|
|
|
|
return nil, fmt.Errorf("server returned %d: %s", resp.StatusCode, string(body)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return io.ReadAll(resp.Body) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func deleteBlob(sec, pub []byte, hash string) error { |
|
|
|
|
|
|
|
// Create request
|
|
|
|
|
|
|
|
url := strings.TrimSuffix(*relayURL, "/") + "/blossom/" + hash |
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodDelete, url, nil) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add authorization if not disabled
|
|
|
|
|
|
|
|
if !*noAuth && sec != nil && pub != nil { |
|
|
|
|
|
|
|
authEvent, err := createAuthEvent(sec, pub, "delete", hash) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// Base64-encode the auth event as per BUD-01
|
|
|
|
|
|
|
|
authEventB64 := base64.StdEncoding.EncodeToString([]byte(authEvent)) |
|
|
|
|
|
|
|
req.Header.Set("Authorization", "Nostr "+authEventB64) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if *verbose { |
|
|
|
|
|
|
|
fmt.Printf(" DELETE %s\n", url) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Send request
|
|
|
|
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second} |
|
|
|
|
|
|
|
resp, err := client.Do(req) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
defer resp.Body.Close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { |
|
|
|
|
|
|
|
body, _ := io.ReadAll(resp.Body) |
|
|
|
|
|
|
|
reason := resp.Header.Get("X-Reason") |
|
|
|
|
|
|
|
if reason == "" { |
|
|
|
|
|
|
|
reason = string(body) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return fmt.Errorf("server returned %d: %s", resp.StatusCode, reason) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return nil |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func verifyDeleted(hash string) error { |
|
|
|
|
|
|
|
url := strings.TrimSuffix(*relayURL, "/") + "/blossom/" + hash |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if *verbose { |
|
|
|
|
|
|
|
fmt.Printf(" GET %s (expecting 404)\n", url) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := http.Get(url) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
defer resp.Body.Close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode == http.StatusOK { |
|
|
|
|
|
|
|
return fmt.Errorf("blob still exists (expected 404, got 200)") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusNotFound { |
|
|
|
|
|
|
|
return fmt.Errorf("unexpected status code: %d (expected 404)", resp.StatusCode) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return nil |
|
|
|
|
|
|
|
} |