- commit
- 028d33f
- parent
- c79df32
- author
- Antonio Mika
- date
- 2025-03-12 17:29:25 -0400 EDT
Refactor modules
222 files changed,
+3123,
-258
M
go.mod
+1,
-1
1@@ -1,6 +1,6 @@
2 package main
3
4-import "github.com/picosh/pico/auth"
5+import "github.com/picosh/pico/pkg/apps/auth"
6
7 func main() {
8 auth.StartApiServer()
+2,
-2
1@@ -4,8 +4,8 @@ import (
2 "fmt"
3
4 "github.com/mmcdole/gofeed"
5- "github.com/picosh/pico/db/postgres"
6- "github.com/picosh/pico/feeds"
7+ "github.com/picosh/pico/pkg/apps/feeds"
8+ "github.com/picosh/pico/pkg/db/postgres"
9 )
10
11 func main() {
+1,
-1
1@@ -1,6 +1,6 @@
2 package main
3
4-import "github.com/picosh/pico/feeds"
5+import "github.com/picosh/pico/pkg/apps/feeds"
6
7 func main() {
8 feeds.StartSshServer()
+1,
-1
1@@ -1,6 +1,6 @@
2 package main
3
4-import "github.com/picosh/pico/feeds"
5+import "github.com/picosh/pico/pkg/apps/feeds"
6
7 func main() {
8 feeds.StartApiServer()
+1,
-1
1@@ -1,6 +1,6 @@
2 package main
3
4-import "github.com/picosh/pico/pastes"
5+import "github.com/picosh/pico/pkg/apps/pastes"
6
7 func main() {
8 pastes.StartSshServer()
+1,
-1
1@@ -1,6 +1,6 @@
2 package main
3
4-import "github.com/picosh/pico/pastes"
5+import "github.com/picosh/pico/pkg/apps/pastes"
6
7 func main() {
8 pastes.StartApiServer()
+4,
-4
1@@ -1,10 +1,10 @@
2 package main
3
4 import (
5- "github.com/picosh/pico/pgs"
6- pgsdb "github.com/picosh/pico/pgs/db"
7- "github.com/picosh/pico/shared"
8- "github.com/picosh/pico/shared/storage"
9+ "github.com/picosh/pico/pkg/apps/pgs"
10+ pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
11+ "github.com/picosh/pico/pkg/shared"
12+ "github.com/picosh/pico/pkg/shared/storage"
13 "github.com/picosh/utils"
14 )
15
+4,
-4
1@@ -1,10 +1,10 @@
2 package main
3
4 import (
5- "github.com/picosh/pico/pgs"
6- pgsdb "github.com/picosh/pico/pgs/db"
7- "github.com/picosh/pico/shared"
8- "github.com/picosh/pico/shared/storage"
9+ "github.com/picosh/pico/pkg/apps/pgs"
10+ pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
11+ "github.com/picosh/pico/pkg/shared"
12+ "github.com/picosh/pico/pkg/shared/storage"
13 "github.com/picosh/utils"
14 )
15
+1,
-1
1@@ -1,6 +1,6 @@
2 package main
3
4-import "github.com/picosh/pico/pico"
5+import "github.com/picosh/pico/pkg/apps/pico"
6
7 func main() {
8 pico.StartSshServer()
+1,
-1
1@@ -1,6 +1,6 @@
2 package main
3
4-import "github.com/picosh/pico/pipe"
5+import "github.com/picosh/pico/pkg/apps/pipe"
6
7 func main() {
8 pipe.StartSshServer()
+1,
-1
1@@ -1,6 +1,6 @@
2 package main
3
4-import "github.com/picosh/pico/pipe"
5+import "github.com/picosh/pico/pkg/apps/pipe"
6
7 func main() {
8 pipe.StartApiServer()
+1,
-1
1@@ -1,6 +1,6 @@
2 package main
3
4-import "github.com/picosh/pico/prose"
5+import "github.com/picosh/pico/pkg/apps/prose"
6
7 func main() {
8 prose.StartSshServer()
+1,
-1
1@@ -1,6 +1,6 @@
2 package main
3
4-import "github.com/picosh/pico/prose"
5+import "github.com/picosh/pico/pkg/apps/prose"
6
7 func main() {
8 prose.StartApiServer()
+2,
-2
1@@ -4,8 +4,8 @@ import (
2 "log/slog"
3 "os"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/db/postgres"
7+ "github.com/picosh/pico/pkg/db"
8+ "github.com/picosh/pico/pkg/db/postgres"
9 "github.com/picosh/utils"
10 )
11
+2,
-2
1@@ -5,8 +5,8 @@ import (
2 "log/slog"
3 "os"
4
5- "github.com/picosh/pico/db/postgres"
6- "github.com/picosh/pico/shared"
7+ "github.com/picosh/pico/pkg/db/postgres"
8+ "github.com/picosh/pico/pkg/shared"
9 )
10
11 func main() {
1@@ -5,11 +5,11 @@ import (
2 "os"
3 "strings"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/pgs"
7- pgsdb "github.com/picosh/pico/pgs/db"
8- "github.com/picosh/pico/shared"
9- "github.com/picosh/pico/shared/storage"
10+ "github.com/picosh/pico/pkg/apps/pgs"
11+ pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
12+ "github.com/picosh/pico/pkg/db"
13+ "github.com/picosh/pico/pkg/shared"
14+ "github.com/picosh/pico/pkg/shared/storage"
15 "github.com/picosh/utils"
16 )
17
+3,
-3
1@@ -7,9 +7,9 @@ import (
2 "os"
3 "time"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/db/postgres"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/db/postgres"
10+ "github.com/picosh/pico/pkg/shared"
11 )
12
13 func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
+2,
-2
1@@ -5,8 +5,8 @@ import (
2 "log/slog"
3 "os"
4
5- "github.com/picosh/pico/db/postgres"
6- "github.com/picosh/pico/shared"
7+ "github.com/picosh/pico/pkg/db/postgres"
8+ "github.com/picosh/pico/pkg/shared"
9 )
10
11 func bail(err error) {
+3,
-3
1@@ -7,9 +7,9 @@ import (
2 "log/slog"
3 "os"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/db/postgres"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/db/postgres"
10+ "github.com/picosh/pico/pkg/shared"
11 )
12
13 func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
+1,
-1
1@@ -4,7 +4,7 @@ import (
2 "log/slog"
3 "os"
4
5- "github.com/picosh/pico/db/postgres"
6+ "github.com/picosh/pico/pkg/db/postgres"
7 )
8
9 func main() {
1@@ -7,13 +7,13 @@ import (
2 "path/filepath"
3 "time"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/db/postgres"
7- "github.com/picosh/pico/prose"
8- "github.com/picosh/pico/shared"
9- "github.com/picosh/pico/shared/storage"
10- sst "github.com/picosh/pobj/storage"
11- sendUtils "github.com/picosh/send/utils"
12+ "github.com/picosh/pico/pkg/apps/prose"
13+ "github.com/picosh/pico/pkg/db"
14+ "github.com/picosh/pico/pkg/db/postgres"
15+ sst "github.com/picosh/pico/pkg/pobj/storage"
16+ sendUtils "github.com/picosh/pico/pkg/send/utils"
17+ "github.com/picosh/pico/pkg/shared"
18+ "github.com/picosh/pico/pkg/shared/storage"
19 )
20
21 func bail(err error) {
1@@ -6,9 +6,9 @@ import (
2
3 "github.com/minio/minio-go/v7"
4 "github.com/minio/minio-go/v7/pkg/credentials"
5- "github.com/picosh/pico/db/postgres"
6- "github.com/picosh/pico/prose"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/apps/prose"
9+ "github.com/picosh/pico/pkg/db/postgres"
10+ "github.com/picosh/pico/pkg/shared"
11 )
12
13 func bail(err error) {
+2,
-2
1@@ -4,8 +4,8 @@ import (
2 "log/slog"
3 "os"
4
5- "github.com/picosh/pico/db/postgres"
6- "github.com/picosh/pico/shared"
7+ "github.com/picosh/pico/pkg/db/postgres"
8+ "github.com/picosh/pico/pkg/shared"
9 "github.com/picosh/utils"
10 )
11
1@@ -5,9 +5,9 @@ import (
2 "log/slog"
3 "os"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/db/postgres"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/db/postgres"
10+ "github.com/picosh/pico/pkg/shared"
11 )
12
13 func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
M
go.mod
+12,
-14
1@@ -4,13 +4,13 @@ go 1.24
2
3 toolchain go1.24.0
4
5-replace github.com/picosh/tunkit => ../tunkit
6+// replace github.com/picosh/tunkit => ../tunkit
7
8-replace github.com/picosh/send => ../send
9+// replace github.com/picosh/send => ../send
10
11 // replace github.com/picosh/go-rsync-receiver => ../go-rsync-receiver
12
13-replace github.com/picosh/pobj => ../pobj
14+// replace github.com/picosh/pobj => ../pobj
15
16 // replace github.com/picosh/pubsub => ../pubsub
17
18@@ -26,6 +26,12 @@ require (
19 github.com/alecthomas/chroma/v2 v2.14.0
20 github.com/antoniomika/syncmap v1.0.0
21 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
22+ github.com/aws/aws-sdk-go-v2 v1.36.2
23+ github.com/aws/aws-sdk-go-v2/config v1.29.7
24+ github.com/aws/aws-sdk-go-v2/credentials v1.17.60
25+ github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.63
26+ github.com/aws/aws-sdk-go-v2/service/s3 v1.77.1
27+ github.com/aws/smithy-go v1.22.3
28 github.com/containerd/console v1.0.4
29 github.com/darkweak/souin v1.7.5
30 github.com/darkweak/souin/plugins/souin/storages v1.7.5
31@@ -37,14 +43,14 @@ require (
32 github.com/gorilla/websocket v1.5.3
33 github.com/jmoiron/sqlx v1.4.0
34 github.com/lib/pq v1.10.9
35+ github.com/matryer/is v1.4.1
36 github.com/microcosm-cc/bluemonday v1.0.27
37+ github.com/minio/madmin-go/v3 v3.0.94
38 github.com/minio/minio-go/v7 v7.0.87
39 github.com/mmcdole/gofeed v1.3.0
40 github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577
41- github.com/picosh/pobj v0.0.0-20250304201248-a9c7179aa49b
42+ github.com/picosh/go-rsync-receiver v0.0.0-20250304201040-fcc11dd22d79
43 github.com/picosh/pubsub v0.0.0-20241114191831-ec8f16c0eb88
44- github.com/picosh/send v0.0.0-20250304201154-e36cd3bbbb35
45- github.com/picosh/tunkit v0.0.0-00010101000000-000000000000
46 github.com/picosh/utils v0.0.0-20241120033529-8ca070c09bf4
47 github.com/pkg/sftp v1.13.7
48 github.com/prometheus/client_golang v1.21.0-rc.0
49@@ -80,12 +86,8 @@ require (
50 github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
51 github.com/armon/go-metrics v0.4.1 // indirect
52 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
53- github.com/aws/aws-sdk-go-v2 v1.36.2 // indirect
54 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
55- github.com/aws/aws-sdk-go-v2/config v1.29.7 // indirect
56- github.com/aws/aws-sdk-go-v2/credentials v1.17.60 // indirect
57 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29 // indirect
58- github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.63 // indirect
59 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 // indirect
60 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 // indirect
61 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
62@@ -94,11 +96,9 @@ require (
63 github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.6.1 // indirect
64 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14 // indirect
65 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.14 // indirect
66- github.com/aws/aws-sdk-go-v2/service/s3 v1.77.1 // indirect
67 github.com/aws/aws-sdk-go-v2/service/sso v1.24.16 // indirect
68 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15 // indirect
69 github.com/aws/aws-sdk-go-v2/service/sts v1.33.15 // indirect
70- github.com/aws/smithy-go v1.22.3 // indirect
71 github.com/aymerick/douceur v0.2.0 // indirect
72 github.com/beorn7/perks v1.0.1 // indirect
73 github.com/bits-and-blooms/bitset v1.5.0 // indirect
74@@ -210,7 +210,6 @@ require (
75 github.com/mholt/acmez/v2 v2.0.1 // indirect
76 github.com/miekg/dns v1.1.63 // indirect
77 github.com/minio/crc64nvme v1.0.1 // indirect
78- github.com/minio/madmin-go/v3 v3.0.94 // indirect
79 github.com/minio/md5-simd v1.1.2 // indirect
80 github.com/mitchellh/copystructure v1.2.0 // indirect
81 github.com/mitchellh/go-ps v1.0.0 // indirect
82@@ -228,7 +227,6 @@ require (
83 github.com/nutsdb/nutsdb v1.0.4 // indirect
84 github.com/onsi/ginkgo/v2 v2.15.0 // indirect
85 github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
86- github.com/picosh/go-rsync-receiver v0.0.0-20250304201040-fcc11dd22d79 // indirect
87 github.com/pierrec/lz4/v4 v4.1.21 // indirect
88 github.com/pkg/errors v0.9.1 // indirect
89 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
R auth/__snapshots__/api_test.snap =>
pkg/apps/auth/__snapshots__/api_test.snap
+0,
-0
R auth/api.go =>
pkg/apps/auth/api.go
+3,
-3
1@@ -16,9 +16,9 @@ import (
2 "strings"
3 "time"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/db/postgres"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/db/postgres"
10+ "github.com/picosh/pico/pkg/shared"
11 "github.com/picosh/utils"
12 "github.com/picosh/utils/pipe/metrics"
13 "github.com/prometheus/client_golang/prometheus/promhttp"
R auth/api_test.go =>
pkg/apps/auth/api_test.go
+3,
-3
1@@ -12,9 +12,9 @@ import (
2 "time"
3
4 "github.com/gkampitakis/go-snaps/snaps"
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/db/stub"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/db/stub"
10+ "github.com/picosh/pico/pkg/shared"
11 )
12
13 var testUserID = "user-1"
R auth/html/base.layout.tmpl =>
pkg/apps/auth/html/base.layout.tmpl
+0,
-0
R auth/html/redirect.page.tmpl =>
pkg/apps/auth/html/redirect.page.tmpl
+0,
-0
R auth/public/apple-touch-icon.png =>
pkg/apps/auth/public/apple-touch-icon.png
+0,
-0
R auth/public/favicon-16x16.png =>
pkg/apps/auth/public/favicon-16x16.png
+0,
-0
R auth/public/favicon.ico =>
pkg/apps/auth/public/favicon.ico
+0,
-0
R auth/public/main.css =>
pkg/apps/auth/public/main.css
+0,
-0
R auth/public/robots.txt =>
pkg/apps/auth/public/robots.txt
+0,
-0
R feeds/api.go =>
pkg/apps/feeds/api.go
+2,
-2
1@@ -6,8 +6,8 @@ import (
2 "net/url"
3 "time"
4
5- "github.com/picosh/pico/db/postgres"
6- "github.com/picosh/pico/shared"
7+ "github.com/picosh/pico/pkg/db/postgres"
8+ "github.com/picosh/pico/pkg/shared"
9 "github.com/prometheus/client_golang/prometheus/promhttp"
10 )
11
R feeds/cli.go =>
pkg/apps/feeds/cli.go
+3,
-3
1@@ -5,9 +5,9 @@ import (
2 "text/tabwriter"
3 "time"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/pssh"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/pssh"
10+ "github.com/picosh/pico/pkg/shared"
11 )
12
13 func WishMiddleware(dbpool db.DB, cfg *shared.ConfigSite) pssh.SSHServerMiddleware {
R feeds/config.go =>
pkg/apps/feeds/config.go
+1,
-1
1@@ -1,7 +1,7 @@
2 package feeds
3
4 import (
5- "github.com/picosh/pico/shared"
6+ "github.com/picosh/pico/pkg/shared"
7 "github.com/picosh/utils"
8 )
9
R feeds/cron.go =>
pkg/apps/feeds/cron.go
+2,
-2
1@@ -15,8 +15,8 @@ import (
2 "time"
3
4 "github.com/mmcdole/gofeed"
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/shared"
7+ "github.com/picosh/pico/pkg/db"
8+ "github.com/picosh/pico/pkg/shared"
9 "github.com/sendgrid/sendgrid-go"
10 "github.com/sendgrid/sendgrid-go/helpers/mail"
11 )
R feeds/html/base.layout.tmpl =>
pkg/apps/feeds/html/base.layout.tmpl
+0,
-0
R feeds/html/digest.page.tmpl =>
pkg/apps/feeds/html/digest.page.tmpl
+0,
-0
R feeds/html/digest_text.page.tmpl =>
pkg/apps/feeds/html/digest_text.page.tmpl
+0,
-0
R feeds/html/marketing.page.tmpl =>
pkg/apps/feeds/html/marketing.page.tmpl
+0,
-0
R feeds/public/apple-touch-icon.png =>
pkg/apps/feeds/public/apple-touch-icon.png
+0,
-0
R feeds/public/card.png =>
pkg/apps/feeds/public/card.png
+0,
-0
R feeds/public/favicon-16x16.png =>
pkg/apps/feeds/public/favicon-16x16.png
+0,
-0
R feeds/public/favicon.ico =>
pkg/apps/feeds/public/favicon.ico
+0,
-0
R feeds/public/main.css =>
pkg/apps/feeds/public/main.css
+0,
-0
R feeds/public/robots.txt =>
pkg/apps/feeds/public/robots.txt
+0,
-0
R feeds/scp_hooks.go =>
pkg/apps/feeds/scp_hooks.go
+4,
-4
1@@ -8,10 +8,10 @@ import (
2 "strings"
3 "time"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/filehandlers"
7- "github.com/picosh/pico/pssh"
8- "github.com/picosh/pico/shared"
9+ "github.com/picosh/pico/pkg/db"
10+ "github.com/picosh/pico/pkg/filehandlers"
11+ "github.com/picosh/pico/pkg/pssh"
12+ "github.com/picosh/pico/pkg/shared"
13 "github.com/picosh/utils"
14 )
15
R feeds/ssh.go =>
pkg/apps/feeds/ssh.go
+10,
-10
1@@ -6,16 +6,16 @@ import (
2 "os/signal"
3 "syscall"
4
5- "github.com/picosh/pico/db/postgres"
6- "github.com/picosh/pico/filehandlers"
7- "github.com/picosh/pico/pssh"
8- "github.com/picosh/pico/shared"
9- "github.com/picosh/send/auth"
10- "github.com/picosh/send/list"
11- "github.com/picosh/send/pipe"
12- "github.com/picosh/send/protocols/rsync"
13- "github.com/picosh/send/protocols/scp"
14- "github.com/picosh/send/protocols/sftp"
15+ "github.com/picosh/pico/pkg/db/postgres"
16+ "github.com/picosh/pico/pkg/filehandlers"
17+ "github.com/picosh/pico/pkg/pssh"
18+ "github.com/picosh/pico/pkg/send/auth"
19+ "github.com/picosh/pico/pkg/send/list"
20+ "github.com/picosh/pico/pkg/send/pipe"
21+ "github.com/picosh/pico/pkg/send/protocols/rsync"
22+ "github.com/picosh/pico/pkg/send/protocols/scp"
23+ "github.com/picosh/pico/pkg/send/protocols/sftp"
24+ "github.com/picosh/pico/pkg/shared"
25 "github.com/picosh/utils"
26 "golang.org/x/crypto/ssh"
27 )
R pastes/api.go =>
pkg/apps/pastes/api.go
+3,
-3
1@@ -8,9 +8,9 @@ import (
2 "os"
3 "time"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/db/postgres"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/db/postgres"
10+ "github.com/picosh/pico/pkg/shared"
11 "github.com/picosh/utils"
12 "github.com/prometheus/client_golang/prometheus/promhttp"
13 )
R pastes/config.go =>
pkg/apps/pastes/config.go
+1,
-1
1@@ -1,7 +1,7 @@
2 package pastes
3
4 import (
5- "github.com/picosh/pico/shared"
6+ "github.com/picosh/pico/pkg/shared"
7 "github.com/picosh/utils"
8 )
9
R pastes/cron.go =>
pkg/apps/pastes/cron.go
+2,
-2
1@@ -3,8 +3,8 @@ package pastes
2 import (
3 "time"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/shared"
7+ "github.com/picosh/pico/pkg/db"
8+ "github.com/picosh/pico/pkg/shared"
9 )
10
11 func deleteExpiredPosts(cfg *shared.ConfigSite, dbpool db.DB) error {
R pastes/html/base.layout.tmpl =>
pkg/apps/pastes/html/base.layout.tmpl
+0,
-0
R pastes/html/blog.page.tmpl =>
pkg/apps/pastes/html/blog.page.tmpl
+0,
-0
R pastes/html/marketing.page.tmpl =>
pkg/apps/pastes/html/marketing.page.tmpl
+0,
-0
R pastes/html/post.page.tmpl =>
pkg/apps/pastes/html/post.page.tmpl
+0,
-0
R pastes/parser.go =>
pkg/apps/pastes/parser.go
+0,
-0
R pastes/public/apple-touch-icon.png =>
pkg/apps/pastes/public/apple-touch-icon.png
+0,
-0
R pastes/public/card.png =>
pkg/apps/pastes/public/card.png
+0,
-0
R pastes/public/favicon-16x16.png =>
pkg/apps/pastes/public/favicon-16x16.png
+0,
-0
R pastes/public/favicon.ico =>
pkg/apps/pastes/public/favicon.ico
+0,
-0
R pastes/public/main.css =>
pkg/apps/pastes/public/main.css
+0,
-0
R pastes/public/robots.txt =>
pkg/apps/pastes/public/robots.txt
+0,
-0
R pastes/public/smol.css =>
pkg/apps/pastes/public/smol.css
+0,
-0
R pastes/public/syntax.css =>
pkg/apps/pastes/public/syntax.css
+0,
-0
R pastes/scp_hooks.go =>
pkg/apps/pastes/scp_hooks.go
+4,
-4
1@@ -7,10 +7,10 @@ import (
2 "time"
3
4 "github.com/araddon/dateparse"
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/filehandlers"
7- "github.com/picosh/pico/pssh"
8- "github.com/picosh/pico/shared"
9+ "github.com/picosh/pico/pkg/db"
10+ "github.com/picosh/pico/pkg/filehandlers"
11+ "github.com/picosh/pico/pkg/pssh"
12+ "github.com/picosh/pico/pkg/shared"
13 "github.com/picosh/utils"
14 )
15
R pastes/ssh.go =>
pkg/apps/pastes/ssh.go
+10,
-10
1@@ -6,16 +6,16 @@ import (
2 "os/signal"
3 "syscall"
4
5- "github.com/picosh/pico/db/postgres"
6- "github.com/picosh/pico/filehandlers"
7- "github.com/picosh/pico/pssh"
8- "github.com/picosh/pico/shared"
9- "github.com/picosh/send/auth"
10- "github.com/picosh/send/list"
11- "github.com/picosh/send/pipe"
12- "github.com/picosh/send/protocols/rsync"
13- "github.com/picosh/send/protocols/scp"
14- "github.com/picosh/send/protocols/sftp"
15+ "github.com/picosh/pico/pkg/db/postgres"
16+ "github.com/picosh/pico/pkg/filehandlers"
17+ "github.com/picosh/pico/pkg/pssh"
18+ "github.com/picosh/pico/pkg/send/auth"
19+ "github.com/picosh/pico/pkg/send/list"
20+ "github.com/picosh/pico/pkg/send/pipe"
21+ "github.com/picosh/pico/pkg/send/protocols/rsync"
22+ "github.com/picosh/pico/pkg/send/protocols/scp"
23+ "github.com/picosh/pico/pkg/send/protocols/sftp"
24+ "github.com/picosh/pico/pkg/shared"
25 "github.com/picosh/utils"
26 "golang.org/x/crypto/ssh"
27 )
R pgs/access.go =>
pkg/apps/pgs/access.go
+1,
-1
1@@ -3,7 +3,7 @@ package pgs
2 import (
3 "slices"
4
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 "golang.org/x/crypto/ssh"
8 )
9
R pgs/calc_route.go =>
pkg/apps/pgs/calc_route.go
+3,
-3
1@@ -8,9 +8,9 @@ import (
2 "regexp"
3 "strings"
4
5- "github.com/picosh/pico/shared"
6- "github.com/picosh/pico/shared/storage"
7- "github.com/picosh/send/utils"
8+ "github.com/picosh/pico/pkg/send/utils"
9+ "github.com/picosh/pico/pkg/shared"
10+ "github.com/picosh/pico/pkg/shared/storage"
11 )
12
13 type HttpReply struct {
R pgs/calc_route_test.go =>
pkg/apps/pgs/calc_route_test.go
+0,
-0
R pgs/cli.go =>
pkg/apps/pgs/cli.go
+4,
-4
1@@ -11,10 +11,10 @@ import (
2 "text/tabwriter"
3 "time"
4
5- "github.com/picosh/pico/db"
6- pgsdb "github.com/picosh/pico/pgs/db"
7- "github.com/picosh/pico/shared"
8- sst "github.com/picosh/pobj/storage"
9+ pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
10+ "github.com/picosh/pico/pkg/db"
11+ sst "github.com/picosh/pico/pkg/pobj/storage"
12+ "github.com/picosh/pico/pkg/shared"
13 "github.com/picosh/utils"
14 )
15
R pgs/cli_wish.go =>
pkg/apps/pgs/cli_wish.go
+4,
-4
1@@ -6,10 +6,10 @@ import (
2 "slices"
3 "strings"
4
5- "github.com/picosh/pico/db"
6- pgsdb "github.com/picosh/pico/pgs/db"
7- "github.com/picosh/pico/pssh"
8- sendutils "github.com/picosh/send/utils"
9+ pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
10+ "github.com/picosh/pico/pkg/db"
11+ "github.com/picosh/pico/pkg/pssh"
12+ sendutils "github.com/picosh/pico/pkg/send/utils"
13 "github.com/picosh/utils"
14 )
15
R pgs/config.go =>
pkg/apps/pgs/config.go
+2,
-2
1@@ -6,8 +6,8 @@ import (
2 "path/filepath"
3 "time"
4
5- pgsdb "github.com/picosh/pico/pgs/db"
6- "github.com/picosh/pico/shared/storage"
7+ pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
8+ "github.com/picosh/pico/pkg/shared/storage"
9 "github.com/picosh/utils"
10 )
11
R pgs/db/db.go =>
pkg/apps/pgs/db/db.go
+1,
-1
1@@ -1,6 +1,6 @@
2 package pgsdb
3
4-import "github.com/picosh/pico/db"
5+import "github.com/picosh/pico/pkg/db"
6
7 type PgsDB interface {
8 FindUser(userID string) (*db.User, error)
R pgs/db/memory.go =>
pkg/apps/pgs/db/memory.go
+1,
-1
1@@ -6,7 +6,7 @@ import (
2 "time"
3
4 "github.com/google/uuid"
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/utils"
8 )
9
R pgs/db/postgres.go =>
pkg/apps/pgs/db/postgres.go
+1,
-1
1@@ -7,7 +7,7 @@ import (
2
3 "github.com/jmoiron/sqlx"
4 _ "github.com/lib/pq"
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/utils"
8 )
9
R pgs/fs.go =>
pkg/apps/pgs/fs.go
+0,
-0
R pgs/header.go =>
pkg/apps/pgs/header.go
+0,
-0
R pgs/header_test.go =>
pkg/apps/pgs/header_test.go
+0,
-0
R pgs/html/base.layout.tmpl =>
pkg/apps/pgs/html/base.layout.tmpl
+0,
-0
R pgs/html/marketing.page.tmpl =>
pkg/apps/pgs/html/marketing.page.tmpl
+0,
-0
R pgs/public/card.png =>
pkg/apps/pgs/public/card.png
+0,
-0
R pgs/public/favicon-16x16.png =>
pkg/apps/pgs/public/favicon-16x16.png
+0,
-0
R pgs/public/favicon.ico =>
pkg/apps/pgs/public/favicon.ico
+0,
-0
R pgs/public/main.css =>
pkg/apps/pgs/public/main.css
+0,
-0
R pgs/public/robots.txt =>
pkg/apps/pgs/public/robots.txt
+0,
-0
R pgs/redirect.go =>
pkg/apps/pgs/redirect.go
+0,
-0
R pgs/redirect_test.go =>
pkg/apps/pgs/redirect_test.go
+0,
-0
R pgs/ssh.go =>
pkg/apps/pgs/ssh.go
+9,
-9
1@@ -6,15 +6,15 @@ import (
2 "os/signal"
3 "syscall"
4
5- "github.com/picosh/pico/pssh"
6- "github.com/picosh/pico/shared"
7- "github.com/picosh/send/auth"
8- "github.com/picosh/send/list"
9- "github.com/picosh/send/pipe"
10- "github.com/picosh/send/protocols/rsync"
11- "github.com/picosh/send/protocols/scp"
12- "github.com/picosh/send/protocols/sftp"
13- "github.com/picosh/tunkit"
14+ "github.com/picosh/pico/pkg/pssh"
15+ "github.com/picosh/pico/pkg/send/auth"
16+ "github.com/picosh/pico/pkg/send/list"
17+ "github.com/picosh/pico/pkg/send/pipe"
18+ "github.com/picosh/pico/pkg/send/protocols/rsync"
19+ "github.com/picosh/pico/pkg/send/protocols/scp"
20+ "github.com/picosh/pico/pkg/send/protocols/sftp"
21+ "github.com/picosh/pico/pkg/shared"
22+ "github.com/picosh/pico/pkg/tunkit"
23 "github.com/picosh/utils"
24 "golang.org/x/crypto/ssh"
25 )
R pgs/ssh_test.go =>
pkg/apps/pgs/ssh_test.go
+3,
-3
1@@ -14,9 +14,9 @@ import (
2 "testing"
3 "time"
4
5- "github.com/picosh/pico/db"
6- pgsdb "github.com/picosh/pico/pgs/db"
7- "github.com/picosh/pico/shared/storage"
8+ pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
9+ "github.com/picosh/pico/pkg/db"
10+ "github.com/picosh/pico/pkg/shared/storage"
11 "github.com/picosh/utils"
12 "github.com/pkg/sftp"
13 "github.com/prometheus/client_golang/prometheus"
R pgs/tunnel.go =>
pkg/apps/pgs/tunnel.go
+3,
-3
1@@ -6,9 +6,9 @@ import (
2 "strings"
3 "time"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/pssh"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/pssh"
10+ "github.com/picosh/pico/pkg/shared"
11 "golang.org/x/crypto/ssh"
12 )
13
R pgs/uploader.go =>
pkg/apps/pgs/uploader.go
+7,
-7
1@@ -15,13 +15,13 @@ import (
2 "sync"
3 "time"
4
5- "github.com/picosh/pico/db"
6- pgsdb "github.com/picosh/pico/pgs/db"
7- "github.com/picosh/pico/pssh"
8- "github.com/picosh/pico/shared"
9- "github.com/picosh/pobj"
10- sst "github.com/picosh/pobj/storage"
11- sendutils "github.com/picosh/send/utils"
12+ pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
13+ "github.com/picosh/pico/pkg/db"
14+ "github.com/picosh/pico/pkg/pobj"
15+ sst "github.com/picosh/pico/pkg/pobj/storage"
16+ "github.com/picosh/pico/pkg/pssh"
17+ sendutils "github.com/picosh/pico/pkg/send/utils"
18+ "github.com/picosh/pico/pkg/shared"
19 "github.com/picosh/utils"
20 ignore "github.com/sabhiram/go-gitignore"
21 )
R pgs/web.go =>
pkg/apps/pgs/web.go
+4,
-4
1@@ -20,10 +20,10 @@ import (
2 "github.com/darkweak/souin/plugins/souin/storages"
3 "github.com/darkweak/storages/core"
4 "github.com/gorilla/feeds"
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/shared"
7- "github.com/picosh/pico/shared/storage"
8- sst "github.com/picosh/pobj/storage"
9+ "github.com/picosh/pico/pkg/db"
10+ sst "github.com/picosh/pico/pkg/pobj/storage"
11+ "github.com/picosh/pico/pkg/shared"
12+ "github.com/picosh/pico/pkg/shared/storage"
13 "github.com/prometheus/client_golang/prometheus/promhttp"
14 "google.golang.org/protobuf/proto"
15 )
R pgs/web_asset_handler.go =>
pkg/apps/pgs/web_asset_handler.go
+2,
-2
1@@ -14,8 +14,8 @@ import (
2 "net/http/httputil"
3 _ "net/http/pprof"
4
5- "github.com/picosh/pico/shared/storage"
6- sst "github.com/picosh/pobj/storage"
7+ sst "github.com/picosh/pico/pkg/pobj/storage"
8+ "github.com/picosh/pico/pkg/shared/storage"
9 )
10
11 type ApiAssetHandler struct {
R pgs/web_cache.go =>
pkg/apps/pgs/web_cache.go
+1,
-1
1@@ -6,7 +6,7 @@ import (
2 "log/slog"
3 "time"
4
5- "github.com/picosh/pico/shared"
6+ "github.com/picosh/pico/pkg/shared"
7 "github.com/picosh/utils/pipe"
8 )
9
R pgs/web_test.go =>
pkg/apps/pgs/web_test.go
+4,
-4
1@@ -10,10 +10,10 @@ import (
2 "testing"
3 "time"
4
5- pgsdb "github.com/picosh/pico/pgs/db"
6- "github.com/picosh/pico/shared"
7- "github.com/picosh/pico/shared/storage"
8- sst "github.com/picosh/pobj/storage"
9+ pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
10+ sst "github.com/picosh/pico/pkg/pobj/storage"
11+ "github.com/picosh/pico/pkg/shared"
12+ "github.com/picosh/pico/pkg/shared/storage"
13 )
14
15 type ApiExample struct {
R pico/cli.go =>
pkg/apps/pico/cli.go
+3,
-3
1@@ -8,9 +8,9 @@ import (
2 "log/slog"
3 "strings"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/pssh"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/pssh"
10+ "github.com/picosh/pico/pkg/shared"
11 "github.com/picosh/utils"
12
13 pipeLogger "github.com/picosh/utils/pipe/log"
R pico/config.go =>
pkg/apps/pico/config.go
+1,
-1
1@@ -1,7 +1,7 @@
2 package pico
3
4 import (
5- "github.com/picosh/pico/shared"
6+ "github.com/picosh/pico/pkg/shared"
7 "github.com/picosh/utils"
8 )
9
R pico/file_handler.go =>
pkg/apps/pico/file_handler.go
+4,
-4
1@@ -11,10 +11,10 @@ import (
2 "strings"
3 "time"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/pssh"
7- "github.com/picosh/pico/shared"
8- sendutils "github.com/picosh/send/utils"
9+ "github.com/picosh/pico/pkg/db"
10+ "github.com/picosh/pico/pkg/pssh"
11+ sendutils "github.com/picosh/pico/pkg/send/utils"
12+ "github.com/picosh/pico/pkg/shared"
13 "github.com/picosh/utils"
14 "golang.org/x/crypto/ssh"
15 )
R pico/html/.gitkeep =>
pkg/apps/pico/html/.gitkeep
+0,
-0
R pico/ssh.go =>
pkg/apps/pico/ssh.go
+10,
-10
1@@ -7,16 +7,16 @@ import (
2 "syscall"
3
4 "git.sr.ht/~rockorager/vaxis"
5- "github.com/picosh/pico/db/postgres"
6- "github.com/picosh/pico/pssh"
7- "github.com/picosh/pico/shared"
8- "github.com/picosh/pico/tui"
9- "github.com/picosh/send/auth"
10- "github.com/picosh/send/list"
11- "github.com/picosh/send/pipe"
12- "github.com/picosh/send/protocols/rsync"
13- "github.com/picosh/send/protocols/scp"
14- "github.com/picosh/send/protocols/sftp"
15+ "github.com/picosh/pico/pkg/db/postgres"
16+ "github.com/picosh/pico/pkg/pssh"
17+ "github.com/picosh/pico/pkg/send/auth"
18+ "github.com/picosh/pico/pkg/send/list"
19+ "github.com/picosh/pico/pkg/send/pipe"
20+ "github.com/picosh/pico/pkg/send/protocols/rsync"
21+ "github.com/picosh/pico/pkg/send/protocols/scp"
22+ "github.com/picosh/pico/pkg/send/protocols/sftp"
23+ "github.com/picosh/pico/pkg/shared"
24+ "github.com/picosh/pico/pkg/tui"
25 "github.com/picosh/utils"
26 "golang.org/x/crypto/ssh"
27 )
R pipe/api.go =>
pkg/apps/pipe/api.go
+2,
-2
1@@ -16,8 +16,8 @@ import (
2
3 "github.com/google/uuid"
4 "github.com/gorilla/websocket"
5- "github.com/picosh/pico/db/postgres"
6- "github.com/picosh/pico/shared"
7+ "github.com/picosh/pico/pkg/db/postgres"
8+ "github.com/picosh/pico/pkg/shared"
9 "github.com/picosh/utils/pipe"
10 "github.com/prometheus/client_golang/prometheus/promhttp"
11 )
R pipe/cli.go =>
pkg/apps/pipe/cli.go
+3,
-3
1@@ -14,9 +14,9 @@ import (
2
3 "github.com/antoniomika/syncmap"
4 "github.com/google/uuid"
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/pssh"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/pssh"
10+ "github.com/picosh/pico/pkg/shared"
11 psub "github.com/picosh/pubsub"
12 gossh "golang.org/x/crypto/ssh"
13 )
R pipe/config.go =>
pkg/apps/pipe/config.go
+1,
-1
1@@ -1,7 +1,7 @@
2 package pipe
3
4 import (
5- "github.com/picosh/pico/shared"
6+ "github.com/picosh/pico/pkg/shared"
7 "github.com/picosh/utils"
8 )
9
R pipe/html/base.layout.tmpl =>
pkg/apps/pipe/html/base.layout.tmpl
+0,
-0
R pipe/html/marketing.page.tmpl =>
pkg/apps/pipe/html/marketing.page.tmpl
+0,
-0
R pipe/public/anim.js =>
pkg/apps/pipe/public/anim.js
+0,
-0
R pipe/public/apple-touch-icon.png =>
pkg/apps/pipe/public/apple-touch-icon.png
+0,
-0
R pipe/public/favicon-16x16.png =>
pkg/apps/pipe/public/favicon-16x16.png
+0,
-0
R pipe/public/favicon.ico =>
pkg/apps/pipe/public/favicon.ico
+0,
-0
R pipe/public/robots.txt =>
pkg/apps/pipe/public/robots.txt
+0,
-0
R pipe/ssh.go =>
pkg/apps/pipe/ssh.go
+3,
-3
1@@ -7,9 +7,9 @@ import (
2 "syscall"
3
4 "github.com/antoniomika/syncmap"
5- "github.com/picosh/pico/db/postgres"
6- "github.com/picosh/pico/pssh"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/db/postgres"
9+ "github.com/picosh/pico/pkg/pssh"
10+ "github.com/picosh/pico/pkg/shared"
11 psub "github.com/picosh/pubsub"
12 "github.com/picosh/utils"
13 "golang.org/x/crypto/ssh"
R prose/api.go =>
pkg/apps/prose/api.go
+4,
-4
1@@ -15,10 +15,10 @@ import (
2 "slices"
3
4 "github.com/gorilla/feeds"
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/db/postgres"
7- "github.com/picosh/pico/shared"
8- "github.com/picosh/pico/shared/storage"
9+ "github.com/picosh/pico/pkg/db"
10+ "github.com/picosh/pico/pkg/db/postgres"
11+ "github.com/picosh/pico/pkg/shared"
12+ "github.com/picosh/pico/pkg/shared/storage"
13 "github.com/picosh/utils"
14 "github.com/prometheus/client_golang/prometheus/promhttp"
15 )
R prose/artifacts/main.css =>
pkg/apps/prose/artifacts/main.css
+0,
-0
R prose/config.go =>
pkg/apps/prose/config.go
+1,
-1
1@@ -1,7 +1,7 @@
2 package prose
3
4 import (
5- "github.com/picosh/pico/shared"
6+ "github.com/picosh/pico/pkg/shared"
7 "github.com/picosh/utils"
8 )
9
R prose/html/base.layout.tmpl =>
pkg/apps/prose/html/base.layout.tmpl
+0,
-0
R prose/html/blog-aside.partial.tmpl =>
pkg/apps/prose/html/blog-aside.partial.tmpl
+0,
-0
R prose/html/blog-default.partial.tmpl =>
pkg/apps/prose/html/blog-default.partial.tmpl
+0,
-0
R prose/html/blog.page.tmpl =>
pkg/apps/prose/html/blog.page.tmpl
+0,
-0
R prose/html/imgs.page.tmpl =>
pkg/apps/prose/html/imgs.page.tmpl
+0,
-0
R prose/html/post.page.tmpl =>
pkg/apps/prose/html/post.page.tmpl
+0,
-0
R prose/html/read.page.tmpl =>
pkg/apps/prose/html/read.page.tmpl
+0,
-0
R prose/html/rss.page.tmpl =>
pkg/apps/prose/html/rss.page.tmpl
+0,
-0
R prose/public/card.png =>
pkg/apps/prose/public/card.png
+0,
-0
R prose/public/favicon-16x16.png =>
pkg/apps/prose/public/favicon-16x16.png
+0,
-0
R prose/public/favicon.ico =>
pkg/apps/prose/public/favicon.ico
+0,
-0
R prose/public/robots.txt =>
pkg/apps/prose/public/robots.txt
+0,
-0
R prose/public/smol-v2.css =>
pkg/apps/prose/public/smol-v2.css
+0,
-0
R prose/public/smol.css =>
pkg/apps/prose/public/smol.css
+0,
-0
R prose/public/syntax.css =>
pkg/apps/prose/public/syntax.css
+0,
-0
R prose/scp_hooks.go =>
pkg/apps/prose/scp_hooks.go
+4,
-4
1@@ -6,10 +6,10 @@ import (
2
3 "slices"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/filehandlers"
7- "github.com/picosh/pico/pssh"
8- "github.com/picosh/pico/shared"
9+ "github.com/picosh/pico/pkg/db"
10+ "github.com/picosh/pico/pkg/filehandlers"
11+ "github.com/picosh/pico/pkg/pssh"
12+ "github.com/picosh/pico/pkg/shared"
13 "github.com/picosh/utils"
14 pipeUtil "github.com/picosh/utils/pipe"
15 )
R prose/ssh.go =>
pkg/apps/prose/ssh.go
+12,
-12
1@@ -6,18 +6,18 @@ import (
2 "os/signal"
3 "syscall"
4
5- "github.com/picosh/pico/db/postgres"
6- "github.com/picosh/pico/filehandlers"
7- uploadimgs "github.com/picosh/pico/filehandlers/imgs"
8- "github.com/picosh/pico/pssh"
9- "github.com/picosh/pico/shared"
10- "github.com/picosh/pico/shared/storage"
11- "github.com/picosh/send/auth"
12- "github.com/picosh/send/list"
13- "github.com/picosh/send/pipe"
14- "github.com/picosh/send/protocols/rsync"
15- "github.com/picosh/send/protocols/scp"
16- "github.com/picosh/send/protocols/sftp"
17+ "github.com/picosh/pico/pkg/db/postgres"
18+ "github.com/picosh/pico/pkg/filehandlers"
19+ uploadimgs "github.com/picosh/pico/pkg/filehandlers/imgs"
20+ "github.com/picosh/pico/pkg/pssh"
21+ "github.com/picosh/pico/pkg/send/auth"
22+ "github.com/picosh/pico/pkg/send/list"
23+ "github.com/picosh/pico/pkg/send/pipe"
24+ "github.com/picosh/pico/pkg/send/protocols/rsync"
25+ "github.com/picosh/pico/pkg/send/protocols/scp"
26+ "github.com/picosh/pico/pkg/send/protocols/sftp"
27+ "github.com/picosh/pico/pkg/shared"
28+ "github.com/picosh/pico/pkg/shared/storage"
29 "github.com/picosh/utils"
30 "golang.org/x/crypto/ssh"
31 )
R db/db.go =>
pkg/db/db.go
+0,
-0
R db/postgres/storage.go =>
pkg/db/postgres/storage.go
+1,
-1
1@@ -13,7 +13,7 @@ import (
2 "slices"
3
4 _ "github.com/lib/pq"
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/utils"
8 )
9
R db/stub/stub.go =>
pkg/db/stub/stub.go
+1,
-1
1@@ -6,7 +6,7 @@ import (
2 "log/slog"
3 "time"
4
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 )
8
9 type StubDB struct {
R db/util.go =>
pkg/db/util.go
+0,
-0
R filehandlers/imgs/handler.go =>
pkg/filehandlers/imgs/handler.go
+7,
-7
1@@ -12,13 +12,13 @@ import (
2 "strings"
3
4 exifremove "github.com/neurosnap/go-exif-remove"
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/pssh"
7- "github.com/picosh/pico/shared"
8- "github.com/picosh/pico/shared/storage"
9- "github.com/picosh/pobj"
10- sst "github.com/picosh/pobj/storage"
11- sendutils "github.com/picosh/send/utils"
12+ "github.com/picosh/pico/pkg/db"
13+ "github.com/picosh/pico/pkg/pobj"
14+ sst "github.com/picosh/pico/pkg/pobj/storage"
15+ "github.com/picosh/pico/pkg/pssh"
16+ sendutils "github.com/picosh/pico/pkg/send/utils"
17+ "github.com/picosh/pico/pkg/shared"
18+ "github.com/picosh/pico/pkg/shared/storage"
19 "github.com/picosh/utils"
20 )
21
R filehandlers/post_handler.go =>
pkg/filehandlers/post_handler.go
+4,
-4
1@@ -10,10 +10,10 @@ import (
2 "strings"
3 "time"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/pssh"
7- "github.com/picosh/pico/shared"
8- sendutils "github.com/picosh/send/utils"
9+ "github.com/picosh/pico/pkg/db"
10+ "github.com/picosh/pico/pkg/pssh"
11+ sendutils "github.com/picosh/pico/pkg/send/utils"
12+ "github.com/picosh/pico/pkg/shared"
13 "github.com/picosh/utils"
14 )
15
R filehandlers/router_handler.go =>
pkg/filehandlers/router_handler.go
+4,
-4
1@@ -8,10 +8,10 @@ import (
2 "os"
3 "path/filepath"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/pssh"
7- "github.com/picosh/pico/shared"
8- "github.com/picosh/send/utils"
9+ "github.com/picosh/pico/pkg/db"
10+ "github.com/picosh/pico/pkg/pssh"
11+ "github.com/picosh/pico/pkg/send/utils"
12+ "github.com/picosh/pico/pkg/shared"
13 )
14
15 type ReadWriteHandler interface {
+45,
-0
1@@ -0,0 +1,45 @@
2+package pobj
3+
4+import (
5+ "fmt"
6+
7+ "github.com/picosh/pico/pkg/pssh"
8+ "github.com/picosh/pico/pkg/send/utils"
9+)
10+
11+type AssetNames interface {
12+ BucketName(sesh *pssh.SSHServerConnSession) (string, error)
13+ ObjectName(sesh *pssh.SSHServerConnSession, entry *utils.FileEntry) (string, error)
14+ PrintObjectName(sesh *pssh.SSHServerConnSession, entry *utils.FileEntry, bucketName string) (string, error)
15+}
16+
17+type AssetNamesBasic struct{}
18+
19+var _ AssetNames = &AssetNamesBasic{}
20+var _ AssetNames = (*AssetNamesBasic)(nil)
21+
22+func (an *AssetNamesBasic) BucketName(sesh *pssh.SSHServerConnSession) (string, error) {
23+ return sesh.User(), nil
24+}
25+func (an *AssetNamesBasic) ObjectName(sesh *pssh.SSHServerConnSession, entry *utils.FileEntry) (string, error) {
26+ return entry.Filepath, nil
27+}
28+func (an *AssetNamesBasic) PrintObjectName(sesh *pssh.SSHServerConnSession, entry *utils.FileEntry, bucketName string) (string, error) {
29+ objectName, err := an.ObjectName(sesh, entry)
30+ if err != nil {
31+ return "", err
32+ }
33+ return fmt.Sprintf("%s%s", bucketName, objectName), nil
34+}
35+
36+type AssetNamesForceBucket struct {
37+ *AssetNamesBasic
38+ Name string
39+}
40+
41+var _ AssetNames = &AssetNamesForceBucket{}
42+var _ AssetNames = (*AssetNamesForceBucket)(nil)
43+
44+func (an *AssetNamesForceBucket) BucketName(sesh *pssh.SSHServerConnSession) (string, error) {
45+ return an.Name, nil
46+}
+269,
-0
1@@ -0,0 +1,269 @@
2+package pobj
3+
4+import (
5+ "bytes"
6+ "encoding/binary"
7+ "fmt"
8+ "io"
9+ "log/slog"
10+ "os"
11+ "path/filepath"
12+ "time"
13+
14+ "github.com/picosh/pico/pkg/pobj/storage"
15+ "github.com/picosh/pico/pkg/pssh"
16+ "github.com/picosh/pico/pkg/send/utils"
17+)
18+
19+type ctxBucketKey struct{}
20+
21+func getBucket(ctx *pssh.SSHServerConnSession) (storage.Bucket, error) {
22+ bucket, ok := ctx.Value(ctxBucketKey{}).(storage.Bucket)
23+ if !ok {
24+ return bucket, fmt.Errorf("bucket not set on `ssh.Context()` for connection")
25+ }
26+ if bucket.Name == "" {
27+ return bucket, fmt.Errorf("bucket not set on `ssh.Context()` for connection")
28+ }
29+ return bucket, nil
30+}
31+func setBucket(ctx *pssh.SSHServerConnSession, bucket storage.Bucket) {
32+ ctx.SetValue(ctxBucketKey{}, bucket)
33+}
34+
35+type FileData struct {
36+ *utils.FileEntry
37+ Text []byte
38+ User string
39+ Bucket storage.Bucket
40+}
41+
42+type Config struct {
43+ Logger *slog.Logger
44+ Storage storage.ObjectStorage
45+ AssetNames AssetNames
46+}
47+
48+type UploadAssetHandler struct {
49+ Cfg *Config
50+}
51+
52+var _ utils.CopyFromClientHandler = &UploadAssetHandler{}
53+var _ utils.CopyFromClientHandler = (*UploadAssetHandler)(nil)
54+
55+func NewUploadAssetHandler(cfg *Config) *UploadAssetHandler {
56+ if cfg.AssetNames == nil {
57+ cfg.AssetNames = &AssetNamesBasic{}
58+ }
59+
60+ return &UploadAssetHandler{
61+ Cfg: cfg,
62+ }
63+}
64+
65+func (h *UploadAssetHandler) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
66+ return h.Cfg.Logger
67+}
68+
69+func (h *UploadAssetHandler) Delete(s *pssh.SSHServerConnSession, entry *utils.FileEntry) error {
70+ h.Cfg.Logger.Info("deleting file", "file", entry.Filepath)
71+ bucket, err := getBucket(s)
72+ if err != nil {
73+ h.Cfg.Logger.Error(err.Error())
74+ return err
75+ }
76+
77+ objectFileName, err := h.Cfg.AssetNames.ObjectName(s, entry)
78+ if err != nil {
79+ return err
80+ }
81+ return h.Cfg.Storage.DeleteObject(bucket, objectFileName)
82+}
83+
84+func (h *UploadAssetHandler) Read(s *pssh.SSHServerConnSession, entry *utils.FileEntry) (os.FileInfo, utils.ReadAndReaderAtCloser, error) {
85+ fileInfo := &utils.VirtualFile{
86+ FName: filepath.Base(entry.Filepath),
87+ FIsDir: false,
88+ FSize: entry.Size,
89+ FModTime: time.Unix(entry.Mtime, 0),
90+ }
91+ h.Cfg.Logger.Info("reading file", "file", fileInfo)
92+
93+ bucketName, err := h.Cfg.AssetNames.BucketName(s)
94+ if err != nil {
95+ return nil, nil, err
96+ }
97+ bucket, err := h.Cfg.Storage.GetBucket(bucketName)
98+ if err != nil {
99+ return nil, nil, err
100+ }
101+
102+ fname, err := h.Cfg.AssetNames.ObjectName(s, entry)
103+ if err != nil {
104+ return nil, nil, err
105+ }
106+ contents, info, err := h.Cfg.Storage.GetObject(bucket, fname)
107+ if err != nil {
108+ return nil, nil, err
109+ }
110+
111+ fileInfo.FSize = info.Size
112+ fileInfo.FModTime = info.LastModified
113+
114+ reader := NewAllReaderAt(contents)
115+
116+ return fileInfo, reader, nil
117+}
118+
119+func (h *UploadAssetHandler) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
120+ h.Cfg.Logger.Info(
121+ "listing path",
122+ "dir", fpath,
123+ "isDir", isDir,
124+ "recursive", recursive,
125+ )
126+ var fileList []os.FileInfo
127+
128+ cleanFilename := fpath
129+
130+ bucketName, err := h.Cfg.AssetNames.BucketName(s)
131+ if err != nil {
132+ return fileList, err
133+ }
134+ bucket, err := h.Cfg.Storage.GetBucket(bucketName)
135+ if err != nil {
136+ return fileList, err
137+ }
138+
139+ fname, err := h.Cfg.AssetNames.ObjectName(s, &utils.FileEntry{Filepath: cleanFilename})
140+ if err != nil {
141+ return fileList, err
142+ }
143+
144+ if fname == "" || fname == "." {
145+ name := fname
146+ if name == "" {
147+ name = "/"
148+ }
149+
150+ info := &utils.VirtualFile{
151+ FName: name,
152+ FIsDir: true,
153+ }
154+
155+ fileList = append(fileList, info)
156+ } else {
157+ name := fname
158+ if name != "/" && isDir {
159+ name += "/"
160+ }
161+
162+ foundList, err := h.Cfg.Storage.ListObjects(bucket, name, recursive)
163+ if err != nil {
164+ return fileList, err
165+ }
166+
167+ fileList = append(fileList, foundList...)
168+ }
169+
170+ return fileList, nil
171+}
172+
173+func (h *UploadAssetHandler) Validate(s *pssh.SSHServerConnSession) error {
174+ var err error
175+ userName := s.User()
176+
177+ assetBucket, err := h.Cfg.AssetNames.BucketName(s)
178+ if err != nil {
179+ return err
180+ }
181+ bucket, err := h.Cfg.Storage.UpsertBucket(assetBucket)
182+ if err != nil {
183+ return err
184+ }
185+ setBucket(s, bucket)
186+
187+ pk, _ := utils.KeyText(s)
188+ h.Cfg.Logger.Info(
189+ "attempting to upload files",
190+ "user", userName,
191+ "bucket", bucket.Name,
192+ "publicKey", pk,
193+ )
194+ return nil
195+}
196+
197+func (h *UploadAssetHandler) Write(s *pssh.SSHServerConnSession, entry *utils.FileEntry) (string, error) {
198+ var origText []byte
199+ if b, err := io.ReadAll(entry.Reader); err == nil {
200+ origText = b
201+ }
202+ fileSize := binary.Size(origText)
203+ // TODO: hack for now until I figure out how to get correct
204+ // filesize from sftp,scp,rsync
205+ entry.Size = int64(fileSize)
206+ userName := s.User()
207+
208+ bucket, err := getBucket(s)
209+ if err != nil {
210+ h.Cfg.Logger.Error(err.Error())
211+ return "", err
212+ }
213+
214+ data := &FileData{
215+ FileEntry: entry,
216+ User: userName,
217+ Text: origText,
218+ Bucket: bucket,
219+ }
220+ err = h.writeAsset(s, data)
221+ if err != nil {
222+ h.Cfg.Logger.Error(err.Error())
223+ return "", err
224+ }
225+
226+ url, err := h.Cfg.AssetNames.PrintObjectName(s, entry, bucket.Name)
227+ if err != nil {
228+ return "", err
229+ }
230+ return url, nil
231+}
232+
233+func (h *UploadAssetHandler) validateAsset(_ *FileData) (bool, error) {
234+ return true, nil
235+}
236+
237+func (h *UploadAssetHandler) writeAsset(s *pssh.SSHServerConnSession, data *FileData) error {
238+ valid, err := h.validateAsset(data)
239+ if !valid {
240+ return err
241+ }
242+
243+ objectFileName, err := h.Cfg.AssetNames.ObjectName(s, data.FileEntry)
244+ if err != nil {
245+ return err
246+ }
247+ reader := bytes.NewReader(data.Text)
248+
249+ h.Cfg.Logger.Info(
250+ "uploading file to bucket",
251+ "user",
252+ data.User,
253+ "bucket",
254+ data.Bucket.Name,
255+ "object",
256+ objectFileName,
257+ )
258+
259+ _, _, err = h.Cfg.Storage.PutObject(
260+ data.Bucket,
261+ objectFileName,
262+ utils.NopReadAndReaderAtCloser(reader),
263+ data.FileEntry,
264+ )
265+ if err != nil {
266+ return err
267+ }
268+
269+ return nil
270+}
+25,
-0
1@@ -0,0 +1,25 @@
2+package pobj
3+
4+// func createRouter(handler utils.CopyFromClientHandler) proxy.Router {
5+// return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
6+// return []wish.Middleware{
7+// pipe.Middleware(handler, ""),
8+// list.Middleware(handler),
9+// scp.Middleware(handler),
10+// wishrsync.Middleware(handler),
11+// auth.Middleware(handler),
12+// lm.Middleware(),
13+// }
14+// }
15+// }
16+
17+// func WithProxy(handler utils.CopyFromClientHandler, otherMiddleware ...wish.Middleware) ssh.Option {
18+// return func(server *ssh.Server) error {
19+// err := sftp.SSHOption(handler)(server)
20+// if err != nil {
21+// return err
22+// }
23+
24+// return proxy.WithProxy(createRouter(handler), otherMiddleware...)(server)
25+// }
26+// }
+42,
-0
1@@ -0,0 +1,42 @@
2+package pobj
3+
4+import (
5+ "errors"
6+ "io"
7+ "net/http"
8+
9+ "github.com/minio/minio-go/v7"
10+ "github.com/picosh/pico/pkg/send/utils"
11+)
12+
13+type AllReaderAt struct {
14+ Reader utils.ReadAndReaderAtCloser
15+}
16+
17+func NewAllReaderAt(reader utils.ReadAndReaderAtCloser) *AllReaderAt {
18+ return &AllReaderAt{reader}
19+}
20+
21+func (a *AllReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
22+ n, err = a.Reader.ReadAt(p, off)
23+
24+ if errors.Is(err, io.EOF) {
25+ return
26+ }
27+
28+ resp := minio.ToErrorResponse(err)
29+
30+ if resp.StatusCode == http.StatusRequestedRangeNotSatisfiable {
31+ err = io.EOF
32+ }
33+
34+ return
35+}
36+
37+func (a *AllReaderAt) Read(p []byte) (int, error) {
38+ return a.Reader.Read(p)
39+}
40+
41+func (a *AllReaderAt) Close() error {
42+ return a.Reader.Close()
43+}
+218,
-0
1@@ -0,0 +1,218 @@
2+package storage
3+
4+import (
5+ "fmt"
6+ "io"
7+ "io/fs"
8+ "log/slog"
9+ "os"
10+ "path"
11+ "path/filepath"
12+ "strings"
13+ "time"
14+
15+ "github.com/picosh/pico/pkg/send/utils"
16+)
17+
18+// https://stackoverflow.com/a/32482941
19+func dirSize(path string) (int64, error) {
20+ var size int64
21+ err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
22+ if err != nil {
23+ return err
24+ }
25+ if !info.IsDir() {
26+ size += info.Size()
27+ }
28+ return err
29+ })
30+
31+ return size, err
32+}
33+
34+type StorageFS struct {
35+ Dir string
36+ Logger *slog.Logger
37+}
38+
39+var _ ObjectStorage = &StorageFS{}
40+var _ ObjectStorage = (*StorageFS)(nil)
41+
42+func NewStorageFS(logger *slog.Logger, dir string) (*StorageFS, error) {
43+ return &StorageFS{Logger: logger, Dir: dir}, nil
44+}
45+
46+func (s *StorageFS) GetBucket(name string) (Bucket, error) {
47+ dirPath := filepath.Join(s.Dir, name)
48+ bucket := Bucket{
49+ Name: name,
50+ Path: dirPath,
51+ }
52+ s.Logger.Info("get bucket", "dir", dirPath)
53+
54+ info, err := os.Stat(dirPath)
55+ if os.IsNotExist(err) {
56+ return bucket, fmt.Errorf("directory does not exist: %v %w", dirPath, err)
57+ }
58+
59+ if err != nil {
60+ return bucket, fmt.Errorf("directory error: %v %w", dirPath, err)
61+
62+ }
63+
64+ if !info.IsDir() {
65+ return bucket, fmt.Errorf("directory is a file, not a directory: %#v", dirPath)
66+ }
67+
68+ return bucket, nil
69+}
70+
71+func (s *StorageFS) UpsertBucket(name string) (Bucket, error) {
72+ s.Logger.Info("upsert bucket", "name", name)
73+ bucket, err := s.GetBucket(name)
74+ if err == nil {
75+ return bucket, nil
76+ }
77+
78+ dir := filepath.Join(s.Dir, bucket.Path)
79+ s.Logger.Info("bucket not found, creating", "dir", dir, "err", err)
80+ err = os.MkdirAll(dir, os.ModePerm)
81+ if err != nil {
82+ return bucket, err
83+ }
84+
85+ return bucket, nil
86+}
87+
88+func (s *StorageFS) GetBucketQuota(bucket Bucket) (uint64, error) {
89+ dsize, err := dirSize(bucket.Path)
90+ return uint64(dsize), err
91+}
92+
93+func (s *StorageFS) DeleteBucket(bucket Bucket) error {
94+ return os.RemoveAll(bucket.Path)
95+}
96+
97+func (s *StorageFS) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
98+ objInfo := &ObjectInfo{
99+ LastModified: time.Time{},
100+ Metadata: nil,
101+ UserMetadata: map[string]string{},
102+ }
103+
104+ dat, err := os.Open(filepath.Join(bucket.Path, fpath))
105+ if err != nil {
106+ return nil, objInfo, err
107+ }
108+
109+ info, err := dat.Stat()
110+ if err != nil {
111+ return nil, objInfo, err
112+ }
113+
114+ objInfo.Size = info.Size()
115+ objInfo.LastModified = info.ModTime()
116+ return dat, objInfo, nil
117+}
118+
119+func (s *StorageFS) PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error) {
120+ loc := filepath.Join(bucket.Path, fpath)
121+ err := os.MkdirAll(filepath.Dir(loc), os.ModePerm)
122+ if err != nil {
123+ return "", 0, err
124+ }
125+ f, err := os.OpenFile(loc, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
126+ if err != nil {
127+ return "", 0, err
128+ }
129+
130+ size, err := io.Copy(f, contents)
131+ if err != nil {
132+ return "", 0, err
133+ }
134+
135+ f.Close()
136+
137+ if entry.Mtime > 0 {
138+ uTime := time.Unix(entry.Mtime, 0)
139+ _ = os.Chtimes(loc, uTime, uTime)
140+ }
141+
142+ return loc, size, nil
143+}
144+
145+func (s *StorageFS) DeleteObject(bucket Bucket, fpath string) error {
146+ loc := filepath.Join(bucket.Path, fpath)
147+ err := os.Remove(loc)
148+ if err != nil {
149+ return err
150+ }
151+
152+ return nil
153+}
154+
155+func (s *StorageFS) ListBuckets() ([]string, error) {
156+ return []string{}, fmt.Errorf("not implemented")
157+}
158+
159+func (s *StorageFS) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
160+ var fileList []os.FileInfo
161+
162+ fpath := path.Join(bucket.Path, dir)
163+
164+ info, err := os.Stat(fpath)
165+ if err != nil {
166+ return fileList, err
167+ }
168+
169+ if info.IsDir() && !strings.HasSuffix(dir, "/") {
170+ fileList = append(fileList, &utils.VirtualFile{
171+ FName: "",
172+ FIsDir: info.IsDir(),
173+ FSize: info.Size(),
174+ FModTime: info.ModTime(),
175+ })
176+
177+ return fileList, err
178+ }
179+
180+ var files []fs.DirEntry
181+
182+ if recursive {
183+ err = filepath.WalkDir(fpath, func(s string, d fs.DirEntry, err error) error {
184+ if err != nil {
185+ return err
186+ }
187+ files = append(files, d)
188+ return nil
189+ })
190+ if err != nil {
191+ fileList = append(fileList, info)
192+ return fileList, nil
193+ }
194+ } else {
195+ files, err = os.ReadDir(fpath)
196+ if err != nil {
197+ fileList = append(fileList, info)
198+ return fileList, nil
199+ }
200+ }
201+
202+ for _, f := range files {
203+ info, err := f.Info()
204+ if err != nil {
205+ return fileList, err
206+ }
207+
208+ i := &utils.VirtualFile{
209+ FName: f.Name(),
210+ FIsDir: f.IsDir(),
211+ FSize: info.Size(),
212+ FModTime: info.ModTime(),
213+ }
214+
215+ fileList = append(fileList, i)
216+ }
217+
218+ return fileList, err
219+}
+208,
-0
1@@ -0,0 +1,208 @@
2+package storage
3+
4+import (
5+ "fmt"
6+ "io"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+ "sync"
11+ "time"
12+
13+ "github.com/picosh/pico/pkg/send/utils"
14+)
15+
16+type StorageMemory struct {
17+ storage map[string]map[string]string
18+ mu sync.RWMutex
19+}
20+
21+var _ ObjectStorage = &StorageMemory{}
22+var _ ObjectStorage = (*StorageMemory)(nil)
23+
24+func NewStorageMemory(st map[string]map[string]string) (*StorageMemory, error) {
25+ return &StorageMemory{
26+ storage: st,
27+ }, nil
28+}
29+
30+func (s *StorageMemory) GetBucket(name string) (Bucket, error) {
31+ s.mu.RLock()
32+ defer s.mu.RUnlock()
33+
34+ bucket := Bucket{
35+ Name: name,
36+ Path: name,
37+ }
38+
39+ _, ok := s.storage[name]
40+ if !ok {
41+ return bucket, fmt.Errorf("bucket does not exist")
42+ }
43+
44+ return bucket, nil
45+}
46+
47+func (s *StorageMemory) UpsertBucket(name string) (Bucket, error) {
48+ bucket, err := s.GetBucket(name)
49+ if err == nil {
50+ return bucket, nil
51+ }
52+
53+ s.mu.Lock()
54+ defer s.mu.Unlock()
55+
56+ s.storage[name] = map[string]string{}
57+ return bucket, nil
58+}
59+
60+func (s *StorageMemory) GetBucketQuota(bucket Bucket) (uint64, error) {
61+ s.mu.RLock()
62+ defer s.mu.RUnlock()
63+
64+ objects := s.storage[bucket.Path]
65+ size := 0
66+ for _, val := range objects {
67+ size += len([]byte(val))
68+ }
69+ return uint64(size), nil
70+}
71+
72+func (s *StorageMemory) DeleteBucket(bucket Bucket) error {
73+ s.mu.Lock()
74+ defer s.mu.Unlock()
75+
76+ delete(s.storage, bucket.Path)
77+ return nil
78+}
79+
80+func (s *StorageMemory) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
81+ s.mu.RLock()
82+ defer s.mu.RUnlock()
83+
84+ if !strings.HasPrefix(fpath, "/") {
85+ fpath = "/" + fpath
86+ }
87+
88+ objInfo := &ObjectInfo{
89+ LastModified: time.Time{},
90+ Metadata: nil,
91+ UserMetadata: map[string]string{},
92+ }
93+
94+ dat, ok := s.storage[bucket.Path][fpath]
95+ if !ok {
96+ return nil, objInfo, fmt.Errorf("object does not exist: %s", fpath)
97+ }
98+
99+ objInfo.Size = int64(len([]byte(dat)))
100+ reader := utils.NopReadAndReaderAtCloser(strings.NewReader(dat))
101+ return reader, objInfo, nil
102+}
103+
104+func (s *StorageMemory) PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error) {
105+ s.mu.Lock()
106+ defer s.mu.Unlock()
107+
108+ d, err := io.ReadAll(contents)
109+ if err != nil {
110+ return "", 0, err
111+ }
112+
113+ s.storage[bucket.Path][fpath] = string(d)
114+ return fmt.Sprintf("%s%s", bucket.Path, fpath), int64(len(d)), nil
115+}
116+
117+func (s *StorageMemory) DeleteObject(bucket Bucket, fpath string) error {
118+ s.mu.Lock()
119+ defer s.mu.Unlock()
120+
121+ delete(s.storage[bucket.Path], fpath)
122+ return nil
123+}
124+
125+func (s *StorageMemory) ListBuckets() ([]string, error) {
126+ s.mu.RLock()
127+ defer s.mu.RUnlock()
128+
129+ buckets := []string{}
130+ for key := range s.storage {
131+ buckets = append(buckets, key)
132+ }
133+ return buckets, nil
134+}
135+
136+func (s *StorageMemory) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
137+ s.mu.RLock()
138+ defer s.mu.RUnlock()
139+
140+ var fileList []os.FileInfo
141+
142+ resolved := dir
143+
144+ if !strings.HasPrefix(resolved, "/") {
145+ resolved = "/" + resolved
146+ }
147+
148+ objects := s.storage[bucket.Path]
149+ // dir is actually an object
150+ oval, ok := objects[resolved]
151+ if ok {
152+ fileList = append(fileList, &utils.VirtualFile{
153+ FName: filepath.Base(resolved),
154+ FIsDir: false,
155+ FSize: int64(len([]byte(oval))),
156+ FModTime: time.Time{},
157+ })
158+ return fileList, nil
159+ }
160+
161+ for key, val := range objects {
162+ if !strings.HasPrefix(key, resolved) {
163+ continue
164+ }
165+
166+ rep := strings.Replace(key, resolved, "", 1)
167+ fdir := filepath.Dir(rep)
168+ fname := filepath.Base(rep)
169+ paths := strings.Split(fdir, "/")
170+
171+ if fdir == "/" {
172+ ffname := filepath.Base(resolved)
173+ fileList = append(fileList, &utils.VirtualFile{
174+ FName: ffname,
175+ FIsDir: true,
176+ })
177+ }
178+
179+ for _, p := range paths {
180+ if p == "" || p == "/" || p == "." {
181+ continue
182+ }
183+ fileList = append(fileList, &utils.VirtualFile{
184+ FName: p,
185+ FIsDir: true,
186+ })
187+ }
188+
189+ trimRes := strings.TrimSuffix(resolved, "/")
190+ dirKey := filepath.Dir(key)
191+ if recursive {
192+ fileList = append(fileList, &utils.VirtualFile{
193+ FName: fname,
194+ FIsDir: false,
195+ FSize: int64(len([]byte(val))),
196+ FModTime: time.Time{},
197+ })
198+ } else if resolved == dirKey || trimRes == dirKey {
199+ fileList = append(fileList, &utils.VirtualFile{
200+ FName: fname,
201+ FIsDir: false,
202+ FSize: int64(len([]byte(val))),
203+ FModTime: time.Time{},
204+ })
205+ }
206+ }
207+
208+ return fileList, nil
209+}
+215,
-0
1@@ -0,0 +1,215 @@
2+package storage
3+
4+import (
5+ "context"
6+ "errors"
7+ "fmt"
8+ "io"
9+ "net/url"
10+ "os"
11+ "strconv"
12+ "strings"
13+ "time"
14+
15+ "github.com/minio/madmin-go/v3"
16+ "github.com/minio/minio-go/v7"
17+ "github.com/minio/minio-go/v7/pkg/credentials"
18+ "github.com/picosh/pico/pkg/send/utils"
19+)
20+
21+type StorageMinio struct {
22+ Client *minio.Client
23+ Admin *madmin.AdminClient
24+}
25+
26+var _ ObjectStorage = &StorageMinio{}
27+var _ ObjectStorage = (*StorageMinio)(nil)
28+
29+func NewStorageMinio(address, user, pass string) (*StorageMinio, error) {
30+ endpoint, err := url.Parse(address)
31+ if err != nil {
32+ return nil, err
33+ }
34+ ssl := endpoint.Scheme == "https"
35+
36+ mClient, err := minio.New(endpoint.Host, &minio.Options{
37+ Creds: credentials.NewStaticV4(user, pass, ""),
38+ Secure: ssl,
39+ })
40+ if err != nil {
41+ return nil, err
42+ }
43+
44+ aClient, err := madmin.New(
45+ endpoint.Host,
46+ user,
47+ pass,
48+ ssl,
49+ )
50+ if err != nil {
51+ return nil, err
52+ }
53+
54+ mini := &StorageMinio{
55+ Client: mClient,
56+ Admin: aClient,
57+ }
58+ return mini, err
59+}
60+
61+func (s *StorageMinio) GetBucket(name string) (Bucket, error) {
62+ bucket := Bucket{
63+ Name: name,
64+ }
65+
66+ exists, err := s.Client.BucketExists(context.TODO(), bucket.Name)
67+ if err != nil || !exists {
68+ if err == nil {
69+ err = errors.New("bucket does not exist")
70+ }
71+ return bucket, err
72+ }
73+
74+ return bucket, nil
75+}
76+
77+func (s *StorageMinio) UpsertBucket(name string) (Bucket, error) {
78+ bucket, err := s.GetBucket(name)
79+ if err == nil {
80+ return bucket, nil
81+ }
82+
83+ err = s.Client.MakeBucket(context.TODO(), name, minio.MakeBucketOptions{})
84+ if err != nil {
85+ return bucket, err
86+ }
87+
88+ return bucket, nil
89+}
90+
91+func (s *StorageMinio) GetBucketQuota(bucket Bucket) (uint64, error) {
92+ info, err := s.Admin.AccountInfo(context.TODO(), madmin.AccountOpts{})
93+ if err != nil {
94+ return 0, nil
95+ }
96+ for _, b := range info.Buckets {
97+ if b.Name == bucket.Name {
98+ return b.Size, nil
99+ }
100+ }
101+
102+ return 0, fmt.Errorf("%s bucket not found in account info", bucket.Name)
103+}
104+
105+func (s *StorageMinio) ListBuckets() ([]string, error) {
106+ bcks := []string{}
107+ buckets, err := s.Client.ListBuckets(context.Background())
108+ if err != nil {
109+ return bcks, err
110+ }
111+ for _, bucket := range buckets {
112+ bcks = append(bcks, bucket.Name)
113+ }
114+
115+ return bcks, nil
116+}
117+
118+func (s *StorageMinio) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
119+ var fileList []os.FileInfo
120+
121+ resolved := strings.TrimPrefix(dir, "/")
122+
123+ opts := minio.ListObjectsOptions{Prefix: resolved, Recursive: recursive, WithMetadata: true}
124+ for obj := range s.Client.ListObjects(context.Background(), bucket.Name, opts) {
125+ if obj.Err != nil {
126+ return fileList, obj.Err
127+ }
128+
129+ isDir := strings.HasSuffix(obj.Key, string(os.PathSeparator))
130+
131+ modTime := obj.LastModified
132+
133+ if mtime, ok := obj.UserMetadata["Mtime"]; ok {
134+ mtimeUnix, err := strconv.Atoi(mtime)
135+ if err == nil {
136+ modTime = time.Unix(int64(mtimeUnix), 0)
137+ }
138+ }
139+
140+ info := &utils.VirtualFile{
141+ FName: strings.TrimSuffix(strings.TrimPrefix(obj.Key, resolved), "/"),
142+ FIsDir: isDir,
143+ FSize: obj.Size,
144+ FModTime: modTime,
145+ }
146+ fileList = append(fileList, info)
147+ }
148+
149+ return fileList, nil
150+}
151+
152+func (s *StorageMinio) DeleteBucket(bucket Bucket) error {
153+ return s.Client.RemoveBucket(context.TODO(), bucket.Name)
154+}
155+
156+func (s *StorageMinio) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
157+ objInfo := &ObjectInfo{
158+ Size: 0,
159+ LastModified: time.Time{},
160+ ETag: "",
161+ }
162+
163+ info, err := s.Client.StatObject(context.Background(), bucket.Name, fpath, minio.StatObjectOptions{})
164+ if err != nil {
165+ return nil, objInfo, err
166+ }
167+
168+ objInfo.LastModified = info.LastModified
169+ objInfo.ETag = info.ETag
170+ objInfo.Metadata = info.Metadata
171+ objInfo.UserMetadata = info.UserMetadata
172+ objInfo.Size = info.Size
173+
174+ obj, err := s.Client.GetObject(context.Background(), bucket.Name, fpath, minio.GetObjectOptions{})
175+ if err != nil {
176+ return nil, objInfo, err
177+ }
178+
179+ if mtime, ok := info.UserMetadata["Mtime"]; ok {
180+ mtimeUnix, err := strconv.Atoi(mtime)
181+ if err == nil {
182+ objInfo.LastModified = time.Unix(int64(mtimeUnix), 0)
183+ }
184+ }
185+
186+ return obj, objInfo, nil
187+}
188+
189+func (s *StorageMinio) PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error) {
190+ opts := minio.PutObjectOptions{
191+ UserMetadata: map[string]string{
192+ "Mtime": fmt.Sprint(time.Now().Unix()),
193+ },
194+ }
195+
196+ if entry.Mtime > 0 {
197+ opts.UserMetadata["Mtime"] = fmt.Sprint(entry.Mtime)
198+ }
199+
200+ var objSize int64 = -1
201+ if entry.Size > 0 {
202+ objSize = entry.Size
203+ }
204+ info, err := s.Client.PutObject(context.TODO(), bucket.Name, fpath, contents, objSize, opts)
205+
206+ if err != nil {
207+ return "", 0, err
208+ }
209+
210+ return fmt.Sprintf("%s/%s", info.Bucket, info.Key), info.Size, nil
211+}
212+
213+func (s *StorageMinio) DeleteObject(bucket Bucket, fpath string) error {
214+ err := s.Client.RemoveObject(context.TODO(), bucket.Name, fpath, minio.RemoveObjectOptions{})
215+ return err
216+}
+269,
-0
1@@ -0,0 +1,269 @@
2+package storage
3+
4+import (
5+ "bytes"
6+ "context"
7+ "errors"
8+ "fmt"
9+ "io"
10+ "os"
11+ "strings"
12+ "time"
13+
14+ "github.com/aws/aws-sdk-go-v2/aws"
15+ "github.com/aws/aws-sdk-go-v2/config"
16+ "github.com/aws/aws-sdk-go-v2/credentials"
17+ "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
18+ "github.com/aws/aws-sdk-go-v2/service/s3"
19+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
20+ "github.com/aws/smithy-go"
21+ "github.com/picosh/pico/pkg/send/utils"
22+)
23+
24+type StorageS3 struct {
25+ Client *s3.Client
26+ Region string
27+}
28+
29+var _ ObjectStorage = &StorageS3{}
30+var _ ObjectStorage = (*StorageS3)(nil)
31+
32+func NewStorageS3(region, key, secret string) (*StorageS3, error) {
33+ creds := credentials.NewStaticCredentialsProvider(key, secret, "")
34+ cfg, err := config.LoadDefaultConfig(
35+ context.TODO(),
36+ config.WithRegion(region),
37+ config.WithCredentialsProvider(creds),
38+ )
39+ if err != nil {
40+ return nil, err
41+ }
42+
43+ client := s3.NewFromConfig(cfg)
44+ return &StorageS3{Client: client}, nil
45+}
46+
47+func (s *StorageS3) GetBucket(name string) (Bucket, error) {
48+ bucket := Bucket{
49+ Name: name,
50+ }
51+
52+ _, err := s.Client.HeadBucket(context.TODO(), &s3.HeadBucketInput{
53+ Bucket: aws.String(name),
54+ })
55+ if err != nil {
56+ var apiError smithy.APIError
57+ if errors.As(err, &apiError) {
58+ switch apiError.(type) {
59+ case *types.NotFound:
60+ return bucket, fmt.Errorf("bucket not found")
61+ default:
62+ return bucket, err
63+ }
64+ }
65+ }
66+ return bucket, nil
67+}
68+
69+func (s *StorageS3) UpsertBucket(name string) (Bucket, error) {
70+ bucket, err := s.GetBucket(name)
71+ if err == nil {
72+ return bucket, nil
73+ }
74+
75+ _, err = s.Client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
76+ Bucket: aws.String(name),
77+ CreateBucketConfiguration: &types.CreateBucketConfiguration{
78+ LocationConstraint: types.BucketLocationConstraint(s.Region),
79+ },
80+ })
81+
82+ return bucket, err
83+}
84+
85+func (s *StorageS3) GetBucketQuota(bucket Bucket) (uint64, error) {
86+ var totalSize uint64
87+ paginator := s3.NewListObjectsV2Paginator(s.Client, &s3.ListObjectsV2Input{
88+ Bucket: aws.String(bucket.Name),
89+ })
90+
91+ for paginator.HasMorePages() {
92+ page, err := paginator.NextPage(context.TODO())
93+ if err != nil {
94+ return 0, err
95+ }
96+
97+ for _, object := range page.Contents {
98+ totalSize += uint64(*object.Size)
99+ }
100+ }
101+
102+ return totalSize, nil
103+}
104+
105+func (s *StorageS3) ListBuckets() ([]string, error) {
106+ bcks := []string{}
107+ maxBuckets := int32(1000)
108+ result, err := s.Client.ListBuckets(context.TODO(), &s3.ListBucketsInput{MaxBuckets: &maxBuckets})
109+ if err != nil {
110+ return bcks, err
111+ }
112+
113+ for _, bucket := range result.Buckets {
114+ bcks = append(bcks, *bucket.Name)
115+ }
116+
117+ return bcks, nil
118+}
119+
120+func (s *StorageS3) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
121+ var fileList []os.FileInfo
122+
123+ prefix := strings.TrimPrefix(dir, "/")
124+ input := &s3.ListObjectsV2Input{
125+ Bucket: aws.String(bucket.Name),
126+ Prefix: aws.String(prefix),
127+ }
128+ if !recursive {
129+ input.Delimiter = aws.String("/")
130+ }
131+
132+ paginator := s3.NewListObjectsV2Paginator(s.Client, input)
133+
134+ for paginator.HasMorePages() {
135+ page, err := paginator.NextPage(context.TODO())
136+ if err != nil {
137+ return fileList, err
138+ }
139+
140+ for _, pref := range page.CommonPrefixes {
141+ modTime := time.Time{}
142+ fname := strings.TrimSuffix(strings.TrimPrefix(*pref.Prefix, prefix), "/")
143+ info := &utils.VirtualFile{
144+ FName: fname,
145+ FIsDir: true,
146+ FSize: 0,
147+ FModTime: modTime,
148+ }
149+ fileList = append(fileList, info)
150+ }
151+
152+ for _, obj := range page.Contents {
153+ modTime := obj.LastModified
154+ fname := strings.TrimSuffix(strings.TrimPrefix(*obj.Key, prefix), "/")
155+ info := &utils.VirtualFile{
156+ FName: fname,
157+ FIsDir: false,
158+ FSize: *obj.Size,
159+ FModTime: *modTime,
160+ }
161+ fileList = append(fileList, info)
162+ }
163+ }
164+
165+ return fileList, nil
166+}
167+
168+func (s *StorageS3) deleteAllObjects(bucket Bucket) error {
169+ paginator := s3.NewListObjectsV2Paginator(s.Client, &s3.ListObjectsV2Input{
170+ Bucket: aws.String(bucket.Name),
171+ })
172+
173+ for paginator.HasMorePages() {
174+ page, err := paginator.NextPage(context.TODO())
175+ if err != nil {
176+ return err
177+ }
178+
179+ var objectIdentifiers []types.ObjectIdentifier
180+ for _, object := range page.Contents {
181+ objectIdentifiers = append(objectIdentifiers, types.ObjectIdentifier{Key: object.Key})
182+ }
183+
184+ _, err = s.Client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
185+ Bucket: aws.String(bucket.Name),
186+ Delete: &types.Delete{
187+ Objects: objectIdentifiers,
188+ Quiet: aws.Bool(true),
189+ },
190+ })
191+ if err != nil {
192+ return err
193+ }
194+ }
195+
196+ return nil
197+}
198+
199+func (s *StorageS3) DeleteBucket(bucket Bucket) error {
200+ err := s.deleteAllObjects(bucket)
201+ if err != nil {
202+ return err
203+ }
204+
205+ _, err = s.Client.DeleteBucket(context.TODO(), &s3.DeleteBucketInput{
206+ Bucket: aws.String(bucket.Name),
207+ })
208+ return err
209+}
210+
211+func (s *StorageS3) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
212+ input := &s3.GetObjectInput{
213+ Bucket: aws.String(bucket.Name),
214+ Key: aws.String(fpath),
215+ }
216+
217+ objInfo := &ObjectInfo{
218+ LastModified: time.Time{},
219+ Metadata: nil,
220+ UserMetadata: map[string]string{},
221+ }
222+
223+ result, err := s.Client.GetObject(context.TODO(), input)
224+ if err != nil {
225+ return nil, objInfo, err
226+ }
227+
228+ objInfo.UserMetadata = result.Metadata
229+ objInfo.ETag = *result.ETag
230+ objInfo.Size = *result.ContentLength
231+ objInfo.LastModified = *result.LastModified
232+
233+ // unfortunately we have to read the object into memory because we
234+ // require io.ReadAt
235+ data, err := io.ReadAll(result.Body)
236+ if err != nil {
237+ return nil, objInfo, err
238+ }
239+ defer result.Body.Close()
240+
241+ // Create a bytes.Reader which implements io.ReaderAt
242+ body := bytes.NewReader(data)
243+ content := utils.NopReadAndReaderAtCloser(body)
244+
245+ return content, objInfo, nil
246+}
247+
248+func (s *StorageS3) PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error) {
249+ key := strings.TrimPrefix(fpath, "/")
250+ uploader := manager.NewUploader(s.Client)
251+ info, err := uploader.Upload(context.TODO(), &s3.PutObjectInput{
252+ Bucket: aws.String(bucket.Name),
253+ Key: aws.String(key),
254+ Body: contents,
255+ })
256+ if err != nil {
257+ return "", 0, err
258+ }
259+
260+ return fmt.Sprintf("%s/%s", bucket.Name, *info.Key), entry.Size, nil
261+}
262+
263+func (s *StorageS3) DeleteObject(bucket Bucket, fpath string) error {
264+ key := strings.TrimPrefix(fpath, "/")
265+ _, err := s.Client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
266+ Bucket: aws.String(bucket.Name),
267+ Key: aws.String(key),
268+ })
269+ return err
270+}
+37,
-0
1@@ -0,0 +1,37 @@
2+package storage
3+
4+import (
5+ "io"
6+ "net/http"
7+ "os"
8+ "time"
9+
10+ "github.com/picosh/pico/pkg/send/utils"
11+)
12+
13+type Bucket struct {
14+ Name string
15+ Path string
16+ Root string
17+}
18+
19+type ObjectStorage interface {
20+ GetBucket(name string) (Bucket, error)
21+ GetBucketQuota(bucket Bucket) (uint64, error)
22+ UpsertBucket(name string) (Bucket, error)
23+ ListBuckets() ([]string, error)
24+ DeleteBucket(bucket Bucket) error
25+
26+ GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error)
27+ PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error)
28+ DeleteObject(bucket Bucket, fpath string) error
29+ ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error)
30+}
31+
32+type ObjectInfo struct {
33+ Size int64
34+ LastModified time.Time
35+ ETag string
36+ Metadata http.Header
37+ UserMetadata map[string]string
38+}
+44,
-0
1@@ -0,0 +1,44 @@
2+package pobj
3+
4+import (
5+ "log/slog"
6+ "os"
7+
8+ "github.com/picosh/pico/pkg/pobj/storage"
9+)
10+
11+func GetEnv(key string, defaultVal string) string {
12+ if value, exists := os.LookupEnv(key); exists {
13+ return value
14+ }
15+ return defaultVal
16+}
17+
18+func EnvDriverDetector(logger *slog.Logger) (storage.ObjectStorage, error) {
19+ driver := GetEnv("OBJECT_DRIVER", "fs")
20+ logger.Info("driver detected", "driver", driver)
21+
22+ if driver == "memory" {
23+ return storage.NewStorageMemory(map[string]map[string]string{})
24+ } else if driver == "minio" {
25+ url := GetEnv("MINIO_URL", "")
26+ user := GetEnv("MINIO_ROOT_USER", "")
27+ pass := GetEnv("MINIO_ROOT_PASSWORD", "")
28+ logger.Info(
29+ "object config detected",
30+ "url", url,
31+ "user", user,
32+ )
33+ return storage.NewStorageMinio(url, user, pass)
34+ } else if driver == "s3" {
35+ region := GetEnv("AWS_REGION", "us-east-1")
36+ key := GetEnv("AWS_ACCESS_KEY_ID", "")
37+ secret := GetEnv("AWS_SECRET_ACCESS_KEY", "")
38+ return storage.NewStorageS3(region, key, secret)
39+ }
40+
41+ // implied driver == "fs"
42+ storageDir := GetEnv("OBJECT_URL", "./.storage")
43+ logger.Info("object config detected", "dir", storageDir)
44+ return storage.NewStorageFS(logger, storageDir)
45+}
R pssh/logger.go =>
pkg/pssh/logger.go
+1,
-1
1@@ -4,7 +4,7 @@ import (
2 "log/slog"
3 "time"
4
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 )
8
9 type ctxLoggerKey struct{}
R pssh/pty.go =>
pkg/pssh/pty.go
+0,
-0
R pssh/server.go =>
pkg/pssh/server.go
+0,
-0
R pssh/server_test.go =>
pkg/pssh/server_test.go
+1,
-1
1@@ -8,7 +8,7 @@ import (
2 "testing"
3 "time"
4
5- "github.com/picosh/pico/pssh"
6+ "github.com/picosh/pico/pkg/pssh"
7 "golang.org/x/crypto/ssh"
8 )
9
+26,
-0
1@@ -0,0 +1,26 @@
2+package auth
3+
4+import (
5+ "github.com/picosh/pico/pkg/pssh"
6+ "github.com/picosh/pico/pkg/send/utils"
7+)
8+
9+func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware {
10+ return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler {
11+ return func(session *pssh.SSHServerConnSession) error {
12+ defer func() {
13+ if r := recover(); r != nil {
14+ writeHandler.GetLogger(session).Error("error running auth middleware", "err", r)
15+ }
16+ }()
17+
18+ err := writeHandler.Validate(session)
19+ if err != nil {
20+ utils.ErrorHandler(session, err)
21+ return err
22+ }
23+
24+ return sshHandler(session)
25+ }
26+ }
27+}
+44,
-0
1@@ -0,0 +1,44 @@
2+package list
3+
4+import (
5+ "sort"
6+ "strings"
7+
8+ "github.com/picosh/pico/pkg/pssh"
9+ "github.com/picosh/pico/pkg/send/utils"
10+)
11+
12+func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware {
13+ return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler {
14+ return func(session *pssh.SSHServerConnSession) error {
15+ cmd := session.Command()
16+ if !(len(cmd) > 1 && cmd[0] == "command" && cmd[1] == "ls") {
17+ return sshHandler(session)
18+ }
19+
20+ fileList, err := writeHandler.List(session, "/", true, false)
21+ if err != nil {
22+ utils.ErrorHandler(session, err)
23+ return err
24+ }
25+
26+ var data []string
27+ for _, file := range fileList {
28+ name := strings.ReplaceAll(file.Name(), "/", "")
29+ if file.IsDir() {
30+ name += "/"
31+ }
32+
33+ data = append(data, name)
34+ }
35+
36+ sort.Strings(data)
37+
38+ _, err = session.Write([]byte(strings.Join(data, "\r\n")))
39+ if err != nil {
40+ utils.ErrorHandler(session, err)
41+ }
42+ return err
43+ }
44+ }
45+}
+64,
-0
1@@ -0,0 +1,64 @@
2+package pipe
3+
4+import (
5+ "fmt"
6+ "io/fs"
7+ "strconv"
8+ "strings"
9+ "time"
10+
11+ "github.com/picosh/pico/pkg/pssh"
12+ "github.com/picosh/pico/pkg/send/utils"
13+)
14+
15+func Middleware(writeHandler utils.CopyFromClientHandler, ext string) pssh.SSHServerMiddleware {
16+ return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler {
17+ return func(session *pssh.SSHServerConnSession) error {
18+ _, _, activePty := session.Pty()
19+ if activePty {
20+ err := session.Exit(0)
21+ err = session.Close()
22+ return err
23+ }
24+
25+ cmd := session.Command()
26+
27+ name := ""
28+ if len(cmd) > 0 {
29+ name = strings.TrimSpace(cmd[0])
30+ if strings.Contains(name, "=") {
31+ name = ""
32+ }
33+ }
34+
35+ postTime := time.Now()
36+
37+ if name == "" {
38+ name = fmt.Sprintf("%s%s", strconv.Itoa(int(postTime.UnixNano())), ext)
39+ }
40+
41+ result, err := writeHandler.Write(session, &utils.FileEntry{
42+ Filepath: name,
43+ Mode: fs.FileMode(0777),
44+ Size: 0,
45+ Mtime: postTime.Unix(),
46+ Atime: postTime.Unix(),
47+ Reader: session,
48+ })
49+ if err != nil {
50+ utils.ErrorHandler(session, err)
51+ return err
52+ }
53+
54+ if result != "" {
55+ _, err = session.Write([]byte(fmt.Sprintf("%s\r\n", result)))
56+ if err != nil {
57+ utils.ErrorHandler(session, err)
58+ }
59+ return err
60+ }
61+
62+ return sshHandler(session)
63+ }
64+ }
65+}
+244,
-0
1@@ -0,0 +1,244 @@
2+package rsync
3+
4+import (
5+ "errors"
6+ "fmt"
7+ "io/fs"
8+ "os"
9+ "path"
10+ "slices"
11+ "strings"
12+
13+ "github.com/picosh/go-rsync-receiver/rsyncopts"
14+ "github.com/picosh/go-rsync-receiver/rsyncreceiver"
15+ "github.com/picosh/go-rsync-receiver/rsyncsender"
16+ rsyncutils "github.com/picosh/go-rsync-receiver/utils"
17+ "github.com/picosh/pico/pkg/pssh"
18+ "github.com/picosh/pico/pkg/send/utils"
19+)
20+
21+type handler struct {
22+ session *pssh.SSHServerConnSession
23+ writeHandler utils.CopyFromClientHandler
24+ root string
25+ recursive bool
26+ ignoreTimes bool
27+}
28+
29+func (h *handler) List(rPath string) ([]fs.FileInfo, error) {
30+ isDir := false
31+ if rPath == "." {
32+ rPath = "/"
33+ isDir = true
34+ }
35+
36+ list, err := h.writeHandler.List(h.session, rPath, isDir, h.recursive)
37+ if err != nil {
38+ return nil, err
39+ }
40+
41+ var dirs []string
42+
43+ var newList []fs.FileInfo
44+
45+ for _, f := range list {
46+ if !f.IsDir() && f.Size() == 0 {
47+ continue
48+ }
49+
50+ fname := f.Name()
51+ if strings.HasPrefix(f.Name(), "/") {
52+ fname = path.Join(rPath, f.Name())
53+ }
54+
55+ if fname == "" && !f.IsDir() {
56+ fname = path.Base(rPath)
57+ }
58+
59+ newFile := &utils.VirtualFile{
60+ FName: fname,
61+ FIsDir: f.IsDir(),
62+ FSize: f.Size(),
63+ FModTime: f.ModTime(),
64+ FSys: f.Sys(),
65+ }
66+
67+ newList = append(newList, newFile)
68+
69+ parts := strings.Split(newFile.Name(), string(os.PathSeparator))
70+ lastDir := newFile.Name()
71+ for i := 0; i < len(parts); i++ {
72+ lastDir, _ = path.Split(lastDir)
73+ if lastDir == "" {
74+ continue
75+ }
76+
77+ lastDir = lastDir[:len(lastDir)-1]
78+ dirs = append(dirs, lastDir)
79+ }
80+ }
81+
82+ for _, dir := range dirs {
83+ newList = append(newList, &utils.VirtualFile{
84+ FName: dir,
85+ FIsDir: true,
86+ })
87+ }
88+
89+ slices.Reverse(newList)
90+
91+ onlyEmpty := true
92+ for _, f := range newList {
93+ if f.Name() != "" {
94+ onlyEmpty = false
95+ }
96+ }
97+
98+ if len(newList) == 0 || onlyEmpty {
99+ return nil, errors.New("no files to send, the directory may not exist or could be empty")
100+ }
101+
102+ return newList, nil
103+}
104+
105+func (h *handler) Read(file *rsyncutils.SenderFile) (os.FileInfo, rsyncutils.ReaderAtCloser, error) {
106+ filePath := file.WPath
107+
108+ if strings.HasSuffix(h.root, file.WPath) {
109+ filePath = h.root
110+ } else if !strings.HasPrefix(filePath, h.root) {
111+ filePath = path.Join(h.root, file.Path, file.WPath)
112+ }
113+
114+ return h.writeHandler.Read(h.session, &utils.FileEntry{Filepath: filePath})
115+}
116+
117+func (h *handler) Put(file *rsyncutils.ReceiverFile) (int64, error) {
118+ fileEntry := &utils.FileEntry{
119+ Filepath: path.Join("/", h.root, file.Name),
120+ Mode: fs.FileMode(0600),
121+ Size: file.Length,
122+ Mtime: file.ModTime.Unix(),
123+ Atime: file.ModTime.Unix(),
124+ }
125+ fileEntry.Reader = file.Reader
126+
127+ msg, err := h.writeHandler.Write(h.session, fileEntry)
128+ if err != nil {
129+ errMsg := fmt.Sprintf("%s\r\n", err.Error())
130+ _, err = h.session.Stderr().Write([]byte(errMsg))
131+ }
132+ if msg != "" {
133+ nMsg := fmt.Sprintf("%s\r\n", msg)
134+ _, err = h.session.Stderr().Write([]byte(nMsg))
135+ }
136+ return 0, err
137+}
138+
139+func (h *handler) Remove(willReceive []*rsyncutils.ReceiverFile) error {
140+ entries, err := h.writeHandler.List(h.session, path.Join("/", h.root), true, true)
141+ if err != nil {
142+ return err
143+ }
144+
145+ var toDelete []string
146+
147+ for _, entry := range entries {
148+ exists := slices.ContainsFunc(willReceive, func(rf *rsyncutils.ReceiverFile) bool {
149+ return rf.Name == entry.Name()
150+ })
151+
152+ if !exists && entry.Name() != "._pico_keep_dir" {
153+ toDelete = append(toDelete, entry.Name())
154+ }
155+ }
156+
157+ var errs []error
158+
159+ for _, file := range toDelete {
160+ errs = append(errs, h.writeHandler.Delete(h.session, &utils.FileEntry{Filepath: path.Join("/", h.root, file)}))
161+ _, err = h.session.Stderr().Write([]byte(fmt.Sprintf("deleting %s\r\n", file)))
162+ errs = append(errs, err)
163+ }
164+
165+ return errors.Join(errs...)
166+}
167+
168+func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware {
169+ return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler {
170+ return func(session *pssh.SSHServerConnSession) error {
171+ cmd := session.Command()
172+ if len(cmd) == 0 || cmd[0] != "rsync" {
173+ return sshHandler(session)
174+ }
175+
176+ logger := writeHandler.GetLogger(session).With(
177+ "rsync", true,
178+ "cmd", cmd,
179+ )
180+
181+ defer func() {
182+ if r := recover(); r != nil {
183+ logger.Error("error running rsync middleware", "err", r)
184+ _, _ = session.Stderr().Write([]byte("error running rsync middleware, check the flags you are using\r\n"))
185+ }
186+ }()
187+
188+ cmdFlags := session.Command()
189+
190+ optsCtx, err := rsyncopts.ParseArguments(cmdFlags[1:], true)
191+ if err != nil {
192+ fmt.Fprintf(session.Stderr(), "error parsing rsync arguments: %s\r\n", err.Error())
193+ return err
194+ }
195+
196+ if optsCtx.Options.Compress() {
197+ err := fmt.Errorf("compression is currently unsupported")
198+ fmt.Fprintf(session.Stderr(), "error: %s\r\n", err.Error())
199+ return err
200+ }
201+
202+ if optsCtx.Options.AlwaysChecksum() {
203+ err := fmt.Errorf("checksum is currently unsupported")
204+ fmt.Fprintf(session.Stderr(), "error: %s\r\n", err.Error())
205+ return err
206+ }
207+
208+ if len(optsCtx.RemainingArgs) != 2 {
209+ err := fmt.Errorf("missing source and destination arguments")
210+ fmt.Fprintf(session.Stderr(), "error: %s\r\n", err.Error())
211+ return err
212+ }
213+
214+ root := strings.TrimPrefix(optsCtx.RemainingArgs[len(optsCtx.RemainingArgs)-1], "/")
215+ if root == "" {
216+ root = "/"
217+ }
218+
219+ fileHandler := &handler{
220+ session: session,
221+ writeHandler: writeHandler,
222+ root: root,
223+ recursive: optsCtx.Options.Recurse(),
224+ ignoreTimes: !optsCtx.Options.PreserveMTimes(),
225+ }
226+
227+ for _, arg := range cmd {
228+ if arg == "--sender" {
229+ err := rsyncsender.ClientRun(logger, optsCtx.Options, session, fileHandler, []string{fileHandler.root}, true)
230+ if err != nil {
231+ logger.Error("error running rsync sender", "err", err)
232+ }
233+ return err
234+ }
235+ }
236+
237+ err = rsyncreceiver.ClientRun(logger, optsCtx.Options, session, fileHandler, []string{fileHandler.root}, true)
238+ if err != nil {
239+ logger.Error("error running rsync receiver", "err", err)
240+ }
241+
242+ return err
243+ }
244+ }
245+}
+141,
-0
1@@ -0,0 +1,141 @@
2+package scp
3+
4+import (
5+ "bufio"
6+ "errors"
7+ "fmt"
8+ "io"
9+ "io/fs"
10+ "path/filepath"
11+ "regexp"
12+ "strconv"
13+
14+ "github.com/picosh/pico/pkg/pssh"
15+ "github.com/picosh/pico/pkg/send/utils"
16+)
17+
18+var (
19+ reTimestamp = regexp.MustCompile(`^T(\d{10}) 0 (\d{10}) 0$`)
20+ reNewFolder = regexp.MustCompile(`^D(\d{4}) 0 (.*)$`)
21+ reNewFile = regexp.MustCompile(`^C(\d{4}) (\d+) (.*)$`)
22+)
23+
24+type parseError struct {
25+ subject string
26+}
27+
28+func (e parseError) Error() string {
29+ return fmt.Sprintf("failed to parse: %q", e.subject)
30+}
31+
32+func copyFromClient(session *pssh.SSHServerConnSession, info Info, handler utils.CopyFromClientHandler) error {
33+ // accepts the request
34+ _, _ = session.Write(utils.NULL)
35+
36+ writeErrors := []error{}
37+ writeSuccess := []string{}
38+
39+ var (
40+ path = info.Path
41+ r = bufio.NewReader(session)
42+ mtime int64
43+ atime int64
44+ )
45+
46+ for {
47+ line, _, err := r.ReadLine()
48+ if err != nil {
49+ if errors.Is(err, io.EOF) {
50+ break
51+ }
52+ return fmt.Errorf("failed to read line: %w", err)
53+ }
54+
55+ if matches := reTimestamp.FindAllStringSubmatch(string(line), 2); matches != nil {
56+ mtime, err = strconv.ParseInt(matches[0][1], 10, 64)
57+ if err != nil {
58+ return parseError{string(line)}
59+ }
60+ atime, err = strconv.ParseInt(matches[0][2], 10, 64)
61+ if err != nil {
62+ return parseError{string(line)}
63+ }
64+
65+ // accepts the header
66+ _, _ = session.Write(utils.NULL)
67+ continue
68+ }
69+
70+ if matches := reNewFile.FindAllStringSubmatch(string(line), 3); matches != nil {
71+ if len(matches) != 1 || len(matches[0]) != 4 {
72+ return parseError{string(line)}
73+ }
74+
75+ mode, err := strconv.ParseUint(matches[0][1], 8, 32)
76+ if err != nil {
77+ return parseError{string(line)}
78+ }
79+
80+ size, err := strconv.ParseInt(matches[0][2], 10, 64)
81+ if err != nil {
82+ return parseError{string(line)}
83+ }
84+ name := matches[0][3]
85+
86+ // accepts the header
87+ _, _ = session.Write(utils.NULL)
88+
89+ result, err := handler.Write(session, &utils.FileEntry{
90+ Filepath: filepath.Join(path, name),
91+ Mode: fs.FileMode(mode),
92+ Size: size,
93+ Mtime: mtime,
94+ Atime: atime,
95+ Reader: utils.NewLimitReader(r, int(size)),
96+ })
97+
98+ if err == nil {
99+ writeSuccess = append(writeSuccess, result)
100+ } else {
101+ writeErrors = append(writeErrors, err)
102+ fmt.Printf("failed to write file: %q: %v\n", name, err)
103+ }
104+
105+ // read the trailing nil char
106+ _, _ = r.ReadByte() // TODO: check if it is indeed a utils.NULL?
107+
108+ mtime = 0
109+ atime = 0
110+ // says 'hey im done'
111+ _, _ = session.Write(utils.NULL)
112+ continue
113+ }
114+
115+ if matches := reNewFolder.FindAllStringSubmatch(string(line), 2); matches != nil {
116+ if len(matches) != 1 || len(matches[0]) != 3 {
117+ return parseError{string(line)}
118+ }
119+
120+ name := matches[0][2]
121+ path = filepath.Join(path, name)
122+ // says 'hey im done'
123+ _, _ = session.Write(utils.NULL)
124+ continue
125+ }
126+
127+ if string(line) == "E" {
128+ path = filepath.Dir(path)
129+
130+ // says 'hey im done'
131+ _, _ = session.Write(utils.NULL)
132+ continue
133+ }
134+
135+ return fmt.Errorf("unhandled input: %q", string(line))
136+ }
137+
138+ utils.PrintMsg(session, writeSuccess, writeErrors)
139+
140+ _, _ = session.Write(utils.NULL)
141+ return nil
142+}
+12,
-0
1@@ -0,0 +1,12 @@
2+package scp
3+
4+import (
5+ "errors"
6+
7+ "github.com/picosh/pico/pkg/pssh"
8+ "github.com/picosh/pico/pkg/send/utils"
9+)
10+
11+func copyToClient(session *pssh.SSHServerConnSession, info Info, handler utils.CopyFromClientHandler) error {
12+ return errors.New("unsupported, use rsync or sftp")
13+}
+107,
-0
1@@ -0,0 +1,107 @@
2+package scp
3+
4+import (
5+ "fmt"
6+
7+ "github.com/picosh/pico/pkg/pssh"
8+ "github.com/picosh/pico/pkg/send/utils"
9+)
10+
11+func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware {
12+ return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler {
13+ return func(session *pssh.SSHServerConnSession) error {
14+ cmd := session.Command()
15+ if len(cmd) == 0 || cmd[0] != "scp" {
16+ return sshHandler(session)
17+ }
18+
19+ logger := writeHandler.GetLogger(session).With(
20+ "scp", true,
21+ "cmd", cmd,
22+ )
23+
24+ defer func() {
25+ if r := recover(); r != nil {
26+ logger.Error("error running scp middleware", "err", r)
27+ _, _ = session.Stderr().Write([]byte("error running scp middleware, check the flags you are using\r\n"))
28+ }
29+ }()
30+
31+ info := GetInfo(cmd)
32+ if !info.Ok {
33+ return sshHandler(session)
34+ }
35+
36+ var err error
37+
38+ switch info.Op {
39+ case OpCopyToClient:
40+ if writeHandler == nil {
41+ err = fmt.Errorf("no handler provided for scp -t")
42+ break
43+ }
44+ err = copyToClient(session, info, writeHandler)
45+ case OpCopyFromClient:
46+ if writeHandler == nil {
47+ err = fmt.Errorf("no handler provided for scp -t")
48+ break
49+ }
50+ err = copyFromClient(session, info, writeHandler)
51+ }
52+ if err != nil {
53+ utils.ErrorHandler(session, err)
54+ }
55+
56+ return err
57+ }
58+ }
59+}
60+
61+// Op defines which kind of SCP Operation is going on.
62+type Op byte
63+
64+const (
65+ // OpCopyToClient is when a file is being copied from the server to the client.
66+ OpCopyToClient Op = 'f'
67+
68+ // OpCopyFromClient is when a file is being copied from the client into the server.
69+ OpCopyFromClient Op = 't'
70+)
71+
72+// Info provides some information about the current SCP Operation.
73+type Info struct {
74+ // Ok is true if the current session is a SCP.
75+ Ok bool
76+
77+ // Recursice is true if its a recursive SCP.
78+ Recursive bool
79+
80+ // Path is the server path of the scp operation.
81+ Path string
82+
83+ // Op is the SCP operation kind.
84+ Op Op
85+}
86+
87+func GetInfo(cmd []string) Info {
88+ info := Info{}
89+ if len(cmd) == 0 || cmd[0] != "scp" {
90+ return info
91+ }
92+
93+ for i, p := range cmd {
94+ switch p {
95+ case "-r":
96+ info.Recursive = true
97+ case "-f":
98+ info.Op = OpCopyToClient
99+ info.Path = cmd[i+1]
100+ case "-t":
101+ info.Op = OpCopyFromClient
102+ info.Path = cmd[i+1]
103+ }
104+ }
105+
106+ info.Ok = true
107+ return info
108+}
+17,
-0
1@@ -0,0 +1,17 @@
2+package protocols
3+
4+// func Middleware(writeHandler utils.CopyFromClientHandler) ssh.Option {
5+// return func(server *ssh.Server) error {
6+// err := wish.WithMiddleware(
7+// pipe.Middleware(writeHandler, ""),
8+// scp.Middleware(writeHandler),
9+// rsync.Middleware(writeHandler),
10+// auth.Middleware(writeHandler),
11+// )(server)
12+// if err != nil {
13+// return err
14+// }
15+
16+// return sftp.SSHOption(writeHandler)(server)
17+// }
18+// }
+176,
-0
1@@ -0,0 +1,176 @@
2+package sftp
3+
4+import (
5+ "bytes"
6+ "errors"
7+ "fmt"
8+ "io"
9+ "io/fs"
10+ "os"
11+ "path/filepath"
12+
13+ "slices"
14+
15+ "github.com/picosh/pico/pkg/pssh"
16+ "github.com/picosh/pico/pkg/send/utils"
17+ "github.com/pkg/sftp"
18+)
19+
20+type listerat []os.FileInfo
21+
22+func (f listerat) ListAt(ls []os.FileInfo, offset int64) (int, error) {
23+ var n int
24+ if offset >= int64(len(f)) {
25+ return 0, io.EOF
26+ }
27+ n = copy(ls, f[offset:])
28+ if n < len(ls) {
29+ return n, io.EOF
30+ }
31+ return n, nil
32+}
33+
34+type handler struct {
35+ session *pssh.SSHServerConnSession
36+ writeHandler utils.CopyFromClientHandler
37+}
38+
39+func (f *handler) Filecmd(r *sftp.Request) error {
40+ switch r.Method {
41+ case "Rmdir", "Remove":
42+ entry := toFileEntry(r)
43+
44+ if r.Method == "Rmdir" {
45+ entry.Mode = os.ModeDir
46+ }
47+
48+ return f.writeHandler.Delete(f.session, entry)
49+ case "Mkdir":
50+ entry := toFileEntry(r)
51+
52+ entry.Mode = os.ModeDir
53+
54+ _, err := f.writeHandler.Write(f.session, entry)
55+
56+ return err
57+ case "Setstat":
58+ return nil
59+ }
60+ return errors.New("unsupported")
61+}
62+
63+func (f *handler) Filelist(r *sftp.Request) (sftp.ListerAt, error) {
64+ switch r.Method {
65+ case "List", "Stat":
66+ list := r.Method == "List"
67+
68+ listData, err := f.writeHandler.List(f.session, r.Filepath, list, false)
69+ if err != nil {
70+ return nil, err
71+ }
72+
73+ // an empty string from minio or exact match from filepath base name is what we want
74+
75+ if !list {
76+ listData = slices.DeleteFunc(listData, func(f os.FileInfo) bool {
77+ return !(f.Name() == "" || f.Name() == filepath.Base(r.Filepath))
78+ })
79+ }
80+
81+ if r.Filepath == "/" {
82+ listData = slices.DeleteFunc(listData, func(f os.FileInfo) bool {
83+ return f.Name() == "/"
84+ })
85+ listData = slices.Insert(listData, 0, os.FileInfo(&utils.VirtualFile{
86+ FName: ".",
87+ FIsDir: true,
88+ }))
89+ }
90+
91+ return listerat(listData), nil
92+ }
93+
94+ return nil, errors.New("unsupported")
95+}
96+
97+func toFileEntry(r *sftp.Request) *utils.FileEntry {
98+ attrs := r.Attributes()
99+ var size int64 = 0
100+ var mtime int64 = 0
101+ var atime int64 = 0
102+ var mode fs.FileMode
103+ if attrs != nil {
104+ mode = attrs.FileMode()
105+ size = int64(attrs.Size)
106+ mtime = int64(attrs.Mtime)
107+ atime = int64(attrs.Atime)
108+ }
109+
110+ entry := &utils.FileEntry{
111+ Filepath: r.Filepath,
112+ Mode: mode,
113+ Size: size,
114+ Mtime: mtime,
115+ Atime: atime,
116+ }
117+ return entry
118+}
119+
120+func (f *handler) Filewrite(r *sftp.Request) (io.WriterAt, error) {
121+ entry := toFileEntry(r)
122+ entry.Reader = bytes.NewReader([]byte{})
123+
124+ _, err := f.writeHandler.Write(f.session, entry)
125+ if err != nil {
126+ return nil, err
127+ }
128+
129+ buf := &buffer{}
130+ entry.Reader = buf
131+
132+ return fakeWrite{fileEntry: entry, buf: buf, handler: f}, nil
133+}
134+
135+func (f *handler) Fileread(r *sftp.Request) (io.ReaderAt, error) {
136+ if r.Filepath == "/" {
137+ return nil, os.ErrInvalid
138+ }
139+
140+ fileEntry := toFileEntry(r)
141+ _, reader, err := f.writeHandler.Read(f.session, fileEntry)
142+
143+ return reader, err
144+}
145+
146+type handlererr struct {
147+ Handler *handler
148+}
149+
150+func (f *handlererr) Filecmd(r *sftp.Request) error {
151+ err := f.Handler.Filecmd(r)
152+ if err != nil {
153+ fmt.Fprintln(f.Handler.session.Stderr(), err)
154+ }
155+ return err
156+}
157+func (f *handlererr) Filelist(r *sftp.Request) (sftp.ListerAt, error) {
158+ result, err := f.Handler.Filelist(r)
159+ if err != nil {
160+ fmt.Fprintln(f.Handler.session.Stderr(), err)
161+ }
162+ return result, err
163+}
164+func (f *handlererr) Filewrite(r *sftp.Request) (io.WriterAt, error) {
165+ result, err := f.Handler.Filewrite(r)
166+ if err != nil {
167+ fmt.Fprintln(f.Handler.session.Stderr(), err)
168+ }
169+ return result, err
170+}
171+func (f *handlererr) Fileread(r *sftp.Request) (io.ReaderAt, error) {
172+ result, err := f.Handler.Fileread(r)
173+ if err != nil {
174+ fmt.Fprintln(f.Handler.session.Stderr(), err)
175+ }
176+ return result, err
177+}
+69,
-0
1@@ -0,0 +1,69 @@
2+package sftp
3+
4+import (
5+ "errors"
6+ "fmt"
7+ "io"
8+
9+ "github.com/picosh/pico/pkg/pssh"
10+ "github.com/picosh/pico/pkg/send/utils"
11+ "github.com/pkg/sftp"
12+)
13+
14+// func SSHOption(writeHandler utils.CopyFromClientHandler) ssh.Option {
15+// return func(server *ssh.Server) error {
16+// if server.SubsystemHandlers == nil {
17+// server.SubsystemHandlers = map[string]ssh.SubsystemHandler{}
18+// }
19+
20+// server.SubsystemHandlers["sftp"] = SubsystemHandler(writeHandler)
21+// return nil
22+// }
23+// }
24+
25+func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware {
26+ return func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
27+ return func(session *pssh.SSHServerConnSession) error {
28+ logger := writeHandler.GetLogger(session).With(
29+ "sftp", true,
30+ )
31+
32+ defer func() {
33+ if r := recover(); r != nil {
34+ logger.Error("error running sftp middleware", "err", r)
35+ fmt.Fprintln(session, "error running sftp middleware, check the flags you are using")
36+ }
37+ }()
38+
39+ err := writeHandler.Validate(session)
40+ if err != nil {
41+ fmt.Fprintln(session.Stderr(), err)
42+ return err
43+ }
44+
45+ handler := &handlererr{
46+ Handler: &handler{
47+ session: session,
48+ writeHandler: writeHandler,
49+ },
50+ }
51+
52+ handlers := sftp.Handlers{
53+ FilePut: handler,
54+ FileList: handler,
55+ FileGet: handler,
56+ FileCmd: handler,
57+ }
58+
59+ requestServer := sftp.NewRequestServer(session, handlers)
60+
61+ err = requestServer.Serve()
62+ if err != nil && !errors.Is(err, io.EOF) {
63+ fmt.Fprintln(session.Stderr(), err)
64+ logger.Error("Error serving sftp subsystem", "err", err)
65+ }
66+
67+ return err
68+ }
69+ }
70+}
+75,
-0
1@@ -0,0 +1,75 @@
2+package sftp
3+
4+import (
5+ "fmt"
6+ "io"
7+ "sync"
8+
9+ "github.com/picosh/pico/pkg/send/utils"
10+)
11+
12+type buffer struct {
13+ buf []byte
14+ m sync.Mutex
15+ off int
16+}
17+
18+func (b *buffer) WriteAt(p []byte, pos int64) (n int, err error) {
19+ pLen := len(p)
20+ expLen := pos + int64(pLen)
21+ b.m.Lock()
22+ defer b.m.Unlock()
23+ if int64(len(b.buf)) < expLen {
24+ if int64(cap(b.buf)) < expLen {
25+ newBuf := make([]byte, expLen)
26+ copy(newBuf, b.buf)
27+ b.buf = newBuf
28+ }
29+ b.buf = b.buf[:expLen]
30+ }
31+ copy(b.buf[pos:], p)
32+ return pLen, nil
33+}
34+
35+func (b *buffer) Read(p []byte) (n int, err error) {
36+ b.m.Lock()
37+ defer b.m.Unlock()
38+ if len(b.buf) <= b.off {
39+ if len(p) == 0 {
40+ return 0, nil
41+ }
42+ return 0, io.EOF
43+ }
44+ n = copy(p, b.buf[b.off:])
45+ b.off += n
46+ return n, nil
47+}
48+
49+func (b *buffer) Close() error {
50+ b.buf = []byte{}
51+ return nil
52+}
53+
54+type fakeWrite struct {
55+ fileEntry *utils.FileEntry
56+ handler *handler
57+ buf *buffer
58+}
59+
60+func (f fakeWrite) WriteAt(p []byte, off int64) (int, error) {
61+ return f.buf.WriteAt(p, off)
62+}
63+
64+func (f fakeWrite) Close() error {
65+ msg, err := f.handler.writeHandler.Write(f.handler.session, f.fileEntry)
66+ if err != nil {
67+ errMsg := fmt.Sprintf("%s\r\n", err.Error())
68+ _, err = f.handler.session.Stderr().Write([]byte(errMsg))
69+ }
70+ if msg != "" {
71+ nMsg := fmt.Sprintf("%s\r\n", msg)
72+ _, err = f.handler.session.Stderr().Write([]byte(nMsg))
73+ }
74+ f.buf.Close()
75+ return err
76+}
+26,
-0
1@@ -0,0 +1,26 @@
2+package proxy
3+
4+// type Router func(sh ssh.Handler, s ssh.Session) []wish.Middleware
5+
6+// func withMiddleware(mdw ...wish.Middleware) ssh.Handler {
7+// handler := func(s ssh.Session) {}
8+// for _, mw := range mdw {
9+// handler = mw(handler)
10+// }
11+// return handler
12+// }
13+
14+// func WithProxy(router Router, otherMiddleware ...wish.Middleware) ssh.Option {
15+// mdw := func(sh ssh.Handler) ssh.Handler {
16+// return func(s ssh.Session) {
17+// mw := router(sh, s)
18+// fn := withMiddleware(mw...)
19+// fn(s)
20+// }
21+// }
22+
23+// newMiddleware := []wish.Middleware{mdw}
24+// newMiddleware = append(newMiddleware, otherMiddleware...)
25+
26+// return wish.WithMiddleware(newMiddleware...)
27+// }
+31,
-0
1@@ -0,0 +1,31 @@
2+package utils
3+
4+import (
5+ "os"
6+ "time"
7+)
8+
9+type VirtualFile struct {
10+ FName string
11+ FIsDir bool
12+ FSize int64
13+ FModTime time.Time
14+ FSys any
15+}
16+
17+func (f *VirtualFile) Name() string { return f.FName }
18+func (f *VirtualFile) Size() int64 { return f.FSize }
19+func (f *VirtualFile) Mode() os.FileMode {
20+ if f.FIsDir {
21+ return os.FileMode(0755) | os.ModeDir
22+ }
23+ return os.FileMode(0644)
24+}
25+func (f *VirtualFile) ModTime() time.Time {
26+ if f.FModTime.IsZero() {
27+ return time.Now()
28+ }
29+ return f.FModTime
30+}
31+func (f *VirtualFile) IsDir() bool { return f.FIsDir }
32+func (f *VirtualFile) Sys() any { return f.FSys }
+26,
-0
1@@ -0,0 +1,26 @@
2+package utils
3+
4+import (
5+ "io"
6+)
7+
8+type ReadAndReaderAt interface {
9+ io.ReaderAt
10+ io.Reader
11+}
12+
13+type ReadAndReaderAtCloser interface {
14+ io.Reader
15+ io.ReaderAt
16+ io.ReadCloser
17+}
18+
19+func NopReadAndReaderAtCloser(r ReadAndReaderAt) ReadAndReaderAtCloser {
20+ return nopReadAndReaderAt{r}
21+}
22+
23+type nopReadAndReaderAt struct {
24+ ReadAndReaderAt
25+}
26+
27+func (nopReadAndReaderAt) Close() error { return nil }
+35,
-0
1@@ -0,0 +1,35 @@
2+package utils
3+
4+import (
5+ "io"
6+ "sync"
7+)
8+
9+func NewLimitReader(r io.Reader, limit int) io.Reader {
10+ return &LimitReader{
11+ r: r,
12+ left: limit,
13+ }
14+}
15+
16+type LimitReader struct {
17+ r io.Reader
18+
19+ lock sync.Mutex
20+ left int
21+}
22+
23+func (r *LimitReader) Read(b []byte) (int, error) {
24+ r.lock.Lock()
25+ defer r.lock.Unlock()
26+
27+ if r.left <= 0 {
28+ return 0, io.EOF
29+ }
30+ if len(b) > r.left {
31+ b = b[0:r.left]
32+ }
33+ n, err := r.r.Read(b)
34+ r.left -= n
35+ return n, err
36+}
+44,
-0
1@@ -0,0 +1,44 @@
2+package utils
3+
4+import (
5+ "bytes"
6+ "io"
7+ "testing"
8+
9+ "github.com/matryer/is"
10+)
11+
12+func TestLimitedReader(t *testing.T) {
13+ t.Run("partial", func(t *testing.T) {
14+ is := is.New(t)
15+ var b bytes.Buffer
16+ b.WriteString("writing some bytes")
17+ r := NewLimitReader(&b, 7)
18+
19+ bts, err := io.ReadAll(r)
20+ is.NoErr(err)
21+ is.Equal("writing", string(bts))
22+ })
23+
24+ t.Run("full", func(t *testing.T) {
25+ is := is.New(t)
26+ var b bytes.Buffer
27+ b.WriteString("some text")
28+ r := NewLimitReader(&b, b.Len())
29+
30+ bts, err := io.ReadAll(r)
31+ is.NoErr(err)
32+ is.Equal("some text", string(bts))
33+ })
34+
35+ t.Run("pass limit", func(t *testing.T) {
36+ is := is.New(t)
37+ var b bytes.Buffer
38+ b.WriteString("another text")
39+ r := NewLimitReader(&b, b.Len()+10)
40+
41+ bts, err := io.ReadAll(r)
42+ is.NoErr(err)
43+ is.Equal("another text", string(bts))
44+ })
45+}
+101,
-0
1@@ -0,0 +1,101 @@
2+package utils
3+
4+import (
5+ "encoding/base64"
6+ "fmt"
7+ "io"
8+ "io/fs"
9+ "log/slog"
10+ "os"
11+ "path/filepath"
12+ "strconv"
13+
14+ "github.com/picosh/pico/pkg/pssh"
15+)
16+
17+// NULL is an array with a single NULL byte.
18+var NULL = []byte{'\x00'}
19+
20+// FileEntry is an Entry that reads from a Reader, defining a file and
21+// its contents.
22+type FileEntry struct {
23+ Filepath string
24+ Mode fs.FileMode
25+ Size int64
26+ Reader io.Reader
27+ Atime int64
28+ Mtime int64
29+ Metadata map[string]string
30+}
31+
32+// Write a file to the given writer.
33+func (e *FileEntry) Write(w io.Writer) error {
34+ if e.Mtime > 0 && e.Atime > 0 {
35+ if _, err := fmt.Fprintf(w, "T%d 0 %d 0\n", e.Mtime, e.Atime); err != nil {
36+ return fmt.Errorf("failed to write file: %q: %w", e.Filepath, err)
37+ }
38+ }
39+ fname := filepath.Base(e.Filepath)
40+ if _, err := fmt.Fprintf(w, "C%s %d %s\n", octalPerms(e.Mode), e.Size, fname); err != nil {
41+ return fmt.Errorf("failed to write file: %q: %w", e.Filepath, err)
42+ }
43+
44+ if _, err := io.Copy(w, e.Reader); err != nil {
45+ return fmt.Errorf("failed to read file: %q: %w", e.Filepath, err)
46+ }
47+
48+ if _, err := w.Write(NULL); err != nil {
49+ return fmt.Errorf("failed to write file: %q: %w", e.Filepath, err)
50+ }
51+ return nil
52+}
53+
54+func octalPerms(info fs.FileMode) string {
55+ return "0" + strconv.FormatUint(uint64(info.Perm()), 8)
56+}
57+
58+// CopyFromClientHandler is a handler that can be implemented to handle files
59+// being copied from the client to the server.
60+type CopyFromClientHandler interface {
61+ // Write should write the given file.
62+ Delete(*pssh.SSHServerConnSession, *FileEntry) error
63+ Write(*pssh.SSHServerConnSession, *FileEntry) (string, error)
64+ Read(*pssh.SSHServerConnSession, *FileEntry) (os.FileInfo, ReadAndReaderAtCloser, error)
65+ List(*pssh.SSHServerConnSession, string, bool, bool) ([]os.FileInfo, error)
66+ GetLogger(*pssh.SSHServerConnSession) *slog.Logger
67+ Validate(*pssh.SSHServerConnSession) error
68+}
69+
70+func KeyText(session *pssh.SSHServerConnSession) (string, error) {
71+ if session.PublicKey() == nil {
72+ return "", fmt.Errorf("session doesn't have public key")
73+ }
74+ kb := base64.StdEncoding.EncodeToString(session.PublicKey().Marshal())
75+ return fmt.Sprintf("%s %s", session.PublicKey().Type(), kb), nil
76+}
77+
78+func ErrorHandler(session *pssh.SSHServerConnSession, err error) {
79+ _, _ = fmt.Fprint(session.Stderr(), err, "\r\n")
80+ _ = session.Exit(1)
81+ _ = session.Close()
82+}
83+
84+func PrintMsg(session *pssh.SSHServerConnSession, stdout []string, stderr []error) {
85+ output := ""
86+ if len(stdout) > 0 {
87+ for _, msg := range stdout {
88+ if msg != "" {
89+ output += fmt.Sprintf("%s\r\n", msg)
90+ }
91+ }
92+ _, _ = fmt.Fprintln(session.Stderr(), output)
93+ }
94+
95+ outputErr := ""
96+ if len(stderr) > 0 {
97+ for _, err := range stderr {
98+ outputErr += fmt.Sprintf("%v\r\n", err)
99+ }
100+ _, _ = fmt.Fprintln(session.Stderr(), outputErr)
101+ }
102+}
1@@ -15,7 +15,7 @@ import (
2 "strings"
3 "time"
4
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/utils/pipe/metrics"
8 "github.com/simplesurance/go-ip-anonymizer/ipanonymizer"
9 "github.com/x-way/crawlerdetect"
1@@ -8,7 +8,7 @@ import (
2 "os"
3 "strings"
4
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/utils"
8 "golang.org/x/crypto/ssh"
9 )
1@@ -6,7 +6,7 @@ import (
2 "path/filepath"
3 "strings"
4
5- "github.com/picosh/send/utils"
6+ "github.com/picosh/pico/pkg/send/utils"
7 )
8
9 func GetImgsBucketName(userID string) string {
1@@ -12,7 +12,7 @@ import (
2 "strings"
3 "time"
4
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/utils"
8
9 pipeLogger "github.com/picosh/utils/pipe/log"
1@@ -5,7 +5,7 @@ import (
2 "time"
3
4 "github.com/gorilla/feeds"
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 )
8
9 func UserFeed(me db.DB, userID, token string) (*feeds.Feed, error) {
1@@ -10,9 +10,9 @@ import (
2 "regexp"
3 "strings"
4
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/pssh"
7- "github.com/picosh/pico/shared/storage"
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/pssh"
10+ "github.com/picosh/pico/pkg/shared/storage"
11 )
12
13 type Route struct {
1@@ -7,7 +7,7 @@ import (
2
3 "git.sr.ht/~delthas/senpai"
4 "github.com/containerd/console"
5- "github.com/picosh/pico/pssh"
6+ "github.com/picosh/pico/pkg/pssh"
7 )
8
9 type consoleData struct {
1@@ -4,7 +4,7 @@ import (
2 "fmt"
3 "log/slog"
4
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/utils"
8 "golang.org/x/crypto/ssh"
9 )
1@@ -8,7 +8,7 @@ import (
2 "path/filepath"
3 "strings"
4
5- sst "github.com/picosh/pobj/storage"
6+ sst "github.com/picosh/pico/pkg/pobj/storage"
7 )
8
9 type StorageFS struct {
1@@ -5,7 +5,7 @@ import (
2 "net/http"
3 "time"
4
5- sst "github.com/picosh/pobj/storage"
6+ sst "github.com/picosh/pico/pkg/pobj/storage"
7 )
8
9 type StorageMemory struct {
1@@ -8,7 +8,7 @@ import (
2 "path/filepath"
3 "strings"
4
5- sst "github.com/picosh/pobj/storage"
6+ sst "github.com/picosh/pico/pkg/pobj/storage"
7 )
8
9 type StorageMinio struct {
1@@ -15,7 +15,7 @@ import (
2 "strings"
3 "time"
4
5- "github.com/picosh/pobj/storage"
6+ "github.com/picosh/pico/pkg/pobj/storage"
7 )
8
9 func GetMimeType(fpath string) string {
1@@ -3,7 +3,7 @@ package storage
2 import (
3 "io"
4
5- sst "github.com/picosh/pobj/storage"
6+ sst "github.com/picosh/pico/pkg/pobj/storage"
7 )
8
9 type StorageServe interface {
R tui/analytics.go =>
pkg/tui/analytics.go
+1,
-1
1@@ -10,7 +10,7 @@ import (
2 "git.sr.ht/~rockorager/vaxis/vxfw/list"
3 "git.sr.ht/~rockorager/vaxis/vxfw/richtext"
4 "git.sr.ht/~rockorager/vaxis/vxfw/text"
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/utils"
8 )
9
R tui/border.go =>
pkg/tui/border.go
+0,
-0
R tui/chat.go =>
pkg/tui/chat.go
+0,
-0
R tui/group.go =>
pkg/tui/group.go
+0,
-0
R tui/info.go =>
pkg/tui/info.go
+1,
-1
1@@ -7,7 +7,7 @@ import (
2 "git.sr.ht/~rockorager/vaxis"
3 "git.sr.ht/~rockorager/vaxis/vxfw"
4 "git.sr.ht/~rockorager/vaxis/vxfw/text"
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 )
8
9 type UsageInfo struct {
R tui/input.go =>
pkg/tui/input.go
+0,
-0
R tui/kv.go =>
pkg/tui/kv.go
+0,
-0
R tui/logs.go =>
pkg/tui/logs.go
+1,
-1
1@@ -13,7 +13,7 @@ import (
2 "git.sr.ht/~rockorager/vaxis/vxfw/list"
3 "git.sr.ht/~rockorager/vaxis/vxfw/richtext"
4 "git.sr.ht/~rockorager/vaxis/vxfw/text"
5- "github.com/picosh/pico/shared"
6+ "github.com/picosh/pico/pkg/shared"
7 "github.com/picosh/utils"
8 pipeLogger "github.com/picosh/utils/pipe/log"
9 )
1@@ -5,7 +5,7 @@ import (
2 "git.sr.ht/~rockorager/vaxis/vxfw"
3 "git.sr.ht/~rockorager/vaxis/vxfw/list"
4 "git.sr.ht/~rockorager/vaxis/vxfw/text"
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 )
8
9 var menuChoices = []string{
R tui/pager.go =>
pkg/tui/pager.go
+0,
-0
R tui/plus.go =>
pkg/tui/plus.go
+0,
-0
R tui/pubkeys.go =>
pkg/tui/pubkeys.go
+1,
-1
1@@ -10,7 +10,7 @@ import (
2 "git.sr.ht/~rockorager/vaxis/vxfw/list"
3 "git.sr.ht/~rockorager/vaxis/vxfw/richtext"
4 "git.sr.ht/~rockorager/vaxis/vxfw/text"
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/utils"
8 "golang.org/x/crypto/ssh"
9 )
R tui/senpai.go =>
pkg/tui/senpai.go
+1,
-1
1@@ -3,7 +3,7 @@ package tui
2 import (
3 "io"
4
5- "github.com/picosh/pico/shared"
6+ "github.com/picosh/pico/pkg/shared"
7 )
8
9 type SenpaiCmd struct {
R tui/signup.go =>
pkg/tui/signup.go
+1,
-1
1@@ -8,7 +8,7 @@ import (
2 "git.sr.ht/~rockorager/vaxis/vxfw/button"
3 "git.sr.ht/~rockorager/vaxis/vxfw/richtext"
4 "git.sr.ht/~rockorager/vaxis/vxfw/text"
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/utils"
8 "golang.org/x/crypto/ssh"
9 )
R tui/tokens.go =>
pkg/tui/tokens.go
+1,
-1
1@@ -10,7 +10,7 @@ import (
2 "git.sr.ht/~rockorager/vaxis/vxfw/list"
3 "git.sr.ht/~rockorager/vaxis/vxfw/richtext"
4 "git.sr.ht/~rockorager/vaxis/vxfw/text"
5- "github.com/picosh/pico/db"
6+ "github.com/picosh/pico/pkg/db"
7 )
8
9 type TokensPage struct {
R tui/ui.go =>
pkg/tui/ui.go
+3,
-3
1@@ -9,9 +9,9 @@ import (
2 "git.sr.ht/~rockorager/vaxis"
3 "git.sr.ht/~rockorager/vaxis/vxfw"
4 "git.sr.ht/~rockorager/vaxis/vxfw/richtext"
5- "github.com/picosh/pico/db"
6- "github.com/picosh/pico/pssh"
7- "github.com/picosh/pico/shared"
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/pssh"
10+ "github.com/picosh/pico/pkg/shared"
11 "github.com/picosh/utils"
12 )
13
+108,
-0
1@@ -0,0 +1,108 @@
2+package tunkit
3+
4+import (
5+ "errors"
6+ "io"
7+ "log/slog"
8+ "net"
9+ "sync"
10+
11+ "github.com/picosh/pico/pkg/pssh"
12+ "golang.org/x/crypto/ssh"
13+)
14+
15+type forwardedTCPPayload struct {
16+ Addr string
17+ Port uint32
18+ OriginAddr string
19+ OriginPort uint32
20+}
21+
22+type Tunnel interface {
23+ CreateConn(ctx *pssh.SSHServerConnSession) (net.Conn, error)
24+ GetLogger() *slog.Logger
25+ Close(ctx *pssh.SSHServerConnSession) error
26+}
27+
28+func LocalForwardHandler(handler Tunnel) pssh.SSHServerChannelMiddleware {
29+ return func(newChan ssh.NewChannel, sc *pssh.SSHServerConn) error {
30+ check := &forwardedTCPPayload{}
31+ err := ssh.Unmarshal(newChan.ExtraData(), check)
32+ logger := handler.GetLogger()
33+ if err != nil {
34+ logger.Error(
35+ "error unmarshaling information",
36+ "err", err,
37+ )
38+ return err
39+ }
40+
41+ log := logger.With(
42+ "addr", check.Addr,
43+ "port", check.Port,
44+ "origAddr", check.OriginAddr,
45+ "origPort", check.OriginPort,
46+ )
47+ log.Info("local forward request")
48+
49+ ch, reqs, err := newChan.Accept()
50+ if err != nil {
51+ log.Error("cannot accept new channel", "err", err)
52+ return err
53+ }
54+
55+ ctx := &pssh.SSHServerConnSession{
56+ Channel: ch,
57+ SSHServerConn: sc,
58+ }
59+
60+ go ssh.DiscardRequests(reqs)
61+
62+ go func() {
63+ downConn, err := handler.CreateConn(ctx)
64+ if err != nil {
65+ log.Error("unable to connect to conn", "err", err)
66+ ch.Close()
67+ return
68+ }
69+ defer downConn.Close()
70+
71+ var wg sync.WaitGroup
72+ wg.Add(2)
73+
74+ go func() {
75+ defer wg.Done()
76+ defer func() {
77+ _ = ch.CloseWrite()
78+ }()
79+ defer downConn.Close()
80+ _, err := io.Copy(ch, downConn)
81+ if err != nil {
82+ if !errors.Is(err, net.ErrClosed) {
83+ log.Error("io copy", "err", err)
84+ }
85+ }
86+ }()
87+ go func() {
88+ defer wg.Done()
89+ defer ch.Close()
90+ defer downConn.Close()
91+ _, err := io.Copy(downConn, ch)
92+ if err != nil {
93+ if !errors.Is(err, net.ErrClosed) {
94+ log.Error("io copy", "err", err)
95+ }
96+ }
97+ }()
98+
99+ wg.Wait()
100+ }()
101+
102+ <-ctx.Done()
103+ err = handler.Close(ctx)
104+ if err != nil {
105+ log.Error("tunnel handler error", "err", err)
106+ }
107+ return err
108+ }
109+}
+92,
-0
1@@ -0,0 +1,92 @@
2+package tunkit
3+
4+import (
5+ "fmt"
6+ "log/slog"
7+ "net"
8+ "os"
9+
10+ "github.com/picosh/pico/pkg/pssh"
11+)
12+
13+type ctxAddressKey struct{}
14+
15+func getAddressCtx(ctx *pssh.SSHServerConnSession) (string, error) {
16+ address, ok := ctx.Value(ctxAddressKey{}).(string)
17+ if address == "" || !ok {
18+ return address, fmt.Errorf("address not set on `*pssh.SSHServerConnSession()` for connection")
19+ }
20+ return address, nil
21+}
22+func setAddressCtx(ctx *pssh.SSHServerConnSession, address string) {
23+ ctx.SetValue(ctxAddressKey{}, address)
24+}
25+
26+type WebTunnelHandler struct {
27+ HttpHandler HttpHandlerFn
28+ Logger *slog.Logger
29+}
30+
31+func NewWebTunnelHandler(handler HttpHandlerFn, logger *slog.Logger) *WebTunnelHandler {
32+ return &WebTunnelHandler{
33+ HttpHandler: handler,
34+ Logger: logger,
35+ }
36+}
37+
38+func (wt *WebTunnelHandler) GetLogger() *slog.Logger {
39+ return wt.Logger
40+}
41+
42+func (wt *WebTunnelHandler) GetHttpHandler() HttpHandlerFn {
43+ return wt.HttpHandler
44+}
45+
46+func (wt *WebTunnelHandler) Close(ctx *pssh.SSHServerConnSession) error {
47+ listener, err := getListenerCtx(ctx)
48+ if err != nil {
49+ return err
50+ }
51+
52+ if listener != nil {
53+ _ = listener.Close()
54+ setListenerCtx(ctx, nil)
55+ }
56+
57+ return nil
58+}
59+
60+func (wt *WebTunnelHandler) CreateListener(ctx *pssh.SSHServerConnSession) (net.Listener, error) {
61+ tempFile, err := os.CreateTemp("", "")
62+ if err != nil {
63+ return nil, err
64+ }
65+
66+ tempFile.Close()
67+ address := tempFile.Name()
68+ os.Remove(address)
69+
70+ connListener, err := net.Listen("unix", address)
71+ if err != nil {
72+ return nil, err
73+ }
74+ setAddressCtx(ctx, address)
75+ setListenerCtx(ctx, connListener)
76+
77+ return connListener, nil
78+}
79+
80+func (wt *WebTunnelHandler) CreateConn(ctx *pssh.SSHServerConnSession) (net.Conn, error) {
81+ _, err := httpServe(wt, ctx, wt.GetLogger())
82+ if err != nil {
83+ wt.GetLogger().Info("unable to create listener", "err", err)
84+ return nil, err
85+ }
86+
87+ address, err := getAddressCtx(ctx)
88+ if err != nil {
89+ return nil, err
90+ }
91+
92+ return net.Dial("unix", address)
93+}
+57,
-0
1@@ -0,0 +1,57 @@
2+package tunkit
3+
4+import (
5+ "fmt"
6+ "log/slog"
7+ "net"
8+ "net/http"
9+
10+ "github.com/picosh/pico/pkg/pssh"
11+)
12+
13+type HttpHandlerFn = func(ctx *pssh.SSHServerConnSession) http.Handler
14+
15+type WebTunnel interface {
16+ GetHttpHandler() HttpHandlerFn
17+ CreateListener(ctx *pssh.SSHServerConnSession) (net.Listener, error)
18+ CreateConn(ctx *pssh.SSHServerConnSession) (net.Conn, error)
19+ GetLogger() *slog.Logger
20+ Close(ctx *pssh.SSHServerConnSession) error
21+}
22+
23+type ctxListenerKey struct{}
24+
25+func getListenerCtx(ctx *pssh.SSHServerConnSession) (net.Listener, error) {
26+ listener, ok := ctx.Value(ctxListenerKey{}).(net.Listener)
27+ if listener == nil || !ok {
28+ return nil, fmt.Errorf("listener not set on `*pssh.SSHServerConnSession()` for connection")
29+ }
30+ return listener, nil
31+}
32+
33+func setListenerCtx(ctx *pssh.SSHServerConnSession, listener net.Listener) {
34+ ctx.SetValue(ctxListenerKey{}, listener)
35+}
36+
37+func httpServe(handler WebTunnel, ctx *pssh.SSHServerConnSession, log *slog.Logger) (net.Listener, error) {
38+ cached, _ := getListenerCtx(ctx)
39+ if cached != nil {
40+ return cached, nil
41+ }
42+
43+ listener, err := handler.CreateListener(ctx)
44+ if err != nil {
45+ return nil, err
46+ }
47+ setListenerCtx(ctx, listener)
48+
49+ go func() {
50+ httpHandler := handler.GetHttpHandler()
51+ err := http.Serve(listener, httpHandler(ctx))
52+ if err != nil {
53+ log.Error("serving http content", "err", err)
54+ }
55+ }()
56+
57+ return listener, nil
58+}