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 · 20 Sep 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/filehandlers/util"
 18	"github.com/picosh/pico/shared"
 19	"github.com/picosh/pico/shared/storage"
 20	"github.com/picosh/pobj"
 21	"github.com/picosh/send/send/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	*utils.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 *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
 51	user, err := util.GetUser(s.Context())
 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 := &utils.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 *utils.FileEntry) (string, error) {
 90	logger := h.Cfg.Logger
 91	user, err := util.GetUser(s.Context())
 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 := shared.Shasum(text)
127	slug := shared.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, err := util.GetFeatureFlag(s.Context())
149	if err != nil {
150		return "", err
151	}
152	metadata := PostMetaData{
153		OrigText:    text,
154		Post:        &nextPost,
155		User:        user,
156		FileEntry:   entry,
157		Cur:         post,
158		FeatureFlag: featureFlag,
159	}
160
161	if post != nil {
162		metadata.Post.PublishAt = post.PublishAt
163	}
164
165	err = h.writeImg(s, &metadata)
166	if err != nil {
167		logger.Error("could not write img", "err", err.Error())
168		return "", err
169	}
170
171	totalFileSize, err := h.DBPool.FindTotalSizeForUser(user.ID)
172	if err != nil {
173		logger.Error("could not find total storage size for user", "err", err.Error())
174		return "", err
175	}
176
177	curl := shared.NewCreateURL(h.Cfg)
178	url := h.Cfg.FullPostURL(
179		curl,
180		user.Name,
181		metadata.Filename,
182	)
183	maxSize := int(featureFlag.Data.StorageMax)
184	str := fmt.Sprintf(
185		"%s (space: %.2f/%.2fGB, %.2f%%)",
186		url,
187		shared.BytesToGB(totalFileSize),
188		shared.BytesToGB(maxSize),
189		(float32(totalFileSize)/float32(maxSize))*100,
190	)
191	return str, nil
192}
193
194func (h *UploadImgHandler) Delete(s ssh.Session, entry *utils.FileEntry) error {
195	user, err := util.GetUser(s.Context())
196	if err != nil {
197		return err
198	}
199
200	filename := filepath.Base(entry.Filepath)
201
202	logger := h.Cfg.Logger
203	logger = shared.LoggerWithUser(logger, user)
204	logger = logger.With(
205		"filename", filename,
206	)
207
208	post, err := h.DBPool.FindPostWithFilename(
209		filename,
210		user.ID,
211		Space,
212	)
213	if err != nil {
214		logger.Info("unable to find image, continuing", "err", err.Error())
215		return err
216	}
217
218	err = h.DBPool.RemovePosts([]string{post.ID})
219	if err != nil {
220		logger.Error("error removing image", "error", err)
221		return fmt.Errorf("error for %s: %v", filename, err)
222	}
223
224	bucket, err := h.Storage.UpsertBucket(user.ID)
225	if err != nil {
226		return err
227	}
228
229	err = h.Storage.DeleteObject(bucket, filename)
230	if err != nil {
231		return err
232	}
233
234	logger.Info("deleting image")
235
236	return nil
237}