Upgrading server dependancies (#8154)

This commit is contained in:
Christopher Speller
2018-01-29 14:17:40 -08:00
committed by GitHub
parent 8d66523ba7
commit 961c04cae9
1508 changed files with 182081 additions and 24688 deletions

View File

@@ -21,6 +21,9 @@ import (
_ "github.com/go-ldap/ldap"
_ "github.com/hashicorp/memberlist"
_ "github.com/mattermost/rsc/qr"
_ "github.com/prometheus/client_golang/prometheus"
_ "github.com/prometheus/client_golang/prometheus/promhttp"
_ "github.com/tylerb/graceful"
_ "gopkg.in/olivere/elastic.v5"
)

111
glide.lock generated
View File

@@ -1,23 +1,19 @@
hash: fa27dd8f4fd1b15c505a3d6f2023662471391e4a3ea22e67c829376c7e6e90c8
updated: 2018-01-05T10:08:00.418197689+01:00
hash: 5e8ab6acb5c3bb7dbedfc6e837cc529e72affea22384ec59e82f0e3c13379b6f
updated: 2018-01-25T15:20:13.899302483-08:00
imports:
- name: github.com/alecthomas/log4go
version: 3fbce08846379ec7f4f6bc7fce6dd01ce28fae4c
repo: https://github.com/mattermost/log4go.git
- name: github.com/armon/go-metrics
version: 7aa49fde808223f8dadfdbfd3a20ff6c19e5f9ec
- name: github.com/avct/uasurfer
version: 7b6f7205267b5b81d20da3f09ec3dea3b3d90cf8
- name: github.com/beorn7/perks
version: 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9
subpackages:
- quantile
- name: github.com/corpix/uarand
version: 2b8494104d86337cdd41d0a49cbed8e4583c0ab4
- name: github.com/cpanato/html2text
version: d47a5532a7bc36ad7b2b8ec3eebe24e975154f94
- name: github.com/davecgh/go-spew
version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9
version: ecdeabc65495df2dec95d7c4a4c3e021903035e5
subpackages:
- spew
- name: github.com/dgryski/dgoogauth
@@ -25,19 +21,19 @@ imports:
- name: github.com/dimchansky/utfbom
version: 6c6132ff69f0f6c088739067407b5d32c52e1d0f
- name: github.com/disintegration/imaging
version: dd50a3ee9985ccd313a2f03c398fcaedc96dc707
version: 1884593a19ddc6f2ea050403430d02c1d0fc1283
- name: github.com/dyatlov/go-opengraph
version: 41a3523719dfbe7e8f853fbd4061867543db5270
subpackages:
- opengraph
- name: github.com/fsnotify/fsnotify
version: 629574ca2a5df945712d3079857300b5e4da0236
version: c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9
- name: github.com/go-ini/ini
version: 32e4c1e6bc4e7d0d8451aa6b75200d19e37a536a
- name: github.com/go-ldap/ldap
version: 8168ee085ee43257585e50c6441aadf54ecb2c9f
version: bb7a9ca6e4fbc2129e3db588a34bc970ffe811a9
- name: github.com/go-redis/redis
version: d0f86971b5d61de9cebd2616b932acb7ba14d957
version: 4021ace05686f632ff17fd824bbed229fc474cf8
subpackages:
- internal
- internal/consistenthash
@@ -45,14 +41,14 @@ imports:
- internal/pool
- internal/proto
- name: github.com/go-sql-driver/mysql
version: a0583e0143b1624142adab07e0e97fe106d99561
version: bc14601d1bd56421dd60f561e6052c9ed77f9daf
- name: github.com/golang/freetype
version: e2365dfdc4a05e4b8299a783240d4a7d5a65d4e4
subpackages:
- raster
- truetype
- name: github.com/golang/protobuf
version: 1e59b77b52bf8e4b449a57e6f79f21226d571845
version: 925541529c1fa6821df4e44ce2723319eb2be768
subpackages:
- proto
- name: github.com/gorilla/context
@@ -60,13 +56,13 @@ imports:
- name: github.com/gorilla/handlers
version: 90663712d74cb411cbef281bc1e08c19d1a76145
- name: github.com/gorilla/mux
version: 7f08801859139f86dfafd1c296e2cba9a80d292e
version: 53c1911da2b537f792e7cafcb446b05ffe33b996
- name: github.com/gorilla/websocket
version: ea4d1f681babbce9545c9c5f3d5194a789c89f5b
version: 91f589db023d66e4aba7112d44cc0d2fb091c553
- name: github.com/hashicorp/errwrap
version: 7554cd9344cec97297fa6649b055a8c98c2a1e55
- name: github.com/hashicorp/go-immutable-radix
version: 8aac2701530899b64bdea735a1de8da899815220
version: 59b67882ec612f43b9d4c4fd97cebd507be4b3ee
- name: github.com/hashicorp/go-msgpack
version: fa3f63826f7c23912c15263591e65d54d080b458
subpackages:
@@ -92,23 +88,25 @@ imports:
- json/scanner
- json/token
- name: github.com/hashicorp/memberlist
<<<<<<< HEAD
version: caa5d20d6a642b7543b3745e54031a96008bee57
version: 3d8438da9589e7b608a83ffac1ef8211486bcb7c
- name: github.com/icrowley/fake
version: e64cc2cf92049a299f359734c6ea76073f2a8b2c
=======
version: 3d8438da9589e7b608a83ffac1ef8211486bcb7c
>>>>>>> Updated dependencies and added avct/uasurfer
- name: github.com/inconshreveable/mousetrap
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
- name: github.com/jehiah/go-strftime
version: 834e15c05a45371503440cc195bbd05c9a0968d9
- name: github.com/lib/pq
version: 83612a56d3dd153a94a629cd64925371c9adad78
version: 19c8e9ad00952ce0c64489b60e8df88bb16dd514
subpackages:
- oid
- name: github.com/magiconair/properties
version: 49d762b9817ba1c2e9d0c69183c2b4a8b8f1d934
- name: github.com/mailru/easyjson
version: 32fa128f234d041f196a9f3e0fea5ac9772c08e1
subpackages:
- buffer
- jlexer
- jwriter
- name: github.com/mattermost/gorp
version: 995ddf2264c4ad45fbaf342f7500e4787ebae84a
- name: github.com/mattermost/html2text
@@ -120,15 +118,13 @@ imports:
- qr
- qr/coding
- name: github.com/matttproud/golang_protobuf_extensions
version: 3247c84500bff8d9fb6d579d800f20b3e091582c
version: c12348ce28de40eed0136aa2b644d0ee0650e56c
subpackages:
- pbutil
- name: github.com/miekg/dns
version: f5ac34d755eb6975958108413c7802a782ca16c8
- name: github.com/minio/go-homedir
version: 4d76aabb80b22bad8695d3904e943f1fb5e6199f
version: 5364553f1ee9cddc7ac8b62dce148309c386695b
- name: github.com/minio/minio-go
version: 4e0f567303d4cc90ceb055a451959fb9fc391fb9
version: 14f1d472d115bac5ca4804094aa87484a72ced61
subpackages:
- pkg/credentials
- pkg/encrypt
@@ -136,8 +132,10 @@ imports:
- pkg/s3signer
- pkg/s3utils
- pkg/set
- name: github.com/mitchellh/go-homedir
version: b8bc1bf767474819792c23f32d8286a45736f1c6
- name: github.com/mitchellh/mapstructure
version: 06020f85339e21b2478f756a78e295255ffa4d6a
version: b4575eea38cca1123ec2dc90c26529b5c5acfcff
- name: github.com/mssola/user_agent
version: 5243daae23628aeae9b6268541406bd5e95d5964
- name: github.com/nicksnyder/go-i18n
@@ -148,21 +146,27 @@ imports:
- i18n/language
- i18n/translation
- name: github.com/NYTimes/gziphandler
version: 47ca22a0aeea4c9ceddfb935d818d636d934c312
version: 289a3b81f5aedc99f8d6eb0f67827c142f1310d8
- name: github.com/olivere/elastic
version: c51e74f9bcab8906a2f6cf5660dac396ba51b3d6
subpackages:
- config
- uritemplates
- name: github.com/pborman/uuid
version: e790cca94e6cc75c7064b1332e63811d4aae1a53
- name: github.com/pelletier/go-toml
version: 0131db6d737cfbbfb678f8b7d92e55e27ce46224
version: acdc4509485b587f5e675510c4f2c63e90ff68a8
- name: github.com/pkg/errors
version: e881fd58d78e04cf6d0de1217f8707c8cc2249bc
version: 645ef00459ed84a119197bfb8d8205042c6df63d
- name: github.com/pmezard/go-difflib
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
version: 792786c7400a136282c1664665ae0a8db921c6c2
subpackages:
- difflib
- name: github.com/prometheus/client_golang
version: c5b7fccd204277076155f10851dad72b76a49317
version: 06bc6e01f4baf4ee783ffcd23abfcb0b0f9dfada
subpackages:
- prometheus
- prometheus/promhttp
- name: github.com/prometheus/client_model
version: 99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c
subpackages:
@@ -171,8 +175,14 @@ imports:
version: 89604d197083d4781071d3c65855d24ecfb0a563
subpackages:
- expfmt
- internal/bitbucket.org/ww/goautoneg
- model
- name: github.com/prometheus/procfs
version: b15cd069a83443be3154b719d0cc9fe8117f09fb
version: cb4147076ac75738c9a7d279075a253c0cc5acbd
subpackages:
- internal/util
- nfs
- xfs
- name: github.com/rsc/letsencrypt
version: 33926faef6d434b854ea994228f11d0185faa0c1
- name: github.com/rwcarlsen/goexif
@@ -187,38 +197,38 @@ imports:
- name: github.com/segmentio/backo-go
version: 204274ad699c0983a70203a566887f17a717fef4
- name: github.com/spf13/afero
version: 57afd63c68602b63ed976de00dd066ccb3c319db
version: bb8f1927f2a9d3ab41c9340aa034f6b803f4359c
subpackages:
- mem
- name: github.com/spf13/cast
version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4
- name: github.com/spf13/cobra
version: b95ab734e27d33e0d8fbabf71ca990568d4e2020
version: f91529fc609202eededff4de2dc0ba2f662240a3
- name: github.com/spf13/jwalterweatherman
version: 7c0cea34c8ece3fbeb2b27ab9b59511d360fb394
- name: github.com/spf13/pflag
version: e57e3eeb33f795204c1ca35f56c44f83227c6e66
version: 4c012f6dcd9546820e378d0bdda4d8fc772cdfea
- name: github.com/spf13/viper
version: aafc9e6bc7b7bb53ddaa75a5ef49a17d6e654be5
- name: github.com/stretchr/objx
version: cbeaeb16a013161a98496fad62933b1d21786672
version: 477a77ecc69700c7cdeb1fa9e129548e1c1c393c
- name: github.com/stretchr/testify
version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0
version: b91bfb9ebec76498946beb6af7c0230c7cc7ba6c
subpackages:
- assert
- mock
- require
- suite
- name: github.com/tylerb/graceful
version: 4654dfbb6ad53cb5e27f37d99b02e16c1872fbbb
version: d72b0151351a13d0421b763b88f791469c4f5dc7
- name: github.com/xenolf/lego
version: b929aa5aab5ad2e197bb3d74ef99fac61bfa47bc
version: 6bddbfd17a6e1ab782617eeab2f2007c6550b160
subpackages:
- acme
- name: github.com/xtgo/uuid
version: a0b114877d4caeffbd7f87e3757c17fce570fea7
- name: golang.org/x/crypto
version: b3c9a1d25cfbbbab0ff4780b71c4f54e6e92a0de
version: 3d37316aaa6bd9929127ac9a527abf408178ea7b
subpackages:
- bcrypt
- blowfish
@@ -234,29 +244,37 @@ imports:
- tiff
- tiff/lzw
- name: golang.org/x/net
version: 434ec0c7fe3742c984919a691b2018a6e9694425
version: 0ed95abb35c445290478a5348a7b38bb154135fd
subpackages:
- bpf
- context
- html
- html/atom
- idna
- internal/iana
- internal/socket
- ipv4
- ipv6
- lex/httplex
- name: golang.org/x/sys
version: 810d7000345868fc619eb81f46307107118f4ae1
version: 03467258950d845cd1877eab69461b98e8c09219
subpackages:
- unix
- name: golang.org/x/text
version: e19ae1496984b1c655b8044a65c0300a3c878dd3
subpackages:
- secure/bidirule
- transform
- unicode/bidi
- unicode/norm
- name: golang.org/x/time
version: 6dc17368e09b0e8634d71cac8168d853e869a0c7
subpackages:
- rate
- name: google.golang.org/appengine
version: 5bee14b453b4c71be47ec1781b0fa61c2ea182db
subpackages:
- cloudsql
- name: gopkg.in/alexcesaro/quotedprintable.v3
version: 2caba252f4dc53eaf6b553000885530023f54623
- name: gopkg.in/asn1-ber.v1
@@ -264,10 +282,7 @@ imports:
- name: gopkg.in/gomail.v2
version: 41f3572897373c5538c50a2402db15db079fa4fd
- name: gopkg.in/olivere/elastic.v5
version: 171ce647da4acfb30ffc99981d66d80bdea6bcee
subpackages:
- config
- uritemplates
version: c51e74f9bcab8906a2f6cf5660dac396ba51b3d6
- name: gopkg.in/square/go-jose.v1
version: aa2e30fdd1fe9dd3394119af66451ae790d50e0d
subpackages:

View File

@@ -5,29 +5,35 @@ import:
repo: https://github.com/mattermost/log4go.git
- package: github.com/dgryski/dgoogauth
- package: github.com/disintegration/imaging
version: v1.2.4
version: v1.3.0
- package: github.com/dyatlov/go-opengraph
subpackages:
- opengraph
- package: github.com/fsnotify/fsnotify
version: v1.4.2
version: v1.4.7
- package: github.com/go-ldap/ldap
version: v2.5.0
version: v2.5.1
- package: github.com/go-redis/redis
version: v6.8.2
- package: github.com/go-sql-driver/mysql
version: v1.3
- package: github.com/golang/freetype
- package: github.com/gorilla/handlers
version: v1.3.0
- package: github.com/gorilla/mux
version: v1.6.0
version: v1.6.1
- package: github.com/gorilla/websocket
version: v1.2.0
- package: github.com/hashicorp/memberlist
- package: github.com/icrowley/fake
- package: github.com/lib/pq
- package: github.com/mattermost/gorp
- package: github.com/mattermost/html2text
- package: github.com/mattermost/rsc
subpackages:
- qr
- package: github.com/minio/minio-go
version: v3.0.3
version: 4.0.6
subpackages:
- pkg/credentials
- package: github.com/mssola/user_agent
- package: github.com/nicksnyder/go-i18n
version: v1.10.0
@@ -35,6 +41,8 @@ import:
- i18n
- package: github.com/pborman/uuid
version: v1.1
- package: github.com/pkg/errors
version: v0.8.0
- package: github.com/rsc/letsencrypt
version: v0.0.1
- package: github.com/rwcarlsen/goexif
@@ -43,56 +51,31 @@ import:
- package: github.com/segmentio/analytics-go
version: 2.1.1
- package: github.com/spf13/cobra
- package: github.com/spf13/pflag
version: v1.0.0
- package: github.com/spf13/viper
- package: github.com/tylerb/graceful
version: v1.2.15
- package: github.com/stretchr/testify
version: v1.2.0
subpackages:
- assert
- mock
- require
- package: golang.org/x/crypto
subpackages:
- bcrypt
- package: golang.org/x/image
subpackages:
- bmp
- package: golang.org/x/net
subpackages:
- bpf
- package: golang.org/x/sys
subpackages:
- unix
- package: gopkg.in/gomail.v2
version: 2.0.0
- package: gopkg.in/olivere/elastic.v5
version: v6.1.4
- package: gopkg.in/throttled/throttled.v2
version: v2.1.0
subpackages:
- store/memstore
- package: github.com/prometheus/client_golang
version: v0.8.0
subpackages:
- prometheus
- package: github.com/beorn7/perks
subpackages:
- quantile
- package: github.com/golang/protobuf
subpackages:
- proto
- package: github.com/prometheus/client_model
subpackages:
- go
- package: github.com/prometheus/common
subpackages:
- expfmt
- package: github.com/matttproud/golang_protobuf_extensions
version: v1.0.0
subpackages:
- pbutil
- package: github.com/prometheus/procfs
- package: github.com/cpanato/html2text
- package: gopkg.in/olivere/elastic.v5
version: v5.0.53
- package: github.com/mattermost/gorp
version: 995ddf2264c4ad45fbaf342f7500e4787ebae84a
- package: github.com/go-redis/redis
version: v6.7.3
- package: github.com/stretchr/testify
version: v1.1.4
subpackages:
- assert
- require
- package: gopkg.in/gomail.v2
version: 2.0.0
- package: github.com/mattermost/html2text
- package: github.com/icrowley/fake
- package: github.com/avct/uasurfer
- package: gopkg.in/yaml.v2

View File

@@ -83,7 +83,7 @@ func (b *S3FileBackend) ReadFile(path string) ([]byte, *model.AppError) {
if err != nil {
return nil, model.NewAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
}
minioObject, err := s3Clnt.GetObject(b.bucket, path)
minioObject, err := s3Clnt.GetObject(b.bucket, path, s3.GetObjectOptions{})
if err != nil {
return nil, model.NewAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
}
@@ -138,13 +138,18 @@ func (b *S3FileBackend) WriteFile(f []byte, path string) *model.AppError {
return model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
}
ext := filepath.Ext(path)
metaData := s3Metadata(b.encrypt, "binary/octet-stream")
if model.IsFileExtImage(ext) {
metaData = s3Metadata(b.encrypt, model.GetImageMimeType(ext))
options := s3.PutObjectOptions{}
if b.encrypt {
options.UserMetadata["x-amz-server-side-encryption"] = "AES256"
}
if _, err = s3Clnt.PutObjectWithMetadata(b.bucket, path, bytes.NewReader(f), metaData, nil); err != nil {
if ext := filepath.Ext(path); model.IsFileExtImage(ext) {
options.ContentType = model.GetImageMimeType(ext)
} else {
options.ContentType = "binary/octet-stream"
}
if _, err = s3Clnt.PutObject(b.bucket, path, bytes.NewReader(f), -1, options); err != nil {
return model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
}
@@ -225,17 +230,6 @@ func (b *S3FileBackend) RemoveDirectory(path string) *model.AppError {
return nil
}
func s3Metadata(encrypt bool, contentType string) map[string][]string {
metaData := make(map[string][]string)
if contentType != "" {
metaData["Content-Type"] = []string{"contentType"}
}
if encrypt {
metaData["x-amz-server-side-encryption"] = []string{"AES256"}
}
return metaData
}
func s3CopyMetadata(encrypt bool) map[string]string {
metaData := make(map[string]string)
metaData["x-amz-server-side-encryption"] = "AES256"

View File

@@ -88,7 +88,7 @@ type GzipResponseWriterWithCloseNotify struct {
*GzipResponseWriter
}
func (w *GzipResponseWriterWithCloseNotify) CloseNotify() <-chan bool {
func (w GzipResponseWriterWithCloseNotify) CloseNotify() <-chan bool {
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
}

View File

@@ -325,17 +325,32 @@ func TestFlushBeforeWrite(t *testing.T) {
}
func TestImplementCloseNotifier(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/", nil)
request.Header.Set(acceptEncoding, "gzip")
GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request){
_, ok := rw.(http.CloseNotifier)
assert.True(t, ok, "response writer must implement http.CloseNotifier")
})).ServeHTTP(&mockRWCloseNotify{}, &http.Request{})
})).ServeHTTP(&mockRWCloseNotify{}, request)
}
func TestImplementFlusherAndCloseNotifier(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/", nil)
request.Header.Set(acceptEncoding, "gzip")
GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request){
_, okCloseNotifier := rw.(http.CloseNotifier)
assert.True(t, okCloseNotifier, "response writer must implement http.CloseNotifier")
_, okFlusher := rw.(http.Flusher)
assert.True(t, okFlusher, "response writer must implement http.Flusher")
})).ServeHTTP(&mockRWCloseNotify{}, request)
}
func TestNotImplementCloseNotifier(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/", nil)
request.Header.Set(acceptEncoding, "gzip")
GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request){
_, ok := rw.(http.CloseNotifier)
assert.False(t, ok, "response writer must not implement http.CloseNotifier")
})).ServeHTTP(httptest.NewRecorder(), &http.Request{})
})).ServeHTTP(httptest.NewRecorder(), request)
}

View File

@@ -1,56 +0,0 @@
# Compiled bin #
###################
# Compiled source #
###################
*.dll
*.exe
*.o
*.so
# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# Configuration Files #
#######################
*.cfg
# Logs and databases #
######################
*.log
*.sql
*.sqlite
logs
coverage.html
coverage.out
# Test Files #
#######################
*.test
# OS generated files #
######################
.DS_Store
.DS_Store?
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# go.rice generated files
*.rice-box.go
# Dev Tools #
######################
.vagrant

View File

@@ -1,11 +0,0 @@
sudo: false
language: go
go:
- 1.9.x
- 1.8.x
- 1.7.x
script:
- go test

View File

@@ -1,169 +0,0 @@
[![Build Status](https://travis-ci.org/avct/uasurfer.svg?branch=master)](https://travis-ci.org/avct/uasurfer) [![GoDoc](https://godoc.org/github.com/avct/uasurfer?status.svg)](https://godoc.org/github.com/avct/uasurfer) [![Go Report Card](https://goreportcard.com/badge/github.com/avct/uasurfer)](https://goreportcard.com/report/github.com/avct/uasurfer)
# uasurfer
![uasurfer-100px](https://cloud.githubusercontent.com/assets/597902/16172506/9debc136-357a-11e6-90fb-c7c46f50dff0.png)
**User Agent Surfer** (uasurfer) is a lightweight Golang package that parses and abstracts [HTTP User-Agent strings](https://en.wikipedia.org/wiki/User_agent) with particular attention to device type.
The following information is returned by uasurfer from a raw HTTP User-Agent string:
| Name | Example | Coverage in 192,792 parses |
|----------------|---------|--------------------------------|
| Browser name | `chrome` | 99.85% |
| Browser version | `53` | 99.17% |
| Platform | `ipad` | 99.97% |
| OS name | `ios` | 99.96% |
| OS version | `10` | 98.81% |
| Device type | `tablet` | 99.98% |
Layout engine, browser language, and other esoteric attributes are not parsed.
Coverage is estimated from a random sample of real UA strings collected across thousands of sources in US and EU mid-2016.
## Usage
### Parse(ua string) Function
The `Parse()` function accepts a user agent `string` and returns UserAgent struct with named constants and integers for versions (minor, major and patch separately), and the full UA string that was parsed (lowercase). A string can be retrieved by adding `.String()` to a variable, such as `uasurfer.BrowserName.String()`.
```
// Define a user agent string
myUA := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36"
// Parse() returns all attributes, including returning the full UA string last
ua, uaString := uasurfer.Parse(myUA)
```
where example UserAgent is:
```
{
Browser {
BrowserName: BrowserChrome,
Version: {
Major: 45,
Minor: 0,
Patch: 2454,
},
},
OS {
Platform: PlatformMac,
Name: OSMacOSX,
Version: {
Major: 10,
Minor: 10,
Patch: 5,
},
},
DeviceType: DeviceComputer,
}
```
**Usage note:** There are some OSes that do not return a version, see docs below. Linux is typically not reported with a specific Linux distro name or version.
#### Browser Name
* `BrowserChrome` - Google [Chrome](https://en.wikipedia.org/wiki/Google_Chrome), [Chromium](https://en.wikipedia.org/wiki/Chromium_(web_browser))
* `BrowserSafari` - Apple [Safari](https://en.wikipedia.org/wiki/Safari_(web_browser)), Google Search ([GSA](https://itunes.apple.com/us/app/google/id284815942))
* `BrowserIE` - Microsoft [Internet Explorer](https://en.wikipedia.org/wiki/Internet_Explorer), [Edge](https://en.wikipedia.org/wiki/Microsoft_Edge)
* `BrowserFirefox` - Mozilla [Firefox](https://en.wikipedia.org/wiki/Firefox), GNU [IceCat](https://en.wikipedia.org/wiki/GNU_IceCat), [Iceweasel](https://en.wikipedia.org/wiki/Mozilla_Corporation_software_rebranded_by_the_Debian_project#Iceweasel), [Seamonkey](https://en.wikipedia.org/wiki/SeaMonkey)
* `BrowserAndroid` - Android [WebView](https://developer.chrome.com/multidevice/webview/overview) (Android OS <4.4 only)
* `BrowserOpera` - [Opera](https://en.wikipedia.org/wiki/Opera_(web_browser))
* `BrowserUCBrowser` - [UC Browser](https://en.wikipedia.org/wiki/UC_Browser)
* `BrowserSilk` - Amazon [Silk](https://en.wikipedia.org/wiki/Amazon_Silk)
* `BrowserSpotify` - [Spotify](https://en.wikipedia.org/wiki/Spotify#Clients) desktop client
* `BrowserBlackberry` - RIM [BlackBerry](https://en.wikipedia.org/wiki/BlackBerry)
* `BrowserUnknown` - Unknown
#### Browser Version
Browser version returns an `unint8` of the major version attribute of the User-Agent String. For example Chrome 45.0.23423 would return `45`. The intention is to support math operators with versions, such as "do XYZ for Chrome version >23".
Unknown version is returned as `0`.
#### Platform
* `PlatformWindows` - Microsoft Windows
* `PlatformMac` - Apple Macintosh
* `PlatformLinux` - Linux, including Android and other OSes
* `PlatformiPad` - Apple iPad
* `PlatformiPhone` - Apple iPhone
* `PlatformBlackberry` - RIM Blackberry
* `PlatformWindowsPhone` Microsoft Windows Phone & Mobile
* `PlatformKindle` - Amazon Kindle & Kindle Fire
* `PlatformPlaystation` - Sony Playstation, Vita, PSP
* `PlatformXbox` - Microsoft Xbox - `PlatformXbox`
* `PlatformNintendo` - Nintendo DS, Wii, etc.
* `PlatformUnknown` - Unknown
#### OS Name
* `OSWindows`
* `OSMacOSX` - includes "macOS Sierra"
* `OSiOS`
* `OSAndroid`
* `OSChromeOS`
* `OSWebOS`
* `OSLinux`
* `OSPlaystation`
* `OSXbox`
* `OSNintendo`
* `OSUnknown`
#### OS Version
OS X major version is alway 10 with consecutive minor versions indicating release releases (10 - Yosemite, 11 - El Capitain, 12 Sierra, etc). Windows version is NT version. `Version{0, 0, 0}` indicated version is unknown or not evaluated.
Versions can be compared using `Less` function: `if ver1.Less(ver2) {}`
Here are some examples across the platform, os.name, and os.version:
* For Windows XP (Windows NT 5.1), "`PlatformWindows`" is the platform, "`OSWindows`" is the name, and `{5, 1, 0}` the version.
* For OS X 10.5.1, "`PlatformMac`" is the platform, "`OSMacOSX`" the name, and `{10, 5, 1}` the version.
* For Android 5.1, "`PlatformLinux`" is the platform, "`OSAndroid`" is the name, and `{5, 1, 0}` the version.
* For iOS 5.1, "`PlatformiPhone`" or "`PlatformiPad`" is the platform, "`OSiOS`" is the name, and `{5, 1, 0}` the version.
###### Windows Version Guide
* Windows 10 - `{10, 0, 0}`
* Windows 8.1 - `{6, 3, 0}`
* Windows 8 - `{6, 2, 0}`
* Windows 7 - `{6, 1, 0}`
* Windows Vista - `{6, 0, 0}`
* Windows XP - `{5, 1, 0}` or `{5, 2, 0}`
* Windows 2000 - `{5, 0, 0}`
Windows 95, 98, and ME represent 0.01% of traffic worldwide and are not available through this package at this time.
#### DeviceType
DeviceType is typically quite accurate, though determining between phones and tablets on Android is not always possible due to how some vendors design their UA strings. A mobile Android device without tablet indicator defaults to being classified as a phone. DeviceTV supports major brands such as Philips, Sharp, Vizio and steaming boxes such as Apple, Google, Roku, Amazon.
* `DeviceComputer`
* `DevicePhone`
* `DeviceTablet`
* `DeviceTV`
* `DeviceConsole`
* `DeviceWearable`
* `DeviceUnknown`
## Example Combinations of Attributes
* Surface RT -> `OSWindows8`, `DeviceTablet`, OSVersion >= `6`
* Android Tablet -> `OSAndroid`, `DeviceTablet`
* Microsoft Edge -> `BrowserIE`, BrowserVersion >= `12.0.0`
## To do
* Remove compiled regexp in favor of string.Contains wherever possible (lowers mem/alloc)
* Better version support on Firefox derivatives (e.g. SeaMonkey)
* Potential additional browser support:
* "NetFront" (1% share in India)
* "QQ Browser" (6.5% share in China)
* "Sogou Explorer" (5% share in China)
* "Maxthon" (1.5% share in China)
* "Nokia"
* Potential additional OS support:
* "Nokia" (5% share in India)
* "Series 40" (5.5% share in India)
* Windows 2003 Server
* iOS safari browser identification based on iOS version
* Add android version to browser identification
* old Macs
* "opera/9.64 (macintosh; ppc mac os x; u; en) presto/2.1.1"
* old Windows
* "mozilla/5.0 (windows nt 4.0; wow64) applewebkit/537.36 (khtml, like gecko) chrome/37.0.2049.0 safari/537.36"

View File

@@ -1,192 +0,0 @@
package uasurfer
import (
"strings"
)
// Browser struct contains the lowercase name of the browser, along
// with its browser version number. Browser are grouped together without
// consideration for device. For example, Chrome (Chrome/43.0) and Chrome for iOS
// (CriOS/43.0) would both return as "chrome" (name) and 43.0 (version). Similarly
// Internet Explorer 11 and Edge 12 would return as "ie" and "11" or "12", respectively.
// type Browser struct {
// Name BrowserName
// Version struct {
// Major int
// Minor int
// Patch int
// }
// }
// Retrieve browser name from UA strings
func (u *UserAgent) evalBrowserName(ua string) bool {
// Blackberry goes first because it reads as MSIE & Safari
if strings.Contains(ua, "blackberry") || strings.Contains(ua, "playbook") || strings.Contains(ua, "bb10") || strings.Contains(ua, "rim ") {
u.Browser.Name = BrowserBlackberry
return u.isBot()
}
if strings.Contains(ua, "applewebkit") {
switch {
case strings.Contains(ua, "opr/") || strings.Contains(ua, "opios/"):
u.Browser.Name = BrowserOpera
case strings.Contains(ua, "silk/"):
u.Browser.Name = BrowserSilk
case strings.Contains(ua, "edge/") || strings.Contains(ua, "iemobile/") || strings.Contains(ua, "msie "):
u.Browser.Name = BrowserIE
case strings.Contains(ua, "ucbrowser/") || strings.Contains(ua, "ucweb/"):
u.Browser.Name = BrowserUCBrowser
// Edge, Silk and other chrome-identifying browsers must evaluate before chrome, unless we want to add more overhead
case strings.Contains(ua, "chrome/") || strings.Contains(ua, "crios/") || strings.Contains(ua, "chromium/") || strings.Contains(ua, "crmo/"):
u.Browser.Name = BrowserChrome
case strings.Contains(ua, "android") && !strings.Contains(ua, "chrome/") && strings.Contains(ua, "version/") && !strings.Contains(ua, "like android"):
// Android WebView on Android >= 4.4 is purposefully being identified as Chrome above -- https://developer.chrome.com/multidevice/webview/overview
u.Browser.Name = BrowserAndroid
case strings.Contains(ua, "fxios"):
u.Browser.Name = BrowserFirefox
case strings.Contains(ua, " spotify/"):
u.Browser.Name = BrowserSpotify
// AppleBot uses webkit signature as well
case strings.Contains(ua, "applebot"):
u.Browser.Name = BrowserAppleBot
// presume it's safari unless an esoteric browser is being specified (webOSBrowser, SamsungBrowser, etc.)
case strings.Contains(ua, "like gecko") && strings.Contains(ua, "mozilla/") && strings.Contains(ua, "safari/") && !strings.Contains(ua, "linux") && !strings.Contains(ua, "android") && !strings.Contains(ua, "browser/") && !strings.Contains(ua, "os/"):
u.Browser.Name = BrowserSafari
// if we got this far and the device is iPhone or iPad, assume safari. Some agents don't actually contain the word "safari"
case strings.Contains(ua, "iphone") || strings.Contains(ua, "ipad"):
u.Browser.Name = BrowserSafari
// Google's search app on iPhone, leverages native Safari rather than Chrome
case strings.Contains(ua, " gsa/"):
u.Browser.Name = BrowserSafari
default:
goto notwebkit
}
return u.isBot()
}
notwebkit:
switch {
case strings.Contains(ua, "msie") || strings.Contains(ua, "trident"):
u.Browser.Name = BrowserIE
case strings.Contains(ua, "gecko") && (strings.Contains(ua, "firefox") || strings.Contains(ua, "iceweasel") || strings.Contains(ua, "seamonkey") || strings.Contains(ua, "icecat")):
u.Browser.Name = BrowserFirefox
case strings.Contains(ua, "presto") || strings.Contains(ua, "opera"):
u.Browser.Name = BrowserOpera
case strings.Contains(ua, "ucbrowser"):
u.Browser.Name = BrowserUCBrowser
case strings.Contains(ua, "applebot"):
u.Browser.Name = BrowserAppleBot
case strings.Contains(ua, "baiduspider"):
u.Browser.Name = BrowserBaiduBot
case strings.Contains(ua, "adidxbot") || strings.Contains(ua, "bingbot") || strings.Contains(ua, "bingpreview"):
u.Browser.Name = BrowserBingBot
case strings.Contains(ua, "duckduckbot"):
u.Browser.Name = BrowserDuckDuckGoBot
case strings.Contains(ua, "facebot") || strings.Contains(ua, "facebookexternalhit"):
u.Browser.Name = BrowserFacebookBot
case strings.Contains(ua, "googlebot"):
u.Browser.Name = BrowserGoogleBot
case strings.Contains(ua, "linkedinbot"):
u.Browser.Name = BrowserLinkedInBot
case strings.Contains(ua, "msnbot"):
u.Browser.Name = BrowserMsnBot
case strings.Contains(ua, "pingdom.com_bot"):
u.Browser.Name = BrowserPingdomBot
case strings.Contains(ua, "twitterbot"):
u.Browser.Name = BrowserTwitterBot
case strings.Contains(ua, "yandex") || strings.Contains(ua, "yadirectfetcher"):
u.Browser.Name = BrowserYandexBot
case strings.Contains(ua, "yahoo"):
u.Browser.Name = BrowserYahooBot
case strings.Contains(ua, "phantomjs"):
u.Browser.Name = BrowserBot
default:
u.Browser.Name = BrowserUnknown
}
return u.isBot()
}
// Retrieve browser version
// Methods used in order:
// 1st: look for generic version/#
// 2nd: look for browser-specific instructions (e.g. chrome/34)
// 3rd: infer from OS (iOS only)
func (u *UserAgent) evalBrowserVersion(ua string) {
// if there is a 'version/#' attribute with numeric version, use it -- except for Chrome since Android vendors sometimes hijack version/#
if u.Browser.Name != BrowserChrome && u.Browser.Version.findVersionNumber(ua, "version/") {
return
}
switch u.Browser.Name {
case BrowserChrome:
// match both chrome and crios
_ = u.Browser.Version.findVersionNumber(ua, "chrome/") || u.Browser.Version.findVersionNumber(ua, "crios/") || u.Browser.Version.findVersionNumber(ua, "crmo/")
case BrowserIE:
if u.Browser.Version.findVersionNumber(ua, "msie ") || u.Browser.Version.findVersionNumber(ua, "edge/") {
return
}
// get MSIE version from trident version https://en.wikipedia.org/wiki/Trident_(layout_engine)
if u.Browser.Version.findVersionNumber(ua, "trident/") {
// convert trident versions 3-7 to MSIE version
if (u.Browser.Version.Major >= 3) && (u.Browser.Version.Major <= 7) {
u.Browser.Version.Major += 4
}
}
case BrowserFirefox:
_ = u.Browser.Version.findVersionNumber(ua, "firefox/") || u.Browser.Version.findVersionNumber(ua, "fxios/")
case BrowserSafari: // executes typically if we're on iOS and not using a familiar browser
u.Browser.Version = u.OS.Version
// early Safari used a version number +1 to OS version
if (u.Browser.Version.Major <= 3) && (u.Browser.Version.Major >= 1) {
u.Browser.Version.Major++
}
case BrowserUCBrowser:
_ = u.Browser.Version.findVersionNumber(ua, "ucbrowser/")
case BrowserOpera:
_ = u.Browser.Version.findVersionNumber(ua, "opr/") || u.Browser.Version.findVersionNumber(ua, "opios/") || u.Browser.Version.findVersionNumber(ua, "opera/")
case BrowserSilk:
_ = u.Browser.Version.findVersionNumber(ua, "silk/")
case BrowserSpotify:
_ = u.Browser.Version.findVersionNumber(ua, "spotify/")
}
}

View File

@@ -1,49 +0,0 @@
// Code generated by "stringer -type=DeviceType,BrowserName,OSName,Platform -output=const_string.go"; DO NOT EDIT.
package uasurfer
import "fmt"
const _DeviceType_name = "DeviceUnknownDeviceComputerDeviceTabletDevicePhoneDeviceConsoleDeviceWearableDeviceTV"
var _DeviceType_index = [...]uint8{0, 13, 27, 39, 50, 63, 77, 85}
func (i DeviceType) String() string {
if i < 0 || i >= DeviceType(len(_DeviceType_index)-1) {
return fmt.Sprintf("DeviceType(%d)", i)
}
return _DeviceType_name[_DeviceType_index[i]:_DeviceType_index[i+1]]
}
const _BrowserName_name = "BrowserUnknownBrowserChromeBrowserIEBrowserSafariBrowserFirefoxBrowserAndroidBrowserOperaBrowserBlackberryBrowserUCBrowserBrowserSilkBrowserNokiaBrowserNetFrontBrowserQQBrowserMaxthonBrowserSogouExplorerBrowserSpotifyBrowserBotBrowserAppleBotBrowserBaiduBotBrowserBingBotBrowserDuckDuckGoBotBrowserFacebookBotBrowserGoogleBotBrowserLinkedInBotBrowserMsnBotBrowserPingdomBotBrowserTwitterBotBrowserYandexBotBrowserYahooBot"
var _BrowserName_index = [...]uint16{0, 14, 27, 36, 49, 63, 77, 89, 106, 122, 133, 145, 160, 169, 183, 203, 217, 227, 242, 257, 271, 291, 309, 325, 343, 356, 373, 390, 406, 421}
func (i BrowserName) String() string {
if i < 0 || i >= BrowserName(len(_BrowserName_index)-1) {
return fmt.Sprintf("BrowserName(%d)", i)
}
return _BrowserName_name[_BrowserName_index[i]:_BrowserName_index[i+1]]
}
const _OSName_name = "OSUnknownOSWindowsPhoneOSWindowsOSMacOSXOSiOSOSAndroidOSBlackberryOSChromeOSOSKindleOSWebOSOSLinuxOSPlaystationOSXboxOSNintendoOSBot"
var _OSName_index = [...]uint8{0, 9, 23, 32, 40, 45, 54, 66, 76, 84, 91, 98, 111, 117, 127, 132}
func (i OSName) String() string {
if i < 0 || i >= OSName(len(_OSName_index)-1) {
return fmt.Sprintf("OSName(%d)", i)
}
return _OSName_name[_OSName_index[i]:_OSName_index[i+1]]
}
const _Platform_name = "PlatformUnknownPlatformWindowsPlatformMacPlatformLinuxPlatformiPadPlatformiPhonePlatformiPodPlatformBlackberryPlatformWindowsPhonePlatformPlaystationPlatformXboxPlatformNintendoPlatformBot"
var _Platform_index = [...]uint8{0, 15, 30, 41, 54, 66, 80, 92, 110, 130, 149, 161, 177, 188}
func (i Platform) String() string {
if i < 0 || i >= Platform(len(_Platform_index)-1) {
return fmt.Sprintf("Platform(%d)", i)
}
return _Platform_name[_Platform_index[i]:_Platform_index[i+1]]
}

View File

@@ -1,60 +0,0 @@
package uasurfer
import (
"strings"
)
func (u *UserAgent) evalDevice(ua string) {
switch {
case u.OS.Platform == PlatformWindows || u.OS.Platform == PlatformMac || u.OS.Name == OSChromeOS:
if strings.Contains(ua, "mobile") || strings.Contains(ua, "touch") {
u.DeviceType = DeviceTablet // windows rt, linux haxor tablets
return
}
u.DeviceType = DeviceComputer
case u.OS.Platform == PlatformiPad || u.OS.Platform == PlatformiPod || strings.Contains(ua, "tablet") || strings.Contains(ua, "kindle/") || strings.Contains(ua, "playbook"):
u.DeviceType = DeviceTablet
case u.OS.Platform == PlatformiPhone || u.OS.Platform == PlatformBlackberry || strings.Contains(ua, "phone"):
u.DeviceType = DevicePhone
// long list of smarttv and tv dongle identifiers
case strings.Contains(ua, "tv") || strings.Contains(ua, "crkey") || strings.Contains(ua, "googletv") || strings.Contains(ua, "aftb") || strings.Contains(ua, "adt-") || strings.Contains(ua, "roku") || strings.Contains(ua, "viera") || strings.Contains(ua, "aquos") || strings.Contains(ua, "dtv") || strings.Contains(ua, "appletv") || strings.Contains(ua, "smarttv") || strings.Contains(ua, "tuner") || strings.Contains(ua, "smart-tv") || strings.Contains(ua, "hbbtv") || strings.Contains(ua, "netcast") || strings.Contains(ua, "vizio"):
u.DeviceType = DeviceTV
case u.OS.Name == OSAndroid:
// android phones report as "mobile", android tablets should not but often do -- http://android-developers.blogspot.com/2010/12/android-browser-user-agent-issues.html
if strings.Contains(ua, "mobile") {
u.DeviceType = DevicePhone
return
}
if strings.Contains(ua, "tablet") || strings.Contains(ua, "nexus 7") || strings.Contains(ua, "nexus 9") || strings.Contains(ua, "nexus 10") || strings.Contains(ua, "xoom") {
u.DeviceType = DeviceTablet
return
}
u.DeviceType = DevicePhone // default to phone
case u.OS.Platform == PlatformPlaystation || u.OS.Platform == PlatformXbox || u.OS.Platform == PlatformNintendo:
u.DeviceType = DeviceConsole
case strings.Contains(ua, "glass") || strings.Contains(ua, "watch") || strings.Contains(ua, "sm-v"):
u.DeviceType = DeviceWearable
// specifically above "mobile" string check as Kindle Fire tablets report as "mobile"
case u.Browser.Name == BrowserSilk || u.OS.Name == OSKindle && !strings.Contains(ua, "sd4930ur"):
u.DeviceType = DeviceTablet
case strings.Contains(ua, "mobile") || strings.Contains(ua, "touch") || strings.Contains(ua, " mobi") || strings.Contains(ua, "webos"): //anything "mobile"/"touch" that didn't get captured as tablet, console or wearable is presumed a phone
u.DeviceType = DevicePhone
case u.OS.Name == OSLinux: // linux goes last since it's in so many other device types (tvs, wearables, android-based stuff)
u.DeviceType = DeviceComputer
default:
u.DeviceType = DeviceUnknown
}
}

View File

@@ -1,332 +0,0 @@
package uasurfer
import (
"regexp"
"strconv"
"strings"
)
var (
amazonFireFingerprint = regexp.MustCompile("\\s(k[a-z]{3,5}|sd\\d{4}ur)\\s") //tablet or phone
)
func (u *UserAgent) evalOS(ua string) bool {
s := strings.IndexRune(ua, '(')
e := strings.IndexRune(ua, ')')
if s > e {
s = 0
e = len(ua)
}
if e == -1 {
e = len(ua)
}
agentPlatform := ua[s+1 : e]
specsEnd := strings.Index(agentPlatform, ";")
var specs string
if specsEnd != -1 {
specs = agentPlatform[:specsEnd]
} else {
specs = agentPlatform
}
//strict OS & version identification
switch specs {
case "android":
u.evalLinux(ua, agentPlatform)
case "bb10", "playbook":
u.OS.Platform = PlatformBlackberry
u.OS.Name = OSBlackberry
case "x11", "linux":
u.evalLinux(ua, agentPlatform)
case "ipad", "iphone", "ipod touch", "ipod":
u.evaliOS(specs, agentPlatform)
case "macintosh":
u.evalMacintosh(ua)
default:
switch {
// Blackberry
case strings.Contains(ua, "blackberry") || strings.Contains(ua, "playbook"):
u.OS.Platform = PlatformBlackberry
u.OS.Name = OSBlackberry
// Windows Phone
case strings.Contains(agentPlatform, "windows phone "):
u.evalWindowsPhone(agentPlatform)
// Windows, Xbox
case strings.Contains(ua, "windows "):
u.evalWindows(ua)
// Kindle
case strings.Contains(ua, "kindle/") || amazonFireFingerprint.MatchString(agentPlatform):
u.OS.Platform = PlatformLinux
u.OS.Name = OSKindle
// Linux (broader attempt)
case strings.Contains(ua, "linux"):
u.evalLinux(ua, agentPlatform)
// WebOS (non-linux flagged)
case strings.Contains(ua, "webos") || strings.Contains(ua, "hpwos"):
u.OS.Platform = PlatformLinux
u.OS.Name = OSWebOS
// Nintendo
case strings.Contains(ua, "nintendo"):
u.OS.Platform = PlatformNintendo
u.OS.Name = OSNintendo
// Playstation
case strings.Contains(ua, "playstation") || strings.Contains(ua, "vita") || strings.Contains(ua, "psp"):
u.OS.Platform = PlatformPlaystation
u.OS.Name = OSPlaystation
// Android
case strings.Contains(ua, "android"):
u.evalLinux(ua, agentPlatform)
default:
u.OS.Platform = PlatformUnknown
u.OS.Name = OSUnknown
}
}
return u.isBot()
}
func (u *UserAgent) isBot() bool {
if u.OS.Platform == PlatformBot || u.OS.Name == OSBot {
u.DeviceType = DeviceComputer
return true
}
if u.Browser.Name >= BrowserBot && u.Browser.Name <= BrowserYahooBot {
u.OS.Platform = PlatformBot
u.OS.Name = OSBot
u.DeviceType = DeviceComputer
return true
}
return false
}
// evalLinux returns the `Platform`, `OSName` and Version of UAs with
// 'linux' listed as their platform.
func (u *UserAgent) evalLinux(ua string, agentPlatform string) {
switch {
// Kindle Fire
case strings.Contains(ua, "kindle") || amazonFireFingerprint.MatchString(agentPlatform):
// get the version of Android if available, though we don't call this OSAndroid
u.OS.Platform = PlatformLinux
u.OS.Name = OSKindle
u.OS.Version.findVersionNumber(agentPlatform, "android ")
// Android, Kindle Fire
case strings.Contains(ua, "android") || strings.Contains(ua, "googletv"):
// Android
u.OS.Platform = PlatformLinux
u.OS.Name = OSAndroid
u.OS.Version.findVersionNumber(agentPlatform, "android ")
// ChromeOS
case strings.Contains(ua, "cros"):
u.OS.Platform = PlatformLinux
u.OS.Name = OSChromeOS
// WebOS
case strings.Contains(ua, "webos") || strings.Contains(ua, "hpwos"):
u.OS.Platform = PlatformLinux
u.OS.Name = OSWebOS
// Linux, "Linux-like"
case strings.Contains(ua, "x11") || strings.Contains(ua, "bsd") || strings.Contains(ua, "suse") || strings.Contains(ua, "debian") || strings.Contains(ua, "ubuntu"):
u.OS.Platform = PlatformLinux
u.OS.Name = OSLinux
default:
u.OS.Platform = PlatformLinux
u.OS.Name = OSLinux
}
}
// evaliOS returns the `Platform`, `OSName` and Version of UAs with
// 'ipad' or 'iphone' listed as their platform.
func (u *UserAgent) evaliOS(uaPlatform string, agentPlatform string) {
switch uaPlatform {
// iPhone
case "iphone":
u.OS.Platform = PlatformiPhone
u.OS.Name = OSiOS
u.OS.getiOSVersion(agentPlatform)
// iPad
case "ipad":
u.OS.Platform = PlatformiPad
u.OS.Name = OSiOS
u.OS.getiOSVersion(agentPlatform)
// iPod
case "ipod touch", "ipod":
u.OS.Platform = PlatformiPod
u.OS.Name = OSiOS
u.OS.getiOSVersion(agentPlatform)
default:
u.OS.Platform = PlatformiPad
u.OS.Name = OSUnknown
}
}
func (u *UserAgent) evalWindowsPhone(agentPlatform string) {
u.OS.Platform = PlatformWindowsPhone
if u.OS.Version.findVersionNumber(agentPlatform, "windows phone os ") || u.OS.Version.findVersionNumber(agentPlatform, "windows phone ") {
u.OS.Name = OSWindowsPhone
} else {
u.OS.Name = OSUnknown
}
}
func (u *UserAgent) evalWindows(ua string) {
switch {
//Xbox -- it reads just like Windows
case strings.Contains(ua, "xbox"):
u.OS.Platform = PlatformXbox
u.OS.Name = OSXbox
if !u.OS.Version.findVersionNumber(ua, "windows nt ") {
u.OS.Version.Major = 6
u.OS.Version.Minor = 0
u.OS.Version.Patch = 0
}
// No windows version
case !strings.Contains(ua, "windows "):
u.OS.Platform = PlatformWindows
u.OS.Name = OSUnknown
case strings.Contains(ua, "windows nt ") && u.OS.Version.findVersionNumber(ua, "windows nt "):
u.OS.Platform = PlatformWindows
u.OS.Name = OSWindows
case strings.Contains(ua, "windows xp"):
u.OS.Platform = PlatformWindows
u.OS.Name = OSWindows
u.OS.Version.Major = 5
u.OS.Version.Minor = 1
u.OS.Version.Patch = 0
default:
u.OS.Platform = PlatformWindows
u.OS.Name = OSUnknown
}
}
func (u *UserAgent) evalMacintosh(uaPlatformGroup string) {
u.OS.Platform = PlatformMac
if i := strings.Index(uaPlatformGroup, "os x 10"); i != -1 {
u.OS.Name = OSMacOSX
u.OS.Version.parse(uaPlatformGroup[i+5:])
return
}
u.OS.Name = OSUnknown
}
func (v *Version) findVersionNumber(s string, m string) bool {
if ind := strings.Index(s, m); ind != -1 {
return v.parse(s[ind+len(m):])
}
return false
}
// getiOSVersion accepts the platform portion of a UA string and returns
// a Version.
func (o *OS) getiOSVersion(uaPlatformGroup string) {
if i := strings.Index(uaPlatformGroup, "cpu iphone os "); i != -1 {
o.Version.parse(uaPlatformGroup[i+14:])
return
}
if i := strings.Index(uaPlatformGroup, "cpu os "); i != -1 {
o.Version.parse(uaPlatformGroup[i+7:])
return
}
o.Version.parse(uaPlatformGroup)
}
// strToInt simply accepts a string and returns a `int`,
// with '0' being default.
func strToInt(str string) int {
i, _ := strconv.Atoi(str)
return i
}
// strToVer accepts a string and returns a Version,
// with {0, 0, 0} being default.
func (v *Version) parse(str string) bool {
if len(str) == 0 || str[0] < '0' || str[0] > '9' {
return false
}
for i := 0; i < 3; i++ {
empty := true
val := 0
l := len(str) - 1
for k, c := range str {
if c >= '0' && c <= '9' {
if empty {
val = int(c) - 48
empty = false
if k == l {
str = str[:0]
}
continue
}
if val == 0 {
if c == '0' {
if k == l {
str = str[:0]
}
continue
}
str = str[k:]
break
}
val = 10*val + int(c) - 48
if k == l {
str = str[:0]
}
continue
}
str = str[k+1:]
break
}
switch i {
case 0:
v.Major = val
case 1:
v.Minor = val
case 2:
v.Patch = val
}
}
return true
}

View File

@@ -1,227 +0,0 @@
// Package uasurfer provides fast and reliable abstraction
// of HTTP User-Agent strings. The philosophy is to identify
// technologies that holds >1% market share, and to avoid
// expending resources and accuracy on guessing at esoteric UA
// strings.
package uasurfer
import "strings"
//go:generate stringer -type=DeviceType,BrowserName,OSName,Platform -output=const_string.go
// DeviceType (int) returns a constant.
type DeviceType int
// A complete list of supported devices in the
// form of constants.
const (
DeviceUnknown DeviceType = iota
DeviceComputer
DeviceTablet
DevicePhone
DeviceConsole
DeviceWearable
DeviceTV
)
// BrowserName (int) returns a constant.
type BrowserName int
// A complete list of supported web browsers in the
// form of constants.
const (
BrowserUnknown BrowserName = iota
BrowserChrome
BrowserIE
BrowserSafari
BrowserFirefox
BrowserAndroid
BrowserOpera
BrowserBlackberry
BrowserUCBrowser
BrowserSilk
BrowserNokia
BrowserNetFront
BrowserQQ
BrowserMaxthon
BrowserSogouExplorer
BrowserSpotify
BrowserBot // Bot list begins here
BrowserAppleBot
BrowserBaiduBot
BrowserBingBot
BrowserDuckDuckGoBot
BrowserFacebookBot
BrowserGoogleBot
BrowserLinkedInBot
BrowserMsnBot
BrowserPingdomBot
BrowserTwitterBot
BrowserYandexBot
BrowserYahooBot // Bot list ends here
)
// OSName (int) returns a constant.
type OSName int
// A complete list of supported OSes in the
// form of constants. For handling particular versions
// of operating systems (e.g. Windows 2000), see
// the README.md file.
const (
OSUnknown OSName = iota
OSWindowsPhone
OSWindows
OSMacOSX
OSiOS
OSAndroid
OSBlackberry
OSChromeOS
OSKindle
OSWebOS
OSLinux
OSPlaystation
OSXbox
OSNintendo
OSBot
)
// Platform (int) returns a constant.
type Platform int
// A complete list of supported platforms in the
// form of constants. Many OSes report their
// true platform, such as Android OS being Linux
// platform.
const (
PlatformUnknown Platform = iota
PlatformWindows
PlatformMac
PlatformLinux
PlatformiPad
PlatformiPhone
PlatformiPod
PlatformBlackberry
PlatformWindowsPhone
PlatformPlaystation
PlatformXbox
PlatformNintendo
PlatformBot
)
type Version struct {
Major int
Minor int
Patch int
}
func (v Version) Less(c Version) bool {
if v.Major < c.Major {
return true
}
if v.Major > c.Major {
return false
}
if v.Minor < c.Minor {
return true
}
if v.Minor > c.Minor {
return false
}
return v.Patch < c.Patch
}
type UserAgent struct {
Browser Browser
OS OS
DeviceType DeviceType
}
type Browser struct {
Name BrowserName
Version Version
}
type OS struct {
Platform Platform
Name OSName
Version Version
}
// Reset resets the UserAgent to it's zero value
func (ua *UserAgent) Reset() {
ua.Browser = Browser{}
ua.OS = OS{}
ua.DeviceType = DeviceUnknown
}
// Parse accepts a raw user agent (string) and returns the UserAgent.
func Parse(ua string) *UserAgent {
dest := new(UserAgent)
parse(ua, dest)
return dest
}
// ParseUserAgent is the same as Parse, but populates the supplied UserAgent.
// It is the caller's responsibility to call Reset() on the UserAgent before
// passing it to this function.
func ParseUserAgent(ua string, dest *UserAgent) {
parse(ua, dest)
}
func parse(ua string, dest *UserAgent) {
ua = normalise(ua)
switch {
case len(ua) == 0:
dest.OS.Platform = PlatformUnknown
dest.OS.Name = OSUnknown
dest.Browser.Name = BrowserUnknown
dest.DeviceType = DeviceUnknown
// stop on on first case returning true
case dest.evalOS(ua):
case dest.evalBrowserName(ua):
default:
dest.evalBrowserVersion(ua)
dest.evalDevice(ua)
}
}
// normalise normalises the user supplied agent string so that
// we can more easily parse it.
func normalise(ua string) string {
if len(ua) <= 1024 {
var buf [1024]byte
ascii := copyLower(buf[:len(ua)], ua)
if !ascii {
// Fall back for non ascii characters
return strings.ToLower(ua)
}
return string(buf[:len(ua)])
}
// Fallback for unusually long strings
return strings.ToLower(ua)
}
// copyLower copies a lowercase version of s to b. It assumes s contains only single byte characters
// and will panic if b is nil or is not long enough to contain all the bytes from s.
// It returns early with false if any characters were non ascii.
func copyLower(b []byte, s string) bool {
for j := 0; j < len(s); j++ {
c := s[j]
if c > 127 {
return false
}
if 'A' <= c && c <= 'Z' {
c += 'a' - 'A'
}
b[j] = c
}
return true
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
language: go
go:
- tip
- 1.8
- 1.7
- 1.6
- 1.5
- 1.4
- 1.3
- 1.2
notifications:
email:
on_success: change
on_failure: always

View File

@@ -1,108 +0,0 @@
# html2text
[![Documentation](https://godoc.org/github.com/cpanato/html2text?status.svg)](https://godoc.org/github.com/cpanato/html2text)
[![Build Status](https://travis-ci.org/cpanato/html2text.svg?branch=master)](https://travis-ci.org/cpanato/html2text)
[![Report Card](https://goreportcard.com/badge/github.com/jaytaylor/html2text)](https://goreportcard.com/report/github.com/cpanato/html2text)
### Initial information
This project was forked from [github.com/jaytaylor/html2text](https://github.com/jaytaylor/html2text) in order to use another clean bom library due the original one has no license.
### Converts HTML into text
## Introduction
Ensure your emails are readable by all!
Turns HTML into raw text, useful for sending fancy HTML emails with a equivalently nicely formatted TXT document as a fallback (e.g. for people who don't allow HTML emails or have other display issues).
html2text is a simple golang package for rendering HTML into plaintext.
There are still lots of improvements to be had, but FWIW this has worked fine for my [basic] HTML-2-text needs.
It requires go 1.x or newer ;)
## Download the package
```bash
go get github.com/cpanato/html2text
```
## Example usage
```go
package main
import (
"fmt"
"github.com/cpanato/html2text"
)
func main() {
inputHtml := `
<html>
<head>
<title>My Mega Service</title>
<link rel=\"stylesheet\" href=\"main.css\">
<style type=\"text/css\">body { color: #fff; }</style>
</head>
<body>
<div class="logo">
<a href="http://mymegaservice.com/"><img src="/logo-image.jpg" alt="Mega Service"/></a>
</div>
<h1>Welcome to your new account on my service!</h1>
<p>
Here is some more information:
<ul>
<li>Link 1: <a href="https://example.com">Example.com</a></li>
<li>Link 2: <a href="https://example2.com">Example2.com</a></li>
<li>Something else</li>
</ul>
</p>
</body>
</html>
`
text, err := html2text.FromString(inputHtml)
if err != nil {
panic(err)
}
fmt.Println(text)
}
```
Output:
```
Mega Service ( http://mymegaservice.com/ )
******************************************
Welcome to your new account on my service!
******************************************
Here is some more information:
* Link 1: Example.com ( https://example.com )
* Link 2: Example2.com ( https://example2.com )
* Something else
```
## Unit-tests
Running the unit-tests is straightforward and standard:
```bash
go test
```
# License
Permissive MIT license.

View File

@@ -1,312 +0,0 @@
package html2text
import (
"bytes"
"io"
"io/ioutil"
"regexp"
"strings"
"unicode"
"github.com/dimchansky/utfbom"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
var (
spacingRe = regexp.MustCompile(`[ \r\n\t]+`)
newlineRe = regexp.MustCompile(`\n\n+`)
)
type textifyTraverseCtx struct {
Buf bytes.Buffer
prefix string
blockquoteLevel int
lineLength int
endsWithSpace bool
endsWithNewline bool
justClosedDiv bool
}
func (ctx *textifyTraverseCtx) traverse(node *html.Node) error {
switch node.Type {
default:
return ctx.traverseChildren(node)
case html.TextNode:
data := strings.Trim(spacingRe.ReplaceAllString(node.Data, " "), " ")
return ctx.emit(data)
case html.ElementNode:
return ctx.handleElementNode(node)
}
}
func (ctx *textifyTraverseCtx) handleElementNode(node *html.Node) error {
ctx.justClosedDiv = false
switch node.DataAtom {
case atom.Br:
return ctx.emit("\n")
case atom.H1, atom.H2, atom.H3:
subCtx := textifyTraverseCtx{}
if err := subCtx.traverseChildren(node); err != nil {
return err
}
str := subCtx.Buf.String()
dividerLen := 0
for _, line := range strings.Split(str, "\n") {
if lineLen := len([]rune(line)); lineLen-1 > dividerLen {
dividerLen = lineLen - 1
}
}
divider := ""
if node.DataAtom == atom.H1 {
divider = strings.Repeat("*", dividerLen)
} else {
divider = strings.Repeat("-", dividerLen)
}
if node.DataAtom == atom.H3 {
return ctx.emit("\n\n" + str + "\n" + divider + "\n\n")
}
return ctx.emit("\n\n" + divider + "\n" + str + "\n" + divider + "\n\n")
case atom.Blockquote:
ctx.blockquoteLevel++
ctx.prefix = strings.Repeat(">", ctx.blockquoteLevel) + " "
if err := ctx.emit("\n"); err != nil {
return err
}
if ctx.blockquoteLevel == 1 {
if err := ctx.emit("\n"); err != nil {
return err
}
}
if err := ctx.traverseChildren(node); err != nil {
return err
}
ctx.blockquoteLevel--
ctx.prefix = strings.Repeat(">", ctx.blockquoteLevel)
if ctx.blockquoteLevel > 0 {
ctx.prefix += " "
}
return ctx.emit("\n\n")
case atom.Div:
if ctx.lineLength > 0 {
if err := ctx.emit("\n"); err != nil {
return err
}
}
if err := ctx.traverseChildren(node); err != nil {
return err
}
var err error
if ctx.justClosedDiv == false {
err = ctx.emit("\n")
}
ctx.justClosedDiv = true
return err
case atom.Li:
if err := ctx.emit("* "); err != nil {
return err
}
if err := ctx.traverseChildren(node); err != nil {
return err
}
return ctx.emit("\n")
case atom.B, atom.Strong:
subCtx := textifyTraverseCtx{}
subCtx.endsWithSpace = true
if err := subCtx.traverseChildren(node); err != nil {
return err
}
str := subCtx.Buf.String()
return ctx.emit("*" + str + "*")
case atom.A:
// If image is the only child, take its alt text as the link text
if img := node.FirstChild; img != nil && node.LastChild == img && img.DataAtom == atom.Img {
if altText := getAttrVal(img, "alt"); altText != "" {
ctx.emit(altText)
}
} else if err := ctx.traverseChildren(node); err != nil {
return err
}
hrefLink := ""
if attrVal := getAttrVal(node, "href"); attrVal != "" {
attrVal = ctx.normalizeHrefLink(attrVal)
if attrVal != "" {
hrefLink = "( " + attrVal + " )"
}
}
return ctx.emit(hrefLink)
case atom.P, atom.Ul, atom.Table:
if err := ctx.emit("\n\n"); err != nil {
return err
}
if err := ctx.traverseChildren(node); err != nil {
return err
}
return ctx.emit("\n\n")
case atom.Tr:
if err := ctx.traverseChildren(node); err != nil {
return err
}
return ctx.emit("\n")
case atom.Style, atom.Script, atom.Head:
// Ignore the subtree
return nil
default:
return ctx.traverseChildren(node)
}
}
func (ctx *textifyTraverseCtx) traverseChildren(node *html.Node) error {
for c := node.FirstChild; c != nil; c = c.NextSibling {
if err := ctx.traverse(c); err != nil {
return err
}
}
return nil
}
func (ctx *textifyTraverseCtx) emit(data string) error {
if len(data) == 0 {
return nil
}
lines := ctx.breakLongLines(data)
var err error
for _, line := range lines {
runes := []rune(line)
startsWithSpace := unicode.IsSpace(runes[0])
if !startsWithSpace && !ctx.endsWithSpace {
ctx.Buf.WriteByte(' ')
ctx.lineLength++
}
ctx.endsWithSpace = unicode.IsSpace(runes[len(runes)-1])
for _, c := range line {
_, err = ctx.Buf.WriteString(string(c))
if err != nil {
return err
}
ctx.lineLength++
if c == '\n' {
ctx.lineLength = 0
if ctx.prefix != "" {
_, err = ctx.Buf.WriteString(ctx.prefix)
if err != nil {
return err
}
}
}
}
}
return nil
}
func (ctx *textifyTraverseCtx) breakLongLines(data string) []string {
// only break lines when we are in blockquotes
if ctx.blockquoteLevel == 0 {
return []string{data}
}
var ret []string
runes := []rune(data)
l := len(runes)
existing := ctx.lineLength
if existing >= 74 {
ret = append(ret, "\n")
existing = 0
}
for l+existing > 74 {
i := 74 - existing
for i >= 0 && !unicode.IsSpace(runes[i]) {
i--
}
if i == -1 {
// no spaces, so go the other way
i = 74 - existing
for i < l && !unicode.IsSpace(runes[i]) {
i++
}
}
ret = append(ret, string(runes[:i])+"\n")
for i < l && unicode.IsSpace(runes[i]) {
i++
}
runes = runes[i:]
l = len(runes)
existing = 0
}
if len(runes) > 0 {
ret = append(ret, string(runes))
}
return ret
}
func (ctx *textifyTraverseCtx) normalizeHrefLink(link string) string {
link = strings.TrimSpace(link)
link = strings.TrimPrefix(link, "mailto:")
return link
}
func getAttrVal(node *html.Node, attrName string) string {
for _, attr := range node.Attr {
if attr.Key == attrName {
return attr.Val
}
}
return ""
}
func FromHtmlNode(doc *html.Node) (string, error) {
ctx := textifyTraverseCtx{
Buf: bytes.Buffer{},
}
if err := ctx.traverse(doc); err != nil {
return "", err
}
text := strings.TrimSpace(newlineRe.ReplaceAllString(
strings.Replace(ctx.Buf.String(), "\n ", "\n", -1), "\n\n"))
return text, nil
}
func FromReader(reader io.Reader) (string, error) {
bs, err := ioutil.ReadAll(reader)
newReader, _ := utfbom.Skip(bytes.NewReader(bs))
doc, err := html.Parse(newReader)
if err != nil {
return "", err
}
return FromHtmlNode(doc)
}
func FromString(input string) (string, error) {
bs := utfbom.SkipOnly(bytes.NewReader([]byte(input)))
text, err := FromReader(bs)
if err != nil {
return "", err
}
return text, nil
}

View File

@@ -1,674 +0,0 @@
package html2text
import (
"bytes"
"fmt"
"io/ioutil"
"path"
"regexp"
"strings"
"testing"
)
const (
destPath = "testdata"
)
func TestParseUTF8(t *testing.T) {
htmlFiles := []struct {
file string
keywordShouldNotExist string
keywordShouldExist string
}{
{
"utf8.html",
"学习之道:美国公认学习第一书title",
"次世界冠军赛上,我几近疯狂",
},
{
"utf8_with_bom.xhtml",
"1892年波兰文版序言title",
"种新的波兰文本已成为必要",
},
}
for _, htmlFile := range htmlFiles {
bs, err := ioutil.ReadFile(path.Join(destPath, htmlFile.file))
if err != nil {
t.Fatal(err)
}
text, err := FromReader(bytes.NewReader(bs))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(text, htmlFile.keywordShouldExist) {
t.Fatalf("keyword %s should exists in file %s", htmlFile.keywordShouldExist, htmlFile.file)
}
if strings.Contains(text, htmlFile.keywordShouldNotExist) {
t.Fatalf("keyword %s should not exists in file %s", htmlFile.keywordShouldNotExist, htmlFile.file)
}
}
}
func TestStrippingWhitespace(t *testing.T) {
testCases := []struct {
input string
output string
}{
{
"test text",
"test text",
},
{
" \ttext\ntext\n",
"text text",
},
{
" \na \n\t \n \n a \t",
"a a",
},
{
"test text",
"test text",
},
{
"test&nbsp;&nbsp;&nbsp; text&nbsp;",
"test    text",
},
}
for _, testCase := range testCases {
assertString(t, testCase.input, testCase.output)
}
}
func TestParagraphsAndBreaks(t *testing.T) {
testCases := []struct {
input string
output string
}{
{
"Test text",
"Test text",
},
{
"Test text<br>",
"Test text",
},
{
"Test text<br>Test",
"Test text\nTest",
},
{
"<p>Test text</p>",
"Test text",
},
{
"<p>Test text</p><p>Test text</p>",
"Test text\n\nTest text",
},
{
"\n<p>Test text</p>\n\n\n\t<p>Test text</p>\n",
"Test text\n\nTest text",
},
{
"\n<p>Test text<br/>Test text</p>\n",
"Test text\nTest text",
},
{
"\n<p>Test text<br> \tTest text<br></p>\n",
"Test text\nTest text",
},
{
"Test text<br><BR />Test text",
"Test text\n\nTest text",
},
}
for _, testCase := range testCases {
assertString(t, testCase.input, testCase.output)
}
}
func TestTables(t *testing.T) {
testCases := []struct {
input string
output string
}{
{
"<table><tr><td></td><td></td></tr></table>",
"",
},
{
"<table><tr><td>cell1</td><td>cell2</td></tr></table>",
"cell1 cell2",
},
{
"<table><tr><td>row1</td></tr><tr><td>row2</td></tr></table>",
"row1\nrow2",
},
{
`<table>
<tr><td>cell1-1</td><td>cell1-2</td></tr>
<tr><td>cell2-1</td><td>cell2-2</td></tr>
</table>`,
"cell1-1 cell1-2\ncell2-1 cell2-2",
},
{
"_<table><tr><td>cell</td></tr></table>_",
"_\n\ncell\n\n_",
},
}
for _, testCase := range testCases {
assertString(t, testCase.input, testCase.output)
}
}
func TestStrippingLists(t *testing.T) {
testCases := []struct {
input string
output string
}{
{
"<ul></ul>",
"",
},
{
"<ul><li>item</li></ul>_",
"* item\n\n_",
},
{
"<li class='123'>item 1</li> <li>item 2</li>\n_",
"* item 1\n* item 2\n_",
},
{
"<li>item 1</li> \t\n <li>item 2</li> <li> item 3</li>\n_",
"* item 1\n* item 2\n* item 3\n_",
},
}
for _, testCase := range testCases {
assertString(t, testCase.input, testCase.output)
}
}
func TestLinks(t *testing.T) {
testCases := []struct {
input string
output string
}{
{
`<a></a>`,
``,
},
{
`<a href=""></a>`,
``,
},
{
`<a href="http://example.com/"></a>`,
`( http://example.com/ )`,
},
{
`<a href="">Link</a>`,
`Link`,
},
{
`<a href="http://example.com/">Link</a>`,
`Link ( http://example.com/ )`,
},
{
`<a href="http://example.com/"><span class="a">Link</span></a>`,
`Link ( http://example.com/ )`,
},
{
"<a href='http://example.com/'>\n\t<span class='a'>Link</span>\n\t</a>",
`Link ( http://example.com/ )`,
},
{
"<a href='mailto:contact@example.org'>Contact Us</a>",
`Contact Us ( contact@example.org )`,
},
{
"<a href=\"http://example.com:80/~user?aaa=bb&amp;c=d,e,f#foo\">Link</a>",
`Link ( http://example.com:80/~user?aaa=bb&c=d,e,f#foo )`,
},
{
"<a title='title' href=\"http://example.com/\">Link</a>",
`Link ( http://example.com/ )`,
},
{
"<a href=\" http://example.com/ \"> Link </a>",
`Link ( http://example.com/ )`,
},
{
"<a href=\"http://example.com/a/\">Link A</a> <a href=\"http://example.com/b/\">Link B</a>",
`Link A ( http://example.com/a/ ) Link B ( http://example.com/b/ )`,
},
{
"<a href=\"%%LINK%%\">Link</a>",
`Link ( %%LINK%% )`,
},
{
"<a href=\"[LINK]\">Link</a>",
`Link ( [LINK] )`,
},
{
"<a href=\"{LINK}\">Link</a>",
`Link ( {LINK} )`,
},
{
"<a href=\"[[!unsubscribe]]\">Link</a>",
`Link ( [[!unsubscribe]] )`,
},
{
"<p>This is <a href=\"http://www.google.com\" >link1</a> and <a href=\"http://www.google.com\" >link2 </a> is next.</p>",
`This is link1 ( http://www.google.com ) and link2 ( http://www.google.com ) is next.`,
},
}
for _, testCase := range testCases {
assertString(t, testCase.input, testCase.output)
}
}
func TestImageAltTags(t *testing.T) {
testCases := []struct {
input string
output string
}{
{
`<img />`,
``,
},
{
`<img src="http://example.ru/hello.jpg" />`,
``,
},
{
`<img alt="Example"/>`,
``,
},
{
`<img src="http://example.ru/hello.jpg" alt="Example"/>`,
``,
},
// Images do matter if they are in a link
{
`<a href="http://example.com/"><img src="http://example.ru/hello.jpg" alt="Example"/></a>`,
`Example ( http://example.com/ )`,
},
{
`<a href="http://example.com/"><img src="http://example.ru/hello.jpg" alt="Example"></a>`,
`Example ( http://example.com/ )`,
},
{
`<a href='http://example.com/'><img src='http://example.ru/hello.jpg' alt='Example'/></a>`,
`Example ( http://example.com/ )`,
},
{
`<a href='http://example.com/'><img src='http://example.ru/hello.jpg' alt='Example'></a>`,
`Example ( http://example.com/ )`,
},
}
for _, testCase := range testCases {
assertString(t, testCase.input, testCase.output)
}
}
func TestHeadings(t *testing.T) {
testCases := []struct {
input string
output string
}{
{
"<h1>Test</h1>",
"****\nTest\n****",
},
{
"\t<h1>\nTest</h1> ",
"****\nTest\n****",
},
{
"\t<h1>\nTest line 1<br>Test 2</h1> ",
"***********\nTest line 1\nTest 2\n***********",
},
{
"<h1>Test</h1> <h1>Test</h1>",
"****\nTest\n****\n\n****\nTest\n****",
},
{
"<h2>Test</h2>",
"----\nTest\n----",
},
{
"<h1><a href='http://example.com/'>Test</a></h1>",
"****************************\nTest ( http://example.com/ )\n****************************",
},
{
"<h3> <span class='a'>Test </span></h3>",
"Test\n----",
},
}
for _, testCase := range testCases {
assertString(t, testCase.input, testCase.output)
}
}
func TestBold(t *testing.T) {
testCases := []struct {
input string
output string
}{
{
"<b>Test</b>",
"*Test*",
},
{
"\t<b>Test</b> ",
"*Test*",
},
{
"\t<b>Test line 1<br>Test 2</b> ",
"*Test line 1\nTest 2*",
},
{
"<b>Test</b> <b>Test</b>",
"*Test* *Test*",
},
}
for _, testCase := range testCases {
assertString(t, testCase.input, testCase.output)
}
}
func TestDiv(t *testing.T) {
testCases := []struct {
input string
output string
}{
{
"<div>Test</div>",
"Test",
},
{
"\t<div>Test</div> ",
"Test",
},
{
"<div>Test line 1<div>Test 2</div></div>",
"Test line 1\nTest 2",
},
{
"Test 1<div>Test 2</div> <div>Test 3</div>Test 4",
"Test 1\nTest 2\nTest 3\nTest 4",
},
}
for _, testCase := range testCases {
assertString(t, testCase.input, testCase.output)
}
}
func TestBlockquotes(t *testing.T) {
testCases := []struct {
input string
output string
}{
{
"<div>level 0<blockquote>level 1<br><blockquote>level 2</blockquote>level 1</blockquote><div>level 0</div></div>",
"level 0\n> \n> level 1\n> \n>> level 2\n> \n> level 1\n\nlevel 0",
},
{
"<blockquote>Test</blockquote>Test",
"> \n> Test\n\nTest",
},
{
"\t<blockquote> \nTest<br></blockquote> ",
"> \n> Test\n>",
},
{
"\t<blockquote> \nTest line 1<br>Test 2</blockquote> ",
"> \n> Test line 1\n> Test 2",
},
{
"<blockquote>Test</blockquote> <blockquote>Test</blockquote> Other Test",
"> \n> Test\n\n> \n> Test\n\nOther Test",
},
{
"<blockquote>Lorem ipsum Commodo id consectetur pariatur ea occaecat minim aliqua ad sit consequat quis ex commodo Duis incididunt eu mollit consectetur fugiat voluptate dolore in pariatur in commodo occaecat Ut occaecat velit esse labore aute quis commodo non sit dolore officia Excepteur cillum amet cupidatat culpa velit labore ullamco dolore mollit elit in aliqua dolor irure do</blockquote>",
"> \n> Lorem ipsum Commodo id consectetur pariatur ea occaecat minim aliqua ad\n> sit consequat quis ex commodo Duis incididunt eu mollit consectetur fugiat\n> voluptate dolore in pariatur in commodo occaecat Ut occaecat velit esse\n> labore aute quis commodo non sit dolore officia Excepteur cillum amet\n> cupidatat culpa velit labore ullamco dolore mollit elit in aliqua dolor\n> irure do",
},
{
"<blockquote>Lorem<b>ipsum</b><b>Commodo</b><b>id</b><b>consectetur</b><b>pariatur</b><b>ea</b><b>occaecat</b><b>minim</b><b>aliqua</b><b>ad</b><b>sit</b><b>consequat</b><b>quis</b><b>ex</b><b>commodo</b><b>Duis</b><b>incididunt</b><b>eu</b><b>mollit</b><b>consectetur</b><b>fugiat</b><b>voluptate</b><b>dolore</b><b>in</b><b>pariatur</b><b>in</b><b>commodo</b><b>occaecat</b><b>Ut</b><b>occaecat</b><b>velit</b><b>esse</b><b>labore</b><b>aute</b><b>quis</b><b>commodo</b><b>non</b><b>sit</b><b>dolore</b><b>officia</b><b>Excepteur</b><b>cillum</b><b>amet</b><b>cupidatat</b><b>culpa</b><b>velit</b><b>labore</b><b>ullamco</b><b>dolore</b><b>mollit</b><b>elit</b><b>in</b><b>aliqua</b><b>dolor</b><b>irure</b><b>do</b></blockquote>",
"> \n> Lorem *ipsum* *Commodo* *id* *consectetur* *pariatur* *ea* *occaecat* *minim*\n> *aliqua* *ad* *sit* *consequat* *quis* *ex* *commodo* *Duis* *incididunt* *eu*\n> *mollit* *consectetur* *fugiat* *voluptate* *dolore* *in* *pariatur* *in* *commodo*\n> *occaecat* *Ut* *occaecat* *velit* *esse* *labore* *aute* *quis* *commodo*\n> *non* *sit* *dolore* *officia* *Excepteur* *cillum* *amet* *cupidatat* *culpa*\n> *velit* *labore* *ullamco* *dolore* *mollit* *elit* *in* *aliqua* *dolor* *irure*\n> *do*",
},
}
for _, testCase := range testCases {
assertString(t, testCase.input, testCase.output)
}
}
func TestIgnoreStylesScriptsHead(t *testing.T) {
testCases := []struct {
input string
output string
}{
{
"<style>Test</style>",
"",
},
{
"<style type=\"text/css\">body { color: #fff; }</style>",
"",
},
{
"<link rel=\"stylesheet\" href=\"main.css\">",
"",
},
{
"<script>Test</script>",
"",
},
{
"<script src=\"main.js\"></script>",
"",
},
{
"<script type=\"text/javascript\" src=\"main.js\"></script>",
"",
},
{
"<script type=\"text/javascript\">Test</script>",
"",
},
{
"<script type=\"text/ng-template\" id=\"template.html\"><a href=\"http://google.com\">Google</a></script>",
"",
},
{
"<script type=\"bla-bla-bla\" id=\"template.html\">Test</script>",
"",
},
{
`<html><head><title>Title</title></head><body></body></html>`,
"",
},
}
for _, testCase := range testCases {
assertString(t, testCase.input, testCase.output)
}
}
func TestText(t *testing.T) {
testCases := []struct {
input string
expr string
}{
{
`<li>
<a href="/new" data-ga-click="Header, create new repository, icon:repo"><span class="octicon octicon-repo"></span> New repository</a>
</li>`,
`\* New repository \( /new \)`,
},
{
`hi
<br>
hello <a href="https://google.com">google</a>
<br><br>
test<p>List:</p>
<ul>
<li><a href="foo">Foo</a></li>
<li><a href="http://www.microshwhat.com/bar/soapy">Barsoap</a></li>
<li>Baz</li>
</ul>
`,
`hi
hello google \( https://google.com \)
test
List:
\* Foo \( foo \)
\* Barsoap \( http://www.microshwhat.com/bar/soapy \)
\* Baz`,
},
// Malformed input html.
{
`hi
hello <a href="https://google.com">google</a>
test<p>List:</p>
<ul>
<li><a href="foo">Foo</a>
<li><a href="/
bar/baz">Bar</a>
<li>Baz</li>
</ul>
`,
`hi hello google \( https://google.com \) test
List:
\* Foo \( foo \)
\* Bar \( /\n[ \t]+bar/baz \)
\* Baz`,
},
}
for _, testCase := range testCases {
assertRegexp(t, testCase.input, testCase.expr)
}
}
type StringMatcher interface {
MatchString(string) bool
String() string
}
type RegexpStringMatcher string
func (m RegexpStringMatcher) MatchString(str string) bool {
return regexp.MustCompile(string(m)).MatchString(str)
}
func (m RegexpStringMatcher) String() string {
return string(m)
}
type ExactStringMatcher string
func (m ExactStringMatcher) MatchString(str string) bool {
return string(m) == str
}
func (m ExactStringMatcher) String() string {
return string(m)
}
func assertRegexp(t *testing.T, input string, outputRE string) {
assertPlaintext(t, input, RegexpStringMatcher(outputRE))
}
func assertString(t *testing.T, input string, output string) {
assertPlaintext(t, input, ExactStringMatcher(output))
}
func assertPlaintext(t *testing.T, input string, matcher StringMatcher) {
text, err := FromString(input)
if err != nil {
t.Error(err)
}
if !matcher.MatchString(text) {
t.Errorf("Input did not match expression\n"+
"Input:\n>>>>\n%s\n<<<<\n\n"+
"Output:\n>>>>\n%s\n<<<<\n\n"+
"Expected output:\n>>>>\n%s\n<<<<\n\n",
input, text, matcher.String())
} else {
t.Logf("input:\n\n%s\n\n\n\noutput:\n\n%s\n", input, text)
}
}
func Example() {
inputHtml := `
<html>
<head>
<title>My Mega Service</title>
<link rel=\"stylesheet\" href=\"main.css\">
<style type=\"text/css\">body { color: #fff; }</style>
</head>
<body>
<div class="logo">
<a href="http://mymegaservice.com/"><img src="/logo-image.jpg" alt="Mega Service"/></a>
</div>
<h1>Welcome to your new account on my service!</h1>
<p>
Here is some more information:
<ul>
<li>Link 1: <a href="https://example.com">Example.com</a></li>
<li>Link 2: <a href="https://example2.com">Example2.com</a></li>
<li>Something else</li>
</ul>
</p>
</body>
</html>
`
text, err := FromString(inputHtml)
if err != nil {
panic(err)
}
fmt.Println(text)
// Output:
// Mega Service ( http://mymegaservice.com/ )
//
// ******************************************
// Welcome to your new account on my service!
// ******************************************
//
// Here is some more information:
//
// * Link 1: Example.com ( https://example.com )
// * Link 2: Example2.com ( https://example2.com )
// * Something else
}

View File

@@ -1,22 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>学习之道:美国公认学习第一书title</title>
<link href="stylesheet.css" rel="stylesheet" type="text/css" />
<link href="page_styles.css" rel="stylesheet" type="text/css" />
</head>
<body class="calibre">
<p id="filepos9452" class="calibre_"><span class="calibre6"><span class="bold">写在前面的话</span></span>
</p>
<p class="calibre_12">在台湾的那次世界冠军赛上,我几近疯狂,直至两年后的今天,我仍沉浸在这次的经历中。这是我生平第一次如此深入地审视我自己,甚至是第一次尝试审视自己。这个过程令人很是兴奋,同时也有点感觉怪异。我重新认识了自我,看到了自己的另外一面,自己从未发觉的另外一面。为了生存,为了取胜,我成了一名角斗士,彻头彻尾,简单纯粹。我并没有意识到这一角色早已在我的心中生根发芽,呼之欲出。也许,他的出现已是不可避免。</p>
<p class="calibre_7">而我这全新的一面,与我一直熟识的那个乔希,那个曾经害怕黑暗的孩子,那个象棋手,那个狂热于雨水、反复诵读杰克·克鲁亚克作品的年轻人之间,又有什么样的联系呢?这些都是我正在努力弄清楚的问题。</p>
<p class="calibre_7">自台湾赛事之后,我急切非常,一心想要回到训练中去,摆脱自己已经达到巅峰的想法。在过去的两年中,我已经重新开始。这是一个新的起点。前方的路还很长,有待进一步的探索。</p>
<p class="calibre_7">这本书的创作耗费了相当多的时间和精力。在成长的过程中,我在我的小房间里从未想过等待我的会是这样的战斗。在创作中,我的思想逐渐成熟;爱恋从分崩离析,到失而复得,世界冠军头衔从失之交臂,到囊中取物。如果说在我人生的第一个二十九年中,我学到了什么,那就是,我们永远无法预测结局,无论是重要的比赛、冒险,还是轰轰烈烈的爱情。我们唯一可以肯定的只有,出乎意料。不管我们做了多么万全的准备,在生活的真实场景中,我们总是会处于陌生的境地。我们也许会无法冷静,失去理智,感觉似乎整个世界都在针对我们。在这个时候,我们所要做的是要付出加倍的努力,要表现得比预想得更好。我认为,关键在于准备好随机应变,准备好在所能想象的高压下发挥出创造力。</p>
<p class="calibre_7">读者朋友们,我非常希望你们在读过这本书后,可以得到启发,甚至会得到触动,从而能够根据各自的天赋与特长,去实现自己的梦想。这就是我写作此书的目的。我在字里行间所传达的理念曾经使我受益匪浅,我很希望它们可以为大家提供一个基本的框架和方向。如果我的方法言之有理,那么就请接受它,琢磨它,并加之自己的见解。忘记我的那些数字。真正的掌握需要通过自己发现一些最能够引起共鸣的信息,并将其彻底地融合进来,直至成为一体,这样我们才能随心所欲地驾驭它。</p>
<div class="mbp_pagebreak" id="calibre_pb_4"></div>
</body>
</html>

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
<title>1892年波兰文版序言title</title>
<link rel="stylesheet" href="css/stylesheet.css" type="text/css" />
</head>
<body>
<div id="page30" />
<h2 id="CHP2-6">1892年波兰文版序言<a id="wzyy_18_30" href="#wz_18_30"><sup>[18]</sup></a></h2>
<p>出版共产主义宣言的一种新的波兰文本已成为必要,这一事实,引起了许多感想。</p>
<p>首先值得注意的是,近来宣言在一定程度上已成为欧洲大陆大工业发展的一种尺度。一个国家的大工业越发展,该国工人中想认清自己作为工人阶级在有产阶级面前所处地位的要求就越增加,他们中间的社会主义运动也越扩大,因而对宣言的需求也越增长。这样,根据宣言用某国文字销行的份数,不仅能够相当确切地断定该国工人运动的状况,而且还能够相当确切地断定该国大工业发展的程度。</p>
<p>因此,波兰文的新版本标志着波兰工业的决定性进步。从十年前发表的上一个版本以来确实有了这种进步,对此丝毫不容置疑。俄国的波兰,会议的波兰<a id="wzyy_19_30" href="#wz_19_30"><sup>[19]</sup></a>,成了俄罗斯帝国巨大的工业区。俄国大工业是零星分散的,一部分在芬兰湾沿岸,一部分在中央区(莫斯科和弗拉基米尔),第三部分在黑海和亚速海沿岸,还有另一些散布在别处;而波兰工业则紧缩于相对狭小的地区,享受到由这种积聚引起的长处与短处。这种长处是竞争着的俄罗斯工厂主所承认的,他们要求实行保护关税以对付波兰,尽管他们渴望使波兰人俄罗斯化。这种短处,对波兰工厂主与俄罗斯政府来说,表现在社会主义思想在波兰工人中间的迅速传播和对宣言需求的增长。</p>
<p>但是,波兰工业的迅速发展——它超过了俄国工业——本身<a id="page31" />是波兰人民的坚强生命力的一个新证明是波兰人民临近的民族复兴的一个新保证。而一个独立强盛的波兰的复兴不只是一件同波兰人有关、而且是同我们大家有关的事情。只有当每个民族在自己内部完全自主时欧洲各民族间真诚的国际合作才是可能的。1848年革命在无产阶级旗帜下使无产阶级的战士最终只作了资产阶级的工作这次革命通过自己遗嘱的执行者路易·波拿巴和俾斯麦也实现了意大利、德国和匈牙利的独立。然而波兰它从1792年以来为革命做的比所有这三个国家总共做的还要多而当它1863年失败于强大十倍的俄军的时候人们却把它抛弃不顾了。贵族既未能保持住、也未能重新争得波兰的独立今天波兰的独立对资产阶级至少是无所谓的。然而波兰的独立对于欧洲各民族和谐的合作是必需的。这种独立只有年轻的波兰无产阶级才能争得而且在它的手中会很好地保持住。因为欧洲所有其余的工人都象波兰工人自己一样也需要波兰的独立。</p>
<p>弗·恩格斯</p>
<p>1892年2月10日于伦敦</p>
<div id="page74" />
<div><a id="wz_18_30" href="#wzyy_18_30">[18]</a> 恩格斯用德文为《宣言》新的波兰文本写了这篇序言。1892年由波兰社会主义者在伦敦办的《黎明》杂志社出版。序言寄出后恩格斯写信给门德尔森1892年2月11日信中说他很愿意学会波兰文并且深入研究波兰工人运动的发展以便能够为《宣言》的下一版写一篇更详细的序言。——第20页</div>
<div><a id="wz_19_30" href="#wzyy_19_30">[19]</a> 指维也纳会议的波兰即根据1814—1815年维也纳会议的决定以波兰王国的正式名义割给俄国的那部分波兰土地。——第20页</div>
</body>
</html>

View File

@@ -1,14 +1,27 @@
language: go
go_import_path: github.com/davecgh/go-spew
go:
- 1.5.4
- 1.6.3
- 1.7
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- tip
sudo: false
install:
- go get -v golang.org/x/tools/cmd/cover
- go get -v github.com/alecthomas/gometalinter
- gometalinter --install
script:
- go test -v -tags=safe ./spew
- go test -v -tags=testcgo ./spew -covermode=count -coverprofile=profile.cov
- export PATH=$PATH:$HOME/gopath/bin
- export GORACE="halt_on_error=1"
- test -z "$(gometalinter --disable-all
--enable=gofmt
--enable=golint
--enable=vet
--enable=gosimple
--enable=unconvert
--deadline=4m ./spew | tee /dev/stderr)"
- go test -v -race -tags safe ./spew
- go test -v -race -tags testcgo ./spew -covermode=atomic -coverprofile=profile.cov
after_success:
- go get -v github.com/mattn/goveralls
- export PATH=$PATH:$HOME/gopath/bin
- goveralls -coverprofile=profile.cov -service=travis-ci

View File

@@ -1,8 +1,8 @@
ISC License
Copyright (c) 2012-2013 Dave Collins <dave@davec.name>
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
Permission to use, copy, modify, and distribute this software for any
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

View File

@@ -1,10 +1,9 @@
go-spew
=======
[![Build Status](https://travis-ci.org/davecgh/go-spew.png?branch=master)]
(https://travis-ci.org/davecgh/go-spew) [![Coverage Status]
(https://coveralls.io/repos/davecgh/go-spew/badge.png?branch=master)]
(https://coveralls.io/r/davecgh/go-spew?branch=master)
[![Build Status](https://img.shields.io/travis/davecgh/go-spew.svg)](https://travis-ci.org/davecgh/go-spew)
[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org)
[![Coverage Status](https://img.shields.io/coveralls/davecgh/go-spew.svg)](https://coveralls.io/r/davecgh/go-spew?branch=master)
Go-spew implements a deep pretty printer for Go data structures to aid in
debugging. A comprehensive suite of tests with 100% test coverage is provided
@@ -19,8 +18,7 @@ post about it
## Documentation
[![GoDoc](https://godoc.org/github.com/davecgh/go-spew/spew?status.png)]
(http://godoc.org/github.com/davecgh/go-spew/spew)
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/davecgh/go-spew/spew)
Full `go doc` style documentation for the project can be viewed online without
installing this package by using the excellent GoDoc site here:
@@ -160,6 +158,15 @@ options. See the ConfigState documentation for more details.
App Engine or with the "safe" build tag specified.
Pointer method invocation is enabled by default.
* DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
* DisableCapacities
DisableCapacities specifies whether to disable the printing of capacities
for arrays, slices, maps and channels. This is useful when diffing data
structures in tests.
* ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.
@@ -191,4 +198,4 @@ using the unsafe package.
## License
Go-spew is licensed under the liberal ISC License.
Go-spew is licensed under the [copyfree](http://copyfree.org) ISC License.

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Dave Collins <dave@davec.name>
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -41,9 +41,9 @@ var (
// after commit 82f48826c6c7 which changed the format again to mirror
// the original format. Code in the init function updates these offsets
// as necessary.
offsetPtr = uintptr(ptrSize)
offsetPtr = ptrSize
offsetScalar = uintptr(0)
offsetFlag = uintptr(ptrSize * 2)
offsetFlag = ptrSize * 2
// flagKindWidth and flagKindShift indicate various bits that the
// reflect package uses internally to track kind information.
@@ -58,7 +58,7 @@ var (
// changed their positions. Code in the init function updates these
// flags as necessary.
flagKindWidth = uintptr(5)
flagKindShift = uintptr(flagKindWidth - 1)
flagKindShift = flagKindWidth - 1
flagRO = uintptr(1 << 0)
flagIndir = uintptr(1 << 1)
)

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Dave Collins <dave@davec.name>
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -180,7 +180,7 @@ func printComplex(w io.Writer, c complex128, floatPrecision int) {
w.Write(closeParenBytes)
}
// printHexPtr outputs a uintptr formatted as hexidecimal with a leading '0x'
// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
// prefix to Writer w.
func printHexPtr(w io.Writer, p uintptr) {
// Null pointer.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -67,6 +67,15 @@ type ConfigState struct {
// Google App Engine or with the "safe" build tag specified.
DisablePointerMethods bool
// DisablePointerAddresses specifies whether to disable the printing of
// pointer addresses. This is useful when diffing data structures in tests.
DisablePointerAddresses bool
// DisableCapacities specifies whether to disable the printing of capacities
// for arrays, slices, maps and channels. This is useful when diffing
// data structures in tests.
DisableCapacities bool
// ContinueOnMethod specifies whether or not recursion should continue once
// a custom error or Stringer interface is invoked. The default, false,
// means it will print the results of invoking the custom error or Stringer

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -91,6 +91,15 @@ The following configuration options are available:
which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default.
* DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
* DisableCapacities
DisableCapacities specifies whether to disable the printing of
capacities for arrays, slices, maps and channels. This is useful when
diffing data structures in tests.
* ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -35,16 +35,16 @@ var (
// cCharRE is a regular expression that matches a cgo char.
// It is used to detect character arrays to hexdump them.
cCharRE = regexp.MustCompile("^.*\\._Ctype_char$")
cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`)
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
// char. It is used to detect unsigned character arrays to hexdump
// them.
cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$")
cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
// It is used to detect uint8_t arrays to hexdump them.
cUint8tCharRE = regexp.MustCompile("^.*\\._Ctype_uint8_t$")
cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`)
)
// dumpState contains information about the state of a dump operation.
@@ -129,7 +129,7 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
d.w.Write(closeParenBytes)
// Display pointer information.
if len(pointerChain) > 0 {
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
d.w.Write(openParenBytes)
for i, addr := range pointerChain {
if i > 0 {
@@ -143,10 +143,10 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
// Display dereferenced value.
d.w.Write(openParenBytes)
switch {
case nilFound == true:
case nilFound:
d.w.Write(nilAngleBytes)
case cycleFound == true:
case cycleFound:
d.w.Write(circularBytes)
default:
@@ -282,13 +282,13 @@ func (d *dumpState) dump(v reflect.Value) {
case reflect.Map, reflect.String:
valueLen = v.Len()
}
if valueLen != 0 || valueCap != 0 {
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
d.w.Write(openParenBytes)
if valueLen != 0 {
d.w.Write(lenEqualsBytes)
printInt(d.w, int64(valueLen), 10)
}
if valueCap != 0 {
if !d.cs.DisableCapacities && valueCap != 0 {
if valueLen != 0 {
d.w.Write(spaceBytes)
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -70,7 +70,7 @@ import (
"github.com/davecgh/go-spew/spew"
)
// dumpTest is used to describe a test to be perfomed against the Dump method.
// dumpTest is used to describe a test to be performed against the Dump method.
type dumpTest struct {
in interface{}
wants []string

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2013 Dave Collins <dave@davec.name>
// Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -82,18 +82,20 @@ func addCgoDumpTests() {
v5Len := fmt.Sprintf("%d", v5l)
v5Cap := fmt.Sprintf("%d", v5c)
v5t := "[6]testdata._Ctype_uint8_t"
v5t2 := "[6]testdata._Ctype_uchar"
v5s := "(len=" + v5Len + " cap=" + v5Cap + ") " +
"{\n 00000000 74 65 73 74 35 00 " +
" |test5.|\n}"
addDumpTest(v5, "("+v5t+") "+v5s+"\n")
addDumpTest(v5, "("+v5t+") "+v5s+"\n", "("+v5t2+") "+v5s+"\n")
// C typedefed unsigned char array.
v6, v6l, v6c := testdata.GetCgoTypdefedUnsignedCharArray()
v6Len := fmt.Sprintf("%d", v6l)
v6Cap := fmt.Sprintf("%d", v6c)
v6t := "[6]testdata._Ctype_custom_uchar_t"
v6t2 := "[6]testdata._Ctype_uchar"
v6s := "(len=" + v6Len + " cap=" + v6Cap + ") " +
"{\n 00000000 74 65 73 74 36 00 " +
" |test6.|\n}"
addDumpTest(v6, "("+v6t+") "+v6s+"\n")
addDumpTest(v6, "("+v6t+") "+v6s+"\n", "("+v6t2+") "+v6s+"\n")
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -182,10 +182,10 @@ func (f *formatState) formatPtr(v reflect.Value) {
// Display dereferenced value.
switch {
case nilFound == true:
case nilFound:
f.fs.Write(nilAngleBytes)
case cycleFound == true:
case cycleFound:
f.fs.Write(circularShortBytes)
default:

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -75,7 +75,7 @@ import (
"github.com/davecgh/go-spew/spew"
)
// formatterTest is used to describe a test to be perfomed against NewFormatter.
// formatterTest is used to describe a test to be performed against NewFormatter.
type formatterTest struct {
format string
in interface{}
@@ -1536,14 +1536,14 @@ func TestPrintSortedKeys(t *testing.T) {
t.Errorf("Sorted keys mismatch 3:\n %v %v", s, expected)
}
s = cfg.Sprint(map[testStruct]int{testStruct{1}: 1, testStruct{3}: 3, testStruct{2}: 2})
s = cfg.Sprint(map[testStruct]int{{1}: 1, {3}: 3, {2}: 2})
expected = "map[ts.1:1 ts.2:2 ts.3:3]"
if s != expected {
t.Errorf("Sorted keys mismatch 4:\n %v %v", s, expected)
}
if !spew.UnsafeDisabled {
s = cfg.Sprint(map[testStructP]int{testStructP{1}: 1, testStructP{3}: 3, testStructP{2}: 2})
s = cfg.Sprint(map[testStructP]int{{1}: 1, {3}: 3, {2}: 2})
expected = "map[ts.1:1 ts.2:2 ts.3:3]"
if s != expected {
t.Errorf("Sorted keys mismatch 5:\n %v %v", s, expected)

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -36,10 +36,7 @@ type dummyFmtState struct {
}
func (dfs *dummyFmtState) Flag(f int) bool {
if f == int('+') {
return true
}
return false
return f == int('+')
}
func (dfs *dummyFmtState) Precision() (int, bool) {

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2013-2015 Dave Collins <dave@davec.name>
// Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -130,12 +130,19 @@ func initSpewTests() {
scsNoPmethods := &spew.ConfigState{Indent: " ", DisablePointerMethods: true}
scsMaxDepth := &spew.ConfigState{Indent: " ", MaxDepth: 1}
scsContinue := &spew.ConfigState{Indent: " ", ContinueOnMethod: true}
scsNoPtrAddr := &spew.ConfigState{DisablePointerAddresses: true}
scsNoCap := &spew.ConfigState{DisableCapacities: true}
// Variables for tests on types which implement Stringer interface with and
// without a pointer receiver.
ts := stringer("test")
tps := pstringer("test")
type ptrTester struct {
s *struct{}
}
tptr := &ptrTester{s: &struct{}{}}
// depthTester is used to test max depth handling for structs, array, slices
// and maps.
type depthTester struct {
@@ -192,6 +199,10 @@ func initSpewTests() {
{scsContinue, fCSFprint, "", te, "(error: 10) 10"},
{scsContinue, fCSFdump, "", te, "(spew_test.customError) " +
"(error: 10) 10\n"},
{scsNoPtrAddr, fCSFprint, "", tptr, "<*>{<*>{}}"},
{scsNoPtrAddr, fCSSdump, "", tptr, "(*spew_test.ptrTester)({\ns: (*struct {})({\n})\n})\n"},
{scsNoCap, fCSSdump, "", make([]string, 0, 10), "([]string) {\n}\n"},
{scsNoCap, fCSSdump, "", make([]string, 1, 10), "([]string) (len=1) {\n(string) \"\"\n}\n"},
}
}

View File

@@ -76,8 +76,32 @@ func Open(filename string) (image.Image, error) {
return img, err
}
type encodeConfig struct {
jpegQuality int
}
var defaultEncodeConfig = encodeConfig{
jpegQuality: 95,
}
// EncodeOption sets an optional parameter for the Encode and Save functions.
type EncodeOption func(*encodeConfig)
// JPEGQuality returns an EncodeOption that sets the output JPEG quality.
// Quality ranges from 1 to 100 inclusive, higher is better. Default is 95.
func JPEGQuality(quality int) EncodeOption {
return func(c *encodeConfig) {
c.jpegQuality = quality
}
}
// Encode writes the image img to w in the specified format (JPEG, PNG, GIF, TIFF or BMP).
func Encode(w io.Writer, img image.Image, format Format) error {
func Encode(w io.Writer, img image.Image, format Format, opts ...EncodeOption) error {
cfg := defaultEncodeConfig
for _, option := range opts {
option(&cfg)
}
var err error
switch format {
case JPEG:
@@ -92,9 +116,9 @@ func Encode(w io.Writer, img image.Image, format Format) error {
}
}
if rgba != nil {
err = jpeg.Encode(w, rgba, &jpeg.Options{Quality: 95})
err = jpeg.Encode(w, rgba, &jpeg.Options{Quality: cfg.jpegQuality})
} else {
err = jpeg.Encode(w, img, &jpeg.Options{Quality: 95})
err = jpeg.Encode(w, img, &jpeg.Options{Quality: cfg.jpegQuality})
}
case PNG:
@@ -113,7 +137,16 @@ func Encode(w io.Writer, img image.Image, format Format) error {
// Save saves the image to file with the specified filename.
// The format is determined from the filename extension: "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported.
func Save(img image.Image, filename string) (err error) {
//
// Examples:
//
// // Save the image as PNG.
// err := imaging.Save(img, "out.png")
//
// // Save the image as JPEG with optional quality parameter set to 80.
// err := imaging.Save(img, "out.jpg", imaging.JPEGQuality(80))
//
func Save(img image.Image, filename string, opts ...EncodeOption) (err error) {
formats := map[string]Format{
".jpg": JPEG,
".jpeg": JPEG,
@@ -136,7 +169,7 @@ func Save(img image.Image, filename string) (err error) {
}
defer file.Close()
return Encode(file, img, f)
return Encode(file, img, f, opts...)
}
// New creates a new image with the specified width and height, and fills it with the specified color.

View File

@@ -2,12 +2,14 @@ sudo: false
language: go
go:
- 1.6.3
- 1.8.x
- 1.9.x
- tip
matrix:
allow_failures:
- go: tip
fast_finish: true
before_script:
- go get -u github.com/golang/lint/golint

View File

@@ -8,8 +8,10 @@
# Please keep the list sorted.
Aaron L <aaron@bettercoder.net>
Adrien Bustany <adrien@bustany.org>
Amit Krishnan <amit.krishnan@oracle.com>
Anmol Sethi <me@anmol.io>
Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Bruno Bigras <bigras.bruno@gmail.com>
Caleb Spare <cespare@gmail.com>
@@ -26,6 +28,7 @@ Kelvin Fo <vmirage@gmail.com>
Ken-ichirou MATSUZAWA <chamas@h4.dion.ne.jp>
Matt Layher <mdlayher@gmail.com>
Nathan Youngman <git@nathany.com>
Nickolai Zeldovich <nickolai@csail.mit.edu>
Patrick <patrick@dropbox.com>
Paul Hammond <paul@paulhammond.org>
Pawel Knap <pawelknap88@gmail.com>
@@ -33,12 +36,15 @@ Pieter Droogendijk <pieter@binky.org.uk>
Pursuit92 <JoshChase@techpursuit.net>
Riku Voipio <riku.voipio@linaro.org>
Rob Figueiredo <robfig@gmail.com>
Rodrigo Chiossi <rodrigochiossi@gmail.com>
Slawek Ligus <root@ooz.ie>
Soge Zhang <zhssoge@gmail.com>
Tiffany Jernigan <tiffany.jernigan@intel.com>
Tilak Sharma <tilaks@google.com>
Tom Payne <twpayne@gmail.com>
Travis Cline <travis.cline@gmail.com>
Tudor Golubenco <tudor.g@gmail.com>
Vahe Khachikyan <vahe@live.ca>
Yukang <moorekang@gmail.com>
bronze1man <bronze1man@gmail.com>
debrando <denis.brandolini@gmail.com>

View File

@@ -1,5 +1,15 @@
# Changelog
## v1.4.7 / 2018-01-09
* BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine)
* Tests: Fix missing verb on format string (thanks @rchiossi)
* Linux: Fix deadlock in Remove (thanks @aarondl)
* Linux: Watch.Add improvements (avoid race, fix consistency, reduce garbage) (thanks @twpayne)
* Docs: Moved FAQ into the README (thanks @vahe)
* Linux: Properly handle inotify's IN_Q_OVERFLOW event (thanks @zeldovich)
* Docs: replace references to OS X with macOS
## v1.4.2 / 2016-10-10
* Linux: use InotifyInit1 with IN_CLOEXEC to stop leaking a file descriptor to a child process when using fork/exec [#178](https://github.com/fsnotify/fsnotify/pull/178) (thanks @pattyshack)
@@ -79,7 +89,7 @@ kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsn
## v1.0.2 / 2014-08-17
* [Fix] Missing create events on OS X. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
* [Fix] Missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
* [Fix] Make ./path and path equivalent. (thanks @zhsso)
## v1.0.0 / 2014-08-15
@@ -142,7 +152,7 @@ kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsn
## v0.9.2 / 2014-08-17
* [Backport] Fix missing create events on OS X. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
* [Backport] Fix missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
## v0.9.1 / 2014-06-12
@@ -161,7 +171,7 @@ kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsn
## v0.8.11 / 2013-11-02
* [Doc] Add Changelog [#72][] (thanks @nathany)
* [Doc] Spotlight and double modify events on OS X [#62][] (reported by @paulhammond)
* [Doc] Spotlight and double modify events on macOS [#62][] (reported by @paulhammond)
## v0.8.10 / 2013-10-19

View File

@@ -17,7 +17,7 @@ Please indicate that you have signed the CLA in your pull request.
### How fsnotify is Developed
* Development is done on feature branches.
* Tests are run on BSD, Linux, OS X and Windows.
* Tests are run on BSD, Linux, macOS and Windows.
* Pull requests are reviewed and [applied to master][am] using [hub][].
* Maintainers may modify or squash commits rather than asking contributors to.
* To issue a new release, the maintainers will:
@@ -44,7 +44,7 @@ This workflow is [thoroughly explained by Katrina Owen](https://splice.com/blog/
### Testing
fsnotify uses build tags to compile different code on Linux, BSD, OS X, and Windows.
fsnotify uses build tags to compile different code on Linux, BSD, macOS, and Windows.
Before doing a pull request, please do your best to test your changes on multiple platforms, and list which platforms you were able/unable to test on.
@@ -58,7 +58,7 @@ To aid in cross-platform testing there is a Vagrantfile for Linux and BSD.
Notice: fsnotify file system events won't trigger in shared folders. The tests get around this limitation by using the /tmp directory.
Right now there is no equivalent solution for Windows and OS X, but there are Windows VMs [freely available from Microsoft](http://www.modern.ie/en-us/virtualization-tools#downloads).
Right now there is no equivalent solution for Windows and macOS, but there are Windows VMs [freely available from Microsoft](http://www.modern.ie/en-us/virtualization-tools#downloads).
### Maintainers

View File

@@ -8,14 +8,14 @@ fsnotify utilizes [golang.org/x/sys](https://godoc.org/golang.org/x/sys) rather
go get -u golang.org/x/sys/...
```
Cross platform: Windows, Linux, BSD and OS X.
Cross platform: Windows, Linux, BSD and macOS.
|Adapter |OS |Status |
|----------|----------|----------|
|inotify |Linux 2.6.27 or later, Android\*|Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify)|
|kqueue |BSD, OS X, iOS\*|Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify)|
|kqueue |BSD, macOS, iOS\*|Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify)|
|ReadDirectoryChangesW|Windows|Supported [![Build status](https://ci.appveyor.com/api/projects/status/ivwjubaih4r0udeh/branch/master?svg=true)](https://ci.appveyor.com/project/NathanYoungman/fsnotify/branch/master)|
|FSEvents |OS X |[Planned](https://github.com/fsnotify/fsnotify/issues/11)|
|FSEvents |macOS |[Planned](https://github.com/fsnotify/fsnotify/issues/11)|
|FEN |Solaris 11 |[In Progress](https://github.com/fsnotify/fsnotify/issues/12)|
|fanotify |Linux 2.6.37+ | |
|USN Journals |Windows |[Maybe](https://github.com/fsnotify/fsnotify/issues/53)|
@@ -23,7 +23,7 @@ Cross platform: Windows, Linux, BSD and OS X.
\* Android and iOS are untested.
Please see [the documentation](https://godoc.org/github.com/fsnotify/fsnotify) for usage. Consult the [Wiki](https://github.com/fsnotify/fsnotify/wiki) for the FAQ and further information.
Please see [the documentation](https://godoc.org/github.com/fsnotify/fsnotify) and consult the [FAQ](#faq) for usage information.
## API stability
@@ -41,6 +41,35 @@ Please refer to [CONTRIBUTING][] before opening an issue or pull request.
See [example_test.go](https://github.com/fsnotify/fsnotify/blob/master/example_test.go).
## FAQ
**When a file is moved to another directory is it still being watched?**
No (it shouldn't be, unless you are watching where it was moved to).
**When I watch a directory, are all subdirectories watched as well?**
No, you must add watches for any directory you want to watch (a recursive watcher is on the roadmap [#18][]).
**Do I have to watch the Error and Event channels in a separate goroutine?**
As of now, yes. Looking into making this single-thread friendly (see [howeyc #7][#7])
**Why am I receiving multiple events for the same file on OS X?**
Spotlight indexing on OS X can result in multiple events (see [howeyc #62][#62]). A temporary workaround is to add your folder(s) to the *Spotlight Privacy settings* until we have a native FSEvents implementation (see [#11][]).
**How many files can be watched at once?**
There are OS-specific limits as to how many watches can be created:
* Linux: /proc/sys/fs/inotify/max_user_watches contains the limit, reaching this limit results in a "no space left on device" error.
* BSD / OSX: sysctl variables "kern.maxfiles" and "kern.maxfilesperproc", reaching these limits results in a "too many open files" error.
[#62]: https://github.com/howeyc/fsnotify/issues/62
[#18]: https://github.com/fsnotify/fsnotify/issues/18
[#11]: https://github.com/fsnotify/fsnotify/issues/11
[#7]: https://github.com/howeyc/fsnotify/issues/7
[contributing]: https://github.com/fsnotify/fsnotify/blob/master/CONTRIBUTING.md
## Related Projects

View File

@@ -9,6 +9,7 @@ package fsnotify
import (
"bytes"
"errors"
"fmt"
)
@@ -60,3 +61,6 @@ func (op Op) String() string {
func (e Event) String() string {
return fmt.Sprintf("%q: %s", e.Name, e.Op.String())
}
// Common errors that can be reported by a watcher
var ErrEventOverflow = errors.New("fsnotify queue overflow")

View File

@@ -6,7 +6,11 @@
package fsnotify
import "testing"
import (
"os"
"testing"
"time"
)
func TestEventStringWithValue(t *testing.T) {
for opMask, expectedString := range map[Op]string{
@@ -38,3 +42,29 @@ func TestEventOpStringWithNoValue(t *testing.T) {
t.Fatalf("Expected %s, got: %v", expectedOpString, event.Op.String())
}
}
// TestWatcherClose tests that the goroutine started by creating the watcher can be
// signalled to return at any time, even if there is no goroutine listening on the events
// or errors channels.
func TestWatcherClose(t *testing.T) {
t.Parallel()
name := tempMkFile(t, "")
w := newWatcher(t)
err := w.Add(name)
if err != nil {
t.Fatal(err)
}
err = os.Remove(name)
if err != nil {
t.Fatal(err)
}
// Allow the watcher to receive the event.
time.Sleep(time.Millisecond * 100)
err = w.Close()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -24,7 +24,6 @@ type Watcher struct {
Events chan Event
Errors chan error
mu sync.Mutex // Map access
cv *sync.Cond // sync removing on rm_watch with IN_IGNORE
fd int
poller *fdPoller
watches map[string]*watch // Map of inotify watches (key: path)
@@ -56,7 +55,6 @@ func NewWatcher() (*Watcher, error) {
done: make(chan struct{}),
doneResp: make(chan struct{}),
}
w.cv = sync.NewCond(&w.mu)
go w.readEvents()
return w, nil
@@ -103,21 +101,23 @@ func (w *Watcher) Add(name string) error {
var flags uint32 = agnosticEvents
w.mu.Lock()
watchEntry, found := w.watches[name]
w.mu.Unlock()
if found {
watchEntry.flags |= flags
flags |= unix.IN_MASK_ADD
defer w.mu.Unlock()
watchEntry := w.watches[name]
if watchEntry != nil {
flags |= watchEntry.flags | unix.IN_MASK_ADD
}
wd, errno := unix.InotifyAddWatch(w.fd, name, flags)
if wd == -1 {
return errno
}
w.mu.Lock()
w.watches[name] = &watch{wd: uint32(wd), flags: flags}
w.paths[wd] = name
w.mu.Unlock()
if watchEntry == nil {
w.watches[name] = &watch{wd: uint32(wd), flags: flags}
w.paths[wd] = name
} else {
watchEntry.wd = uint32(wd)
watchEntry.flags = flags
}
return nil
}
@@ -135,6 +135,13 @@ func (w *Watcher) Remove(name string) error {
if !ok {
return fmt.Errorf("can't remove non-existent inotify watch for: %s", name)
}
// We successfully removed the watch if InotifyRmWatch doesn't return an
// error, we need to clean up our internal state to ensure it matches
// inotify's kernel state.
delete(w.paths, int(watch.wd))
delete(w.watches, name)
// inotify_rm_watch will return EINVAL if the file has been deleted;
// the inotify will already have been removed.
// watches and pathes are deleted in ignoreLinux() implicitly and asynchronously
@@ -152,13 +159,6 @@ func (w *Watcher) Remove(name string) error {
return errno
}
// wait until ignoreLinux() deleting maps
exists := true
for exists {
w.cv.Wait()
_, exists = w.watches[name]
}
return nil
}
@@ -245,13 +245,31 @@ func (w *Watcher) readEvents() {
mask := uint32(raw.Mask)
nameLen := uint32(raw.Len)
if mask&unix.IN_Q_OVERFLOW != 0 {
select {
case w.Errors <- ErrEventOverflow:
case <-w.done:
return
}
}
// If the event happened to the watched directory or the watched file, the kernel
// doesn't append the filename to the event, but we would like to always fill the
// the "Name" field with a valid filename. We retrieve the path of the watch from
// the "paths" map.
w.mu.Lock()
name := w.paths[int(raw.Wd)]
name, ok := w.paths[int(raw.Wd)]
// IN_DELETE_SELF occurs when the file/directory being watched is removed.
// This is a sign to clean up the maps, otherwise we are no longer in sync
// with the inotify kernel state which has already deleted the watch
// automatically.
if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
delete(w.paths, int(raw.Wd))
delete(w.watches, name)
}
w.mu.Unlock()
if nameLen > 0 {
// Point "bytes" at the first byte of the filename
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))
@@ -262,7 +280,7 @@ func (w *Watcher) readEvents() {
event := newEvent(name, mask)
// Send the events that are not ignored on the events channel
if !event.ignoreLinux(w, raw.Wd, mask) {
if !event.ignoreLinux(mask) {
select {
case w.Events <- event:
case <-w.done:
@@ -279,15 +297,9 @@ func (w *Watcher) readEvents() {
// Certain types of events can be "ignored" and not sent over the Events
// channel. Such as events marked ignore by the kernel, or MODIFY events
// against files that do not exist.
func (e *Event) ignoreLinux(w *Watcher, wd int32, mask uint32) bool {
func (e *Event) ignoreLinux(mask uint32) bool {
// Ignore anything the inotify API says to ignore
if mask&unix.IN_IGNORED == unix.IN_IGNORED {
w.mu.Lock()
defer w.mu.Unlock()
name := w.paths[int(wd)]
delete(w.paths, int(wd))
delete(w.watches, name)
w.cv.Broadcast()
return true
}

View File

@@ -293,25 +293,23 @@ func TestInotifyRemoveTwice(t *testing.T) {
t.Fatalf("Failed to add testFile: %v", err)
}
err = os.Remove(testFile)
err = w.Remove(testFile)
if err != nil {
t.Fatalf("Failed to remove testFile: %v", err)
t.Fatalf("wanted successful remove but got: %v", err)
}
err = w.Remove(testFile)
if err == nil {
t.Fatalf("no error on removing invalid file")
}
s1 := fmt.Sprintf("%s", err)
err = w.Remove(testFile)
if err == nil {
t.Fatalf("no error on removing invalid file")
w.mu.Lock()
defer w.mu.Unlock()
if len(w.watches) != 0 {
t.Fatalf("Expected watches len is 0, but got: %d, %v", len(w.watches), w.watches)
}
s2 := fmt.Sprintf("%s", err)
if s1 != s2 {
t.Fatalf("receive different error - %s / %s", s1, s2)
if len(w.paths) != 0 {
t.Fatalf("Expected paths len is 0, but got: %d, %v", len(w.paths), w.paths)
}
}
@@ -358,3 +356,94 @@ func TestInotifyInnerMapLength(t *testing.T) {
t.Fatalf("Expected paths len is 0, but got: %d, %v", len(w.paths), w.paths)
}
}
func TestInotifyOverflow(t *testing.T) {
// We need to generate many more events than the
// fs.inotify.max_queued_events sysctl setting.
// We use multiple goroutines (one per directory)
// to speed up file creation.
numDirs := 128
numFiles := 1024
testDir := tempMkdir(t)
defer os.RemoveAll(testDir)
w, err := NewWatcher()
if err != nil {
t.Fatalf("Failed to create watcher: %v", err)
}
defer w.Close()
for dn := 0; dn < numDirs; dn++ {
testSubdir := fmt.Sprintf("%s/%d", testDir, dn)
err := os.Mkdir(testSubdir, 0777)
if err != nil {
t.Fatalf("Cannot create subdir: %v", err)
}
err = w.Add(testSubdir)
if err != nil {
t.Fatalf("Failed to add subdir: %v", err)
}
}
errChan := make(chan error, numDirs*numFiles)
for dn := 0; dn < numDirs; dn++ {
testSubdir := fmt.Sprintf("%s/%d", testDir, dn)
go func() {
for fn := 0; fn < numFiles; fn++ {
testFile := fmt.Sprintf("%s/%d", testSubdir, fn)
handle, err := os.Create(testFile)
if err != nil {
errChan <- fmt.Errorf("Create failed: %v", err)
continue
}
err = handle.Close()
if err != nil {
errChan <- fmt.Errorf("Close failed: %v", err)
continue
}
}
}()
}
creates := 0
overflows := 0
after := time.After(10 * time.Second)
for overflows == 0 && creates < numDirs*numFiles {
select {
case <-after:
t.Fatalf("Not done")
case err := <-errChan:
t.Fatalf("Got an error from file creator goroutine: %v", err)
case err := <-w.Errors:
if err == ErrEventOverflow {
overflows++
} else {
t.Fatalf("Got an error from watcher: %v", err)
}
case evt := <-w.Events:
if !strings.HasPrefix(evt.Name, testDir) {
t.Fatalf("Got an event for an unknown file: %s", evt.Name)
}
if evt.Op == Create {
creates++
}
}
}
if creates == numDirs*numFiles {
t.Fatalf("Could not trigger overflow")
}
if overflows == 0 {
t.Fatalf("No overflow and not enough creates (expected %d, got %d)",
numDirs*numFiles, creates)
}
}

View File

@@ -13,9 +13,9 @@ import (
"golang.org/x/sys/unix"
)
// testExchangedataForWatcher tests the watcher with the exchangedata operation on OS X.
// testExchangedataForWatcher tests the watcher with the exchangedata operation on macOS.
//
// This is widely used for atomic saves on OS X, e.g. TextMate and in Apple's NSDocument.
// This is widely used for atomic saves on macOS, e.g. TextMate and in Apple's NSDocument.
//
// See https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/exchangedata.2.html
// Also see: https://github.com/textmate/textmate/blob/cd016be29489eba5f3c09b7b70b06da134dda550/Frameworks/io/src/swap_file_data.cc#L20

View File

@@ -22,7 +22,7 @@ import (
type Watcher struct {
Events chan Event
Errors chan error
done chan bool // Channel for sending a "quit message" to the reader goroutine
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
kq int // File descriptor (as returned by the kqueue() syscall).
@@ -56,7 +56,7 @@ func NewWatcher() (*Watcher, error) {
externalWatches: make(map[string]bool),
Events: make(chan Event),
Errors: make(chan error),
done: make(chan bool),
done: make(chan struct{}),
}
go w.readEvents()
@@ -71,10 +71,8 @@ func (w *Watcher) Close() error {
return nil
}
w.isClosed = true
w.mu.Unlock()
// copy paths to remove while locked
w.mu.Lock()
var pathsToRemove = make([]string, 0, len(w.watches))
for name := range w.watches {
pathsToRemove = append(pathsToRemove, name)
@@ -82,15 +80,12 @@ func (w *Watcher) Close() error {
w.mu.Unlock()
// unlock before calling Remove, which also locks
var err error
for _, name := range pathsToRemove {
if e := w.Remove(name); e != nil && err == nil {
err = e
}
w.Remove(name)
}
// Send "quit" message to the reader goroutine:
w.done <- true
// send a "quit" message to the reader goroutine
close(w.done)
return nil
}
@@ -266,17 +261,12 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
func (w *Watcher) readEvents() {
eventBuffer := make([]unix.Kevent_t, 10)
loop:
for {
// See if there is a message on the "done" channel
select {
case <-w.done:
err := unix.Close(w.kq)
if err != nil {
w.Errors <- err
}
close(w.Events)
close(w.Errors)
return
break loop
default:
}
@@ -284,7 +274,11 @@ func (w *Watcher) readEvents() {
kevents, err := read(w.kq, eventBuffer, &keventWaitTime)
// EINTR is okay, the syscall was interrupted before timeout expired.
if err != nil && err != unix.EINTR {
w.Errors <- err
select {
case w.Errors <- err:
case <-w.done:
break loop
}
continue
}
@@ -319,8 +313,12 @@ func (w *Watcher) readEvents() {
if path.isDir && event.Op&Write == Write && !(event.Op&Remove == Remove) {
w.sendDirectoryChangeEvents(event.Name)
} else {
// Send the event on the Events channel
w.Events <- event
// Send the event on the Events channel.
select {
case w.Events <- event:
case <-w.done:
break loop
}
}
if event.Op&Remove == Remove {
@@ -352,6 +350,18 @@ func (w *Watcher) readEvents() {
kevents = kevents[1:]
}
}
// cleanup
err := unix.Close(w.kq)
if err != nil {
// only way the previous loop breaks is if w.done was closed so we need to async send to w.Errors.
select {
case w.Errors <- err:
default:
}
}
close(w.Events)
close(w.Errors)
}
// newEvent returns an platform-independent Event based on kqueue Fflags.
@@ -407,7 +417,11 @@ func (w *Watcher) sendDirectoryChangeEvents(dirPath string) {
// Get all files
files, err := ioutil.ReadDir(dirPath)
if err != nil {
w.Errors <- err
select {
case w.Errors <- err:
case <-w.done:
return
}
}
// Search for new files
@@ -428,7 +442,11 @@ func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInf
w.mu.Unlock()
if !doesExist {
// Send create event
w.Events <- newCreateEvent(filePath)
select {
case w.Events <- newCreateEvent(filePath):
case <-w.done:
return
}
}
// like watchDirectoryFiles (but without doing another ReadDir)

View File

@@ -1,8 +1,8 @@
language: go
env:
global:
- VET_VERSIONS="1.6 1.7 tip"
- LINT_VERSIONS="1.6 1.7 tip"
- VET_VERSIONS="1.6 1.7 1.8 1.9 tip"
- LINT_VERSIONS="1.6 1.7 1.8 1.9 tip"
go:
- 1.2
- 1.3
@@ -10,6 +10,8 @@ go:
- 1.5
- 1.6
- 1.7
- 1.8
- 1.9
- tip
matrix:
fast_finish: true

View File

@@ -1,5 +1,15 @@
.PHONY: default install build test quicktest fmt vet lint
GO_VERSION := $(shell go version | cut -d' ' -f3 | cut -d. -f2)
# Only use the `-race` flag on newer versions of Go
IS_OLD_GO := $(shell test $(GO_VERSION) -le 2 && echo true)
ifeq ($(IS_OLD_GO),true)
RACE_FLAG :=
else
RACE_FLAG := -race -cpu 1,2,4
endif
default: fmt vet lint build quicktest
install:
@@ -9,7 +19,7 @@ build:
go build -v ./...
test:
go test -v -cover ./...
go test -v $(RACE_FLAG) -cover ./...
quicktest:
go test ./...

13
vendor/github.com/go-ldap/ldap/atomic_value.go generated vendored Normal file
View File

@@ -0,0 +1,13 @@
// +build go1.4
package ldap
import (
"sync/atomic"
)
// For compilers that support it, we just use the underlying sync/atomic.Value
// type.
type atomicValue struct {
atomic.Value
}

28
vendor/github.com/go-ldap/ldap/atomic_value_go13.go generated vendored Normal file
View File

@@ -0,0 +1,28 @@
// +build !go1.4
package ldap
import (
"sync"
)
// This is a helper type that emulates the use of the "sync/atomic.Value"
// struct that's available in Go 1.4 and up.
type atomicValue struct {
value interface{}
lock sync.RWMutex
}
func (av *atomicValue) Store(val interface{}) {
av.lock.Lock()
av.value = val
av.lock.Unlock()
}
func (av *atomicValue) Load() interface{} {
av.lock.RLock()
ret := av.value
av.lock.RUnlock()
return ret
}

View File

@@ -11,6 +11,7 @@ import (
"log"
"net"
"sync"
"sync/atomic"
"time"
"gopkg.in/asn1-ber.v1"
@@ -82,20 +83,18 @@ const (
type Conn struct {
conn net.Conn
isTLS bool
isClosing bool
closeErr error
closing uint32
closeErr atomicValue
isStartingTLS bool
Debug debugging
chanConfirm chan bool
chanConfirm chan struct{}
messageContexts map[int64]*messageContext
chanMessage chan *messagePacket
chanMessageID chan int64
wgSender sync.WaitGroup
wgClose sync.WaitGroup
once sync.Once
outstandingRequests uint
messageMutex sync.Mutex
requestTimeout time.Duration
requestTimeout int64
}
var _ Client = &Conn{}
@@ -142,7 +141,7 @@ func DialTLS(network, addr string, config *tls.Config) (*Conn, error) {
func NewConn(conn net.Conn, isTLS bool) *Conn {
return &Conn{
conn: conn,
chanConfirm: make(chan bool),
chanConfirm: make(chan struct{}),
chanMessageID: make(chan int64),
chanMessage: make(chan *messagePacket, 10),
messageContexts: map[int64]*messageContext{},
@@ -158,12 +157,22 @@ func (l *Conn) Start() {
l.wgClose.Add(1)
}
// isClosing returns whether or not we're currently closing.
func (l *Conn) isClosing() bool {
return atomic.LoadUint32(&l.closing) == 1
}
// setClosing sets the closing value to true
func (l *Conn) setClosing() bool {
return atomic.CompareAndSwapUint32(&l.closing, 0, 1)
}
// Close closes the connection.
func (l *Conn) Close() {
l.once.Do(func() {
l.isClosing = true
l.wgSender.Wait()
l.messageMutex.Lock()
defer l.messageMutex.Unlock()
if l.setClosing() {
l.Debug.Printf("Sending quit message and waiting for confirmation")
l.chanMessage <- &messagePacket{Op: MessageQuit}
<-l.chanConfirm
@@ -171,27 +180,25 @@ func (l *Conn) Close() {
l.Debug.Printf("Closing network connection")
if err := l.conn.Close(); err != nil {
log.Print(err)
log.Println(err)
}
l.wgClose.Done()
})
}
l.wgClose.Wait()
}
// SetTimeout sets the time after a request is sent that a MessageTimeout triggers
func (l *Conn) SetTimeout(timeout time.Duration) {
if timeout > 0 {
l.requestTimeout = timeout
atomic.StoreInt64(&l.requestTimeout, int64(timeout))
}
}
// Returns the next available messageID
func (l *Conn) nextMessageID() int64 {
if l.chanMessageID != nil {
if messageID, ok := <-l.chanMessageID; ok {
return messageID
}
if messageID, ok := <-l.chanMessageID; ok {
return messageID
}
return 0
}
@@ -258,7 +265,7 @@ func (l *Conn) sendMessage(packet *ber.Packet) (*messageContext, error) {
}
func (l *Conn) sendMessageWithFlags(packet *ber.Packet, flags sendMessageFlags) (*messageContext, error) {
if l.isClosing {
if l.isClosing() {
return nil, NewError(ErrorNetwork, errors.New("ldap: connection closed"))
}
l.messageMutex.Lock()
@@ -297,7 +304,7 @@ func (l *Conn) sendMessageWithFlags(packet *ber.Packet, flags sendMessageFlags)
func (l *Conn) finishMessage(msgCtx *messageContext) {
close(msgCtx.done)
if l.isClosing {
if l.isClosing() {
return
}
@@ -316,12 +323,12 @@ func (l *Conn) finishMessage(msgCtx *messageContext) {
}
func (l *Conn) sendProcessMessage(message *messagePacket) bool {
if l.isClosing {
l.messageMutex.Lock()
defer l.messageMutex.Unlock()
if l.isClosing() {
return false
}
l.wgSender.Add(1)
l.chanMessage <- message
l.wgSender.Done()
return true
}
@@ -333,15 +340,14 @@ func (l *Conn) processMessages() {
for messageID, msgCtx := range l.messageContexts {
// If we are closing due to an error, inform anyone who
// is waiting about the error.
if l.isClosing && l.closeErr != nil {
msgCtx.sendResponse(&PacketResponse{Error: l.closeErr})
if l.isClosing() && l.closeErr.Load() != nil {
msgCtx.sendResponse(&PacketResponse{Error: l.closeErr.Load().(error)})
}
l.Debug.Printf("Closing channel for MessageID %d", messageID)
close(msgCtx.responses)
delete(l.messageContexts, messageID)
}
close(l.chanMessageID)
l.chanConfirm <- true
close(l.chanConfirm)
}()
@@ -350,11 +356,7 @@ func (l *Conn) processMessages() {
select {
case l.chanMessageID <- messageID:
messageID++
case message, ok := <-l.chanMessage:
if !ok {
l.Debug.Printf("Shutting down - message channel is closed")
return
}
case message := <-l.chanMessage:
switch message.Op {
case MessageQuit:
l.Debug.Printf("Shutting down - quit message received")
@@ -377,14 +379,15 @@ func (l *Conn) processMessages() {
l.messageContexts[message.MessageID] = message.Context
// Add timeout if defined
if l.requestTimeout > 0 {
requestTimeout := time.Duration(atomic.LoadInt64(&l.requestTimeout))
if requestTimeout > 0 {
go func() {
defer func() {
if err := recover(); err != nil {
log.Printf("ldap: recovered panic in RequestTimeout: %v", err)
}
}()
time.Sleep(l.requestTimeout)
time.Sleep(requestTimeout)
timeoutMessage := &messagePacket{
Op: MessageTimeout,
MessageID: message.MessageID,
@@ -397,7 +400,7 @@ func (l *Conn) processMessages() {
if msgCtx, ok := l.messageContexts[message.MessageID]; ok {
msgCtx.sendResponse(&PacketResponse{message.Packet, nil})
} else {
log.Printf("Received unexpected message %d, %v", message.MessageID, l.isClosing)
log.Printf("Received unexpected message %d, %v", message.MessageID, l.isClosing())
ber.PrintPacket(message.Packet)
}
case MessageTimeout:
@@ -439,8 +442,8 @@ func (l *Conn) reader() {
packet, err := ber.ReadPacket(l.conn)
if err != nil {
// A read error is expected here if we are closing the connection...
if !l.isClosing {
l.closeErr = fmt.Errorf("unable to read LDAP response packet: %s", err)
if !l.isClosing() {
l.closeErr.Store(fmt.Errorf("unable to read LDAP response packet: %s", err))
l.Debug.Printf("reader error: %s", err.Error())
}
return

View File

@@ -60,7 +60,7 @@ func TestUnresponsiveConnection(t *testing.T) {
// TestFinishMessage tests that we do not enter deadlock when a goroutine makes
// a request but does not handle all responses from the server.
func TestConn(t *testing.T) {
func TestFinishMessage(t *testing.T) {
ptc := newPacketTranslatorConn()
defer ptc.Close()
@@ -174,16 +174,12 @@ func testSendUnhandledResponsesAndFinish(t *testing.T, ptc *packetTranslatorConn
}
func runWithTimeout(t *testing.T, timeout time.Duration, f func()) {
runtime.Gosched()
done := make(chan struct{})
go func() {
f()
close(done)
}()
runtime.Gosched()
select {
case <-done: // Success!
case <-time.After(timeout):
@@ -192,7 +188,7 @@ func runWithTimeout(t *testing.T, timeout time.Duration, f func()) {
}
}
// packetTranslatorConn is a helful type which can be used with various tests
// packetTranslatorConn is a helpful type which can be used with various tests
// in this package. It implements the net.Conn interface to be used as an
// underlying connection for a *ldap.Conn. Most methods are no-ops but the
// Read() and Write() methods are able to translate ber-encoded packets for
@@ -245,7 +241,7 @@ func (c *packetTranslatorConn) Read(b []byte) (n int, err error) {
}
// SendResponse writes the given response packet to the response buffer for
// this conection, signalling any goroutine waiting to read a response.
// this connection, signalling any goroutine waiting to read a response.
func (c *packetTranslatorConn) SendResponse(packet *ber.Packet) error {
c.lock.Lock()
defer c.lock.Unlock()

View File

@@ -6,7 +6,7 @@ import (
"gopkg.in/asn1-ber.v1"
)
// debbuging type
// debugging type
// - has a Printf method to write the debug output
type debugging bool

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// File contains DN parsing functionallity
// File contains DN parsing functionality
//
// https://tools.ietf.org/html/rfc4514
//
@@ -52,7 +52,7 @@ import (
"fmt"
"strings"
ber "gopkg.in/asn1-ber.v1"
"gopkg.in/asn1-ber.v1"
)
// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514
@@ -143,6 +143,9 @@ func ParseDN(str string) (*DN, error) {
}
} else if char == ',' || char == '+' {
// We're done with this RDN or value, push it
if len(attribute.Type) == 0 {
return nil, errors.New("incomplete type, value pair")
}
attribute.Value = stringFromBuffer()
rdn.Attributes = append(rdn.Attributes, attribute)
attribute = new(AttributeTypeAndValue)

View File

@@ -75,11 +75,13 @@ func TestSuccessfulDNParsing(t *testing.T) {
func TestErrorDNParsing(t *testing.T) {
testcases := map[string]string{
"*": "DN ended with incomplete type, value pair",
"cn=Jim\\0Test": "Failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'",
"cn=Jim\\0": "Got corrupted escaped character",
"DC=example,=net": "DN ended with incomplete type, value pair",
"1=#0402486": "Failed to decode BER encoding: encoding/hex: odd length hex string",
"*": "DN ended with incomplete type, value pair",
"cn=Jim\\0Test": "Failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'",
"cn=Jim\\0": "Got corrupted escaped character",
"DC=example,=net": "DN ended with incomplete type, value pair",
"1=#0402486": "Failed to decode BER encoding: encoding/hex: odd length hex string",
"test,DC=example,DC=com": "incomplete type, value pair",
"=test,DC=example,DC=com": "incomplete type, value pair",
}
for test, answer := range testcases {

View File

@@ -97,6 +97,13 @@ var LDAPResultCodeMap = map[uint8]string{
LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited",
LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs",
LDAPResultOther: "Other",
ErrorNetwork: "Network Error",
ErrorFilterCompile: "Filter Compile Error",
ErrorFilterDecompile: "Filter Decompile Error",
ErrorDebugging: "Debugging Error",
ErrorUnexpectedMessage: "Unexpected Message",
ErrorUnexpectedResponse: "Unexpected Response",
}
func getLDAPResultCode(packet *ber.Packet) (code uint8, description string) {

View File

@@ -49,7 +49,7 @@ func TestConnReadErr(t *testing.T) {
// Send the signal after a short amount of time.
time.AfterFunc(10*time.Millisecond, func() { conn.signals <- expectedError })
// This should block until the underlyiny conn gets the error signal
// This should block until the underlying conn gets the error signal
// which should bubble up through the reader() goroutine, close the
// connection, and
_, err := ldapConn.Search(searchReq)
@@ -58,7 +58,7 @@ func TestConnReadErr(t *testing.T) {
}
}
// signalErrConn is a helful type used with TestConnReadErr. It implements the
// signalErrConn is a helpful type used with TestConnReadErr. It implements the
// net.Conn interface to be used as a connection for the test. Most methods are
// no-ops but the Read() method blocks until it receives a signal which it
// returns as an error.

View File

@@ -9,7 +9,7 @@ import (
)
// ExampleConn_Bind demonstrates how to bind a connection to an ldap user
// allowing access to restricted attrabutes that user has access to
// allowing access to restricted attributes that user has access to
func ExampleConn_Bind() {
l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389))
if err != nil {
@@ -63,10 +63,10 @@ func ExampleConn_StartTLS() {
log.Fatal(err)
}
// Opertations via l are now encrypted
// Operations via l are now encrypted
}
// ExampleConn_Compare demonstrates how to comapre an attribute with a value
// ExampleConn_Compare demonstrates how to compare an attribute with a value
func ExampleConn_Compare() {
l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389))
if err != nil {
@@ -193,7 +193,7 @@ func Example_userAuthentication() {
searchRequest := ldap.NewSearchRequest(
"dc=example,dc=com",
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass=organizationalPerson)&(uid=%s))", username),
fmt.Sprintf("(&(objectClass=organizationalPerson)(uid=%s))", username),
[]string{"dn"},
nil,
)
@@ -215,7 +215,7 @@ func Example_userAuthentication() {
log.Fatal(err)
}
// Rebind as the read only user for any futher queries
// Rebind as the read only user for any further queries
err = l.Bind(bindusername, bindpassword)
if err != nil {
log.Fatal(err)
@@ -240,7 +240,7 @@ func Example_beherappolicy() {
if ppolicyControl != nil {
ppolicy = ppolicyControl.(*ldap.ControlBeheraPasswordPolicy)
} else {
log.Printf("ppolicyControl response not avaliable.\n")
log.Printf("ppolicyControl response not available.\n")
}
if err != nil {
errStr := "ERROR: Cannot bind: " + err.Error()

View File

@@ -82,7 +82,10 @@ func CompileFilter(filter string) (*ber.Packet, error) {
if err != nil {
return nil, err
}
if pos != len(filter) {
switch {
case pos > len(filter):
return nil, NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter"))
case pos < len(filter):
return nil, NewError(ErrorFilterCompile, errors.New("ldap: finished compiling filter with extra at end: "+fmt.Sprint(filter[pos:])))
}
return packet, nil

View File

@@ -131,6 +131,12 @@ var testFilters = []compileTest{
expectedType: 0,
expectedErr: "unexpected end of filter",
},
compileTest{
filterStr: `((cn=)`,
expectedFilter: ``,
expectedType: 0,
expectedErr: "unexpected end of filter",
},
compileTest{
filterStr: `(&(objectclass=inetorgperson)(cn=中文))`,
expectedFilter: `(&(objectclass=inetorgperson)(cn=\e4\b8\ad\e6\96\87))`,

View File

@@ -9,7 +9,7 @@ import (
"io/ioutil"
"os"
ber "gopkg.in/asn1-ber.v1"
"gopkg.in/asn1-ber.v1"
)
// LDAP Application Codes

View File

@@ -135,10 +135,10 @@ func (l *Conn) PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*Pa
extendedResponse := packet.Children[1]
for _, child := range extendedResponse.Children {
if child.Tag == 11 {
passwordModifyReponseValue := ber.DecodePacket(child.Data.Bytes())
if len(passwordModifyReponseValue.Children) == 1 {
if passwordModifyReponseValue.Children[0].Tag == 0 {
result.GeneratedPassword = ber.DecodeString(passwordModifyReponseValue.Children[0].Data.Bytes())
passwordModifyResponseValue := ber.DecodePacket(child.Data.Bytes())
if len(passwordModifyResponseValue.Children) == 1 {
if passwordModifyResponseValue.Children[0].Tag == 0 {
result.GeneratedPassword = ber.DecodeString(passwordModifyResponseValue.Children[0].Data.Bytes())
}
}
}

View File

@@ -15,7 +15,7 @@ func TestNewEntry(t *testing.T) {
"delta": {"value"},
"epsilon": {"value"},
}
exectedEntry := NewEntry(dn, attributes)
executedEntry := NewEntry(dn, attributes)
iteration := 0
for {
@@ -23,8 +23,8 @@ func TestNewEntry(t *testing.T) {
break
}
testEntry := NewEntry(dn, attributes)
if !reflect.DeepEqual(exectedEntry, testEntry) {
t.Fatalf("consequent calls to NewEntry did not yield the same result:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", exectedEntry, testEntry)
if !reflect.DeepEqual(executedEntry, testEntry) {
t.Fatalf("subsequent calls to NewEntry did not yield the same result:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", executedEntry, testEntry)
}
iteration = iteration + 1
}

View File

@@ -5,7 +5,6 @@ services:
- redis-server
go:
- 1.4.x
- 1.7.x
- 1.8.x
- 1.9.x
@@ -13,7 +12,6 @@ go:
matrix:
allow_failures:
- go: 1.4.x
- go: tip
install:

View File

@@ -2,6 +2,7 @@
[![Build Status](https://travis-ci.org/go-redis/redis.png?branch=master)](https://travis-ci.org/go-redis/redis)
[![GoDoc](https://godoc.org/github.com/go-redis/redis?status.svg)](https://godoc.org/github.com/go-redis/redis)
[![Airbrake](https://img.shields.io/badge/kudos-airbrake.io-orange.svg)](https://airbrake.io)
Supports:
@@ -66,14 +67,14 @@ func ExampleClient() {
val2, err := client.Get("key2").Result()
if err == redis.Nil {
fmt.Println("key2 does not exists")
fmt.Println("key2 does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("key2", val2)
}
// Output: key value
// key2 does not exists
// key2 does not exist
}
```

View File

@@ -226,7 +226,7 @@ func (c *clusterNodes) NextGeneration() uint32 {
}
// GC removes unused nodes.
func (c *clusterNodes) GC(generation uint32) error {
func (c *clusterNodes) GC(generation uint32) {
var collected []*clusterNode
c.mu.Lock()
for i := 0; i < len(c.addrs); {
@@ -243,14 +243,11 @@ func (c *clusterNodes) GC(generation uint32) error {
}
c.mu.Unlock()
var firstErr error
for _, node := range collected {
if err := node.Client.Close(); err != nil && firstErr == nil {
firstErr = err
time.AfterFunc(time.Minute, func() {
for _, node := range collected {
_ = node.Client.Close()
}
}
return firstErr
})
}
func (c *clusterNodes) All() ([]*clusterNode, error) {
@@ -533,16 +530,22 @@ func (c *ClusterClient) cmdInfo(name string) *CommandInfo {
return info
}
func cmdSlot(cmd Cmder, pos int) int {
if pos == 0 {
return hashtag.RandomSlot()
}
firstKey := cmd.stringArg(pos)
return hashtag.Slot(firstKey)
}
func (c *ClusterClient) cmdSlot(cmd Cmder) int {
cmdInfo := c.cmdInfo(cmd.Name())
firstKey := cmd.stringArg(cmdFirstKeyPos(cmd, cmdInfo))
return hashtag.Slot(firstKey)
return cmdSlot(cmd, cmdFirstKeyPos(cmd, cmdInfo))
}
func (c *ClusterClient) cmdSlotAndNode(state *clusterState, cmd Cmder) (int, *clusterNode, error) {
cmdInfo := c.cmdInfo(cmd.Name())
firstKey := cmd.stringArg(cmdFirstKeyPos(cmd, cmdInfo))
slot := hashtag.Slot(firstKey)
slot := cmdSlot(cmd, cmdFirstKeyPos(cmd, cmdInfo))
if cmdInfo != nil && cmdInfo.ReadOnly && c.opt.ReadOnly {
if c.opt.RouteByLatency {
@@ -590,6 +593,10 @@ func (c *ClusterClient) Watch(fn func(*Tx) error, keys ...string) error {
break
}
if internal.IsRetryableError(err, true) {
continue
}
moved, ask, addr := internal.IsMovedError(err)
if moved || ask {
c.lazyReloadState()
@@ -600,6 +607,13 @@ func (c *ClusterClient) Watch(fn func(*Tx) error, keys ...string) error {
continue
}
if err == pool.ErrClosed {
node, err = state.slotMasterNode(slot)
if err != nil {
return err
}
}
return err
}
@@ -635,10 +649,10 @@ func (c *ClusterClient) Process(cmd Cmder) error {
if ask {
pipe := node.Client.Pipeline()
pipe.Process(NewCmd("ASKING"))
pipe.Process(cmd)
_ = pipe.Process(NewCmd("ASKING"))
_ = pipe.Process(cmd)
_, err = pipe.Exec()
pipe.Close()
_ = pipe.Close()
ask = false
} else {
err = node.Client.Process(cmd)
@@ -679,6 +693,14 @@ func (c *ClusterClient) Process(cmd Cmder) error {
continue
}
if err == pool.ErrClosed {
_, node, err = c.cmdSlotAndNode(state, cmd)
if err != nil {
cmd.setErr(err)
return err
}
}
break
}
@@ -915,7 +937,11 @@ func (c *ClusterClient) pipelineExec(cmds []Cmder) error {
for node, cmds := range cmdsMap {
cn, _, err := node.Client.getConn()
if err != nil {
setCmdsErr(cmds, err)
if err == pool.ErrClosed {
c.remapCmds(cmds, failedCmds)
} else {
setCmdsErr(cmds, err)
}
continue
}
@@ -955,6 +981,18 @@ func (c *ClusterClient) mapCmdsByNode(cmds []Cmder) (map[*clusterNode][]Cmder, e
return cmdsMap, nil
}
func (c *ClusterClient) remapCmds(cmds []Cmder, failedCmds map[*clusterNode][]Cmder) {
remappedCmds, err := c.mapCmdsByNode(cmds)
if err != nil {
setCmdsErr(cmds, err)
return
}
for node, cmds := range remappedCmds {
failedCmds[node] = cmds
}
}
func (c *ClusterClient) pipelineProcessCmds(
node *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds map[*clusterNode][]Cmder,
) error {
@@ -1061,7 +1099,11 @@ func (c *ClusterClient) txPipelineExec(cmds []Cmder) error {
for node, cmds := range cmdsMap {
cn, _, err := node.Client.getConn()
if err != nil {
setCmdsErr(cmds, err)
if err == pool.ErrClosed {
c.remapCmds(cmds, failedCmds)
} else {
setCmdsErr(cmds, err)
}
continue
}

View File

@@ -536,6 +536,32 @@ var _ = Describe("ClusterClient", func() {
Expect(nodesList).Should(HaveLen(1))
})
It("should RANDOMKEY", func() {
const nkeys = 100
for i := 0; i < nkeys; i++ {
err := client.Set(fmt.Sprintf("key%d", i), "value", 0).Err()
Expect(err).NotTo(HaveOccurred())
}
var keys []string
addKey := func(key string) {
for _, k := range keys {
if k == key {
return
}
}
keys = append(keys, key)
}
for i := 0; i < nkeys*10; i++ {
key := client.RandomKey().Val()
addKey(key)
}
Expect(len(keys)).To(BeNumerically("~", nkeys, nkeys/10))
})
assertClusterClient()
})

View File

@@ -82,13 +82,13 @@ func cmdFirstKeyPos(cmd Cmder, info *CommandInfo) int {
if cmd.stringArg(2) != "0" {
return 3
} else {
return -1
return 0
}
case "publish":
return 1
}
if info == nil {
return -1
return 0
}
return int(info.FirstKeyPos)
}
@@ -675,6 +675,44 @@ func (cmd *StringIntMapCmd) readReply(cn *pool.Conn) error {
//------------------------------------------------------------------------------
type StringStructMapCmd struct {
baseCmd
val map[string]struct{}
}
var _ Cmder = (*StringStructMapCmd)(nil)
func NewStringStructMapCmd(args ...interface{}) *StringStructMapCmd {
return &StringStructMapCmd{
baseCmd: baseCmd{_args: args},
}
}
func (cmd *StringStructMapCmd) Val() map[string]struct{} {
return cmd.val
}
func (cmd *StringStructMapCmd) Result() (map[string]struct{}, error) {
return cmd.val, cmd.err
}
func (cmd *StringStructMapCmd) String() string {
return cmdString(cmd, cmd.val)
}
func (cmd *StringStructMapCmd) readReply(cn *pool.Conn) error {
var v interface{}
v, cmd.err = cn.Rd.ReadArrayReply(stringStructMapParser)
if cmd.err != nil {
return cmd.err
}
cmd.val = v.(map[string]struct{})
return nil
}
//------------------------------------------------------------------------------
type ZSliceCmd struct {
baseCmd

View File

@@ -143,6 +143,7 @@ type Cmdable interface {
SInterStore(destination string, keys ...string) *IntCmd
SIsMember(key string, member interface{}) *BoolCmd
SMembers(key string) *StringSliceCmd
SMembersMap(key string) *StringStructMapCmd
SMove(source, destination string, member interface{}) *BoolCmd
SPop(key string) *StringCmd
SPopN(key string, count int64) *StringSliceCmd
@@ -676,6 +677,7 @@ func (c *cmdable) DecrBy(key string, decrement int64) *IntCmd {
return cmd
}
// Redis `GET key` command. It returns redis.Nil error when key does not exist.
func (c *cmdable) Get(key string) *StringCmd {
cmd := NewStringCmd("get", key)
c.process(cmd)
@@ -1163,12 +1165,20 @@ func (c *cmdable) SIsMember(key string, member interface{}) *BoolCmd {
return cmd
}
// Redis `SMEMBERS key` command output as a slice
func (c *cmdable) SMembers(key string) *StringSliceCmd {
cmd := NewStringSliceCmd("smembers", key)
c.process(cmd)
return cmd
}
// Redis `SMEMBERS key` command output as a map
func (c *cmdable) SMembersMap(key string) *StringStructMapCmd {
cmd := NewStringStructMapCmd("smembers", key)
c.process(cmd)
return cmd
}
func (c *cmdable) SMove(source, destination string, member interface{}) *BoolCmd {
cmd := NewBoolCmd("smove", source, destination, member)
c.process(cmd)

View File

@@ -1848,6 +1848,17 @@ var _ = Describe("Commands", func() {
Expect(sMembers.Val()).To(ConsistOf([]string{"Hello", "World"}))
})
It("should SMembersMap", func() {
sAdd := client.SAdd("set", "Hello")
Expect(sAdd.Err()).NotTo(HaveOccurred())
sAdd = client.SAdd("set", "World")
Expect(sAdd.Err()).NotTo(HaveOccurred())
sMembersMap := client.SMembersMap("set")
Expect(sMembersMap.Err()).NotTo(HaveOccurred())
Expect(sMembersMap.Val()).To(Equal(map[string]struct{}{"Hello": struct{}{}, "World": struct{}{}}))
})
It("should SMove", func() {
sAdd := client.SAdd("set1", "one")
Expect(sAdd.Err()).NotTo(HaveOccurred())

View File

@@ -96,14 +96,14 @@ func ExampleClient() {
val2, err := client.Get("key2").Result()
if err == redis.Nil {
fmt.Println("key2 does not exists")
fmt.Println("key2 does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("key2", val2)
}
// Output: key value
// key2 does not exists
// key2 does not exist
}
func ExampleClient_Set() {

View File

@@ -55,13 +55,17 @@ func Key(key string) string {
return key
}
func RandomSlot() int {
return rand.Intn(SlotNumber)
}
// hashSlot returns a consistent slot number between 0 and 16383
// for any given string key.
func Slot(key string) int {
key = Key(key)
if key == "" {
return rand.Intn(SlotNumber)
return RandomSlot()
}
key = Key(key)
return int(crc16sum(key)) % SlotNumber
}

View File

@@ -123,8 +123,9 @@ func ScanSlice(data []string, slice interface{}) error {
next := internal.MakeSliceNextElemFunc(v)
for i, s := range data {
elem := next()
if err := Scan(internal.StringToBytes(s), elem.Addr().Interface()); err != nil {
return fmt.Errorf("redis: ScanSlice(index=%d value=%q) failed: %s", i, s, err)
if err := Scan([]byte(s), elem.Addr().Interface()); err != nil {
err = fmt.Errorf("redis: ScanSlice index=%d value=%q failed: %s", i, s, err)
return err
}
}

View File

@@ -5,7 +5,3 @@ package internal
func BytesToString(b []byte) string {
return string(b)
}
func StringToBytes(s string) []byte {
return []byte(s)
}

View File

@@ -3,25 +3,10 @@
package internal
import (
"reflect"
"unsafe"
)
// BytesToString converts byte slice to string.
func BytesToString(b []byte) string {
bytesHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
strHeader := reflect.StringHeader{
Data: bytesHeader.Data,
Len: bytesHeader.Len,
}
return *(*string)(unsafe.Pointer(&strHeader))
}
func StringToBytes(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
return *(*string)(unsafe.Pointer(&b))
}

View File

@@ -71,7 +71,7 @@ func TestParseURL(t *testing.T) {
t.Run(c.u, func(t *testing.T) {
o, err := ParseURL(c.u)
if c.err == nil && err != nil {
t.Fatalf("unexpected error: '%q'", err)
t.Fatalf("unexpected error: %q", err)
return
}
if c.err != nil && err != nil {

View File

@@ -97,6 +97,20 @@ func stringIntMapParser(rd *proto.Reader, n int64) (interface{}, error) {
return m, nil
}
// Implements proto.MultiBulkParse
func stringStructMapParser(rd *proto.Reader, n int64) (interface{}, error) {
m := make(map[string]struct{}, n)
for i := int64(0); i < n; i++ {
key, err := rd.ReadStringReply()
if err != nil {
return nil, err
}
m[key] = struct{}{}
}
return m, nil
}
// Implements proto.MultiBulkParse
func zSliceParser(rd *proto.Reader, n int64) (interface{}, error) {
zz := make([]Z, n/2)

View File

@@ -11,7 +11,7 @@ import (
"github.com/go-redis/redis/internal/proto"
)
// Redis nil reply, .e.g. when key does not exist.
// Redis nil reply returned when key does not exist.
const Nil = internal.Nil
func init() {

View File

@@ -298,6 +298,9 @@ func (c *Ring) cmdInfo(name string) *CommandInfo {
if err != nil {
return nil
}
if c.cmdsInfo == nil {
return nil
}
info := c.cmdsInfo[name]
if info == nil {
internal.Logf("info for cmd=%s not found", name)
@@ -343,7 +346,11 @@ func (c *Ring) shardByName(name string) (*ringShard, error) {
func (c *Ring) cmdShard(cmd Cmder) (*ringShard, error) {
cmdInfo := c.cmdInfo(cmd.Name())
firstKey := cmd.stringArg(cmdFirstKeyPos(cmd, cmdInfo))
pos := cmdFirstKeyPos(cmd, cmdInfo)
if pos == 0 {
return c.randomShard()
}
firstKey := cmd.stringArg(pos)
return c.shardByKey(firstKey)
}

View File

@@ -114,6 +114,8 @@ func (o *UniversalOptions) simple() *Options {
type UniversalClient interface {
Cmdable
Process(cmd Cmder) error
Subscribe(channels ...string) *PubSub
PSubscribe(channels ...string) *PubSub
Close() error
}

View File

@@ -6,3 +6,4 @@
Icon?
ehthumbs.db
Thumbs.db
.idea

View File

@@ -1,13 +1,92 @@
sudo: false
language: go
go:
- 1.2
- 1.3
- 1.4
- 1.5
- 1.6
- 1.7
- tip
- 1.7.x
- 1.8.x
- 1.9.x
- master
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
before_script:
- echo -e "[server]\ninnodb_log_file_size=256MB\ninnodb_buffer_pool_size=512MB\nmax_allowed_packet=16MB" | sudo tee -a /etc/mysql/my.cnf
- sudo service mysql restart
- .travis/wait_mysql.sh
- mysql -e 'create database gotest;'
matrix:
include:
- env: DB=MYSQL57
sudo: required
dist: trusty
go: 1.9.x
services:
- docker
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- docker pull mysql:5.7
- docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
mysql:5.7 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB
- sleep 30
- cp .travis/docker.cnf ~/.my.cnf
- mysql --print-defaults
- .travis/wait_mysql.sh
before_script:
- export MYSQL_TEST_USER=gotest
- export MYSQL_TEST_PASS=secret
- export MYSQL_TEST_ADDR=127.0.0.1:3307
- export MYSQL_TEST_CONCURRENT=1
- env: DB=MARIA55
sudo: required
dist: trusty
go: 1.9.x
services:
- docker
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- docker pull mariadb:5.5
- docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
mariadb:5.5 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB
- sleep 30
- cp .travis/docker.cnf ~/.my.cnf
- mysql --print-defaults
- .travis/wait_mysql.sh
before_script:
- export MYSQL_TEST_USER=gotest
- export MYSQL_TEST_PASS=secret
- export MYSQL_TEST_ADDR=127.0.0.1:3307
- export MYSQL_TEST_CONCURRENT=1
- env: DB=MARIA10_1
sudo: required
dist: trusty
go: 1.9.x
services:
- docker
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- docker pull mariadb:10.1
- docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
mariadb:10.1 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB
- sleep 30
- cp .travis/docker.cnf ~/.my.cnf
- mysql --print-defaults
- .travis/wait_mysql.sh
before_script:
- export MYSQL_TEST_USER=gotest
- export MYSQL_TEST_PASS=secret
- export MYSQL_TEST_ADDR=127.0.0.1:3307
- export MYSQL_TEST_CONCURRENT=1
script:
- go test -v -covermode=count -coverprofile=coverage.out
- go vet ./...
- test -z "$(gofmt -d -s . | tee /dev/stderr)"
after_script:
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci

View File

@@ -0,0 +1,5 @@
[client]
user = gotest
password = secret
host = 127.0.0.1
port = 3307

View File

@@ -0,0 +1,8 @@
#!/bin/sh
while :
do
sleep 3
if mysql -e 'select version()'; then
break
fi
done

View File

@@ -12,35 +12,58 @@
# Individual Persons
Aaron Hopkins <go-sql-driver at die.net>
Achille Roussel <achille.roussel at gmail.com>
Arne Hormann <arnehormann at gmail.com>
Asta Xie <xiemengjun at gmail.com>
Bulat Gaifullin <gaifullinbf at gmail.com>
Carlos Nieto <jose.carlos at menteslibres.net>
Chris Moos <chris at tech9computers.com>
Daniel Montoya <dsmontoyam at gmail.com>
Daniel Nichter <nil at codenode.com>
Daniël van Eeden <git at myname.nl>
Dave Protasowski <dprotaso at gmail.com>
DisposaBoy <disposaboy at dby.me>
Egor Smolyakov <egorsmkv at gmail.com>
Evan Shaw <evan at vendhq.com>
Frederick Mayle <frederickmayle at gmail.com>
Gustavo Kristic <gkristic at gmail.com>
Hanno Braun <mail at hannobraun.com>
Henri Yandell <flamefew at gmail.com>
Hirotaka Yamamoto <ymmt2005 at gmail.com>
ICHINOSE Shogo <shogo82148 at gmail.com>
INADA Naoki <songofacandy at gmail.com>
Jacek Szwec <szwec.jacek at gmail.com>
James Harr <james.harr at gmail.com>
Jeff Hodges <jeff at somethingsimilar.com>
Jeffrey Charles <jeffreycharles at gmail.com>
Jian Zhen <zhenjl at gmail.com>
Joshua Prunier <joshua.prunier at gmail.com>
Julien Lefevre <julien.lefevr at gmail.com>
Julien Schmidt <go-sql-driver at julienschmidt.com>
Justin Li <jli at j-li.net>
Justin Nuß <nuss.justin at gmail.com>
Kamil Dziedzic <kamil at klecza.pl>
Kevin Malachowski <kevin at chowski.com>
Kieron Woodhouse <kieron.woodhouse at infosum.com>
Lennart Rudolph <lrudolph at hmc.edu>
Leonardo YongUk Kim <dalinaum at gmail.com>
Linh Tran Tuan <linhduonggnu at gmail.com>
Lion Yang <lion at aosc.xyz>
Luca Looz <luca.looz92 at gmail.com>
Lucas Liu <extrafliu at gmail.com>
Luke Scott <luke at webconnex.com>
Maciej Zimnoch <maciej.zimnoch@codilime.com>
Michael Woolnough <michael.woolnough at gmail.com>
Nicola Peduzzi <thenikso at gmail.com>
Olivier Mengué <dolmen at cpan.org>
oscarzhao <oscarzhaosl at gmail.com>
Paul Bonser <misterpib at gmail.com>
Peter Schultz <peter.schultz at classmarkets.com>
Rebecca Chin <rchin at pivotal.io>
Reed Allman <rdallman10 at gmail.com>
Runrioter Wung <runrioter at gmail.com>
Robert Russell <robert at rrbrussell.com>
Shuode Li <elemount at qq.com>
Soroush Pour <me at soroushjp.com>
Stan Putrya <root.vagner at gmail.com>
Stanley Gunawan <gunawan.stanley at gmail.com>
@@ -52,5 +75,9 @@ Zhenye Xie <xiezhenye at gmail.com>
# Organizations
Barracuda Networks, Inc.
Counting Ltd.
Google Inc.
InfoSum Ltd.
Keybase Inc.
Pivotal Inc.
Stripe Inc.

View File

@@ -1,6 +1,6 @@
# Go-MySQL-Driver
A MySQL-Driver for Go's [database/sql](http://golang.org/pkg/database/sql) package
A MySQL-Driver for Go's [database/sql](https://golang.org/pkg/database/sql/) package
![Go-MySQL-Driver logo](https://raw.github.com/wiki/go-sql-driver/mysql/gomysql_m.png "Golang Gopher holding the MySQL Dolphin")
@@ -15,6 +15,9 @@ A MySQL-Driver for Go's [database/sql](http://golang.org/pkg/database/sql) packa
* [Address](#address)
* [Parameters](#parameters)
* [Examples](#examples)
* [Connection pool and timeouts](#connection-pool-and-timeouts)
* [context.Context Support](#contextcontext-support)
* [ColumnType Support](#columntype-support)
* [LOAD DATA LOCAL INFILE support](#load-data-local-infile-support)
* [time.Time support](#timetime-support)
* [Unicode support](#unicode-support)
@@ -26,31 +29,31 @@ A MySQL-Driver for Go's [database/sql](http://golang.org/pkg/database/sql) packa
## Features
* Lightweight and [fast](https://github.com/go-sql-driver/sql-benchmark "golang MySQL-Driver performance")
* Native Go implementation. No C-bindings, just pure Go
* Connections over TCP/IPv4, TCP/IPv6, Unix domain sockets or [custom protocols](http://godoc.org/github.com/go-sql-driver/mysql#DialFunc)
* Connections over TCP/IPv4, TCP/IPv6, Unix domain sockets or [custom protocols](https://godoc.org/github.com/go-sql-driver/mysql#DialFunc)
* Automatic handling of broken connections
* Automatic Connection Pooling *(by database/sql package)*
* Supports queries larger than 16MB
* Full [`sql.RawBytes`](http://golang.org/pkg/database/sql/#RawBytes) support.
* Full [`sql.RawBytes`](https://golang.org/pkg/database/sql/#RawBytes) support.
* Intelligent `LONG DATA` handling in prepared statements
* Secure `LOAD DATA LOCAL INFILE` support with file Whitelisting and `io.Reader` support
* Optional `time.Time` parsing
* Optional placeholder interpolation
## Requirements
* Go 1.2 or higher
* Go 1.7 or higher. We aim to support the 3 latest versions of Go.
* MySQL (4.1+), MariaDB, Percona Server, Google CloudSQL or Sphinx (2.2.3+)
---------------------------------------
## Installation
Simple install the package to your [$GOPATH](http://code.google.com/p/go-wiki/wiki/GOPATH "GOPATH") with the [go tool](http://golang.org/cmd/go/ "go command") from shell:
Simple install the package to your [$GOPATH](https://github.com/golang/go/wiki/GOPATH "GOPATH") with the [go tool](https://golang.org/cmd/go/ "go command") from shell:
```bash
$ go get github.com/go-sql-driver/mysql
$ go get -u github.com/go-sql-driver/mysql
```
Make sure [Git is installed](http://git-scm.com/downloads) on your machine and in your system's `PATH`.
Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`.
## Usage
_Go MySQL Driver_ is an implementation of Go's `database/sql/driver` interface. You only need to import the driver and can use the full [`database/sql`](http://golang.org/pkg/database/sql) API then.
_Go MySQL Driver_ is an implementation of Go's `database/sql/driver` interface. You only need to import the driver and can use the full [`database/sql`](https://golang.org/pkg/database/sql/) API then.
Use `mysql` as `driverName` and a valid [DSN](#dsn-data-source-name) as `dataSourceName`:
```go
@@ -95,13 +98,14 @@ Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mys
Passwords can consist of any character. Escaping is **not** necessary.
#### Protocol
See [net.Dial](http://golang.org/pkg/net/#Dial) for more information which networks are available.
See [net.Dial](https://golang.org/pkg/net/#Dial) for more information which networks are available.
In general you should use an Unix domain socket if available and TCP otherwise for best performance.
#### Address
For TCP and UDP networks, addresses have the form `host:port`.
For TCP and UDP networks, addresses have the form `host[:port]`.
If `port` is omitted, the default port will be used.
If `host` is a literal IPv6 address, it must be enclosed in square brackets.
The functions [net.JoinHostPort](http://golang.org/pkg/net/#JoinHostPort) and [net.SplitHostPort](http://golang.org/pkg/net/#SplitHostPort) manipulate addresses in this form.
The functions [net.JoinHostPort](https://golang.org/pkg/net/#JoinHostPort) and [net.SplitHostPort](https://golang.org/pkg/net/#SplitHostPort) manipulate addresses in this form.
For Unix domain sockets the address is the absolute path to the MySQL-Server-socket, e.g. `/var/run/mysqld/mysqld.sock` or `/tmp/mysql.sock`.
@@ -136,9 +140,9 @@ Default: false
```
Type: bool
Valid Values: true, false
Default: false
Default: true
```
`allowNativePasswords=true` allows the usage of the mysql native password method.
`allowNativePasswords=false` disallows the usage of MySQL native password method.
##### `allowOldPasswords`
@@ -220,19 +224,19 @@ Valid Values: <escaped name>
Default: UTC
```
Sets the location for time.Time values (when using `parseTime=true`). *"Local"* sets the system's location. See [time.LoadLocation](http://golang.org/pkg/time/#LoadLocation) for details.
Sets the location for time.Time values (when using `parseTime=true`). *"Local"* sets the system's location. See [time.LoadLocation](https://golang.org/pkg/time/#LoadLocation) for details.
Note that this sets the location for time.Time values but does not change MySQL's [time_zone setting](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html). For that see the [time_zone system variable](#system-variables), which can also be set as a DSN parameter.
Please keep in mind, that param values must be [url.QueryEscape](http://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`.
Please keep in mind, that param values must be [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`.
##### `maxAllowedPacket`
```
Type: decimal number
Default: 0
Default: 4194304
```
Max packet size allowed in bytes. Use `maxAllowedPacket=0` to automatically fetch the `max_allowed_packet` variable from server.
Max packet size allowed in bytes. The default value is 4 MiB and should be adjusted to match the server settings. `maxAllowedPacket=0` can be used to automatically fetch the `max_allowed_packet` variable from server *on every connection*.
##### `multiStatements`
@@ -260,13 +264,13 @@ Default: false
##### `readTimeout`
```
Type: decimal number
Type: duration
Default: 0
```
I/O read timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*.
I/O read timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
##### `strict`
##### `rejectReadOnly`
```
Type: bool
@@ -274,20 +278,37 @@ Valid Values: true, false
Default: false
```
`strict=true` enables a driver-side strict mode in which MySQL warnings are treated as errors. This mode should not be used in production as it may lead to data corruption in certain situations.
A server-side strict mode, which is safe for production use, can be set via the [`sql_mode`](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html) system variable.
`rejectReadOnly=true` causes the driver to reject read-only connections. This
is for a possible race condition during an automatic failover, where the mysql
client gets connected to a read-only replica after the failover.
Note that this should be a fairly rare case, as an automatic failover normally
happens when the primary is down, and the race condition shouldn't happen
unless it comes back up online as soon as the failover is kicked off. On the
other hand, when this happens, a MySQL application can get stuck on a
read-only connection until restarted. It is however fairly easy to reproduce,
for example, using a manual failover on AWS Aurora's MySQL-compatible cluster.
If you are not relying on read-only transactions to reject writes that aren't
supposed to happen, setting this on some MySQL providers (such as AWS Aurora)
is safer for failovers.
Note that ERROR 1290 can be returned for a `read-only` server and this option will
cause a retry for that error. However the same error number is used for some
other cases. You should ensure your application will never cause an ERROR 1290
except for `read-only` mode when enabling this option.
By default MySQL also treats notes as warnings. Use [`sql_notes=false`](http://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_sql_notes) to ignore notes.
##### `timeout`
```
Type: decimal number
Type: duration
Default: OS default
```
*Driver* side connection timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. To set a server side timeout, use the parameter [`wait_timeout`](http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_wait_timeout).
Timeout for establishing connections, aka dial timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
##### `tls`
@@ -297,16 +318,17 @@ Valid Values: true, false, skip-verify, <name>
Default: false
```
`tls=true` enables TLS / SSL encrypted connection to the server. Use `skip-verify` if you want to use a self-signed or invalid certificate (server side). Use a custom value registered with [`mysql.RegisterTLSConfig`](http://godoc.org/github.com/go-sql-driver/mysql#RegisterTLSConfig).
`tls=true` enables TLS / SSL encrypted connection to the server. Use `skip-verify` if you want to use a self-signed or invalid certificate (server side). Use a custom value registered with [`mysql.RegisterTLSConfig`](https://godoc.org/github.com/go-sql-driver/mysql#RegisterTLSConfig).
##### `writeTimeout`
```
Type: decimal number
Type: duration
Default: 0
```
I/O write timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*.
I/O write timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
##### System Variables
@@ -317,9 +339,9 @@ Any other parameters are interpreted as system variables:
* `<string_var>=%27<value>%27`: `SET <string_var>='<value>'`
Rules:
* The values for string variables must be quoted with '
* The values for string variables must be quoted with `'`.
* The values must also be [url.QueryEscape](http://golang.org/pkg/net/url/#QueryEscape)'ed!
(which implies values of string variables must be wrapped with `%27`)
(which implies values of string variables must be wrapped with `%27`).
Examples:
* `autocommit=1`: `SET autocommit=1`
@@ -380,6 +402,18 @@ No Database preselected:
user:password@/
```
### Connection pool and timeouts
The connection pool is managed by Go's database/sql package. For details on how to configure the size of the pool and how long connections stay in the pool see `*DB.SetMaxOpenConns`, `*DB.SetMaxIdleConns`, and `*DB.SetConnMaxLifetime` in the [database/sql documentation](https://golang.org/pkg/database/sql/). The read, write, and dial timeouts for each individual connection are configured with the DSN parameters [`readTimeout`](#readtimeout), [`writeTimeout`](#writetimeout), and [`timeout`](#timeout), respectively.
## `ColumnType` Support
This driver supports the [`ColumnType` interface](https://golang.org/pkg/database/sql/#ColumnType) introduced in Go 1.8, with the exception of [`ColumnType.Length()`](https://golang.org/pkg/database/sql/#ColumnType.Length), which is currently not supported.
## `context.Context` Support
Go 1.8 added `database/sql` support for `context.Context`. This driver supports query timeouts and cancellation via contexts.
See [context support in the database/sql package](https://golang.org/doc/go1.8#database_sql) for more details.
### `LOAD DATA LOCAL INFILE` support
For this feature you need direct access to the package. Therefore you must change the import path (no `_`):
```go
@@ -390,17 +424,17 @@ Files must be whitelisted by registering them with `mysql.RegisterLocalFile(file
To use a `io.Reader` a handler function must be registered with `mysql.RegisterReaderHandler(name, handler)` which returns a `io.Reader` or `io.ReadCloser`. The Reader is available with the filepath `Reader::<name>` then. Choose different names for different handlers and `DeregisterReaderHandler` when you don't need it anymore.
See the [godoc of Go-MySQL-Driver](http://godoc.org/github.com/go-sql-driver/mysql "golang mysql driver documentation") for details.
See the [godoc of Go-MySQL-Driver](https://godoc.org/github.com/go-sql-driver/mysql "golang mysql driver documentation") for details.
### `time.Time` support
The default internal output type of MySQL `DATE` and `DATETIME` values is `[]byte` which allows you to scan the value into a `[]byte`, `string` or `sql.RawBytes` variable in your programm.
The default internal output type of MySQL `DATE` and `DATETIME` values is `[]byte` which allows you to scan the value into a `[]byte`, `string` or `sql.RawBytes` variable in your program.
However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` variables, which is the logical opposite in Go to `DATE` and `DATETIME` in MySQL. You can do that by changing the internal output type from `[]byte` to `time.Time` with the DSN parameter `parseTime=true`. You can set the default [`time.Time` location](http://golang.org/pkg/time/#Location) with the `loc` DSN parameter.
However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` variables, which is the logical opposite in Go to `DATE` and `DATETIME` in MySQL. You can do that by changing the internal output type from `[]byte` to `time.Time` with the DSN parameter `parseTime=true`. You can set the default [`time.Time` location](https://golang.org/pkg/time/#Location) with the `loc` DSN parameter.
**Caution:** As of Go 1.1, this makes `time.Time` the only variable type you can scan `DATE` and `DATETIME` values into. This breaks for example [`sql.RawBytes` support](https://github.com/go-sql-driver/mysql/wiki/Examples#rawbytes).
Alternatively you can use the [`NullTime`](http://godoc.org/github.com/go-sql-driver/mysql#NullTime) type as the scan destination, which works with both `time.Time` and `string` / `[]byte`.
Alternatively you can use the [`NullTime`](https://godoc.org/github.com/go-sql-driver/mysql#NullTime) type as the scan destination, which works with both `time.Time` and `string` / `[]byte`.
### Unicode support
@@ -412,7 +446,6 @@ Version 1.0 of the driver recommended adding `&charset=utf8` (alias for `SET NAM
See http://dev.mysql.com/doc/refman/5.7/en/charset-unicode.html for more details on MySQL's Unicode support.
## Testing / Development
To run the driver tests you may need to adjust the configuration. See the [Testing Wiki-Page](https://github.com/go-sql-driver/mysql/wiki/Testing "Testing") for details.
@@ -431,13 +464,13 @@ Mozilla summarizes the license scope as follows:
That means:
* You can **use** the **unchanged** source code both in private and commercially
* When distributing, you **must publish** the source code of any **changed files** licensed under the MPL 2.0 under a) the MPL 2.0 itself or b) a compatible license (e.g. GPL 3.0 or Apache License 2.0)
* You **needn't publish** the source code of your library as long as the files licensed under the MPL 2.0 are **unchanged**
* You can **use** the **unchanged** source code both in private and commercially.
* When distributing, you **must publish** the source code of any **changed files** licensed under the MPL 2.0 under a) the MPL 2.0 itself or b) a compatible license (e.g. GPL 3.0 or Apache License 2.0).
* You **needn't publish** the source code of your library as long as the files licensed under the MPL 2.0 are **unchanged**.
Please read the [MPL 2.0 FAQ](http://www.mozilla.org/MPL/2.0/FAQ.html) if you have further questions regarding the license.
Please read the [MPL 2.0 FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/) if you have further questions regarding the license.
You can read the full terms here: [LICENSE](https://raw.github.com/go-sql-driver/mysql/master/LICENSE)
You can read the full terms here: [LICENSE](https://raw.github.com/go-sql-driver/mysql/master/LICENSE).
![Go Gopher and MySQL Dolphin](https://raw.github.com/wiki/go-sql-driver/mysql/go-mysql-driver_m.jpg "Golang Gopher transporting the MySQL Dolphin in a wheelbarrow")

View File

@@ -11,7 +11,7 @@
package mysql
import (
"appengine/cloudsql"
"google.golang.org/appengine/cloudsql"
)
func init() {

View File

@@ -0,0 +1,93 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2017 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build go1.8
package mysql
import (
"context"
"database/sql"
"fmt"
"runtime"
"testing"
)
func benchmarkQueryContext(b *testing.B, db *sql.DB, p int) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db.SetMaxIdleConns(p * runtime.GOMAXPROCS(0))
tb := (*TB)(b)
stmt := tb.checkStmt(db.PrepareContext(ctx, "SELECT val FROM foo WHERE id=?"))
defer stmt.Close()
b.SetParallelism(p)
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
var got string
for pb.Next() {
tb.check(stmt.QueryRow(1).Scan(&got))
if got != "one" {
b.Fatalf("query = %q; want one", got)
}
}
})
}
func BenchmarkQueryContext(b *testing.B) {
db := initDB(b,
"DROP TABLE IF EXISTS foo",
"CREATE TABLE foo (id INT PRIMARY KEY, val CHAR(50))",
`INSERT INTO foo VALUES (1, "one")`,
`INSERT INTO foo VALUES (2, "two")`,
)
defer db.Close()
for _, p := range []int{1, 2, 3, 4} {
b.Run(fmt.Sprintf("%d", p), func(b *testing.B) {
benchmarkQueryContext(b, db, p)
})
}
}
func benchmarkExecContext(b *testing.B, db *sql.DB, p int) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db.SetMaxIdleConns(p * runtime.GOMAXPROCS(0))
tb := (*TB)(b)
stmt := tb.checkStmt(db.PrepareContext(ctx, "DO 1"))
defer stmt.Close()
b.SetParallelism(p)
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if _, err := stmt.ExecContext(ctx); err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkExecContext(b *testing.B) {
db := initDB(b,
"DROP TABLE IF EXISTS foo",
"CREATE TABLE foo (id INT PRIMARY KEY, val CHAR(50))",
`INSERT INTO foo VALUES (1, "one")`,
`INSERT INTO foo VALUES (2, "two")`,
)
defer db.Close()
for _, p := range []int{1, 2, 3, 4} {
b.Run(fmt.Sprintf("%d", p), func(b *testing.B) {
benchmarkQueryContext(b, db, p)
})
}
}

View File

@@ -48,11 +48,7 @@ func initDB(b *testing.B, queries ...string) *sql.DB {
db := tb.checkDB(sql.Open("mysql", dsn))
for _, query := range queries {
if _, err := db.Exec(query); err != nil {
if w, ok := err.(MySQLWarnings); ok {
b.Logf("warning on %q: %v", query, w)
} else {
b.Fatalf("error on %q: %v", query, err)
}
b.Fatalf("error on %q: %v", query, err)
}
}
return db

View File

@@ -9,6 +9,7 @@
package mysql
const defaultCollation = "utf8_general_ci"
const binaryCollation = "binary"
// A list of available collations mapped to the internal ID.
// To update this map use the following MySQL query:

View File

@@ -10,12 +10,23 @@ package mysql
import (
"database/sql/driver"
"io"
"net"
"strconv"
"strings"
"time"
)
// a copy of context.Context for Go 1.7 and earlier
type mysqlContext interface {
Done() <-chan struct{}
Err() error
// defined in context.Context, but not used in this driver:
// Deadline() (deadline time.Time, ok bool)
// Value(key interface{}) interface{}
}
type mysqlConn struct {
buf buffer
netConn net.Conn
@@ -29,7 +40,14 @@ type mysqlConn struct {
status statusFlag
sequence uint8
parseTime bool
strict bool
// for context support (Go 1.8+)
watching bool
watcher chan<- mysqlContext
closech chan struct{}
finished chan<- struct{}
canceled atomicError // set non-nil if conn is canceled
closed atomicBool // set when conn is closed, before closech is closed
}
// Handles parameters set in DSN after the connection is established
@@ -62,22 +80,41 @@ func (mc *mysqlConn) handleParams() (err error) {
return
}
func (mc *mysqlConn) markBadConn(err error) error {
if mc == nil {
return err
}
if err != errBadConnNoWrite {
return err
}
return driver.ErrBadConn
}
func (mc *mysqlConn) Begin() (driver.Tx, error) {
if mc.netConn == nil {
return mc.begin(false)
}
func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) {
if mc.closed.IsSet() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
err := mc.exec("START TRANSACTION")
var q string
if readOnly {
q = "START TRANSACTION READ ONLY"
} else {
q = "START TRANSACTION"
}
err := mc.exec(q)
if err == nil {
return &mysqlTx{mc}, err
}
return nil, err
return nil, mc.markBadConn(err)
}
func (mc *mysqlConn) Close() (err error) {
// Makes Close idempotent
if mc.netConn != nil {
if !mc.closed.IsSet() {
err = mc.writeCommandPacket(comQuit)
}
@@ -91,26 +128,39 @@ func (mc *mysqlConn) Close() (err error) {
// is called before auth or on auth failure because MySQL will have already
// closed the network connection.
func (mc *mysqlConn) cleanup() {
// Makes cleanup idempotent
if mc.netConn != nil {
if err := mc.netConn.Close(); err != nil {
errLog.Print(err)
}
mc.netConn = nil
if !mc.closed.TrySet(true) {
return
}
mc.cfg = nil
mc.buf.nc = nil
// Makes cleanup idempotent
close(mc.closech)
if mc.netConn == nil {
return
}
if err := mc.netConn.Close(); err != nil {
errLog.Print(err)
}
}
func (mc *mysqlConn) error() error {
if mc.closed.IsSet() {
if err := mc.canceled.Value(); err != nil {
return err
}
return ErrInvalidConn
}
return nil
}
func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) {
if mc.netConn == nil {
if mc.closed.IsSet() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
// Send command
err := mc.writeCommandPacketStr(comStmtPrepare, query)
if err != nil {
return nil, err
return nil, mc.markBadConn(err)
}
stmt := &mysqlStmt{
@@ -144,7 +194,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin
if buf == nil {
// can not take the buffer. Something must be wrong with the connection
errLog.Print(ErrBusyBuffer)
return "", driver.ErrBadConn
return "", ErrInvalidConn
}
buf = buf[:0]
argPos := 0
@@ -257,7 +307,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin
}
func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) {
if mc.netConn == nil {
if mc.closed.IsSet() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
@@ -271,7 +321,6 @@ func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, err
return nil, err
}
query = prepared
args = nil
}
mc.affectedRows = 0
mc.insertId = 0
@@ -283,32 +332,43 @@ func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, err
insertId: int64(mc.insertId),
}, err
}
return nil, err
return nil, mc.markBadConn(err)
}
// Internal function to execute commands
func (mc *mysqlConn) exec(query string) error {
// Send command
err := mc.writeCommandPacketStr(comQuery, query)
if err != nil {
return err
if err := mc.writeCommandPacketStr(comQuery, query); err != nil {
return mc.markBadConn(err)
}
// Read Result
resLen, err := mc.readResultSetHeaderPacket()
if err == nil && resLen > 0 {
if err = mc.readUntilEOF(); err != nil {
if err != nil {
return err
}
if resLen > 0 {
// columns
if err := mc.readUntilEOF(); err != nil {
return err
}
err = mc.readUntilEOF()
// rows
if err := mc.readUntilEOF(); err != nil {
return err
}
}
return err
return mc.discardResults()
}
func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) {
if mc.netConn == nil {
return mc.query(query, args)
}
func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) {
if mc.closed.IsSet() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
@@ -322,7 +382,6 @@ func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, erro
return nil, err
}
query = prepared
args = nil
}
// Send command
err := mc.writeCommandPacketStr(comQuery, query)
@@ -335,15 +394,22 @@ func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, erro
rows.mc = mc
if resLen == 0 {
// no columns, no more data
return emptyRows{}, nil
rows.rs.done = true
switch err := rows.NextResultSet(); err {
case nil, io.EOF:
return rows, nil
default:
return nil, err
}
}
// Columns
rows.columns, err = mc.readColumns(resLen)
rows.rs.columns, err = mc.readColumns(resLen)
return rows, err
}
}
return nil, err
return nil, mc.markBadConn(err)
}
// Gets the value of the given MySQL System Variable
@@ -359,7 +425,7 @@ func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) {
if err == nil {
rows := new(textRows)
rows.mc = mc
rows.columns = []mysqlField{{fieldType: fieldTypeVarChar}}
rows.rs.columns = []mysqlField{{fieldType: fieldTypeVarChar}}
if resLen > 0 {
// Columns
@@ -375,3 +441,21 @@ func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) {
}
return nil, err
}
// finish is called when the query has canceled.
func (mc *mysqlConn) cancel(err error) {
mc.canceled.Set(err)
mc.cleanup()
}
// finish is called when the query has succeeded.
func (mc *mysqlConn) finish() {
if !mc.watching || mc.finished == nil {
return
}
select {
case mc.finished <- struct{}{}:
mc.watching = false
case <-mc.closech:
}
}

View File

@@ -0,0 +1,202 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build go1.8
package mysql
import (
"context"
"database/sql"
"database/sql/driver"
)
// Ping implements driver.Pinger interface
func (mc *mysqlConn) Ping(ctx context.Context) error {
if mc.closed.IsSet() {
errLog.Print(ErrInvalidConn)
return driver.ErrBadConn
}
if err := mc.watchCancel(ctx); err != nil {
return err
}
defer mc.finish()
if err := mc.writeCommandPacket(comPing); err != nil {
return err
}
if _, err := mc.readResultOK(); err != nil {
return err
}
return nil
}
// BeginTx implements driver.ConnBeginTx interface
func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
defer mc.finish()
if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault {
level, err := mapIsolationLevel(opts.Isolation)
if err != nil {
return nil, err
}
err = mc.exec("SET TRANSACTION ISOLATION LEVEL " + level)
if err != nil {
return nil, err
}
}
return mc.begin(opts.ReadOnly)
}
func (mc *mysqlConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
rows, err := mc.query(query, dargs)
if err != nil {
mc.finish()
return nil, err
}
rows.finish = mc.finish
return rows, err
}
func (mc *mysqlConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
defer mc.finish()
return mc.Exec(query, dargs)
}
func (mc *mysqlConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
stmt, err := mc.Prepare(query)
mc.finish()
if err != nil {
return nil, err
}
select {
default:
case <-ctx.Done():
stmt.Close()
return nil, ctx.Err()
}
return stmt, nil
}
func (stmt *mysqlStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
if err := stmt.mc.watchCancel(ctx); err != nil {
return nil, err
}
rows, err := stmt.query(dargs)
if err != nil {
stmt.mc.finish()
return nil, err
}
rows.finish = stmt.mc.finish
return rows, err
}
func (stmt *mysqlStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
if err := stmt.mc.watchCancel(ctx); err != nil {
return nil, err
}
defer stmt.mc.finish()
return stmt.Exec(dargs)
}
func (mc *mysqlConn) watchCancel(ctx context.Context) error {
if mc.watching {
// Reach here if canceled,
// so the connection is already invalid
mc.cleanup()
return nil
}
if ctx.Done() == nil {
return nil
}
mc.watching = true
select {
default:
case <-ctx.Done():
return ctx.Err()
}
if mc.watcher == nil {
return nil
}
mc.watcher <- ctx
return nil
}
func (mc *mysqlConn) startWatcher() {
watcher := make(chan mysqlContext, 1)
mc.watcher = watcher
finished := make(chan struct{})
mc.finished = finished
go func() {
for {
var ctx mysqlContext
select {
case ctx = <-watcher:
case <-mc.closech:
return
}
select {
case <-ctx.Done():
mc.cancel(ctx.Err())
case <-finished:
case <-mc.closech:
return
}
}
}()
}
func (mc *mysqlConn) CheckNamedValue(nv *driver.NamedValue) (err error) {
nv.Value, err = converter{}.ConvertValue(nv.Value)
return
}

View File

@@ -0,0 +1,30 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2017 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build go1.8
package mysql
import (
"database/sql/driver"
"testing"
)
func TestCheckNamedValue(t *testing.T) {
value := driver.NamedValue{Value: ^uint64(0)}
x := &mysqlConn{}
err := x.CheckNamedValue(&value)
if err != nil {
t.Fatal("uint64 high-bit not convertible", err)
}
if value.Value != "18446744073709551615" {
t.Fatalf("uint64 high-bit not converted, got %#v %T", value.Value, value.Value)
}
}

View File

@@ -9,7 +9,8 @@
package mysql
const (
minProtocolVersion byte = 10
defaultMaxAllowedPacket = 4 << 20 // 4 MiB
minProtocolVersion = 10
maxPacketSize = 1<<24 - 1
timeFormat = "2006-01-02 15:04:05.999999"
)
@@ -87,8 +88,10 @@ const (
)
// https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::ColumnType
type fieldType byte
const (
fieldTypeDecimal byte = iota
fieldTypeDecimal fieldType = iota
fieldTypeTiny
fieldTypeShort
fieldTypeLong
@@ -107,7 +110,7 @@ const (
fieldTypeBit
)
const (
fieldTypeJSON byte = iota + 0xf5
fieldTypeJSON fieldType = iota + 0xf5
fieldTypeNewDecimal
fieldTypeEnum
fieldTypeSet

Some files were not shown because too many files have changed in this diff Show More