Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
What's new
10
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Open sidebar
dms3
go-dms3
Commits
68bb1069
Commit
68bb1069
authored
Nov 21, 2017
by
Jeromy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Delete some now unused commands lib code
License: MIT Signed-off-by:
Jeromy
<
jeromyj@gmail.com
>
parent
b18b1e90
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
0 additions
and
2948 deletions
+0
-2948
commands/cli/cmd_suggestion.go
commands/cli/cmd_suggestion.go
+0
-89
commands/cli/helptext.go
commands/cli/helptext.go
+0
-449
commands/cli/helptext_test.go
commands/cli/helptext_test.go
+0
-47
commands/cli/parse.go
commands/cli/parse.go
+0
-526
commands/cli/parse_test.go
commands/cli/parse_test.go
+0
-313
commands/http/client.go
commands/http/client.go
+0
-310
commands/http/handler.go
commands/http/handler.go
+0
-466
commands/http/handler_test.go
commands/http/handler_test.go
+0
-348
commands/http/multifilereader.go
commands/http/multifilereader.go
+0
-126
commands/http/multifilereader_test.go
commands/http/multifilereader_test.go
+0
-114
commands/http/parse.go
commands/http/parse.go
+0
-160
No files found.
commands/cli/cmd_suggestion.go
deleted
100644 → 0
View file @
b18b1e90
package
cli
import
(
"fmt"
"sort"
"strings"
levenshtein
"github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein"
cmds
"github.com/ipfs/go-ipfs/commands"
)
// Make a custom slice that can be sorted by its levenshtein value
type
suggestionSlice
[]
*
suggestion
type
suggestion
struct
{
cmd
string
levenshtein
int
}
func
(
s
suggestionSlice
)
Len
()
int
{
return
len
(
s
)
}
func
(
s
suggestionSlice
)
Swap
(
i
,
j
int
)
{
s
[
i
],
s
[
j
]
=
s
[
j
],
s
[
i
]
}
func
(
s
suggestionSlice
)
Less
(
i
,
j
int
)
bool
{
return
s
[
i
]
.
levenshtein
<
s
[
j
]
.
levenshtein
}
func
suggestUnknownCmd
(
args
[]
string
,
root
*
cmds
.
Command
)
[]
string
{
if
root
==
nil
{
return
nil
}
arg
:=
args
[
0
]
var
suggestions
[]
string
sortableSuggestions
:=
make
(
suggestionSlice
,
0
)
var
sFinal
[]
string
const
MIN_LEVENSHTEIN
=
3
var
options
=
levenshtein
.
Options
{
InsCost
:
1
,
DelCost
:
3
,
SubCost
:
2
,
Matches
:
func
(
sourceCharacter
rune
,
targetCharacter
rune
)
bool
{
return
sourceCharacter
==
targetCharacter
},
}
// Start with a simple strings.Contains check
for
name
:=
range
root
.
Subcommands
{
if
strings
.
Contains
(
arg
,
name
)
{
suggestions
=
append
(
suggestions
,
name
)
}
}
// If the string compare returns a match, return
if
len
(
suggestions
)
>
0
{
return
suggestions
}
for
name
:=
range
root
.
Subcommands
{
lev
:=
levenshtein
.
DistanceForStrings
([]
rune
(
arg
),
[]
rune
(
name
),
options
)
if
lev
<=
MIN_LEVENSHTEIN
{
sortableSuggestions
=
append
(
sortableSuggestions
,
&
suggestion
{
name
,
lev
})
}
}
sort
.
Sort
(
sortableSuggestions
)
for
_
,
j
:=
range
sortableSuggestions
{
sFinal
=
append
(
sFinal
,
j
.
cmd
)
}
return
sFinal
}
func
printSuggestions
(
inputs
[]
string
,
root
*
cmds
.
Command
)
(
err
error
)
{
suggestions
:=
suggestUnknownCmd
(
inputs
,
root
)
if
len
(
suggestions
)
>
1
{
err
=
fmt
.
Errorf
(
"Unknown Command
\"
%s
\"\n\n
Did 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\n
Did you mean this?
\n\n\t
%s"
,
inputs
[
0
],
suggestions
[
0
])
}
else
{
err
=
fmt
.
Errorf
(
"Unknown Command %q"
,
inputs
[
0
])
}
return
}
commands/cli/helptext.go
deleted
100644 → 0
View file @
b18b1e90
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
)
}
commands/cli/helptext_test.go
deleted
100644 → 0
View file @
b18b1e90
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"
)
}
}
commands/cli/parse.go
deleted
100644 → 0
View file @
b18b1e90
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
)
}
commands/cli/parse_test.go
deleted
100644 → 0
View file @
b18b1e90
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
\n
stdin2"
)
test
([]
string
{
"stdinenabled"
},
fstdin
,
[]
string
{
"stdin1"
,
"stdin2"
})
fstdin
=
fileToSimulateStdin
(
t
,
"stdin1
\n
stdin2
\n
stdin3"
)
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
\n
stdin2"
)
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"
})
}
commands/http/client.go
deleted
100644 → 0
View file @
b18b1e90
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
()
}
commands/http/handler.go
deleted
100644 → 0
View file @
b18b1e90
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
}
commands/http/handler_test.go
deleted
100644 → 0
View file @
b18b1e90
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
)
}
}
commands/http/multifilereader.go
deleted
100644 → 0
View file @
b18b1e90
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
()
}
commands/http/multifilereader_test.go
deleted
100644 → 0
View file @
b18b1e90
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)"
)
}
}
commands/http/parse.go
deleted
100644 → 0
View file @
b18b1e90
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
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment