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-cmds
Commits
90c723d6
Unverified
Commit
90c723d6
authored
Apr 29, 2020
by
Hector Sanjuan
Committed by
GitHub
Apr 29, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Staticcheck fixes (#196)
* Staticcheck fixes This fixes all staticcheck warnings for this library.
parent
c3c51f79
Changes
22
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
100 additions
and
112 deletions
+100
-112
.travis.yml
.travis.yml
+1
-1
chan_test.go
chan_test.go
+6
-5
cli/cmd_suggestion.go
cli/cmd_suggestion.go
+10
-4
cli/helptext.go
cli/helptext.go
+5
-12
cli/parse_test.go
cli/parse_test.go
+4
-3
cli/responseemitter_test.go
cli/responseemitter_test.go
+1
-7
cli/single_test.go
cli/single_test.go
+3
-2
command.go
command.go
+5
-4
command_test.go
command_test.go
+6
-5
executor_test.go
executor_test.go
+2
-2
http/config.go
http/config.go
+3
-7
http/handler.go
http/handler.go
+5
-11
http/parse.go
http/parse.go
+3
-3
http/parse_test.go
http/parse_test.go
+1
-1
http/reforigin_test.go
http/reforigin_test.go
+1
-1
http/responseemitter.go
http/responseemitter.go
+7
-10
reqlog.go
reqlog.go
+6
-2
request.go
request.go
+6
-6
response_test.go
response_test.go
+0
-14
responseemitter_test.go
responseemitter_test.go
+18
-9
single_test.go
single_test.go
+4
-2
writer.go
writer.go
+3
-1
No files found.
.travis.yml
View file @
90c723d6
...
@@ -4,7 +4,7 @@ os:
...
@@ -4,7 +4,7 @@ os:
language
:
go
language
:
go
go
:
go
:
-
1.1
1.x
-
1.1
4.2
env
:
env
:
global
:
global
:
...
...
chan_test.go
View file @
90c723d6
...
@@ -29,12 +29,12 @@ func TestChanResponsePair(t *testing.T) {
...
@@ -29,12 +29,12 @@ func TestChanResponsePair(t *testing.T) {
wg
.
Add
(
1
)
wg
.
Add
(
1
)
go
func
()
{
go
func
()
{
for
_
,
v
:=
range
tc
.
values
{
for
_
,
v
:=
range
tc
.
values
{
v
_
,
err
:=
res
.
Next
()
v
2
,
err
:=
res
.
Next
()
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Error
(
"Next returned unexpected error:"
,
err
)
t
.
Error
(
"Next returned unexpected error:"
,
err
)
}
}
if
v
!=
v
_
{
if
v
!=
v
2
{
t
.
Errorf
(
"Next returned unexpected value %q, expected %q"
,
v
_
,
v
)
t
.
Errorf
(
"Next returned unexpected value %q, expected %q"
,
v
2
,
v
)
}
}
}
}
...
@@ -93,7 +93,7 @@ func TestSingle1(t *testing.T) {
...
@@ -93,7 +93,7 @@ func TestSingle1(t *testing.T) {
err
:=
re
.
Close
()
err
:=
re
.
Close
()
if
err
!=
ErrClosingClosedEmitter
{
if
err
!=
ErrClosingClosedEmitter
{
t
.
Fatal
f
(
"expected double close error, got %v"
,
err
)
t
.
Error
f
(
"expected double close error, got %v"
,
err
)
}
}
close
(
wait
)
close
(
wait
)
}()
}()
...
@@ -127,7 +127,8 @@ func TestSingle2(t *testing.T) {
...
@@ -127,7 +127,8 @@ func TestSingle2(t *testing.T) {
go
func
()
{
go
func
()
{
err
:=
re
.
Emit
(
Single
{
42
})
err
:=
re
.
Emit
(
Single
{
42
})
if
err
!=
ErrClosedEmitter
{
if
err
!=
ErrClosedEmitter
{
t
.
Fatal
(
"expected closed emitter error, got"
,
err
)
t
.
Error
(
"expected closed emitter error, got"
,
err
)
return
}
}
}()
}()
...
...
cli/cmd_suggestion.go
View file @
90c723d6
...
@@ -38,7 +38,7 @@ func suggestUnknownCmd(args []string, root *cmds.Command) []string {
...
@@ -38,7 +38,7 @@ func suggestUnknownCmd(args []string, root *cmds.Command) []string {
var
suggestions
[]
string
var
suggestions
[]
string
sortableSuggestions
:=
make
(
suggestionSlice
,
0
)
sortableSuggestions
:=
make
(
suggestionSlice
,
0
)
var
sFinal
[]
string
var
sFinal
[]
string
const
M
IN_LEVENSHTEIN
=
3
const
M
inLevenshtein
=
3
var
options
levenshtein
.
Options
=
levenshtein
.
Options
{
var
options
levenshtein
.
Options
=
levenshtein
.
Options
{
InsCost
:
1
,
InsCost
:
1
,
...
@@ -50,7 +50,7 @@ func suggestUnknownCmd(args []string, root *cmds.Command) []string {
...
@@ -50,7 +50,7 @@ func suggestUnknownCmd(args []string, root *cmds.Command) []string {
}
}
// Start with a simple strings.Contains check
// Start with a simple strings.Contains check
for
name
,
_
:=
range
root
.
Subcommands
{
for
name
:=
range
root
.
Subcommands
{
if
strings
.
Contains
(
arg
,
name
)
{
if
strings
.
Contains
(
arg
,
name
)
{
suggestions
=
append
(
suggestions
,
name
)
suggestions
=
append
(
suggestions
,
name
)
}
}
...
@@ -61,9 +61,9 @@ func suggestUnknownCmd(args []string, root *cmds.Command) []string {
...
@@ -61,9 +61,9 @@ func suggestUnknownCmd(args []string, root *cmds.Command) []string {
return
suggestions
return
suggestions
}
}
for
name
,
_
:=
range
root
.
Subcommands
{
for
name
:=
range
root
.
Subcommands
{
lev
:=
levenshtein
.
DistanceForStrings
([]
rune
(
arg
),
[]
rune
(
name
),
options
)
lev
:=
levenshtein
.
DistanceForStrings
([]
rune
(
arg
),
[]
rune
(
name
),
options
)
if
lev
<=
M
IN_LEVENSHTEIN
{
if
lev
<=
M
inLevenshtein
{
sortableSuggestions
=
append
(
sortableSuggestions
,
&
suggestion
{
name
,
lev
})
sortableSuggestions
=
append
(
sortableSuggestions
,
&
suggestion
{
name
,
lev
})
}
}
}
}
...
@@ -78,11 +78,17 @@ func suggestUnknownCmd(args []string, root *cmds.Command) []string {
...
@@ -78,11 +78,17 @@ func suggestUnknownCmd(args []string, root *cmds.Command) []string {
func
printSuggestions
(
inputs
[]
string
,
root
*
cmds
.
Command
)
(
err
error
)
{
func
printSuggestions
(
inputs
[]
string
,
root
*
cmds
.
Command
)
(
err
error
)
{
suggestions
:=
suggestUnknownCmd
(
inputs
,
root
)
suggestions
:=
suggestUnknownCmd
(
inputs
,
root
)
if
len
(
suggestions
)
>
1
{
if
len
(
suggestions
)
>
1
{
//lint:ignore ST1005 user facing error
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
"
))
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
{
}
else
if
len
(
suggestions
)
>
0
{
//lint:ignore ST1005 user facing error
err
=
fmt
.
Errorf
(
"Unknown Command
\"
%s
\"\n\n
Did you mean this?
\n\n\t
%s"
,
inputs
[
0
],
suggestions
[
0
])
err
=
fmt
.
Errorf
(
"Unknown Command
\"
%s
\"\n\n
Did you mean this?
\n\n\t
%s"
,
inputs
[
0
],
suggestions
[
0
])
}
else
{
}
else
{
//lint:ignore ST1005 user facing error
err
=
fmt
.
Errorf
(
"Unknown Command
\"
%s
\"\n
"
,
inputs
[
0
])
err
=
fmt
.
Errorf
(
"Unknown Command
\"
%s
\"\n
"
,
inputs
[
0
])
}
}
return
return
...
...
cli/helptext.go
View file @
90c723d6
...
@@ -137,8 +137,11 @@ func init() {
...
@@ -137,8 +137,11 @@ func init() {
shortHelpTemplate
=
template
.
Must
(
template
.
New
(
"shortHelp"
)
.
Parse
(
shortHelpFormat
))
shortHelpTemplate
=
template
.
Must
(
template
.
New
(
"shortHelp"
)
.
Parse
(
shortHelpFormat
))
}
}
// ErrNoHelpRequested returns when request for help help does not include the
// short nor the long option.
var
ErrNoHelpRequested
=
errors
.
New
(
"no help requested"
)
var
ErrNoHelpRequested
=
errors
.
New
(
"no help requested"
)
// HandleHelp writes help to a writer for the given request's command.
func
HandleHelp
(
appName
string
,
req
*
cmds
.
Request
,
out
io
.
Writer
)
error
{
func
HandleHelp
(
appName
string
,
req
*
cmds
.
Request
,
out
io
.
Writer
)
error
{
long
,
_
:=
req
.
Options
[
cmds
.
OptLongHelp
]
.
(
bool
)
long
,
_
:=
req
.
Options
[
cmds
.
OptLongHelp
]
.
(
bool
)
short
,
_
:=
req
.
Options
[
cmds
.
OptShortHelp
]
.
(
bool
)
short
,
_
:=
req
.
Options
[
cmds
.
OptShortHelp
]
.
(
bool
)
...
@@ -369,18 +372,15 @@ func appendWrapped(prefix, text string, width int) string {
...
@@ -369,18 +372,15 @@ func appendWrapped(prefix, text string, width int) string {
func
optionFlag
(
flag
string
)
string
{
func
optionFlag
(
flag
string
)
string
{
if
len
(
flag
)
==
1
{
if
len
(
flag
)
==
1
{
return
fmt
.
Sprintf
(
shortFlag
,
flag
)
return
fmt
.
Sprintf
(
shortFlag
,
flag
)
}
else
{
return
fmt
.
Sprintf
(
longFlag
,
flag
)
}
}
return
fmt
.
Sprintf
(
longFlag
,
flag
)
}
}
func
optionText
(
width
int
,
cmd
...*
cmds
.
Command
)
[]
string
{
func
optionText
(
width
int
,
cmd
...*
cmds
.
Command
)
[]
string
{
// get a slice of the options we want to list out
// get a slice of the options we want to list out
options
:=
make
([]
cmds
.
Option
,
0
)
options
:=
make
([]
cmds
.
Option
,
0
)
for
_
,
c
:=
range
cmd
{
for
_
,
c
:=
range
cmd
{
for
_
,
opt
:=
range
c
.
Options
{
options
=
append
(
options
,
c
.
Options
...
)
options
=
append
(
options
,
opt
)
}
}
}
// add option names to output
// add option names to output
...
@@ -503,13 +503,6 @@ func align(lines []string) []string {
...
@@ -503,13 +503,6 @@ func align(lines []string) []string {
return
lines
return
lines
}
}
func
indent
(
lines
[]
string
,
prefix
string
)
[]
string
{
for
i
,
line
:=
range
lines
{
lines
[
i
]
=
prefix
+
indentString
(
line
,
prefix
)
}
return
lines
}
func
indentString
(
line
string
,
prefix
string
)
string
{
func
indentString
(
line
string
,
prefix
string
)
string
{
return
prefix
+
strings
.
Replace
(
line
,
"
\n
"
,
"
\n
"
+
prefix
,
-
1
)
return
prefix
+
strings
.
Replace
(
line
,
"
\n
"
,
"
\n
"
+
prefix
,
-
1
)
}
}
...
...
cli/parse_test.go
View file @
90c723d6
...
@@ -3,7 +3,6 @@ package cli
...
@@ -3,7 +3,6 @@ package cli
import
(
import
(
"context"
"context"
"fmt"
"fmt"
"github.com/ipfs/go-ipfs-files"
"io"
"io"
"io/ioutil"
"io/ioutil"
"net/url"
"net/url"
...
@@ -12,6 +11,8 @@ import (
...
@@ -12,6 +11,8 @@ import (
"strings"
"strings"
"testing"
"testing"
files
"github.com/ipfs/go-ipfs-files"
cmds
"github.com/ipfs/go-ipfs-cmds"
cmds
"github.com/ipfs/go-ipfs-cmds"
)
)
...
@@ -211,7 +212,7 @@ func TestArgumentParsing(t *testing.T) {
...
@@ -211,7 +212,7 @@ func TestArgumentParsing(t *testing.T) {
test
:=
func
(
cmd
words
,
f
*
os
.
File
,
res
words
)
{
test
:=
func
(
cmd
words
,
f
*
os
.
File
,
res
words
)
{
if
f
!=
nil
{
if
f
!=
nil
{
if
_
,
err
:=
f
.
Seek
(
0
,
os
.
SEEK_SET
);
err
!=
nil
{
if
_
,
err
:=
f
.
Seek
(
0
,
io
.
SeekStart
);
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Fatal
(
err
)
}
}
}
}
...
@@ -486,7 +487,7 @@ func TestBodyArgs(t *testing.T) {
...
@@ -486,7 +487,7 @@ func TestBodyArgs(t *testing.T) {
for
_
,
tc
:=
range
tcs
{
for
_
,
tc
:=
range
tcs
{
if
tc
.
f
!=
nil
{
if
tc
.
f
!=
nil
{
if
_
,
err
:=
tc
.
f
.
Seek
(
0
,
os
.
SEEK_SET
);
err
!=
nil
{
if
_
,
err
:=
tc
.
f
.
Seek
(
0
,
io
.
SeekStart
);
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Fatal
(
err
)
}
}
}
}
...
...
cli/responseemitter_test.go
View file @
90c723d6
...
@@ -5,15 +5,9 @@ import (
...
@@ -5,15 +5,9 @@ import (
"fmt"
"fmt"
"testing"
"testing"
"github.com/ipfs/go-ipfs-cmds"
cmds
"github.com/ipfs/go-ipfs-cmds"
)
)
type
writeCloser
struct
{
*
bytes
.
Buffer
}
func
(
wc
writeCloser
)
Close
()
error
{
return
nil
}
type
tcCloseWithError
struct
{
type
tcCloseWithError
struct
{
stdout
,
stderr
*
bytes
.
Buffer
stdout
,
stderr
*
bytes
.
Buffer
exStdout
,
exStderr
string
exStdout
,
exStderr
string
...
...
cli/single_test.go
View file @
90c723d6
...
@@ -5,7 +5,7 @@ import (
...
@@ -5,7 +5,7 @@ import (
"context"
"context"
"testing"
"testing"
"github.com/ipfs/go-ipfs-cmds"
cmds
"github.com/ipfs/go-ipfs-cmds"
)
)
func
TestSingle
(
t
*
testing
.
T
)
{
func
TestSingle
(
t
*
testing
.
T
)
{
...
@@ -25,7 +25,8 @@ func TestSingle(t *testing.T) {
...
@@ -25,7 +25,8 @@ func TestSingle(t *testing.T) {
go
func
()
{
go
func
()
{
if
err
:=
cmds
.
EmitOnce
(
re
,
"test"
);
err
!=
nil
{
if
err
:=
cmds
.
EmitOnce
(
re
,
"test"
);
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
err
:=
re
.
Emit
(
"this should not be emitted"
)
err
:=
re
.
Emit
(
"this should not be emitted"
)
...
...
command.go
View file @
90c723d6
...
@@ -13,11 +13,12 @@ import (
...
@@ -13,11 +13,12 @@ import (
"fmt"
"fmt"
"strings"
"strings"
"github.com/ipfs/go-ipfs-files"
files
"github.com/ipfs/go-ipfs-files"
logging
"github.com/ipfs/go-log"
logging
"github.com/ipfs/go-log"
)
)
// DefaultOutputEncoding defines the default API output encoding.
const
DefaultOutputEncoding
=
JSON
const
DefaultOutputEncoding
=
JSON
var
log
=
logging
.
Logger
(
"cmds"
)
var
log
=
logging
.
Logger
(
"cmds"
)
...
@@ -93,13 +94,13 @@ type Command struct {
...
@@ -93,13 +94,13 @@ type Command struct {
var
(
var
(
// ErrNotCallable signals a command that cannot be called.
// ErrNotCallable signals a command that cannot be called.
ErrNotCallable
=
ClientError
(
"
T
his command can
'
t be called directly
. T
ry one of its subcommands."
)
ErrNotCallable
=
ClientError
(
"
t
his command can
no
t be called directly
; t
ry one of its subcommands."
)
// ErrNoFormatter signals that the command can not be formatted.
// ErrNoFormatter signals that the command can not be formatted.
ErrNoFormatter
=
ClientError
(
"
T
his command cannot be formatted to plain text"
)
ErrNoFormatter
=
ClientError
(
"
t
his command cannot be formatted to plain text"
)
// ErrIncorrectType signales that the commands returned a value with unexpected type.
// ErrIncorrectType signales that the commands returned a value with unexpected type.
ErrIncorrectType
=
errors
.
New
(
"
T
he command returned a value with a different type than expected"
)
ErrIncorrectType
=
errors
.
New
(
"
t
he command returned a value with a different type than expected"
)
)
)
// Call invokes the command for the given Request
// Call invokes the command for the given Request
...
...
command_test.go
View file @
90c723d6
...
@@ -39,7 +39,7 @@ func TestOptionValidation(t *testing.T) {
...
@@ -39,7 +39,7 @@ func TestOptionValidation(t *testing.T) {
cmd
.
Call
(
req
,
re
,
nil
)
cmd
.
Call
(
req
,
re
,
nil
)
}
else
{
}
else
{
if
err
==
nil
{
if
err
==
nil
{
t
.
Errorf
(
"
S
hould have failed with error %q"
,
tc
.
NewRequestError
)
t
.
Errorf
(
"
s
hould have failed with error %q"
,
tc
.
NewRequestError
)
}
else
if
err
.
Error
()
!=
tc
.
NewRequestError
{
}
else
if
err
.
Error
()
!=
tc
.
NewRequestError
{
t
.
Errorf
(
"expected error %q, got %q"
,
tc
.
NewRequestError
,
err
)
t
.
Errorf
(
"expected error %q, got %q"
,
tc
.
NewRequestError
,
err
)
}
}
...
@@ -50,7 +50,7 @@ func TestOptionValidation(t *testing.T) {
...
@@ -50,7 +50,7 @@ func TestOptionValidation(t *testing.T) {
tcs
:=
[]
testcase
{
tcs
:=
[]
testcase
{
{
{
opts
:
map
[
string
]
interface
{}{
"boop"
:
true
},
opts
:
map
[
string
]
interface
{}{
"boop"
:
true
},
NewRequestError
:
`
O
ption "boop" should be type "string", but got type "bool"`
,
NewRequestError
:
`
o
ption "boop" should be type "string", but got type "bool"`
,
},
},
{
opts
:
map
[
string
]
interface
{}{
"beep"
:
5
}},
{
opts
:
map
[
string
]
interface
{}{
"beep"
:
5
}},
{
opts
:
map
[
string
]
interface
{}{
"beep"
:
5
,
"boop"
:
"test"
}},
{
opts
:
map
[
string
]
interface
{}{
"beep"
:
5
,
"boop"
:
"test"
}},
...
@@ -61,10 +61,10 @@ func TestOptionValidation(t *testing.T) {
...
@@ -61,10 +61,10 @@ func TestOptionValidation(t *testing.T) {
{
opts
:
map
[
string
]
interface
{}{
"S"
:
[
2
]
string
{
"a"
,
"b"
}}},
{
opts
:
map
[
string
]
interface
{}{
"S"
:
[
2
]
string
{
"a"
,
"b"
}}},
{
{
opts
:
map
[
string
]
interface
{}{
"S"
:
true
},
opts
:
map
[
string
]
interface
{}{
"S"
:
true
},
NewRequestError
:
`
O
ption "S" should be type "array", but got type "bool"`
},
NewRequestError
:
`
o
ption "S" should be type "array", but got type "bool"`
},
{
{
opts
:
map
[
string
]
interface
{}{
"beep"
:
":)"
},
opts
:
map
[
string
]
interface
{}{
"beep"
:
":)"
},
NewRequestError
:
`
C
ould not convert value ":)" to type "int" (for option "-beep")`
,
NewRequestError
:
`
c
ould not convert value ":)" to type "int" (for option "-beep")`
,
},
},
}
}
...
@@ -279,7 +279,8 @@ func TestPostRun(t *testing.T) {
...
@@ -279,7 +279,8 @@ func TestPostRun(t *testing.T) {
t
.
Log
(
"next returned"
,
v
,
err
)
t
.
Log
(
"next returned"
,
v
,
err
)
if
err
!=
nil
{
if
err
!=
nil
{
close
(
ch
)
close
(
ch
)
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
ch
<-
v
ch
<-
v
...
...
executor_test.go
View file @
90c723d6
...
@@ -8,7 +8,7 @@ import (
...
@@ -8,7 +8,7 @@ import (
"testing"
"testing"
)
)
var
theError
=
errors
.
New
(
"an error occurred"
)
var
errGeneric
=
errors
.
New
(
"an error occurred"
)
var
root
=
&
Command
{
var
root
=
&
Command
{
Subcommands
:
map
[
string
]
*
Command
{
Subcommands
:
map
[
string
]
*
Command
{
...
@@ -20,7 +20,7 @@ var root = &Command{
...
@@ -20,7 +20,7 @@ var root = &Command{
},
},
"testError"
:
&
Command
{
"testError"
:
&
Command
{
Run
:
func
(
req
*
Request
,
re
ResponseEmitter
,
env
Environment
)
error
{
Run
:
func
(
req
*
Request
,
re
ResponseEmitter
,
env
Environment
)
error
{
err
:=
theError
err
:=
errGeneric
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
...
...
http/config.go
View file @
90c723d6
...
@@ -170,15 +170,11 @@ func allowUserAgent(r *http.Request, cfg *ServerConfig) bool {
...
@@ -170,15 +170,11 @@ func allowUserAgent(r *http.Request, cfg *ServerConfig) bool {
return
true
return
true
}
}
// Allow if the user agent does not start with Mozilla... (i.e. curl)
// Allow if the user agent does not start with Mozilla... (i.e. curl).
ua
:=
r
.
Header
.
Get
(
"User-agent"
)
if
!
strings
.
HasPrefix
(
ua
,
"Mozilla"
)
{
return
true
}
// Disallow otherwise.
// Disallow otherwise.
//
//
// This means the request probably came from a browser and thus, it
// This means the request probably came from a browser and thus, it
// should have included Origin or referer headers.
// should have included Origin or referer headers.
return
false
ua
:=
r
.
Header
.
Get
(
"User-agent"
)
return
!
strings
.
HasPrefix
(
ua
,
"Mozilla"
)
}
}
http/handler.go
View file @
90c723d6
...
@@ -5,7 +5,6 @@ import (
...
@@ -5,7 +5,6 @@ import (
"errors"
"errors"
"net/http"
"net/http"
"runtime/debug"
"runtime/debug"
"strings"
"sync"
"sync"
"time"
"time"
...
@@ -17,11 +16,12 @@ import (
...
@@ -17,11 +16,12 @@ import (
var
log
=
logging
.
Logger
(
"cmds/http"
)
var
log
=
logging
.
Logger
(
"cmds/http"
)
var
(
var
(
ErrNotFound
=
errors
.
New
(
"404 page not found"
)
//
ErrNotFound
is returned when the endpoint does not exist.
e
rr
ApiVersionMismatch
=
errors
.
New
(
"api version mismatch
"
)
E
rr
NotFound
=
errors
.
New
(
"404 page not found
"
)
)
)
const
(
const
(
// StreamErrHeader is used as trailer when stream errors happen.
StreamErrHeader
=
"X-Stream-Error"
StreamErrHeader
=
"X-Stream-Error"
streamHeader
=
"X-Stream-Output"
streamHeader
=
"X-Stream-Output"
channelHeader
=
"X-Chunked-Output"
channelHeader
=
"X-Chunked-Output"
...
@@ -32,7 +32,7 @@ const (
...
@@ -32,7 +32,7 @@ const (
transferEncodingHeader
=
"Transfer-Encoding"
transferEncodingHeader
=
"Transfer-Encoding"
originHeader
=
"origin"
originHeader
=
"origin"
applicationJ
son
=
"application/json"
applicationJ
SON
=
"application/json"
applicationOctetStream
=
"application/octet-stream"
applicationOctetStream
=
"application/octet-stream"
plainText
=
"text/plain"
plainText
=
"text/plain"
)
)
...
@@ -57,6 +57,7 @@ type handler struct {
...
@@ -57,6 +57,7 @@ type handler struct {
env
cmds
.
Environment
env
cmds
.
Environment
}
}
// NewHandler creates the http.Handler for the given commands.
func
NewHandler
(
env
cmds
.
Environment
,
root
*
cmds
.
Command
,
cfg
*
ServerConfig
)
http
.
Handler
{
func
NewHandler
(
env
cmds
.
Environment
,
root
*
cmds
.
Command
,
cfg
*
ServerConfig
)
http
.
Handler
{
if
cfg
==
nil
{
if
cfg
==
nil
{
panic
(
"must provide a valid ServerConfig"
)
panic
(
"must provide a valid ServerConfig"
)
...
@@ -190,13 +191,6 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
...
@@ -190,13 +191,6 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h
.
root
.
Call
(
req
,
re
,
h
.
env
)
h
.
root
.
Call
(
req
,
re
,
h
.
env
)
}
}
func
sanitizedErrStr
(
err
error
)
string
{
s
:=
err
.
Error
()
s
=
strings
.
Split
(
s
,
"
\n
"
)[
0
]
s
=
strings
.
Split
(
s
,
"
\r
"
)[
0
]
return
s
}
func
setAllowedHeaders
(
w
http
.
ResponseWriter
,
allowGet
bool
)
{
func
setAllowedHeaders
(
w
http
.
ResponseWriter
,
allowGet
bool
)
{
w
.
Header
()
.
Add
(
"Allow"
,
http
.
MethodHead
)
w
.
Header
()
.
Add
(
"Allow"
,
http
.
MethodHead
)
w
.
Header
()
.
Add
(
"Allow"
,
http
.
MethodOptions
)
w
.
Header
()
.
Add
(
"Allow"
,
http
.
MethodOptions
)
...
...
http/parse.go
View file @
90c723d6
...
@@ -10,9 +10,9 @@ import (
...
@@ -10,9 +10,9 @@ import (
"strconv"
"strconv"
"strings"
"strings"
"github.com/ipfs/go-ipfs-cmds"
cmds
"github.com/ipfs/go-ipfs-cmds"
"github.com/ipfs/go-ipfs-files"
files
"github.com/ipfs/go-ipfs-files"
logging
"github.com/ipfs/go-log"
logging
"github.com/ipfs/go-log"
)
)
...
@@ -133,7 +133,7 @@ func parseRequest(r *http.Request, root *cmds.Command) (*cmds.Request, error) {
...
@@ -133,7 +133,7 @@ func parseRequest(r *http.Request, root *cmds.Command) (*cmds.Request, error) {
// if there is a required filearg, error if no files were provided
// if there is a required filearg, error if no files were provided
if
len
(
requiredFile
)
>
0
&&
f
==
nil
{
if
len
(
requiredFile
)
>
0
&&
f
==
nil
{
return
nil
,
fmt
.
Errorf
(
"
F
ile argument '%s' is required"
,
requiredFile
)
return
nil
,
fmt
.
Errorf
(
"
f
ile argument '%s' is required"
,
requiredFile
)
}
}
ctx
:=
logging
.
ContextWithLoggable
(
r
.
Context
(),
uuidLoggable
())
ctx
:=
logging
.
ContextWithLoggable
(
r
.
Context
(),
uuidLoggable
())
...
...
http/parse_test.go
View file @
90c723d6
...
@@ -47,7 +47,7 @@ func TestParse(t *testing.T) {
...
@@ -47,7 +47,7 @@ func TestParse(t *testing.T) {
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Fatal
(
err
)
}
}
req
,
err
=
parseRequest
(
r
,
root
)
_
,
err
=
parseRequest
(
r
,
root
)
if
err
!=
ErrNotFound
{
if
err
!=
ErrNotFound
{
t
.
Errorf
(
"expected ErrNotFound, got: %v"
,
err
)
t
.
Errorf
(
"expected ErrNotFound, got: %v"
,
err
)
}
}
...
...
http/reforigin_test.go
View file @
90c723d6
...
@@ -384,7 +384,7 @@ func TestEncoding(t *testing.T) {
...
@@ -384,7 +384,7 @@ func TestEncoding(t *testing.T) {
}
}
tcs
:=
[]
httpTestCase
{
tcs
:=
[]
httpTestCase
{
gtc
(
cmds
.
JSON
,
applicationJ
son
),
gtc
(
cmds
.
JSON
,
applicationJ
SON
),
gtc
(
cmds
.
XML
,
"application/xml"
),
gtc
(
cmds
.
XML
,
"application/xml"
),
}
}
...
...
http/responseemitter.go
View file @
90c723d6
...
@@ -8,14 +8,14 @@ import (
...
@@ -8,14 +8,14 @@ import (
"strings"
"strings"
"sync"
"sync"
"github.com/ipfs/go-ipfs-cmds"
cmds
"github.com/ipfs/go-ipfs-cmds"
)
)
var
(
var
(
HeadRequest
=
fmt
.
Errorf
(
"HEAD request"
)
// AllowedExposedHeadersArr defines the default Access-Control-Expose-Headers.
AllowedExposedHeadersArr
=
[]
string
{
streamHeader
,
channelHeader
,
extraContentLengthHeader
}
AllowedExposedHeadersArr
=
[]
string
{
streamHeader
,
channelHeader
,
extraContentLengthHeader
}
AllowedExposedHeaders
=
strings
.
Join
(
AllowedExposedHeadersArr
,
", "
)
// AllowedExposedHeaders is the list of defaults Access-Control-Expose-Headers separated by comma.
AllowedExposedHeaders
=
strings
.
Join
(
AllowedExposedHeadersArr
,
", "
)
mimeTypes
=
map
[
cmds
.
EncodingType
]
string
{
mimeTypes
=
map
[
cmds
.
EncodingType
]
string
{
cmds
.
Protobuf
:
"application/protobuf"
,
cmds
.
Protobuf
:
"application/protobuf"
,
...
@@ -25,7 +25,7 @@ var (
...
@@ -25,7 +25,7 @@ var (
}
}
)
)
// NewResponeEmitter returns a new ResponseEmitter.
// NewRespon
s
eEmitter returns a new ResponseEmitter.
func
NewResponseEmitter
(
w
http
.
ResponseWriter
,
method
string
,
req
*
cmds
.
Request
,
opts
...
ResponseEmitterOption
)
(
ResponseEmitter
,
error
)
{
func
NewResponseEmitter
(
w
http
.
ResponseWriter
,
method
string
,
req
*
cmds
.
Request
,
opts
...
ResponseEmitterOption
)
(
ResponseEmitter
,
error
)
{
encType
,
enc
,
err
:=
cmds
.
GetEncoder
(
req
,
w
,
cmds
.
JSON
)
encType
,
enc
,
err
:=
cmds
.
GetEncoder
(
req
,
w
,
cmds
.
JSON
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -61,6 +61,8 @@ func withRequestBodyEOFChan(ch <-chan struct{}) ResponseEmitterOption {
...
@@ -61,6 +61,8 @@ func withRequestBodyEOFChan(ch <-chan struct{}) ResponseEmitterOption {
}
}
}
}
// ResponseEmitter interface defines the components that can care of sending
// the response to HTTP Requests.
type
ResponseEmitter
interface
{
type
ResponseEmitter
interface
{
cmds
.
ResponseEmitter
cmds
.
ResponseEmitter
http
.
Flusher
http
.
Flusher
...
@@ -75,7 +77,6 @@ type responseEmitter struct {
...
@@ -75,7 +77,6 @@ type responseEmitter struct {
l
sync
.
Mutex
l
sync
.
Mutex
length
uint64
length
uint64
err
*
cmds
.
Error
bodyEOFChan
<-
chan
struct
{}
bodyEOFChan
<-
chan
struct
{}
...
@@ -315,10 +316,6 @@ func (re *responseEmitter) doPreamble(value interface{}) {
...
@@ -315,10 +316,6 @@ func (re *responseEmitter) doPreamble(value interface{}) {
re
.
w
.
WriteHeader
(
http
.
StatusOK
)
re
.
w
.
WriteHeader
(
http
.
StatusOK
)
}
}
type
responseWriterer
interface
{
Lower
()
http
.
ResponseWriter
}
func
flushCopy
(
w
io
.
Writer
,
r
io
.
Reader
)
error
{
func
flushCopy
(
w
io
.
Writer
,
r
io
.
Reader
)
error
{
buf
:=
make
([]
byte
,
4096
)
buf
:=
make
([]
byte
,
4096
)
f
,
ok
:=
w
.
(
http
.
Flusher
)
f
,
ok
:=
w
.
(
http
.
Flusher
)
...
...
reqlog.go
View file @
90c723d6
...
@@ -6,6 +6,7 @@ import (
...
@@ -6,6 +6,7 @@ import (
"time"
"time"
)
)
// ReqLogEntry represent a log entry for a request.
type
ReqLogEntry
struct
{
type
ReqLogEntry
struct
{
StartTime
time
.
Time
StartTime
time
.
Time
EndTime
time
.
Time
EndTime
time
.
Time
...
@@ -16,11 +17,13 @@ type ReqLogEntry struct {
...
@@ -16,11 +17,13 @@ type ReqLogEntry struct {
ID
int
ID
int
}
}
// Copy copies a log entry and returns a pointer to the copy.
func
(
r
*
ReqLogEntry
)
Copy
()
*
ReqLogEntry
{
func
(
r
*
ReqLogEntry
)
Copy
()
*
ReqLogEntry
{
out
:=
*
r
out
:=
*
r
return
&
out
return
&
out
}
}
// ReqLog represents a request log.
type
ReqLog
struct
{
type
ReqLog
struct
{
Requests
[]
*
ReqLogEntry
Requests
[]
*
ReqLogEntry
nextID
int
nextID
int
...
@@ -28,6 +31,7 @@ type ReqLog struct {
...
@@ -28,6 +31,7 @@ type ReqLog struct {
keep
time
.
Duration
keep
time
.
Duration
}
}
// Add ads an entry to the log for the given request.
func
(
rl
*
ReqLog
)
Add
(
req
*
Request
)
*
ReqLogEntry
{
func
(
rl
*
ReqLog
)
Add
(
req
*
Request
)
*
ReqLogEntry
{
rle
:=
&
ReqLogEntry
{
rle
:=
&
ReqLogEntry
{
StartTime
:
time
.
Now
(),
StartTime
:
time
.
Now
(),
...
@@ -42,6 +46,7 @@ func (rl *ReqLog) Add(req *Request) *ReqLogEntry {
...
@@ -42,6 +46,7 @@ func (rl *ReqLog) Add(req *Request) *ReqLogEntry {
return
rle
return
rle
}
}
// AddEntry adds an entry to the log.
func
(
rl
*
ReqLog
)
AddEntry
(
rle
*
ReqLogEntry
)
{
func
(
rl
*
ReqLog
)
AddEntry
(
rle
*
ReqLogEntry
)
{
rl
.
lock
.
Lock
()
rl
.
lock
.
Lock
()
defer
rl
.
lock
.
Unlock
()
defer
rl
.
lock
.
Unlock
()
...
@@ -52,10 +57,9 @@ func (rl *ReqLog) AddEntry(rle *ReqLogEntry) {
...
@@ -52,10 +57,9 @@ func (rl *ReqLog) AddEntry(rle *ReqLogEntry) {
if
rle
==
nil
||
!
rle
.
Active
{
if
rle
==
nil
||
!
rle
.
Active
{
rl
.
maybeCleanup
()
rl
.
maybeCleanup
()
}
}
return
}
}
// ClearInactive clears any inactive requests from the log.
func
(
rl
*
ReqLog
)
ClearInactive
()
{
func
(
rl
*
ReqLog
)
ClearInactive
()
{
rl
.
lock
.
Lock
()
rl
.
lock
.
Lock
()
defer
rl
.
lock
.
Unlock
()
defer
rl
.
lock
.
Unlock
()
...
...
request.go
View file @
90c723d6
...
@@ -66,6 +66,7 @@ func (req *Request) BodyArgs() StdinArguments {
...
@@ -66,6 +66,7 @@ func (req *Request) BodyArgs() StdinArguments {
return
nil
return
nil
}
}
// ParseBodyArgs parses arguments in the request body.
func
(
req
*
Request
)
ParseBodyArgs
()
error
{
func
(
req
*
Request
)
ParseBodyArgs
()
error
{
s
:=
req
.
BodyArgs
()
s
:=
req
.
BodyArgs
()
if
s
==
nil
{
if
s
==
nil
{
...
@@ -78,6 +79,7 @@ func (req *Request) ParseBodyArgs() error {
...
@@ -78,6 +79,7 @@ func (req *Request) ParseBodyArgs() error {
return
s
.
Err
()
return
s
.
Err
()
}
}
// SetOption sets a request option.
func
(
req
*
Request
)
SetOption
(
name
string
,
value
interface
{})
{
func
(
req
*
Request
)
SetOption
(
name
string
,
value
interface
{})
{
optDefs
,
err
:=
req
.
Root
.
GetOptions
(
req
.
Path
)
optDefs
,
err
:=
req
.
Root
.
GetOptions
(
req
.
Path
)
optDef
,
found
:=
optDefs
[
name
]
optDef
,
found
:=
optDefs
[
name
]
...
@@ -95,8 +97,6 @@ func (req *Request) SetOption(name string, value interface{}) {
...
@@ -95,8 +97,6 @@ func (req *Request) SetOption(name string, value interface{}) {
name
=
optDef
.
Name
()
name
=
optDef
.
Name
()
req
.
Options
[
name
]
=
value
req
.
Options
[
name
]
=
value
return
}
}
func
checkAndConvertOptions
(
root
*
Command
,
opts
OptMap
,
path
[]
string
)
(
OptMap
,
error
)
{
func
checkAndConvertOptions
(
root
*
Command
,
opts
OptMap
,
path
[]
string
)
(
OptMap
,
error
)
{
...
@@ -125,20 +125,20 @@ func checkAndConvertOptions(root *Command, opts OptMap, path []string) (OptMap,
...
@@ -125,20 +125,20 @@ func checkAndConvertOptions(root *Command, opts OptMap, path []string) (OptMap,
if
len
(
str
)
==
0
{
if
len
(
str
)
==
0
{
value
=
"empty value"
value
=
"empty value"
}
}
return
options
,
fmt
.
Errorf
(
"
C
ould not convert %s to type %q (for option %q)"
,
return
options
,
fmt
.
Errorf
(
"
c
ould not convert %s to type %q (for option %q)"
,
value
,
opt
.
Type
()
.
String
(),
"-"
+
k
)
value
,
opt
.
Type
()
.
String
(),
"-"
+
k
)
}
}
options
[
k
]
=
val
options
[
k
]
=
val
}
else
{
}
else
{
return
options
,
fmt
.
Errorf
(
"
O
ption %q should be type %q, but got type %q"
,
return
options
,
fmt
.
Errorf
(
"
o
ption %q should be type %q, but got type %q"
,
k
,
opt
.
Type
()
.
String
(),
kind
.
String
())
k
,
opt
.
Type
()
.
String
(),
kind
.
String
())
}
}
}
}
for
_
,
name
:=
range
opt
.
Names
()
{
for
_
,
name
:=
range
opt
.
Names
()
{
if
_
,
ok
:=
options
[
name
];
name
!=
k
&&
ok
{
if
_
,
ok
:=
options
[
name
];
name
!=
k
&&
ok
{
return
options
,
fmt
.
Errorf
(
"
D
uplicate command options were provided (%q and %q)"
,
return
options
,
fmt
.
Errorf
(
"
d
uplicate command options were provided (%q and %q)"
,
k
,
name
)
k
,
name
)
}
}
}
}
...
@@ -162,7 +162,7 @@ func GetEncoding(req *Request, def EncodingType) EncodingType {
...
@@ -162,7 +162,7 @@ func GetEncoding(req *Request, def EncodingType) EncodingType {
}
}
}
}
//
f
illDefault fills in default values if option has not been set
//
F
illDefault
s
fills in default values if option has not been set
.
func
(
req
*
Request
)
FillDefaults
()
error
{
func
(
req
*
Request
)
FillDefaults
()
error
{
optDefMap
,
err
:=
req
.
Root
.
GetOptions
(
req
.
Path
)
optDefMap
,
err
:=
req
.
Root
.
GetOptions
(
req
.
Path
)
if
err
!=
nil
{
if
err
!=
nil
{
...
...
response_test.go
View file @
90c723d6
...
@@ -12,20 +12,6 @@ type TestOutput struct {
...
@@ -12,20 +12,6 @@ type TestOutput struct {
Baz
int
Baz
int
}
}
func
eqStringSlice
(
a
,
b
[]
string
)
bool
{
if
len
(
a
)
!=
len
(
b
)
{
return
false
}
for
i
:=
range
a
{
if
a
[
i
]
!=
b
[
i
]
{
return
false
}
}
return
true
}
func
TestMarshalling
(
t
*
testing
.
T
)
{
func
TestMarshalling
(
t
*
testing
.
T
)
{
cmd
:=
&
Command
{}
cmd
:=
&
Command
{}
...
...
responseemitter_test.go
View file @
90c723d6
...
@@ -19,18 +19,21 @@ func TestCopy(t *testing.T) {
...
@@ -19,18 +19,21 @@ func TestCopy(t *testing.T) {
go
func
()
{
go
func
()
{
err
:=
Copy
(
re2
,
res1
)
err
:=
Copy
(
re2
,
res1
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
}()
}()
go
func
()
{
go
func
()
{
err
:=
re1
.
Emit
(
"test"
)
err
:=
re1
.
Emit
(
"test"
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
err
=
re1
.
Close
()
err
=
re1
.
Close
()
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
}()
}()
...
@@ -64,18 +67,21 @@ func TestCopyError(t *testing.T) {
...
@@ -64,18 +67,21 @@ func TestCopyError(t *testing.T) {
go
func
()
{
go
func
()
{
err
:=
Copy
(
re2
,
res1
)
err
:=
Copy
(
re2
,
res1
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
}()
}()
go
func
()
{
go
func
()
{
err
:=
re1
.
Emit
(
"test"
)
err
:=
re1
.
Emit
(
"test"
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
err
=
re1
.
CloseWithError
(
fooErr
)
err
=
re1
.
CloseWithError
(
fooErr
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
}()
}()
...
@@ -106,17 +112,20 @@ func TestError(t *testing.T) {
...
@@ -106,17 +112,20 @@ func TestError(t *testing.T) {
go
func
()
{
go
func
()
{
err
:=
re
.
Emit
(
"value1"
)
err
:=
re
.
Emit
(
"value1"
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
err
=
re
.
Emit
(
"value2"
)
err
=
re
.
Emit
(
"value2"
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
err
=
re
.
CloseWithError
(
&
Error
{
Message
:
"foo"
})
err
=
re
.
CloseWithError
(
&
Error
{
Message
:
"foo"
})
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
}()
}()
...
...
single_test.go
View file @
90c723d6
...
@@ -22,7 +22,8 @@ func TestSingleChan(t *testing.T) {
...
@@ -22,7 +22,8 @@ func TestSingleChan(t *testing.T) {
defer
wg
.
Done
()
defer
wg
.
Done
()
if
err
:=
EmitOnce
(
re
,
"test"
);
err
!=
nil
{
if
err
:=
EmitOnce
(
re
,
"test"
);
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
err
:=
re
.
Emit
(
"test"
)
err
:=
re
.
Emit
(
"test"
)
...
@@ -73,7 +74,8 @@ func TestSingleWriter(t *testing.T) {
...
@@ -73,7 +74,8 @@ func TestSingleWriter(t *testing.T) {
wg
.
Add
(
1
)
wg
.
Add
(
1
)
go
func
()
{
go
func
()
{
if
err
:=
EmitOnce
(
re
,
"test"
);
err
!=
nil
{
if
err
:=
EmitOnce
(
re
,
"test"
);
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Error
(
err
)
return
}
}
err
:=
re
.
Emit
(
"this should not be sent"
)
err
:=
re
.
Emit
(
"this should not be sent"
)
...
...
writer.go
View file @
90c723d6
...
@@ -8,6 +8,8 @@ import (
...
@@ -8,6 +8,8 @@ import (
"sync"
"sync"
)
)
// NewWriterResponseEmitter creates a response emitter that sends responses to
// the given WriterCloser.
func
NewWriterResponseEmitter
(
w
io
.
WriteCloser
,
req
*
Request
)
(
ResponseEmitter
,
error
)
{
func
NewWriterResponseEmitter
(
w
io
.
WriteCloser
,
req
*
Request
)
(
ResponseEmitter
,
error
)
{
_
,
valEnc
,
err
:=
GetEncoder
(
req
,
w
,
Undefined
)
_
,
valEnc
,
err
:=
GetEncoder
(
req
,
w
,
Undefined
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -24,6 +26,7 @@ func NewWriterResponseEmitter(w io.WriteCloser, req *Request) (ResponseEmitter,
...
@@ -24,6 +26,7 @@ func NewWriterResponseEmitter(w io.WriteCloser, req *Request) (ResponseEmitter,
return
re
,
nil
return
re
,
nil
}
}
// NewReaderResponse creates a Response from the given reader.
func
NewReaderResponse
(
r
io
.
Reader
,
req
*
Request
)
(
Response
,
error
)
{
func
NewReaderResponse
(
r
io
.
Reader
,
req
*
Request
)
(
Response
,
error
)
{
encType
:=
GetEncoding
(
req
,
Undefined
)
encType
:=
GetEncoding
(
req
,
Undefined
)
dec
,
ok
:=
Decoders
[
encType
]
dec
,
ok
:=
Decoders
[
encType
]
...
@@ -99,7 +102,6 @@ type writerResponseEmitter struct {
...
@@ -99,7 +102,6 @@ type writerResponseEmitter struct {
req
*
Request
req
*
Request
length
*
uint64
length
*
uint64
err
*
Error
emitted
bool
emitted
bool
closed
bool
closed
bool
...
...
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