Eric Bower
·
2026-01-25
handler.go
1package uploadimgs
2
3import (
4 "bytes"
5 "encoding/binary"
6 "fmt"
7 "io"
8 "net/http"
9 "os"
10 "path/filepath"
11 "slices"
12 "strings"
13
14 exifremove "github.com/neurosnap/go-exif-remove"
15 "github.com/picosh/pico/pkg/db"
16 "github.com/picosh/pico/pkg/pobj"
17 sst "github.com/picosh/pico/pkg/pobj/storage"
18 "github.com/picosh/pico/pkg/pssh"
19 sendutils "github.com/picosh/pico/pkg/send/utils"
20 "github.com/picosh/pico/pkg/shared"
21 "github.com/picosh/pico/pkg/shared/storage"
22)
23
24var Space = "imgs"
25
26type PostMetaData struct {
27 Text []byte
28 FileSize int
29 TotalFileSize int
30 Filename string
31 User *db.User
32 FeatureFlag *db.FeatureFlag
33 Bucket sst.Bucket
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) getObjectPath(fpath string) string {
51 return filepath.Join("prose", fpath)
52}
53
54func (h *UploadImgHandler) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
55 var fileList []os.FileInfo
56
57 logger := pssh.GetLogger(s)
58 user := pssh.GetUser(s)
59
60 if user == nil {
61 err := fmt.Errorf("could not get user from ctx")
62 logger.Error("error getting user from ctx", "err", err)
63 return fileList, err
64 }
65
66 cleanFilename := fpath
67
68 bucketName := shared.GetAssetBucketName(user.ID)
69 bucket, err := h.Storage.GetBucket(bucketName)
70 if err != nil {
71 return fileList, err
72 }
73
74 if cleanFilename == "" || cleanFilename == "." {
75 name := cleanFilename
76 if name == "" {
77 name = "/"
78 }
79
80 info := &sendutils.VirtualFile{
81 FName: name,
82 FIsDir: true,
83 }
84
85 fileList = append(fileList, info)
86 } else {
87 fp := h.getObjectPath(cleanFilename)
88 if fp != "/" && isDir {
89 fp += "/"
90 }
91
92 foundList, err := h.Storage.ListObjects(bucket, fp, recursive)
93 if err != nil {
94 return fileList, err
95 }
96
97 fileList = append(fileList, foundList...)
98 }
99
100 return fileList, nil
101}
102
103func (h *UploadImgHandler) Read(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
104 logger := pssh.GetLogger(s)
105 user := pssh.GetUser(s)
106
107 if user == nil {
108 err := fmt.Errorf("could not get user from ctx")
109 logger.Error("error getting user from ctx", "err", err)
110 return nil, nil, err
111 }
112
113 cleanFilename := filepath.Base(entry.Filepath)
114
115 if cleanFilename == "" || cleanFilename == "." {
116 return nil, nil, os.ErrNotExist
117 }
118
119 bucket, err := h.Storage.GetBucket(shared.GetAssetBucketName(user.ID))
120 if err != nil {
121 return nil, nil, err
122 }
123
124 contents, info, err := h.Storage.GetObject(bucket, h.getObjectPath(cleanFilename))
125 if err != nil {
126 return nil, nil, err
127 }
128 reader := pobj.NewAllReaderAt(contents)
129
130 fileInfo := &sendutils.VirtualFile{
131 FName: cleanFilename,
132 FIsDir: false,
133 FSize: info.Size,
134 FModTime: info.LastModified,
135 }
136
137 return fileInfo, reader, nil
138}
139
140func (h *UploadImgHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (string, error) {
141 logger := pssh.GetLogger(s)
142 user := pssh.GetUser(s)
143
144 if user == nil {
145 err := fmt.Errorf("could not get user from ctx")
146 logger.Error("error getting user from ctx", "err", err)
147 return "", err
148 }
149
150 filename := filepath.Base(entry.Filepath)
151
152 var text []byte
153 if b, err := io.ReadAll(entry.Reader); err == nil {
154 text = b
155 }
156 mimeType := http.DetectContentType(text)
157 ext := filepath.Ext(filename)
158 if ext == ".svg" {
159 mimeType = "image/svg+xml"
160 }
161 // strip exif data
162 if slices.Contains([]string{"image/png", "image/jpg", "image/jpeg"}, mimeType) {
163 noExifBytes, err := exifremove.Remove(text)
164 if err == nil {
165 if len(noExifBytes) == 0 {
166 logger.Info("file silently failed to strip exif data", "filename", filename)
167 } else {
168 text = noExifBytes
169 logger.Info("stripped exif data", "filename", filename)
170 }
171 } else {
172 logger.Error("could not strip exif data", "err", err.Error())
173 }
174 }
175
176 fileSize := binary.Size(text)
177 featureFlag := shared.FindPlusFF(h.DBPool, h.Cfg, user.ID)
178
179 bucket, err := h.Storage.UpsertBucket(shared.GetAssetBucketName(user.ID))
180 if err != nil {
181 return "", err
182 }
183
184 totalFileSize, err := h.Storage.GetBucketQuota(bucket)
185 if err != nil {
186 logger.Error("bucket quota", "err", err)
187 return "", err
188 }
189
190 metadata := PostMetaData{
191 Filename: filename,
192 FileSize: fileSize,
193 Text: text,
194 User: user,
195 FeatureFlag: featureFlag,
196 Bucket: bucket,
197 TotalFileSize: int(totalFileSize),
198 }
199
200 err = h.writeImg(s, &metadata)
201 if err != nil {
202 logger.Error("could not write img", "err", err.Error())
203 return "", err
204 }
205
206 curl := shared.NewCreateURL(h.Cfg)
207 url := h.Cfg.FullPostURL(
208 curl,
209 user.Name,
210 metadata.Filename,
211 )
212 maxSize := int(featureFlag.Data.StorageMax)
213 str := fmt.Sprintf(
214 "%s (space: %.2f/%.2fGB, %.2f%%)",
215 url,
216 shared.BytesToGB(metadata.TotalFileSize+fileSize),
217 shared.BytesToGB(maxSize),
218 (float32(totalFileSize)/float32(maxSize))*100,
219 )
220 return str, nil
221}
222
223func (h *UploadImgHandler) Delete(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
224 logger := pssh.GetLogger(s)
225 user := pssh.GetUser(s)
226
227 if user == nil {
228 err := fmt.Errorf("could not get user from ctx")
229 logger.Error("error getting user from ctx", "err", err)
230 return err
231 }
232
233 filename := filepath.Base(entry.Filepath)
234
235 logger = logger.With(
236 "filename", filename,
237 )
238
239 bucket, err := h.Storage.UpsertBucket(shared.GetAssetBucketName(user.ID))
240 if err != nil {
241 return err
242 }
243
244 logger.Info("deleting image")
245 err = h.Storage.DeleteObject(bucket, h.getObjectPath(filename))
246 if err != nil {
247 return err
248 }
249
250 return nil
251}
252
253func (h *UploadImgHandler) validateImg(data *PostMetaData) (bool, error) {
254 fileMax := data.FeatureFlag.Data.FileMax
255 if int64(data.FileSize) > fileMax {
256 return false, fmt.Errorf("ERROR: file (%s) has exceeded maximum file size (%d bytes)", data.Filename, fileMax)
257 }
258
259 storageMax := data.FeatureFlag.Data.StorageMax
260 if uint64(data.TotalFileSize+data.FileSize) > storageMax {
261 return false, fmt.Errorf("ERROR: user (%s) has exceeded (%d bytes) max (%d bytes)", data.User.Name, data.TotalFileSize, storageMax)
262 }
263
264 if !shared.IsExtAllowed(data.Filename, h.Cfg.AllowedExt) {
265 extStr := strings.Join(h.Cfg.AllowedExt, ",")
266 err := fmt.Errorf(
267 "ERROR: (%s) invalid file, format must be (%s), skipping",
268 data.Filename,
269 extStr,
270 )
271 return false, err
272 }
273
274 return true, nil
275}
276
277func (h *UploadImgHandler) metaImg(data *PostMetaData) error {
278 // if the file is empty that means we should delete it
279 // so we can skip all the meta info
280 if data.FileSize == 0 {
281 return nil
282 }
283
284 // make sure we have a bucket
285 bucket, err := h.Storage.UpsertBucket(shared.GetAssetBucketName(data.User.ID))
286 if err != nil {
287 return err
288 }
289
290 reader := bytes.NewReader([]byte(data.Text))
291 _, _, err = h.Storage.PutObject(
292 bucket,
293 h.getObjectPath(data.Filename),
294 sendutils.NopReadAndReaderAtCloser(reader),
295 &sendutils.FileEntry{},
296 )
297 if err != nil {
298 return err
299 }
300
301 return nil
302}
303
304func (h *UploadImgHandler) writeImg(s *pssh.SSHServerConnSession, data *PostMetaData) error {
305 valid, err := h.validateImg(data)
306 if !valid {
307 return err
308 }
309
310 logger := pssh.GetLogger(s)
311 logger = logger.With(
312 "filename", data.Filename,
313 )
314
315 logger.Info("uploading image")
316 err = h.metaImg(data)
317 if err != nil {
318 logger.Error("meta img", "err", err)
319 return err
320 }
321
322 return nil
323}