gitlab.com/SkynetLabs/skyd@v1.6.9/cmd/skyc/skynetcmd.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"mime/multipart"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync"
    13  	"text/tabwriter"
    14  
    15  	"github.com/spf13/cobra"
    16  	"github.com/vbauerster/mpb/v5"
    17  	"github.com/vbauerster/mpb/v5/decor"
    18  	"go.sia.tech/siad/crypto"
    19  	"go.sia.tech/siad/modules"
    20  
    21  	"gitlab.com/SkynetLabs/skyd/node/api"
    22  	"gitlab.com/SkynetLabs/skyd/siatest"
    23  	"gitlab.com/SkynetLabs/skyd/skymodules"
    24  	"gitlab.com/SkynetLabs/skyd/skymodules/renter"
    25  )
    26  
    27  var (
    28  	skynetCmd = &cobra.Command{
    29  		Use:   "skynet",
    30  		Short: "Perform actions related to Skynet",
    31  		Long: `Perform actions related to Skynet, a file sharing and data publication platform
    32  on top of Sia.`,
    33  		Run: skynetcmd,
    34  	}
    35  
    36  	skynetBackupCmd = &cobra.Command{
    37  		Use:   "backup [skylink] [backup path]",
    38  		Short: "Backup a skyfile to a file on disk.",
    39  		Long:  "Create a backup of a skyfile as a file on disk.",
    40  		Run:   wrap(skynetbackupcmd),
    41  	}
    42  
    43  	skynetBlocklistCmd = &cobra.Command{
    44  		Use:   "blocklist",
    45  		Short: "Add, remove, or list skylinks from the blocklist.",
    46  		Long:  "Add, remove, or list skylinks from the blocklist.",
    47  		Run:   skynetblocklistgetcmd,
    48  	}
    49  
    50  	skynetBlocklistAddCmd = &cobra.Command{
    51  		Use:   "add [skylink] ...",
    52  		Short: "Add skylinks to the blocklist",
    53  		Long:  "Add space separated skylinks to the blocklist.",
    54  		Run:   skynetblocklistaddcmd,
    55  	}
    56  
    57  	skynetBlocklistRemoveCmd = &cobra.Command{
    58  		Use:   "remove [skylink] ...",
    59  		Short: "Remove skylinks from the blocklist",
    60  		Long:  "Remove space separated skylinks from the blocklist.",
    61  		Run:   skynetblocklistremovecmd,
    62  	}
    63  
    64  	skynetConvertCmd = &cobra.Command{
    65  		Use:   "convert [source siaPath] [destination siaPath]",
    66  		Short: "Convert a siafile to a skyfile with a skylink.",
    67  		Long: `Convert a siafile to a skyfile and then generate its skylink. A new skylink
    68  	will be created in the user's skyfile directory. The skyfile and the original
    69  	siafile are both necessary to pin the file and keep the skylink active. The
    70  	skyfile will consume an additional 40 MiB of storage.`,
    71  		Run: wrap(skynetconvertcmd),
    72  	}
    73  
    74  	skynetDownloadCmd = &cobra.Command{
    75  		Use:   "download [skylink] [destination]",
    76  		Short: "Download a skylink from skynet.",
    77  		Long: `Download a file from skynet using a skylink. The download may fail unless this
    78  node is configured as a skynet portal. Use the --portal flag to fetch a skylink
    79  file from a chosen skynet portal.`,
    80  		Run: skynetdownloadcmd,
    81  	}
    82  
    83  	skynetIsBlockedCmd = &cobra.Command{
    84  		Use:   "isblocked [skylink] ...",
    85  		Short: "Checks if a skylink is on the blocklist.",
    86  		Long: `Checks if a skylink, or a list of space separated skylinks, is on the blocklist 
    87  since the list returned from 'skyc skynet blocklist' is a list of hashes of the skylinks' 
    88  merkleroots so they cannot be visually verified.`,
    89  		Run: skynetisblockedcmd,
    90  	}
    91  
    92  	skynetLsCmd = &cobra.Command{
    93  		Use:   "ls",
    94  		Short: "List all skyfiles that the user has pinned.",
    95  		Long: `List all skyfiles that the user has pinned along with the corresponding
    96  skylinks. By default, only files in var/skynet/ will be displayed. The --root
    97  flag can be used to view skyfiles pinned in other folders.`,
    98  		Run: skynetlscmd,
    99  	}
   100  
   101  	skynetPinCmd = &cobra.Command{
   102  		Use:   "pin [skylink] [destination siapath]",
   103  		Short: "Pin a skylink from skynet by re-uploading it yourself.",
   104  		Long: `Pin the file associated with this skylink by re-uploading an exact copy. This
   105  ensures that the file will still be available on skynet as long as you continue
   106  maintaining the file in your renter.`,
   107  		Run: wrap(skynetpincmd),
   108  	}
   109  
   110  	skynetPortalsCmd = &cobra.Command{
   111  		Use:   "portals",
   112  		Short: "Add, remove, or list registered Skynet portals.",
   113  		Long:  "Add, remove, or list registered Skynet portals.",
   114  		Run:   wrap(skynetportalsgetcmd),
   115  	}
   116  
   117  	skynetPortalsAddCmd = &cobra.Command{
   118  		Use:   "add [url]",
   119  		Short: "Add a Skynet portal as public or private to the persisted portals list.",
   120  		Long: `Add a Skynet portal as public or private. Specify the url of the Skynet portal followed
   121  by --public if you want it to be publicly available.`,
   122  		Run: wrap(skynetportalsaddcmd),
   123  	}
   124  
   125  	skynetPortalsRemoveCmd = &cobra.Command{
   126  		Use:   "remove [url]",
   127  		Short: "Remove a Skynet portal from the persisted portals list.",
   128  		Long:  "Remove a Skynet portal from the persisted portals list.",
   129  		Run:   wrap(skynetportalsremovecmd),
   130  	}
   131  
   132  	skynetRestoreCmd = &cobra.Command{
   133  		Use:   "restore [backup source]",
   134  		Short: "Restore a skyfile from a backup file.",
   135  		Long:  "Restore a skyfile from a backup file.",
   136  		Run:   wrap(skynetrestorecmd),
   137  	}
   138  
   139  	skynetSkylinkCmd = &cobra.Command{
   140  		Use:   "skylink",
   141  		Short: "Perform various util functions for a skylink.",
   142  		Long: `Perform various util functions for a skylink like check the layout
   143  metadata, or recomputing.`,
   144  		Run: skynetskylinkcmd,
   145  	}
   146  
   147  	skynetSkylinkCompareCmd = &cobra.Command{
   148  		Use:   "compare [skylink] [metadata filename]",
   149  		Short: "Compare a skylink to a regenerated skylink",
   150  		Long: `This command regenerates a skylink by doing the following:
   151  First, it reads some provided metadata from the provided filename.
   152  Second, it downloads the skylink and records the metadata, layout, and filedata.
   153  Third, it compares the downloaded metadata to the metadata read from disk.
   154  Fourth, it computesthe base sector and then the skylink from the downloaded information.
   155  Lastly, it compares the generated skylink to the skylink that was passed in.`,
   156  		Run: wrap(skynetskylinkcomparecmd),
   157  	}
   158  
   159  	skynetSkylinkHealthCmd = &cobra.Command{
   160  		Use:   "health [skylink]",
   161  		Short: "Print the health of a skylink",
   162  		Long:  "Print the health of a skylink",
   163  		Run:   wrap(skynetskylinkhealthcmd),
   164  	}
   165  
   166  	skynetSkylinkLayoutCmd = &cobra.Command{
   167  		Use:   "layout [skylink]",
   168  		Short: "Print the layout associated with a skylink",
   169  		Long:  "Print the layout associated with a skylink",
   170  		Run:   wrap(skynetskylinklayoutcmd),
   171  	}
   172  
   173  	skynetSkylinkMetadataCmd = &cobra.Command{
   174  		Use:   "metadata [skylink]",
   175  		Short: "Print the metadata associated with a skylink",
   176  		Long:  "Print the metadata associated with a skylink",
   177  		Run:   wrap(skynetskylinkmetadatacmd),
   178  	}
   179  
   180  	skynetUnpinCmd = &cobra.Command{
   181  		Use:   "unpin [skylink]",
   182  		Short: "Unpin pinned skyfiles by skylink.",
   183  		Long: `Unpin one or more pinned skyfiles by skylink. The files and
   184  directories will continue to be available on Skynet if other nodes have pinned
   185  them.
   186  
   187  NOTE: To use the prior functionality of unpinning by SiaPath, use the 'skyc
   188  renter delete' command and set the --root flag.`,
   189  		Run: skynetunpincmd,
   190  	}
   191  
   192  	skynetUploadCmd = &cobra.Command{
   193  		Use:   "upload [source path] [destination siapath]",
   194  		Short: "Upload a file or a directory to Skynet.",
   195  		Long: `Upload a file or a directory to Skynet. A skylink will be 
   196  produced which can be shared and used to retrieve the file. If the given path is
   197  a directory it will be uploaded as a single skylink unless the --separately flag
   198  is passed, in which case all files under that directory will be uploaded 
   199  individually and an individual skylink will be produced for each. All files that
   200  get uploaded will be pinned to this Sia node, meaning that this node will pay
   201  for storage and repairs until the files are manually deleted. Use the --dry-run 
   202  flag to fetch the skylink without actually uploading the file.`,
   203  		Run: skynetuploadcmd,
   204  	}
   205  )
   206  
   207  // skynetcmd displays the usage info for the command.
   208  //
   209  // TODO: Could put some stats or summaries or something here.
   210  func skynetcmd(cmd *cobra.Command, _ []string) {
   211  	_ = cmd.UsageFunc()(cmd)
   212  	os.Exit(exitCodeUsage)
   213  }
   214  
   215  // skynetbackupcmd will backup a skyfile by writing it to a backup writer.
   216  func skynetbackupcmd(skylinkStr, backupPath string) {
   217  	// Create backup file
   218  	f, err := os.Create(backupPath)
   219  	if err != nil {
   220  		die("Unable to create backup file:", err)
   221  	}
   222  	defer func() {
   223  		if err := f.Close(); err != nil {
   224  			die("Unable to close backup file:", err)
   225  		}
   226  	}()
   227  
   228  	// Create backup
   229  	err = httpClient.SkynetSkylinkBackup(skylinkStr, f)
   230  	if err != nil {
   231  		die("Unable to create backup:", err)
   232  	}
   233  	fmt.Println("Backup successfully created at ", backupPath)
   234  }
   235  
   236  // skynetblocklistaddcmd adds skylinks to the blocklist
   237  func skynetblocklistaddcmd(cmd *cobra.Command, args []string) {
   238  	skynetBlocklistUpdate(args, nil)
   239  }
   240  
   241  // skynetblocklistremovecmd removes skylinks from the blocklist
   242  func skynetblocklistremovecmd(cmd *cobra.Command, args []string) {
   243  	skynetBlocklistUpdate(nil, args)
   244  }
   245  
   246  // skynetBlocklistUpdate adds/removes trimmed skylinks to the blocklist
   247  func skynetBlocklistUpdate(additions, removals []string) {
   248  	additions = sanitizeSkylinks(additions)
   249  	removals = sanitizeSkylinks(removals)
   250  
   251  	res, err := httpClient.SkynetBlocklistHashPost(additions, removals, skynetBlocklistHash)
   252  	if err != nil {
   253  		die("Unable to update skynet blocklist:", err)
   254  	}
   255  
   256  	if len(res.Invalids) > 0 {
   257  		fmt.Printf("Skynet Blocklist partially updated, invalid inputs: %+v\n", res.Invalids)
   258  		return
   259  	}
   260  	fmt.Println("Skynet Blocklist updated")
   261  }
   262  
   263  // skynetblocklistgetcmd will return the list of hashed merkleroots that are blocked
   264  // from Skynet.
   265  func skynetblocklistgetcmd(_ *cobra.Command, _ []string) {
   266  	response, err := httpClient.SkynetBlocklistGet()
   267  	if err != nil {
   268  		die("Unable to get skynet blocklist:", err)
   269  	}
   270  
   271  	fmt.Printf("Listing %d blocked skylink(s) merkleroots:\n", len(response.Blocklist))
   272  	for _, hash := range response.Blocklist {
   273  		fmt.Printf("\t%s\n", hash)
   274  	}
   275  }
   276  
   277  // skynetconvertcmd will convert an existing siafile to a skyfile and skylink on
   278  // the Sia network.
   279  func skynetconvertcmd(sourceSiaPathStr, destSiaPathStr string) {
   280  	// Create the siapaths.
   281  	sourceSiaPath, err := skymodules.NewSiaPath(sourceSiaPathStr)
   282  	if err != nil {
   283  		die("Could not parse source siapath:", err)
   284  	}
   285  	destSiaPath, err := skymodules.NewSiaPath(destSiaPathStr)
   286  	if err != nil {
   287  		die("Could not parse destination siapath:", err)
   288  	}
   289  
   290  	// Perform the conversion and print the result.
   291  	sup := skymodules.SkyfileUploadParameters{
   292  		SiaPath: destSiaPath,
   293  	}
   294  	sup = parseAndAddSkykey(sup)
   295  	sshp, err := httpClient.SkynetConvertSiafileToSkyfilePost(sup, sourceSiaPath)
   296  	if err != nil {
   297  		die("could not convert siafile to skyfile:", err)
   298  	}
   299  	skylink := sshp.Skylink
   300  
   301  	// Calculate the siapath that was used for the upload.
   302  	var skypath skymodules.SiaPath
   303  	if skynetUploadRoot {
   304  		skypath = destSiaPath
   305  	} else {
   306  		skypath, err = skymodules.SkynetFolder.Join(destSiaPath.String())
   307  		if err != nil {
   308  			die("could not fetch skypath:", err)
   309  		}
   310  	}
   311  	fmt.Printf("Skyfile uploaded successfully to %v\nSkylink: sia://%v\n", skypath, skylink)
   312  }
   313  
   314  // skynetdownloadcmd will perform the download of a skylink.
   315  func skynetdownloadcmd(cmd *cobra.Command, args []string) {
   316  	if len(args) != 2 {
   317  		_ = cmd.UsageFunc()(cmd)
   318  		os.Exit(exitCodeUsage)
   319  	}
   320  
   321  	// Open the file.
   322  	skylink := args[0]
   323  	skylink = strings.TrimPrefix(skylink, "sia://")
   324  	filename := args[1]
   325  	file, err := os.Create(filename)
   326  	if err != nil {
   327  		die("Unable to create destination file:", err)
   328  	}
   329  	defer func() {
   330  		if err := file.Close(); err != nil {
   331  			die(err)
   332  		}
   333  	}()
   334  
   335  	// Try to perform a download using the client package.
   336  	reader, err := httpClient.SkynetSkylinkReaderGet(skylink)
   337  	if err != nil {
   338  		die("Unable to fetch skylink:", err)
   339  	}
   340  	defer func() {
   341  		err = reader.Close()
   342  		if err != nil {
   343  			die("unable to close reader:", err)
   344  		}
   345  	}()
   346  
   347  	_, err = io.Copy(file, reader)
   348  	if err != nil {
   349  		die("Unable to write full data:", err)
   350  	}
   351  }
   352  
   353  // skynetisblockedcmd will check if a skylink, or list of skylinks, is on the
   354  // blocklist.
   355  func skynetisblockedcmd(_ *cobra.Command, skylinkStrs []string) {
   356  	// Get the blocklist
   357  	var err error
   358  	response, err := httpClient.SkynetBlocklistGet()
   359  	if err != nil {
   360  		die("Unable to get skynet blocklist:", err)
   361  	}
   362  
   363  	// Parse the slice response into a map
   364  	blocklistMap := make(map[crypto.Hash]struct{})
   365  	for _, hash := range response.Blocklist {
   366  		blocklistMap[hash] = struct{}{}
   367  	}
   368  
   369  	// Check the skylinks
   370  	//
   371  	// NOTE: errors are printed and won't cause the function to exit.
   372  	for _, skylinkStr := range skylinkStrs {
   373  		// Load the string
   374  		var skylink skymodules.Skylink
   375  		err := skylink.LoadString(skylinkStr)
   376  		if err != nil {
   377  			fmt.Printf("Skylink %v \tis an invalid skylink: %v\n", skylinkStr, err)
   378  			continue
   379  		}
   380  		// Generate the hash of the merkleroot and check the blocklist
   381  		hash := crypto.HashObject(skylink.MerkleRoot())
   382  		_, blocked := blocklistMap[hash]
   383  		if blocked {
   384  			fmt.Printf("Skylink %v \tis on the blocklist\n", skylinkStr)
   385  		}
   386  	}
   387  }
   388  
   389  // skynetlscmd is the handler for the command `skyc skynet ls`. Works very
   390  // similar to 'skyc renter ls' but defaults to the SkynetFolder and only
   391  // displays files that are pinning skylinks.
   392  func skynetlscmd(cmd *cobra.Command, args []string) {
   393  	// Parse the SiaPath
   394  	sp, err := parseLSArgs(args)
   395  	if err != nil {
   396  		fmt.Fprintln(os.Stderr, err)
   397  		_ = cmd.UsageFunc()(cmd)
   398  		os.Exit(exitCodeUsage)
   399  	}
   400  
   401  	// Check whether the command is based in root or based in the skynet folder.
   402  	if !skynetLsRoot {
   403  		if sp.IsRoot() {
   404  			sp = skymodules.SkynetFolder
   405  		} else {
   406  			sp, err = skymodules.SkynetFolder.Join(sp.String())
   407  			if err != nil {
   408  				die("could not build siapath:", err)
   409  			}
   410  		}
   411  	}
   412  
   413  	// Check if the command is hitting a single file.
   414  	if !sp.IsRoot() {
   415  		tryDir, err := printSingleFile(sp, true, true)
   416  		if err != nil {
   417  			die(err)
   418  		}
   419  		if !tryDir {
   420  			return
   421  		}
   422  	}
   423  
   424  	// Get the full set of files and directories. They will be sorted by siapath
   425  	//
   426  	// NOTE: we always pass in true for root as this is referring to the client
   427  	// method needed to query the directories, not whether or not the siapath is
   428  	// relative to root or the skynet folder.
   429  	//
   430  	// NOTE: We want to get the directories recursively if we are either checking
   431  	// from root or the user wants recursive.
   432  	getRecursive := skynetLsRecursive || skynetLsRoot
   433  	dirs := getDirSorted(sp, true, getRecursive, verbose)
   434  
   435  	// Determine the total number of Skyfiles and Skynet Directories. A Skynet
   436  	// directory is a directory that is either in the Skynet Folder or it contains
   437  	// at least one skyfile.
   438  	var numFilesDirs uint64
   439  	root := dirs[0] // Root directory we are querying.
   440  
   441  	// Grab the starting value for the number of Skyfiles and Skynet Directories.
   442  	if skynetLsRecursive {
   443  		numFilesDirs = root.dir.AggregateSkynetFiles + root.dir.AggregateNumSubDirs
   444  	} else {
   445  		numFilesDirs = root.dir.SkynetFiles + root.dir.NumSubDirs
   446  	}
   447  
   448  	// If we are referencing the root of the file system then we need to check all
   449  	// the directories we queried and see which ones need to be omitted.
   450  	if skynetLsRoot {
   451  		// Figure out how many directories don't contain skyfiles
   452  		var nonSkynetDir uint64
   453  		for _, dir := range dirs {
   454  			if !skymodules.IsSkynetDir(dir.dir.SiaPath) && dir.dir.SkynetFiles == 0 {
   455  				nonSkynetDir++
   456  			}
   457  		}
   458  
   459  		// Subtract the number of non skynet directories from the total.
   460  		numFilesDirs -= nonSkynetDir
   461  	}
   462  
   463  	// Print totals.
   464  	totalStoredStr := modules.FilesizeUnits(root.dir.AggregateSkynetSize)
   465  	fmt.Printf("\nListing %v files/dirs:\t%9s\n\n", numFilesDirs, totalStoredStr)
   466  
   467  	// Print Dirs
   468  	err = printSkynetDirs(dirs, skynetLsRecursive)
   469  	if err != nil {
   470  		die(err)
   471  	}
   472  }
   473  
   474  // skynetPin will pin the Skyfile associated with the provided Skylink at the
   475  // provided SiaPath
   476  func skynetPin(skylink string, siaPath skymodules.SiaPath) (string, error) {
   477  	spp := skymodules.SkyfilePinParameters{
   478  		SiaPath: siaPath,
   479  		Root:    skynetUploadRoot,
   480  	}
   481  	fmt.Println("Pinning Skyfile ...")
   482  	return skylink, httpClient.SkynetSkylinkPinPost(skylink, spp)
   483  }
   484  
   485  // skynetpincmd will pin the file from this skylink.
   486  func skynetpincmd(sourceSkylink, destSiaPath string) {
   487  	skylink := strings.TrimPrefix(sourceSkylink, "sia://")
   488  	// Create the siapath.
   489  	siaPath, err := skymodules.NewSiaPath(destSiaPath)
   490  	if err != nil {
   491  		die("Could not parse destination siapath:", err)
   492  	}
   493  
   494  	// Pin the Skyfile
   495  	skylink, err = skynetPin(skylink, siaPath)
   496  	if err != nil {
   497  		die("Unable to Pin Skyfile:", err)
   498  	}
   499  	fmt.Printf("Skyfile pinned successfully\nSkylink: sia://%v\n", skylink)
   500  }
   501  
   502  // skynetrestorecmd will restore a skyfile from a backup writer.
   503  func skynetrestorecmd(backupPath string) {
   504  	// Open the backup file
   505  	f, err := os.Open(backupPath)
   506  	if err != nil {
   507  		die("Unable to open backup file:", err)
   508  	}
   509  	defer func() {
   510  		// Attempt to close the file, API call appears to close file so ignore the
   511  		// error to avoid getting an error for closing a closed file.
   512  		_ = f.Close()
   513  	}()
   514  
   515  	// Create backup
   516  	skylink, err := httpClient.SkynetSkylinkRestorePost(f)
   517  	if err != nil {
   518  		die("Unable to restore skyfile:", err)
   519  	}
   520  	fmt.Println("Restore successful! Skylink: ", skylink)
   521  }
   522  
   523  // skynetskylinkcmd displays the usage info for the command.
   524  func skynetskylinkcmd(cmd *cobra.Command, args []string) {
   525  	_ = cmd.UsageFunc()(cmd)
   526  	os.Exit(exitCodeUsage)
   527  }
   528  
   529  // skynetskylinkcomparecmd compares a provided skylink to with a re-generated
   530  // skylink based on metadata provided in a metadata.json file and downloading
   531  // the file data and the layout from the skylink.
   532  func skynetskylinkcomparecmd(expectedSkylink string, filename string) {
   533  	// Read Metadata file and trim a potential newline.
   534  	skyfileMetadataFromFile := fileData(filename)
   535  	skyfileMetadataFromFile = bytes.TrimSuffix(skyfileMetadataFromFile, []byte{'\n'})
   536  
   537  	// Download the skyfile
   538  	skyfileDownloadedData, layoutFromHeader, skyfileMetadataFromHeader, err := smallSkyfileDownload(expectedSkylink)
   539  	if err != nil {
   540  		die(err)
   541  	}
   542  
   543  	// Check if the metadata download is the same as the metadata loaded from disk
   544  	if !bytes.Equal(skyfileMetadataFromFile, skyfileMetadataFromHeader) {
   545  		var sb strings.Builder
   546  		sb.WriteString(fmt.Sprintf("Metadata read from file %d\n", len(skyfileMetadataFromFile)))
   547  		sb.WriteString(string(skyfileMetadataFromFile))
   548  		sb.WriteString(fmt.Sprintf("Metadata read from header %d\n", len(skyfileMetadataFromHeader)))
   549  		sb.WriteString(string(skyfileMetadataFromHeader))
   550  		die(sb.String(), "Metadatas not equal")
   551  	}
   552  	fmt.Println("Metadatas Equal")
   553  
   554  	// build base sector
   555  	baseSector, fetchSize, _ := skymodules.BuildBaseSector(layoutFromHeader.Encode(), nil, skyfileMetadataFromFile, skyfileDownloadedData)
   556  	baseSectorRoot := crypto.MerkleRoot(baseSector)
   557  	skylink, err := skymodules.NewSkylinkV1(baseSectorRoot, 0, fetchSize)
   558  	if err != nil {
   559  		die(err)
   560  	}
   561  
   562  	if skylink.String() != expectedSkylink {
   563  		comp := fmt.Sprintf("Expected %s\nGenerated %s\n", expectedSkylink, skylink.String())
   564  		die(comp, "Generated Skylink not Equal to Expected")
   565  	}
   566  	fmt.Println("Generated Skylink as Expected!")
   567  }
   568  
   569  // skynetskylinkhealthcmd prints the health of the skylink.
   570  func skynetskylinkhealthcmd(skylinkStr string) {
   571  	// Load the Skylink
   572  	var skylink skymodules.Skylink
   573  	err := skylink.LoadString(skylinkStr)
   574  	if err != nil {
   575  		die(err)
   576  	}
   577  
   578  	// Get the health
   579  	health, err := httpClient.SkylinkHealthGET(skylink)
   580  	if err != nil {
   581  		die(err)
   582  	}
   583  	// Print the layout
   584  	healthStr, err := siatest.PrintJSONProd(health)
   585  	if err != nil {
   586  		die(err)
   587  	}
   588  
   589  	fmt.Println("Skylink Health:")
   590  	fmt.Println(healthStr)
   591  }
   592  
   593  // skynetskylinklayoutcmd prints the SkyfileLayout of the skylink.
   594  func skynetskylinklayoutcmd(skylink string) {
   595  	// Download the layout
   596  	_, sl, _, err := smallSkyfileDownload(skylink)
   597  	if err != nil {
   598  		die(err)
   599  	}
   600  	// Print the layout
   601  	str, err := siatest.PrintJSONProd(sl)
   602  	if err != nil {
   603  		die(err)
   604  	}
   605  	fmt.Println("Skyfile Layout:")
   606  	fmt.Println(str)
   607  }
   608  
   609  // skynetskylinkmetadatacmd downloads and prints the SkyfileMetadata for a
   610  // skylink.
   611  func skynetskylinkmetadatacmd(skylink string) {
   612  	// Download the metadata
   613  	_, _, sm, err := smallSkyfileDownload(skylink)
   614  	if err != nil {
   615  		die(err)
   616  	}
   617  	// Print the metadata
   618  	fmt.Println("Skyfile Metadata:")
   619  	fmt.Println(string(sm))
   620  }
   621  
   622  // skynetunpincmd will unpin and delete either a single or multiple skylinks
   623  // from the renter.
   624  func skynetunpincmd(cmd *cobra.Command, skylinks []string) {
   625  	if len(skylinks) == 0 {
   626  		_ = cmd.UsageFunc()(cmd)
   627  		os.Exit(exitCodeUsage)
   628  	}
   629  
   630  	for _, skylink := range skylinks {
   631  		// Unpin skylink
   632  		err := httpClient.SkynetSkylinkUnpinPost(skylink)
   633  		if err != nil {
   634  			fmt.Printf("Unable to unpin skylink %v: %v\n", skylink, err)
   635  		}
   636  	}
   637  }
   638  
   639  // skynetuploadcmd will upload a file or directory to Skynet. If --dry-run is
   640  // passed, it will fetch the skylinks without uploading.
   641  func skynetuploadcmd(_ *cobra.Command, args []string) {
   642  	if len(args) == 1 {
   643  		skynetuploadpipecmd(args[0])
   644  		return
   645  	}
   646  	if len(args) != 2 {
   647  		die("wrong number of arguments")
   648  	}
   649  	sourcePath, destSiaPath := args[0], args[1]
   650  	fi, err := os.Stat(sourcePath)
   651  	if err != nil {
   652  		die("Unable to fetch source fileinfo:", err)
   653  	}
   654  
   655  	// create a new progress bar set:
   656  	pbs := mpb.New(mpb.WithWidth(40))
   657  
   658  	if !fi.IsDir() {
   659  		skynetUploadFile(sourcePath, sourcePath, destSiaPath, pbs)
   660  		if skynetUploadDryRun {
   661  			fmt.Print("[dry run] ")
   662  		}
   663  		pbs.Wait()
   664  		fmt.Printf("Successfully uploaded skyfile!\n")
   665  		return
   666  	}
   667  
   668  	if skynetUploadSeparately {
   669  		skynetUploadFilesSeparately(sourcePath, destSiaPath, pbs)
   670  		return
   671  	}
   672  	skynetUploadDirectory(sourcePath, destSiaPath)
   673  }
   674  
   675  // skynetuploadpipecmd will upload a file or directory to Skynet. If --dry-run is
   676  // passed, it will fetch the skylinks without uploading.
   677  func skynetuploadpipecmd(destSiaPath string) {
   678  	fi, err := os.Stdin.Stat()
   679  	if err != nil {
   680  		die(err)
   681  	}
   682  	if fi.Mode()&os.ModeNamedPipe == 0 {
   683  		die("Command is meant to be used with either a pipe or src file")
   684  	}
   685  	// Create the siapath.
   686  	siaPath, err := skymodules.NewSiaPath(destSiaPath)
   687  	if err != nil {
   688  		die("Could not parse destination siapath:", err)
   689  	}
   690  	filename := siaPath.Name()
   691  
   692  	// create a new progress bar set:
   693  	pbs := mpb.New(mpb.WithWidth(40))
   694  	// Create the single bar.
   695  	bar := pbs.AddSpinner(
   696  		-1, // size is unknown
   697  		mpb.SpinnerOnLeft,
   698  		mpb.SpinnerStyle([]string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"}),
   699  		mpb.BarFillerClearOnComplete(),
   700  		mpb.PrependDecorators(
   701  			decor.AverageSpeed(decor.UnitKiB, "% .1f", decor.WC{W: 4}),
   702  			decor.Counters(decor.UnitKiB, " - %.1f / %.1f", decor.WC{W: 4}),
   703  		),
   704  	)
   705  	// Create the proxy reader from stdin.
   706  	r := bar.ProxyReader(os.Stdin)
   707  	// Set a spinner to start after the upload is finished
   708  	pSpinner := newProgressSpinner(pbs, bar, filename)
   709  	// Perform the upload
   710  	skylink := skynetUploadFileFromReader(r, filename, siaPath, skymodules.DefaultFilePerm)
   711  	// Replace the spinner with the skylink and stop it
   712  	newProgressSkylink(pbs, pSpinner, filename, skylink)
   713  	return
   714  }
   715  
   716  // skynetportalsgetcmd displays the list of persisted Skynet portals
   717  func skynetportalsgetcmd() {
   718  	portals, err := httpClient.SkynetPortalsGet()
   719  	if err != nil {
   720  		die("Could not get portal list:", err)
   721  	}
   722  
   723  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   724  
   725  	fmt.Fprintf(w, "Address\tPublic\n")
   726  	fmt.Fprintf(w, "-------\t------\n")
   727  
   728  	for _, portal := range portals.Portals {
   729  		fmt.Fprintf(w, "%s\t%t\n", portal.Address, portal.Public)
   730  	}
   731  
   732  	if err = w.Flush(); err != nil {
   733  		die(err)
   734  	}
   735  }
   736  
   737  // skynetportalsaddcmd adds a Skynet portal as either public or private
   738  func skynetportalsaddcmd(portalURL string) {
   739  	addition := skymodules.SkynetPortal{
   740  		Address: modules.NetAddress(portalURL),
   741  		Public:  skynetPortalPublic,
   742  	}
   743  
   744  	err := httpClient.SkynetPortalsPost([]skymodules.SkynetPortal{addition}, nil)
   745  	if err != nil {
   746  		die("Could not add portal:", err)
   747  	}
   748  }
   749  
   750  // skynetportalsremovecmd removes a Skynet portal
   751  func skynetportalsremovecmd(portalUrl string) {
   752  	removal := modules.NetAddress(portalUrl)
   753  
   754  	err := httpClient.SkynetPortalsPost(nil, []modules.NetAddress{removal})
   755  	if err != nil {
   756  		die("Could not remove portal:", err)
   757  	}
   758  }
   759  
   760  // skynetUploadFile uploads a file to Skynet
   761  func skynetUploadFile(basePath, sourcePath string, destSiaPath string, pbs *mpb.Progress) {
   762  	// Create the siapath.
   763  	siaPath, err := skymodules.NewSiaPath(destSiaPath)
   764  	if err != nil {
   765  		die("Could not parse destination siapath:", err)
   766  	}
   767  	filename := filepath.Base(sourcePath)
   768  
   769  	// Open the source.
   770  	file, err := os.Open(sourcePath)
   771  	if err != nil {
   772  		die("Unable to open source path:", err)
   773  	}
   774  	defer func() { _ = file.Close() }()
   775  
   776  	fi, err := file.Stat()
   777  	if err != nil {
   778  		die("Unable to fetch source fileinfo:", err)
   779  	}
   780  
   781  	if skynetUploadSilent {
   782  		// Silently upload the file and print a simple source -> skylink
   783  		// matching after it's done.
   784  		skylink := skynetUploadFileFromReader(file, filename, siaPath, fi.Mode())
   785  		fmt.Printf("%s -> %s\n", sourcePath, skylink)
   786  		return
   787  	}
   788  
   789  	// Display progress bars while uploading and processing the file.
   790  	var relPath string
   791  	if sourcePath == basePath {
   792  		// when uploading a single file we only display the filename
   793  		relPath = filename
   794  	} else {
   795  		// when uploading multiple files we strip the common basePath
   796  		relPath, err = filepath.Rel(basePath, sourcePath)
   797  		if err != nil {
   798  			die("Could not get relative path:", err)
   799  		}
   800  	}
   801  	// Wrap the file reader in a progress bar reader
   802  	pUpload, rc := newProgressReader(pbs, fi.Size(), relPath, file)
   803  	// Set a spinner to start after the upload is finished
   804  	pSpinner := newProgressSpinner(pbs, pUpload, relPath)
   805  	// Perform the upload
   806  	skylink := skynetUploadFileFromReader(rc, filename, siaPath, fi.Mode())
   807  	// Replace the spinner with the skylink and stop it
   808  	newProgressSkylink(pbs, pSpinner, relPath, skylink)
   809  	return
   810  }
   811  
   812  // skynetUploadFilesSeparately uploads a number of files to Skynet, printing out
   813  // separate skylink for each
   814  func skynetUploadFilesSeparately(sourcePath, destSiaPath string, pbs *mpb.Progress) {
   815  	// Walk the target directory and collect all files that are going to be
   816  	// uploaded.
   817  	filesToUpload := make([]string, 0)
   818  	err := filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error {
   819  		if err != nil {
   820  			fmt.Println("Warning: skipping file:", err)
   821  			return nil
   822  		}
   823  		if !info.IsDir() {
   824  			filesToUpload = append(filesToUpload, path)
   825  		}
   826  		return nil
   827  	})
   828  	if err != nil {
   829  		die(err)
   830  	}
   831  
   832  	// Confirm with the user that they want to upload all of them.
   833  	if skynetUploadDryRun {
   834  		fmt.Print("[dry run] ")
   835  	}
   836  	ok := askForConfirmation(fmt.Sprintf("Are you sure that you want to upload %d files to Skynet?", len(filesToUpload)))
   837  	if !ok {
   838  		os.Exit(0)
   839  	}
   840  
   841  	// Start the workers.
   842  	filesChan := make(chan string)
   843  	var wg sync.WaitGroup
   844  	for i := 0; i < SimultaneousSkynetUploads; i++ {
   845  		wg.Add(1)
   846  		go func() {
   847  			defer wg.Done()
   848  			for filename := range filesChan {
   849  				// get only the filename and path, relative to the original destSiaPath
   850  				// in order to figure out where to put the file
   851  				newDestSiaPath := filepath.Join(destSiaPath, strings.TrimPrefix(filename, sourcePath))
   852  				skynetUploadFile(sourcePath, filename, newDestSiaPath, pbs)
   853  			}
   854  		}()
   855  	}
   856  	// Send all files for upload.
   857  	for _, path := range filesToUpload {
   858  		filesChan <- path
   859  	}
   860  	// Signal the workers that there is no more work.
   861  	close(filesChan)
   862  	wg.Wait()
   863  	pbs.Wait()
   864  	if skynetUploadDryRun {
   865  		fmt.Print("[dry run] ")
   866  	}
   867  	fmt.Printf("Successfully uploaded %d skyfiles!\n", len(filesToUpload))
   868  }
   869  
   870  // skynetUploadDirectory uploads a directory as a single skyfile
   871  func skynetUploadDirectory(sourcePath, destSiaPath string) {
   872  	skyfilePath, err := skymodules.NewSiaPath(destSiaPath)
   873  	if err != nil {
   874  		die(fmt.Sprintf("Failed to create siapath %s\n", destSiaPath), err)
   875  	}
   876  	if skynetUploadDisableDefaultPath && skynetUploadDefaultPath != "" {
   877  		die("Illegal combination of parameters: --defaultpath and --disabledefaultpath are mutually exclusive.")
   878  	}
   879  	if skynetUploadTryFiles != "" && (skynetUploadDisableDefaultPath || skynetUploadDefaultPath != "") {
   880  		die("Illegal combination of parameters: --tryfiles is not compatible with --defaultpath and --disabledefaultpath.")
   881  	}
   882  	tryfiles := strings.Split(skynetUploadTryFiles, ",")
   883  	errPages, err := api.UnmarshalErrorPages(skynetUploadErrorPages)
   884  	if err != nil {
   885  		die(err)
   886  	}
   887  	pr, pw := io.Pipe()
   888  	defer pr.Close()
   889  	writer := multipart.NewWriter(pw)
   890  	go func() {
   891  		defer pw.Close()
   892  		// Walk the target directory and collect all files that are going to be
   893  		// uploaded.
   894  		var offset uint64
   895  		err = filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error {
   896  			if err != nil {
   897  				die(fmt.Sprintf("Failed to read file %s.\n", path), err)
   898  			}
   899  			if info.IsDir() {
   900  				return nil
   901  			}
   902  			data, err := ioutil.ReadFile(path)
   903  			if err != nil {
   904  				die(fmt.Sprintf("Failed to read file %s.\n", path), err)
   905  			}
   906  			_, err = skymodules.AddMultipartFile(writer, data, "files[]", info.Name(), skymodules.DefaultFilePerm, &offset)
   907  			if err != nil {
   908  				die(fmt.Sprintf("Failed to add file %s to multipart upload.\n", path), err)
   909  			}
   910  			return nil
   911  		})
   912  		if err != nil {
   913  			die(err)
   914  		}
   915  		if err = writer.Close(); err != nil {
   916  			die(err)
   917  		}
   918  	}()
   919  
   920  	sup := skymodules.SkyfileMultipartUploadParameters{
   921  		SiaPath:             skyfilePath,
   922  		Force:               false,
   923  		Root:                false,
   924  		BaseChunkRedundancy: renter.SkyfileDefaultBaseChunkRedundancy,
   925  		Reader:              pr,
   926  		Filename:            skyfilePath.Name(),
   927  		DefaultPath:         skynetUploadDefaultPath,
   928  		DisableDefaultPath:  skynetUploadDisableDefaultPath,
   929  		TryFiles:            tryfiles,
   930  		ErrorPages:          errPages,
   931  		ContentType:         writer.FormDataContentType(),
   932  	}
   933  	skylink, _, err := httpClient.SkynetSkyfileMultiPartPost(sup)
   934  	if err != nil {
   935  		die("Failed to upload directory.", err)
   936  	}
   937  	fmt.Println("Successfully uploaded directory:", skylink)
   938  }
   939  
   940  // skynetUploadFileFromReader is a helper method that uploads a file to Skynet
   941  func skynetUploadFileFromReader(source io.Reader, filename string, siaPath skymodules.SiaPath, mode os.FileMode) (skylink string) {
   942  	// Upload the file and return a skylink
   943  	sup := skymodules.SkyfileUploadParameters{
   944  		SiaPath: siaPath,
   945  		Root:    skynetUploadRoot,
   946  
   947  		Filename: filename,
   948  		Mode:     mode,
   949  
   950  		DryRun: skynetUploadDryRun,
   951  		Reader: source,
   952  	}
   953  	sup = parseAndAddSkykey(sup)
   954  	skylink, _, err := httpClient.SkynetSkyfilePost(sup)
   955  	if err != nil {
   956  		die("could not upload file to Skynet:", err)
   957  	}
   958  	return skylink
   959  }
   960  
   961  // newProgressSkylink creates a static progress bar that starts after `afterBar`
   962  // and displays the skylink. The bar is stopped immediately.
   963  func newProgressSkylink(pbs *mpb.Progress, afterBar *mpb.Bar, filename, skylink string) *mpb.Bar {
   964  	bar := pbs.AddBar(
   965  		1, // we'll increment it once to stop it
   966  		mpb.BarQueueAfter(afterBar),
   967  		mpb.PrependDecorators(
   968  			decor.Name(pBarJobDone, decor.WC{W: 10}),
   969  			decor.Name(skylink),
   970  		),
   971  		mpb.AppendDecorators(
   972  			decor.Name(filename, decor.WC{W: len(filename) + 1, C: decor.DidentRight}),
   973  		),
   974  	)
   975  	afterBar.Increment()
   976  	bar.Increment()
   977  	// Wait for finished bars to be rendered.
   978  	pbs.Wait()
   979  	return bar
   980  }