mirror of
https://github.com/gohugoio/hugo.git
synced 2025-05-04 08:01:25 +00:00
tpl/data: Clean up data namespace
- Move the main GetCSV and GetJSON into data.go. - Add error returns to GetCSV and GetJSON. - Add http client to Namespace for test mocking. - Send accept headers on remote requests. Fixes #3395 - Return an error on non-2XX HTTP response codes and don't retry. - Move cache tests to cache_test.go.
This commit is contained in:
parent
1cf2f3dc4f
commit
08c0de5cc3
5 changed files with 476 additions and 310 deletions
63
tpl/data/cache_test.go
Normal file
63
tpl/data/cache_test.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright 2017 The Hugo Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCache(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
fs := new(afero.MemMapFs)
|
||||||
|
|
||||||
|
for i, test := range []struct {
|
||||||
|
path string
|
||||||
|
content []byte
|
||||||
|
ignore bool
|
||||||
|
}{
|
||||||
|
{"http://Foo.Bar/foo_Bar-Foo", []byte(`T€st Content 123`), false},
|
||||||
|
{"fOO,bar:foo%bAR", []byte(`T€st Content 123 fOO,bar:foo%bAR`), false},
|
||||||
|
{"FOo/BaR.html", []byte(`FOo/BaR.html T€st Content 123`), false},
|
||||||
|
{"трям/трям", []byte(`T€st трям/трям Content 123`), false},
|
||||||
|
{"은행", []byte(`T€st C은행ontent 123`), false},
|
||||||
|
{"Банковский кассир", []byte(`Банковский кассир T€st Content 123`), false},
|
||||||
|
{"Банковский кассир", []byte(`Банковский кассир T€st Content 456`), true},
|
||||||
|
} {
|
||||||
|
msg := fmt.Sprintf("Test #%d: %v", i, test)
|
||||||
|
|
||||||
|
cfg := viper.New()
|
||||||
|
|
||||||
|
c, err := getCache(test.path, fs, cfg, test.ignore)
|
||||||
|
assert.NoError(t, err, msg)
|
||||||
|
assert.Nil(t, c, msg)
|
||||||
|
|
||||||
|
err = writeCache(test.path, test.content, fs, cfg, test.ignore)
|
||||||
|
assert.NoError(t, err, msg)
|
||||||
|
|
||||||
|
c, err = getCache(test.path, fs, cfg, test.ignore)
|
||||||
|
assert.NoError(t, err, msg)
|
||||||
|
|
||||||
|
if test.ignore {
|
||||||
|
assert.Nil(t, c, msg)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, string(test.content), string(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
tpl/data/data.go
114
tpl/data/data.go
|
@ -13,16 +13,126 @@
|
||||||
|
|
||||||
package data
|
package data
|
||||||
|
|
||||||
import "github.com/spf13/hugo/deps"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/hugo/deps"
|
||||||
|
jww "github.com/spf13/jwalterweatherman"
|
||||||
|
)
|
||||||
|
|
||||||
// New returns a new instance of the data-namespaced template functions.
|
// New returns a new instance of the data-namespaced template functions.
|
||||||
func New(deps *deps.Deps) *Namespace {
|
func New(deps *deps.Deps) *Namespace {
|
||||||
return &Namespace{
|
return &Namespace{
|
||||||
deps: deps,
|
deps: deps,
|
||||||
|
client: http.DefaultClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Namespace provides template functions for the "data" namespace.
|
// Namespace provides template functions for the "data" namespace.
|
||||||
type Namespace struct {
|
type Namespace struct {
|
||||||
deps *deps.Deps
|
deps *deps.Deps
|
||||||
|
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCSV expects a data separator and one or n-parts of a URL to a resource which
|
||||||
|
// can either be a local or a remote one.
|
||||||
|
// The data separator can be a comma, semi-colon, pipe, etc, but only one character.
|
||||||
|
// If you provide multiple parts for the URL they will be joined together to the final URL.
|
||||||
|
// GetCSV returns nil or a slice slice to use in a short code.
|
||||||
|
func (ns *Namespace) GetCSV(sep string, urlParts ...string) (d [][]string, err error) {
|
||||||
|
url := strings.Join(urlParts, "")
|
||||||
|
|
||||||
|
var clearCacheSleep = func(i int, u string) {
|
||||||
|
jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep)
|
||||||
|
time.Sleep(resSleep)
|
||||||
|
deleteCache(url, ns.deps.Fs.Source, ns.deps.Cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i <= resRetries; i++ {
|
||||||
|
var req *http.Request
|
||||||
|
req, err = http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
jww.ERROR.Printf("Failed to create request for getJSON: %s", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Accept", "text/csv")
|
||||||
|
req.Header.Add("Accept", "text/plain")
|
||||||
|
|
||||||
|
var c []byte
|
||||||
|
c, err = ns.getResource(req)
|
||||||
|
if err != nil {
|
||||||
|
jww.ERROR.Printf("Failed to read csv resource %q with error message %s", url, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Contains(c, []byte(sep)) {
|
||||||
|
err = errors.New("Cannot find separator " + sep + " in CSV.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if d, err = parseCSV(c, sep); err != nil {
|
||||||
|
jww.ERROR.Printf("Failed to parse csv file %s with error message %s", url, err)
|
||||||
|
clearCacheSleep(i, url)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJSON expects one or n-parts of a URL to a resource which can either be a local or a remote one.
|
||||||
|
// If you provide multiple parts they will be joined together to the final URL.
|
||||||
|
// GetJSON returns nil or parsed JSON to use in a short code.
|
||||||
|
func (ns *Namespace) GetJSON(urlParts ...string) (v interface{}, err error) {
|
||||||
|
url := strings.Join(urlParts, "")
|
||||||
|
|
||||||
|
for i := 0; i <= resRetries; i++ {
|
||||||
|
var req *http.Request
|
||||||
|
req, err = http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
jww.ERROR.Printf("Failed to create request for getJSON: %s", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
|
||||||
|
var c []byte
|
||||||
|
c, err = ns.getResource(req)
|
||||||
|
if err != nil {
|
||||||
|
jww.ERROR.Printf("Failed to get json resource %s with error message %s", url, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(c, &v)
|
||||||
|
if err != nil {
|
||||||
|
jww.ERROR.Printf("Cannot read json from resource %s with error message %s", url, err)
|
||||||
|
jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep)
|
||||||
|
time.Sleep(resSleep)
|
||||||
|
deleteCache(url, ns.deps.Fs.Source, ns.deps.Cfg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCSV parses bytes of CSV data into a slice slice string or an error
|
||||||
|
func parseCSV(c []byte, sep string) ([][]string, error) {
|
||||||
|
if len(sep) != 1 {
|
||||||
|
return nil, errors.New("Incorrect length of csv separator: " + sep)
|
||||||
|
}
|
||||||
|
b := bytes.NewReader(c)
|
||||||
|
r := csv.NewReader(b)
|
||||||
|
rSep := []rune(sep)
|
||||||
|
r.Comma = rSep[0]
|
||||||
|
r.FieldsPerRecord = 0
|
||||||
|
return r.ReadAll()
|
||||||
}
|
}
|
||||||
|
|
251
tpl/data/data_test.go
Normal file
251
tpl/data/data_test.go
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
// Copyright 2017 The Hugo Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetCSV(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ns := New(newDeps(viper.New()))
|
||||||
|
|
||||||
|
for i, test := range []struct {
|
||||||
|
sep string
|
||||||
|
url string
|
||||||
|
content string
|
||||||
|
expect interface{}
|
||||||
|
}{
|
||||||
|
// Remotes
|
||||||
|
{
|
||||||
|
",",
|
||||||
|
`http://success/`,
|
||||||
|
"gomeetup,city\nyes,Sydney\nyes,San Francisco\nyes,Stockholm\n",
|
||||||
|
[][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
",",
|
||||||
|
`http://error.extra.field/`,
|
||||||
|
"gomeetup,city\nyes,Sydney\nyes,San Francisco\nyes,Stockholm,EXTRA\n",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
",",
|
||||||
|
`http://error.no.sep/`,
|
||||||
|
"gomeetup;city\nyes;Sydney\nyes;San Francisco\nyes;Stockholm\n",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
",",
|
||||||
|
`http://nofound/404`,
|
||||||
|
``,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Locals
|
||||||
|
{
|
||||||
|
";",
|
||||||
|
"pass/semi",
|
||||||
|
"gomeetup;city\nyes;Sydney\nyes;San Francisco\nyes;Stockholm\n",
|
||||||
|
[][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
";",
|
||||||
|
"fail/no-file",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
msg := fmt.Sprintf("Test %d", i)
|
||||||
|
|
||||||
|
// Setup HTTP test server
|
||||||
|
var srv *httptest.Server
|
||||||
|
srv, ns.client = getTestServer(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !haveHeader(r.Header, "Accept", "text/csv") && !haveHeader(r.Header, "Accept", "text/plain") {
|
||||||
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/404" {
|
||||||
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-type", "text/csv")
|
||||||
|
|
||||||
|
w.Write([]byte(test.content))
|
||||||
|
})
|
||||||
|
defer func() { srv.Close() }()
|
||||||
|
|
||||||
|
// Setup local test file for schema-less URLs
|
||||||
|
if !strings.Contains(test.url, ":") && !strings.HasPrefix(test.url, "fail/") {
|
||||||
|
f, err := ns.deps.Fs.Source.Create(filepath.Join(ns.deps.Cfg.GetString("workingDir"), test.url))
|
||||||
|
require.NoError(t, err, msg)
|
||||||
|
f.WriteString(test.content)
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get on with it
|
||||||
|
got, err := ns.GetCSV(test.sep, test.url)
|
||||||
|
|
||||||
|
if _, ok := test.expect.(bool); ok {
|
||||||
|
assert.Error(t, err, msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NoError(t, err, msg)
|
||||||
|
require.NotNil(t, got, msg)
|
||||||
|
|
||||||
|
assert.EqualValues(t, test.expect, got, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetJSON(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ns := New(newDeps(viper.New()))
|
||||||
|
|
||||||
|
for i, test := range []struct {
|
||||||
|
url string
|
||||||
|
content string
|
||||||
|
expect interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`http://success/`,
|
||||||
|
`{"gomeetup":["Sydney","San Francisco","Stockholm"]}`,
|
||||||
|
map[string]interface{}{"gomeetup": []interface{}{"Sydney", "San Francisco", "Stockholm"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`http://malformed/`,
|
||||||
|
`{gomeetup:["Sydney","San Francisco","Stockholm"]}`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`http://nofound/404`,
|
||||||
|
``,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
// Locals
|
||||||
|
{
|
||||||
|
"pass/semi",
|
||||||
|
`{"gomeetup":["Sydney","San Francisco","Stockholm"]}`,
|
||||||
|
map[string]interface{}{"gomeetup": []interface{}{"Sydney", "San Francisco", "Stockholm"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fail/no-file",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
msg := fmt.Sprintf("Test %d", i)
|
||||||
|
|
||||||
|
// Setup HTTP test server
|
||||||
|
var srv *httptest.Server
|
||||||
|
srv, ns.client = getTestServer(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !haveHeader(r.Header, "Accept", "application/json") {
|
||||||
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/404" {
|
||||||
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-type", "application/json")
|
||||||
|
|
||||||
|
w.Write([]byte(test.content))
|
||||||
|
})
|
||||||
|
defer func() { srv.Close() }()
|
||||||
|
|
||||||
|
// Setup local test file for schema-less URLs
|
||||||
|
if !strings.Contains(test.url, ":") && !strings.HasPrefix(test.url, "fail/") {
|
||||||
|
f, err := ns.deps.Fs.Source.Create(filepath.Join(ns.deps.Cfg.GetString("workingDir"), test.url))
|
||||||
|
require.NoError(t, err, msg)
|
||||||
|
f.WriteString(test.content)
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get on with it
|
||||||
|
got, err := ns.GetJSON(test.url)
|
||||||
|
|
||||||
|
if _, ok := test.expect.(bool); ok {
|
||||||
|
assert.Error(t, err, msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NoError(t, err, msg)
|
||||||
|
require.NotNil(t, got, msg)
|
||||||
|
|
||||||
|
assert.EqualValues(t, test.expect, got, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCSV(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for i, test := range []struct {
|
||||||
|
csv []byte
|
||||||
|
sep string
|
||||||
|
exp string
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{[]byte("a,b,c\nd,e,f\n"), "", "", true},
|
||||||
|
{[]byte("a,b,c\nd,e,f\n"), "~/", "", true},
|
||||||
|
{[]byte("a,b,c\nd,e,f"), "|", "a,b,cd,e,f", false},
|
||||||
|
{[]byte("q,w,e\nd,e,f"), ",", "qwedef", false},
|
||||||
|
{[]byte("a|b|c\nd|e|f|g"), "|", "abcdefg", true},
|
||||||
|
{[]byte("z|y|c\nd|e|f"), "|", "zycdef", false},
|
||||||
|
} {
|
||||||
|
msg := fmt.Sprintf("Test %d: %v", i, test)
|
||||||
|
|
||||||
|
csv, err := parseCSV(test.csv, test.sep)
|
||||||
|
if test.err {
|
||||||
|
assert.Error(t, err, msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NoError(t, err, msg)
|
||||||
|
|
||||||
|
act := ""
|
||||||
|
for _, v := range csv {
|
||||||
|
act = act + strings.Join(v, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, test.exp, act, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func haveHeader(m http.Header, key, needle string) bool {
|
||||||
|
var s []string
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
if s, ok = m[key]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s {
|
||||||
|
if v == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -14,14 +14,10 @@
|
||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"fmt"
|
||||||
"encoding/csv"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -67,14 +63,16 @@ func (l *remoteLock) URLUnlock(url string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getRemote loads the content of a remote file. This method is thread safe.
|
// getRemote loads the content of a remote file. This method is thread safe.
|
||||||
func getRemote(url string, fs afero.Fs, cfg config.Provider, hc *http.Client) ([]byte, error) {
|
func getRemote(req *http.Request, fs afero.Fs, cfg config.Provider, hc *http.Client) ([]byte, error) {
|
||||||
|
url := req.URL.String()
|
||||||
|
|
||||||
c, err := getCache(url, fs, cfg, cfg.GetBool("ignoreCache"))
|
c, err := getCache(url, fs, cfg, cfg.GetBool("ignoreCache"))
|
||||||
if c != nil && err == nil {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if c != nil {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
// avoid race condition with locks, block other goroutines if the current url is processing
|
// avoid race condition with locks, block other goroutines if the current url is processing
|
||||||
remoteURLLock.URLLock(url)
|
remoteURLLock.URLLock(url)
|
||||||
|
@ -82,27 +80,34 @@ func getRemote(url string, fs afero.Fs, cfg config.Provider, hc *http.Client) ([
|
||||||
|
|
||||||
// avoid multiple locks due to calling getCache twice
|
// avoid multiple locks due to calling getCache twice
|
||||||
c, err = getCache(url, fs, cfg, cfg.GetBool("ignoreCache"))
|
c, err = getCache(url, fs, cfg, cfg.GetBool("ignoreCache"))
|
||||||
if c != nil && err == nil {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c != nil {
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jww.INFO.Printf("Downloading: %s ...", url)
|
||||||
|
res, err := hc.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
jww.INFO.Printf("Downloading: %s ...", url)
|
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||||
res, err := hc.Get(url)
|
return nil, fmt.Errorf("Failed to retrieve remote file: %s", http.StatusText(res.StatusCode))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err = ioutil.ReadAll(res.Body)
|
c, err = ioutil.ReadAll(res.Body)
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = writeCache(url, c, fs, cfg, cfg.GetBool("ignoreCache"))
|
err = writeCache(url, c, fs, cfg, cfg.GetBool("ignoreCache"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
jww.INFO.Printf("... and cached to: %s", getCacheFileID(cfg, url))
|
jww.INFO.Printf("... and cached to: %s", getCacheFileID(cfg, url))
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
@ -119,90 +124,11 @@ func getLocal(url string, fs afero.Fs, cfg config.Provider) ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getResource loads the content of a local or remote file
|
// getResource loads the content of a local or remote file
|
||||||
func (ns *Namespace) getResource(url string) ([]byte, error) {
|
func (ns *Namespace) getResource(req *http.Request) ([]byte, error) {
|
||||||
if url == "" {
|
switch req.URL.Scheme {
|
||||||
return nil, nil
|
case "":
|
||||||
|
return getLocal(req.URL.String(), ns.deps.Fs.Source, ns.deps.Cfg)
|
||||||
|
default:
|
||||||
|
return getRemote(req, ns.deps.Fs.Source, ns.deps.Cfg, ns.client)
|
||||||
}
|
}
|
||||||
if strings.Contains(url, "://") {
|
|
||||||
return getRemote(url, ns.deps.Fs.Source, ns.deps.Cfg, http.DefaultClient)
|
|
||||||
}
|
|
||||||
return getLocal(url, ns.deps.Fs.Source, ns.deps.Cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetJSON expects one or n-parts of a URL to a resource which can either be a local or a remote one.
|
|
||||||
// If you provide multiple parts they will be joined together to the final URL.
|
|
||||||
// GetJSON returns nil or parsed JSON to use in a short code.
|
|
||||||
func (ns *Namespace) GetJSON(urlParts ...string) interface{} {
|
|
||||||
var v interface{}
|
|
||||||
url := strings.Join(urlParts, "")
|
|
||||||
|
|
||||||
for i := 0; i <= resRetries; i++ {
|
|
||||||
c, err := ns.getResource(url)
|
|
||||||
if err != nil {
|
|
||||||
jww.ERROR.Printf("Failed to get json resource %s with error message %s", url, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(c, &v)
|
|
||||||
if err != nil {
|
|
||||||
jww.ERROR.Printf("Cannot read json from resource %s with error message %s", url, err)
|
|
||||||
jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep)
|
|
||||||
time.Sleep(resSleep)
|
|
||||||
deleteCache(url, ns.deps.Fs.Source, ns.deps.Cfg)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseCSV parses bytes of CSV data into a slice slice string or an error
|
|
||||||
func parseCSV(c []byte, sep string) ([][]string, error) {
|
|
||||||
if len(sep) != 1 {
|
|
||||||
return nil, errors.New("Incorrect length of csv separator: " + sep)
|
|
||||||
}
|
|
||||||
b := bytes.NewReader(c)
|
|
||||||
r := csv.NewReader(b)
|
|
||||||
rSep := []rune(sep)
|
|
||||||
r.Comma = rSep[0]
|
|
||||||
r.FieldsPerRecord = 0
|
|
||||||
return r.ReadAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCSV expects a data separator and one or n-parts of a URL to a resource which
|
|
||||||
// can either be a local or a remote one.
|
|
||||||
// The data separator can be a comma, semi-colon, pipe, etc, but only one character.
|
|
||||||
// If you provide multiple parts for the URL they will be joined together to the final URL.
|
|
||||||
// GetCSV returns nil or a slice slice to use in a short code.
|
|
||||||
func (ns *Namespace) GetCSV(sep string, urlParts ...string) [][]string {
|
|
||||||
var d [][]string
|
|
||||||
url := strings.Join(urlParts, "")
|
|
||||||
|
|
||||||
var clearCacheSleep = func(i int, u string) {
|
|
||||||
jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep)
|
|
||||||
time.Sleep(resSleep)
|
|
||||||
deleteCache(url, ns.deps.Fs.Source, ns.deps.Cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i <= resRetries; i++ {
|
|
||||||
c, err := ns.getResource(url)
|
|
||||||
|
|
||||||
if err == nil && !bytes.Contains(c, []byte(sep)) {
|
|
||||||
err = errors.New("Cannot find separator " + sep + " in CSV.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
jww.ERROR.Printf("Failed to read csv resource %s with error message %s", url, err)
|
|
||||||
clearCacheSleep(i, url)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if d, err = parseCSV(c, sep); err != nil {
|
|
||||||
jww.ERROR.Printf("Failed to parse csv file %s with error message %s", url, err)
|
|
||||||
clearCacheSleep(i, url)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -31,58 +30,9 @@ import (
|
||||||
"github.com/spf13/hugo/hugofs"
|
"github.com/spf13/hugo/hugofs"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestScpCache(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
path string
|
|
||||||
content []byte
|
|
||||||
ignore bool
|
|
||||||
}{
|
|
||||||
{"http://Foo.Bar/foo_Bar-Foo", []byte(`T€st Content 123`), false},
|
|
||||||
{"fOO,bar:foo%bAR", []byte(`T€st Content 123 fOO,bar:foo%bAR`), false},
|
|
||||||
{"FOo/BaR.html", []byte(`FOo/BaR.html T€st Content 123`), false},
|
|
||||||
{"трям/трям", []byte(`T€st трям/трям Content 123`), false},
|
|
||||||
{"은행", []byte(`T€st C은행ontent 123`), false},
|
|
||||||
{"Банковский кассир", []byte(`Банковский кассир T€st Content 123`), false},
|
|
||||||
{"Банковский кассир", []byte(`Банковский кассир T€st Content 456`), true},
|
|
||||||
}
|
|
||||||
|
|
||||||
fs := new(afero.MemMapFs)
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
cfg := viper.New()
|
|
||||||
c, err := getCache(test.path, fs, cfg, test.ignore)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error getting cache: %s", err)
|
|
||||||
}
|
|
||||||
if c != nil {
|
|
||||||
t.Errorf("There is content where there should not be anything: %s", string(c))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writeCache(test.path, test.content, fs, cfg, test.ignore)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error writing cache: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err = getCache(test.path, fs, cfg, test.ignore)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error getting cache after writing: %s", err)
|
|
||||||
}
|
|
||||||
if test.ignore {
|
|
||||||
if c != nil {
|
|
||||||
t.Errorf("Cache ignored but content is not nil: %s", string(c))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !bytes.Equal(c, test.content) {
|
|
||||||
t.Errorf("\nExpected: %s\nActual: %s\n", string(test.content), string(c))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestScpGetLocal(t *testing.T) {
|
func TestScpGetLocal(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
|
@ -146,6 +96,10 @@ func TestScpGetRemote(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
msg := fmt.Sprintf("%v", test)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", test.path, nil)
|
||||||
|
require.NoError(t, err, msg)
|
||||||
|
|
||||||
srv, cl := getTestServer(func(w http.ResponseWriter, r *http.Request) {
|
srv, cl := getTestServer(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write(test.content)
|
w.Write(test.content)
|
||||||
|
@ -154,41 +108,38 @@ func TestScpGetRemote(t *testing.T) {
|
||||||
|
|
||||||
cfg := viper.New()
|
cfg := viper.New()
|
||||||
|
|
||||||
c, err := getRemote(test.path, fs, cfg, cl)
|
c, err := getRemote(req, fs, cfg, cl)
|
||||||
if err != nil {
|
require.NoError(t, err, msg)
|
||||||
t.Errorf("Error getting resource content: %s", err)
|
assert.Equal(t, string(test.content), string(c))
|
||||||
}
|
|
||||||
if !bytes.Equal(c, test.content) {
|
c, err = getCache(req.URL.String(), fs, cfg, test.ignore)
|
||||||
t.Errorf("\nNet Expected: %s\nNet Actual: %s\n", string(test.content), string(c))
|
require.NoError(t, err, msg)
|
||||||
}
|
|
||||||
cc, cErr := getCache(test.path, fs, cfg, test.ignore)
|
|
||||||
if cErr != nil {
|
|
||||||
t.Error(cErr)
|
|
||||||
}
|
|
||||||
if test.ignore {
|
if test.ignore {
|
||||||
if cc != nil {
|
assert.Empty(t, c, msg)
|
||||||
t.Errorf("Cache ignored but content is not nil: %s", string(cc))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if !bytes.Equal(cc, test.content) {
|
assert.Equal(t, string(test.content), string(c))
|
||||||
t.Errorf("\nCache Expected: %s\nCache Actual: %s\n", string(test.content), string(cc))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestScpGetRemoteParallel(t *testing.T) {
|
func TestScpGetRemoteParallel(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
fs := new(afero.MemMapFs)
|
|
||||||
|
ns := New(newDeps(viper.New()))
|
||||||
|
|
||||||
content := []byte(`T€st Content 123`)
|
content := []byte(`T€st Content 123`)
|
||||||
url := "http://Foo.Bar/foo_Bar-Foo"
|
|
||||||
srv, cl := getTestServer(func(w http.ResponseWriter, r *http.Request) {
|
srv, cl := getTestServer(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write(content)
|
w.Write(content)
|
||||||
})
|
})
|
||||||
defer func() { srv.Close() }()
|
defer func() { srv.Close() }()
|
||||||
|
|
||||||
for _, ignoreCache := range []bool{false, true} {
|
url := "http://Foo.Bar/foo_Bar-Foo"
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, ignoreCache := range []bool{false, true} {
|
||||||
cfg := viper.New()
|
cfg := viper.New()
|
||||||
cfg.Set("ignoreCache", ignoreCache)
|
cfg.Set("ignoreCache", ignoreCache)
|
||||||
|
|
||||||
|
@ -199,13 +150,9 @@ func TestScpGetRemoteParallel(t *testing.T) {
|
||||||
go func(gor int) {
|
go func(gor int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := 0; j < 10; j++ {
|
for j := 0; j < 10; j++ {
|
||||||
c, err := getRemote(url, fs, cfg, cl)
|
c, err := getRemote(req, ns.deps.Fs.Source, ns.deps.Cfg, cl)
|
||||||
if err != nil {
|
assert.NoError(t, err)
|
||||||
t.Errorf("Error getting resource content: %s", err)
|
assert.Equal(t, string(content), string(c))
|
||||||
}
|
|
||||||
if !bytes.Equal(c, content) {
|
|
||||||
t.Errorf("\nNet Expected: %s\nNet Actual: %s\n", string(content), string(c))
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(23 * time.Millisecond)
|
time.Sleep(23 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
@ -214,137 +161,6 @@ func TestScpGetRemoteParallel(t *testing.T) {
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Log("Done!")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseCSV(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
csv []byte
|
|
||||||
sep string
|
|
||||||
exp string
|
|
||||||
err bool
|
|
||||||
}{
|
|
||||||
{[]byte("a,b,c\nd,e,f\n"), "", "", true},
|
|
||||||
{[]byte("a,b,c\nd,e,f\n"), "~/", "", true},
|
|
||||||
{[]byte("a,b,c\nd,e,f"), "|", "a,b,cd,e,f", false},
|
|
||||||
{[]byte("q,w,e\nd,e,f"), ",", "qwedef", false},
|
|
||||||
{[]byte("a|b|c\nd|e|f|g"), "|", "abcdefg", true},
|
|
||||||
{[]byte("z|y|c\nd|e|f"), "|", "zycdef", false},
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
csv, err := parseCSV(test.csv, test.sep)
|
|
||||||
if test.err && err == nil {
|
|
||||||
t.Error("Expecting an error")
|
|
||||||
}
|
|
||||||
if test.err {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !test.err && err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
act := ""
|
|
||||||
for _, v := range csv {
|
|
||||||
act = act + strings.Join(v, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if act != test.exp {
|
|
||||||
t.Errorf("\nExpected: %s\nActual: %s\n%#v\n", test.exp, act, csv)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetJSONFailParse(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ns := New(newDeps(viper.New()))
|
|
||||||
|
|
||||||
reqCount := 0
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if reqCount > 0 {
|
|
||||||
w.Header().Add("Content-type", "application/json")
|
|
||||||
fmt.Fprintln(w, `{"gomeetup":["Sydney", "San Francisco", "Stockholm"]}`)
|
|
||||||
} else {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
fmt.Fprintln(w, `ERROR 500`)
|
|
||||||
}
|
|
||||||
reqCount++
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
url := ts.URL + "/test.json"
|
|
||||||
|
|
||||||
want := map[string]interface{}{"gomeetup": []interface{}{"Sydney", "San Francisco", "Stockholm"}}
|
|
||||||
have := ns.GetJSON(url)
|
|
||||||
assert.NotNil(t, have)
|
|
||||||
if have != nil {
|
|
||||||
assert.EqualValues(t, want, have)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetCSVFailParseSep(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ns := New(newDeps(viper.New()))
|
|
||||||
|
|
||||||
reqCount := 0
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if reqCount > 0 {
|
|
||||||
w.Header().Add("Content-type", "application/json")
|
|
||||||
fmt.Fprintln(w, `gomeetup,city`)
|
|
||||||
fmt.Fprintln(w, `yes,Sydney`)
|
|
||||||
fmt.Fprintln(w, `yes,San Francisco`)
|
|
||||||
fmt.Fprintln(w, `yes,Stockholm`)
|
|
||||||
} else {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
fmt.Fprintln(w, `ERROR 500`)
|
|
||||||
}
|
|
||||||
reqCount++
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
url := ts.URL + "/test.csv"
|
|
||||||
|
|
||||||
want := [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}}
|
|
||||||
have := ns.GetCSV(",", url)
|
|
||||||
assert.NotNil(t, have)
|
|
||||||
if have != nil {
|
|
||||||
assert.EqualValues(t, want, have)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetCSVFailParse(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ns := New(newDeps(viper.New()))
|
|
||||||
|
|
||||||
reqCount := 0
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Add("Content-type", "application/json")
|
|
||||||
if reqCount > 0 {
|
|
||||||
fmt.Fprintln(w, `gomeetup,city`)
|
|
||||||
fmt.Fprintln(w, `yes,Sydney`)
|
|
||||||
fmt.Fprintln(w, `yes,San Francisco`)
|
|
||||||
fmt.Fprintln(w, `yes,Stockholm`)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintln(w, `gomeetup,city`)
|
|
||||||
fmt.Fprintln(w, `yes,Sydney,Bondi,`) // wrong number of fields in line
|
|
||||||
fmt.Fprintln(w, `yes,San Francisco`)
|
|
||||||
fmt.Fprintln(w, `yes,Stockholm`)
|
|
||||||
}
|
|
||||||
reqCount++
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
url := ts.URL + "/test.csv"
|
|
||||||
|
|
||||||
want := [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}}
|
|
||||||
have := ns.GetCSV(",", url)
|
|
||||||
assert.NotNil(t, have)
|
|
||||||
if have != nil {
|
|
||||||
assert.EqualValues(t, want, have)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDeps(cfg config.Provider) *deps.Deps {
|
func newDeps(cfg config.Provider) *deps.Deps {
|
||||||
|
|
Loading…
Add table
Reference in a new issue