repos / pico

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

commit
61caf14
parent
a8ab7f3
author
Antonio Mika
date
2023-11-10 01:29:09 +0000 UTC
Working on adding more features to rsync
29 files changed,  +281, -133
M go.mod
A .vscode/launch.json
+16, -0
 1@@ -0,0 +1,16 @@
 2+{
 3+    // Use IntelliSense to learn about possible attributes.
 4+    // Hover to view descriptions of existing attributes.
 5+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
 6+    "version": "0.2.0",
 7+    "configurations": [
 8+        {
 9+            "name": "Launch Package",
10+            "type": "go",
11+            "request": "launch",
12+            "mode": "auto",
13+            "program": "${workspaceFolder}/cmd/pgs/ssh/main.go",
14+            "envFile": "${workspaceFolder}/.env"
15+        }
16+    ]
17+}
M cmd/scripts/webp/webp.go
+3, -2
 1@@ -9,6 +9,7 @@ import (
 2 	"github.com/picosh/pico/imgs"
 3 	"github.com/picosh/pico/shared"
 4 	"github.com/picosh/pico/shared/storage"
 5+	"github.com/picosh/pico/wish/send/utils"
 6 )
 7 
 8 func main() {
 9@@ -42,7 +43,7 @@ func main() {
10 			continue
11 		}
12 
13-		reader, err := st.GetFile(bucket, post.Filename)
14+		reader, _, err := st.GetFile(bucket, post.Filename)
15 		if err != nil {
16 			cfg.Logger.Infof("file not found %s/%s", post.UserID, post.Filename)
17 			continue
18@@ -67,7 +68,7 @@ func main() {
19 		_, err = st.PutFile(
20 			bucket,
21 			fmt.Sprintf("%s.webp", shared.SanitizeFileExt(post.Filename)),
22-			storage.NopReaderAtCloser(webpReader),
23+			utils.NopReaderAtCloser(webpReader),
24 		)
25 		if err != nil {
26 			cfg.Logger.Error(err)
M db/postgres/storage.go
+2, -1
 1@@ -9,11 +9,12 @@ import (
 2 	"strings"
 3 	"time"
 4 
 5+	"slices"
 6+
 7 	_ "github.com/lib/pq"
 8 	"github.com/picosh/pico/db"
 9 	"github.com/picosh/pico/shared"
10 	"go.uber.org/zap"
11-	"golang.org/x/exp/slices"
12 )
13 
14 var PAGER_SIZE = 15
M feeds/scp_hooks.go
+2, -1
 1@@ -5,11 +5,12 @@ import (
 2 	"strings"
 3 	"time"
 4 
 5+	"slices"
 6+
 7 	"github.com/picosh/pico/db"
 8 	"github.com/picosh/pico/filehandlers"
 9 	"github.com/picosh/pico/imgs"
10 	"github.com/picosh/pico/shared"
11-	"golang.org/x/exp/slices"
12 )
13 
14 type FeedHooks struct {
M filehandlers/assets/asset.go
+2, -2
 1@@ -7,7 +7,7 @@ import (
 2 	"strings"
 3 
 4 	"github.com/picosh/pico/shared"
 5-	"github.com/picosh/pico/shared/storage"
 6+	"github.com/picosh/pico/wish/send/utils"
 7 )
 8 
 9 func (h *UploadAssetHandler) validateAsset(data *FileData) (bool, error) {
10@@ -81,7 +81,7 @@ func (h *UploadAssetHandler) writeAsset(data *FileData) error {
11 		_, err := h.Storage.PutFile(
12 			data.Bucket,
13 			assetFilename,
14-			storage.NopReaderAtCloser(reader),
15+			utils.NopReaderAtCloser(reader),
16 		)
17 		if err != nil {
18 			return err
M filehandlers/assets/handler.go
+6, -4
 1@@ -73,7 +73,7 @@ func NewUploadAssetHandler(dbpool db.DB, cfg *shared.ConfigSite, storage storage
 2 	}
 3 }
 4 
 5-func (h *UploadAssetHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, io.ReaderAt, error) {
 6+func (h *UploadAssetHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
 7 	user, err := getUser(s)
 8 	if err != nil {
 9 		return nil, nil, err
10@@ -92,17 +92,19 @@ func (h *UploadAssetHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.Fil
11 	}
12 
13 	fname := shared.GetAssetFileName(entry)
14-	contents, err := h.Storage.GetFile(bucket, fname)
15+	contents, size, err := h.Storage.GetFile(bucket, fname)
16 	if err != nil {
17 		return nil, nil, err
18 	}
19 
20+	fileInfo.FSize = size
21+
22 	reader := utils.NewAllReaderAt(contents)
23 
24 	return fileInfo, reader, nil
25 }
26 
27-func (h *UploadAssetHandler) List(s ssh.Session, fpath string, isDir bool) ([]os.FileInfo, error) {
28+func (h *UploadAssetHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
29 	var fileList []os.FileInfo
30 
31 	user, err := getUser(s)
32@@ -135,7 +137,7 @@ func (h *UploadAssetHandler) List(s ssh.Session, fpath string, isDir bool) ([]os
33 			cleanFilename += "/"
34 		}
35 
36-		foundList, err := h.Storage.ListFiles(bucket, cleanFilename, false)
37+		foundList, err := h.Storage.ListFiles(bucket, cleanFilename, recursive)
38 		if err != nil {
39 			return fileList, err
40 		}
M filehandlers/imgs/handler.go
+5, -4
 1@@ -9,6 +9,8 @@ import (
 2 	"path/filepath"
 3 	"time"
 4 
 5+	"slices"
 6+
 7 	"github.com/charmbracelet/ssh"
 8 	exifremove "github.com/neurosnap/go-exif-remove"
 9 	"github.com/picosh/pico/db"
10@@ -16,7 +18,6 @@ import (
11 	"github.com/picosh/pico/shared/storage"
12 	"github.com/picosh/pico/wish/cms/util"
13 	"github.com/picosh/pico/wish/send/utils"
14-	"golang.org/x/exp/slices"
15 )
16 
17 var maxSize = 1 * shared.GB
18@@ -72,7 +73,7 @@ func (h *UploadImgHandler) removePost(data *PostMetaData) error {
19 	return nil
20 }
21 
22-func (h *UploadImgHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, io.ReaderAt, error) {
23+func (h *UploadImgHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
24 	user, err := getUser(s)
25 	if err != nil {
26 		return nil, nil, err
27@@ -101,7 +102,7 @@ func (h *UploadImgHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileI
28 		return nil, nil, err
29 	}
30 
31-	contents, err := h.Storage.GetFile(bucket, post.Filename)
32+	contents, _, err := h.Storage.GetFile(bucket, post.Filename)
33 	if err != nil {
34 		return nil, nil, err
35 	}
36@@ -111,7 +112,7 @@ func (h *UploadImgHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileI
37 	return fileInfo, reader, nil
38 }
39 
40-func (h *UploadImgHandler) List(s ssh.Session, fpath string, isDir bool) ([]os.FileInfo, error) {
41+func (h *UploadImgHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
42 	var fileList []os.FileInfo
43 	user, err := getUser(s)
44 	if err != nil {
M filehandlers/imgs/img.go
+3, -3
 1@@ -9,7 +9,7 @@ import (
 2 	"github.com/charmbracelet/ssh"
 3 	"github.com/picosh/pico/db"
 4 	"github.com/picosh/pico/shared"
 5-	"github.com/picosh/pico/shared/storage"
 6+	"github.com/picosh/pico/wish/send/utils"
 7 )
 8 
 9 func (h *UploadImgHandler) validateImg(data *PostMetaData) (bool, error) {
10@@ -59,7 +59,7 @@ func (h *UploadImgHandler) metaImg(data *PostMetaData) error {
11 	fname, err := h.Storage.PutFile(
12 		bucket,
13 		data.Filename,
14-		storage.NopReaderAtCloser(reader),
15+		utils.NopReaderAtCloser(reader),
16 	)
17 	if err != nil {
18 		return err
19@@ -105,7 +105,7 @@ func (h *UploadImgHandler) metaImg(data *PostMetaData) error {
20 	_, err = h.Storage.PutFile(
21 		bucket,
22 		finalName,
23-		storage.NopReaderAtCloser(webpReader),
24+		utils.NopReaderAtCloser(webpReader),
25 	)
26 	if err != nil {
27 		return err
M filehandlers/post_handler.go
+3, -3
 1@@ -61,7 +61,7 @@ func NewScpPostHandler(dbpool db.DB, cfg *shared.ConfigSite, hooks ScpFileHooks,
 2 	}
 3 }
 4 
 5-func (h *ScpUploadHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, io.ReaderAt, error) {
 6+func (h *ScpUploadHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
 7 	user, err := getUser(s)
 8 	if err != nil {
 9 		return nil, nil, err
10@@ -84,12 +84,12 @@ func (h *ScpUploadHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileI
11 		FModTime: *post.UpdatedAt,
12 	}
13 
14-	reader := strings.NewReader(post.Text)
15+	reader := utils.NopReaderAtCloser(strings.NewReader(post.Text))
16 
17 	return fileInfo, reader, nil
18 }
19 
20-func (h *ScpUploadHandler) List(s ssh.Session, fpath string, isDir bool) ([]os.FileInfo, error) {
21+func (h *ScpUploadHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
22 	var fileList []os.FileInfo
23 	user, err := getUser(s)
24 	if err != nil {
M go.mod
+2, -0
1@@ -2,6 +2,8 @@ module github.com/picosh/pico
2 
3 go 1.21
4 
5+replace github.com/antoniomika/go-rsync-receiver => /workspaces/go-rsync-receiver
6+
7 require (
8 	github.com/alecthomas/chroma v0.10.0
9 	github.com/antoniomika/go-rsync-receiver v0.0.0-20220901010427-e6494124f0c8
M imgs/api.go
+5, -3
 1@@ -11,14 +11,16 @@ import (
 2 
 3 	_ "net/http/pprof"
 4 
 5+	"slices"
 6+
 7 	"github.com/gorilla/feeds"
 8 	gocache "github.com/patrickmn/go-cache"
 9 	"github.com/picosh/pico/db"
10 	"github.com/picosh/pico/db/postgres"
11 	"github.com/picosh/pico/shared"
12 	"github.com/picosh/pico/shared/storage"
13+	"github.com/picosh/pico/wish/send/utils"
14 	"go.uber.org/zap"
15-	"golang.org/x/exp/slices"
16 )
17 
18 type PageData struct {
19@@ -199,7 +201,7 @@ type ImgHandler struct {
20 
21 type ImgResizer struct {
22 	Key      string
23-	contents storage.ReaderAtCloser
24+	contents utils.ReaderAtCloser
25 	writer   io.Writer
26 	Img      *shared.ImgOptimizer
27 	Cache    *gocache.Cache
28@@ -282,7 +284,7 @@ func imgHandler(w http.ResponseWriter, h *ImgHandler) {
29 		fname = fmt.Sprintf("%s.webp", shared.SanitizeFileExt(post.Filename))
30 	}
31 
32-	contents, err := h.Storage.GetFile(bucket, fname)
33+	contents, _, err := h.Storage.GetFile(bucket, fname)
34 	if err != nil {
35 		h.Logger.Infof(
36 			"file not found %s/%s in storage (bucket: %s, name: %s)",
M lists/api.go
+2, -1
 1@@ -10,6 +10,8 @@ import (
 2 	"strconv"
 3 	"time"
 4 
 5+	"slices"
 6+
 7 	"github.com/gorilla/feeds"
 8 	gocache "github.com/patrickmn/go-cache"
 9 	"github.com/picosh/pico/db"
10@@ -17,7 +19,6 @@ import (
11 	"github.com/picosh/pico/imgs"
12 	"github.com/picosh/pico/shared"
13 	"github.com/picosh/pico/shared/storage"
14-	"golang.org/x/exp/slices"
15 )
16 
17 type PostItemData struct {
M lists/scp_hooks.go
+2, -1
 1@@ -4,11 +4,12 @@ import (
 2 	"fmt"
 3 	"strings"
 4 
 5+	"slices"
 6+
 7 	"github.com/picosh/pico/db"
 8 	"github.com/picosh/pico/filehandlers"
 9 	"github.com/picosh/pico/imgs"
10 	"github.com/picosh/pico/shared"
11-	"golang.org/x/exp/slices"
12 )
13 
14 type ListHooks struct {
M pgs/api.go
+3, -3
 1@@ -210,7 +210,7 @@ func assetHandler(w http.ResponseWriter, h *AssetHandler) {
 2 	}
 3 
 4 	var redirects []*RedirectRule
 5-	redirectFp, err := h.Storage.GetFile(bucket, filepath.Join(h.ProjectDir, "_redirects"))
 6+	redirectFp, _, err := h.Storage.GetFile(bucket, filepath.Join(h.ProjectDir, "_redirects"))
 7 	if err == nil {
 8 		defer redirectFp.Close()
 9 		buf := new(strings.Builder)
10@@ -228,11 +228,11 @@ func assetHandler(w http.ResponseWriter, h *AssetHandler) {
11 	}
12 
13 	routes := calcPossibleRoutes(h.ProjectDir, h.Filepath, redirects)
14-	var contents storage.ReaderAtCloser
15+	var contents utils.ReaderAtCloser
16 	assetFilepath := ""
17 	status := 200
18 	for _, fp := range routes {
19-		c, err := h.Storage.GetFile(bucket, fp.Filepath)
20+		c, _, err := h.Storage.GetFile(bucket, fp.Filepath)
21 		if err == nil {
22 			contents = c
23 			assetFilepath = fp.Filepath
M pgs/redirect.go
+2, -2
 1@@ -93,7 +93,7 @@ func parseRedirectText(text string) ([]*RedirectRule, error) {
 2 
 3 		parts := reSplitWhitespace.Split(trimmed, -1)
 4 		if len(parts) < 2 {
 5-			return rules, fmt.Errorf("Missing destination path/URL")
 6+			return rules, fmt.Errorf("missing destination path/URL")
 7 		}
 8 
 9 		from := parts[0]
10@@ -114,7 +114,7 @@ func parseRedirectText(text string) ([]*RedirectRule, error) {
11 			}
12 
13 			if toIndex == -1 {
14-				return rules, fmt.Errorf("The destination path/URL must start with '/', 'http:' or 'https:'")
15+				return rules, fmt.Errorf("the destination path/URL must start with '/', 'http:' or 'https:'")
16 			}
17 
18 			queryParts := parts[:toIndex]
M prose/api.go
+2, -1
 1@@ -10,6 +10,8 @@ import (
 2 	"strconv"
 3 	"time"
 4 
 5+	"slices"
 6+
 7 	"github.com/gorilla/feeds"
 8 	gocache "github.com/patrickmn/go-cache"
 9 	"github.com/picosh/pico/db"
10@@ -17,7 +19,6 @@ import (
11 	"github.com/picosh/pico/imgs"
12 	"github.com/picosh/pico/shared"
13 	"github.com/picosh/pico/shared/storage"
14-	"golang.org/x/exp/slices"
15 )
16 
17 type PageData struct {
M prose/scp_hooks.go
+2, -1
 1@@ -4,11 +4,12 @@ import (
 2 	"fmt"
 3 	"strings"
 4 
 5+	"slices"
 6+
 7 	"github.com/picosh/pico/db"
 8 	"github.com/picosh/pico/filehandlers"
 9 	"github.com/picosh/pico/imgs"
10 	"github.com/picosh/pico/shared"
11-	"golang.org/x/exp/slices"
12 )
13 
14 type MarkdownHooks struct {
M shared/listparser.go
+2, -1
 1@@ -9,8 +9,9 @@ import (
 2 	"strings"
 3 	"time"
 4 
 5+	"slices"
 6+
 7 	"github.com/araddon/dateparse"
 8-	"golang.org/x/exp/slices"
 9 )
10 
11 var reIndent = regexp.MustCompile(`^[[:blank:]]+`)
M shared/storage/fs.go
+30, -8
 1@@ -3,6 +3,7 @@ package storage
 2 import (
 3 	"fmt"
 4 	"io"
 5+	"io/fs"
 6 	"os"
 7 	"path"
 8 	"path/filepath"
 9@@ -82,16 +83,21 @@ func (s *StorageFS) DeleteBucket(bucket Bucket) error {
10 	return os.RemoveAll(bucket.Path)
11 }
12 
13-func (s *StorageFS) GetFile(bucket Bucket, fpath string) (ReaderAtCloser, error) {
14+func (s *StorageFS) GetFile(bucket Bucket, fpath string) (utils.ReaderAtCloser, int64, error) {
15 	dat, err := os.Open(filepath.Join(bucket.Path, fpath))
16 	if err != nil {
17-		return nil, err
18+		return nil, 0, err
19 	}
20 
21-	return dat, nil
22+	info, err := dat.Stat()
23+	if err != nil {
24+		return nil, 0, err
25+	}
26+
27+	return dat, info.Size(), nil
28 }
29 
30-func (s *StorageFS) PutFile(bucket Bucket, fpath string, contents ReaderAtCloser) (string, error) {
31+func (s *StorageFS) PutFile(bucket Bucket, fpath string, contents utils.ReaderAtCloser) (string, error) {
32 	loc := filepath.Join(bucket.Path, fpath)
33 	err := os.MkdirAll(filepath.Dir(loc), os.ModePerm)
34 	if err != nil {
35@@ -142,10 +148,26 @@ func (s *StorageFS) ListFiles(bucket Bucket, dir string, recursive bool) ([]os.F
36 		return fileList, err
37 	}
38 
39-	files, err := os.ReadDir(fpath)
40-	if err != nil {
41-		fileList = append(fileList, info)
42-		return fileList, nil
43+	var files []fs.DirEntry
44+
45+	if recursive {
46+		err = filepath.WalkDir(fpath, func(s string, d fs.DirEntry, err error) error {
47+			if err != nil {
48+				return err
49+			}
50+			files = append(files, d)
51+			return nil
52+		})
53+		if err != nil {
54+			fileList = append(fileList, info)
55+			return fileList, nil
56+		}
57+	} else {
58+		files, err = os.ReadDir(fpath)
59+		if err != nil {
60+			fileList = append(fileList, info)
61+			return fileList, nil
62+		}
63 	}
64 
65 	for _, f := range files {
M shared/storage/minio.go
+6, -6
 1@@ -126,23 +126,23 @@ func (s *StorageMinio) DeleteBucket(bucket Bucket) error {
 2 	return s.Client.RemoveBucket(context.TODO(), bucket.Name)
 3 }
 4 
 5-func (s *StorageMinio) GetFile(bucket Bucket, fpath string) (ReaderAtCloser, error) {
 6+func (s *StorageMinio) GetFile(bucket Bucket, fpath string) (utils.ReaderAtCloser, int64, error) {
 7 	// we have to stat the object first to see if it exists
 8 	// https://github.com/minio/minio-go/issues/654
 9-	_, err := s.Client.StatObject(context.Background(), bucket.Name, fpath, minio.StatObjectOptions{})
10+	info, err := s.Client.StatObject(context.Background(), bucket.Name, fpath, minio.StatObjectOptions{})
11 	if err != nil {
12-		return nil, err
13+		return nil, 0, err
14 	}
15 
16 	obj, err := s.Client.GetObject(context.Background(), bucket.Name, fpath, minio.GetObjectOptions{})
17 	if err != nil {
18-		return nil, err
19+		return nil, 0, err
20 	}
21 
22-	return obj, nil
23+	return obj, info.Size, nil
24 }
25 
26-func (s *StorageMinio) PutFile(bucket Bucket, fpath string, contents ReaderAtCloser) (string, error) {
27+func (s *StorageMinio) PutFile(bucket Bucket, fpath string, contents utils.ReaderAtCloser) (string, error) {
28 	info, err := s.Client.PutObject(context.TODO(), bucket.Name, fpath, contents, -1, minio.PutObjectOptions{})
29 	if err != nil {
30 		return "", err
M shared/storage/storage.go
+4, -23
 1@@ -1,8 +1,9 @@
 2 package storage
 3 
 4 import (
 5-	"io"
 6 	"os"
 7+
 8+	"github.com/picosh/pico/wish/send/utils"
 9 )
10 
11 type Bucket struct {
12@@ -10,34 +11,14 @@ type Bucket struct {
13 	Path string
14 }
15 
16-type ReadAndReaderAt interface {
17-	io.ReaderAt
18-	io.Reader
19-}
20-
21-type ReaderAtCloser interface {
22-	io.ReaderAt
23-	io.ReadCloser
24-}
25-
26-func NopReaderAtCloser(r ReadAndReaderAt) ReaderAtCloser {
27-	return nopReaderAtCloser{r}
28-}
29-
30-type nopReaderAtCloser struct {
31-	ReadAndReaderAt
32-}
33-
34-func (nopReaderAtCloser) Close() error { return nil }
35-
36 type ObjectStorage interface {
37 	GetBucket(name string) (Bucket, error)
38 	UpsertBucket(name string) (Bucket, error)
39 
40 	DeleteBucket(bucket Bucket) error
41 	GetBucketQuota(bucket Bucket) (uint64, error)
42-	GetFile(bucket Bucket, fpath string) (ReaderAtCloser, error)
43-	PutFile(bucket Bucket, fpath string, contents ReaderAtCloser) (string, error)
44+	GetFile(bucket Bucket, fpath string) (utils.ReaderAtCloser, int64, error)
45+	PutFile(bucket Bucket, fpath string, contents utils.ReaderAtCloser) (string, error)
46 	DeleteFile(bucket Bucket, fpath string) error
47 	ListFiles(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error)
48 }
M shared/util.go
+2, -1
 1@@ -15,8 +15,9 @@ import (
 2 	"unicode"
 3 	"unicode/utf8"
 4 
 5+	"slices"
 6+
 7 	"github.com/charmbracelet/ssh"
 8-	"golang.org/x/exp/slices"
 9 )
10 
11 var fnameRe = regexp.MustCompile(`[-_]+`)
M wish/cmd/server/main.go
+3, -4
 1@@ -2,7 +2,6 @@ package main
 2 
 3 import (
 4 	"fmt"
 5-	"io"
 6 	"log"
 7 	"os"
 8 	"strings"
 9@@ -29,7 +28,7 @@ func (h *handler) Validate(session ssh.Session) error {
10 	return nil
11 }
12 
13-func (h *handler) Read(session ssh.Session, entry *utils.FileEntry) (os.FileInfo, io.ReaderAt, error) {
14+func (h *handler) Read(session ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
15 	log.Printf("Received validate from session: %+v", session)
16 
17 	data := strings.NewReader("lorem ipsum dolor")
18@@ -39,10 +38,10 @@ func (h *handler) Read(session ssh.Session, entry *utils.FileEntry) (os.FileInfo
19 		FIsDir:   false,
20 		FSize:    data.Size(),
21 		FModTime: time.Now(),
22-	}, data, nil
23+	}, utils.NopReaderAtCloser(data), nil
24 }
25 
26-func (h *handler) List(session ssh.Session, fpath string, isDir bool) ([]os.FileInfo, error) {
27+func (h *handler) List(session ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
28 	return nil, nil
29 }
30 
M wish/list/list.go
+1, -1
1@@ -18,7 +18,7 @@ func Middleware(writeHandler utils.CopyFromClientHandler) wish.Middleware {
2 				return
3 			}
4 
5-			fileList, err := writeHandler.List(session, "/", true)
6+			fileList, err := writeHandler.List(session, "/", true, false)
7 			if err != nil {
8 				utils.ErrorHandler(session, err)
9 				return
M wish/send/rsync/rsync.go
+105, -20
  1@@ -1,12 +1,15 @@
  2 package rsync
  3 
  4 import (
  5+	"errors"
  6 	"fmt"
  7 	"io"
  8 	"io/fs"
  9 	"log"
 10 	"os"
 11+	"path"
 12 	"path/filepath"
 13+	"strings"
 14 
 15 	"github.com/antoniomika/go-rsync-receiver/rsyncreceiver"
 16 	"github.com/antoniomika/go-rsync-receiver/rsyncsender"
 17@@ -19,41 +22,114 @@ import (
 18 type handler struct {
 19 	session      ssh.Session
 20 	writeHandler utils.CopyFromClientHandler
 21+	root         string
 22 }
 23 
 24 func (h *handler) Skip(file *rsyncutils.ReceiverFile) bool {
 25-	return false
 26+	log.Printf("SKIP %+v", file)
 27+	return file.FileMode().IsDir()
 28 }
 29 
 30-func (h *handler) List(path string) ([]fs.FileInfo, error) {
 31-	list, err := h.writeHandler.List(h.session, path, true)
 32+func (h *handler) List(rPath string) ([]fs.FileInfo, error) {
 33+	log.Println("LIST", rPath)
 34+	isDir := false
 35+	if rPath == "." {
 36+		rPath = "/"
 37+		isDir = true
 38+	}
 39+
 40+	list, err := h.writeHandler.List(h.session, rPath, isDir, true)
 41 	if err != nil {
 42 		return nil, err
 43 	}
 44 
 45-	newList := list
 46-	if list[0].IsDir() {
 47-		newList = list[1:]
 48+	for _, f := range list {
 49+		log.Printf("first %+v", f)
 50+	}
 51+
 52+	var dirs []string
 53+
 54+	var newList []fs.FileInfo
 55+
 56+	for _, f := range list {
 57+		fname := f.Name()
 58+		if strings.HasPrefix(f.Name(), "/") {
 59+			fname = path.Join(rPath, f.Name())
 60+		}
 61+
 62+		if fname == "" && !f.IsDir() {
 63+			fname = path.Base(rPath)
 64+		}
 65+
 66+		newFile := &utils.VirtualFile{
 67+			FName:    fname,
 68+			FIsDir:   f.IsDir(),
 69+			FSize:    f.Size(),
 70+			FModTime: f.ModTime(),
 71+			FSys:     f.Sys(),
 72+		}
 73+
 74+		newList = append(newList, newFile)
 75+
 76+		parts := strings.Split(newFile.Name(), string(os.PathSeparator))
 77+		lastDir := newFile.Name()
 78+		for i := 0; i < len(parts); i++ {
 79+			lastDir, _ = path.Split(lastDir)
 80+			if lastDir == "" {
 81+				continue
 82+			}
 83+
 84+			lastDir = lastDir[:len(lastDir)-1]
 85+			dirs = append(dirs, lastDir)
 86+		}
 87+	}
 88+
 89+	for _, dir := range dirs {
 90+		newList = append(newList, &utils.VirtualFile{
 91+			FName:  dir,
 92+			FIsDir: true,
 93+		})
 94+	}
 95+
 96+	for _, f := range newList {
 97+		log.Printf("%+v", f)
 98+	}
 99+
100+	if len(newList) == 0 {
101+		return nil, errors.New("no files to process")
102 	}
103 
104 	return newList, nil
105 }
106 
107-func (h *handler) Read(path string) (os.FileInfo, io.ReaderAt, error) {
108-	return h.writeHandler.Read(h.session, &utils.FileEntry{Filepath: path})
109+func (h *handler) Read(file *rsyncutils.SenderFile) (os.FileInfo, io.ReaderAt, error) {
110+	log.Printf("READ %+v %s", file, h.root)
111+
112+	filePath := file.WPath
113+
114+	if strings.HasSuffix(h.root, file.WPath) {
115+		filePath = h.root
116+	} else if !strings.HasPrefix(filePath, h.root) {
117+		filePath = path.Join(h.root, file.Path, file.WPath)
118+	}
119+
120+	log.Printf("READ %+v %s", file, filePath)
121+
122+	return h.writeHandler.Read(h.session, &utils.FileEntry{Filepath: filePath})
123 }
124 
125-func (h *handler) Put(fileName string, content io.Reader, fileSize int64, mTime int64, aTime int64) (int64, error) {
126-	cleanName := filepath.Base(fileName)
127-	fpath := "/"
128+func (h *handler) Put(file *rsyncutils.ReceiverFile) (int64, error) {
129+	log.Printf("PUT %+v", file)
130+	fpath := path.Join("/", h.root)
131 	fileEntry := &utils.FileEntry{
132-		Filepath: filepath.Join(fpath, cleanName),
133+		Filepath: filepath.Join(fpath, file.Name),
134 		Mode:     fs.FileMode(0600),
135-		Size:     fileSize,
136-		Mtime:    mTime,
137-		Atime:    aTime,
138+		Size:     file.Length,
139+		Mtime:    file.ModTime.Unix(),
140+		Atime:    file.ModTime.Unix(),
141 	}
142-	fileEntry.Reader = content
143+	log.Printf("%+v", fileEntry)
144+	fileEntry.Reader = file.Buf
145 
146 	msg, err := h.writeHandler.Write(h.session, fileEntry)
147 	if err != nil {
148@@ -79,19 +155,28 @@ func Middleware(writeHandler utils.CopyFromClientHandler) wish.Middleware {
149 			fileHandler := &handler{
150 				session:      session,
151 				writeHandler: writeHandler,
152+				root:         strings.TrimPrefix(cmd[len(cmd)-1], "/"),
153 			}
154 
155+			cmdFlags := session.Command()
156+
157 			for _, arg := range cmd {
158 				if arg == "--sender" {
159-					if err := rsyncsender.ClientRun(nil, session, fileHandler, cmd[len(cmd)-1], true); err != nil {
160-						log.Println("error running rsync:", err)
161+					opts, parser := rsyncsender.NewGetOpt()
162+					_, _ = parser.Parse(cmdFlags)
163+
164+					if err := rsyncsender.ClientRun(opts, session, fileHandler, fileHandler.root, true); err != nil {
165+						log.Println("error running rsync sender:", err)
166 					}
167 					return
168 				}
169 			}
170 
171-			if _, err := rsyncreceiver.ClientRun(nil, session, fileHandler, true); err != nil {
172-				log.Println("error running rsync:", err)
173+			opts, parser := rsyncreceiver.NewGetOpt()
174+			_, _ = parser.Parse(cmdFlags)
175+
176+			if _, err := rsyncreceiver.ClientRun(opts, session, fileHandler, true); err != nil {
177+				log.Println("error running rsync receiver:", err)
178 			}
179 		}
180 	}
M wish/send/sftp/handler.go
+3, -2
 1@@ -6,10 +6,11 @@ import (
 2 	"io"
 3 	"os"
 4 
 5+	"slices"
 6+
 7 	"github.com/charmbracelet/ssh"
 8 	"github.com/picosh/pico/wish/send/utils"
 9 	"github.com/pkg/sftp"
10-	"golang.org/x/exp/slices"
11 )
12 
13 type listerat []os.FileInfo
14@@ -51,7 +52,7 @@ func (f *handler) Filelist(r *sftp.Request) (sftp.ListerAt, error) {
15 	case "List", "Stat":
16 		list := r.Method == "List"
17 
18-		listData, err := f.writeHandler.List(f.session, r.Filepath, list)
19+		listData, err := f.writeHandler.List(f.session, r.Filepath, list, false)
20 		if err != nil {
21 			return nil, err
22 		}
D wish/send/utils/allreaderat.go
+0, -33
 1@@ -1,33 +0,0 @@
 2-package utils
 3-
 4-import (
 5-	"errors"
 6-	"io"
 7-	"net/http"
 8-
 9-	"github.com/minio/minio-go/v7"
10-)
11-
12-type AllReaderAt struct {
13-	Reader io.ReaderAt
14-}
15-
16-func NewAllReaderAt(reader io.ReaderAt) *AllReaderAt {
17-	return &AllReaderAt{reader}
18-}
19-
20-func (a *AllReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
21-	n, err = a.Reader.ReadAt(p, off)
22-
23-	if errors.Is(err, io.EOF) {
24-		return
25-	}
26-
27-	resp := minio.ToErrorResponse(err)
28-
29-	if resp.StatusCode == http.StatusRequestedRangeNotSatisfiable {
30-		err = io.EOF
31-	}
32-
33-	return
34-}
A wish/send/utils/io.go
+61, -0
 1@@ -0,0 +1,61 @@
 2+package utils
 3+
 4+import (
 5+	"errors"
 6+	"io"
 7+	"net/http"
 8+
 9+	"github.com/minio/minio-go/v7"
10+)
11+
12+type ReadAndReaderAt interface {
13+	io.ReaderAt
14+	io.Reader
15+}
16+
17+type ReaderAtCloser interface {
18+	io.ReaderAt
19+	io.ReadCloser
20+}
21+
22+func NopReaderAtCloser(r ReadAndReaderAt) ReaderAtCloser {
23+	return nopReaderAtCloser{r}
24+}
25+
26+type nopReaderAtCloser struct {
27+	ReadAndReaderAt
28+}
29+
30+func (nopReaderAtCloser) Close() error { return nil }
31+
32+type AllReaderAt struct {
33+	Reader ReaderAtCloser
34+}
35+
36+func NewAllReaderAt(reader ReaderAtCloser) *AllReaderAt {
37+	return &AllReaderAt{reader}
38+}
39+
40+func (a *AllReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
41+	n, err = a.Reader.ReadAt(p, off)
42+
43+	if errors.Is(err, io.EOF) {
44+		return
45+	}
46+
47+	resp := minio.ToErrorResponse(err)
48+
49+	if resp.StatusCode == http.StatusRequestedRangeNotSatisfiable {
50+		err = io.EOF
51+	}
52+
53+	return
54+}
55+
56+func (a *AllReaderAt) Read(p []byte) (int, error) {
57+	return a.Reader.Read(p)
58+}
59+
60+func (a *AllReaderAt) Close() error {
61+	return a.Reader.Close()
62+}
M wish/send/utils/utils.go
+2, -2
 1@@ -57,8 +57,8 @@ func octalPerms(info fs.FileMode) string {
 2 type CopyFromClientHandler interface {
 3 	// Write should write the given file.
 4 	Write(ssh.Session, *FileEntry) (string, error)
 5-	Read(ssh.Session, *FileEntry) (os.FileInfo, io.ReaderAt, error)
 6-	List(ssh.Session, string, bool) ([]os.FileInfo, error)
 7+	Read(ssh.Session, *FileEntry) (os.FileInfo, ReaderAtCloser, error)
 8+	List(ssh ssh.Session, path string, isDir bool, recursive bool) ([]os.FileInfo, error)
 9 	Validate(ssh.Session) error
10 }
11