Commit 68bb1069 authored by Jeromy's avatar Jeromy

Delete some now unused commands lib code

License: MIT
Signed-off-by: default avatarJeromy <jeromyj@gmail.com>
parent b18b1e90
package cli
import (
"fmt"
"sort"
"strings"
levenshtein "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein"
cmds "github.com/ipfs/go-ipfs/commands"
)
// Make a custom slice that can be sorted by its levenshtein value
type suggestionSlice []*suggestion
type suggestion struct {
cmd string
levenshtein int
}
func (s suggestionSlice) Len() int {
return len(s)
}
func (s suggestionSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s suggestionSlice) Less(i, j int) bool {
return s[i].levenshtein < s[j].levenshtein
}
func suggestUnknownCmd(args []string, root *cmds.Command) []string {
if root == nil {
return nil
}
arg := args[0]
var suggestions []string
sortableSuggestions := make(suggestionSlice, 0)
var sFinal []string
const MIN_LEVENSHTEIN = 3
var options = levenshtein.Options{
InsCost: 1,
DelCost: 3,
SubCost: 2,
Matches: func(sourceCharacter rune, targetCharacter rune) bool {
return sourceCharacter == targetCharacter
},
}
// Start with a simple strings.Contains check
for name := range root.Subcommands {
if strings.Contains(arg, name) {
suggestions = append(suggestions, name)
}
}
// If the string compare returns a match, return
if len(suggestions) > 0 {
return suggestions
}
for name := range root.Subcommands {
lev := levenshtein.DistanceForStrings([]rune(arg), []rune(name), options)
if lev <= MIN_LEVENSHTEIN {
sortableSuggestions = append(sortableSuggestions, &suggestion{name, lev})
}
}
sort.Sort(sortableSuggestions)
for _, j := range sortableSuggestions {
sFinal = append(sFinal, j.cmd)
}
return sFinal
}
func printSuggestions(inputs []string, root *cmds.Command) (err error) {
suggestions := suggestUnknownCmd(inputs, root)
if len(suggestions) > 1 {
err = fmt.Errorf("Unknown Command \"%s\"\n\nDid you mean any of these?\n\n\t%s", inputs[0], strings.Join(suggestions, "\n\t"))
} else if len(suggestions) > 0 {
err = fmt.Errorf("Unknown Command \"%s\"\n\nDid you mean this?\n\n\t%s", inputs[0], suggestions[0])
} else {
err = fmt.Errorf("Unknown Command %q", inputs[0])
}
return
}
package cli
import (
"fmt"
"io"
"sort"
"strings"
"text/template"
cmds "github.com/ipfs/go-ipfs/commands"
cmdkit "gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
)
const (
requiredArg = "<%v>"
optionalArg = "[<%v>]"
variadicArg = "%v..."
shortFlag = "-%v"
longFlag = "--%v"
indentStr = " "
)
type helpFields struct {
Indent string
Usage string
Path string
ArgUsage string
Tagline string
Arguments string
Options string
Synopsis string
Subcommands string
Description string
MoreHelp bool
}
// TrimNewlines removes extra newlines from fields. This makes aligning
// commands easier. Below, the leading + tralining newlines are removed:
// Synopsis: `
// ipfs config <key> - Get value of <key>
// ipfs config <key> <value> - Set value of <key> to <value>
// ipfs config --show - Show config file
// ipfs config --edit - Edit config file in $EDITOR
// `
func (f *helpFields) TrimNewlines() {
f.Path = strings.Trim(f.Path, "\n")
f.ArgUsage = strings.Trim(f.ArgUsage, "\n")
f.Tagline = strings.Trim(f.Tagline, "\n")
f.Arguments = strings.Trim(f.Arguments, "\n")
f.Options = strings.Trim(f.Options, "\n")
f.Synopsis = strings.Trim(f.Synopsis, "\n")
f.Subcommands = strings.Trim(f.Subcommands, "\n")
f.Description = strings.Trim(f.Description, "\n")
}
// Indent adds whitespace the lines of fields.
func (f *helpFields) IndentAll() {
indent := func(s string) string {
if s == "" {
return s
}
return indentString(s, indentStr)
}
f.Arguments = indent(f.Arguments)
f.Options = indent(f.Options)
f.Synopsis = indent(f.Synopsis)
f.Subcommands = indent(f.Subcommands)
f.Description = indent(f.Description)
}
const usageFormat = "{{if .Usage}}{{.Usage}}{{else}}{{.Path}}{{if .ArgUsage}} {{.ArgUsage}}{{end}} - {{.Tagline}}{{end}}"
const longHelpFormat = `USAGE
{{.Indent}}{{template "usage" .}}
{{if .Synopsis}}SYNOPSIS
{{.Synopsis}}
{{end}}{{if .Arguments}}ARGUMENTS
{{.Arguments}}
{{end}}{{if .Options}}OPTIONS
{{.Options}}
{{end}}{{if .Description}}DESCRIPTION
{{.Description}}
{{end}}{{if .Subcommands}}SUBCOMMANDS
{{.Subcommands}}
{{.Indent}}Use '{{.Path}} <subcmd> --help' for more information about each command.
{{end}}
`
const shortHelpFormat = `USAGE
{{.Indent}}{{template "usage" .}}
{{if .Synopsis}}
{{.Synopsis}}
{{end}}{{if .Description}}
{{.Description}}
{{end}}{{if .Subcommands}}
SUBCOMMANDS
{{.Subcommands}}
{{end}}{{if .MoreHelp}}
Use '{{.Path}} --help' for more information about this command.
{{end}}
`
var usageTemplate *template.Template
var longHelpTemplate *template.Template
var shortHelpTemplate *template.Template
func init() {
usageTemplate = template.Must(template.New("usage").Parse(usageFormat))
longHelpTemplate = template.Must(usageTemplate.New("longHelp").Parse(longHelpFormat))
shortHelpTemplate = template.Must(usageTemplate.New("shortHelp").Parse(shortHelpFormat))
}
// LongHelp writes a formatted CLI helptext string to a Writer for the given command
func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer) error {
cmd, err := root.Get(path)
if err != nil {
return err
}
pathStr := rootName
if len(path) > 0 {
pathStr += " " + strings.Join(path, " ")
}
fields := helpFields{
Indent: indentStr,
Path: pathStr,
ArgUsage: usageText(cmd),
Tagline: cmd.Helptext.Tagline,
Arguments: cmd.Helptext.Arguments,
Options: cmd.Helptext.Options,
Synopsis: cmd.Helptext.Synopsis,
Subcommands: cmd.Helptext.Subcommands,
Description: cmd.Helptext.ShortDescription,
Usage: cmd.Helptext.Usage,
MoreHelp: (cmd != root),
}
if len(cmd.Helptext.LongDescription) > 0 {
fields.Description = cmd.Helptext.LongDescription
}
// autogen fields that are empty
if len(fields.Arguments) == 0 {
fields.Arguments = strings.Join(argumentText(cmd), "\n")
}
if len(fields.Options) == 0 {
fields.Options = strings.Join(optionText(cmd), "\n")
}
if len(fields.Subcommands) == 0 {
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
}
if len(fields.Synopsis) == 0 {
fields.Synopsis = generateSynopsis(cmd, pathStr)
}
// trim the extra newlines (see TrimNewlines doc)
fields.TrimNewlines()
// indent all fields that have been set
fields.IndentAll()
return longHelpTemplate.Execute(out, fields)
}
// ShortHelp writes a formatted CLI helptext string to a Writer for the given command
func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer) error {
cmd, err := root.Get(path)
if err != nil {
return err
}
// default cmd to root if there is no path
if path == nil && cmd == nil {
cmd = root
}
pathStr := rootName
if len(path) > 0 {
pathStr += " " + strings.Join(path, " ")
}
fields := helpFields{
Indent: indentStr,
Path: pathStr,
ArgUsage: usageText(cmd),
Tagline: cmd.Helptext.Tagline,
Synopsis: cmd.Helptext.Synopsis,
Description: cmd.Helptext.ShortDescription,
Subcommands: cmd.Helptext.Subcommands,
Usage: cmd.Helptext.Usage,
MoreHelp: (cmd != root),
}
// autogen fields that are empty
if len(fields.Subcommands) == 0 {
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
}
if len(fields.Synopsis) == 0 {
fields.Synopsis = generateSynopsis(cmd, pathStr)
}
// trim the extra newlines (see TrimNewlines doc)
fields.TrimNewlines()
// indent all fields that have been set
fields.IndentAll()
return shortHelpTemplate.Execute(out, fields)
}
func generateSynopsis(cmd *cmds.Command, path string) string {
res := path
for _, opt := range cmd.Options {
valopt, ok := cmd.Helptext.SynopsisOptionsValues[opt.Names()[0]]
if !ok {
valopt = opt.Names()[0]
}
sopt := ""
for i, n := range opt.Names() {
pre := "-"
if len(n) > 1 {
pre = "--"
}
if opt.Type() == cmdkit.Bool && opt.Default() == true {
pre = "--"
sopt = fmt.Sprintf("%s%s=false", pre, n)
break
} else {
if i == 0 {
if opt.Type() == cmdkit.Bool {
sopt = fmt.Sprintf("%s%s", pre, n)
} else {
sopt = fmt.Sprintf("%s%s=<%s>", pre, n, valopt)
}
} else {
sopt = fmt.Sprintf("%s | %s%s", sopt, pre, n)
}
}
}
res = fmt.Sprintf("%s [%s]", res, sopt)
}
if len(cmd.Arguments) > 0 {
res = fmt.Sprintf("%s [--]", res)
}
for _, arg := range cmd.Arguments {
sarg := fmt.Sprintf("<%s>", arg.Name)
if arg.Variadic {
sarg = sarg + "..."
}
if !arg.Required {
sarg = fmt.Sprintf("[%s]", sarg)
}
res = fmt.Sprintf("%s %s", res, sarg)
}
return strings.Trim(res, " ")
}
func argumentText(cmd *cmds.Command) []string {
lines := make([]string, len(cmd.Arguments))
for i, arg := range cmd.Arguments {
lines[i] = argUsageText(arg)
}
lines = align(lines)
for i, arg := range cmd.Arguments {
lines[i] += " - " + arg.Description
}
return lines
}
func optionFlag(flag string) string {
if len(flag) == 1 {
return fmt.Sprintf(shortFlag, flag)
}
return fmt.Sprintf(longFlag, flag)
}
func optionText(cmd ...*cmds.Command) []string {
// get a slice of the options we want to list out
options := make([]cmdkit.Option, 0)
for _, c := range cmd {
options = append(options, c.Options...)
}
// add option names to output (with each name aligned)
lines := make([]string, 0)
j := 0
for {
done := true
i := 0
for _, opt := range options {
if len(lines) < i+1 {
lines = append(lines, "")
}
names := sortByLength(opt.Names())
if len(names) >= j+1 {
lines[i] += optionFlag(names[j])
}
if len(names) > j+1 {
lines[i] += ", "
done = false
}
i++
}
if done {
break
}
lines = align(lines)
j++
}
lines = align(lines)
// add option types to output
for i, opt := range options {
lines[i] += " " + fmt.Sprintf("%v", opt.Type())
}
lines = align(lines)
// add option descriptions to output
for i, opt := range options {
lines[i] += " - " + opt.Description()
}
return lines
}
func subcommandText(cmd *cmds.Command, rootName string, path []string) []string {
prefix := fmt.Sprintf("%v %v", rootName, strings.Join(path, " "))
if len(path) > 0 {
prefix += " "
}
// Sorting fixes changing order bug #2981.
sortedNames := make([]string, 0)
for name := range cmd.Subcommands {
sortedNames = append(sortedNames, name)
}
sort.Strings(sortedNames)
subcmds := make([]*cmds.Command, len(cmd.Subcommands))
lines := make([]string, len(cmd.Subcommands))
for i, name := range sortedNames {
sub := cmd.Subcommands[name]
usage := usageText(sub)
if len(usage) > 0 {
usage = " " + usage
}
lines[i] = prefix + name + usage
subcmds[i] = sub
}
lines = align(lines)
for i, sub := range subcmds {
lines[i] += " - " + sub.Helptext.Tagline
}
return lines
}
func usageText(cmd *cmds.Command) string {
s := ""
for i, arg := range cmd.Arguments {
if i != 0 {
s += " "
}
s += argUsageText(arg)
}
return s
}
func argUsageText(arg cmdkit.Argument) string {
s := arg.Name
if arg.Required {
s = fmt.Sprintf(requiredArg, s)
} else {
s = fmt.Sprintf(optionalArg, s)
}
if arg.Variadic {
s = fmt.Sprintf(variadicArg, s)
}
return s
}
func align(lines []string) []string {
longest := 0
for _, line := range lines {
length := len(line)
if length > longest {
longest = length
}
}
for i, line := range lines {
length := len(line)
if length > 0 {
lines[i] += strings.Repeat(" ", longest-length)
}
}
return lines
}
func indentString(line string, prefix string) string {
return prefix + strings.Replace(line, "\n", "\n"+prefix, -1)
}
type lengthSlice []string
func (ls lengthSlice) Len() int {
return len(ls)
}
func (ls lengthSlice) Swap(a, b int) {
ls[a], ls[b] = ls[b], ls[a]
}
func (ls lengthSlice) Less(a, b int) bool {
return len(ls[a]) < len(ls[b])
}
func sortByLength(slice []string) []string {
output := make(lengthSlice, len(slice))
for i, val := range slice {
output[i] = val
}
sort.Sort(output)
return []string(output)
}
package cli
import (
"strings"
"testing"
cmds "github.com/ipfs/go-ipfs/commands"
"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
)
func TestSynopsisGenerator(t *testing.T) {
command := &cmds.Command{
Arguments: []cmdkit.Argument{
cmdkit.StringArg("required", true, false, ""),
cmdkit.StringArg("variadic", false, true, ""),
},
Options: []cmdkit.Option{
cmdkit.StringOption("opt", "o", "Option"),
},
Helptext: cmdkit.HelpText{
SynopsisOptionsValues: map[string]string{
"opt": "OPTION",
},
},
}
syn := generateSynopsis(command, "cmd")
t.Logf("Synopsis is: %s", syn)
if !strings.HasPrefix(syn, "cmd ") {
t.Fatal("Synopsis should start with command name")
}
if !strings.Contains(syn, "[--opt=<OPTION> | -o]") {
t.Fatal("Synopsis should contain option descriptor")
}
if !strings.Contains(syn, "<required>") {
t.Fatal("Synopsis should contain required argument")
}
if !strings.Contains(syn, "<variadic>...") {
t.Fatal("Synopsis should contain variadic argument")
}
if !strings.Contains(syn, "[<variadic>...]") {
t.Fatal("Synopsis should contain optional argument")
}
if !strings.Contains(syn, "[--]") {
t.Fatal("Synopsis should contain options finalizer")
}
}
package cli
import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"sort"
"strings"
cmds "github.com/ipfs/go-ipfs/commands"
u "gx/ipfs/QmSU6eubNdhXjFBJBSksTp8kv8YRub8mGAPv8tVJHmL2EU/go-ipfs-util"
logging "gx/ipfs/QmSpJByNKFX1sCsHBEp3R73FL4NF6FnQTEGyNAXHm2GS52/go-log"
"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit/files"
osh "gx/ipfs/QmXuBJ7DR6k3rmUEKtvVMhwjmXDuJgXXPUt4LQXKBMsU93/go-os-helper"
)
var log = logging.Logger("commands/cli")
// Parse parses the input commandline string (cmd, flags, and args).
// returns the corresponding command Request object.
func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *cmds.Command, []string, error) {
path, opts, stringVals, cmd, err := parseOpts(input, root)
if err != nil {
return nil, nil, path, err
}
optDefs, err := root.GetOptions(path)
if err != nil {
return nil, cmd, path, err
}
req, err := cmds.NewRequest(path, opts, nil, nil, cmd, optDefs)
if err != nil {
return nil, cmd, path, err
}
// This is an ugly hack to maintain our current CLI interface while fixing
// other stdin usage bugs. Let this serve as a warning, be careful about the
// choices you make, they will haunt you forever.
if len(path) == 2 && path[0] == "bootstrap" {
if (path[1] == "add" && opts["default"] == true) ||
(path[1] == "rm" && opts["all"] == true) {
stdin = nil
}
}
stringArgs, fileArgs, err := ParseArgs(req, stringVals, stdin, cmd.Arguments, root)
if err != nil {
return req, cmd, path, err
}
req.SetArguments(stringArgs)
if len(fileArgs) > 0 {
file := files.NewSliceFile("", "", fileArgs)
req.SetFiles(file)
}
err = cmd.CheckArguments(req)
return req, cmd, path, err
}
func ParseArgs(req cmds.Request, inputs []string, stdin *os.File, argDefs []cmdkit.Argument, root *cmds.Command) ([]string, []files.File, error) {
var err error
// if -r is provided, and it is associated with the package builtin
// recursive path option, allow recursive file paths
recursiveOpt := req.Option(cmdkit.RecShort)
recursive := false
if recursiveOpt != nil && recursiveOpt.Definition() == cmdkit.OptionRecursivePath {
recursive, _, err = recursiveOpt.Bool()
if err != nil {
return nil, nil, u.ErrCast()
}
}
// if '--hidden' is provided, enumerate hidden paths
hiddenOpt := req.Option("hidden")
hidden := false
if hiddenOpt != nil {
hidden, _, err = hiddenOpt.Bool()
if err != nil {
return nil, nil, u.ErrCast()
}
}
return parseArgs(inputs, stdin, argDefs, recursive, hidden, root)
}
// Parse a command line made up of sub-commands, short arguments, long arguments and positional arguments
func parseOpts(args []string, root *cmds.Command) (
path []string,
opts map[string]interface{},
stringVals []string,
cmd *cmds.Command,
err error,
) {
path = make([]string, 0, len(args))
stringVals = make([]string, 0, len(args))
optDefs := map[string]cmdkit.Option{}
opts = map[string]interface{}{}
cmd = root
// parseFlag checks that a flag is valid and saves it into opts
// Returns true if the optional second argument is used
parseFlag := func(name string, arg *string, mustUse bool) (bool, error) {
if _, ok := opts[name]; ok {
return false, fmt.Errorf("Duplicate values for option '%s'", name)
}
optDef, found := optDefs[name]
if !found {
err = fmt.Errorf("Unrecognized option '%s'", name)
return false, err
}
// mustUse implies that you must use the argument given after the '='
// eg. -r=true means you must take true into consideration
// mustUse == true in the above case
// eg. ipfs -r <file> means disregard <file> since there is no '='
// mustUse == false in the above situation
//arg == nil implies the flag was specified without an argument
if optDef.Type() == cmdkit.Bool {
if arg == nil || !mustUse {
opts[name] = true
return false, nil
}
argVal := strings.ToLower(*arg)
switch argVal {
case "true":
opts[name] = true
return true, nil
case "false":
opts[name] = false
return true, nil
default:
return true, fmt.Errorf("Option '%s' takes true/false arguments, but was passed '%s'", name, argVal)
}
} else {
if arg == nil {
return true, fmt.Errorf("Missing argument for option '%s'", name)
}
opts[name] = *arg
return true, nil
}
}
optDefs, err = root.GetOptions(path)
if err != nil {
return
}
consumed := false
for i, arg := range args {
switch {
case consumed:
// arg was already consumed by the preceding flag
consumed = false
continue
case arg == "--":
// treat all remaining arguments as positional arguments
stringVals = append(stringVals, args[i+1:]...)
return
case strings.HasPrefix(arg, "--"):
// arg is a long flag, with an optional argument specified
// using `=' or in args[i+1]
var slurped bool
var next *string
split := strings.SplitN(arg, "=", 2)
if len(split) == 2 {
slurped = false
arg = split[0]
next = &split[1]
} else {
slurped = true
if i+1 < len(args) {
next = &args[i+1]
} else {
next = nil
}
}
consumed, err = parseFlag(arg[2:], next, len(split) == 2)
if err != nil {
return
}
if !slurped {
consumed = false
}
case strings.HasPrefix(arg, "-") && arg != "-":
// args is one or more flags in short form, followed by an optional argument
// all flags except the last one have type bool
for arg = arg[1:]; len(arg) != 0; arg = arg[1:] {
var rest *string
var slurped bool
mustUse := false
if len(arg) > 1 {
slurped = false
str := arg[1:]
if len(str) > 0 && str[0] == '=' {
str = str[1:]
mustUse = true
}
rest = &str
} else {
slurped = true
if i+1 < len(args) {
rest = &args[i+1]
} else {
rest = nil
}
}
var end bool
end, err = parseFlag(arg[:1], rest, mustUse)
if err != nil {
return
}
if end {
consumed = slurped
break
}
}
default:
// arg is a sub-command or a positional argument
sub := cmd.Subcommand(arg)
if sub != nil {
cmd = sub
path = append(path, arg)
optDefs, err = root.GetOptions(path)
if err != nil {
return
}
// If we've come across an external binary call, pass all the remaining
// arguments on to it
if cmd.External {
stringVals = append(stringVals, args[i+1:]...)
return
}
} else {
stringVals = append(stringVals, arg)
if len(path) == 0 {
// found a typo or early argument
err = printSuggestions(stringVals, root)
return
}
}
}
}
return
}
const msgStdinInfo = "ipfs: Reading from %s; send Ctrl-d to stop."
func parseArgs(inputs []string, stdin *os.File, argDefs []cmdkit.Argument, recursive, hidden bool, root *cmds.Command) ([]string, []files.File, error) {
// ignore stdin on Windows
if osh.IsWindows() {
stdin = nil
}
// count required argument definitions
numRequired := 0
for _, argDef := range argDefs {
if argDef.Required {
numRequired++
}
}
// count number of values provided by user.
// if there is at least one ArgDef, we can safely trigger the inputs loop
// below to parse stdin.
numInputs := len(inputs)
if len(argDefs) > 0 && argDefs[len(argDefs)-1].SupportsStdin && stdin != nil {
numInputs++
}
// if we have more arg values provided than argument definitions,
// and the last arg definition is not variadic (or there are no definitions), return an error
notVariadic := len(argDefs) == 0 || !argDefs[len(argDefs)-1].Variadic
if notVariadic && len(inputs) > len(argDefs) {
err := printSuggestions(inputs, root)
return nil, nil, err
}
stringArgs := make([]string, 0, numInputs)
fileArgs := make(map[string]files.File)
argDefIndex := 0 // the index of the current argument definition
for i := 0; i < numInputs; i++ {
argDef := getArgDef(argDefIndex, argDefs)
// skip optional argument definitions if there aren't sufficient remaining inputs
for numInputs-i <= numRequired && !argDef.Required {
argDefIndex++
argDef = getArgDef(argDefIndex, argDefs)
}
if argDef.Required {
numRequired--
}
fillingVariadic := argDefIndex+1 > len(argDefs)
switch argDef.Type {
case cmdkit.ArgString:
if len(inputs) > 0 {
stringArgs, inputs = append(stringArgs, inputs[0]), inputs[1:]
} else if stdin != nil && argDef.SupportsStdin && !fillingVariadic {
if r, err := maybeWrapStdin(stdin, msgStdinInfo); err == nil {
fileArgs[stdin.Name()] = files.NewReaderFile("stdin", "", r, nil)
stdin = nil
}
}
case cmdkit.ArgFile:
if len(inputs) > 0 {
// treat stringArg values as file paths
fpath := inputs[0]
inputs = inputs[1:]
var file files.File
if fpath == "-" {
r, err := maybeWrapStdin(stdin, msgStdinInfo)
if err != nil {
return nil, nil, err
}
fpath = stdin.Name()
file = files.NewReaderFile("", fpath, r, nil)
} else {
nf, err := appendFile(fpath, argDef, recursive, hidden)
if err != nil {
return nil, nil, err
}
file = nf
}
fileArgs[fpath] = file
} else if stdin != nil && argDef.SupportsStdin &&
argDef.Required && !fillingVariadic {
r, err := maybeWrapStdin(stdin, msgStdinInfo)
if err != nil {
return nil, nil, err
}
fpath := stdin.Name()
fileArgs[fpath] = files.NewReaderFile("", fpath, r, nil)
}
}
argDefIndex++
}
// check to make sure we didn't miss any required arguments
if len(argDefs) > argDefIndex {
for _, argDef := range argDefs[argDefIndex:] {
if argDef.Required {
return nil, nil, fmt.Errorf("Argument '%s' is required", argDef.Name)
}
}
}
return stringArgs, filesMapToSortedArr(fileArgs), nil
}
func filesMapToSortedArr(fs map[string]files.File) []files.File {
var names []string
for name := range fs {
names = append(names, name)
}
sort.Strings(names)
var out []files.File
for _, f := range names {
out = append(out, fs[f])
}
return out
}
func getArgDef(i int, argDefs []cmdkit.Argument) *cmdkit.Argument {
if i < len(argDefs) {
// get the argument definition (usually just argDefs[i])
return &argDefs[i]
} else if len(argDefs) > 0 {
// but if i > len(argDefs) we use the last argument definition)
return &argDefs[len(argDefs)-1]
}
// only happens if there aren't any definitions
return nil
}
const notRecursiveFmtStr = "'%s' is a directory, use the '-%s' flag to specify directories"
const dirNotSupportedFmtStr = "Invalid path '%s', argument '%s' does not support directories"
const winDriveLetterFmtStr = "%q is a drive letter, not a drive path"
func appendFile(fpath string, argDef *cmdkit.Argument, recursive, hidden bool) (files.File, error) {
// resolve Windows relative dot paths like `X:.\somepath`
if osh.IsWindows() {
if len(fpath) >= 3 && fpath[1:3] == ":." {
var err error
fpath, err = filepath.Abs(fpath)
if err != nil {
return nil, err
}
}
}
if fpath == "." {
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
cwd, err = filepath.EvalSymlinks(cwd)
if err != nil {
return nil, err
}
fpath = cwd
}
fpath = filepath.Clean(fpath)
stat, err := os.Lstat(fpath)
if err != nil {
return nil, err
}
if stat.IsDir() {
if !argDef.Recursive {
return nil, fmt.Errorf(dirNotSupportedFmtStr, fpath, argDef.Name)
}
if !recursive {
return nil, fmt.Errorf(notRecursiveFmtStr, fpath, cmdkit.RecShort)
}
}
if osh.IsWindows() {
return windowsParseFile(fpath, hidden, stat)
}
return files.NewSerialFile(path.Base(fpath), fpath, hidden, stat)
}
// Inform the user if a file is waiting on input
func maybeWrapStdin(f *os.File, msg string) (io.ReadCloser, error) {
isTty, err := isTty(f)
if err != nil {
return nil, err
}
if isTty {
return newMessageReader(f, fmt.Sprintf(msg, f.Name())), nil
}
return f, nil
}
func isTty(f *os.File) (bool, error) {
fInfo, err := f.Stat()
if err != nil {
log.Error(err)
return false, err
}
return (fInfo.Mode() & os.ModeCharDevice) != 0, nil
}
type messageReader struct {
r io.ReadCloser
done bool
message string
}
func newMessageReader(r io.ReadCloser, msg string) io.ReadCloser {
return &messageReader{
r: r,
message: msg,
}
}
func (r *messageReader) Read(b []byte) (int, error) {
if !r.done {
fmt.Fprintln(os.Stderr, r.message)
r.done = true
}
return r.r.Read(b)
}
func (r *messageReader) Close() error {
return r.r.Close()
}
func windowsParseFile(fpath string, hidden bool, stat os.FileInfo) (files.File, error) {
// special cases for Windows drive roots i.e. `X:\` and their long form `\\?\X:\`
// drive path must be preserved as `X:\` (or it's longform) and not converted to `X:`, `X:.`, `\`, or `/` here
switch len(fpath) {
case 3:
// `X:` is cleaned to `X:.` which may not be the expected behaviour by the user, they'll need to provide more specific input
if fpath[1:3] == ":." {
return nil, fmt.Errorf(winDriveLetterFmtStr, fpath[:2])
}
// `X:\` needs to preserve the `\`, path.Base(filepath.ToSlash(fpath)) results in `X:` which is not valid
if fpath[1:3] == ":\\" {
return files.NewSerialFile(fpath, fpath, hidden, stat)
}
case 6:
// `\\?\X:` long prefix form of `X:`, still ambiguous
if fpath[:4] == "\\\\?\\" && fpath[5] == ':' {
return nil, fmt.Errorf(winDriveLetterFmtStr, fpath)
}
case 7:
// `\\?\X:\` long prefix form is translated into short form `X:\`
if fpath[:4] == "\\\\?\\" && fpath[5] == ':' && fpath[6] == '\\' {
fpath = string(fpath[4]) + ":\\"
return files.NewSerialFile(fpath, fpath, hidden, stat)
}
}
return files.NewSerialFile(path.Base(filepath.ToSlash(fpath)), fpath, hidden, stat)
}
package cli
import (
"io"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
"github.com/ipfs/go-ipfs/commands"
"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
)
type kvs map[string]interface{}
type words []string
func sameWords(a words, b words) bool {
if len(a) != len(b) {
return false
}
for i, w := range a {
if w != b[i] {
return false
}
}
return true
}
func sameKVs(a kvs, b kvs) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if v != b[k] {
return false
}
}
return true
}
func TestSameWords(t *testing.T) {
a := []string{"v1", "v2"}
b := []string{"v1", "v2", "v3"}
c := []string{"v2", "v3"}
d := []string{"v2"}
e := []string{"v2", "v3"}
f := []string{"v2", "v1"}
test := func(a words, b words, v bool) {
if sameWords(a, b) != v {
t.Errorf("sameWords('%v', '%v') != %v", a, b, v)
}
}
test(a, b, false)
test(a, a, true)
test(a, c, false)
test(b, c, false)
test(c, d, false)
test(c, e, true)
test(b, e, false)
test(a, b, false)
test(a, f, false)
test(e, f, false)
test(f, f, true)
}
func TestOptionParsing(t *testing.T) {
subCmd := &commands.Command{}
cmd := &commands.Command{
Options: []cmdkit.Option{
cmdkit.StringOption("string", "s", "a string"),
cmdkit.BoolOption("bool", "b", "a bool"),
},
Subcommands: map[string]*commands.Command{
"test": subCmd,
},
}
testHelper := func(args string, expectedOpts kvs, expectedWords words, expectErr bool) {
var opts map[string]interface{}
var input []string
_, opts, input, _, err := parseOpts(strings.Split(args, " "), cmd)
if expectErr {
if err == nil {
t.Errorf("Command line '%v' parsing should have failed", args)
}
} else if err != nil {
t.Errorf("Command line '%v' failed to parse: %v", args, err)
} else if !sameWords(input, expectedWords) || !sameKVs(opts, expectedOpts) {
t.Errorf("Command line '%v':\n parsed as %v %v\n instead of %v %v",
args, opts, input, expectedOpts, expectedWords)
}
}
testFail := func(args string) {
testHelper(args, kvs{}, words{}, true)
}
test := func(args string, expectedOpts kvs, expectedWords words) {
testHelper(args, expectedOpts, expectedWords, false)
}
test("test -", kvs{}, words{"-"})
testFail("-b -b")
test("test beep boop", kvs{}, words{"beep", "boop"})
testFail("-s")
test("-s foo", kvs{"s": "foo"}, words{})
test("-sfoo", kvs{"s": "foo"}, words{})
test("-s=foo", kvs{"s": "foo"}, words{})
test("-b", kvs{"b": true}, words{})
test("-bs foo", kvs{"b": true, "s": "foo"}, words{})
test("-sb", kvs{"s": "b"}, words{})
test("-b test foo", kvs{"b": true}, words{"foo"})
test("--bool test foo", kvs{"bool": true}, words{"foo"})
testFail("--bool=foo")
testFail("--string")
test("--string foo", kvs{"string": "foo"}, words{})
test("--string=foo", kvs{"string": "foo"}, words{})
test("-- -b", kvs{}, words{"-b"})
test("test foo -b", kvs{"b": true}, words{"foo"})
test("-b=false", kvs{"b": false}, words{})
test("-b=true", kvs{"b": true}, words{})
test("-b=false test foo", kvs{"b": false}, words{"foo"})
test("-b=true test foo", kvs{"b": true}, words{"foo"})
test("--bool=true test foo", kvs{"bool": true}, words{"foo"})
test("--bool=false test foo", kvs{"bool": false}, words{"foo"})
test("-b test true", kvs{"b": true}, words{"true"})
test("-b test false", kvs{"b": true}, words{"false"})
test("-b=FaLsE test foo", kvs{"b": false}, words{"foo"})
test("-b=TrUe test foo", kvs{"b": true}, words{"foo"})
test("-b test true", kvs{"b": true}, words{"true"})
test("-b test false", kvs{"b": true}, words{"false"})
test("-b --string foo test bar", kvs{"b": true, "string": "foo"}, words{"bar"})
test("-b=false --string bar", kvs{"b": false, "string": "bar"}, words{})
testFail("foo test")
}
func TestArgumentParsing(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("stdin handling doesnt yet work on windows")
}
rootCmd := &commands.Command{
Subcommands: map[string]*commands.Command{
"noarg": {},
"onearg": {
Arguments: []cmdkit.Argument{
cmdkit.StringArg("a", true, false, "some arg"),
},
},
"twoargs": {
Arguments: []cmdkit.Argument{
cmdkit.StringArg("a", true, false, "some arg"),
cmdkit.StringArg("b", true, false, "another arg"),
},
},
"variadic": {
Arguments: []cmdkit.Argument{
cmdkit.StringArg("a", true, true, "some arg"),
},
},
"optional": {
Arguments: []cmdkit.Argument{
cmdkit.StringArg("b", false, true, "another arg"),
},
},
"optionalsecond": {
Arguments: []cmdkit.Argument{
cmdkit.StringArg("a", true, false, "some arg"),
cmdkit.StringArg("b", false, false, "another arg"),
},
},
"reversedoptional": {
Arguments: []cmdkit.Argument{
cmdkit.StringArg("a", false, false, "some arg"),
cmdkit.StringArg("b", true, false, "another arg"),
},
},
"stdinenabled": {
Arguments: []cmdkit.Argument{
cmdkit.StringArg("a", true, true, "some arg").EnableStdin(),
},
},
"stdinenabled2args": &commands.Command{
Arguments: []cmdkit.Argument{
cmdkit.StringArg("a", true, false, "some arg"),
cmdkit.StringArg("b", true, true, "another arg").EnableStdin(),
},
},
"stdinenablednotvariadic": &commands.Command{
Arguments: []cmdkit.Argument{
cmdkit.StringArg("a", true, false, "some arg").EnableStdin(),
},
},
"stdinenablednotvariadic2args": &commands.Command{
Arguments: []cmdkit.Argument{
cmdkit.StringArg("a", true, false, "some arg"),
cmdkit.StringArg("b", true, false, "another arg").EnableStdin(),
},
},
},
}
test := func(cmd words, f *os.File, res words) {
if f != nil {
if _, err := f.Seek(0, io.SeekStart); err != nil {
t.Fatal(err)
}
}
req, _, _, err := Parse(cmd, f, rootCmd)
if err != nil {
t.Errorf("Command '%v' should have passed parsing: %v", cmd, err)
}
if !sameWords(req.Arguments(), res) {
t.Errorf("Arguments parsed from '%v' are '%v' instead of '%v'", cmd, req.Arguments(), res)
}
}
testFail := func(cmd words, fi *os.File, msg string) {
_, _, _, err := Parse(cmd, nil, rootCmd)
if err == nil {
t.Errorf("Should have failed: %v", msg)
}
}
test([]string{"noarg"}, nil, []string{})
testFail([]string{"noarg", "value!"}, nil, "provided an arg, but command didn't define any")
test([]string{"onearg", "value!"}, nil, []string{"value!"})
testFail([]string{"onearg"}, nil, "didn't provide any args, arg is required")
test([]string{"twoargs", "value1", "value2"}, nil, []string{"value1", "value2"})
testFail([]string{"twoargs", "value!"}, nil, "only provided 1 arg, needs 2")
testFail([]string{"twoargs"}, nil, "didn't provide any args, 2 required")
test([]string{"variadic", "value!"}, nil, []string{"value!"})
test([]string{"variadic", "value1", "value2", "value3"}, nil, []string{"value1", "value2", "value3"})
testFail([]string{"variadic"}, nil, "didn't provide any args, 1 required")
test([]string{"optional", "value!"}, nil, []string{"value!"})
test([]string{"optional"}, nil, []string{})
test([]string{"optional", "value1", "value2"}, nil, []string{"value1", "value2"})
test([]string{"optionalsecond", "value!"}, nil, []string{"value!"})
test([]string{"optionalsecond", "value1", "value2"}, nil, []string{"value1", "value2"})
testFail([]string{"optionalsecond"}, nil, "didn't provide any args, 1 required")
testFail([]string{"optionalsecond", "value1", "value2", "value3"}, nil, "provided too many args, takes 2 maximum")
test([]string{"reversedoptional", "value1", "value2"}, nil, []string{"value1", "value2"})
test([]string{"reversedoptional", "value!"}, nil, []string{"value!"})
testFail([]string{"reversedoptional"}, nil, "didn't provide any args, 1 required")
testFail([]string{"reversedoptional", "value1", "value2", "value3"}, nil, "provided too many args, only takes 1")
// Use a temp file to simulate stdin
fileToSimulateStdin := func(t *testing.T, content string) *os.File {
fstdin, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fstdin.Name())
if _, err := io.WriteString(fstdin, content); err != nil {
t.Fatal(err)
}
return fstdin
}
test([]string{"stdinenabled", "value1", "value2"}, nil, []string{"value1", "value2"})
fstdin := fileToSimulateStdin(t, "stdin1")
test([]string{"stdinenabled"}, fstdin, []string{"stdin1"})
test([]string{"stdinenabled", "value1"}, fstdin, []string{"value1"})
test([]string{"stdinenabled", "value1", "value2"}, fstdin, []string{"value1", "value2"})
fstdin = fileToSimulateStdin(t, "stdin1\nstdin2")
test([]string{"stdinenabled"}, fstdin, []string{"stdin1", "stdin2"})
fstdin = fileToSimulateStdin(t, "stdin1\nstdin2\nstdin3")
test([]string{"stdinenabled"}, fstdin, []string{"stdin1", "stdin2", "stdin3"})
test([]string{"stdinenabled2args", "value1", "value2"}, nil, []string{"value1", "value2"})
fstdin = fileToSimulateStdin(t, "stdin1")
test([]string{"stdinenabled2args", "value1"}, fstdin, []string{"value1", "stdin1"})
test([]string{"stdinenabled2args", "value1", "value2"}, fstdin, []string{"value1", "value2"})
test([]string{"stdinenabled2args", "value1", "value2", "value3"}, fstdin, []string{"value1", "value2", "value3"})
fstdin = fileToSimulateStdin(t, "stdin1\nstdin2")
test([]string{"stdinenabled2args", "value1"}, fstdin, []string{"value1", "stdin1", "stdin2"})
test([]string{"stdinenablednotvariadic", "value1"}, nil, []string{"value1"})
fstdin = fileToSimulateStdin(t, "stdin1")
test([]string{"stdinenablednotvariadic"}, fstdin, []string{"stdin1"})
test([]string{"stdinenablednotvariadic", "value1"}, fstdin, []string{"value1"})
test([]string{"stdinenablednotvariadic2args", "value1", "value2"}, nil, []string{"value1", "value2"})
fstdin = fileToSimulateStdin(t, "stdin1")
test([]string{"stdinenablednotvariadic2args", "value1"}, fstdin, []string{"value1", "stdin1"})
test([]string{"stdinenablednotvariadic2args", "value1", "value2"}, fstdin, []string{"value1", "value2"})
testFail([]string{"stdinenablednotvariadic2args"}, fstdin, "cant use stdin for non stdin arg")
fstdin = fileToSimulateStdin(t, "stdin1")
test([]string{"noarg"}, fstdin, []string{})
fstdin = fileToSimulateStdin(t, "stdin1")
test([]string{"optionalsecond", "value1", "value2"}, fstdin, []string{"value1", "value2"})
}
package http
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
cmds "github.com/ipfs/go-ipfs/commands"
config "github.com/ipfs/go-ipfs/repo/config"
"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
)
const (
ApiUrlFormat = "http://%s%s/%s?%s"
ApiPath = "/api/v0" // TODO: make configurable
)
var OptionSkipMap = map[string]bool{
"api": true,
}
// Client is the commands HTTP client interface.
type Client interface {
Send(req cmds.Request) (cmds.Response, error)
}
type client struct {
serverAddress string
httpClient *http.Client
}
func NewClient(address string) Client {
return &client{
serverAddress: address,
httpClient: http.DefaultClient,
}
}
func (c *client) Send(req cmds.Request) (cmds.Response, error) {
if req.Context() == nil {
log.Warningf("no context set in request")
if err := req.SetRootContext(context.TODO()); err != nil {
return nil, err
}
}
// save user-provided encoding
previousUserProvidedEncoding, found, err := req.Option(cmdkit.EncShort).String()
if err != nil {
return nil, err
}
// override with json to send to server
req.SetOption(cmdkit.EncShort, cmds.JSON)
// stream channel output
req.SetOption(cmdkit.ChanOpt, "true")
query, err := getQuery(req)
if err != nil {
return nil, err
}
var fileReader *MultiFileReader
var reader io.Reader
if req.Files() != nil {
fileReader = NewMultiFileReader(req.Files(), true)
reader = fileReader
}
path := strings.Join(req.Path(), "/")
url := fmt.Sprintf(ApiUrlFormat, c.serverAddress, ApiPath, path, query)
httpReq, err := http.NewRequest("POST", url, reader)
if err != nil {
return nil, err
}
// TODO extract string consts?
if fileReader != nil {
httpReq.Header.Set(contentTypeHeader, "multipart/form-data; boundary="+fileReader.Boundary())
} else {
httpReq.Header.Set(contentTypeHeader, applicationOctetStream)
}
httpReq.Header.Set(uaHeader, config.ApiVersion)
httpReq.Cancel = req.Context().Done()
httpReq.Close = true
httpRes, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, err
}
// using the overridden JSON encoding in request
res, err := getResponse(httpRes, req)
if err != nil {
return nil, err
}
if found && len(previousUserProvidedEncoding) > 0 {
// reset to user provided encoding after sending request
// NB: if user has provided an encoding but it is the empty string,
// still leave it as JSON.
req.SetOption(cmdkit.EncShort, previousUserProvidedEncoding)
}
return res, nil
}
func getQuery(req cmds.Request) (string, error) {
query := url.Values{}
for k, v := range req.Options() {
if OptionSkipMap[k] {
continue
}
str := fmt.Sprintf("%v", v)
query.Set(k, str)
}
args := req.StringArguments()
argDefs := req.Command().Arguments
argDefIndex := 0
for _, arg := range args {
argDef := argDefs[argDefIndex]
// skip ArgFiles
for argDef.Type == cmdkit.ArgFile {
argDefIndex++
argDef = argDefs[argDefIndex]
}
query.Add("arg", arg)
if len(argDefs) > argDefIndex+1 {
argDefIndex++
}
}
return query.Encode(), nil
}
// getResponse decodes a http.Response to create a cmds.Response
func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error) {
var err error
res := cmds.NewResponse(req)
contentType := httpRes.Header.Get(contentTypeHeader)
contentType = strings.Split(contentType, ";")[0]
lengthHeader := httpRes.Header.Get(extraContentLengthHeader)
if len(lengthHeader) > 0 {
length, err := strconv.ParseUint(lengthHeader, 10, 64)
if err != nil {
return nil, err
}
res.SetLength(length)
}
rr := &httpResponseReader{httpRes}
res.SetCloser(rr)
if contentType != applicationJson {
// for all non json output types, just stream back the output
res.SetOutput(rr)
return res, nil
} else if len(httpRes.Header.Get(channelHeader)) > 0 {
// if output is coming from a channel, decode each chunk
outChan := make(chan interface{})
go readStreamedJson(req, rr, outChan, res)
res.SetOutput((<-chan interface{})(outChan))
return res, nil
}
dec := json.NewDecoder(rr)
// If we ran into an error
if httpRes.StatusCode >= http.StatusBadRequest {
var e *cmdkit.Error
switch {
case httpRes.StatusCode == http.StatusNotFound:
// handle 404s
e = &cmdkit.Error{Message: "Command not found.", Code: cmdkit.ErrClient}
case contentType == plainText:
// handle non-marshalled errors
mes, err := ioutil.ReadAll(rr)
if err != nil {
return nil, err
}
e = &cmdkit.Error{Message: string(mes), Code: cmdkit.ErrNormal}
default:
// handle marshalled errors
var rxErr cmdkit.Error
err = dec.Decode(&rxErr)
if err != nil {
return nil, err
}
e = &rxErr
}
res.SetError(e, e.Code)
return res, nil
}
outputType := reflect.TypeOf(req.Command().Type)
v, err := decodeTypedVal(outputType, dec)
if err != nil && err != io.EOF {
return nil, err
}
res.SetOutput(v)
return res, nil
}
// read json objects off of the given stream, and write the objects out to
// the 'out' channel
func readStreamedJson(req cmds.Request, rr io.Reader, out chan<- interface{}, resp cmds.Response) {
defer close(out)
dec := json.NewDecoder(rr)
outputType := reflect.TypeOf(req.Command().Type)
ctx := req.Context()
for {
v, err := decodeTypedVal(outputType, dec)
if err != nil {
if err != io.EOF {
log.Error(err)
resp.SetError(err, cmdkit.ErrNormal)
}
return
}
select {
case <-ctx.Done():
return
case out <- v:
}
}
}
// decode a value of the given type, if the type is nil, attempt to decode into
// an interface{} anyways
func decodeTypedVal(t reflect.Type, dec *json.Decoder) (interface{}, error) {
var v interface{}
var err error
if t != nil {
v = reflect.New(t).Interface()
err = dec.Decode(v)
} else {
err = dec.Decode(&v)
}
return v, err
}
// httpResponseReader reads from the response body, and checks for an error
// in the http trailer upon EOF, this error if present is returned instead
// of the EOF.
type httpResponseReader struct {
resp *http.Response
}
func (r *httpResponseReader) Read(b []byte) (int, error) {
n, err := r.resp.Body.Read(b)
// reading on a closed response body is as good as an io.EOF here
if err != nil && strings.Contains(err.Error(), "read on closed response body") {
err = io.EOF
}
if err == io.EOF {
_ = r.resp.Body.Close()
trailerErr := r.checkError()
if trailerErr != nil {
return n, trailerErr
}
}
return n, err
}
func (r *httpResponseReader) checkError() error {
if e := r.resp.Trailer.Get(StreamErrHeader); e != "" {
return errors.New(e)
}
return nil
}
func (r *httpResponseReader) Close() error {
return r.resp.Body.Close()
}
package http
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"runtime/debug"
"strconv"
"strings"
"sync"
cmds "github.com/ipfs/go-ipfs/commands"
"github.com/ipfs/go-ipfs/repo/config"
cors "gx/ipfs/QmPG2kW5t27LuHgHnvhUwbHCNHAt2eUcb4gPHqofrESUdB/cors"
logging "gx/ipfs/QmSpJByNKFX1sCsHBEp3R73FL4NF6FnQTEGyNAXHm2GS52/go-log"
loggables "gx/ipfs/QmT4PgCNdv73hnFAqzHqwW44q7M9PWpykSswHDxndquZbc/go-libp2p-loggables"
cmdkit "gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
)
var log = logging.Logger("commands/http")
// the internal handler for the API
type internalHandler struct {
ctx cmds.Context
root *cmds.Command
cfg *ServerConfig
}
// The Handler struct is funny because we want to wrap our internal handler
// with CORS while keeping our fields.
type Handler struct {
internalHandler
corsHandler http.Handler
}
var (
ErrNotFound = errors.New("404 page not found")
errApiVersionMismatch = errors.New("api version mismatch")
)
const (
StreamErrHeader = "X-Stream-Error"
streamHeader = "X-Stream-Output"
channelHeader = "X-Chunked-Output"
extraContentLengthHeader = "X-Content-Length"
uaHeader = "User-Agent"
contentTypeHeader = "Content-Type"
applicationJson = "application/json"
applicationOctetStream = "application/octet-stream"
plainText = "text/plain"
)
var AllowedExposedHeadersArr = []string{streamHeader, channelHeader, extraContentLengthHeader}
var AllowedExposedHeaders = strings.Join(AllowedExposedHeadersArr, ", ")
const (
ACAOrigin = "Access-Control-Allow-Origin"
ACAMethods = "Access-Control-Allow-Methods"
ACACredentials = "Access-Control-Allow-Credentials"
)
var mimeTypes = map[string]string{
cmds.Protobuf: "application/protobuf",
cmds.JSON: "application/json",
cmds.XML: "application/xml",
cmds.Text: "text/plain",
}
type ServerConfig struct {
// Headers is an optional map of headers that is written out.
Headers map[string][]string
// cORSOpts is a set of options for CORS headers.
cORSOpts *cors.Options
// cORSOptsRWMutex is a RWMutex for read/write CORSOpts
cORSOptsRWMutex sync.RWMutex
}
func skipAPIHeader(h string) bool {
switch h {
case "Access-Control-Allow-Origin":
return true
case "Access-Control-Allow-Methods":
return true
case "Access-Control-Allow-Credentials":
return true
default:
return false
}
}
func NewHandler(ctx cmds.Context, root *cmds.Command, cfg *ServerConfig) http.Handler {
if cfg == nil {
panic("must provide a valid ServerConfig")
}
// setup request logger
ctx.ReqLog = new(cmds.ReqLog)
// Wrap the internal handler with CORS handling-middleware.
// Create a handler for the API.
internal := internalHandler{
ctx: ctx,
root: root,
cfg: cfg,
}
c := cors.New(*cfg.cORSOpts)
return &Handler{internal, c.Handler(internal)}
}
func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Call the CORS handler which wraps the internal handler.
i.corsHandler.ServeHTTP(w, r)
}
func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Debug("incoming API request: ", r.URL)
defer func() {
if r := recover(); r != nil {
log.Error("a panic has occurred in the commands handler!")
log.Error(r)
debug.PrintStack()
}
}()
// get the node's context to pass into the commands.
node, err := i.ctx.GetNode()
if err != nil {
s := fmt.Sprintf("cmds/http: couldn't GetNode(): %s", err)
http.Error(w, s, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithCancel(node.Context())
defer cancel()
ctx = logging.ContextWithLoggable(ctx, loggables.Uuid("requestId"))
if cn, ok := w.(http.CloseNotifier); ok {
clientGone := cn.CloseNotify()
go func() {
select {
case <-clientGone:
case <-ctx.Done():
}
cancel()
}()
}
if !allowOrigin(r, i.cfg) || !allowReferer(r, i.cfg) {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("403 - Forbidden"))
log.Warningf("API blocked request to %s. (possible CSRF)", r.URL)
return
}
req, err := Parse(r, i.root)
if err != nil {
if err == ErrNotFound {
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(http.StatusBadRequest)
}
w.Write([]byte(err.Error()))
return
}
reqLogEnt := i.ctx.ReqLog.Add(req)
defer i.ctx.ReqLog.Finish(reqLogEnt)
//ps: take note of the name clash - commands.Context != context.Context
req.SetInvocContext(i.ctx)
err = req.SetRootContext(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// call the command
res := i.root.Call(req)
// set user's headers first.
for k, v := range i.cfg.Headers {
if !skipAPIHeader(k) {
w.Header()[k] = v
}
}
// now handle responding to the client properly
sendResponse(w, r, res, req)
}
func guessMimeType(res cmds.Response) (string, error) {
// Try to guess mimeType from the encoding option
enc, found, err := res.Request().Option(cmdkit.EncShort).String()
if err != nil {
return "", err
}
if !found {
return "", errors.New("no encoding option set")
}
if m, ok := mimeTypes[enc]; ok {
return m, nil
}
return mimeTypes[cmds.JSON], nil
}
func sendResponse(w http.ResponseWriter, r *http.Request, res cmds.Response, req cmds.Request) {
h := w.Header()
// Expose our agent to allow identification
h.Set("Server", "go-ipfs/"+config.CurrentVersionNumber)
mime, err := guessMimeType(res)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
status := http.StatusOK
// if response contains an error, write an HTTP error status code
if e := res.Error(); e != nil {
if e.Code == cmdkit.ErrClient {
status = http.StatusBadRequest
} else {
status = http.StatusInternalServerError
}
// NOTE: The error will actually be written out by the reader below
}
out, err := res.Reader()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Set up our potential trailer
h.Set("Trailer", StreamErrHeader)
if res.Length() > 0 {
h.Set("X-Content-Length", strconv.FormatUint(res.Length(), 10))
}
if _, ok := res.Output().(io.Reader); ok {
// set streams output type to text to avoid issues with browsers rendering
// html pages on priveleged api ports
mime = "text/plain"
h.Set(streamHeader, "1")
}
// if output is a channel and user requested streaming channels,
// use chunk copier for the output
_, isChan := res.Output().(chan interface{})
if !isChan {
_, isChan = res.Output().(<-chan interface{})
}
if isChan {
h.Set(channelHeader, "1")
}
// catch-all, set to text as default
if mime == "" {
mime = "text/plain"
}
h.Set(contentTypeHeader, mime)
// set 'allowed' headers
h.Set("Access-Control-Allow-Headers", AllowedExposedHeaders)
// expose those headers
h.Set("Access-Control-Expose-Headers", AllowedExposedHeaders)
if r.Method == "HEAD" { // after all the headers.
return
}
w.WriteHeader(status)
err = flushCopy(w, out)
if err != nil {
log.Error("err: ", err)
w.Header().Set(StreamErrHeader, sanitizedErrStr(err))
}
}
func flushCopy(w io.Writer, r io.Reader) error {
buf := make([]byte, 4096)
f, ok := w.(http.Flusher)
if !ok {
_, err := io.Copy(w, r)
return err
}
for {
n, err := r.Read(buf)
switch err {
case io.EOF:
if n <= 0 {
return nil
}
// if data was returned alongside the EOF, pretend we didnt
// get an EOF. The next read call should also EOF.
case nil:
// continue
default:
return err
}
nw, err := w.Write(buf[:n])
if err != nil {
return err
}
if nw != n {
return fmt.Errorf("http write failed to write full amount: %d != %d", nw, n)
}
f.Flush()
}
}
func sanitizedErrStr(err error) string {
s := err.Error()
s = strings.Split(s, "\n")[0]
s = strings.Split(s, "\r")[0]
return s
}
func NewServerConfig() *ServerConfig {
cfg := new(ServerConfig)
cfg.cORSOpts = new(cors.Options)
return cfg
}
func (cfg ServerConfig) AllowedOrigins() []string {
cfg.cORSOptsRWMutex.RLock()
defer cfg.cORSOptsRWMutex.RUnlock()
return cfg.cORSOpts.AllowedOrigins
}
func (cfg *ServerConfig) SetAllowedOrigins(origins ...string) {
cfg.cORSOptsRWMutex.Lock()
defer cfg.cORSOptsRWMutex.Unlock()
o := make([]string, len(origins))
copy(o, origins)
cfg.cORSOpts.AllowedOrigins = o
}
func (cfg *ServerConfig) AppendAllowedOrigins(origins ...string) {
cfg.cORSOptsRWMutex.Lock()
defer cfg.cORSOptsRWMutex.Unlock()
cfg.cORSOpts.AllowedOrigins = append(cfg.cORSOpts.AllowedOrigins, origins...)
}
func (cfg ServerConfig) AllowedMethods() []string {
cfg.cORSOptsRWMutex.RLock()
defer cfg.cORSOptsRWMutex.RUnlock()
return []string(cfg.cORSOpts.AllowedMethods)
}
func (cfg *ServerConfig) SetAllowedMethods(methods ...string) {
cfg.cORSOptsRWMutex.Lock()
defer cfg.cORSOptsRWMutex.Unlock()
if cfg.cORSOpts == nil {
cfg.cORSOpts = new(cors.Options)
}
cfg.cORSOpts.AllowedMethods = methods
}
func (cfg *ServerConfig) SetAllowCredentials(flag bool) {
cfg.cORSOptsRWMutex.Lock()
defer cfg.cORSOptsRWMutex.Unlock()
cfg.cORSOpts.AllowCredentials = flag
}
// allowOrigin just stops the request if the origin is not allowed.
// the CORS middleware apparently does not do this for us...
func allowOrigin(r *http.Request, cfg *ServerConfig) bool {
origin := r.Header.Get("Origin")
// curl, or ipfs shell, typing it in manually, or clicking link
// NOT in a browser. this opens up a hole. we should close it,
// but right now it would break things. TODO
if origin == "" {
return true
}
origins := cfg.AllowedOrigins()
for _, o := range origins {
if o == "*" { // ok! you asked for it!
return true
}
if o == origin { // allowed explicitly
return true
}
}
return false
}
// allowReferer this is here to prevent some CSRF attacks that
// the API would be vulnerable to. We check that the Referer
// is allowed by CORS Origin (origins and referrers here will
// work similarly in the normla uses of the API).
// See discussion at https://github.com/ipfs/go-ipfs/issues/1532
func allowReferer(r *http.Request, cfg *ServerConfig) bool {
referer := r.Referer()
// curl, or ipfs shell, typing it in manually, or clicking link
// NOT in a browser. this opens up a hole. we should close it,
// but right now it would break things. TODO
if referer == "" {
return true
}
u, err := url.Parse(referer)
if err != nil {
// bad referer. but there _is_ something, so bail.
log.Debug("failed to parse referer: ", referer)
// debug because referer comes straight from the client. dont want to
// let people DOS by putting a huge referer that gets stored in log files.
return false
}
origin := u.Scheme + "://" + u.Host
// check CORS ACAOs and pretend Referer works like an origin.
// this is valid for many (most?) sane uses of the API in
// other applications, and will have the desired effect.
origins := cfg.AllowedOrigins()
for _, o := range origins {
if o == "*" { // ok! you asked for it!
return true
}
// referer is allowed explicitly
if o == origin {
return true
}
}
return false
}
// apiVersionMatches checks whether the api client is running the
// same version of go-ipfs. for now, only the exact same version of
// client + server work. In the future, we should use semver for
// proper API versioning! \o/
func apiVersionMatches(r *http.Request) error {
clientVersion := r.UserAgent()
// skips check if client is not go-ipfs
if clientVersion == "" || !strings.Contains(clientVersion, "/go-ipfs/") {
return nil
}
daemonVersion := config.ApiVersion
if daemonVersion != clientVersion {
return fmt.Errorf("%s (%s != %s)", errApiVersionMismatch, daemonVersion, clientVersion)
}
return nil
}
package http
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
cmds "github.com/ipfs/go-ipfs/commands"
ipfscmd "github.com/ipfs/go-ipfs/core/commands"
coremock "github.com/ipfs/go-ipfs/core/mock"
)
func assertHeaders(t *testing.T, resHeaders http.Header, reqHeaders map[string]string) {
for name, value := range reqHeaders {
if resHeaders.Get(name) != value {
t.Errorf("Invalid header '%s', wanted '%s', got '%s'", name, value, resHeaders.Get(name))
}
}
}
func assertStatus(t *testing.T, actual, expected int) {
if actual != expected {
t.Errorf("Expected status: %d got: %d", expected, actual)
}
}
func originCfg(origins []string) *ServerConfig {
cfg := NewServerConfig()
cfg.SetAllowedOrigins(origins...)
cfg.SetAllowedMethods("GET", "PUT", "POST")
return cfg
}
type testCase struct {
Method string
Path string
Code int
Origin string
Referer string
AllowOrigins []string
ReqHeaders map[string]string
ResHeaders map[string]string
}
var defaultOrigins = []string{
"http://localhost",
"http://127.0.0.1",
"https://localhost",
"https://127.0.0.1",
}
func getTestServer(t *testing.T, origins []string) *httptest.Server {
cmdsCtx, err := coremock.MockCmdsCtx()
if err != nil {
t.Error("failure to initialize mock cmds ctx", err)
return nil
}
cmdRoot := &cmds.Command{
Subcommands: map[string]*cmds.Command{
"version": ipfscmd.VersionCmd,
},
}
if len(origins) == 0 {
origins = defaultOrigins
}
handler := NewHandler(cmdsCtx, cmdRoot, originCfg(origins))
return httptest.NewServer(handler)
}
func (tc *testCase) test(t *testing.T) {
// defaults
method := tc.Method
if method == "" {
method = "GET"
}
path := tc.Path
if path == "" {
path = "/api/v0/version"
}
expectCode := tc.Code
if expectCode == 0 {
expectCode = 200
}
// request
req, err := http.NewRequest(method, path, nil)
if err != nil {
t.Error(err)
return
}
for k, v := range tc.ReqHeaders {
req.Header.Add(k, v)
}
if tc.Origin != "" {
req.Header.Add("Origin", tc.Origin)
}
if tc.Referer != "" {
req.Header.Add("Referer", tc.Referer)
}
// server
server := getTestServer(t, tc.AllowOrigins)
if server == nil {
return
}
defer server.Close()
req.URL, err = url.Parse(server.URL + path)
if err != nil {
t.Error(err)
return
}
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Error(err)
return
}
// checks
t.Log("GET", server.URL+path, req.Header, res.Header)
assertHeaders(t, res.Header, tc.ResHeaders)
assertStatus(t, res.StatusCode, expectCode)
}
func TestDisallowedOrigins(t *testing.T) {
gtc := func(origin string, allowedOrigins []string) testCase {
return testCase{
Origin: origin,
AllowOrigins: allowedOrigins,
ResHeaders: map[string]string{
ACAOrigin: "",
ACAMethods: "",
ACACredentials: "",
"Access-Control-Max-Age": "",
"Access-Control-Expose-Headers": "",
},
Code: http.StatusForbidden,
}
}
tcs := []testCase{
gtc("http://barbaz.com", nil),
gtc("http://barbaz.com", []string{"http://localhost"}),
gtc("http://127.0.0.1", []string{"http://localhost"}),
gtc("http://localhost", []string{"http://127.0.0.1"}),
gtc("http://127.0.0.1:1234", nil),
gtc("http://localhost:1234", nil),
}
for _, tc := range tcs {
tc.test(t)
}
}
func TestAllowedOrigins(t *testing.T) {
gtc := func(origin string, allowedOrigins []string) testCase {
return testCase{
Origin: origin,
AllowOrigins: allowedOrigins,
ResHeaders: map[string]string{
ACAOrigin: origin,
ACAMethods: "",
ACACredentials: "",
"Access-Control-Max-Age": "",
"Access-Control-Expose-Headers": AllowedExposedHeaders,
},
Code: http.StatusOK,
}
}
tcs := []testCase{
gtc("http://barbaz.com", []string{"http://barbaz.com", "http://localhost"}),
gtc("http://localhost", []string{"http://barbaz.com", "http://localhost"}),
gtc("http://localhost", nil),
gtc("http://127.0.0.1", nil),
}
for _, tc := range tcs {
tc.test(t)
}
}
func TestWildcardOrigin(t *testing.T) {
gtc := func(origin string, allowedOrigins []string) testCase {
return testCase{
Origin: origin,
AllowOrigins: allowedOrigins,
ResHeaders: map[string]string{
ACAOrigin: origin,
ACAMethods: "",
ACACredentials: "",
"Access-Control-Max-Age": "",
"Access-Control-Expose-Headers": AllowedExposedHeaders,
},
Code: http.StatusOK,
}
}
tcs := []testCase{
gtc("http://barbaz.com", []string{"*"}),
gtc("http://barbaz.com", []string{"http://localhost", "*"}),
gtc("http://127.0.0.1", []string{"http://localhost", "*"}),
gtc("http://localhost", []string{"http://127.0.0.1", "*"}),
gtc("http://127.0.0.1", []string{"*"}),
gtc("http://localhost", []string{"*"}),
gtc("http://127.0.0.1:1234", []string{"*"}),
gtc("http://localhost:1234", []string{"*"}),
}
for _, tc := range tcs {
tc.test(t)
}
}
func TestDisallowedReferer(t *testing.T) {
gtc := func(referer string, allowedOrigins []string) testCase {
return testCase{
Origin: "http://localhost",
Referer: referer,
AllowOrigins: allowedOrigins,
ResHeaders: map[string]string{
ACAOrigin: "http://localhost",
ACAMethods: "",
ACACredentials: "",
"Access-Control-Max-Age": "",
"Access-Control-Expose-Headers": "",
},
Code: http.StatusForbidden,
}
}
tcs := []testCase{
gtc("http://foobar.com", nil),
gtc("http://localhost:1234", nil),
gtc("http://127.0.0.1:1234", nil),
}
for _, tc := range tcs {
tc.test(t)
}
}
func TestAllowedReferer(t *testing.T) {
gtc := func(referer string, allowedOrigins []string) testCase {
return testCase{
Origin: "http://localhost",
AllowOrigins: allowedOrigins,
ResHeaders: map[string]string{
ACAOrigin: "http://localhost",
ACAMethods: "",
ACACredentials: "",
"Access-Control-Max-Age": "",
"Access-Control-Expose-Headers": AllowedExposedHeaders,
},
Code: http.StatusOK,
}
}
tcs := []testCase{
gtc("http://barbaz.com", []string{"http://barbaz.com", "http://localhost"}),
gtc("http://localhost", []string{"http://barbaz.com", "http://localhost"}),
gtc("http://localhost", nil),
gtc("http://127.0.0.1", nil),
}
for _, tc := range tcs {
tc.test(t)
}
}
func TestWildcardReferer(t *testing.T) {
gtc := func(origin string, allowedOrigins []string) testCase {
return testCase{
Origin: origin,
AllowOrigins: allowedOrigins,
ResHeaders: map[string]string{
ACAOrigin: origin,
ACAMethods: "",
ACACredentials: "",
"Access-Control-Max-Age": "",
"Access-Control-Expose-Headers": AllowedExposedHeaders,
},
Code: http.StatusOK,
}
}
tcs := []testCase{
gtc("http://barbaz.com", []string{"*"}),
gtc("http://barbaz.com", []string{"http://localhost", "*"}),
gtc("http://127.0.0.1", []string{"http://localhost", "*"}),
gtc("http://localhost", []string{"http://127.0.0.1", "*"}),
gtc("http://127.0.0.1", []string{"*"}),
gtc("http://localhost", []string{"*"}),
gtc("http://127.0.0.1:1234", []string{"*"}),
gtc("http://localhost:1234", []string{"*"}),
}
for _, tc := range tcs {
tc.test(t)
}
}
func TestAllowedMethod(t *testing.T) {
gtc := func(method string, ok bool) testCase {
code := http.StatusOK
hdrs := map[string]string{
ACAOrigin: "http://localhost",
ACAMethods: method,
ACACredentials: "",
"Access-Control-Max-Age": "",
"Access-Control-Expose-Headers": "",
}
if !ok {
hdrs[ACAOrigin] = ""
hdrs[ACAMethods] = ""
}
return testCase{
Method: "OPTIONS",
Origin: "http://localhost",
AllowOrigins: []string{"*"},
ReqHeaders: map[string]string{
"Access-Control-Request-Method": method,
},
ResHeaders: hdrs,
Code: code,
}
}
tcs := []testCase{
gtc("PUT", true),
gtc("GET", true),
gtc("FOOBAR", false),
}
for _, tc := range tcs {
tc.test(t)
}
}
package http
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/textproto"
"net/url"
"sync"
files "gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit/files"
)
// MultiFileReader reads from a `commands.File` (which can be a directory of files
// or a regular file) as HTTP multipart encoded data.
type MultiFileReader struct {
io.Reader
files []files.File
currentFile io.Reader
buf bytes.Buffer
mpWriter *multipart.Writer
closed bool
mutex *sync.Mutex
// if true, the data will be type 'multipart/form-data'
// if false, the data will be type 'multipart/mixed'
form bool
}
// NewMultiFileReader constructs a MultiFileReader. `file` can be any `commands.File`.
// If `form` is set to true, the multipart data will have a Content-Type of 'multipart/form-data',
// if `form` is false, the Content-Type will be 'multipart/mixed'.
func NewMultiFileReader(file files.File, form bool) *MultiFileReader {
mfr := &MultiFileReader{
files: []files.File{file},
form: form,
mutex: &sync.Mutex{},
}
mfr.mpWriter = multipart.NewWriter(&mfr.buf)
return mfr
}
func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) {
mfr.mutex.Lock()
defer mfr.mutex.Unlock()
// if we are closed and the buffer is flushed, end reading
if mfr.closed && mfr.buf.Len() == 0 {
return 0, io.EOF
}
// if the current file isn't set, advance to the next file
if mfr.currentFile == nil {
var file files.File
for file == nil {
if len(mfr.files) == 0 {
mfr.mpWriter.Close()
mfr.closed = true
return mfr.buf.Read(buf)
}
nextfile, err := mfr.files[len(mfr.files)-1].NextFile()
if err == io.EOF {
mfr.files = mfr.files[:len(mfr.files)-1]
continue
} else if err != nil {
return 0, err
}
file = nextfile
}
// handle starting a new file part
if !mfr.closed {
var contentType string
if _, ok := file.(*files.Symlink); ok {
contentType = "application/symlink"
} else if file.IsDirectory() {
mfr.files = append(mfr.files, file)
contentType = "application/x-directory"
} else {
// otherwise, use the file as a reader to read its contents
contentType = "application/octet-stream"
}
mfr.currentFile = file
// write the boundary and headers
header := make(textproto.MIMEHeader)
filename := url.QueryEscape(file.FileName())
header.Set("Content-Disposition", fmt.Sprintf("file; filename=\"%s\"", filename))
header.Set("Content-Type", contentType)
if rf, ok := file.(*files.ReaderFile); ok {
header.Set("abspath", rf.AbsPath())
}
_, err := mfr.mpWriter.CreatePart(header)
if err != nil {
return 0, err
}
}
}
// if the buffer has something in it, read from it
if mfr.buf.Len() > 0 {
return mfr.buf.Read(buf)
}
// otherwise, read from file data
written, err = mfr.currentFile.Read(buf)
if err == io.EOF {
mfr.currentFile = nil
return written, nil
}
return written, err
}
// Boundary returns the boundary string to be used to separate files in the multipart data
func (mfr *MultiFileReader) Boundary() string {
return mfr.mpWriter.Boundary()
}
package http
import (
"io"
"io/ioutil"
"mime/multipart"
"strings"
"testing"
"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit/files"
)
func TestOutput(t *testing.T) {
text := "Some text! :)"
fileset := []files.File{
files.NewReaderFile("file.txt", "file.txt", ioutil.NopCloser(strings.NewReader(text)), nil),
files.NewSliceFile("boop", "boop", []files.File{
files.NewReaderFile("boop/a.txt", "boop/a.txt", ioutil.NopCloser(strings.NewReader("bleep")), nil),
files.NewReaderFile("boop/b.txt", "boop/b.txt", ioutil.NopCloser(strings.NewReader("bloop")), nil),
}),
files.NewReaderFile("beep.txt", "beep.txt", ioutil.NopCloser(strings.NewReader("beep")), nil),
}
sf := files.NewSliceFile("", "", fileset)
buf := make([]byte, 20)
// testing output by reading it with the go stdlib "mime/multipart" Reader
mfr := NewMultiFileReader(sf, true)
mpReader := multipart.NewReader(mfr, mfr.Boundary())
part, err := mpReader.NextPart()
if part == nil || err != nil {
t.Fatal("Expected non-nil part, nil error")
}
mpf, err := files.NewFileFromPart(part)
if mpf == nil || err != nil {
t.Fatal("Expected non-nil MultipartFile, nil error")
}
if mpf.IsDirectory() {
t.Fatal("Expected file to not be a directory")
}
if mpf.FileName() != "file.txt" {
t.Fatal("Expected filename to be \"file.txt\"")
}
if n, err := mpf.Read(buf); n != len(text) || err != nil {
t.Fatal("Expected to read from file", n, err)
}
if string(buf[:len(text)]) != text {
t.Fatal("Data read was different than expected")
}
part, err = mpReader.NextPart()
if part == nil || err != nil {
t.Fatal("Expected non-nil part, nil error")
}
mpf, err = files.NewFileFromPart(part)
if mpf == nil || err != nil {
t.Fatal("Expected non-nil MultipartFile, nil error")
}
if !mpf.IsDirectory() {
t.Fatal("Expected file to be a directory")
}
if mpf.FileName() != "boop" {
t.Fatal("Expected filename to be \"boop\"")
}
part, err = mpReader.NextPart()
if part == nil || err != nil {
t.Fatal("Expected non-nil part, nil error")
}
child, err := files.NewFileFromPart(part)
if child == nil || err != nil {
t.Fatal("Expected to be able to read a child file")
}
if child.IsDirectory() {
t.Fatal("Expected file to not be a directory")
}
if child.FileName() != "boop/a.txt" {
t.Fatal("Expected filename to be \"some/file/path\"")
}
part, err = mpReader.NextPart()
if part == nil || err != nil {
t.Fatal("Expected non-nil part, nil error")
}
child, err = files.NewFileFromPart(part)
if child == nil || err != nil {
t.Fatal("Expected to be able to read a child file")
}
if child.IsDirectory() {
t.Fatal("Expected file to not be a directory")
}
if child.FileName() != "boop/b.txt" {
t.Fatal("Expected filename to be \"some/file/path\"")
}
child, err = mpf.NextFile()
if child != nil || err != io.EOF {
t.Fatal("Expected to get (nil, io.EOF)")
}
part, err = mpReader.NextPart()
if part == nil || err != nil {
t.Fatal("Expected non-nil part, nil error")
}
mpf, err = files.NewFileFromPart(part)
if mpf == nil || err != nil {
t.Fatal("Expected non-nil MultipartFile, nil error")
}
part, err = mpReader.NextPart()
if part != nil || err != io.EOF {
t.Fatal("Expected to get (nil, io.EOF)")
}
}
package http
import (
"errors"
"fmt"
"mime"
"net/http"
"strings"
cmds "github.com/ipfs/go-ipfs/commands"
path "github.com/ipfs/go-ipfs/path"
cmdkit "gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
files "gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit/files"
)
// Parse parses the data in a http.Request and returns a command Request object
func Parse(r *http.Request, root *cmds.Command) (cmds.Request, error) {
if !strings.HasPrefix(r.URL.Path, ApiPath) {
return nil, errors.New("Unexpected path prefix")
}
pth := path.SplitList(strings.TrimPrefix(r.URL.Path, ApiPath+"/"))
stringArgs := make([]string, 0)
if err := apiVersionMatches(r); err != nil {
if pth[0] != "version" { // compatibility with previous version check
return nil, err
}
}
cmd, err := root.Get(pth[:len(pth)-1])
if err != nil {
// 404 if there is no command at that path
return nil, ErrNotFound
}
if sub := cmd.Subcommand(pth[len(pth)-1]); sub == nil {
if len(pth) <= 1 {
return nil, ErrNotFound
}
// if the last string in the path isn't a subcommand, use it as an argument
// e.g. /objects/Qabc12345 (we are passing "Qabc12345" to the "objects" command)
stringArgs = append(stringArgs, pth[len(pth)-1])
pth = pth[:len(pth)-1]
} else {
cmd = sub
}
opts, stringArgs2 := parseOptions(r)
stringArgs = append(stringArgs, stringArgs2...)
// count required argument definitions
numRequired := 0
for _, argDef := range cmd.Arguments {
if argDef.Required {
numRequired++
}
}
// count the number of provided argument values
valCount := len(stringArgs)
args := make([]string, valCount)
valIndex := 0
requiredFile := ""
for _, argDef := range cmd.Arguments {
// skip optional argument definitions if there aren't sufficient remaining values
if valCount-valIndex <= numRequired && !argDef.Required {
continue
} else if argDef.Required {
numRequired--
}
if argDef.Type == cmdkit.ArgString {
if argDef.Variadic {
for _, s := range stringArgs {
args[valIndex] = s
valIndex++
}
valCount -= len(stringArgs)
} else if len(stringArgs) > 0 {
args[valIndex] = stringArgs[0]
stringArgs = stringArgs[1:]
valIndex++
} else {
break
}
} else if argDef.Type == cmdkit.ArgFile && argDef.Required && len(requiredFile) == 0 {
requiredFile = argDef.Name
}
}
optDefs, err := root.GetOptions(pth)
if err != nil {
return nil, err
}
// create cmds.File from multipart/form-data contents
contentType := r.Header.Get(contentTypeHeader)
mediatype, _, _ := mime.ParseMediaType(contentType)
var f files.File
if mediatype == "multipart/form-data" {
reader, err := r.MultipartReader()
if err != nil {
return nil, err
}
f = &files.MultipartFile{
Mediatype: mediatype,
Reader: reader,
}
}
// if there is a required filearg, error if no files were provided
if len(requiredFile) > 0 && f == nil {
return nil, fmt.Errorf("File argument '%s' is required", requiredFile)
}
req, err := cmds.NewRequest(pth, opts, args, f, cmd, optDefs)
if err != nil {
return nil, err
}
err = cmd.CheckArguments(req)
if err != nil {
return nil, err
}
return req, nil
}
func parseOptions(r *http.Request) (map[string]interface{}, []string) {
opts := make(map[string]interface{})
var args []string
query := r.URL.Query()
for k, v := range query {
if k == "arg" {
args = v
} else {
opts[k] = v[0]
}
}
// default to setting encoding to JSON
_, short := opts[cmdkit.EncShort]
_, long := opts[cmdkit.EncLong]
if !short && !long {
opts[cmdkit.EncShort] = cmds.JSON
}
return opts, args
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment