Eric Bower
·
11 Dec 24
handler.go
1package uploadimgs
2
3import (
4 "encoding/binary"
5 "fmt"
6 "io"
7 "net/http"
8 "os"
9 "path/filepath"
10 "time"
11
12 "slices"
13
14 "github.com/charmbracelet/ssh"
15 exifremove "github.com/neurosnap/go-exif-remove"
16 "github.com/picosh/pico/db"
17 "github.com/picosh/pico/shared"
18 "github.com/picosh/pico/shared/storage"
19 "github.com/picosh/pobj"
20 sendutils "github.com/picosh/send/utils"
21 "github.com/picosh/utils"
22)
23
24var Space = "imgs"
25
26type PostMetaData struct {
27 *db.Post
28 OrigText []byte
29 Cur *db.Post
30 Tags []string
31 User *db.User
32 *sendutils.FileEntry
33 FeatureFlag *db.FeatureFlag
34}
35
36type UploadImgHandler struct {
37 DBPool db.DB
38 Cfg *shared.ConfigSite
39 Storage storage.StorageServe
40}
41
42func NewUploadImgHandler(dbpool db.DB, cfg *shared.ConfigSite, storage storage.StorageServe) *UploadImgHandler {
43 return &UploadImgHandler{
44 DBPool: dbpool,
45 Cfg: cfg,
46 Storage: storage,
47 }
48}
49
50func (h *UploadImgHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReaderAtCloser, error) {
51 user, err := h.DBPool.FindUser(s.Permissions().Extensions["user_id"])
52 if err != nil {
53 return nil, nil, err
54 }
55
56 cleanFilename := filepath.Base(entry.Filepath)
57
58 if cleanFilename == "" || cleanFilename == "." {
59 return nil, nil, os.ErrNotExist
60 }
61
62 post, err := h.DBPool.FindPostWithFilename(cleanFilename, user.ID, Space)
63 if err != nil {
64 return nil, nil, err
65 }
66
67 fileInfo := &sendutils.VirtualFile{
68 FName: post.Filename,
69 FIsDir: false,
70 FSize: int64(post.FileSize),
71 FModTime: *post.UpdatedAt,
72 }
73
74 bucket, err := h.Storage.GetBucket(user.ID)
75 if err != nil {
76 return nil, nil, err
77 }
78
79 contents, _, err := h.Storage.GetObject(bucket, post.Filename)
80 if err != nil {
81 return nil, nil, err
82 }
83
84 reader := pobj.NewAllReaderAt(contents)
85
86 return fileInfo, reader, nil
87}
88
89func (h *UploadImgHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (string, error) {
90 logger := h.Cfg.Logger
91 user, err := h.DBPool.FindUser(s.Permissions().Extensions["user_id"])
92 if err != nil {
93 logger.Error("could not get user from ctx", "err", err.Error())
94 return "", err
95 }
96 logger = shared.LoggerWithUser(logger, user)
97
98 filename := filepath.Base(entry.Filepath)
99
100 var text []byte
101 if b, err := io.ReadAll(entry.Reader); err == nil {
102 text = b
103 }
104 mimeType := http.DetectContentType(text)
105 ext := filepath.Ext(filename)
106 if ext == ".svg" {
107 mimeType = "image/svg+xml"
108 }
109 // strip exif data
110 if slices.Contains([]string{"image/png", "image/jpg", "image/jpeg"}, mimeType) {
111 noExifBytes, err := exifremove.Remove(text)
112 if err == nil {
113 if len(noExifBytes) == 0 {
114 logger.Info("file silently failed to strip exif data", "filename", filename)
115 } else {
116 text = noExifBytes
117 logger.Info("stripped exif data", "filename", filename)
118 }
119 } else {
120 logger.Error("could not strip exif data", "err", err.Error())
121 }
122 }
123
124 now := time.Now()
125 fileSize := binary.Size(text)
126 shasum := utils.Shasum(text)
127 slug := utils.SanitizeFileExt(filename)
128
129 nextPost := db.Post{
130 Filename: filename,
131 Slug: slug,
132 PublishAt: &now,
133 Text: string(text),
134 MimeType: mimeType,
135 FileSize: fileSize,
136 Shasum: shasum,
137 }
138
139 post, err := h.DBPool.FindPostWithFilename(
140 nextPost.Filename,
141 user.ID,
142 Space,
143 )
144 if err != nil {
145 logger.Info("unable to find image, continuing", "filename", nextPost.Filename, "err", err.Error())
146 }
147
148 featureFlag := shared.FindPlusFF(h.DBPool, h.Cfg, user.ID)
149 metadata := PostMetaData{
150 OrigText: text,
151 Post: &nextPost,
152 User: user,
153 FileEntry: entry,
154 Cur: post,
155 FeatureFlag: featureFlag,
156 }
157
158 if post != nil {
159 metadata.Post.PublishAt = post.PublishAt
160 }
161
162 err = h.writeImg(s, &metadata)
163 if err != nil {
164 logger.Error("could not write img", "err", err.Error())
165 return "", err
166 }
167
168 totalFileSize, err := h.DBPool.FindTotalSizeForUser(user.ID)
169 if err != nil {
170 logger.Error("could not find total storage size for user", "err", err.Error())
171 return "", err
172 }
173
174 curl := shared.NewCreateURL(h.Cfg)
175 url := h.Cfg.FullPostURL(
176 curl,
177 user.Name,
178 metadata.Filename,
179 )
180 maxSize := int(featureFlag.Data.StorageMax)
181 str := fmt.Sprintf(
182 "%s (space: %.2f/%.2fGB, %.2f%%)",
183 url,
184 utils.BytesToGB(totalFileSize),
185 utils.BytesToGB(maxSize),
186 (float32(totalFileSize)/float32(maxSize))*100,
187 )
188 return str, nil
189}
190
191func (h *UploadImgHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) error {
192 user, err := h.DBPool.FindUser(s.Permissions().Extensions["user_id"])
193 if err != nil {
194 return err
195 }
196
197 filename := filepath.Base(entry.Filepath)
198
199 logger := h.Cfg.Logger
200 logger = shared.LoggerWithUser(logger, user)
201 logger = logger.With(
202 "filename", filename,
203 )
204
205 post, err := h.DBPool.FindPostWithFilename(
206 filename,
207 user.ID,
208 Space,
209 )
210 if err != nil {
211 logger.Info("unable to find image, continuing", "err", err.Error())
212 return err
213 }
214
215 err = h.DBPool.RemovePosts([]string{post.ID})
216 if err != nil {
217 logger.Error("error removing image", "error", err)
218 return fmt.Errorf("error for %s: %v", filename, err)
219 }
220
221 bucket, err := h.Storage.UpsertBucket(user.ID)
222 if err != nil {
223 return err
224 }
225
226 err = h.Storage.DeleteObject(bucket, filename)
227 if err != nil {
228 return err
229 }
230
231 logger.Info("deleting image")
232
233 return nil
234}