|
|
|
package conf
|
|
|
|
|
|
|
|
import (
|
|
|
|
"os"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/zeromicro/go-zero/core/fs"
|
|
|
|
"github.com/zeromicro/go-zero/core/hash"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestLoadConfig_notExists(t *testing.T) {
|
|
|
|
assert.NotNil(t, Load("not_a_file", nil))
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLoadConfig_notRecogFile(t *testing.T) {
|
|
|
|
filename, err := fs.TempFilenameWithText("hello")
|
|
|
|
assert.Nil(t, err)
|
|
|
|
defer os.Remove(filename)
|
|
|
|
assert.NotNil(t, Load(filename, nil))
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestConfigJson(t *testing.T) {
|
|
|
|
tests := []string{
|
|
|
|
".json",
|
|
|
|
".yaml",
|
|
|
|
".yml",
|
|
|
|
}
|
|
|
|
text := `{
|
|
|
|
"a": "foo",
|
|
|
|
"b": 1,
|
|
|
|
"c": "${FOO}",
|
|
|
|
"d": "abcd!@#$112"
|
|
|
|
}`
|
|
|
|
for _, test := range tests {
|
|
|
|
test := test
|
|
|
|
t.Run(test, func(t *testing.T) {
|
|
|
|
os.Setenv("FOO", "2")
|
|
|
|
defer os.Unsetenv("FOO")
|
|
|
|
tmpfile, err := createTempFile(test, text)
|
|
|
|
assert.Nil(t, err)
|
|
|
|
defer os.Remove(tmpfile)
|
|
|
|
|
|
|
|
var val struct {
|
|
|
|
A string `json:"a"`
|
|
|
|
B int `json:"b"`
|
|
|
|
C string `json:"c"`
|
|
|
|
D string `json:"d"`
|
|
|
|
}
|
|
|
|
MustLoad(tmpfile, &val)
|
|
|
|
assert.Equal(t, "foo", val.A)
|
|
|
|
assert.Equal(t, 1, val.B)
|
|
|
|
assert.Equal(t, "${FOO}", val.C)
|
|
|
|
assert.Equal(t, "abcd!@#$112", val.D)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLoadFromJsonBytesArray(t *testing.T) {
|
|
|
|
input := []byte(`{"users": [{"name": "foo"}, {"Name": "bar"}]}`)
|
|
|
|
var val struct {
|
|
|
|
Users []struct {
|
|
|
|
Name string
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.NoError(t, LoadFromJsonBytes(input, &val))
|
|
|
|
var expect []string
|
|
|
|
for _, user := range val.Users {
|
|
|
|
expect = append(expect, user.Name)
|
|
|
|
}
|
|
|
|
assert.EqualValues(t, []string{"foo", "bar"}, expect)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestConfigToml(t *testing.T) {
|
|
|
|
text := `a = "foo"
|
|
|
|
b = 1
|
|
|
|
c = "${FOO}"
|
|
|
|
d = "abcd!@#$112"
|
|
|
|
`
|
|
|
|
os.Setenv("FOO", "2")
|
|
|
|
defer os.Unsetenv("FOO")
|
|
|
|
tmpfile, err := createTempFile(".toml", text)
|
|
|
|
assert.Nil(t, err)
|
|
|
|
defer os.Remove(tmpfile)
|
|
|
|
|
|
|
|
var val struct {
|
|
|
|
A string `json:"a"`
|
|
|
|
B int `json:"b"`
|
|
|
|
C string `json:"c"`
|
|
|
|
D string `json:"d"`
|
|
|
|
}
|
|
|
|
MustLoad(tmpfile, &val)
|
|
|
|
assert.Equal(t, "foo", val.A)
|
|
|
|
assert.Equal(t, 1, val.B)
|
|
|
|
assert.Equal(t, "${FOO}", val.C)
|
|
|
|
assert.Equal(t, "abcd!@#$112", val.D)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestConfigOptional(t *testing.T) {
|
|
|
|
text := `a = "foo"
|
|
|
|
b = 1
|
|
|
|
c = "FOO"
|
|
|
|
d = "abcd"
|
|
|
|
`
|
|
|
|
tmpfile, err := createTempFile(".toml", text)
|
|
|
|
assert.Nil(t, err)
|
|
|
|
defer os.Remove(tmpfile)
|
|
|
|
|
|
|
|
var val struct {
|
|
|
|
A string `json:"a"`
|
|
|
|
B int `json:"b,optional"`
|
|
|
|
C string `json:"c,optional=B"`
|
|
|
|
D string `json:"d,optional=b"`
|
|
|
|
}
|
|
|
|
if assert.NoError(t, Load(tmpfile, &val)) {
|
|
|
|
assert.Equal(t, "foo", val.A)
|
|
|
|
assert.Equal(t, 1, val.B)
|
|
|
|
assert.Equal(t, "FOO", val.C)
|
|
|
|
assert.Equal(t, "abcd", val.D)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestConfigJsonCanonical(t *testing.T) {
|
|
|
|
text := []byte(`{"a": "foo", "B": "bar"}`)
|
|
|
|
|
|
|
|
var val1 struct {
|
|
|
|
A string `json:"a"`
|
|
|
|
B string `json:"b"`
|
|
|
|
}
|
|
|
|
var val2 struct {
|
|
|
|
A string
|
|
|
|
B string
|
|
|
|
}
|
|
|
|
assert.NoError(t, LoadFromJsonBytes(text, &val1))
|
|
|
|
assert.Equal(t, "foo", val1.A)
|
|
|
|
assert.Equal(t, "bar", val1.B)
|
|
|
|
assert.NoError(t, LoadFromJsonBytes(text, &val2))
|
|
|
|
assert.Equal(t, "foo", val2.A)
|
|
|
|
assert.Equal(t, "bar", val2.B)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestConfigTomlCanonical(t *testing.T) {
|
|
|
|
text := []byte(`a = "foo"
|
|
|
|
B = "bar"`)
|
|
|
|
|
|
|
|
var val1 struct {
|
|
|
|
A string `json:"a"`
|
|
|
|
B string `json:"b"`
|
|
|
|
}
|
|
|
|
var val2 struct {
|
|
|
|
A string
|
|
|
|
B string
|
|
|
|
}
|
|
|
|
assert.NoError(t, LoadFromTomlBytes(text, &val1))
|
|
|
|
assert.Equal(t, "foo", val1.A)
|
|
|
|
assert.Equal(t, "bar", val1.B)
|
|
|
|
assert.NoError(t, LoadFromTomlBytes(text, &val2))
|
|
|
|
assert.Equal(t, "foo", val2.A)
|
|
|
|
assert.Equal(t, "bar", val2.B)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestConfigYamlCanonical(t *testing.T) {
|
|
|
|
text := []byte(`a: foo
|
|
|
|
B: bar`)
|
|
|
|
|
|
|
|
var val1 struct {
|
|
|
|
A string `json:"a"`
|
|
|
|
B string `json:"b"`
|
|
|
|
}
|
|
|
|
var val2 struct {
|
|
|
|
A string
|
|
|
|
B string
|
|
|
|
}
|
|
|
|
assert.NoError(t, LoadFromYamlBytes(text, &val1))
|
|
|
|
assert.Equal(t, "foo", val1.A)
|
|
|
|
assert.Equal(t, "bar", val1.B)
|
|
|
|
assert.NoError(t, LoadFromYamlBytes(text, &val2))
|
|
|
|
assert.Equal(t, "foo", val2.A)
|
|
|
|
assert.Equal(t, "bar", val2.B)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestConfigTomlEnv(t *testing.T) {
|
|
|
|
text := `a = "foo"
|
|
|
|
b = 1
|
|
|
|
c = "${FOO}"
|
|
|
|
d = "abcd!@#112"
|
|
|
|
`
|
|
|
|
os.Setenv("FOO", "2")
|
|
|
|
defer os.Unsetenv("FOO")
|
|
|
|
tmpfile, err := createTempFile(".toml", text)
|
|
|
|
assert.Nil(t, err)
|
|
|
|
defer os.Remove(tmpfile)
|
|
|
|
|
|
|
|
var val struct {
|
|
|
|
A string `json:"a"`
|
|
|
|
B int `json:"b"`
|
|
|
|
C string `json:"c"`
|
|
|
|
D string `json:"d"`
|
|
|
|
}
|
|
|
|
|
|
|
|
MustLoad(tmpfile, &val, UseEnv())
|
|
|
|
assert.Equal(t, "foo", val.A)
|
|
|
|
assert.Equal(t, 1, val.B)
|
|
|
|
assert.Equal(t, "2", val.C)
|
|
|
|
assert.Equal(t, "abcd!@#112", val.D)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestConfigJsonEnv(t *testing.T) {
|
|
|
|
tests := []string{
|
|
|
|
".json",
|
|
|
|
".yaml",
|
|
|
|
".yml",
|
|
|
|
}
|
|
|
|
text := `{
|
|
|
|
"a": "foo",
|
|
|
|
"b": 1,
|
|
|
|
"c": "${FOO}",
|
|
|
|
"d": "abcd!@#$a12 3"
|
|
|
|
}`
|
|
|
|
for _, test := range tests {
|
|
|
|
test := test
|
|
|
|
t.Run(test, func(t *testing.T) {
|
|
|
|
os.Setenv("FOO", "2")
|
|
|
|
defer os.Unsetenv("FOO")
|
|
|
|
tmpfile, err := createTempFile(test, text)
|
|
|
|
assert.Nil(t, err)
|
|
|
|
defer os.Remove(tmpfile)
|
|
|
|
|
|
|
|
var val struct {
|
|
|
|
A string `json:"a"`
|
|
|
|
B int `json:"b"`
|
|
|
|
C string `json:"c"`
|
|
|
|
D string `json:"d"`
|
|
|
|
}
|
|
|
|
MustLoad(tmpfile, &val, UseEnv())
|
|
|
|
assert.Equal(t, "foo", val.A)
|
|
|
|
assert.Equal(t, 1, val.B)
|
|
|
|
assert.Equal(t, "2", val.C)
|
|
|
|
assert.Equal(t, "abcd!@# 3", val.D)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestToCamelCase(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
input string
|
|
|
|
expect string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
input: "",
|
|
|
|
expect: "",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "A",
|
|
|
|
expect: "a",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "a",
|
|
|
|
expect: "a",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "hello_world",
|
|
|
|
expect: "hello_world",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "Hello_world",
|
|
|
|
expect: "hello_world",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "hello_World",
|
|
|
|
expect: "hello_world",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "helloWorld",
|
|
|
|
expect: "helloworld",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "HelloWorld",
|
|
|
|
expect: "helloworld",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "hello World",
|
|
|
|
expect: "hello world",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "Hello World",
|
|
|
|
expect: "hello world",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "Hello World",
|
|
|
|
expect: "hello world",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "Hello World foo_bar",
|
|
|
|
expect: "hello world foo_bar",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "Hello World foo_Bar",
|
|
|
|
expect: "hello world foo_bar",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "Hello World Foo_bar",
|
|
|
|
expect: "hello world foo_bar",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "Hello World Foo_Bar",
|
|
|
|
expect: "hello world foo_bar",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "Hello.World Foo_Bar",
|
|
|
|
expect: "hello.world foo_bar",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "你好 World Foo_Bar",
|
|
|
|
expect: "你好 world foo_bar",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, test := range tests {
|
|
|
|
test := test
|
|
|
|
t.Run(test.input, func(t *testing.T) {
|
|
|
|
assert.Equal(t, test.expect, toLowerCase(test.input))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLoadFromJsonBytesError(t *testing.T) {
|
|
|
|
var val struct{}
|
|
|
|
assert.Error(t, LoadFromJsonBytes([]byte(`hello`), &val))
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLoadFromTomlBytesError(t *testing.T) {
|
|
|
|
var val struct{}
|
|
|
|
assert.Error(t, LoadFromTomlBytes([]byte(`hello`), &val))
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLoadFromYamlBytesError(t *testing.T) {
|
|
|
|
var val struct{}
|
|
|
|
assert.Error(t, LoadFromYamlBytes([]byte(`':hello`), &val))
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLoadFromYamlBytes(t *testing.T) {
|
|
|
|
input := []byte(`layer1:
|
|
|
|
layer2:
|
|
|
|
layer3: foo`)
|
|
|
|
var val struct {
|
|
|
|
Layer1 struct {
|
|
|
|
Layer2 struct {
|
|
|
|
Layer3 string
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.NoError(t, LoadFromYamlBytes(input, &val))
|
|
|
|
assert.Equal(t, "foo", val.Layer1.Layer2.Layer3)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLoadFromYamlBytesTerm(t *testing.T) {
|
|
|
|
input := []byte(`layer1:
|
|
|
|
layer2:
|
|
|
|
tls_conf: foo`)
|
|
|
|
var val struct {
|
|
|
|
Layer1 struct {
|
|
|
|
Layer2 struct {
|
|
|
|
Layer3 string `json:"tls_conf"`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.NoError(t, LoadFromYamlBytes(input, &val))
|
|
|
|
assert.Equal(t, "foo", val.Layer1.Layer2.Layer3)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLoadFromYamlBytesLayers(t *testing.T) {
|
|
|
|
input := []byte(`layer1:
|
|
|
|
layer2:
|
|
|
|
layer3: foo`)
|
|
|
|
var val struct {
|
|
|
|
Value string `json:"Layer1.Layer2.Layer3"`
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.NoError(t, LoadFromYamlBytes(input, &val))
|
|
|
|
assert.Equal(t, "foo", val.Value)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLoadFromYamlItemOverlay(t *testing.T) {
|
|
|
|
type (
|
|
|
|
Redis struct {
|
|
|
|
Host string
|
|
|
|
Port int
|
|
|
|
}
|
|
|
|
|
|
|
|
RedisKey struct {
|
|
|
|
Redis
|
|
|
|
Key string
|
|
|
|
}
|
|
|
|
|
|
|
|
Server struct {
|
|
|
|
Redis RedisKey
|
|
|
|
}
|
|
|
|
|
|
|
|
TestConfig struct {
|
|
|
|
Server
|
|
|
|
Redis Redis
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
input := []byte(`Redis:
|
|
|
|
Host: localhost
|
|
|
|
Port: 6379
|
|
|
|
Key: test
|
|
|
|
`)
|
|
|
|
|
|
|
|
var c TestConfig
|
|
|
|
if assert.NoError(t, LoadFromYamlBytes(input, &c)) {
|
|
|
|
assert.Equal(t, "localhost", c.Redis.Host)
|
|
|
|
assert.Equal(t, 6379, c.Redis.Port)
|
|
|
|
assert.Equal(t, "test", c.Server.Redis.Key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLoadFromYamlItemOverlayReverse(t *testing.T) {
|
|
|
|
type (
|
|
|
|
Redis struct {
|
|
|
|
Host string
|
|
|
|
Port int
|
|
|
|
}
|
|
|
|
|
|
|
|
RedisKey struct {
|
|
|
|
Redis
|
|
|
|
Key string
|
|
|
|
}
|
|
|
|
|
|
|
|
Server struct {
|
|
|
|
Redis Redis
|
|
|
|
}
|
|
|
|
|
|
|
|
TestConfig struct {
|
|
|
|
Redis RedisKey
|
|
|
|
Server
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
input := []byte(`Redis:
|
|
|
|
Host: localhost
|
|
|
|
Port: 6379
|
|
|
|
Key: test
|
|
|
|
`)
|
|
|
|
|
|
|
|
var c TestConfig
|
|
|
|
if assert.NoError(t, LoadFromYamlBytes(input, &c)) {
|
|
|
|
assert.Equal(t, "localhost", c.Redis.Host)
|
|
|
|
assert.Equal(t, 6379, c.Redis.Port)
|
|
|
|
assert.Equal(t, "test", c.Redis.Key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLoadFromYamlItemOverlayWithMap(t *testing.T) {
|
|
|
|
type (
|
|
|
|
Redis struct {
|
|
|
|
Host string
|
|
|
|
Port int
|
|
|
|
}
|
|
|
|
|
|
|
|
RedisKey struct {
|
|
|
|
Redis
|
|
|
|
Key string
|
|
|
|
}
|
|
|
|
|
|
|
|
Server struct {
|
|
|
|
Redis RedisKey
|
|
|
|
}
|
|
|
|
|
|
|
|
TestConfig struct {
|
|
|
|
Server
|
|
|
|
Redis map[string]interface{}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
input := []byte(`Redis:
|
|
|
|
Host: localhost
|
|
|
|
Port: 6379
|
|
|
|
Key: test
|
|
|
|
`)
|
|
|
|
|
|
|
|
var c TestConfig
|
|
|
|
if assert.NoError(t, LoadFromYamlBytes(input, &c)) {
|
|
|
|
assert.Equal(t, "localhost", c.Server.Redis.Host)
|
|
|
|
assert.Equal(t, 6379, c.Server.Redis.Port)
|
|
|
|
assert.Equal(t, "test", c.Server.Redis.Key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUnmarshalJsonBytesMap(t *testing.T) {
|
|
|
|
input := []byte(`{"foo":{"/mtproto.RPCTos": "bff.bff","bar":"baz"}}`)
|
|
|
|
|
|
|
|
var val struct {
|
|
|
|
Foo map[string]string
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.NoError(t, LoadFromJsonBytes(input, &val))
|
|
|
|
assert.Equal(t, "bff.bff", val.Foo["/mtproto.RPCTos"])
|
|
|
|
assert.Equal(t, "baz", val.Foo["bar"])
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUnmarshalJsonBytesMapWithSliceElements(t *testing.T) {
|
|
|
|
input := []byte(`{"foo":{"/mtproto.RPCTos": ["bff.bff", "any"],"bar":["baz", "qux"]}}`)
|
|
|
|
|
|
|
|
var val struct {
|
|
|
|
Foo map[string][]string
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.NoError(t, LoadFromJsonBytes(input, &val))
|
|
|
|
assert.EqualValues(t, []string{"bff.bff", "any"}, val.Foo["/mtproto.RPCTos"])
|
|
|
|
assert.EqualValues(t, []string{"baz", "qux"}, val.Foo["bar"])
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUnmarshalJsonBytesMapWithSliceOfStructs(t *testing.T) {
|
|
|
|
input := []byte(`{"foo":{
|
|
|
|
"/mtproto.RPCTos": [{"bar": "any"}],
|
|
|
|
"bar":[{"bar": "qux"}, {"bar": "ever"}]}}`)
|
|
|
|
|
|
|
|
var val struct {
|
|
|
|
Foo map[string][]struct {
|
|
|
|
Bar string
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.NoError(t, LoadFromJsonBytes(input, &val))
|
|
|
|
assert.Equal(t, 1, len(val.Foo["/mtproto.RPCTos"]))
|
|
|
|
assert.Equal(t, "any", val.Foo["/mtproto.RPCTos"][0].Bar)
|
|
|
|
assert.Equal(t, 2, len(val.Foo["bar"]))
|
|
|
|
assert.Equal(t, "qux", val.Foo["bar"][0].Bar)
|
|
|
|
assert.Equal(t, "ever", val.Foo["bar"][1].Bar)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUnmarshalJsonBytesWithAnonymousField(t *testing.T) {
|
|
|
|
type (
|
|
|
|
Int int
|
|
|
|
|
|
|
|
InnerConf struct {
|
|
|
|
Name string
|
|
|
|
}
|
|
|
|
|
|
|
|
Conf struct {
|
|
|
|
Int
|
|
|
|
InnerConf
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
input = []byte(`{"Name": "hello", "int": 3}`)
|
|
|
|
c Conf
|
|
|
|
)
|
|
|
|
assert.NoError(t, LoadFromJsonBytes(input, &c))
|
|
|
|
assert.Equal(t, "hello", c.Name)
|
|
|
|
assert.Equal(t, Int(3), c.Int)
|
|
|
|
}
|
|
|
|
|
|
|
|
func createTempFile(ext, text string) (string, error) {
|
|
|
|
tmpfile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := os.WriteFile(tmpfile.Name(), []byte(text), os.ModeTemporary); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
filename := tmpfile.Name()
|
|
|
|
if err = tmpfile.Close(); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return filename, nil
|
|
|
|
}
|