2019-07-12 10:17:16 +00:00
|
|
|
package cli
|
|
|
|
|
|
|
|
import (
|
2020-07-21 23:43:57 +00:00
|
|
|
"encoding/json"
|
2019-07-12 10:17:16 +00:00
|
|
|
"fmt"
|
2019-11-08 01:45:45 +00:00
|
|
|
"os"
|
2019-10-23 07:05:22 +00:00
|
|
|
"path/filepath"
|
2020-07-07 11:45:02 +00:00
|
|
|
"sort"
|
2019-08-02 14:09:54 +00:00
|
|
|
"strconv"
|
2019-11-08 01:45:45 +00:00
|
|
|
"text/tabwriter"
|
2019-07-16 16:07:08 +00:00
|
|
|
|
2019-08-02 14:09:54 +00:00
|
|
|
"github.com/ipfs/go-cid"
|
2020-06-05 21:22:29 +00:00
|
|
|
"github.com/ipfs/go-cidutil/cidenc"
|
2019-09-13 21:00:36 +00:00
|
|
|
"github.com/libp2p/go-libp2p-core/peer"
|
2020-06-05 21:22:29 +00:00
|
|
|
"github.com/multiformats/go-multibase"
|
2020-06-02 18:12:53 +00:00
|
|
|
"github.com/urfave/cli/v2"
|
2020-06-05 22:59:01 +00:00
|
|
|
"golang.org/x/xerrors"
|
2019-08-02 14:09:54 +00:00
|
|
|
|
2019-12-19 20:13:17 +00:00
|
|
|
"github.com/filecoin-project/go-address"
|
2020-01-24 21:44:28 +00:00
|
|
|
"github.com/filecoin-project/go-fil-markets/storagemarket"
|
2020-07-28 06:13:10 +00:00
|
|
|
"github.com/filecoin-project/go-multistore"
|
2020-03-04 02:19:28 +00:00
|
|
|
"github.com/filecoin-project/specs-actors/actors/abi"
|
2020-04-28 17:12:28 +00:00
|
|
|
"github.com/filecoin-project/specs-actors/actors/builtin/market"
|
2020-02-13 03:50:37 +00:00
|
|
|
|
2020-06-16 14:14:49 +00:00
|
|
|
"github.com/filecoin-project/lotus/api"
|
2020-03-03 00:36:01 +00:00
|
|
|
lapi "github.com/filecoin-project/lotus/api"
|
2019-10-18 04:47:41 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/types"
|
2019-07-12 10:17:16 +00:00
|
|
|
)
|
|
|
|
|
2020-06-05 21:22:29 +00:00
|
|
|
var CidBaseFlag = cli.StringFlag{
|
|
|
|
Name: "cid-base",
|
|
|
|
Hidden: true,
|
|
|
|
Value: "base32",
|
|
|
|
Usage: "Multibase encoding used for version 1 CIDs in output.",
|
|
|
|
DefaultText: "base32",
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetCidEncoder returns an encoder using the `cid-base` flag if provided, or
|
|
|
|
// the default (Base32) encoder if not.
|
|
|
|
func GetCidEncoder(cctx *cli.Context) (cidenc.Encoder, error) {
|
|
|
|
val := cctx.String("cid-base")
|
|
|
|
|
|
|
|
e := cidenc.Encoder{Base: multibase.MustNewEncoder(multibase.Base32)}
|
|
|
|
|
|
|
|
if val != "" {
|
|
|
|
var err error
|
|
|
|
e.Base, err = multibase.EncoderByName(val)
|
|
|
|
if err != nil {
|
|
|
|
return e, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return e, nil
|
|
|
|
}
|
|
|
|
|
2019-07-12 10:17:16 +00:00
|
|
|
var clientCmd = &cli.Command{
|
|
|
|
Name: "client",
|
|
|
|
Usage: "Make deals, store data, retrieve data",
|
|
|
|
Subcommands: []*cli.Command{
|
2020-07-24 01:52:29 +00:00
|
|
|
WithCategory("storage", clientDealCmd),
|
|
|
|
WithCategory("storage", clientQueryAskCmd),
|
|
|
|
WithCategory("storage", clientListDeals),
|
|
|
|
WithCategory("storage", clientGetDealCmd),
|
|
|
|
WithCategory("data", clientImportCmd),
|
|
|
|
WithCategory("data", clientDropCmd),
|
|
|
|
WithCategory("data", clientLocalCmd),
|
|
|
|
WithCategory("retrieval", clientFindCmd),
|
|
|
|
WithCategory("retrieval", clientRetrieveCmd),
|
|
|
|
WithCategory("util", clientCommPCmd),
|
|
|
|
WithCategory("util", clientCarGenCmd),
|
2019-07-12 10:17:16 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var clientImportCmd = &cli.Command{
|
2020-03-06 19:01:28 +00:00
|
|
|
Name: "import",
|
|
|
|
Usage: "Import data",
|
2020-03-04 21:46:00 +00:00
|
|
|
ArgsUsage: "[inputPath]",
|
2020-03-03 04:13:08 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.BoolFlag{
|
|
|
|
Name: "car",
|
2020-04-03 22:17:57 +00:00
|
|
|
Usage: "import from a car file instead of a regular file",
|
2020-03-03 04:13:08 +00:00
|
|
|
},
|
2020-07-07 11:45:02 +00:00
|
|
|
&cli.BoolFlag{
|
|
|
|
Name: "quiet",
|
|
|
|
Aliases: []string{"q"},
|
|
|
|
Usage: "Output root CID only",
|
|
|
|
},
|
2020-06-05 21:22:29 +00:00
|
|
|
&CidBaseFlag,
|
2020-03-03 04:13:08 +00:00
|
|
|
},
|
2019-07-12 10:17:16 +00:00
|
|
|
Action: func(cctx *cli.Context) error {
|
2019-10-03 18:12:30 +00:00
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
2019-07-12 10:17:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-10-03 18:12:30 +00:00
|
|
|
defer closer()
|
2019-07-18 23:16:23 +00:00
|
|
|
ctx := ReqContext(cctx)
|
2020-07-13 05:13:29 +00:00
|
|
|
|
|
|
|
if cctx.NArg() != 1 {
|
|
|
|
return xerrors.New("expected input path as the only arg")
|
|
|
|
}
|
|
|
|
|
2019-10-23 07:05:22 +00:00
|
|
|
absPath, err := filepath.Abs(cctx.Args().First())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-07-12 10:17:16 +00:00
|
|
|
|
2020-03-03 04:13:08 +00:00
|
|
|
ref := lapi.FileRef{
|
|
|
|
Path: absPath,
|
|
|
|
IsCAR: cctx.Bool("car"),
|
|
|
|
}
|
|
|
|
c, err := api.ClientImport(ctx, ref)
|
2019-07-12 10:17:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-06-05 20:04:28 +00:00
|
|
|
|
2020-06-05 20:33:45 +00:00
|
|
|
encoder, err := GetCidEncoder(cctx)
|
2020-06-05 20:04:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-07-07 11:45:02 +00:00
|
|
|
if !cctx.Bool("quiet") {
|
|
|
|
fmt.Printf("Import %d, Root ", c.ImportID)
|
|
|
|
}
|
|
|
|
fmt.Println(encoder.Encode(c.Root))
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var clientDropCmd = &cli.Command{
|
|
|
|
Name: "drop",
|
|
|
|
Usage: "Remove import",
|
|
|
|
ArgsUsage: "[import ID...]",
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
if !cctx.Args().Present() {
|
|
|
|
return xerrors.Errorf("no imports specified")
|
|
|
|
}
|
|
|
|
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
2020-07-28 06:13:10 +00:00
|
|
|
var ids []multistore.StoreID
|
2020-07-07 11:45:02 +00:00
|
|
|
for i, s := range cctx.Args().Slice() {
|
2020-07-17 20:14:03 +00:00
|
|
|
id, err := strconv.ParseInt(s, 10, 0)
|
2020-07-07 11:45:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("parsing %d-th import ID: %w", i, err)
|
|
|
|
}
|
|
|
|
|
2020-07-28 06:13:10 +00:00
|
|
|
ids = append(ids, multistore.StoreID(id))
|
2020-07-07 11:45:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, id := range ids {
|
|
|
|
if err := api.ClientRemoveImport(ctx, id); err != nil {
|
|
|
|
return xerrors.Errorf("removing import %d: %w", id, err)
|
|
|
|
}
|
|
|
|
}
|
2020-06-05 20:04:28 +00:00
|
|
|
|
2019-07-12 10:17:16 +00:00
|
|
|
return nil
|
|
|
|
},
|
2019-07-12 10:44:01 +00:00
|
|
|
}
|
|
|
|
|
2020-04-03 22:17:57 +00:00
|
|
|
var clientCommPCmd = &cli.Command{
|
|
|
|
Name: "commP",
|
2020-07-24 01:52:29 +00:00
|
|
|
Usage: "Calculate the piece-cid (commP) of a CAR file",
|
2020-04-03 22:17:57 +00:00
|
|
|
ArgsUsage: "[inputFile minerAddress]",
|
2020-06-05 21:22:29 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
&CidBaseFlag,
|
|
|
|
},
|
2020-04-03 22:17:57 +00:00
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
|
|
|
if cctx.Args().Len() != 2 {
|
|
|
|
return fmt.Errorf("usage: commP <inputPath> <minerAddr>")
|
|
|
|
}
|
|
|
|
|
|
|
|
miner, err := address.NewFromString(cctx.Args().Get(1))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
ret, err := api.ClientCalcCommP(ctx, cctx.Args().Get(0), miner)
|
2020-06-05 20:13:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-04-03 22:17:57 +00:00
|
|
|
|
2020-06-05 20:33:45 +00:00
|
|
|
encoder, err := GetCidEncoder(cctx)
|
2020-04-03 22:17:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-06-05 20:13:16 +00:00
|
|
|
|
2020-06-05 20:33:45 +00:00
|
|
|
fmt.Println("CID: ", encoder.Encode(ret.Root))
|
2020-06-09 18:04:17 +00:00
|
|
|
fmt.Println("Piece size: ", types.SizeStr(types.NewInt(uint64(ret.Size))))
|
2020-04-03 22:17:57 +00:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var clientCarGenCmd = &cli.Command{
|
|
|
|
Name: "generate-car",
|
2020-07-24 01:52:29 +00:00
|
|
|
Usage: "Generate a car file from input",
|
2020-04-03 22:17:57 +00:00
|
|
|
ArgsUsage: "[inputPath outputPath]",
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
|
|
|
if cctx.Args().Len() != 2 {
|
|
|
|
return fmt.Errorf("usage: generate-car <inputPath> <outputPath>")
|
|
|
|
}
|
|
|
|
|
|
|
|
ref := lapi.FileRef{
|
|
|
|
Path: cctx.Args().First(),
|
|
|
|
IsCAR: false,
|
|
|
|
}
|
|
|
|
|
|
|
|
op := cctx.Args().Get(1)
|
|
|
|
|
|
|
|
if err = api.ClientGenCar(ctx, ref, op); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2019-07-12 10:44:01 +00:00
|
|
|
var clientLocalCmd = &cli.Command{
|
|
|
|
Name: "local",
|
|
|
|
Usage: "List locally imported data",
|
2020-06-05 21:22:29 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
&CidBaseFlag,
|
|
|
|
},
|
2019-07-12 10:44:01 +00:00
|
|
|
Action: func(cctx *cli.Context) error {
|
2019-10-03 18:12:30 +00:00
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
2019-07-12 10:44:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-10-03 18:12:30 +00:00
|
|
|
defer closer()
|
2019-07-18 23:16:23 +00:00
|
|
|
ctx := ReqContext(cctx)
|
2019-07-12 10:44:01 +00:00
|
|
|
|
|
|
|
list, err := api.ClientListImports(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-06-05 20:04:28 +00:00
|
|
|
|
2020-06-05 20:33:45 +00:00
|
|
|
encoder, err := GetCidEncoder(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-07-07 11:45:02 +00:00
|
|
|
sort.Slice(list, func(i, j int) bool {
|
|
|
|
return list[i].Key < list[j].Key
|
|
|
|
})
|
|
|
|
|
2019-07-12 10:44:01 +00:00
|
|
|
for _, v := range list {
|
2020-07-07 08:52:19 +00:00
|
|
|
cidStr := "<nil>"
|
|
|
|
if v.Root != nil {
|
|
|
|
cidStr = encoder.Encode(*v.Root)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("%d: %s @%s (%s)\n", v.Key, cidStr, v.FilePath, v.Source)
|
2020-07-07 09:12:32 +00:00
|
|
|
if v.Err != "" {
|
2020-07-07 08:52:19 +00:00
|
|
|
fmt.Printf("\terror: %s\n", v.Err)
|
|
|
|
}
|
2019-07-12 10:44:01 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
2019-08-02 14:09:54 +00:00
|
|
|
|
|
|
|
var clientDealCmd = &cli.Command{
|
2020-03-06 19:01:28 +00:00
|
|
|
Name: "deal",
|
|
|
|
Usage: "Initialize storage deal with a miner",
|
2020-03-04 21:46:00 +00:00
|
|
|
ArgsUsage: "[dataCid miner price duration]",
|
2020-02-08 00:18:14 +00:00
|
|
|
Flags: []cli.Flag{
|
2020-03-03 00:36:01 +00:00
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "manual-piece-cid",
|
2020-04-06 03:30:11 +00:00
|
|
|
Usage: "manually specify piece commitment for data (dataCid must be to a car file)",
|
2020-03-03 00:36:01 +00:00
|
|
|
},
|
2020-03-04 02:19:28 +00:00
|
|
|
&cli.Int64Flag{
|
|
|
|
Name: "manual-piece-size",
|
2020-04-06 03:30:11 +00:00
|
|
|
Usage: "if manually specifying piece cid, used to specify size (dataCid must be to a car file)",
|
2020-03-04 02:19:28 +00:00
|
|
|
},
|
2020-03-04 02:49:00 +00:00
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "from",
|
|
|
|
Usage: "specify address to fund the deal with",
|
|
|
|
},
|
2020-04-30 17:42:16 +00:00
|
|
|
&cli.Int64Flag{
|
|
|
|
Name: "start-epoch",
|
|
|
|
Usage: "specify the epoch that the deal should start at",
|
|
|
|
Value: -1,
|
|
|
|
},
|
2020-06-30 21:08:39 +00:00
|
|
|
&cli.BoolFlag{
|
|
|
|
Name: "fast-retrieval",
|
|
|
|
Usage: "indicates that data should be available for fast retrieval",
|
|
|
|
Value: true,
|
|
|
|
},
|
2020-06-30 21:17:21 +00:00
|
|
|
&cli.BoolFlag{
|
|
|
|
Name: "verified-deal",
|
|
|
|
Usage: "indicate that the deal counts towards verified client total",
|
2020-06-30 21:24:51 +00:00
|
|
|
Value: false,
|
2020-06-30 21:17:21 +00:00
|
|
|
},
|
2020-06-05 21:22:29 +00:00
|
|
|
&CidBaseFlag,
|
2020-02-08 00:18:14 +00:00
|
|
|
},
|
2019-08-02 14:09:54 +00:00
|
|
|
Action: func(cctx *cli.Context) error {
|
2019-10-03 18:12:30 +00:00
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
2019-08-02 14:09:54 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-10-03 18:12:30 +00:00
|
|
|
defer closer()
|
2019-08-02 14:09:54 +00:00
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
2019-08-07 20:06:10 +00:00
|
|
|
if cctx.NArg() != 4 {
|
|
|
|
return xerrors.New("expected 4 args: dataCid, miner, price, duration")
|
2019-08-02 14:09:54 +00:00
|
|
|
}
|
|
|
|
|
2020-04-06 03:30:11 +00:00
|
|
|
// [data, miner, price, dur]
|
2019-08-02 14:09:54 +00:00
|
|
|
|
|
|
|
data, err := cid.Parse(cctx.Args().Get(0))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
miner, err := address.NewFromString(cctx.Args().Get(1))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-10-29 12:02:13 +00:00
|
|
|
price, err := types.ParseFIL(cctx.Args().Get(2))
|
2019-08-02 14:09:54 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-08-07 20:06:10 +00:00
|
|
|
dur, err := strconv.ParseInt(cctx.Args().Get(3), 10, 32)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-04 02:49:00 +00:00
|
|
|
var a address.Address
|
|
|
|
if from := cctx.String("from"); from != "" {
|
|
|
|
faddr, err := address.NewFromString(from)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to parse 'from' address: %w", err)
|
|
|
|
}
|
|
|
|
a = faddr
|
|
|
|
} else {
|
|
|
|
def, err := api.WalletDefaultAddress(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
a = def
|
2019-12-13 19:15:56 +00:00
|
|
|
}
|
2020-02-08 00:18:14 +00:00
|
|
|
|
|
|
|
ref := &storagemarket.DataRef{
|
|
|
|
TransferType: storagemarket.TTGraphsync,
|
|
|
|
Root: data,
|
|
|
|
}
|
|
|
|
|
2020-03-03 00:36:01 +00:00
|
|
|
if mpc := cctx.String("manual-piece-cid"); mpc != "" {
|
|
|
|
c, err := cid.Parse(mpc)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to parse provided manual piece cid: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-03-04 02:19:28 +00:00
|
|
|
ref.PieceCid = &c
|
|
|
|
|
|
|
|
psize := cctx.Int64("manual-piece-size")
|
|
|
|
if psize == 0 {
|
|
|
|
return xerrors.Errorf("must specify piece size when manually setting cid")
|
|
|
|
}
|
|
|
|
|
|
|
|
ref.PieceSize = abi.UnpaddedPieceSize(psize)
|
2020-04-06 03:30:11 +00:00
|
|
|
|
|
|
|
ref.TransferType = storagemarket.TTManual
|
2020-03-03 00:36:01 +00:00
|
|
|
}
|
|
|
|
|
2020-07-02 15:57:10 +00:00
|
|
|
// Check if the address is a verified client
|
2020-07-07 13:59:14 +00:00
|
|
|
dcap, err := api.StateVerifiedClientStatus(ctx, a, types.EmptyTSK)
|
|
|
|
if err != nil {
|
2020-07-02 15:57:10 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-07-07 13:59:14 +00:00
|
|
|
isVerified := dcap != nil
|
|
|
|
|
2020-07-02 15:57:10 +00:00
|
|
|
// If the user has explicitly set the --verified-deal flag
|
|
|
|
if cctx.IsSet("verified-deal") {
|
|
|
|
// If --verified-deal is true, but the address is not a verified
|
|
|
|
// client, return an error
|
|
|
|
verifiedDealParam := cctx.Bool("verified-deal")
|
2020-07-02 18:49:08 +00:00
|
|
|
if verifiedDealParam && !isVerified {
|
2020-07-02 15:57:10 +00:00
|
|
|
return xerrors.Errorf("address %s does not have verified client status", a)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Override the default
|
2020-07-02 18:49:08 +00:00
|
|
|
isVerified = verifiedDealParam
|
2020-07-02 15:57:10 +00:00
|
|
|
}
|
|
|
|
|
2020-03-03 00:36:01 +00:00
|
|
|
proposal, err := api.ClientStartDeal(ctx, &lapi.StartDealParams{
|
2020-04-16 21:43:39 +00:00
|
|
|
Data: ref,
|
|
|
|
Wallet: a,
|
|
|
|
Miner: miner,
|
|
|
|
EpochPrice: types.BigInt(price),
|
|
|
|
MinBlocksDuration: uint64(dur),
|
2020-04-30 17:42:16 +00:00
|
|
|
DealStartEpoch: abi.ChainEpoch(cctx.Int64("start-epoch")),
|
2020-06-30 21:08:39 +00:00
|
|
|
FastRetrieval: cctx.Bool("fast-retrieval"),
|
2020-07-02 18:49:08 +00:00
|
|
|
VerifiedDeal: isVerified,
|
2020-03-03 00:36:01 +00:00
|
|
|
})
|
2019-08-07 20:06:10 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-06-05 20:33:45 +00:00
|
|
|
encoder, err := GetCidEncoder(cctx)
|
2020-06-05 20:13:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-06-05 20:33:45 +00:00
|
|
|
fmt.Println(encoder.Encode(*proposal))
|
2020-06-05 20:13:33 +00:00
|
|
|
|
2019-08-07 20:06:10 +00:00
|
|
|
return nil
|
2019-08-02 14:09:54 +00:00
|
|
|
},
|
|
|
|
}
|
2019-08-26 13:45:36 +00:00
|
|
|
|
|
|
|
var clientFindCmd = &cli.Command{
|
2020-03-06 19:01:28 +00:00
|
|
|
Name: "find",
|
2020-07-24 01:52:29 +00:00
|
|
|
Usage: "Find data in the network",
|
2020-03-04 21:46:00 +00:00
|
|
|
ArgsUsage: "[dataCid]",
|
2020-07-09 16:29:57 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "pieceCid",
|
|
|
|
Usage: "require data to be retrieved from a specific Piece CID",
|
|
|
|
},
|
|
|
|
},
|
2019-08-26 13:45:36 +00:00
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
if !cctx.Args().Present() {
|
2019-08-28 23:01:28 +00:00
|
|
|
fmt.Println("Usage: find [CID]")
|
2019-08-26 13:45:36 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
file, err := cid.Parse(cctx.Args().First())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-10-03 18:12:30 +00:00
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
2019-08-26 13:45:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-10-03 18:12:30 +00:00
|
|
|
defer closer()
|
2019-08-26 13:45:36 +00:00
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
|
|
|
// Check if we already have this data locally
|
|
|
|
|
|
|
|
has, err := api.ClientHasLocal(ctx, file)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if has {
|
|
|
|
fmt.Println("LOCAL")
|
|
|
|
}
|
|
|
|
|
2020-07-09 16:29:57 +00:00
|
|
|
var pieceCid *cid.Cid
|
|
|
|
if cctx.String("pieceCid") != "" {
|
|
|
|
parsed, err := cid.Parse(cctx.String("pieceCid"))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
pieceCid = &parsed
|
|
|
|
}
|
|
|
|
|
|
|
|
offers, err := api.ClientFindData(ctx, file, pieceCid)
|
2019-08-26 13:45:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, offer := range offers {
|
|
|
|
if offer.Err != "" {
|
|
|
|
fmt.Printf("ERR %s@%s: %s\n", offer.Miner, offer.MinerPeerID, offer.Err)
|
|
|
|
continue
|
|
|
|
}
|
2020-07-21 23:43:57 +00:00
|
|
|
fmt.Printf("RETRIEVAL %s@%s-%s-%s\n", offer.Miner, offer.MinerPeerID, types.FIL(offer.MinPrice), types.SizeStr(types.NewInt(offer.Size)))
|
2019-08-26 13:45:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2020-07-10 17:04:41 +00:00
|
|
|
const DefaultMaxRetrievePrice = 1
|
|
|
|
|
2019-08-26 13:45:36 +00:00
|
|
|
var clientRetrieveCmd = &cli.Command{
|
2020-03-06 19:01:28 +00:00
|
|
|
Name: "retrieve",
|
2020-07-24 01:52:29 +00:00
|
|
|
Usage: "Retrieve data from network",
|
2020-03-04 21:46:00 +00:00
|
|
|
ArgsUsage: "[dataCid outputPath]",
|
2019-09-17 07:55:33 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
2020-07-07 21:37:43 +00:00
|
|
|
Name: "from",
|
|
|
|
Usage: "address to send transactions from",
|
2019-09-17 07:55:33 +00:00
|
|
|
},
|
2020-03-03 04:13:08 +00:00
|
|
|
&cli.BoolFlag{
|
|
|
|
Name: "car",
|
|
|
|
Usage: "export to a car file instead of a regular file",
|
|
|
|
},
|
2020-06-16 14:14:49 +00:00
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "miner",
|
|
|
|
Usage: "miner address for retrieval, if not present it'll use local discovery",
|
|
|
|
},
|
2020-07-09 18:47:12 +00:00
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "maxPrice",
|
2020-07-10 17:04:41 +00:00
|
|
|
Usage: fmt.Sprintf("maximum price the client is willing to consider (default: %d FIL)", DefaultMaxRetrievePrice),
|
2020-07-09 18:47:12 +00:00
|
|
|
},
|
2020-07-09 16:29:57 +00:00
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "pieceCid",
|
|
|
|
Usage: "require data to be retrieved from a specific Piece CID",
|
|
|
|
},
|
2019-09-17 07:55:33 +00:00
|
|
|
},
|
2019-08-26 13:45:36 +00:00
|
|
|
Action: func(cctx *cli.Context) error {
|
2019-09-17 07:55:33 +00:00
|
|
|
if cctx.NArg() != 2 {
|
|
|
|
fmt.Println("Usage: retrieve [CID] [outfile]")
|
2019-08-26 13:45:36 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-06-16 14:14:49 +00:00
|
|
|
fapi, closer, err := GetFullNodeAPI(cctx)
|
2019-09-16 13:46:05 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-10-03 18:12:30 +00:00
|
|
|
defer closer()
|
2019-09-17 07:55:33 +00:00
|
|
|
ctx := ReqContext(cctx)
|
2019-09-16 13:46:05 +00:00
|
|
|
|
2019-09-17 07:55:33 +00:00
|
|
|
var payer address.Address
|
2020-07-07 21:37:43 +00:00
|
|
|
if cctx.String("from") != "" {
|
|
|
|
payer, err = address.NewFromString(cctx.String("from"))
|
2019-09-17 07:55:33 +00:00
|
|
|
} else {
|
2020-06-16 14:14:49 +00:00
|
|
|
payer, err = fapi.WalletDefaultAddress(ctx)
|
2019-09-17 07:55:33 +00:00
|
|
|
}
|
2019-08-26 13:45:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-09-17 07:55:33 +00:00
|
|
|
file, err := cid.Parse(cctx.Args().Get(0))
|
2019-08-26 13:45:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if we already have this data locally
|
|
|
|
|
2019-08-27 18:45:21 +00:00
|
|
|
/*has, err := api.ClientHasLocal(ctx, file)
|
2019-08-26 13:45:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if has {
|
|
|
|
fmt.Println("Success: Already in local storage")
|
|
|
|
return nil
|
2019-08-27 22:10:23 +00:00
|
|
|
}*/ // TODO: fix
|
2019-08-26 13:45:36 +00:00
|
|
|
|
2020-07-09 16:29:57 +00:00
|
|
|
var pieceCid *cid.Cid
|
|
|
|
if cctx.String("pieceCid") != "" {
|
|
|
|
parsed, err := cid.Parse(cctx.String("pieceCid"))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
pieceCid = &parsed
|
|
|
|
}
|
|
|
|
|
2020-06-16 14:14:49 +00:00
|
|
|
var offer api.QueryOffer
|
|
|
|
minerStrAddr := cctx.String("miner")
|
|
|
|
if minerStrAddr == "" { // Local discovery
|
2020-07-09 16:29:57 +00:00
|
|
|
offers, err := fapi.ClientFindData(ctx, file, pieceCid)
|
2020-07-09 18:47:12 +00:00
|
|
|
|
2020-07-21 23:43:57 +00:00
|
|
|
var cleaned []api.QueryOffer
|
|
|
|
// filter out offers that errored
|
|
|
|
for _, o := range offers {
|
2020-07-21 23:52:48 +00:00
|
|
|
if o.Err == "" {
|
2020-07-21 23:43:57 +00:00
|
|
|
cleaned = append(cleaned, o)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
offers = cleaned
|
|
|
|
|
2020-07-09 18:47:12 +00:00
|
|
|
// sort by price low to high
|
|
|
|
sort.Slice(offers, func(i, j int) bool {
|
|
|
|
return offers[i].MinPrice.LessThan(offers[j].MinPrice)
|
|
|
|
})
|
2020-06-16 14:14:49 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-08-26 13:45:36 +00:00
|
|
|
|
2020-06-16 14:14:49 +00:00
|
|
|
// TODO: parse offer strings from `client find`, make this smarter
|
|
|
|
if len(offers) < 1 {
|
|
|
|
fmt.Println("Failed to find file")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
offer = offers[0]
|
|
|
|
} else { // Directed retrieval
|
|
|
|
minerAddr, err := address.NewFromString(minerStrAddr)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-07-09 20:02:12 +00:00
|
|
|
offer, err = fapi.ClientMinerQueryOffer(ctx, minerAddr, file, pieceCid)
|
2020-06-16 14:14:49 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-11-20 08:47:24 +00:00
|
|
|
}
|
2020-06-16 20:32:03 +00:00
|
|
|
if offer.Err != "" {
|
|
|
|
return fmt.Errorf("The received offer errored: %s", offer.Err)
|
2019-11-20 08:47:24 +00:00
|
|
|
}
|
2019-09-16 13:46:05 +00:00
|
|
|
|
2020-07-10 19:13:52 +00:00
|
|
|
maxPrice := types.FromFil(DefaultMaxRetrievePrice)
|
2020-07-10 17:04:41 +00:00
|
|
|
|
2020-07-09 18:47:12 +00:00
|
|
|
if cctx.String("maxPrice") != "" {
|
2020-07-10 17:04:41 +00:00
|
|
|
maxPriceFil, err := types.ParseFIL(cctx.String("maxPrice"))
|
2020-07-09 18:47:12 +00:00
|
|
|
if err != nil {
|
2020-07-10 17:04:41 +00:00
|
|
|
return xerrors.Errorf("parsing maxPrice: %w", err)
|
2020-07-09 18:47:12 +00:00
|
|
|
}
|
|
|
|
|
2020-07-10 17:04:41 +00:00
|
|
|
maxPrice = types.BigInt(maxPriceFil)
|
|
|
|
}
|
|
|
|
|
|
|
|
if offer.MinPrice.GreaterThan(maxPrice) {
|
|
|
|
return xerrors.Errorf("failed to find offer satisfying maxPrice: %s", maxPrice)
|
2020-07-09 18:47:12 +00:00
|
|
|
}
|
|
|
|
|
2020-05-26 15:36:21 +00:00
|
|
|
ref := &lapi.FileRef{
|
2020-03-03 04:13:08 +00:00
|
|
|
Path: cctx.Args().Get(1),
|
|
|
|
IsCAR: cctx.Bool("car"),
|
|
|
|
}
|
2020-06-16 14:14:49 +00:00
|
|
|
if err := fapi.ClientRetrieve(ctx, offer.Order(payer), ref); err != nil {
|
2019-12-11 13:59:15 +00:00
|
|
|
return xerrors.Errorf("Retrieval Failed: %w", err)
|
2019-08-27 18:45:21 +00:00
|
|
|
}
|
2019-11-04 19:03:11 +00:00
|
|
|
|
|
|
|
fmt.Println("Success")
|
|
|
|
return nil
|
2019-08-26 13:45:36 +00:00
|
|
|
},
|
|
|
|
}
|
2019-09-13 21:00:36 +00:00
|
|
|
|
|
|
|
var clientQueryAskCmd = &cli.Command{
|
2020-03-06 19:01:28 +00:00
|
|
|
Name: "query-ask",
|
2020-07-24 01:52:29 +00:00
|
|
|
Usage: "Find a miners ask",
|
2020-03-04 21:46:00 +00:00
|
|
|
ArgsUsage: "[minerAddress]",
|
2019-09-13 21:00:36 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "peerid",
|
|
|
|
Usage: "specify peer ID of node to make query against",
|
|
|
|
},
|
2019-10-12 07:21:28 +00:00
|
|
|
&cli.Int64Flag{
|
|
|
|
Name: "size",
|
|
|
|
Usage: "data size in bytes",
|
|
|
|
},
|
|
|
|
&cli.Int64Flag{
|
|
|
|
Name: "duration",
|
|
|
|
Usage: "deal duration",
|
|
|
|
},
|
2019-09-13 21:00:36 +00:00
|
|
|
},
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
if cctx.NArg() != 1 {
|
2020-07-07 21:37:43 +00:00
|
|
|
fmt.Println("Usage: query-ask [minerAddress]")
|
2019-09-13 21:00:36 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
maddr, err := address.NewFromString(cctx.Args().First())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-10-03 18:12:30 +00:00
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
2019-09-13 21:00:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-10-03 18:12:30 +00:00
|
|
|
defer closer()
|
2019-09-13 21:00:36 +00:00
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
|
|
|
var pid peer.ID
|
|
|
|
if pidstr := cctx.String("peerid"); pidstr != "" {
|
2020-03-03 04:55:25 +00:00
|
|
|
p, err := peer.Decode(pidstr)
|
2019-09-13 21:00:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
pid = p
|
|
|
|
} else {
|
2020-04-16 17:36:36 +00:00
|
|
|
mi, err := api.StateMinerInfo(ctx, maddr, types.EmptyTSK)
|
2019-09-13 21:00:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to get peerID for miner: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-06-05 20:06:11 +00:00
|
|
|
if peer.ID(mi.PeerId) == peer.ID("SETME") {
|
2019-12-13 20:05:43 +00:00
|
|
|
return fmt.Errorf("the miner hasn't initialized yet")
|
|
|
|
}
|
2019-09-13 21:00:36 +00:00
|
|
|
|
2020-06-05 20:06:11 +00:00
|
|
|
pid = peer.ID(mi.PeerId)
|
2019-09-13 21:00:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ask, err := api.ClientQueryAsk(ctx, pid, maddr)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("Ask: %s\n", maddr)
|
2019-12-16 19:20:28 +00:00
|
|
|
fmt.Printf("Price per GiB: %s\n", types.FIL(ask.Ask.Price))
|
2020-06-09 18:04:17 +00:00
|
|
|
fmt.Printf("Max Piece size: %s\n", types.SizeStr(types.NewInt(uint64(ask.Ask.MaxPieceSize))))
|
2019-10-12 07:21:28 +00:00
|
|
|
|
|
|
|
size := cctx.Int64("size")
|
|
|
|
if size == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
2019-10-29 10:19:39 +00:00
|
|
|
perEpoch := types.BigDiv(types.BigMul(ask.Ask.Price, types.NewInt(uint64(size))), types.NewInt(1<<30))
|
2019-10-29 12:02:13 +00:00
|
|
|
fmt.Printf("Price per Block: %s\n", types.FIL(perEpoch))
|
2019-10-12 07:21:28 +00:00
|
|
|
|
|
|
|
duration := cctx.Int64("duration")
|
|
|
|
if duration == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
2019-10-29 12:02:13 +00:00
|
|
|
fmt.Printf("Total Price: %s\n", types.FIL(types.BigMul(perEpoch, types.NewInt(uint64(duration)))))
|
2019-10-12 07:21:28 +00:00
|
|
|
|
2019-09-13 21:00:36 +00:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
2019-11-08 01:45:45 +00:00
|
|
|
|
|
|
|
var clientListDeals = &cli.Command{
|
|
|
|
Name: "list-deals",
|
|
|
|
Usage: "List storage market deals",
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
2020-04-28 17:12:28 +00:00
|
|
|
head, err := api.ChainHead(ctx)
|
2019-11-08 01:45:45 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-04-28 17:12:28 +00:00
|
|
|
localDeals, err := api.ClientListDeals(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var deals []deal
|
2020-05-18 21:40:42 +00:00
|
|
|
for _, v := range localDeals {
|
|
|
|
if v.DealID == 0 {
|
|
|
|
deals = append(deals, deal{
|
|
|
|
LocalDeal: v,
|
|
|
|
OnChainDealState: market.DealState{
|
|
|
|
SectorStartEpoch: -1,
|
|
|
|
LastUpdatedEpoch: -1,
|
|
|
|
SlashEpoch: -1,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
onChain, err := api.StateMarketStorageDeal(ctx, v.DealID, head.Key())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
deals = append(deals, deal{
|
|
|
|
LocalDeal: v,
|
|
|
|
OnChainDealState: onChain.State,
|
|
|
|
})
|
2020-04-28 17:12:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-08 01:45:45 +00:00
|
|
|
w := tabwriter.NewWriter(os.Stdout, 2, 4, 2, ' ', 0)
|
2020-04-28 17:12:28 +00:00
|
|
|
fmt.Fprintf(w, "DealCid\tDealId\tProvider\tState\tOn Chain?\tSlashed?\tPieceCID\tSize\tPrice\tDuration\tMessage\n")
|
2019-11-08 01:45:45 +00:00
|
|
|
for _, d := range deals {
|
2020-04-28 17:12:28 +00:00
|
|
|
onChain := "N"
|
|
|
|
if d.OnChainDealState.SectorStartEpoch != -1 {
|
|
|
|
onChain = fmt.Sprintf("Y (epoch %d)", d.OnChainDealState.SectorStartEpoch)
|
|
|
|
}
|
|
|
|
|
|
|
|
slashed := "N"
|
|
|
|
if d.OnChainDealState.SlashEpoch != -1 {
|
|
|
|
slashed = fmt.Sprintf("Y (epoch %d)", d.OnChainDealState.SlashEpoch)
|
|
|
|
}
|
|
|
|
|
2020-07-20 17:02:52 +00:00
|
|
|
price := types.FIL(types.BigMul(d.LocalDeal.PricePerEpoch, types.NewInt(d.LocalDeal.Duration)))
|
|
|
|
fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%d\t%s\n", d.LocalDeal.ProposalCid, d.LocalDeal.DealID, d.LocalDeal.Provider, storagemarket.DealStates[d.LocalDeal.State], onChain, slashed, d.LocalDeal.PieceCID, types.SizeStr(types.NewInt(d.LocalDeal.Size)), price, d.LocalDeal.Duration, d.LocalDeal.Message)
|
2019-11-08 01:45:45 +00:00
|
|
|
}
|
|
|
|
return w.Flush()
|
|
|
|
},
|
|
|
|
}
|
2020-04-28 17:12:28 +00:00
|
|
|
|
|
|
|
type deal struct {
|
|
|
|
LocalDeal lapi.DealInfo
|
|
|
|
OnChainDealState market.DealState
|
|
|
|
}
|
2020-07-21 23:43:57 +00:00
|
|
|
|
|
|
|
var clientGetDealCmd = &cli.Command{
|
|
|
|
Name: "get-deal",
|
|
|
|
Usage: "Print detailed deal information",
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
if !cctx.Args().Present() {
|
|
|
|
return cli.ShowCommandHelp(cctx, cctx.Command.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
api, closer, err := GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := ReqContext(cctx)
|
|
|
|
|
|
|
|
propcid, err := cid.Decode(cctx.Args().First())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
di, err := api.ClientGetDealInfo(ctx, propcid)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
out := map[string]interface{}{
|
|
|
|
"DealInfo: ": di,
|
|
|
|
}
|
|
|
|
|
|
|
|
if di.DealID != 0 {
|
|
|
|
onChain, err := api.StateMarketStorageDeal(ctx, di.DealID, types.EmptyTSK)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
out["OnChain"] = onChain
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := json.MarshalIndent(out, "", " ")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Println(b)
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|