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