Skip to content

Commit b259c6a

Browse files
authored
feat: Space-oriented CLI (#134)
``` $ guppy upload sources add did:key:z6MkgfGRY8VaK7d5YWD92S8PAzRq3AGHpapkmdjjyhUpy2pZ ./dir/of/data $ guppy upload sources list did:key:z6MkgfGRY8VaK7d5YWD92S8PAzRq3AGHpapkmdjjyhUpy2pZ $ guppy upload did:key:z6MkgfGRY8VaK7d5YWD92S8PAzRq3AGHpapkmdjjyhUpy2pZ ``` You can add multiple sources to a space to perform multiple uploads, but the UI currently only shows the first one. Closes #44 #### PR Dependency Tree * **PR #134** 👈 * **PR #135** * **PR #136** * **PR #137** * **PR #138** * **PR #139** * **PR #140** This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
1 parent 780951f commit b259c6a

27 files changed

+745
-921
lines changed

cmd/internal/largeupload/largeupload.go

Lines changed: 0 additions & 471 deletions
This file was deleted.

cmd/internal/upload/demo/demo.go

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
package demo
2+
3+
import (
4+
"context"
5+
"crypto/ed25519"
6+
"crypto/sha512"
7+
"fmt"
8+
"io"
9+
"io/fs"
10+
"net/http"
11+
"path"
12+
"time"
13+
14+
spaceindexcap "github.com/storacha/go-libstoracha/capabilities/space/index"
15+
uploadcap "github.com/storacha/go-libstoracha/capabilities/upload"
16+
"github.com/storacha/go-ucanto/core/invocation"
17+
"github.com/storacha/go-ucanto/core/receipt/fx"
18+
"github.com/storacha/go-ucanto/core/result"
19+
"github.com/storacha/go-ucanto/core/result/failure"
20+
"github.com/storacha/go-ucanto/principal/ed25519/signer"
21+
"github.com/storacha/go-ucanto/server"
22+
"github.com/storacha/go-ucanto/ucan"
23+
"github.com/storacha/guppy/cmd/internal/upload/ui"
24+
"github.com/storacha/guppy/internal/fakefs"
25+
"github.com/storacha/guppy/pkg/client"
26+
ctestutil "github.com/storacha/guppy/pkg/client/testutil"
27+
"github.com/storacha/guppy/pkg/preparation"
28+
"github.com/storacha/guppy/pkg/preparation/sqlrepo"
29+
)
30+
31+
type nullTransport struct{}
32+
33+
func (t nullTransport) RoundTrip(req *http.Request) (*http.Response, error) {
34+
time.Sleep(1 * time.Second)
35+
return &http.Response{
36+
StatusCode: http.StatusOK,
37+
Body: http.NoBody,
38+
}, nil
39+
}
40+
41+
type changingFS struct {
42+
fs.FS
43+
changingFile string
44+
count int
45+
changeModTime bool
46+
changeData bool
47+
}
48+
49+
type seekerFile interface {
50+
fs.File
51+
io.Seeker
52+
}
53+
54+
type changingFile struct {
55+
seekerFile
56+
fs.FileInfo
57+
parent changingFS
58+
}
59+
60+
type changingDir struct {
61+
changingFile
62+
fs fs.FS
63+
}
64+
65+
func (fsys *changingFS) Open(name string) (fs.File, error) {
66+
f, err := fsys.FS.Open(name)
67+
if err != nil {
68+
return nil, err
69+
}
70+
sf, ok := f.(seekerFile)
71+
if !ok {
72+
return nil, fmt.Errorf("file %s is not seekable", name)
73+
}
74+
75+
info, err := f.Stat()
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
cf := changingFile{
81+
seekerFile: sf,
82+
FileInfo: info,
83+
parent: *fsys,
84+
}
85+
86+
if name == fsys.changingFile {
87+
defer func() { fsys.count++ }()
88+
return cf, nil
89+
}
90+
91+
if name == path.Dir(fsys.changingFile) {
92+
subFS, err := fs.Sub(fsys, name)
93+
if err != nil {
94+
return nil, err
95+
}
96+
return changingDir{
97+
changingFile: cf,
98+
fs: subFS,
99+
}, nil
100+
}
101+
102+
return f, nil
103+
}
104+
105+
func (f changingFile) Read(b []byte) (int, error) {
106+
n, err := f.seekerFile.Read(b)
107+
if err != nil && err != io.EOF {
108+
return n, err
109+
}
110+
111+
if f.parent.changeData && n > 0 {
112+
// Just change the first byte.
113+
b[0] = (b[0] + byte(f.parent.count))
114+
}
115+
116+
return n, err
117+
}
118+
119+
func (f changingFile) Stat() (fs.FileInfo, error) {
120+
return f, nil
121+
}
122+
123+
func (d changingDir) ReadDir(n int) ([]fs.DirEntry, error) {
124+
entries, err := d.seekerFile.(fs.ReadDirFile).ReadDir(n)
125+
if err != nil {
126+
return nil, err
127+
}
128+
129+
var changingDEs []fs.DirEntry
130+
for _, entry := range entries {
131+
f, err := d.fs.Open(entry.Name())
132+
if err != nil {
133+
return nil, err
134+
}
135+
info, err := f.Stat()
136+
if err != nil {
137+
return nil, err
138+
}
139+
f.Close()
140+
changingDEs = append(changingDEs, fs.FileInfoToDirEntry(info))
141+
142+
}
143+
return changingDEs, nil
144+
}
145+
146+
func (f changingFile) ModTime() time.Time {
147+
original := f.FileInfo.ModTime()
148+
if f.parent.changeModTime {
149+
newTime := original.Add(time.Duration(f.parent.count) * time.Minute)
150+
return newTime
151+
}
152+
153+
return original
154+
}
155+
156+
func newChangingFS(fsys fs.FS, changeModTime, changeData bool) (fs.FS, error) {
157+
var secondDir string
158+
root, err := fsys.Open(".")
159+
if err != nil {
160+
return nil, err
161+
}
162+
defer root.Close()
163+
rootEntries, err := root.(fs.ReadDirFile).ReadDir(-1)
164+
if err != nil {
165+
return nil, err
166+
}
167+
for range 2 {
168+
for _, entry := range rootEntries {
169+
if entry.IsDir() {
170+
secondDir = entry.Name()
171+
break
172+
}
173+
}
174+
}
175+
if secondDir == "" {
176+
return nil, fmt.Errorf("no directories found in root")
177+
}
178+
179+
var lastDirFirstFile string
180+
lastDirF, err := fsys.Open(secondDir)
181+
if err != nil {
182+
return nil, err
183+
}
184+
defer lastDirF.Close()
185+
lastDirEntries, err := lastDirF.(fs.ReadDirFile).ReadDir(-1)
186+
if err != nil {
187+
return nil, err
188+
}
189+
for _, entry := range lastDirEntries {
190+
if !entry.IsDir() {
191+
lastDirFirstFile = entry.Name()
192+
break
193+
}
194+
}
195+
if lastDirFirstFile == "" {
196+
return nil, fmt.Errorf("no files found in last directory %s", secondDir)
197+
}
198+
199+
return &changingFS{
200+
FS: fsys,
201+
changingFile: path.Join(secondDir, lastDirFirstFile),
202+
changeModTime: changeModTime,
203+
changeData: changeData,
204+
}, nil
205+
}
206+
207+
func Demo(ctx context.Context, repo *sqlrepo.Repo, spaceName string, alterMetadata, alterData bool) error {
208+
hash := sha512.Sum512_256([]byte(spaceName))
209+
space, err := signer.FromRaw(ed25519.NewKeyFromSeed(hash[:]))
210+
if err != nil {
211+
return fmt.Errorf("command failed to create space key: %w", err)
212+
}
213+
spaceDID := space.DID()
214+
215+
baseClient, err := ctestutil.Client(
216+
ctestutil.WithClientOptions(
217+
// Act as space to avoid auth issues
218+
client.WithPrincipal(space),
219+
),
220+
ctestutil.WithSpaceBlobAdd(),
221+
222+
ctestutil.WithServerOptions(
223+
server.WithServiceMethod(
224+
spaceindexcap.Add.Can(),
225+
server.Provide(
226+
spaceindexcap.Add,
227+
func(
228+
ctx context.Context,
229+
cap ucan.Capability[spaceindexcap.AddCaveats],
230+
inv invocation.Invocation,
231+
context server.InvocationContext,
232+
) (result.Result[spaceindexcap.AddOk, failure.IPLDBuilderFailure], fx.Effects, error) {
233+
return result.Ok[spaceindexcap.AddOk, failure.IPLDBuilderFailure](spaceindexcap.AddOk{}), nil, nil
234+
},
235+
),
236+
),
237+
),
238+
239+
ctestutil.WithServerOptions(
240+
server.WithServiceMethod(
241+
uploadcap.Add.Can(),
242+
server.Provide(
243+
uploadcap.Add,
244+
func(
245+
ctx context.Context,
246+
cap ucan.Capability[uploadcap.AddCaveats],
247+
inv invocation.Invocation,
248+
context server.InvocationContext,
249+
) (result.Result[uploadcap.AddOk, failure.IPLDBuilderFailure], fx.Effects, error) {
250+
return result.Ok[uploadcap.AddOk, failure.IPLDBuilderFailure](uploadcap.AddOk{
251+
Root: cap.Nb().Root,
252+
Shards: cap.Nb().Shards,
253+
}), nil, nil
254+
},
255+
),
256+
),
257+
),
258+
)
259+
if err != nil {
260+
return fmt.Errorf("command failed to create client: %w", err)
261+
}
262+
customPutClient := &ctestutil.ClientWithCustomPut{
263+
Client: baseClient,
264+
PutClient: &http.Client{Transport: nullTransport{}},
265+
}
266+
fsys, err := newChangingFS(fakefs.New(0), alterMetadata, alterData)
267+
if err != nil {
268+
return fmt.Errorf("creating changing FS: %w", err)
269+
}
270+
api := preparation.NewAPI(repo, customPutClient, preparation.WithGetLocalFSForPathFn(func(path string) (fs.FS, error) {
271+
return fsys, nil
272+
}))
273+
274+
uploads, err := api.FindOrCreateUploads(ctx, spaceDID)
275+
if err != nil {
276+
return fmt.Errorf("command failed to create uploads: %w", err)
277+
}
278+
279+
if len(uploads) == 0 {
280+
// Try adding the source and running again.
281+
282+
_, err = api.FindOrCreateSpace(ctx, spaceDID, spaceDID.String())
283+
if err != nil {
284+
return fmt.Errorf("command failed to create space: %w", err)
285+
}
286+
287+
source, err := api.CreateSource(ctx, ".", ".")
288+
if err != nil {
289+
return fmt.Errorf("command failed to create source: %w", err)
290+
}
291+
292+
err = repo.AddSourceToSpace(ctx, spaceDID, source.ID())
293+
if err != nil {
294+
return fmt.Errorf("command failed to add source to space: %w", err)
295+
}
296+
297+
uploads, err = api.FindOrCreateUploads(ctx, spaceDID)
298+
if err != nil {
299+
return fmt.Errorf("command failed to create uploads: %w", err)
300+
}
301+
302+
}
303+
304+
return ui.RunUploadUI(ctx, repo, api, uploads)
305+
}

cmd/internal/upload/repo/repo.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package repo
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
8+
"github.com/storacha/guppy/pkg/preparation/sqlrepo"
9+
_ "modernc.org/sqlite"
10+
)
11+
12+
func Make(ctx context.Context, dbPath string) (*sqlrepo.Repo, func() error, error) {
13+
db, err := sql.Open("sqlite", dbPath)
14+
if err != nil {
15+
return nil, nil, fmt.Errorf("command failed to open SQLite database at %s: %w", dbPath, err)
16+
}
17+
db.SetMaxOpenConns(1)
18+
19+
_, err = db.ExecContext(ctx, sqlrepo.Schema)
20+
if err != nil {
21+
return nil, nil, fmt.Errorf("command failed to execute schema: %w", err)
22+
}
23+
24+
repo := sqlrepo.New(db)
25+
return repo, db.Close, nil
26+
}

0 commit comments

Comments
 (0)