mirror of
https://github.com/ipfs/kubo.git
synced 2025-07-01 02:30:39 +08:00
Merge pull request #4415 from ipfs/cleanup/dead-cmds-code
Delete some now unused commands lib code
This commit is contained in:
@ -1,89 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,449 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,526 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,313 +0,0 @@
|
|||||||
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"})
|
|
||||||
}
|
|
@ -1,310 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
@ -1,466 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,348 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
@ -1,114 +0,0 @@
|
|||||||
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)")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,160 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
Reference in New Issue
Block a user