Antonio Mika
·
08 Oct 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 := shared.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 := &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 := shared.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 := 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, err := shared.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 utils.BytesToGB(totalFileSize),
188 utils.BytesToGB(maxSize),
189 (float32(totalFileSize)/float32(maxSize))*100,
190 )
191 return str, nil
192}
193
194func (h *UploadImgHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) error {
195 user, err := shared.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}