repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

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