Skip to content

Commit a19efcc

Browse files
committed
feat(dispatch): Abs paths on inputs to dispatch methods
Background: when `qri connect` is running, it holds an exclusive lock, and other invocations of qri will send their commands to that connected process using RPC. The two processes may be running in different directories, so it is necessary to change file paths to be absolute before sending the params across RPC. Before, we would do this explicitly, which was error-prone and easy to forget about, since the broken behavior would not happen in the common case. The proposed solution implemented here adds tags to structs that are passed to methods. Most use `qri:"fspath"` to specify fields that are file system paths. Diff is special and uses `qri:"dsrefOrFspath"` because its inputs can be either dsrefs or file paths. Adding this behavior for new commands is simple; it just requires adding this tag. An alternative would be to add a type for qfs.FilePath that does this behavior itself. However, that requires users of qri to know about this type, and FilePath types are typically quite tricky to fully implement. This solution is simpler and adds less maintenance vs creating our own path type, though it is admittedly a bit magical. Personally, I don't think it's that bad, as it matches the standard use case of tags for serialization, if we view fully absolute paths being sent over RPC as a type of data marshalling. Plus, the Diff inputs act quite special. Simply creating a qfs.FilePath would not be sufficient to handle Diff's parameters, and fixing Diff in some way would probably add more complication than its worth.
1 parent de9b6de commit a19efcc

File tree

7 files changed

+63
-61
lines changed

7 files changed

+63
-61
lines changed

cmd/checkout.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"strings"
77

88
"github.com/qri-io/ioes"
9-
"github.com/qri-io/qfs"
109
"github.com/qri-io/qri/dsref"
1110
"github.com/qri-io/qri/lib"
1211
"github.com/spf13/cobra"
@@ -88,10 +87,6 @@ func (o *CheckoutOptions) Run() (err error) {
8887
o.Dir = dsref.GenerateName(ref[pos+1:], "")
8988
}
9089

91-
if err = qfs.AbsPath(&o.Dir); err != nil {
92-
return err
93-
}
94-
9590
if err = inst.Filesys().Checkout(ctx, &lib.LinkParams{Dir: o.Dir, Refstr: ref}); err != nil {
9691
return err
9792
}

cmd/save.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"strings"
77

88
"github.com/qri-io/ioes"
9-
"github.com/qri-io/qfs"
109
"github.com/qri-io/qri/dsref"
1110
"github.com/qri-io/qri/lib"
1211
"github.com/qri-io/qri/repo"
@@ -131,18 +130,6 @@ func (o *SaveOptions) Complete(f Factory, args []string) (err error) {
131130
}
132131
}
133132

134-
// Make all paths absolute. Especially important if we are running
135-
// `qri connect` in a different terminal, and that instance is in a different directory;
136-
// that instance won't correctly find the body file we want to load if it's not absolute.
137-
for i := range o.FilePaths {
138-
if err = qfs.AbsPath(&o.FilePaths[i]); err != nil {
139-
return
140-
}
141-
}
142-
143-
if err := qfs.AbsPath(&o.BodyPath); err != nil {
144-
return fmt.Errorf("body file: %s", err)
145-
}
146133
return nil
147134
}
148135

fsi/init.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ import (
1818

1919
// InitParams encapsulates parameters for fsi.InitDataset
2020
type InitParams struct {
21-
TargetDir string `json:"targetDir"`
21+
TargetDir string `json:"targetDir" qri:"fspath"`
2222
Name string `json:"name"`
2323
Format string `json:"format"`
24-
BodyPath string `json:"bodyPath"`
24+
BodyPath string `json:"bodyPath" qri:"fspath"`
2525
UseDscache bool `json:"useDscache"`
2626
}
2727

lib/datasets.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ type GetParams struct {
108108
All bool `json:"all"`
109109

110110
// outfile is a filename to save the dataset to
111-
Outfile string `json:"outfile"`
111+
Outfile string `json:"outfile" qri:"fspath"`
112112
// whether to generate a filename from the dataset name instead
113113
GenFilename bool `json:"genfilename"`
114114
Remote string `json:"remote"`
@@ -249,11 +249,6 @@ type DataResponse struct {
249249
// then res.Bytes is loaded with the body. If the selector is "stats", then res.Bytes is loaded
250250
// with the generated stats.
251251
func (m DatasetMethods) Get(ctx context.Context, p *GetParams) (*GetResult, error) {
252-
// TODO(dustmop): Have Dispatch perform this AbsPath call automatically
253-
if err := qfs.AbsPath(&p.Outfile); err != nil {
254-
return nil, err
255-
}
256-
257252
got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "get"), p)
258253
if res, ok := got.(*GetResult); ok {
259254
return res, err
@@ -305,9 +300,9 @@ type SaveParams struct {
305300
// commit message, defaults to blank
306301
Message string
307302
// path to body data
308-
BodyPath string
303+
BodyPath string `qri:"fspath"`
309304
// absolute path or URL to the list of dataset files or components to load
310-
FilePaths []string
305+
FilePaths []string `qri:"fspath"`
311306
// secrets for transform execution
312307
Secrets map[string]string
313308
// optional writer to have transform script record standard output to
@@ -513,7 +508,7 @@ func (m DatasetMethods) Remove(ctx context.Context, p *RemoveParams) (*RemoveRes
513508
// PullParams encapsulates parameters to the add command
514509
type PullParams struct {
515510
Ref string
516-
LinkDir string
511+
LinkDir string `qri:"fspath"`
517512
Remote string // remote to attempt to pull from
518513
LogsOnly bool // only fetch logbook data
519514
}
@@ -530,9 +525,6 @@ func (p *PullParams) UnmarshalFromRequest(r *http.Request) error {
530525
// Pull downloads and stores an existing dataset to a peer's repository via
531526
// a network connection
532527
func (m DatasetMethods) Pull(ctx context.Context, p *PullParams) (*dataset.Dataset, error) {
533-
if err := qfs.AbsPath(&p.LinkDir); err != nil {
534-
return nil, err
535-
}
536528
got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "pull"), p)
537529
if res, ok := got.(*dataset.Dataset); ok {
538530
return res, err

lib/diff.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77

88
"github.com/qri-io/dataset/tabular"
99
"github.com/qri-io/deepdiff"
10-
"github.com/qri-io/qfs"
1110
"github.com/qri-io/qri/base/component"
1211
"github.com/qri-io/qri/base/dsfs"
1312
"github.com/qri-io/qri/dsref"
@@ -27,10 +26,10 @@ type DiffStat = deepdiff.Stats
2726
// LeftSide set with the UseLeftPrevVersion flag.
2827
type DiffParams struct {
2928
// File paths or reference to datasets
30-
LeftSide string `schema:"leftPath" json:"leftPath"`
31-
RightSide string `schema:"rightPath" json:"rightPath"`
29+
LeftSide string `schema:"leftPath" json:"leftPath" qri:"dsrefOrFspath"`
30+
RightSide string `schema:"rightPath" json:"rightPath" qri:"dsrefOrFspath"`
3231
// If not null, the working directory that the diff is using
33-
WorkingDir string
32+
WorkingDir string `qri:"fspath"`
3433
// Whether to get the previous version of the left parameter
3534
UseLeftPrevVersion bool
3635

@@ -107,17 +106,6 @@ const (
107106

108107
// Diff computes the diff of two sources
109108
func (m DatasetMethods) Diff(ctx context.Context, p *DiffParams) (*DiffResponse, error) {
110-
// absolutize any local paths before a possible trip over RPC to another local process
111-
if !dsref.IsRefString(p.LeftSide) {
112-
if err := qfs.AbsPath(&p.LeftSide); err != nil {
113-
return nil, err
114-
}
115-
}
116-
if !dsref.IsRefString(p.RightSide) {
117-
if err := qfs.AbsPath(&p.RightSide); err != nil {
118-
return nil, err
119-
}
120-
}
121109
got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "diff"), p)
122110
if res, ok := got.(*DiffResponse); ok {
123111
return res, err

lib/dispatch.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"strings"
99
"time"
1010

11+
"github.com/qri-io/qfs"
1112
"github.com/qri-io/qri/auth/token"
13+
"github.com/qri-io/qri/dsref"
1214
"github.com/qri-io/qri/profile"
1315
)
1416

@@ -135,6 +137,9 @@ func (inst *Instance) Dispatch(ctx context.Context, method string, param interfa
135137
return nil, nil, err
136138
}
137139

140+
// Handle filepaths in the params by calling qfs.Abs on each of them
141+
param = normalizeInputParams(param)
142+
138143
// Construct the parameter list for the function call, then call it
139144
args := make([]reflect.Value, 3)
140145
args[0] = reflect.ValueOf(c.Impl)
@@ -417,3 +422,50 @@ func dispatchReturnError(got interface{}, err error) error {
417422
}
418423
return err
419424
}
425+
426+
// normalizeInputParams will look at each field of the params, and modify filepaths so that
427+
// they are absolute paths, making them safe to send across RPC to another process
428+
func normalizeInputParams(param interface{}) interface{} {
429+
typ := reflect.TypeOf(param)
430+
val := reflect.ValueOf(param)
431+
if typ.Kind() == reflect.Ptr {
432+
typ = typ.Elem()
433+
val = val.Elem()
434+
}
435+
if typ.Kind() == reflect.Struct {
436+
num := typ.NumField()
437+
for i := 0; i < num; i++ {
438+
tfield := typ.Field(i)
439+
vfield := val.Field(i)
440+
qriTag := tfield.Tag.Get("qri")
441+
if qriTag == "fspath" || qriTag == "dsrefOrFspath" {
442+
normalizeFileField(vfield, qriTag)
443+
} else if qriTag != "" {
444+
log.Errorf("unknown qri struct tag %q", qriTag)
445+
}
446+
}
447+
}
448+
return param
449+
}
450+
451+
func normalizeFileField(vfield reflect.Value, qriTag string) {
452+
interf := vfield.Interface()
453+
if str, ok := interf.(string); ok {
454+
if qriTag == "dsrefOrFspath" && dsref.IsRefString(str) {
455+
return
456+
}
457+
if err := qfs.AbsPath(&str); err == nil {
458+
vfield.SetString(str)
459+
}
460+
}
461+
if strList, ok := interf.([]string); ok {
462+
build := make([]string, 0, len(strList))
463+
for _, str := range strList {
464+
if qriTag != "dsrefOrFspath" || !dsref.IsRefString(str) {
465+
_ = qfs.AbsPath(&str)
466+
}
467+
build = append(build, str)
468+
}
469+
vfield.Set(reflect.ValueOf(build))
470+
}
471+
}

lib/fsi.go

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"path/filepath"
1010

1111
"github.com/qri-io/dataset"
12-
"github.com/qri-io/qfs"
1312
"github.com/qri-io/qri/base"
1413
"github.com/qri-io/qri/base/component"
1514
"github.com/qri-io/qri/base/dsfs"
@@ -46,7 +45,7 @@ func (m FSIMethods) Attributes() map[string]AttributeSet {
4645

4746
// LinkParams encapsulate parameters for linked datasets
4847
type LinkParams struct {
49-
Dir string
48+
Dir string `qri:"fspath"`
5049
Refstr string
5150
}
5251

@@ -65,7 +64,7 @@ type FSIWriteParams struct {
6564

6665
// RestoreParams provides parameters to the restore method.
6766
type RestoreParams struct {
68-
Dir string
67+
Dir string `qri:"fspath"`
6968
Refstr string
7069
Path string
7170
Component string
@@ -100,10 +99,6 @@ func (m FSIMethods) Unlink(ctx context.Context, p *LinkParams) (string, error) {
10099
// Status checks for any modifications or errors in a linked directory against its previous
101100
// version in the repo. Must only be called if FSI is enabled for this dataset.
102101
func (m FSIMethods) Status(ctx context.Context, p *LinkParams) ([]StatusItem, error) {
103-
// TODO(dustmop): Have Dispatch perform this AbsPath call automatically
104-
if err := qfs.AbsPath(&p.Dir); err != nil {
105-
return nil, err
106-
}
107102
got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "status"), p)
108103
if res, ok := got.([]StatusItem); ok {
109104
return res, err
@@ -144,13 +139,6 @@ func (m FSIMethods) Restore(ctx context.Context, p *RestoreParams) error {
144139

145140
// Init initializes a new working directory for a linked dataset
146141
func (m FSIMethods) Init(ctx context.Context, p *InitDatasetParams) (string, error) {
147-
// TODO(dustmop): Have Dispatch perform these AbsPath calls automatically
148-
if err := qfs.AbsPath(&p.TargetDir); err != nil {
149-
return "", err
150-
}
151-
if err := qfs.AbsPath(&p.BodyPath); err != nil {
152-
return "", err
153-
}
154142
got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "init"), p)
155143
if res, ok := got.(string); ok {
156144
return res, err

0 commit comments

Comments
 (0)