repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

pico / pkg / filehandlers / imgs
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}