A vibe coded tangled fork which supports pijul.
at fd9baa09b253b1f0d614f95665f63c3f81714c5f 139 lines 3.8 kB view raw
1package cloudflare 2 3import ( 4 "bytes" 5 "context" 6 "fmt" 7 "mime" 8 "net/http" 9 "path/filepath" 10 "strings" 11 12 "github.com/aws/aws-sdk-go-v2/aws" 13 "github.com/aws/aws-sdk-go-v2/service/s3" 14 s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" 15) 16 17// SyncFiles uploads the given files (keyed by relative path) to R2 under 18// prefix and deletes any objects from a previous deploy that are no longer 19// present. It is a pure R2 operation — callers are responsible for 20// constructing the prefix and fetching the source files before calling this. 21func (cl *Client) SyncFiles(ctx context.Context, prefix string, files map[string][]byte) error { 22 existingKeys, err := cl.listR2Objects(ctx, prefix) 23 if err != nil { 24 return fmt.Errorf("listing existing R2 objects: %w", err) 25 } 26 27 for relPath, content := range files { 28 key := prefix + relPath 29 _, err := cl.s3.PutObject(ctx, &s3.PutObjectInput{ 30 Bucket: aws.String(cl.bucket), 31 Key: aws.String(key), 32 Body: bytes.NewReader(content), 33 ContentType: aws.String(DetectContentType(relPath, content)), 34 }) 35 if err != nil { 36 return fmt.Errorf("uploading %q: %w", key, err) 37 } 38 } 39 40 for existingKey := range existingKeys { 41 relPath := strings.TrimPrefix(existingKey, prefix) 42 if _, kept := files[relPath]; !kept { 43 if err := cl.deleteR2Object(ctx, existingKey); err != nil { 44 return fmt.Errorf("deleting orphan %q: %w", existingKey, err) 45 } 46 } 47 } 48 49 return nil 50} 51 52// DeleteFiles removes all R2 objects under the given prefix. 53func (cl *Client) DeleteFiles(ctx context.Context, prefix string) error { 54 keys, err := cl.listR2Objects(ctx, prefix) 55 if err != nil { 56 return fmt.Errorf("listing R2 objects for deletion: %w", err) 57 } 58 for key := range keys { 59 if err := cl.deleteR2Object(ctx, key); err != nil { 60 return fmt.Errorf("deleting %q: %w", key, err) 61 } 62 } 63 return nil 64} 65 66// listR2Objects returns all object keys in the bucket under the given prefix, 67// handling pagination automatically. 68func (cl *Client) listR2Objects(ctx context.Context, prefix string) (map[string]struct{}, error) { 69 keys := make(map[string]struct{}) 70 var continuationToken *string 71 72 for { 73 out, err := cl.s3.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ 74 Bucket: aws.String(cl.bucket), 75 Prefix: aws.String(prefix), 76 ContinuationToken: continuationToken, 77 }) 78 if err != nil { 79 return nil, err 80 } 81 82 for _, obj := range out.Contents { 83 if obj.Key != nil { 84 keys[*obj.Key] = struct{}{} 85 } 86 } 87 88 if !aws.ToBool(out.IsTruncated) { 89 break 90 } 91 continuationToken = out.NextContinuationToken 92 } 93 94 return keys, nil 95} 96 97func (cl *Client) deleteR2Object(ctx context.Context, key string) error { 98 _, err := cl.s3.DeleteObject(ctx, &s3.DeleteObjectInput{ 99 Bucket: aws.String(cl.bucket), 100 Key: aws.String(key), 101 }) 102 return err 103} 104 105// deleteBatch deletes up to 1000 objects in a single call. 106// Unused for now, kept for future bulk-delete optimisation. 107func (cl *Client) deleteBatch(ctx context.Context, keys []string) error { 108 if len(keys) == 0 { 109 return nil 110 } 111 112 var objects []s3types.ObjectIdentifier 113 for _, k := range keys { 114 k := k 115 objects = append(objects, s3types.ObjectIdentifier{Key: &k}) 116 } 117 118 _, err := cl.s3.DeleteObjects(ctx, &s3.DeleteObjectsInput{ 119 Bucket: aws.String(cl.bucket), 120 Delete: &s3types.Delete{Objects: objects}, 121 }) 122 return err 123} 124 125// DetectContentType guesses the MIME type from the file extension, falling 126// back to sniffing the first 512 bytes of content. 127func DetectContentType(relPath string, content []byte) string { 128 if ext := filepath.Ext(relPath); ext != "" { 129 if mt := mime.TypeByExtension(ext); mt != "" { 130 return mt 131 } 132 } 133 134 sniff := content 135 if len(sniff) > 512 { 136 sniff = sniff[:512] 137 } 138 return http.DetectContentType(sniff) 139}