apps/blobs/tui_commands.go 3.9 K raw
1
package main
2
3
import (
4
	"context"
5
	"fmt"
6
	"io"
7
	"mime"
8
	"os"
9
	"path/filepath"
10
	"strings"
11
	"time"
12
13
	tea "charm.land/bubbletea/v2"
14
15
	"github.com/stevedylandev/andromeda/apps/blobs/preview"
16
)
17
18
func loadBucketsCmd(s3 *S3Client) tea.Cmd {
19
	return func() tea.Msg {
20
		ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
21
		defer cancel()
22
		bs, err := s3.ListBuckets(ctx)
23
		return bucketsLoadedMsg{Buckets: bs, Err: err}
24
	}
25
}
26
27
func loadListingCmd(s3 *S3Client, bucket, prefix string) tea.Cmd {
28
	return func() tea.Msg {
29
		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
30
		defer cancel()
31
		folders, files, err := s3.List(ctx, bucket, prefix)
32
		return listingLoadedMsg{Bucket: bucket, Prefix: prefix, Folders: folders, Files: files, Err: err}
33
	}
34
}
35
36
const previewMaxBytes = 5 << 20
37
38
func loadPreviewCmd(s3 *S3Client, proto preview.Protocol, seq int, bucket, key string, w, h int) tea.Cmd {
39
	return func() tea.Msg {
40
		if !isImageName(key) {
41
			return previewLoadedMsg{Seq: seq, Bucket: bucket, Key: key, Content: previewMetaText(key, 0, "")}
42
		}
43
		if proto == preview.ProtoNone {
44
			return previewLoadedMsg{Seq: seq, Bucket: bucket, Key: key, Content: previewMetaText(key, 0, "no preview backend (install chafa)")}
45
		}
46
		ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
47
		defer cancel()
48
		body, meta, err := s3.Get(ctx, bucket, key)
49
		if err != nil {
50
			return previewLoadedMsg{Seq: seq, Bucket: bucket, Key: key, Err: err}
51
		}
52
		defer body.Close()
53
		if meta != nil && meta.Size > previewMaxBytes {
54
			return previewLoadedMsg{Seq: seq, Bucket: bucket, Key: key, Content: previewMetaText(key, meta.Size, "too large to preview")}
55
		}
56
		buf, err := io.ReadAll(io.LimitReader(body, previewMaxBytes+1))
57
		if err != nil {
58
			return previewLoadedMsg{Seq: seq, Bucket: bucket, Key: key, Err: err}
59
		}
60
		if len(buf) > previewMaxBytes {
61
			return previewLoadedMsg{Seq: seq, Bucket: bucket, Key: key, Content: previewMetaText(key, int64(len(buf)), "too large to preview")}
62
		}
63
		rendered, rerr := preview.Render(proto, buf, w, h)
64
		if rerr != nil {
65
			return previewLoadedMsg{Seq: seq, Bucket: bucket, Key: key, Content: previewMetaText(key, int64(len(buf)), "render: "+rerr.Error())}
66
		}
67
		return previewLoadedMsg{Seq: seq, Bucket: bucket, Key: key, Content: rendered}
68
	}
69
}
70
71
const previewDebounce = 150 * time.Millisecond
72
73
func debouncePreviewCmd(seq int, bucket, key string, w, h int) tea.Cmd {
74
	return tea.Tick(previewDebounce, func(time.Time) tea.Msg {
75
		return previewDebounceMsg{Seq: seq, Bucket: bucket, Key: key, W: w, H: h}
76
	})
77
}
78
79
func previewMetaText(key string, size int64, note string) string {
80
	ct := mime.TypeByExtension(filepath.Ext(key))
81
	if ct == "" {
82
		ct = "application/octet-stream"
83
	}
84
	out := fmt.Sprintf("key:  %s\ntype: %s", key, ct)
85
	if size > 0 {
86
		out += fmt.Sprintf("\nsize: %s", humanSize(size))
87
	}
88
	if note != "" {
89
		out += "\n\n" + note
90
	}
91
	return out
92
}
93
94
func deleteCmd(s3 *S3Client, bucket, key string) tea.Cmd {
95
	return func() tea.Msg {
96
		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
97
		defer cancel()
98
		err := s3.Delete(ctx, bucket, key)
99
		return deletedMsg{Key: key, Err: err}
100
	}
101
}
102
103
func uploadCmd(s3 *S3Client, cfg ClientConfig, bucket, prefix, localPath string) tea.Cmd {
104
	return func() tea.Msg {
105
		f, err := os.Open(localPath)
106
		if err != nil {
107
			return uploadedMsg{Err: err}
108
		}
109
		defer f.Close()
110
		info, err := f.Stat()
111
		if err != nil {
112
			return uploadedMsg{Err: err}
113
		}
114
		name := filepath.Base(localPath)
115
		pfx := prefix
116
		if pfx != "" && !strings.HasSuffix(pfx, "/") {
117
			pfx += "/"
118
		}
119
		key := pfx + name
120
		ct := mime.TypeByExtension(filepath.Ext(name))
121
		if ct == "" {
122
			ct = "application/octet-stream"
123
		}
124
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
125
		defer cancel()
126
		if err := s3.Put(ctx, bucket, key, ct, f, info.Size()); err != nil {
127
			return uploadedMsg{Key: key, Err: err}
128
		}
129
		url, _ := ResolveURL(ctx, s3, bucket, key)
130
		return uploadedMsg{Key: key, URL: url}
131
	}
132
}