repos / pico

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

commit
310e14b
parent
4105299
author
Eric Bower
date
2025-01-19 13:03:19 -0500 EST
refactor(prose): remove concept of posts for images
11 files changed,  +233, -284
M cmd/scripts/prose-imgs-migrate/main.go
+4, -1
 1@@ -64,7 +64,7 @@ func images(logger *slog.Logger, dbh db.DB, st storage.StorageServe, bucket sst.
 2 		rdr, _, err := st.GetObject(imgBucket, posts.Filename)
 3 		if err != nil {
 4 			logger.Error("get object", "err", err)
 5-			return err
 6+			continue
 7 		}
 8 		err = upload(logger, st, bucket, posts.Filename, rdr)
 9 		if err != nil {
10@@ -86,6 +86,9 @@ func main() {
11 	bail(err)
12 
13 	for _, user := range users {
14+		if user.Name != "erock" {
15+			continue
16+		}
17 		logger.Info("migrating user images", "user", user.Name)
18 
19 		bucket, err := st.UpsertBucket(shared.GetAssetBucketName(user.ID))
M db/db.go
+1, -0
1@@ -403,6 +403,7 @@ type DB interface {
2 	InsertFeedItems(postID string, items []*FeedItem) error
3 	FindFeedItemsByPostID(postID string) ([]*FeedItem, error)
4 
5+	UpsertProject(userID, name, projectDir string) (*Project, error)
6 	InsertProject(userID, name, projectDir string) (string, error)
7 	UpdateProject(userID, name string) error
8 	UpdateProjectAcl(userID, name string, acl ProjectAcl) error
M db/postgres/storage.go
+20, -0
 1@@ -1990,3 +1990,23 @@ func (me *PsqlDB) AddPicoPlusUser(username, email, paymentType, txId string) err
 2 
 3 	return tx.Commit()
 4 }
 5+
 6+func (me *PsqlDB) UpsertProject(userID, projectName, projectDir string) (*db.Project, error) {
 7+	project, err := me.FindProjectByName(userID, projectName)
 8+	if err == nil {
 9+		// this just updates the `createdAt` timestamp, useful for book-keeping
10+		err = me.UpdateProject(userID, projectName)
11+		if err != nil {
12+			me.Logger.Error("could not update project", "err", err)
13+			return nil, err
14+		}
15+		return project, nil
16+	}
17+
18+	_, err = me.InsertProject(userID, projectName, projectName)
19+	if err != nil {
20+		me.Logger.Error("could not create project", "err", err)
21+		return nil, err
22+	}
23+	return me.FindProjectByName(userID, projectName)
24+}
M db/stub/stub.go
+4, -0
 1@@ -213,6 +213,10 @@ func (me *StubDB) FindFeedItemsByPostID(postID string) ([]*db.FeedItem, error) {
 2 	return []*db.FeedItem{}, notImpl
 3 }
 4 
 5+func (me *StubDB) UpsertProject(userID, name, projectDir string) (*db.Project, error) {
 6+	return nil, notImpl
 7+}
 8+
 9 func (me *StubDB) InsertProject(userID, name, projectDir string) (string, error) {
10 	return "", notImpl
11 }
M filehandlers/imgs/handler.go
+154, -71
  1@@ -1,15 +1,15 @@
  2 package uploadimgs
  3 
  4 import (
  5+	"bytes"
  6 	"encoding/binary"
  7 	"fmt"
  8 	"io"
  9 	"net/http"
 10 	"os"
 11 	"path/filepath"
 12-	"time"
 13-
 14 	"slices"
 15+	"strings"
 16 
 17 	"github.com/charmbracelet/ssh"
 18 	exifremove "github.com/neurosnap/go-exif-remove"
 19@@ -17,6 +17,7 @@ import (
 20 	"github.com/picosh/pico/shared"
 21 	"github.com/picosh/pico/shared/storage"
 22 	"github.com/picosh/pobj"
 23+	sst "github.com/picosh/pobj/storage"
 24 	sendutils "github.com/picosh/send/utils"
 25 	"github.com/picosh/utils"
 26 )
 27@@ -24,13 +25,13 @@ import (
 28 var Space = "imgs"
 29 
 30 type PostMetaData struct {
 31-	*db.Post
 32-	OrigText []byte
 33-	Cur      *db.Post
 34-	Tags     []string
 35-	User     *db.User
 36-	*sendutils.FileEntry
 37-	FeatureFlag *db.FeatureFlag
 38+	Text          []byte
 39+	FileSize      int
 40+	TotalFileSize int
 41+	Filename      string
 42+	User          *db.User
 43+	FeatureFlag   *db.FeatureFlag
 44+	Bucket        sst.Bucket
 45 }
 46 
 47 type UploadImgHandler struct {
 48@@ -51,28 +52,61 @@ func (h *UploadImgHandler) getObjectPath(fpath string) string {
 49 	return filepath.Join("prose", fpath)
 50 }
 51 
 52-func (h *UploadImgHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReaderAtCloser, error) {
 53+func (h *UploadImgHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
 54+	var fileList []os.FileInfo
 55+
 56 	user, err := h.DBPool.FindUser(s.Permissions().Extensions["user_id"])
 57 	if err != nil {
 58-		return nil, nil, err
 59+		return fileList, err
 60 	}
 61 
 62-	cleanFilename := filepath.Base(entry.Filepath)
 63+	cleanFilename := fpath
 64+
 65+	bucketName := shared.GetAssetBucketName(user.ID)
 66+	bucket, err := h.Storage.GetBucket(bucketName)
 67+	if err != nil {
 68+		return fileList, err
 69+	}
 70 
 71 	if cleanFilename == "" || cleanFilename == "." {
 72-		return nil, nil, os.ErrNotExist
 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-	post, err := h.DBPool.FindPostWithFilename(cleanFilename, user.ID, Space)
 99+	return fileList, nil
100+}
101+
102+func (h *UploadImgHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReaderAtCloser, error) {
103+	user, err := h.DBPool.FindUser(s.Permissions().Extensions["user_id"])
104 	if err != nil {
105 		return nil, nil, err
106 	}
107 
108-	fileInfo := &sendutils.VirtualFile{
109-		FName:    post.Filename,
110-		FIsDir:   false,
111-		FSize:    int64(post.FileSize),
112-		FModTime: *post.UpdatedAt,
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@@ -80,13 +114,19 @@ func (h *UploadImgHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.F
121 		return nil, nil, err
122 	}
123 
124-	contents, _, err := h.Storage.GetObject(bucket, h.getObjectPath(post.Filename))
125+	contents, info, err := h.Storage.GetObject(bucket, h.getObjectPath(cleanFilename))
126 	if err != nil {
127 		return nil, nil, err
128 	}
129-
130 	reader := pobj.NewAllReaderAt(contents)
131 
132+	fileInfo := &sendutils.VirtualFile{
133+		FName:    cleanFilename,
134+		FIsDir:   false,
135+		FSize:    info.Size,
136+		FModTime: info.LastModified,
137+	}
138+
139 	return fileInfo, reader, nil
140 }
141 
142@@ -125,53 +165,33 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (str
143 		}
144 	}
145 
146-	now := time.Now()
147 	fileSize := binary.Size(text)
148-	shasum := utils.Shasum(text)
149-	slug := utils.SanitizeFileExt(filename)
150-
151-	nextPost := db.Post{
152-		Filename:  filename,
153-		Slug:      slug,
154-		PublishAt: &now,
155-		Text:      string(text),
156-		MimeType:  mimeType,
157-		FileSize:  fileSize,
158-		Shasum:    shasum,
159-	}
160-
161-	post, err := h.DBPool.FindPostWithFilename(
162-		nextPost.Filename,
163-		user.ID,
164-		Space,
165-	)
166-	if err != nil {
167-		logger.Info("unable to find image, continuing", "filename", nextPost.Filename, "err", err.Error())
168-	}
169-
170 	featureFlag := shared.FindPlusFF(h.DBPool, h.Cfg, user.ID)
171-	metadata := PostMetaData{
172-		OrigText:    text,
173-		Post:        &nextPost,
174-		User:        user,
175-		FileEntry:   entry,
176-		Cur:         post,
177-		FeatureFlag: featureFlag,
178-	}
179 
180-	if post != nil {
181-		metadata.Post.PublishAt = post.PublishAt
182+	bucket, err := h.Storage.UpsertBucket(shared.GetAssetBucketName(user.ID))
183+	if err != nil {
184+		return "", err
185 	}
186 
187-	err = h.writeImg(s, &metadata)
188+	totalFileSize, err := h.Storage.GetBucketQuota(bucket)
189 	if err != nil {
190-		logger.Error("could not write img", "err", err.Error())
191+		logger.Error("bucket quota", "err", err)
192 		return "", err
193 	}
194 
195-	totalFileSize, err := h.DBPool.FindTotalSizeForUser(user.ID)
196+	metadata := PostMetaData{
197+		Filename:      filename,
198+		FileSize:      fileSize,
199+		Text:          text,
200+		User:          user,
201+		FeatureFlag:   featureFlag,
202+		Bucket:        bucket,
203+		TotalFileSize: int(totalFileSize),
204+	}
205+
206+	err = h.writeImg(&metadata)
207 	if err != nil {
208-		logger.Error("could not find total storage size for user", "err", err.Error())
209+		logger.Error("could not write img", "err", err.Error())
210 		return "", err
211 	}
212 
213@@ -185,7 +205,7 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (str
214 	str := fmt.Sprintf(
215 		"%s (space: %.2f/%.2fGB, %.2f%%)",
216 		url,
217-		utils.BytesToGB(totalFileSize),
218+		utils.BytesToGB(metadata.TotalFileSize+fileSize),
219 		utils.BytesToGB(maxSize),
220 		(float32(totalFileSize)/float32(maxSize))*100,
221 	)
222@@ -206,30 +226,93 @@ func (h *UploadImgHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) err
223 		"filename", filename,
224 	)
225 
226-	post, err := h.DBPool.FindPostWithFilename(
227-		filename,
228-		user.ID,
229-		Space,
230-	)
231+	bucket, err := h.Storage.UpsertBucket(shared.GetAssetBucketName(user.ID))
232 	if err != nil {
233-		logger.Info("unable to find image, continuing", "err", err.Error())
234 		return err
235 	}
236 
237-	err = h.DBPool.RemovePosts([]string{post.ID})
238+	logger.Info("deleting image")
239+	err = h.Storage.DeleteObject(bucket, h.getObjectPath(filename))
240 	if err != nil {
241-		logger.Error("error removing image", "error", err)
242-		return fmt.Errorf("error for %s: %v", filename, err)
243+		return err
244 	}
245 
246-	bucket, err := h.Storage.UpsertBucket(shared.GetAssetBucketName(user.ID))
247+	return nil
248+}
249+
250+func (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 !utils.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+
274+func (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-	logger.Info("deleting image")
288-	err = h.Storage.DeleteObject(bucket, h.getObjectPath(filename))
289+	// make sure we have a prose project to upload to
290+	_, err = h.DBPool.UpsertProject(data.User.ID, "prose", "prose")
291+	if err != nil {
292+		return err
293+	}
294+
295+	reader := bytes.NewReader([]byte(data.Text))
296+	_, _, err = h.Storage.PutObject(
297+		bucket,
298+		h.getObjectPath(data.Filename),
299+		sendutils.NopReaderAtCloser(reader),
300+		&sendutils.FileEntry{},
301+	)
302+	if err != nil {
303+		return err
304+	}
305+
306+	return nil
307+}
308+
309+func (h *UploadImgHandler) writeImg(data *PostMetaData) error {
310+	valid, err := h.validateImg(data)
311+	if !valid {
312+		return err
313+	}
314+
315+	logger := h.Cfg.Logger
316+	logger = shared.LoggerWithUser(logger, data.User)
317+	logger = logger.With(
318+		"filename", data.Filename,
319+	)
320+
321+	logger.Info("uploading image")
322+	err = h.metaImg(data)
323 	if err != nil {
324+		logger.Error("meta img", "err", err)
325 		return err
326 	}
327 
D filehandlers/imgs/img.go
+0, -161
  1@@ -1,161 +0,0 @@
  2-package uploadimgs
  3-
  4-import (
  5-	"bytes"
  6-	"fmt"
  7-	"strings"
  8-	"time"
  9-
 10-	"github.com/charmbracelet/ssh"
 11-	"github.com/picosh/pico/db"
 12-	"github.com/picosh/pico/shared"
 13-	sendutils "github.com/picosh/send/utils"
 14-	"github.com/picosh/utils"
 15-)
 16-
 17-func (h *UploadImgHandler) validateImg(data *PostMetaData) (bool, error) {
 18-	totalFileSize, err := h.DBPool.FindTotalSizeForUser(data.User.ID)
 19-	if err != nil {
 20-		return false, err
 21-	}
 22-
 23-	fileMax := data.FeatureFlag.Data.FileMax
 24-	if int64(data.FileSize) > fileMax {
 25-		return false, fmt.Errorf("ERROR: file (%s) has exceeded maximum file size (%d bytes)", data.Filename, fileMax)
 26-	}
 27-
 28-	storageMax := data.FeatureFlag.Data.StorageMax
 29-	if uint64(totalFileSize+data.FileSize) > storageMax {
 30-		return false, fmt.Errorf("ERROR: user (%s) has exceeded (%d bytes) max (%d bytes)", data.User.Name, totalFileSize, storageMax)
 31-	}
 32-
 33-	if !utils.IsExtAllowed(data.Filepath, h.Cfg.AllowedExt) {
 34-		extStr := strings.Join(h.Cfg.AllowedExt, ",")
 35-		err := fmt.Errorf(
 36-			"ERROR: (%s) invalid file, format must be (%s), skipping",
 37-			data.Filename,
 38-			extStr,
 39-		)
 40-		return false, err
 41-	}
 42-
 43-	return true, nil
 44-}
 45-
 46-func (h *UploadImgHandler) metaImg(data *PostMetaData) error {
 47-	// if the file is empty that means we should delete it
 48-	// so we can skip all the meta info
 49-	if data.FileSize == 0 {
 50-		return nil
 51-	}
 52-
 53-	bucket, err := h.Storage.UpsertBucket(shared.GetAssetBucketName(data.User.ID))
 54-	if err != nil {
 55-		return err
 56-	}
 57-
 58-	reader := bytes.NewReader([]byte(data.Text))
 59-
 60-	fname, _, err := h.Storage.PutObject(
 61-		bucket,
 62-		h.getObjectPath(data.Filename),
 63-		sendutils.NopReaderAtCloser(reader),
 64-		&sendutils.FileEntry{},
 65-	)
 66-	if err != nil {
 67-		return err
 68-	}
 69-
 70-	data.Data = db.PostData{
 71-		ImgPath: fname,
 72-	}
 73-
 74-	data.Text = ""
 75-
 76-	return nil
 77-}
 78-
 79-func (h *UploadImgHandler) writeImg(s ssh.Session, data *PostMetaData) error {
 80-	valid, err := h.validateImg(data)
 81-	if !valid {
 82-		return err
 83-	}
 84-	user, err := h.DBPool.FindUser(s.Permissions().Extensions["user_id"])
 85-	if err != nil {
 86-		return err
 87-	}
 88-
 89-	logger := h.Cfg.Logger
 90-	logger = shared.LoggerWithUser(logger, user)
 91-
 92-	err = h.metaImg(data)
 93-	if err != nil {
 94-		logger.Error("could not get meta for img", "err", err.Error())
 95-		return err
 96-	}
 97-
 98-	modTime := time.Now()
 99-
100-	if data.Mtime > 0 {
101-		modTime = time.Unix(data.Mtime, 0)
102-	}
103-
104-	logger = logger.With(
105-		"filename", data.Filename,
106-	)
107-
108-	if data.Cur == nil {
109-		logger.Info("file not found, adding record")
110-		insertPost := db.Post{
111-			UserID: user.ID,
112-			Space:  Space,
113-
114-			Data:        data.Data,
115-			Description: data.Description,
116-			Filename:    data.Filename,
117-			FileSize:    data.FileSize,
118-			Hidden:      data.Hidden,
119-			MimeType:    data.MimeType,
120-			PublishAt:   data.PublishAt,
121-			Shasum:      data.Shasum,
122-			Slug:        data.Slug,
123-			Text:        data.Text,
124-			Title:       data.Title,
125-			UpdatedAt:   &modTime,
126-		}
127-		_, err := h.DBPool.InsertPost(&insertPost)
128-		if err != nil {
129-			logger.Error("post could not create", "err", err.Error())
130-			return fmt.Errorf("error for %s: %v", data.Filename, err)
131-		}
132-	} else {
133-		if data.Shasum == data.Cur.Shasum && modTime.Equal(*data.Cur.UpdatedAt) {
134-			logger.Info("image found, but image is identical, skipping")
135-			return nil
136-		}
137-
138-		logger.Info("file found, updating record")
139-
140-		updatePost := db.Post{
141-			ID: data.Cur.ID,
142-
143-			Data:        data.Data,
144-			FileSize:    data.FileSize,
145-			Description: data.Description,
146-			PublishAt:   data.PublishAt,
147-			Slug:        data.Slug,
148-			Shasum:      data.Shasum,
149-			Text:        data.Text,
150-			Title:       data.Title,
151-			Hidden:      data.Hidden,
152-			UpdatedAt:   &modTime,
153-		}
154-		_, err = h.DBPool.UpdatePost(&updatePost)
155-		if err != nil {
156-			logger.Error("post could not update", "err", err.Error())
157-			return fmt.Errorf("error for %s: %v", data.Filename, err)
158-		}
159-	}
160-
161-	return nil
162-}
M filehandlers/post_handler.go
+4, -0
 1@@ -45,6 +45,10 @@ func NewScpPostHandler(dbpool db.DB, cfg *shared.ConfigSite, hooks ScpFileHooks)
 2 	}
 3 }
 4 
 5+func (r *ScpUploadHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
 6+	return BaseList(s, fpath, isDir, recursive, []string{r.Cfg.Space}, r.DBPool)
 7+}
 8+
 9 func (h *ScpUploadHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReaderAtCloser, error) {
10 	user, err := h.DBPool.FindUser(s.Permissions().Extensions["user_id"])
11 	if err != nil {
M filehandlers/router_handler.go
+42, -28
  1@@ -15,6 +15,7 @@ import (
  2 )
  3 
  4 type ReadWriteHandler interface {
  5+	List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error)
  6 	Write(ssh.Session, *utils.FileEntry) (string, error)
  7 	Read(ssh.Session, *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error)
  8 	Delete(ssh.Session, *utils.FileEntry) error
  9@@ -39,8 +40,8 @@ func NewFileHandlerRouter(cfg *shared.ConfigSite, dbpool db.DB, mapper map[strin
 10 	}
 11 }
 12 
 13-func (r *FileHandlerRouter) findHandler(entry *utils.FileEntry) (ReadWriteHandler, error) {
 14-	fext := filepath.Ext(entry.Filepath)
 15+func (r *FileHandlerRouter) findHandler(fp string) (ReadWriteHandler, error) {
 16+	fext := filepath.Ext(fp)
 17 	handler, ok := r.FileMap[fext]
 18 	if !ok {
 19 		hand, hasFallback := r.FileMap["fallback"]
 20@@ -57,7 +58,7 @@ func (r *FileHandlerRouter) Write(s ssh.Session, entry *utils.FileEntry) (string
 21 		return "", os.ErrInvalid
 22 	}
 23 
 24-	handler, err := r.findHandler(entry)
 25+	handler, err := r.findHandler(entry.Filepath)
 26 	if err != nil {
 27 		return "", err
 28 	}
 29@@ -65,7 +66,7 @@ func (r *FileHandlerRouter) Write(s ssh.Session, entry *utils.FileEntry) (string
 30 }
 31 
 32 func (r *FileHandlerRouter) Delete(s ssh.Session, entry *utils.FileEntry) error {
 33-	handler, err := r.findHandler(entry)
 34+	handler, err := r.findHandler(entry.Filepath)
 35 	if err != nil {
 36 		return err
 37 	}
 38@@ -73,13 +74,49 @@ func (r *FileHandlerRouter) Delete(s ssh.Session, entry *utils.FileEntry) error
 39 }
 40 
 41 func (r *FileHandlerRouter) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
 42-	handler, err := r.findHandler(entry)
 43+	handler, err := r.findHandler(entry.Filepath)
 44 	if err != nil {
 45 		return nil, nil, err
 46 	}
 47 	return handler.Read(s, entry)
 48 }
 49 
 50+func (r *FileHandlerRouter) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
 51+	files := []os.FileInfo{}
 52+	for key, handler := range r.FileMap {
 53+		// TODO: hack because we have duplicate keys for .md and .css
 54+		if key == ".css" {
 55+			continue
 56+		}
 57+
 58+		ff, err := handler.List(s, fpath, isDir, recursive)
 59+		if err != nil {
 60+			r.GetLogger().Error("handler list", "err", err)
 61+			continue
 62+		}
 63+		files = append(files, ff...)
 64+	}
 65+	return files, nil
 66+}
 67+
 68+func (r *FileHandlerRouter) GetLogger() *slog.Logger {
 69+	return r.Cfg.Logger
 70+}
 71+
 72+func (r *FileHandlerRouter) Validate(s ssh.Session) error {
 73+	user, err := r.DBPool.FindUser(s.Permissions().Extensions["user_id"])
 74+	if err != nil {
 75+		return err
 76+	}
 77+
 78+	r.Cfg.Logger.Info(
 79+		"attempting to upload files",
 80+		"user", user.Name,
 81+		"space", r.Cfg.Space,
 82+	)
 83+	return nil
 84+}
 85+
 86 func BaseList(s ssh.Session, fpath string, isDir bool, recursive bool, spaces []string, dbpool db.DB) ([]os.FileInfo, error) {
 87 	var fileList []os.FileInfo
 88 	user, err := dbpool.FindUser(s.Permissions().Extensions["user_id"])
 89@@ -112,7 +149,6 @@ func BaseList(s ssh.Session, fpath string, isDir bool, recursive bool, spaces []
 90 		}
 91 	} else {
 92 		for _, space := range spaces {
 93-
 94 			p, e := dbpool.FindPostWithFilename(cleanFilename, user.ID, space)
 95 			if e != nil {
 96 				err = e
 97@@ -143,25 +179,3 @@ func BaseList(s ssh.Session, fpath string, isDir bool, recursive bool, spaces []
 98 
 99 	return fileList, nil
100 }
101-
102-func (r *FileHandlerRouter) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
103-	return BaseList(s, fpath, isDir, recursive, r.Spaces, r.DBPool)
104-}
105-
106-func (r *FileHandlerRouter) GetLogger() *slog.Logger {
107-	return r.Cfg.Logger
108-}
109-
110-func (r *FileHandlerRouter) Validate(s ssh.Session) error {
111-	user, err := r.DBPool.FindUser(s.Permissions().Extensions["user_id"])
112-	if err != nil {
113-		return err
114-	}
115-
116-	r.Cfg.Logger.Info(
117-		"attempting to upload files",
118-		"user", user.Name,
119-		"space", r.Cfg.Space,
120-	)
121-	return nil
122-}
M pgs/uploader.go
+4, -18
 1@@ -279,24 +279,10 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (s
 2 
 3 	// find, create, or update project if we haven't already done it
 4 	if project == nil {
 5-		project, err = h.DBPool.FindProjectByName(user.ID, projectName)
 6-		if err == nil {
 7-			err = h.DBPool.UpdateProject(user.ID, projectName)
 8-			if err != nil {
 9-				logger.Error("could not update project", "err", err.Error())
10-				return "", err
11-			}
12-		} else {
13-			_, err = h.DBPool.InsertProject(user.ID, projectName, projectName)
14-			if err != nil {
15-				logger.Error("could not create project", "err", err.Error())
16-				return "", err
17-			}
18-			project, err = h.DBPool.FindProjectByName(user.ID, projectName)
19-			if err != nil {
20-				logger.Error("could not find project", "err", err.Error())
21-				return "", err
22-			}
23+		project, err = h.DBPool.UpsertProject(user.ID, projectName, projectName)
24+		if err != nil {
25+			logger.Error("upsert project", "err", err.Error())
26+			return "", err
27 		}
28 		setProject(s, project)
29 	}
M pgs/wish.go
+0, -4
 1@@ -148,10 +148,6 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
 2 				err := opts.fzf(projectName)
 3 				opts.bail(err)
 4 				return
 5-			} else if cmd == "stats" {
 6-				err := opts.statsByProject(projectName)
 7-				opts.bail(err)
 8-				return
 9 			} else if cmd == "link" {
10 				linkCmd, write := flagSet("link", sesh)
11 				linkTo := linkCmd.String("to", "", "symbolic link to this project")
M prose/ssh.go
+0, -1
1@@ -85,7 +85,6 @@ func StartSshServer() {
2 		"fallback": uploadimgs.NewUploadImgHandler(dbh, cfg, st),
3 	}
4 	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
5-	handler.Spaces = []string{cfg.Space, "imgs"}
6 
7 	sshAuth := shared.NewSshAuthHandler(dbh, logger, cfg)
8 	s, err := wish.NewServer(