repos / pico

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

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