merge current master
This commit is contained in:
+1
-1
@@ -18,4 +18,4 @@ debug.test
|
||||
.mongo
|
||||
remark42
|
||||
/bin/
|
||||
/backend/var/
|
||||
/backend/var/
|
||||
|
||||
+9
-8
@@ -29,7 +29,9 @@ WORKDIR /build/backend
|
||||
RUN \
|
||||
if [ -f .mongo ] ; then export MONGO_TEST=$(cat .mongo) ; fi && \
|
||||
cd app && \
|
||||
if [ -z "$SKIP_BACKEND_TEST" ] ; then go test -mod=vendor -covermode=count -coverprofile=/profile.cov ./... ; \
|
||||
if [ -z "$SKIP_BACKEND_TEST" ] ; then \
|
||||
go test -mod=vendor -covermode=count -coverprofile=/profile.cov_tmp ./... && \
|
||||
cat /profile.cov_tmp | grep -v "_mock.go" > /profile.cov ; \
|
||||
else echo "skip backend test" ; fi
|
||||
|
||||
RUN echo "mongo=${MONGO_TEST}" >> /etc/hosts
|
||||
@@ -37,10 +39,10 @@ RUN echo "mongo=${MONGO_TEST}" >> /etc/hosts
|
||||
# linters
|
||||
RUN if [ -z "$SKIP_BACKEND_TEST" ] ; then \
|
||||
if [ -f .mongo ] ; then export MONGO_TEST=$(cat .mongo) ; fi && \
|
||||
golangci-lint run --out-format=tab --disable-all --tests=false --enable=unconvert \
|
||||
--enable=megacheck --enable=structcheck --enable=gas --enable=gocyclo --enable=dupl --enable=misspell \
|
||||
--enable=unparam --enable=varcheck --enable=deadcode --enable=typecheck \
|
||||
--enable=ineffassign --enable=varcheck ./... ; \
|
||||
golangci-lint run --out-format=tab --disable-all --tests=false --enable=unconvert \
|
||||
--enable=megacheck --enable=structcheck --enable=gas --enable=gocyclo --enable=dupl --enable=misspell \
|
||||
--enable=unparam --enable=varcheck --enable=deadcode --enable=typecheck \
|
||||
--enable=ineffassign --enable=varcheck ./... ; \
|
||||
else echo "skip backend linters" ; fi
|
||||
|
||||
# submit coverage to coverals if COVERALLS_TOKEN in env
|
||||
@@ -50,11 +52,10 @@ RUN if [ -z "$COVERALLS_TOKEN" ] ; then \
|
||||
|
||||
# if DRONE presented use DRONE_* git env to make version
|
||||
RUN \
|
||||
if [ -z "$DRONE" ] ; then \
|
||||
echo "runs outside of drone" && version="local"; \
|
||||
if [ -z "$DRONE" ] ; then echo "runs outside of drone" && version="local"; \
|
||||
else version=${DRONE_TAG}${DRONE_BRANCH}${DRONE_PULL_REQUEST}-${DRONE_COMMIT:0:7}-$(date +%Y%m%d-%H:%M:%S); fi && \
|
||||
echo "version=$version" && \
|
||||
go build -o remark42 -ldflags "-X main.revision=${version} -s -w" ./app
|
||||
go build -mod=vendor -o remark42 -ldflags "-X main.revision=${version} -s -w" ./app
|
||||
|
||||
|
||||
FROM node:10.11-alpine as build-frontend-deps
|
||||
|
||||
@@ -86,63 +86,68 @@ _this is the recommended way to run remark42_
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Command line | Environment | Default | Description |
|
||||
| ----------------------- | ----------------------- | --------------------- | ------------------------------------------------ |
|
||||
| url | REMARK_URL | | url to remark42 server, _required_ |
|
||||
| secret | SECRET | | secret key, _required_ |
|
||||
| site | SITE | `remark` | site name(s), _multi_ |
|
||||
| store.type | STORE_TYPE | `bolt` | type of storage, `bolt` or `mongo` |
|
||||
| store.bolt.path | STORE_BOLT_PATH | `./var` | path to data directory |
|
||||
| store.bolt.timeout | STORE_BOLT_TIMEOUT | `30s` | boltdb access timeout |
|
||||
| mongo.url | MONGO_URL | | mongo url for all stores using mongodb |
|
||||
| mongo.db | MONGO_DB | | mongo database |
|
||||
| admin.shared.id | ADMIN_SHARED_ID | | admin names (list of user ids), _multi_ |
|
||||
| admin.shared.email | ADMIN_SHARED_EMAIL | `admin@${REMARK_URL}` | admin email |
|
||||
| backup | BACKUP_PATH | `./var/backup` | backups location |
|
||||
| max-back | MAX_BACKUP_FILES | `10` | max backup files to keep |
|
||||
| cache.max.items | CACHE_MAX_ITEMS | `1000` | max number of cached items, `0` - unlimited |
|
||||
| cache.max.value | CACHE_MAX_VALUE | `65536` | max size of cached value, `0` - unlimited |
|
||||
| cache.max.size | CACHE_MAX_SIZE | `50000000` | max size of all cached values, `0` - unlimited |
|
||||
| avatar.type | AVATAR_TYPE | `fs` | type of avatar storage, `fs`, 'bolt`, or `mongo` |
|
||||
| avatar.fs.path | AVATAR_FS_PATH | `./var/avatars` | avatars location for `fs` store |
|
||||
| avatar.bolt.file | AVATAR_BOLT_FILE | `./var/avatars.db` | file name for `bolt` store |
|
||||
| avatar.rsz-lmt | AVATAR_RSZ_LMT | `0` (disabled) | max image size for resizing avatars on save |
|
||||
| auth.ttl.jwt | AUTH_TTL_JWT | `5m` | jwt TTL |
|
||||
| auth.ttl.cookie | AUTH_TTL_COOKIE | `200h` | cookie TTL |
|
||||
| auth.google.cid | AUTH_GOOGLE_CID | | Google OAuth client ID |
|
||||
| auth.google.csec | AUTH_GOOGLE_CSEC | | Google OAuth client secret |
|
||||
| auth.facebook.cid | AUTH_FACEBOOK_CID | | Facebook OAuth client ID |
|
||||
| auth.facebook.csec | AUTH_FACEBOOK_CSEC | | Facebook OAuth client secret |
|
||||
| auth.github.cid | AUTH_GITHUB_CID | | Github OAuth client ID |
|
||||
| auth.github.csec | AUTH_GITHUB_CSEC | | Github OAuth client secret |
|
||||
| auth.yandex.cid | AUTH_YANDEX_CID | | Yandex OAuth client ID |
|
||||
| auth.yandex.csec | AUTH_YANDEX_CSEC | | Yandex OAuth client secret |
|
||||
| auth.dev | AUTH_DEV | `false` | local oauth2 server, development mode only |
|
||||
| auth.anon | AUTH_ANON | `false` | enable anonymous login |
|
||||
| notify.type | NOTIFY_TYPE | none | type of notification (none or telegram) |
|
||||
| notify.queue | NOTIFY_QUEUE | `100` | size of notification queue |
|
||||
| notify.telegram.token | NOTIFY_TELEGRAM_TOKEN | | telegram token |
|
||||
| notify.telegram.chan | NOTIFY_TELEGRAM_CHAN | | telegram channel |
|
||||
| notify.telegram.timeout | NOTIFY_TELEGRAM_TIMEOUT | `5s` | telegram timeout |
|
||||
| ssl.type | SSL_TYPE | none | `none`-http, `static`-https, `auto`-https + le |
|
||||
| ssl.port | SSL_PORT | `8443` | port for https server |
|
||||
| ssl.cert | SSL_CERT | | path to cert.pem file |
|
||||
| ssl.key | SSL_KEY | | path to key.pem file |
|
||||
| ssl.acme-location | SSL_ACME_LOCATION | `./var/acme` | dir where obtained le-certs will be stored |
|
||||
| ssl.acme-email | SSL_ACME_EMAIL | | admin email for receiving notifications from LE |
|
||||
| max-comment | MAX_COMMENT_SIZE | `2048` | comment's size limit |
|
||||
| max-votes | MAX_VOTES | `-1` | votes limit per comment, `-1` - unlimited |
|
||||
| low-score | LOW_SCORE | `-5` | low score threshold |
|
||||
| positive-score | POSITIVE_SCORE | `false` | enable positive score only |
|
||||
| critical-score | CRITICAL_SCORE | `-10` | critical score threshold |
|
||||
| positive-score | POSITIVE_SCORE | `false` | restricts comment's score to be only positive |
|
||||
| restricted-words | RESTRICTED_WORDS | | words banned in comments (can use `*`), _multi_ |
|
||||
| edit-time | EDIT_TIME | `5m` | edit window |
|
||||
| read-age | READONLY_AGE | | read-only age of comments, days |
|
||||
| img-proxy | IMG_PROXY | `false` | enable http->https proxy for images |
|
||||
| update-limit | UPDATE_LIMIT | `0.5` | updates/sec limit |
|
||||
| admin-passwd | ADMIN_PASSWD | none (disabled) | password for `admin` basic auth |
|
||||
| dbg | DEBUG | `false` | debug mode |
|
||||
| Command line | Environment | Default | Description |
|
||||
| ----------------------- | ----------------------- | ------------------------ | ------------------------------------------------ |
|
||||
| url | REMARK_URL | | url to remark42 server, _required_ |
|
||||
| secret | SECRET | | secret key, _required_ |
|
||||
| site | SITE | `remark` | site name(s), _multi_ |
|
||||
| store.type | STORE_TYPE | `bolt` | type of storage, `bolt` or `mongo` |
|
||||
| store.bolt.path | STORE_BOLT_PATH | `./var` | path to data directory |
|
||||
| store.bolt.timeout | STORE_BOLT_TIMEOUT | `30s` | boltdb access timeout |
|
||||
| mongo.url | MONGO_URL | | mongo url for all stores using mongodb |
|
||||
| mongo.db | MONGO_DB | | mongo database |
|
||||
| admin.shared.id | ADMIN_SHARED_ID | | admin names (list of user ids), _multi_ |
|
||||
| admin.shared.email | ADMIN_SHARED_EMAIL | `admin@${REMARK_URL}` | admin email |
|
||||
| backup | BACKUP_PATH | `./var/backup` | backups location |
|
||||
| max-back | MAX_BACKUP_FILES | `10` | max backup files to keep |
|
||||
| cache.max.items | CACHE_MAX_ITEMS | `1000` | max number of cached items, `0` - unlimited |
|
||||
| cache.max.value | CACHE_MAX_VALUE | `65536` | max size of cached value, `0` - unlimited |
|
||||
| cache.max.size | CACHE_MAX_SIZE | `50000000` | max size of all cached values, `0` - unlimited |
|
||||
| avatar.type | AVATAR_TYPE | `fs` | type of avatar storage, `fs`, `bolt`, or `mongo` |
|
||||
| avatar.fs.path | AVATAR_FS_PATH | `./var/avatars` | avatars location for `fs` store |
|
||||
| avatar.bolt.file | AVATAR_BOLT_FILE | `./var/avatars.db` | file name for `bolt` store |
|
||||
| avatar.rsz-lmt | AVATAR_RSZ_LMT | `0` (disabled) | max image size for resizing avatars on save |
|
||||
| image.type | IMAGE_TYPE | `fs` | type of image storage, `fs`, 'bolt`, or `mongo` |
|
||||
| image.max-size | IMAGE_MAX_SIZE | `5000000` | max size of image file |
|
||||
| image.fs.path | IMAGE_FS_PATH | `./var/pictures` | permanent location of images |
|
||||
| image.fs.staging | IMAGE_FS_STAGING | `./var/pictures.staging` | staging location of images |
|
||||
| image.fs.partitions | IMAGE_FS_PARTITIONS | `100` | number of image partitions |
|
||||
| auth.ttl.jwt | AUTH_TTL_JWT | `5m` | jwt TTL |
|
||||
| auth.ttl.cookie | AUTH_TTL_COOKIE | `200h` | cookie TTL |
|
||||
| auth.google.cid | AUTH_GOOGLE_CID | | Google OAuth client ID |
|
||||
| auth.google.csec | AUTH_GOOGLE_CSEC | | Google OAuth client secret |
|
||||
| auth.facebook.cid | AUTH_FACEBOOK_CID | | Facebook OAuth client ID |
|
||||
| auth.facebook.csec | AUTH_FACEBOOK_CSEC | | Facebook OAuth client secret |
|
||||
| auth.github.cid | AUTH_GITHUB_CID | | Github OAuth client ID |
|
||||
| auth.github.csec | AUTH_GITHUB_CSEC | | Github OAuth client secret |
|
||||
| auth.yandex.cid | AUTH_YANDEX_CID | | Yandex OAuth client ID |
|
||||
| auth.yandex.csec | AUTH_YANDEX_CSEC | | Yandex OAuth client secret |
|
||||
| auth.dev | AUTH_DEV | `false` | local oauth2 server, development mode only |
|
||||
| auth.anon | AUTH_ANON | `false` | enable anonymous login |
|
||||
| notify.type | NOTIFY_TYPE | none | type of notification (none or telegram) |
|
||||
| notify.queue | NOTIFY_QUEUE | `100` | size of notification queue |
|
||||
| notify.telegram.token | NOTIFY_TELEGRAM_TOKEN | | telegram token |
|
||||
| notify.telegram.chan | NOTIFY_TELEGRAM_CHAN | | telegram channel |
|
||||
| notify.telegram.timeout | NOTIFY_TELEGRAM_TIMEOUT | `5s` | telegram timeout |
|
||||
| ssl.type | SSL_TYPE | none | `none`-http, `static`-https, `auto`-https + le |
|
||||
| ssl.port | SSL_PORT | `8443` | port for https server |
|
||||
| ssl.cert | SSL_CERT | | path to cert.pem file |
|
||||
| ssl.key | SSL_KEY | | path to key.pem file |
|
||||
| ssl.acme-location | SSL_ACME_LOCATION | `./var/acme` | dir where obtained le-certs will be stored |
|
||||
| ssl.acme-email | SSL_ACME_EMAIL | | admin email for receiving notifications from LE |
|
||||
| max-comment | MAX_COMMENT_SIZE | `2048` | comment's size limit |
|
||||
| max-votes | MAX_VOTES | `-1` | votes limit per comment, `-1` - unlimited |
|
||||
| low-score | LOW_SCORE | `-5` | low score threshold |
|
||||
| positive-score | POSITIVE_SCORE | `false` | enable positive score only |
|
||||
| critical-score | CRITICAL_SCORE | `-10` | critical score threshold |
|
||||
| positive-score | POSITIVE_SCORE | `false` | restricts comment's score to be only positive |
|
||||
| restricted-words | RESTRICTED_WORDS | | words banned in comments (can use `*`), _multi_ |
|
||||
| edit-time | EDIT_TIME | `5m` | edit window |
|
||||
| read-age | READONLY_AGE | | read-only age of comments, days |
|
||||
| img-proxy | IMG_PROXY | `false` | enable http->https proxy for images |
|
||||
| update-limit | UPDATE_LIMIT | `0.5` | updates/sec limit |
|
||||
| admin-passwd | ADMIN_PASSWD | none (disabled) | password for `admin` basic auth |
|
||||
| dbg | DEBUG | `false` | debug mode |
|
||||
|
||||
* command line parameters are long form `--<key>=value`, i.e. `--site=https://demo.remark42.com`
|
||||
* _multi_ parameters separated by `,` in the environment or repeated with command line key, like `--site=s1 --site=s2 ...`
|
||||
@@ -602,6 +607,13 @@ Sort can be `time`, `active` or `score`. Supported sort order with prefix -/+, i
|
||||
* `GET /api/v1/rss/site?site=site-id` - rss feed for given site
|
||||
* `GET /api/v1/rss/reply?site=site-id&user=user-id` - rss feed for replies to user's comments
|
||||
|
||||
### Images management
|
||||
|
||||
* `GET /api/v1/picture/{user}/{id}` - load stored image
|
||||
* `POST /api/v1/picture` - upload and store image, uses post form with `FormFile("file")`. returns `{"id": user/imgid}` _auth required_
|
||||
|
||||
_returned id should be appended to load image url on caller side_
|
||||
|
||||
### Admin
|
||||
|
||||
* `DELETE /api/v1/admin/comment/{id}?site=site-id&url=post-url` - delete comment by `id`.
|
||||
|
||||
@@ -31,6 +31,7 @@ type AvatarMigrator interface {
|
||||
|
||||
type avatarMigrator struct{}
|
||||
|
||||
// Migrate from one avatar store to another. Can be used to convert between stores
|
||||
func (a avatarMigrator) Migrate(dst, src avatar.Store) (int, error) {
|
||||
return avatar.Migrate(dst, src)
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ func (cc *CleanupCommand) listComments(postURL string) ([]store.Comment, error)
|
||||
Info store.PostInfo `json:"info,omitempty"`
|
||||
}{}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&commentsWithInfo); err != nil {
|
||||
if err = json.NewDecoder(r.Body).Decode(&commentsWithInfo); err != nil {
|
||||
return nil, errors.Wrapf(err, "can't decode list of comments for %s", postURL)
|
||||
}
|
||||
return commentsWithInfo.Comments, nil
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
bolt "github.com/coreos/bbolt"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
auth_cache "github.com/patrickmn/go-cache"
|
||||
authcache "github.com/patrickmn/go-cache"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/go-pkgz/auth"
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/admin"
|
||||
"github.com/umputun/remark/backend/app/store/engine"
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
"github.com/umputun/remark/backend/app/store/service"
|
||||
)
|
||||
|
||||
@@ -43,6 +44,7 @@ type ServerCommand struct {
|
||||
Mongo MongoGroup `group:"mongo" namespace:"mongo" env-namespace:"MONGO"`
|
||||
Admin AdminGroup `group:"admin" namespace:"admin" env-namespace:"ADMIN"`
|
||||
Notify NotifyGroup `group:"notify" namespace:"notify" env-namespace:"NOTIFY"`
|
||||
Image ImageGroup `group:"image" namespace:"image" env-namespace:"IMAGE"`
|
||||
SSL SSLGroup `group:"ssl" namespace:"ssl" env-namespace:"SSL"`
|
||||
|
||||
Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","`
|
||||
@@ -93,6 +95,20 @@ type StoreGroup struct {
|
||||
} `group:"bolt" namespace:"bolt" env-namespace:"BOLT"`
|
||||
}
|
||||
|
||||
// ImageGroup defines options group for store pictures
|
||||
type ImageGroup struct {
|
||||
Type string `long:"type" env:"TYPE" description:"type of storage" choice:"fs" choice:"bolt" choice:"mongo" default:"fs"`
|
||||
FS struct {
|
||||
Path string `long:"path" env:"PATH" default:"./var/pictures" description:"images location"`
|
||||
Staging string `long:"staging" env:"STAGING" default:"./var/pictures.staging" description:"staging location"`
|
||||
Partitions int `long:"partitions" env:"PARTITIONS" default:"100" description:"partitions (subdirs)"`
|
||||
} `group:"fs" namespace:"fs" env-namespace:"FS"`
|
||||
Bolt struct {
|
||||
File string `long:"file" env:"FILE" default:"./var/pictures.db" description:"images bolt file location"`
|
||||
} `group:"bolt" namespace:"bolt" env-namespace:"bolt"`
|
||||
MaxSize int `long:"max-size" env:"MAX_SIZE" default:"5000000" description:"max size of image file"`
|
||||
}
|
||||
|
||||
// AvatarGroup defines options group for avatar params
|
||||
type AvatarGroup struct {
|
||||
Type string `long:"type" env:"TYPE" description:"type of avatar storage" choice:"fs" choice:"bolt" choice:"mongo" default:"fs"`
|
||||
@@ -162,6 +178,7 @@ type serverApp struct {
|
||||
dataService *service.DataStore
|
||||
avatarStore avatar.Store
|
||||
notifyService *notify.Service
|
||||
imageService *image.Service
|
||||
terminated chan struct{}
|
||||
}
|
||||
|
||||
@@ -182,6 +199,7 @@ func (s *ServerCommand) Execute(args []string) error {
|
||||
app, err := s.newServerApp()
|
||||
if err != nil {
|
||||
log.Printf("[PANIC] failed to setup application, %+v", err)
|
||||
return err
|
||||
}
|
||||
if err = app.run(ctx); err != nil {
|
||||
log.Printf("[ERROR] remark terminated with error %+v", err)
|
||||
@@ -214,6 +232,11 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
return nil, errors.Wrap(err, "failed to make admin store")
|
||||
}
|
||||
|
||||
imageService, err := s.makePicturesStore()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to make pictures store")
|
||||
}
|
||||
|
||||
dataService := &service.DataStore{
|
||||
Interface: storeEngine,
|
||||
EditDuration: s.EditDuration,
|
||||
@@ -221,6 +244,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
MaxCommentSize: s.MaxCommentSize,
|
||||
MaxVotes: s.MaxVotes,
|
||||
PositiveScore: s.PositiveScore,
|
||||
ImageService: imageService,
|
||||
TitleExtractor: service.NewTitleExtractor(http.Client{Timeout: time.Second * 5}),
|
||||
RestrictedWordsMatcher: service.NewRestrictedWordsMatcher(service.StaticRestrictedWordsLister{Words: s.RestrictedWords}),
|
||||
}
|
||||
@@ -276,15 +300,16 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
NotifyService: notifyService,
|
||||
SSLConfig: sslConfig,
|
||||
UpdateLimiter: s.UpdateLimit,
|
||||
ImageService: imageService,
|
||||
}
|
||||
|
||||
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore
|
||||
|
||||
var devAuth *provider.DevAuthServer
|
||||
if s.Auth.Dev {
|
||||
da, err := authenticator.DevAuth()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't make dev oauth2 server")
|
||||
da, errDevAuth := authenticator.DevAuth()
|
||||
if errDevAuth != nil {
|
||||
return nil, errors.Wrap(errDevAuth, "can't make dev oauth2 server")
|
||||
}
|
||||
devAuth = da
|
||||
}
|
||||
@@ -298,6 +323,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
dataService: dataService,
|
||||
avatarStore: avatarStore,
|
||||
notifyService: notifyService,
|
||||
imageService: imageService,
|
||||
terminated: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
@@ -323,12 +349,17 @@ func (a *serverApp) run(ctx context.Context) error {
|
||||
log.Printf("[WARN] failed to close avatar store, %s", e)
|
||||
}
|
||||
a.notifyService.Close()
|
||||
a.imageService.Close()
|
||||
log.Print("[INFO] shutdown completed")
|
||||
}()
|
||||
|
||||
a.activateBackup(ctx) // runs in goroutine for each site
|
||||
if a.Auth.Dev {
|
||||
go a.devAuth.Run(context.Background()) // dev oauth2 server on :8084
|
||||
}
|
||||
|
||||
go a.imageService.Cleanup(ctx) // pictures cleanup for staging images
|
||||
|
||||
a.restSrv.Run(a.Port)
|
||||
close(a.terminated)
|
||||
return nil
|
||||
@@ -405,6 +436,25 @@ func (s *ServerCommand) makeAvatarStore() (avatar.Store, error) {
|
||||
return nil, errors.Errorf("unsupported avatar store type %s", s.Avatar.Type)
|
||||
}
|
||||
|
||||
func (s *ServerCommand) makePicturesStore() (*image.Service, error) {
|
||||
switch s.Image.Type {
|
||||
case "fs":
|
||||
if err := makeDirs(s.Image.FS.Path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &image.Service{
|
||||
Store: &image.FileSystem{
|
||||
Location: s.Image.FS.Path,
|
||||
Staging: s.Image.FS.Staging,
|
||||
Partitions: s.Image.FS.Partitions,
|
||||
MaxSize: s.Image.MaxSize,
|
||||
},
|
||||
TTL: s.EditDuration + time.Second, // add extra second to image TTL for staging
|
||||
}, nil
|
||||
}
|
||||
return nil, errors.Errorf("unsupported pictures store type %s", s.Image.Type)
|
||||
}
|
||||
|
||||
func (s *ServerCommand) makeAdminStore() (admin.Store, error) {
|
||||
log.Printf("[INFO] make admin store, type=%s", s.Admin.Type)
|
||||
|
||||
@@ -475,6 +525,7 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) {
|
||||
providers++
|
||||
}
|
||||
if s.Auth.Dev {
|
||||
log.Print("[INFO] dev access enabled")
|
||||
authenticator.AddProvider("dev", "", "")
|
||||
providers++
|
||||
}
|
||||
@@ -585,17 +636,19 @@ func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Sto
|
||||
|
||||
// authRefreshCache used by authenticator to minimize repeatable token refreshes
|
||||
type authRefreshCache struct {
|
||||
*auth_cache.Cache
|
||||
*authcache.Cache
|
||||
}
|
||||
|
||||
func newAuthRefreshCache() *authRefreshCache {
|
||||
return &authRefreshCache{Cache: auth_cache.New(5*time.Minute, 10*time.Minute)}
|
||||
return &authRefreshCache{Cache: authcache.New(5*time.Minute, 10*time.Minute)}
|
||||
}
|
||||
|
||||
// Get implements cache getter with key converted to string
|
||||
func (c *authRefreshCache) Get(key interface{}) (interface{}, bool) {
|
||||
return c.Cache.Get(key.(string))
|
||||
}
|
||||
|
||||
// Set implements cache setter with key converted to string
|
||||
func (c *authRefreshCache) Set(key, value interface{}) {
|
||||
c.Cache.Set(key.(string), value, auth_cache.DefaultExpiration)
|
||||
c.Cache.Set(key.(string), value, authcache.DefaultExpiration)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
func TestServerApp(t *testing.T) {
|
||||
app, ctx := prepServerApp(t, 500*time.Millisecond, func(o ServerCommand) ServerCommand {
|
||||
app, ctx := prepServerApp(t, 1500*time.Millisecond, func(o ServerCommand) ServerCommand {
|
||||
o.Port = 18080
|
||||
return o
|
||||
})
|
||||
|
||||
+2
-2
@@ -62,10 +62,10 @@ func main() {
|
||||
|
||||
func setupLog(dbg bool) {
|
||||
if dbg {
|
||||
log.Setup(log.Debug, log.CallerFile, log.Msec, log.LevelBraces, log.CallerIgnore("logger"))
|
||||
log.Setup(log.Debug, log.CallerFile, log.CallerFunc, log.Msec, log.LevelBraces)
|
||||
return
|
||||
}
|
||||
log.Setup(log.Msec, log.LevelBraces, log.CallerPkg, log.CallerIgnore("logger", "rest"))
|
||||
log.Setup(log.Msec, log.LevelBraces)
|
||||
}
|
||||
|
||||
// getDump reads runtime stack and returns as a string
|
||||
|
||||
+31
-16
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -11,20 +12,25 @@ import (
|
||||
"time"
|
||||
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/go-pkgz/repeater"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
func Test_Main(t *testing.T) {
|
||||
|
||||
os.Args = []string{"test", "server", "--secret=123456", "--store.bolt.path=/tmp/xyz", "--backup=/tmp",
|
||||
"--avatar.fs.path=/tmp", "--port=18202", "--url=https://demo.remark42.com", "--dbg", "--notify.type=none"}
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "remark42")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
os.Args = []string{"test", "server", "--secret=123456", "--store.bolt.path=" + dir, "--backup=/tmp",
|
||||
"--avatar.fs.path=" + dir, "--port=18222", "--url=https://demo.remark42.com", "--dbg", "--notify.type=none"}
|
||||
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
err := syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
|
||||
require.Nil(t, err)
|
||||
time.Sleep(2000 * time.Millisecond)
|
||||
e := syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
|
||||
require.Nil(t, e)
|
||||
}()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
@@ -32,20 +38,29 @@ func TestMain(t *testing.T) {
|
||||
go func() {
|
||||
st := time.Now()
|
||||
main()
|
||||
assert.True(t, time.Since(st).Seconds() < 1, "should take about 500msec")
|
||||
assert.True(t, time.Since(st).Seconds() > 2, "should take 2s")
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
time.Sleep(200 * time.Millisecond) // let server start
|
||||
var passed bool
|
||||
err = repeater.NewDefault(10, time.Millisecond*200).Do(context.Background(), func() error {
|
||||
resp, e := http.Get("http://localhost:18222/api/v1/ping")
|
||||
if e != nil {
|
||||
t.Logf("%+v", e)
|
||||
return e
|
||||
}
|
||||
require.Nil(t, e)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, e := ioutil.ReadAll(resp.Body)
|
||||
assert.Nil(t, e)
|
||||
assert.Equal(t, "pong", string(body))
|
||||
passed = true
|
||||
return nil
|
||||
})
|
||||
|
||||
// send ping
|
||||
resp, err := http.Get("http://localhost:18202/api/v1/ping")
|
||||
require.Nil(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "pong", string(body))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, passed)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ func TestBackup_RemoveOldBackupFiles(t *testing.T) {
|
||||
loc := "/tmp/remark-backups.test"
|
||||
defer os.RemoveAll(loc)
|
||||
|
||||
os.MkdirAll(loc, 0700)
|
||||
assert.NoError(t, os.MkdirAll(loc, 0700))
|
||||
|
||||
for i := 1; i <= 10; i++ {
|
||||
fname := fmt.Sprintf("%s/backup-site1-201712%02d.gz", loc, i)
|
||||
err := ioutil.WriteFile(fname, []byte("blah"), 0600)
|
||||
@@ -40,7 +41,7 @@ func TestBackup_RemoveOldBackupFiles(t *testing.T) {
|
||||
func TestBackup_MakeBackup(t *testing.T) {
|
||||
loc := "/tmp/remark-backups.test"
|
||||
defer os.RemoveAll(loc)
|
||||
os.MkdirAll(loc, 0700)
|
||||
assert.NoError(t, os.MkdirAll(loc, 0700))
|
||||
|
||||
bk := AutoBackup{BackupLocation: loc, SiteID: "site1", KeepMax: 3, Exporter: &mockExporter{}}
|
||||
fname, err := bk.makeBackup()
|
||||
@@ -56,7 +57,7 @@ func TestBackup_MakeBackup(t *testing.T) {
|
||||
func TestBackup_Do(t *testing.T) {
|
||||
loc := "/tmp/remark-backups.test"
|
||||
defer os.RemoveAll(loc)
|
||||
os.MkdirAll(loc, 0700)
|
||||
assert.NoError(t, os.MkdirAll(loc, 0700))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
|
||||
@@ -105,7 +105,7 @@ func (d *Disqus) convert(r io.Reader, siteID string) (ch chan store.Comment) {
|
||||
if se.Name.Local == "thread" {
|
||||
stats.inpThreads++
|
||||
thread := disqusThread{}
|
||||
if err := decoder.DecodeElement(&thread, &se); err != nil {
|
||||
if err = decoder.DecodeElement(&thread, &se); err != nil {
|
||||
log.Printf("[WARN] can't decode disqus thread, %s", err)
|
||||
stats.failedThreads++
|
||||
continue
|
||||
@@ -116,7 +116,7 @@ func (d *Disqus) convert(r io.Reader, siteID string) (ch chan store.Comment) {
|
||||
if se.Name.Local == "post" {
|
||||
stats.inpComments++
|
||||
comment := disqusComment{}
|
||||
if err := decoder.DecodeElement(&comment, &se); err != nil {
|
||||
if err = decoder.DecodeElement(&comment, &se); err != nil {
|
||||
log.Printf("[WARN] can't decode disqus comment, %s", err)
|
||||
stats.failedPosts++
|
||||
continue
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/umputun/remark/backend/app/store/service"
|
||||
)
|
||||
|
||||
const natvieVersion = 1
|
||||
const nativeVersion = 1
|
||||
const defaultConcurrent = 8
|
||||
|
||||
// Native implements exporter and importer for internal store format
|
||||
@@ -50,7 +50,7 @@ func (n *Native) Export(w io.Writer, siteID string) (size int, err error) {
|
||||
for i := len(topics) - 1; i >= 0; i-- { // topics from List sorted in opposite direction
|
||||
topic := topics[i]
|
||||
comments, e := n.DataStore.Find(store.Locator{SiteID: siteID, URL: topic.URL}, "time")
|
||||
if err != nil {
|
||||
if e != nil {
|
||||
return commentsCount, e
|
||||
}
|
||||
|
||||
@@ -75,13 +75,13 @@ func (n *Native) Export(w io.Writer, siteID string) (size int, err error) {
|
||||
|
||||
// exportMeta appends user and post metas to exported stream
|
||||
func (n *Native) exportMeta(siteID string, w io.Writer) (err error) {
|
||||
m := meta{Version: natvieVersion}
|
||||
m := meta{Version: nativeVersion}
|
||||
m.Users, m.Posts, err = n.DataStore.Metas(siteID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "can't get meta")
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(m); err != nil {
|
||||
if err = json.NewEncoder(w).Encode(m); err != nil {
|
||||
return errors.Wrap(err, "can't encode meta")
|
||||
}
|
||||
return nil
|
||||
@@ -96,7 +96,7 @@ func (n *Native) Import(reader io.Reader, siteID string) (size int, err error) {
|
||||
return 0, errors.Wrapf(err, "failed to import meta for site %s", siteID)
|
||||
}
|
||||
|
||||
if m.Version != natvieVersion && m.Version != 0 { // this version allows back compatibility with 0 version
|
||||
if m.Version != nativeVersion && m.Version != 0 { // this version allows back compatibility with 0 version
|
||||
return 0, errors.Errorf("unexpected import file version %d", m.Version)
|
||||
}
|
||||
|
||||
@@ -134,9 +134,9 @@ func (n *Native) Import(reader io.Reader, siteID string) (size int, err error) {
|
||||
log.Printf("[WARN] can't write %+v to store, %s", comment, e)
|
||||
return
|
||||
}
|
||||
n := atomic.AddInt64(&comments, 1)
|
||||
if n%1000 == 0 {
|
||||
log.Printf("[DEBUG] imported %d comments", n)
|
||||
num := atomic.AddInt64(&comments, 1)
|
||||
if num%1000 == 0 {
|
||||
log.Printf("[DEBUG] imported %d comments", num)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ type wpTime struct {
|
||||
time time.Time
|
||||
}
|
||||
|
||||
// UnmarshalXML decoding xml with time in WP format
|
||||
func (w *wpTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var v string
|
||||
if err := d.DecodeElement(&v, &start); err != nil {
|
||||
@@ -111,7 +112,7 @@ func (w *WordPress) convert(r io.Reader, siteID string) chan store.Comment {
|
||||
if el.Name.Local == "item" {
|
||||
stats.inpItems++
|
||||
item := wpItem{}
|
||||
if err := decoder.DecodeElement(&item, &el); err != nil {
|
||||
if err = decoder.DecodeElement(&item, &el); err != nil {
|
||||
log.Printf("[WARN] Can't decode item, %s", err)
|
||||
stats.failedItems++
|
||||
continue
|
||||
|
||||
@@ -29,10 +29,11 @@ type Destination interface {
|
||||
Send(ctx context.Context, req request) error
|
||||
}
|
||||
|
||||
// Store defines the minimal interface accessing stored commens used by notifier
|
||||
// Store defines the minimal interface accessing stored comments used by notifier
|
||||
type Store interface {
|
||||
Get(locator store.Locator, id string) (store.Comment, error)
|
||||
}
|
||||
|
||||
type request struct {
|
||||
comment store.Comment
|
||||
parent store.Comment
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
)
|
||||
@@ -71,6 +72,7 @@ func TestTelegram_Send(t *testing.T) {
|
||||
tb, err = NewTelegram("non-json-resp", "remark_test", 2*time.Second, ts.URL+"/")
|
||||
assert.NotNil(t, err, "should failed")
|
||||
err = tb.Send(context.TODO(), request{comment: c, parent: cp})
|
||||
require.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), "unexpected telegram status code 404", "send on broken tg")
|
||||
|
||||
assert.Equal(t, "telegram: @remark_test", tb.String())
|
||||
|
||||
@@ -115,14 +115,14 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.dataService.DeleteUser(claims.Audience, claims.User.ID); err != nil {
|
||||
if err = a.dataService.DeleteUser(claims.Audience, claims.User.ID); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user", rest.ErrNoAccess)
|
||||
return
|
||||
}
|
||||
|
||||
if claims.User.Picture != "" && a.authenticator.AvatarProxy() != nil {
|
||||
avatartStore := a.authenticator.AvatarProxy().Store
|
||||
if err := avatartStore.Remove(path.Base(claims.User.Picture)); err != nil {
|
||||
avatarStore := a.authenticator.AvatarProxy().Store
|
||||
if err = avatarStore.Remove(path.Base(claims.User.Picture)); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user's avatar", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -113,11 +113,13 @@ func TestAdmin_Title(t *testing.T) {
|
||||
srv.DataService.TitleExtractor = service.NewTitleExtractor(http.Client{Timeout: time.Second})
|
||||
tss := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() == "/post1" {
|
||||
w.Write([]byte("<html><title>post1 blah 123</title><body> 2222</body></html>"))
|
||||
_, err := w.Write([]byte("<html><title>post1 blah 123</title><body> 2222</body></html>"))
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
if r.URL.String() == "/post2" {
|
||||
w.Write([]byte("<html><title>post2 blah 123</title><body> 2222</body></html>"))
|
||||
_, err := w.Write([]byte("<html><title>post2 blah 123</title><body> 2222</body></html>"))
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(404)
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/umputun/remark/backend/app/rest"
|
||||
"github.com/umputun/remark/backend/app/rest/proxy"
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
"github.com/umputun/remark/backend/app/store/service"
|
||||
)
|
||||
|
||||
@@ -44,6 +45,7 @@ type Rest struct {
|
||||
CommentFormatter *store.CommentFormatter
|
||||
Migrator *Migrator
|
||||
NotifyService *notify.Service
|
||||
ImageService *image.Service
|
||||
|
||||
WebRoot string
|
||||
RemarkURL string
|
||||
@@ -80,6 +82,7 @@ func (s *Rest) Run(port int) {
|
||||
|
||||
s.lock.Lock()
|
||||
s.httpServer = s.makeHTTPServer(port, s.routes())
|
||||
s.httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
|
||||
s.lock.Unlock()
|
||||
|
||||
err := s.httpServer.ListenAndServe()
|
||||
@@ -89,7 +92,10 @@ func (s *Rest) Run(port int) {
|
||||
|
||||
s.lock.Lock()
|
||||
s.httpsServer = s.makeHTTPSServer(s.SSLConfig.Port, s.routes())
|
||||
s.httpsServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
|
||||
|
||||
s.httpServer = s.makeHTTPServer(port, s.httpToHTTPSRouter())
|
||||
s.httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
|
||||
s.lock.Unlock()
|
||||
|
||||
go func() {
|
||||
@@ -106,7 +112,11 @@ func (s *Rest) Run(port int) {
|
||||
m := s.makeAutocertManager()
|
||||
s.lock.Lock()
|
||||
s.httpsServer = s.makeHTTPSAutocertServer(s.SSLConfig.Port, s.routes(), m)
|
||||
s.httpsServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
|
||||
|
||||
s.httpServer = s.makeHTTPServer(port, s.httpChallengeRouter(m))
|
||||
s.httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
|
||||
|
||||
s.lock.Unlock()
|
||||
|
||||
go func() {
|
||||
@@ -219,6 +229,7 @@ func (s *Rest) routes() chi.Router {
|
||||
ropen.Get("/config", s.configCtrl)
|
||||
ropen.Post("/preview", s.previewCommentCtrl)
|
||||
ropen.Get("/info", s.infoCtrl)
|
||||
ropen.Get("/picture/{user}/{id}", s.loadPictureCtrl)
|
||||
|
||||
ropen.Mount("/rss", s.rssRoutes())
|
||||
ropen.Mount("/img", s.ImageProxy.Routes())
|
||||
@@ -251,14 +262,27 @@ func (s *Rest) routes() chi.Router {
|
||||
rauth.Put("/comment/{id}", s.updateCommentCtrl)
|
||||
rauth.Post("/comment", s.createCommentCtrl)
|
||||
rauth.With(rejectAnonUser).Put("/vote/{id}", s.voteCtrl)
|
||||
rauth.Post("/deleteme", s.deleteMeCtrl)
|
||||
rauth.With(rejectAnonUser).Post("/deleteme", s.deleteMeCtrl)
|
||||
})
|
||||
|
||||
rapi.Group(func(rauth chi.Router) {
|
||||
lmt := 10.0
|
||||
if s.UpdateLimiter > 0 {
|
||||
lmt = s.UpdateLimiter
|
||||
}
|
||||
rauth.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(lmt, nil)))
|
||||
rauth.Use(authMiddleware.Auth)
|
||||
rauth.Use(logger.New(logger.Log(log.Default()), logger.Prefix("[DEBUG]"), logger.IPfn(ipFn)).Handler)
|
||||
rauth.With(rejectAnonUser).Post("/picture", s.savePictureCtrl)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
// respond to /robots.txt with the list of allowed paths
|
||||
router.With(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(50, nil))).
|
||||
Get("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||
allowed := []string{"/find", "/last", "/id", "/count", "/counts", "/list", "/config", "/img", "/avatar"}
|
||||
allowed := []string{"/find", "/last", "/id", "/count", "/counts", "/list", "/config",
|
||||
"/img", "/avatar", "/picture"}
|
||||
for i := range allowed {
|
||||
allowed[i] = "Allow: /api/v1" + allowed[i]
|
||||
}
|
||||
|
||||
@@ -130,14 +130,9 @@ func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment", rest.ErrCommentValidation)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
code := rest.ErrCommentRejected
|
||||
switch {
|
||||
case strings.HasPrefix(err.Error(), "too late to edit"):
|
||||
code = rest.ErrCommentEditExpired
|
||||
case strings.HasPrefix(err.Error(), "parent comment with reply can't be edited"):
|
||||
code = rest.ErrCommentEditChanged
|
||||
}
|
||||
code := s.parseError(err, rest.ErrCommentRejected)
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't update comment", code)
|
||||
return
|
||||
}
|
||||
@@ -178,17 +173,7 @@ func (s *Rest) voteCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
comment, err := s.DataService.Vote(locator, id, user.ID, vote)
|
||||
if err != nil {
|
||||
code := rest.ErrVoteRejected
|
||||
switch {
|
||||
case strings.Contains(err.Error(), "can not vote for his own comment"):
|
||||
code = rest.ErrVoteSelf
|
||||
case strings.Contains(err.Error(), "already voted for"):
|
||||
code = rest.ErrVoteDbl
|
||||
case strings.Contains(err.Error(), "maximum number of votes exceeded for comment"):
|
||||
code = rest.ErrVoteMax
|
||||
case strings.Contains(err.Error(), "minimal score reached for comment"):
|
||||
code = rest.ErrVoteMinScore
|
||||
}
|
||||
code := s.parseError(err, rest.ErrVoteRejected)
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't vote for comment", code)
|
||||
return
|
||||
}
|
||||
@@ -228,14 +213,14 @@ func (s *Rest) userAllDataCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// get comments in 100 in each paginated request
|
||||
for i := 0; i < 100; i++ {
|
||||
comments, err := s.DataService.User(siteID, user.ID, 100, i*100)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't get user comments", rest.ErrInternal)
|
||||
comments, errUser := s.DataService.User(siteID, user.ID, 100, i*100)
|
||||
if errUser != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, errUser, "can't get user comments", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
b, err := json.Marshal(comments)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't marshal user comments", rest.ErrInternal)
|
||||
b, errUser := json.Marshal(comments)
|
||||
if errUser != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, errUser, "can't marshal user comments", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -285,6 +270,31 @@ func (s *Rest) deleteMeCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, R.JSON{"site": siteID, "user_id": user.ID, "token": tokenStr, "link": link})
|
||||
}
|
||||
|
||||
// POST /image - save image with form request
|
||||
func (s *Rest) savePictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
user := rest.MustGetUserInfo(r)
|
||||
|
||||
if err := r.ParseMultipartForm(5 * 1024 * 1024); err != nil { // 5M max memory, if bigger will make a file
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't parse multipart form", rest.ErrDecode)
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't get image file from the request", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
id, err := s.ImageService.Save(header.Filename, user.ID, file)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't save image", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, r, R.JSON{"id": id})
|
||||
}
|
||||
|
||||
func (s *Rest) isReadOnly(locator store.Locator) bool {
|
||||
if s.ReadOnlyAge > 0 {
|
||||
// check RO by age
|
||||
@@ -294,3 +304,28 @@ func (s *Rest) isReadOnly(locator store.Locator) bool {
|
||||
}
|
||||
return s.DataService.IsReadOnly(locator) // ro manually
|
||||
}
|
||||
|
||||
func (s *Rest) parseError(err error, defaultCode int) (code int) {
|
||||
code = defaultCode
|
||||
|
||||
switch {
|
||||
// voting errors
|
||||
case strings.Contains(err.Error(), "can not vote for his own comment"):
|
||||
code = rest.ErrVoteSelf
|
||||
case strings.Contains(err.Error(), "already voted for"):
|
||||
code = rest.ErrVoteDbl
|
||||
case strings.Contains(err.Error(), "maximum number of votes exceeded for comment"):
|
||||
code = rest.ErrVoteMax
|
||||
case strings.Contains(err.Error(), "minimal score reached for comment"):
|
||||
code = rest.ErrVoteMinScore
|
||||
|
||||
// edit errors
|
||||
case strings.HasPrefix(err.Error(), "too late to edit"):
|
||||
code = rest.ErrCommentEditExpired
|
||||
case strings.HasPrefix(err.Error(), "parent comment with reply can't be edited"):
|
||||
code = rest.ErrCommentEditChanged
|
||||
|
||||
}
|
||||
|
||||
return code
|
||||
}
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/umputun/remark/backend/app/rest"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
)
|
||||
|
||||
func TestRest_Create(t *testing.T) {
|
||||
@@ -499,3 +508,164 @@ func TestRest_DeleteMe(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 401, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestRest_SavePictureCtrl(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
// save picture
|
||||
savePic := func(name string) (id string) {
|
||||
r := strings.NewReader("file content 123")
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
bodyWriter := multipart.NewWriter(bodyBuf)
|
||||
fileWriter, err := bodyWriter.CreateFormFile("file", name)
|
||||
require.NoError(t, err)
|
||||
_, err = io.Copy(fileWriter, r)
|
||||
require.NoError(t, err)
|
||||
contentType := bodyWriter.FormDataContentType()
|
||||
require.NoError(t, bodyWriter.Close())
|
||||
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/picture", ts.URL), bodyBuf)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("Content-Type", contentType)
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
resp, err := client.Do(req)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
require.Nil(t, err)
|
||||
|
||||
m := map[string]string{}
|
||||
err = json.Unmarshal(body, &m)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, m["id"] != "")
|
||||
return m["id"]
|
||||
}
|
||||
|
||||
id := savePic("picture.png")
|
||||
resp, err := http.Get(fmt.Sprintf("%s/api/v1/picture/%s", ts.URL, id))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "file content 123", string(body))
|
||||
assert.Equal(t, "image/png", resp.Header.Get("Content-Type"))
|
||||
|
||||
id = savePic("picture.gif")
|
||||
resp, err = http.Get(fmt.Sprintf("%s/api/v1/picture/%s", ts.URL, id))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
assert.Equal(t, "image/gif", resp.Header.Get("Content-Type"))
|
||||
|
||||
id = savePic("picture.jpg")
|
||||
resp, err = http.Get(fmt.Sprintf("%s/api/v1/picture/%s", ts.URL, id))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
assert.Equal(t, "image/jpeg", resp.Header.Get("Content-Type"))
|
||||
|
||||
id = savePic("picture.blah")
|
||||
resp, err = http.Get(fmt.Sprintf("%s/api/v1/picture/%s", ts.URL, id))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
assert.Equal(t, "image/*", resp.Header.Get("Content-Type"))
|
||||
|
||||
resp, err = http.Get(fmt.Sprintf("%s/api/v1/picture/blah/pic.blah", ts.URL))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestRest_CreateWithPictures(t *testing.T) {
|
||||
ts, svc, teardown := startupT(t)
|
||||
defer func() {
|
||||
teardown()
|
||||
os.RemoveAll("/tmp/remark42")
|
||||
}()
|
||||
lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc)
|
||||
|
||||
svc.ImageService = &image.Service{
|
||||
Store: &image.FileSystem{
|
||||
Staging: "/tmp/remark42/images.staging",
|
||||
Location: "/tmp/remark42/images",
|
||||
MaxSize: 1000,
|
||||
},
|
||||
TTL: time.Millisecond * 100,
|
||||
}
|
||||
svc.DataService.EditDuration = time.Millisecond * 100
|
||||
svc.DataService.ImageService = svc.ImageService
|
||||
|
||||
uploadPicture := func(file, content string) (id string) {
|
||||
r := strings.NewReader(content)
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
bodyWriter := multipart.NewWriter(bodyBuf)
|
||||
fileWriter, err := bodyWriter.CreateFormFile("file", file)
|
||||
require.NoError(t, err)
|
||||
_, err = io.Copy(fileWriter, r)
|
||||
require.NoError(t, err)
|
||||
contentType := bodyWriter.FormDataContentType()
|
||||
require.NoError(t, bodyWriter.Close())
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/picture", ts.URL), bodyBuf)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("Content-Type", contentType)
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
resp, err := client.Do(req)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
require.Nil(t, err)
|
||||
m := map[string]string{}
|
||||
err = json.Unmarshal(body, &m)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, m["id"], ".png")
|
||||
return m["id"]
|
||||
}
|
||||
|
||||
id1 := uploadPicture("pic1.png", "file content 123")
|
||||
id2 := uploadPicture("pic2.png", "file content 12345")
|
||||
id3 := uploadPicture("pic3.png", "file content xyz12365789")
|
||||
|
||||
text := fmt.Sprintf(`text 123  *xxx*  `, id1, id2, id3)
|
||||
body := fmt.Sprintf(`{"text": "%s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, text)
|
||||
|
||||
resp, err := post(t, ts.URL+"/api/v1/comment", body)
|
||||
assert.Nil(t, err)
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
assert.Nil(t, err)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode, string(b))
|
||||
|
||||
_, err = os.Stat("/tmp/remark42/images/" + id1)
|
||||
assert.NotNil(t, err, "not moved from staging yet")
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
_, err = os.Stat("/tmp/remark42/images/" + id1)
|
||||
assert.NoError(t, err, "moved from staging")
|
||||
_, err = os.Stat("/tmp/remark42/images/" + id2)
|
||||
assert.NoError(t, err, "moved from staging")
|
||||
_, err = os.Stat("/tmp/remark42/images/" + id3)
|
||||
assert.NoError(t, err, "moved from staging")
|
||||
}
|
||||
|
||||
func TestRest_parseError(t *testing.T) {
|
||||
tbl := []struct {
|
||||
err error
|
||||
res int
|
||||
}{
|
||||
{errors.New("can not vote for his own comment"), rest.ErrVoteSelf},
|
||||
{errors.New("already voted for"), rest.ErrVoteDbl},
|
||||
{errors.New("maximum number of votes exceeded for comment"), rest.ErrVoteMax},
|
||||
{errors.New("minimal score reached for comment"), rest.ErrVoteMinScore},
|
||||
{errors.New("too late to edit"), rest.ErrCommentEditExpired},
|
||||
{errors.New("parent comment with reply can't be edited"), rest.ErrCommentEditChanged},
|
||||
{errors.New("blah blah"), rest.ErrInternal},
|
||||
}
|
||||
|
||||
svc := Rest{}
|
||||
for n, tt := range tbl {
|
||||
t.Run(strconv.Itoa(n), func(t *testing.T) {
|
||||
res := svc.parseError(tt.err, rest.ErrInternal)
|
||||
assert.Equal(t, tt.res, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/sha1" //nolint
|
||||
"crypto/sha1" // nolint
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -274,12 +275,9 @@ func (s *Rest) countMultiCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// key could be long for multiple posts, make it sha1
|
||||
k := URLKey(r) + strings.Join(posts, ",")
|
||||
hasher := sha1.New() //nolint
|
||||
if _, err := hasher.Write([]byte(k)); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't make sha1 for list of urls", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
|
||||
h := sha1.Sum([]byte(k)) //nolint
|
||||
sha := base64.URLEncoding.EncodeToString(h[:])
|
||||
|
||||
key := cache.NewKey(siteID).ID(sha).Scopes(siteID)
|
||||
data, err := s.Cache.Get(key, func() ([]byte, error) {
|
||||
counts, e := s.DataService.Counts(siteID, posts)
|
||||
@@ -330,3 +328,46 @@ func (s *Rest) listCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[WARN] can't render posts lits for site %s", siteID)
|
||||
}
|
||||
}
|
||||
|
||||
// GET /picture/{user}/{id} - get picture
|
||||
func (s *Rest) loadPictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
imgContentType := func(img string) string {
|
||||
img = strings.ToLower(img)
|
||||
switch {
|
||||
case strings.HasSuffix(img, ".png"):
|
||||
return "image/png"
|
||||
case strings.HasSuffix(img, ".jpg") || strings.HasSuffix(img, ".jpeg"):
|
||||
return "image/jpeg"
|
||||
case strings.HasSuffix(img, ".gif"):
|
||||
return "image/gif"
|
||||
}
|
||||
return "image/*"
|
||||
}
|
||||
|
||||
id := chi.URLParam(r, "user") + "/" + chi.URLParam(r, "id")
|
||||
imgRdr, size, err := s.ImageService.Load(id)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get image "+id, rest.ErrAssetNotFound)
|
||||
return
|
||||
}
|
||||
// enforce client-side caching
|
||||
etag := `"` + id + `"`
|
||||
w.Header().Set("Etag", etag)
|
||||
w.Header().Set("Cache-Control", "max-age=604800") // 7 days
|
||||
if match := r.Header.Get("If-None-Match"); match != "" {
|
||||
if strings.Contains(match, etag) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
defer imgRdr.Close()
|
||||
|
||||
w.Header().Set("Content-Type", imgContentType(id))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(int(size)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err = io.Copy(w, imgRdr); err != nil {
|
||||
log.Printf("[WARN] can't send response to %s, %s", r.RemoteAddr, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,6 +377,37 @@ func TestRest_List(t *testing.T) {
|
||||
assert.Equal(t, 3, pi[1].Count)
|
||||
}
|
||||
|
||||
func TestRest_ListWithSkipAndLimit(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
c1 := store.Comment{Text: "test test #1",
|
||||
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
|
||||
c2 := store.Comment{Text: "test test #2", ParentID: "p1",
|
||||
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}}
|
||||
c3 := store.Comment{Text: "test test #3", ParentID: "p1",
|
||||
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah3"}}
|
||||
|
||||
addComment(t, c1, ts)
|
||||
addComment(t, c1, ts)
|
||||
addComment(t, c1, ts)
|
||||
addComment(t, c2, ts)
|
||||
addComment(t, c2, ts)
|
||||
addComment(t, c3, ts)
|
||||
addComment(t, c3, ts)
|
||||
|
||||
body, code := get(t, ts.URL+"/api/v1/list?site=radio-t&skip=1&limit=2")
|
||||
assert.Equal(t, 200, code)
|
||||
pi := []store.PostInfo{}
|
||||
err := json.Unmarshal([]byte(body), &pi)
|
||||
assert.Nil(t, err)
|
||||
require.Equal(t, 2, len(pi))
|
||||
assert.Equal(t, "https://radio-t.com/blah2", pi[0].URL)
|
||||
assert.Equal(t, 2, pi[0].Count)
|
||||
assert.Equal(t, "https://radio-t.com/blah1", pi[1].URL)
|
||||
assert.Equal(t, 3, pi[1].Count)
|
||||
}
|
||||
|
||||
func TestRest_Config(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
defer teardown()
|
||||
@@ -442,5 +473,5 @@ func TestRest_Robots(t *testing.T) {
|
||||
assert.Equal(t, 200, code)
|
||||
assert.Equal(t, "User-agent: *\nDisallow: /auth/\nDisallow: /api/\nAllow: /api/v1/find\n"+
|
||||
"Allow: /api/v1/last\nAllow: /api/v1/id\nAllow: /api/v1/count\nAllow: /api/v1/counts\n"+
|
||||
"Allow: /api/v1/list\nAllow: /api/v1/config\nAllow: /api/v1/img\nAllow: /api/v1/avatar\n", string(body))
|
||||
"Allow: /api/v1/list\nAllow: /api/v1/config\nAllow: /api/v1/img\nAllow: /api/v1/avatar\nAllow: /api/v1/picture\n", string(body))
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
adminstore "github.com/umputun/remark/backend/app/store/admin"
|
||||
"github.com/umputun/remark/backend/app/store/engine"
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
"github.com/umputun/remark/backend/app/store/service"
|
||||
)
|
||||
|
||||
@@ -203,6 +204,7 @@ func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
|
||||
os.Remove(testDb)
|
||||
os.Remove(testHTML)
|
||||
os.RemoveAll("/tmp/ava-remark42")
|
||||
os.RemoveAll("/tmp/pics-remark42")
|
||||
|
||||
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDb, SiteID: "radio-t"})
|
||||
require.Nil(t, err)
|
||||
@@ -232,7 +234,14 @@ func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
|
||||
Cache: memCache,
|
||||
WebRoot: "/tmp",
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
|
||||
ImageService: &image.Service{
|
||||
Store: &image.FileSystem{
|
||||
Location: "/tmp/pics-remark42",
|
||||
Partitions: 100,
|
||||
MaxSize: 10000,
|
||||
},
|
||||
TTL: time.Millisecond * 100,
|
||||
},
|
||||
ImageProxy: &proxy.Image{},
|
||||
ReadOnlyAge: 10,
|
||||
CommentFormatter: store.NewCommentFormatter(&proxy.Image{}),
|
||||
@@ -258,6 +267,7 @@ func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
|
||||
os.Remove(testDb)
|
||||
os.Remove(testHTML)
|
||||
os.RemoveAll("/tmp/ava-remark42")
|
||||
os.RemoveAll("/tmp/pics-remark42")
|
||||
}
|
||||
|
||||
return ts, srv, teardown
|
||||
|
||||
@@ -57,7 +57,7 @@ func (s *Rest) rssPostCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if _, err := w.Write(data); err != nil {
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func (s *Rest) rssSiteCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write(data); err != nil {
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func (s *Rest) rssRepliesCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write(data); err != nil {
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,8 @@ func imgHTTPServer(t *testing.T) *httptest.Server {
|
||||
t.Log("http img request", r.URL)
|
||||
w.Header().Add("Content-Length", "123")
|
||||
w.Header().Add("Content-Type", "image/png")
|
||||
w.Write([]byte(fmt.Sprintf("%123s", "X")))
|
||||
_, err := w.Write([]byte(fmt.Sprintf("%123s", "X")))
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/image/img-slow.png" {
|
||||
|
||||
@@ -74,6 +74,7 @@ func NewBoltDB(options bolt.Options, sites ...BoltSite) (*BoltDB, error) {
|
||||
}
|
||||
|
||||
result.dbs[site.SiteID] = db
|
||||
log.Printf("[DEBUG] bolt store created for %s", site.SiteID)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -155,7 +156,7 @@ func (b *BoltDB) Find(locator store.Locator, sortFld string) (comments []store.C
|
||||
|
||||
return bucket.ForEach(func(k, v []byte) error {
|
||||
comment := store.Comment{}
|
||||
if e := json.Unmarshal(v, &comment); e != nil {
|
||||
if e = json.Unmarshal(v, &comment); e != nil {
|
||||
return errors.Wrap(e, "failed to unmarshal")
|
||||
}
|
||||
comments = append(comments, comment)
|
||||
@@ -195,7 +196,7 @@ func (b *BoltDB) Last(siteID string, max int) (comments []store.Comment, err err
|
||||
}
|
||||
|
||||
comment := store.Comment{}
|
||||
if e := b.load(postBkt, []byte(commentID), &comment); e != nil {
|
||||
if e = b.load(postBkt, []byte(commentID), &comment); e != nil {
|
||||
log.Printf("[WARN] can't load comment for %s from store %s", commentID, url)
|
||||
continue
|
||||
}
|
||||
@@ -335,11 +336,11 @@ func (b *BoltDB) User(siteID, userID string, limit, skip int) (comments []store.
|
||||
|
||||
// retrieve comments for refs
|
||||
for _, v := range commentRefs {
|
||||
url, commentID, e := b.parseRef([]byte(v))
|
||||
if e != nil {
|
||||
return comments, errors.Wrapf(e, "can't parse reference %s", v)
|
||||
url, commentID, errParse := b.parseRef([]byte(v))
|
||||
if errParse != nil {
|
||||
return comments, errors.Wrapf(errParse, "can't parse reference %s", v)
|
||||
}
|
||||
if c, e := b.Get(store.Locator{SiteID: siteID, URL: url}, commentID); e == nil {
|
||||
if c, errRef := b.Get(store.Locator{SiteID: siteID, URL: url}, commentID); errRef == nil {
|
||||
comments = append(comments, c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,19 +29,19 @@ func (b *BoltDB) Delete(locator store.Locator, commentID string, mode store.Dele
|
||||
}
|
||||
|
||||
comment := store.Comment{}
|
||||
if err := b.load(postBkt, []byte(commentID), &comment); err != nil {
|
||||
if err = b.load(postBkt, []byte(commentID), &comment); err != nil {
|
||||
return errors.Wrapf(err, "can't load key %s from bucket %s", commentID, locator.URL)
|
||||
}
|
||||
// set deleted status and clear fields
|
||||
comment.SetDeleted(mode)
|
||||
|
||||
if err := b.save(postBkt, []byte(commentID), comment); err != nil {
|
||||
if err = b.save(postBkt, []byte(commentID), comment); err != nil {
|
||||
return errors.Wrapf(err, "can't save deleted comment for key %s from bucket %s", commentID, locator.URL)
|
||||
}
|
||||
|
||||
// delete from "last" bucket
|
||||
lastBkt := tx.Bucket([]byte(lastBucketName))
|
||||
if err := lastBkt.Delete([]byte(commentID)); err != nil {
|
||||
if err = lastBkt.Delete([]byte(commentID)); err != nil {
|
||||
return errors.Wrapf(err, "can't delete key %s from bucket %s", commentID, lastBucketName)
|
||||
}
|
||||
|
||||
@@ -200,8 +200,8 @@ func (b *BoltDB) IsBlocked(siteID string, userID string) (blocked bool) {
|
||||
return nil
|
||||
}
|
||||
|
||||
until, err := time.Parse(tsNano, string(val))
|
||||
if err != nil {
|
||||
until, e := time.Parse(tsNano, string(val))
|
||||
if e != nil {
|
||||
blocked = false
|
||||
return nil
|
||||
}
|
||||
@@ -223,15 +223,15 @@ func (b *BoltDB) Blocked(siteID string) (users []store.BlockedUser, err error) {
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(blocksBucketName))
|
||||
return bucket.ForEach(func(k []byte, v []byte) error {
|
||||
ts, e := time.ParseInLocation(tsNano, string(v), time.Local)
|
||||
if e != nil {
|
||||
return errors.Wrap(e, "can't parse block ts")
|
||||
ts, errParse := time.ParseInLocation(tsNano, string(v), time.Local)
|
||||
if errParse != nil {
|
||||
return errors.Wrap(errParse, "can't parse block ts")
|
||||
}
|
||||
if time.Now().Before(ts) {
|
||||
// get user name from comment user section
|
||||
userName := ""
|
||||
userComments, e := b.User(siteID, string(k), 1, 0)
|
||||
if e == nil && len(userComments) > 0 {
|
||||
userComments, errUser := b.User(siteID, string(k), 1, 0)
|
||||
if errUser == nil && len(userComments) > 0 {
|
||||
userName = userComments[0].User.Name
|
||||
}
|
||||
users = append(users, store.BlockedUser{ID: string(k), Name: userName, Until: ts})
|
||||
|
||||
@@ -230,8 +230,8 @@ func (m *Mongo) Verified(siteID string) (ids []string, err error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, m := range metas {
|
||||
ids = append(ids, m.ID)
|
||||
for _, meta := range metas {
|
||||
ids = append(ids, meta.ID)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/crc64"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// FileSystem provides image Store for local files. Saves and loads files from Location, restricts max size.
|
||||
type FileSystem struct {
|
||||
Location string
|
||||
Staging string
|
||||
MaxSize int
|
||||
Partitions int
|
||||
|
||||
crc struct {
|
||||
*crc64.Table
|
||||
sync.Once
|
||||
mask string
|
||||
divider uint64
|
||||
}
|
||||
}
|
||||
|
||||
// Save data from reader for given file name to local FS, staging directory. Returns id as user/uuid.ext
|
||||
// Files partitioned across multiple subdirectories and the final path includes part, i.e. /location/user1/03/123-4567.png
|
||||
func (f *FileSystem) Save(fileName string, userID string, r io.Reader) (id string, err error) {
|
||||
|
||||
uid, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "can't make image uuid")
|
||||
}
|
||||
|
||||
id = path.Join(userID, uid.String()) + filepath.Ext(fileName) // make id as user/uuid.ext
|
||||
dst := f.location(f.Staging, id)
|
||||
|
||||
if err = os.MkdirAll(path.Dir(dst), 0700); err != nil {
|
||||
return "", errors.Wrap(err, "can't make image directory")
|
||||
}
|
||||
|
||||
fh, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "can't make image file %s", dst)
|
||||
}
|
||||
lr := io.LimitReader(r, int64(f.MaxSize)+1)
|
||||
written, err := io.Copy(fh, lr)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "can't write image file %s", dst)
|
||||
}
|
||||
if err = fh.Close(); err != nil {
|
||||
return "", errors.Wrapf(err, "can't close image file %s", dst)
|
||||
}
|
||||
if written > int64(f.MaxSize) {
|
||||
if err = os.Remove(dst); err != nil {
|
||||
log.Printf("[WARN] can't remove image file %s, %v", dst, err)
|
||||
}
|
||||
return "", errors.Errorf("file %s is too large", fileName)
|
||||
}
|
||||
log.Printf("[DEBUG] file %s saved for image %s", fh.Name(), fileName)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Commit file stored in staging location by moving it to permanent location
|
||||
func (f *FileSystem) Commit(id string) error {
|
||||
log.Printf("[DEBUG] commit image %s", id)
|
||||
stagingImage, permImage := f.location(f.Staging, id), f.location(f.Location, id)
|
||||
|
||||
if err := os.MkdirAll(path.Dir(permImage), 0700); err != nil {
|
||||
return errors.Wrap(err, "can't make image directory")
|
||||
}
|
||||
|
||||
err := os.Rename(stagingImage, permImage)
|
||||
return errors.Wrapf(err, "failed to commit image %s", id)
|
||||
}
|
||||
|
||||
// Load image from FS. Uses id to get partition subdirectory.
|
||||
// returns ReadCloser and caller should call close after processing completed.
|
||||
func (f *FileSystem) Load(id string) (io.ReadCloser, int64, error) {
|
||||
|
||||
// get image file by id. first try permanent location and if not found - staging
|
||||
img := func(id string) (file string, st os.FileInfo, err error) {
|
||||
file = f.location(f.Location, id)
|
||||
st, err = os.Stat(file)
|
||||
if err != nil {
|
||||
file = f.location(f.Staging, id)
|
||||
st, err = os.Stat(file)
|
||||
}
|
||||
return file, st, errors.Wrapf(err, "can't get image stats for %s", id)
|
||||
}
|
||||
|
||||
imgFile, st, err := img(id)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrapf(err, "can't get image file for %s", id)
|
||||
}
|
||||
|
||||
fh, err := os.Open(imgFile)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrapf(err, "can't load image %s", id)
|
||||
}
|
||||
return fh, st.Size(), nil
|
||||
}
|
||||
|
||||
// Cleanup runs scan of staging and removes old files based on ttl
|
||||
func (f *FileSystem) Cleanup(ctx context.Context, ttl time.Duration) error {
|
||||
|
||||
if _, err := os.Stat(f.Staging); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := filepath.Walk(f.Staging, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
age := time.Since(info.ModTime())
|
||||
if age > ttl {
|
||||
log.Printf("[INFO] remove staging image %s, age %v", path, age)
|
||||
return os.Remove(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return errors.Wrap(err, "failed to cleanup images")
|
||||
}
|
||||
|
||||
// location gets full path for id by adding partition to the final path in order to keep files in different subdirectories
|
||||
// and avoid too many files in a single place.
|
||||
// the end result is a full path like this - /tmp/images/user1/92/xxx-yyy.png.
|
||||
// Number of partitions defined by FileSystem.Partitions
|
||||
func (f *FileSystem) location(base string, id string) string {
|
||||
|
||||
partition := func(id string) string {
|
||||
f.crc.Do(func() {
|
||||
f.crc.Table = crc64.MakeTable(crc64.ECMA)
|
||||
p := int(math.Round(math.Log10(float64(f.Partitions))))
|
||||
f.crc.mask = "%0" + strconv.Itoa(p) + "d"
|
||||
f.crc.divider = uint64(math.Pow(10, float64(p)))
|
||||
})
|
||||
checksum64 := crc64.Checksum([]byte(id), f.crc.Table)
|
||||
partition := checksum64 % f.crc.divider
|
||||
return fmt.Sprintf(f.crc.mask, partition)
|
||||
}
|
||||
|
||||
user, file := "unknown", id // default if no user in id
|
||||
if elems := strings.Split(id, "/"); len(elems) == 2 {
|
||||
user, file = elems[0], elems[1] // user in id
|
||||
}
|
||||
|
||||
if f.Partitions == 0 {
|
||||
return path.Join(base, user, file) // avoid partition directory if 0 Partitions
|
||||
}
|
||||
|
||||
return path.Join(base, user, partition(id), file)
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFsStore_Save(t *testing.T) {
|
||||
svc, teardown := prepareImageTest(t)
|
||||
defer teardown()
|
||||
|
||||
id, err := svc.Save("file1.png", "user1", strings.NewReader("blah blah"))
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, id, "user1/")
|
||||
assert.Contains(t, id, ".png")
|
||||
t.Log(id)
|
||||
|
||||
img := svc.location(svc.Staging, id)
|
||||
t.Log(img)
|
||||
data, err := ioutil.ReadFile(img)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah blah", string(data))
|
||||
}
|
||||
|
||||
func TestFsStore_SaveAndCommit(t *testing.T) {
|
||||
svc, teardown := prepareImageTest(t)
|
||||
defer teardown()
|
||||
|
||||
id, err := svc.Save("file1.png", "user1", strings.NewReader("blah blah"))
|
||||
require.NoError(t, err)
|
||||
err = svc.Commit(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
imgStaging := svc.location(svc.Staging, id)
|
||||
_, err = os.Stat(imgStaging)
|
||||
assert.NotNil(t, err, "no file on staging anymore")
|
||||
|
||||
img := svc.location(svc.Location, id)
|
||||
t.Log(img)
|
||||
data, err := ioutil.ReadFile(img)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah blah", string(data))
|
||||
}
|
||||
|
||||
func TestFsStore_SaveTooLarge(t *testing.T) {
|
||||
svc, teardown := prepareImageTest(t)
|
||||
defer teardown()
|
||||
svc.MaxSize = 5
|
||||
_, err := svc.Save("blah_ff1.png", "user2", strings.NewReader("blah blah"))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "is too large")
|
||||
}
|
||||
|
||||
func TestFsStore_LoadAfterSave(t *testing.T) {
|
||||
|
||||
svc, teardown := prepareImageTest(t)
|
||||
defer teardown()
|
||||
|
||||
id, err := svc.Save("blah_ff1.png", "user1", strings.NewReader("blah blah"))
|
||||
assert.NoError(t, err)
|
||||
t.Log(id)
|
||||
|
||||
r, sz, err := svc.Load(id)
|
||||
assert.NoError(t, err)
|
||||
defer func() { assert.NoError(t, r.Close()) }()
|
||||
data, err := ioutil.ReadAll(r)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah blah", string(data))
|
||||
assert.Equal(t, int64(9), sz)
|
||||
_, _, err = svc.Load("abcd")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestFsStore_LoadAfterCommit(t *testing.T) {
|
||||
|
||||
svc, teardown := prepareImageTest(t)
|
||||
defer teardown()
|
||||
|
||||
id, err := svc.Save("blah_ff1.png", "user1", strings.NewReader("blah blah"))
|
||||
assert.NoError(t, err)
|
||||
t.Log(id)
|
||||
err = svc.Commit(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
r, sz, err := svc.Load(id)
|
||||
assert.NoError(t, err)
|
||||
defer func() { assert.NoError(t, r.Close()) }()
|
||||
data, err := ioutil.ReadAll(r)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah blah", string(data))
|
||||
assert.Equal(t, int64(9), sz)
|
||||
_, _, err = svc.Load("abcd")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestFsStore_location(t *testing.T) {
|
||||
tbl := []struct {
|
||||
partitions int
|
||||
id, res string
|
||||
}{
|
||||
{10, "u1/abcdefg.png", "/tmp/u1/4/abcdefg.png"},
|
||||
{10, "u2/abcdefe", "/tmp/u2/0/abcdefe"},
|
||||
{10, "u3/12345", "/tmp/u3/4/12345"},
|
||||
{100, "12345", "/tmp/unknown/69/12345"},
|
||||
{100, "xyzz", "/tmp/unknown/58/xyzz"},
|
||||
{100, "u4/6851dcde6024e03258a66705f29e14b506048c74.png", "/tmp/u4/07/6851dcde6024e03258a66705f29e14b506048c74.png"},
|
||||
{5, "user/6851dcde6024e03258a66705f29e14b506048c74.png", "/tmp/user/1/6851dcde6024e03258a66705f29e14b506048c74.png"},
|
||||
{5, "aa-xxxyz.png", "/tmp/unknown/3/aa-xxxyz.png"},
|
||||
{0, "12345", "/tmp/unknown/12345"},
|
||||
{0, "user/12345", "/tmp/user/12345"},
|
||||
}
|
||||
for n, tt := range tbl {
|
||||
t.Run(strconv.Itoa(n), func(t *testing.T) {
|
||||
svc := FileSystem{Location: "/tmp", Partitions: tt.partitions}
|
||||
assert.Equal(t, tt.res, svc.location("/tmp", tt.id))
|
||||
})
|
||||
}
|
||||
|
||||
// generate random names and make sure partition never runs out of allowed
|
||||
letterRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
randomID := func(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return "user1" + "/" + string(b)
|
||||
}
|
||||
|
||||
svc := FileSystem{Location: "/tmp", Partitions: 10}
|
||||
for i := 0; i < 1000; i++ {
|
||||
v := randomID(rand.Intn(64))
|
||||
location := svc.location("/tmp", v)
|
||||
elems := strings.Split(location, "/")
|
||||
p, err := strconv.Atoi(elems[3])
|
||||
require.NoError(t, err, location)
|
||||
assert.True(t, p >= 0 && p < 10)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsStore_Cleanup(t *testing.T) {
|
||||
svc, teardown := prepareImageTest(t)
|
||||
defer teardown()
|
||||
|
||||
save := func(file string, user string, content string) (path string) {
|
||||
id, err := svc.Save(file, user, strings.NewReader(content))
|
||||
require.NoError(t, err)
|
||||
img := svc.location(svc.Staging, id)
|
||||
data, err := ioutil.ReadFile(img)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, content, string(data))
|
||||
return img
|
||||
}
|
||||
|
||||
// save 3 images to staging
|
||||
img1 := save("blah_ff1.png", "user1", "blah blah1")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
img2 := save("blah_ff2.png", "user1", "blah blah2")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
img3 := save("blah_ff3.png", "user2", "blah blah3")
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // make first image expired
|
||||
err := svc.Cleanup(context.Background(), time.Millisecond*300)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(img1)
|
||||
assert.NotNil(t, err, "no file on staging anymore")
|
||||
_, err = os.Stat(img2)
|
||||
assert.NoError(t, err, "file on staging")
|
||||
_, err = os.Stat(img3)
|
||||
assert.NoError(t, err, "file on staging")
|
||||
|
||||
time.Sleep(200 * time.Millisecond) // make all images expired
|
||||
err = svc.Cleanup(context.Background(), time.Millisecond*300)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(img2)
|
||||
assert.NotNil(t, err, "no file on staging anymore")
|
||||
_, err = os.Stat(img3)
|
||||
assert.NotNil(t, err, "no file on staging anymore")
|
||||
}
|
||||
|
||||
func prepareImageTest(t *testing.T) (svc *FileSystem, teardown func()) {
|
||||
loc, err := ioutil.TempDir("", "test_image_r42")
|
||||
require.NoError(t, err, "failed to make temp dir")
|
||||
|
||||
staging, err := ioutil.TempDir("", "test_image_r42.staging")
|
||||
require.NoError(t, err, "failed to make temp staging dir")
|
||||
|
||||
svc = &FileSystem{
|
||||
Location: loc,
|
||||
Staging: staging,
|
||||
Partitions: 100,
|
||||
MaxSize: 50,
|
||||
}
|
||||
|
||||
teardown = func() {
|
||||
defer func() {
|
||||
assert.NoError(t, os.RemoveAll(loc))
|
||||
assert.NoError(t, os.RemoveAll(staging))
|
||||
}()
|
||||
}
|
||||
|
||||
return svc, teardown
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// Package image handles storing, resizing and retrieval of images
|
||||
// Provides Store with Save and Load and one implementation on top of local file system.
|
||||
// Service object encloses Store and add common methods, this is the one consumer should use
|
||||
package image
|
||||
|
||||
//go:generate sh -c "mockgen -source=image.go -package=image > image_mock.go"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Store defines interface for saving and loading pictures.
|
||||
// Declares two-stage save with commit
|
||||
type Store interface {
|
||||
Save(fileName string, userID string, r io.Reader) (id string, err error) // get name and reader and returns ID of stored image
|
||||
Commit(id string) error // move image from staging to permanent
|
||||
Load(id string) (io.ReadCloser, int64, error) // load image by ID. Caller has to close the reader.
|
||||
Cleanup(ctx context.Context, ttl time.Duration) error // run removal loop for old images on staging
|
||||
}
|
||||
|
||||
// Service extends Store with common functions needed for any store implementation
|
||||
type Service struct {
|
||||
Store
|
||||
TTL time.Duration // for how long file allowed on staging
|
||||
ImageAPI string // image api matching path
|
||||
|
||||
wg sync.WaitGroup
|
||||
submitCh chan submitReq
|
||||
once sync.Once
|
||||
term int32
|
||||
}
|
||||
|
||||
const submitQueueSize = 5000
|
||||
|
||||
type submitReq struct {
|
||||
idsFn func() (ids []string)
|
||||
TS time.Time
|
||||
}
|
||||
|
||||
// Submit multiple ids via function for delayed commit
|
||||
func (s *Service) Submit(idsFn func() []string) {
|
||||
if idsFn == nil || s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.once.Do(func() {
|
||||
log.Printf("[DEBUG] image submitter activated")
|
||||
s.submitCh = make(chan submitReq, submitQueueSize)
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
for req := range s.submitCh {
|
||||
// wait for TTL expiration with emergency pass on term
|
||||
for atomic.LoadInt32(&s.term) == 0 && time.Since(req.TS) <= s.TTL {
|
||||
time.Sleep(time.Millisecond * 10) // small sleep to relive busy wait but keep reactive for term (close)
|
||||
}
|
||||
for _, id := range req.idsFn() {
|
||||
if err := s.Commit(id); err != nil {
|
||||
log.Printf("[WARN] failed to commit image %s", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Printf("[INFO] image submitter terminated")
|
||||
}()
|
||||
})
|
||||
|
||||
s.submitCh <- submitReq{idsFn: idsFn, TS: time.Now()}
|
||||
}
|
||||
|
||||
// ExtractPictures gets list of images from the doc html and convert from urls to ids, i.e. user/pic.png
|
||||
func (s *Service) ExtractPictures(commentHTML string) (ids []string, err error) {
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(commentHTML))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't create document")
|
||||
}
|
||||
result := []string{}
|
||||
doc.Find("img").Each(func(i int, sl *goquery.Selection) {
|
||||
if im, ok := sl.Attr("src"); ok {
|
||||
if strings.Contains(im, s.ImageAPI) {
|
||||
elems := strings.Split(im, "/")
|
||||
if len(elems) >= 2 {
|
||||
id := elems[len(elems)-2] + "/" + elems[len(elems)-1]
|
||||
result = append(result, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Cleanup runs periodic cleanup with TTL. Blocking loop, should be called inside of goroutine by consumer
|
||||
func (s *Service) Cleanup(ctx context.Context) {
|
||||
log.Printf("[INFO] start pictures cleanup, staging ttl=%v", s.TTL)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("[INFO] cleanup terminated, %v", ctx.Err())
|
||||
return
|
||||
case <-time.After(s.TTL / 2):
|
||||
if err := s.Store.Cleanup(ctx, s.TTL); err != nil {
|
||||
log.Printf("[WARN] failed to cleanup, %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close flushes all in-progress submits and enforces waiting commits
|
||||
func (s *Service) Close() {
|
||||
log.Printf("[INFO] close image service ")
|
||||
atomic.AddInt32(&s.term, 1) // enforce non-delayed commits for all ids left in submitCh
|
||||
if s.submitCh != nil {
|
||||
close(s.submitCh)
|
||||
}
|
||||
s.wg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: image.go
|
||||
|
||||
// Package image is a generated GoMock package.
|
||||
package image
|
||||
|
||||
import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
io "io"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
)
|
||||
|
||||
// MockStore is a mock of Store interface
|
||||
type MockStore struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockStoreMockRecorder
|
||||
}
|
||||
|
||||
// MockStoreMockRecorder is the mock recorder for MockStore
|
||||
type MockStoreMockRecorder struct {
|
||||
mock *MockStore
|
||||
}
|
||||
|
||||
// NewMockStore creates a new mock instance
|
||||
func NewMockStore(ctrl *gomock.Controller) *MockStore {
|
||||
mock := &MockStore{ctrl: ctrl}
|
||||
mock.recorder = &MockStoreMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockStore) EXPECT() *MockStoreMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Save mocks base method
|
||||
func (m *MockStore) Save(fileName, userID string, r io.Reader) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Save", fileName, userID, r)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Save indicates an expected call of Save
|
||||
func (mr *MockStoreMockRecorder) Save(fileName, userID, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockStore)(nil).Save), fileName, userID, r)
|
||||
}
|
||||
|
||||
// Commit mocks base method
|
||||
func (m *MockStore) Commit(id string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Commit", id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Commit indicates an expected call of Commit
|
||||
func (mr *MockStoreMockRecorder) Commit(id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockStore)(nil).Commit), id)
|
||||
}
|
||||
|
||||
// Load mocks base method
|
||||
func (m *MockStore) Load(id string) (io.ReadCloser, int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Load", id)
|
||||
ret0, _ := ret[0].(io.ReadCloser)
|
||||
ret1, _ := ret[1].(int64)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// Load indicates an expected call of Load
|
||||
func (mr *MockStoreMockRecorder) Load(id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Load", reflect.TypeOf((*MockStore)(nil).Load), id)
|
||||
}
|
||||
|
||||
// Cleanup mocks base method
|
||||
func (m *MockStore) Cleanup(ctx context.Context, ttl time.Duration) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Cleanup", ctx, ttl)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Cleanup indicates an expected call of Cleanup
|
||||
func (mr *MockStoreMockRecorder) Cleanup(ctx, ttl interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cleanup", reflect.TypeOf((*MockStore)(nil).Cleanup), ctx, ttl)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_ExtractPictures(t *testing.T) {
|
||||
svc := Service{ImageAPI: "/blah/"}
|
||||
html := `blah <img src="/blah/user1/pic1.png"/> foo
|
||||
<img src="/blah/user2/pic3.png"/> xyz <p>123</p> <img src="/pic3.png"/>`
|
||||
ids, err := svc.ExtractPictures(html)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, len(ids), "two images")
|
||||
assert.Equal(t, "user1/pic1.png", ids[0])
|
||||
assert.Equal(t, "user2/pic3.png", ids[1])
|
||||
}
|
||||
|
||||
func TestService_Cleanup(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
store := NewMockStore(ctrl)
|
||||
store.EXPECT().Cleanup(gomock.Any(), gomock.Any()).Times(10)
|
||||
|
||||
svc := Service{Store: store, TTL: 100 * time.Millisecond}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*549)
|
||||
defer cancel()
|
||||
svc.Cleanup(ctx)
|
||||
}
|
||||
|
||||
func TestService_Submit(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
store := NewMockStore(ctrl)
|
||||
|
||||
store.EXPECT().Commit(gomock.Any()).Times(5) // all 5 should be committed
|
||||
svc := Service{Store: store, ImageAPI: "/blah/", TTL: time.Millisecond * 100}
|
||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||
svc.Submit(func() []string { return []string{"id4", "id5"} })
|
||||
svc.Submit(nil)
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
}
|
||||
|
||||
func TestService_Close(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
store := NewMockStore(ctrl)
|
||||
|
||||
store.EXPECT().Commit(gomock.Any()).Times(5) // all 5 should be committed
|
||||
svc := Service{Store: store, ImageAPI: "/blah/", TTL: time.Millisecond * 500}
|
||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||
svc.Submit(func() []string { return []string{"id4", "id5"} })
|
||||
svc.Submit(nil)
|
||||
svc.Close()
|
||||
}
|
||||
|
||||
func TestService_SubmitDelay(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer func() {
|
||||
ctrl.Finish()
|
||||
}()
|
||||
|
||||
store := NewMockStore(ctrl)
|
||||
|
||||
store.EXPECT().Commit(gomock.Any()).Times(3) // first batch should be committed
|
||||
svc := Service{Store: store, ImageAPI: "/blah/", TTL: time.Millisecond * 100}
|
||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||
time.Sleep(150 * time.Millisecond) // let first batch to pass TTL
|
||||
svc.Submit(func() []string { return []string{"id4", "id5"} })
|
||||
svc.Submit(nil)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
multierror "github.com/hashicorp/go-multierror"
|
||||
cache "github.com/patrickmn/go-cache"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/admin"
|
||||
@@ -28,6 +29,7 @@ type DataStore struct {
|
||||
PositiveScore bool
|
||||
TitleExtractor *TitleExtractor
|
||||
RestrictedWordsMatcher *RestrictedWordsMatcher
|
||||
ImageService *image.Service
|
||||
|
||||
// granular locks
|
||||
scopedLocks struct {
|
||||
@@ -90,9 +92,32 @@ func (s *DataStore) Create(comment store.Comment) (commentID string, err error)
|
||||
comment.PostTitle = title
|
||||
}()
|
||||
|
||||
s.submitImages(comment)
|
||||
return s.Interface.Create(comment)
|
||||
}
|
||||
|
||||
// submitImages initiated delayed commit of all images from the comment uploaded to remark42
|
||||
func (s *DataStore) submitImages(comment store.Comment) {
|
||||
|
||||
s.ImageService.Submit(func() []string {
|
||||
c := comment
|
||||
cc, err := s.Get(c.Locator, c.ID) // this can be called after last edit, we have to retrieve fresh comment
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't get comment's %s text for image extraction, %v", c.ID, err)
|
||||
return nil
|
||||
}
|
||||
imgIds, err := s.ImageService.ExtractPictures(cc.Text)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't get extract pictures from %s, %v", c.ID, err)
|
||||
return nil
|
||||
}
|
||||
if len(imgIds) > 0 {
|
||||
log.Printf("[DEBUG] image ids extracted from %s - %+v", c.ID, imgIds)
|
||||
}
|
||||
return imgIds
|
||||
})
|
||||
}
|
||||
|
||||
// prepareNewComment sets new comment fields, hashing and sanitizing data
|
||||
func (s *DataStore) prepareNewComment(comment store.Comment) (store.Comment, error) {
|
||||
// fill ID and time if empty
|
||||
@@ -129,8 +154,8 @@ func (s *DataStore) SetPin(locator store.Locator, commentID string, status bool)
|
||||
// Vote for comment by id and locator
|
||||
func (s *DataStore) Vote(locator store.Locator, commentID string, userID string, val bool) (comment store.Comment, err error) {
|
||||
|
||||
cLock := s.getsScopedLocks(locator.URL) // get lock for URL scope
|
||||
cLock.Lock() // prevents race on voting
|
||||
cLock := s.getScopedLocks(locator.URL) // get lock for URL scope
|
||||
cLock.Lock() // prevents race on voting
|
||||
defer cLock.Unlock()
|
||||
|
||||
comment, err = s.Get(locator, commentID)
|
||||
@@ -455,8 +480,8 @@ func (s *DataStore) upsAndDowns(c store.Comment) (ups, downs int) {
|
||||
return ups, downs
|
||||
}
|
||||
|
||||
// getsScopedLocks pull lock from the map if found or create a new one
|
||||
func (s *DataStore) getsScopedLocks(id string) (lock sync.Locker) {
|
||||
// getScopedLocks pull lock from the map if found or create a new one
|
||||
func (s *DataStore) getScopedLocks(id string) (lock sync.Locker) {
|
||||
s.scopedLocks.Do(func() { s.scopedLocks.locks = map[string]sync.Locker{} })
|
||||
|
||||
s.scopedLocks.Lock()
|
||||
|
||||
@@ -13,9 +13,12 @@ import (
|
||||
"time"
|
||||
|
||||
bolt "github.com/coreos/bbolt"
|
||||
"github.com/go-pkgz/lgr"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/admin"
|
||||
@@ -25,7 +28,7 @@ import (
|
||||
var testDb = "/tmp/test-remark.db"
|
||||
|
||||
func TestService_CreateFromEmpty(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
ks := admin.NewStaticKeyStore("secret 123")
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: ks}
|
||||
comment := store.Comment{
|
||||
@@ -49,7 +52,7 @@ func TestService_CreateFromEmpty(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_CreateFromPartial(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
ks := admin.NewStaticKeyStore("secret 123")
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: ks}
|
||||
comment := store.Comment{
|
||||
@@ -76,7 +79,7 @@ func TestService_CreateFromPartial(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_CreateFromPartialWithTitle(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
ks := admin.NewStaticKeyStore("secret 123")
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: ks,
|
||||
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second})}
|
||||
@@ -106,7 +109,7 @@ func TestService_CreateFromPartialWithTitle(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_SetTitle(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
|
||||
var titleEnable int32
|
||||
tss := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -114,11 +117,13 @@ func TestService_SetTitle(t *testing.T) {
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
if r.URL.String() == "/post1" {
|
||||
w.Write([]byte("<html><title>post1 blah 123</title><body> 2222</body></html>"))
|
||||
_, err := w.Write([]byte("<html><title>post1 blah 123</title><body> 2222</body></html>"))
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
if r.URL.String() == "/post2" {
|
||||
w.Write([]byte("<html><title>post2 blah 123</title><body> 2222</body></html>"))
|
||||
_, err := w.Write([]byte("<html><title>post2 blah 123</title><body> 2222</body></html>"))
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(404)
|
||||
@@ -158,7 +163,7 @@ func TestService_SetTitle(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_Vote(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: -1}
|
||||
|
||||
comment := store.Comment{
|
||||
@@ -204,7 +209,7 @@ func TestService_Vote(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_VoteLimit(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: 2}
|
||||
|
||||
_, err := b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-1", "user2", true)
|
||||
@@ -222,7 +227,7 @@ func TestService_VoteLimit(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_VotesDisabled(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: 0}
|
||||
|
||||
_, err := b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-1", "user2", true)
|
||||
@@ -230,7 +235,7 @@ func TestService_VotesDisabled(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_VoteAggressive(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: -1}
|
||||
|
||||
comment := store.Comment{
|
||||
@@ -259,7 +264,6 @@ func TestService_VoteAggressive(t *testing.T) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _ = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true)
|
||||
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -291,7 +295,7 @@ func TestService_VoteAggressive(t *testing.T) {
|
||||
|
||||
func TestService_VoteConcurrent(t *testing.T) {
|
||||
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: -1}
|
||||
|
||||
comment := store.Comment{
|
||||
@@ -308,10 +312,11 @@ func TestService_VoteConcurrent(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
i := i
|
||||
ii := i
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, fmt.Sprintf("user1-%d", i), true)
|
||||
_, _ = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
|
||||
fmt.Sprintf("user1-%d", ii), true)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -323,7 +328,7 @@ func TestService_VoteConcurrent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_VotePositive(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"),
|
||||
MaxVotes: -1, PositiveScore: true}
|
||||
|
||||
@@ -342,7 +347,7 @@ func TestService_VotePositive(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_VoteControversy(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: -1}
|
||||
|
||||
c, err := b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-2", "user2", false)
|
||||
@@ -392,7 +397,7 @@ func TestService_Controversy(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_Pin(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123")}
|
||||
|
||||
res, err := b.Last("radio-t", 0)
|
||||
@@ -416,7 +421,7 @@ func TestService_Pin(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_EditComment(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123")}
|
||||
|
||||
res, err := b.Last("radio-t", 0)
|
||||
@@ -443,7 +448,7 @@ func TestService_EditComment(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_DeleteComment(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123")}
|
||||
|
||||
res, err := b.Last("radio-t", 0)
|
||||
@@ -462,7 +467,7 @@ func TestService_DeleteComment(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_EditCommentDurationFailed(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := DataStore{Interface: prepStoreEngine(t), EditDuration: 100 * time.Millisecond, AdminStore: admin.NewStaticKeyStore("secret 123")}
|
||||
|
||||
res, err := b.Last("radio-t", 0)
|
||||
@@ -479,7 +484,7 @@ func TestService_EditCommentDurationFailed(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_EditCommentReplyFailed(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123")}
|
||||
|
||||
res, err := b.Last("radio-t", 0)
|
||||
@@ -530,7 +535,7 @@ func TestService_ValidateComment(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_Counts(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
b := prepStoreEngine(t) // two comments for https://radio-t.com
|
||||
|
||||
// add one more for https://radio-t.com/2
|
||||
@@ -559,7 +564,7 @@ func TestService_Counts(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_GetMetas(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
// two comments for https://radio-t.com
|
||||
b := DataStore{Interface: prepStoreEngine(t), EditDuration: 100 * time.Millisecond,
|
||||
AdminStore: admin.NewStaticKeyStore("secret 123")}
|
||||
@@ -590,7 +595,7 @@ func TestService_GetMetas(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_SetMetas(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
// two comments for https://radio-t.com
|
||||
b := DataStore{Interface: prepStoreEngine(t), EditDuration: 100 * time.Millisecond,
|
||||
AdminStore: admin.NewStaticKeyStore("secret 123")}
|
||||
@@ -614,7 +619,7 @@ func TestService_SetMetas(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_IsAdmin(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
// two comments for https://radio-t.com
|
||||
b := DataStore{Interface: prepStoreEngine(t), EditDuration: 100 * time.Millisecond,
|
||||
AdminStore: admin.NewStaticStore("secret 123", []string{"user2"}, "user@email.com")}
|
||||
@@ -624,7 +629,7 @@ func TestService_IsAdmin(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_HasReplies(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
|
||||
// two comments for https://radio-t.com, no reply
|
||||
b := DataStore{Interface: prepStoreEngine(t), EditDuration: 100 * time.Millisecond,
|
||||
@@ -654,7 +659,7 @@ func TestService_HasReplies(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_Find(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
defer teardown(t)
|
||||
|
||||
// two comments for https://radio-t.com, no reply
|
||||
b := DataStore{Interface: prepStoreEngine(t), EditDuration: 100 * time.Millisecond,
|
||||
@@ -687,9 +692,38 @@ func TestService_Find(t *testing.T) {
|
||||
assert.InDelta(t, 0, res[1].Controversy, 0.01)
|
||||
}
|
||||
|
||||
func TestService_submitImages(t *testing.T) {
|
||||
defer teardown(t)
|
||||
lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockStore := image.NewMockStore(ctrl)
|
||||
imgSvc := &image.Service{Store: mockStore, TTL: time.Millisecond * 50}
|
||||
|
||||
mockStore.EXPECT().Commit(gomock.Any()).Times(2)
|
||||
|
||||
// two comments for https://radio-t.com
|
||||
b := DataStore{Interface: prepStoreEngine(t), EditDuration: 50 * time.Millisecond,
|
||||
AdminStore: admin.NewStaticKeyStore("secret 123"), ImageService: imgSvc}
|
||||
|
||||
c := store.Comment{
|
||||
ID: "id-22",
|
||||
Text: `some text <img src="/images/dev/pic1.png"/> xx <img src="/images/dev/pic2.png"/>`,
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
_, err := b.Interface.Create(c) // create directly with engine, doesn't call submitImages
|
||||
assert.NoError(t, err)
|
||||
|
||||
b.submitImages(c)
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
|
||||
// makes new boltdb, put two records
|
||||
func prepStoreEngine(t *testing.T) engine.Interface {
|
||||
os.Remove(testDb)
|
||||
_ = os.Remove(testDb)
|
||||
|
||||
boltStore, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: "/tmp/test-remark.db", SiteID: "radio-t"})
|
||||
assert.Nil(t, err)
|
||||
@@ -717,3 +751,7 @@ func prepStoreEngine(t *testing.T) engine.Interface {
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func teardown(_ *testing.T) {
|
||||
_ = os.Remove(testDb)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ func TestTitle_Get(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() == "/good" {
|
||||
atomic.AddInt32(&hits, 1)
|
||||
w.Write([]byte("<html><title>blah 123</title><body> 2222</body></html>"))
|
||||
_, err := w.Write([]byte("<html><title>blah 123</title><body> 2222</body></html>"))
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(404)
|
||||
@@ -58,9 +59,9 @@ func TestTitle_Get(t *testing.T) {
|
||||
require.NotNil(t, err)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
title, err := ex.Get(ts.URL + "/good")
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "blah 123", title)
|
||||
r, e := ex.Get(ts.URL + "/good")
|
||||
require.Nil(t, e)
|
||||
assert.Equal(t, "blah 123", r)
|
||||
}
|
||||
assert.Equal(t, int32(1), atomic.LoadInt32(&hits))
|
||||
}
|
||||
@@ -75,7 +76,8 @@ func TestTitle_GetConcurrent(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.String(), "/good") {
|
||||
atomic.AddInt32(&hits, 1)
|
||||
w.Write([]byte(fmt.Sprintf("<html><title>blah 123 %s</title><body>%s</body></html>", r.URL.String(), body)))
|
||||
_, err := w.Write([]byte(fmt.Sprintf("<html><title>blah 123 %s</title><body>%s</body></html>", r.URL.String(), body)))
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(404)
|
||||
@@ -84,11 +86,11 @@ func TestTitle_GetConcurrent(t *testing.T) {
|
||||
g := syncs.NewSizedGroup(10)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
i := i
|
||||
ii := i
|
||||
g.Go(func(_ context.Context) {
|
||||
title, err := ex.Get(ts.URL + "/good/" + strconv.Itoa(i))
|
||||
title, err := ex.Get(ts.URL + "/good/" + strconv.Itoa(ii))
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "blah 123 "+"/good/"+strconv.Itoa(i), title)
|
||||
assert.Equal(t, "blah 123 "+"/good/"+strconv.Itoa(ii), title)
|
||||
})
|
||||
}
|
||||
g.Wait()
|
||||
@@ -107,9 +109,9 @@ func TestTitle_GetFailed(t *testing.T) {
|
||||
require.NotNil(t, err)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
title, err := ex.Get(ts.URL + "/bad")
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "", title)
|
||||
r, e := ex.Get(ts.URL + "/bad")
|
||||
require.Nil(t, e)
|
||||
assert.Equal(t, "", r)
|
||||
}
|
||||
assert.Equal(t, int32(1), atomic.LoadInt32(&hits), "hit once, errors cached")
|
||||
}
|
||||
|
||||
+6
-6
@@ -10,30 +10,30 @@ require (
|
||||
github.com/didip/tollbooth v4.0.0+incompatible
|
||||
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
|
||||
github.com/go-chi/chi v3.3.2+incompatible
|
||||
github.com/go-chi/chi v4.0.2+incompatible
|
||||
github.com/go-chi/cors v1.0.0
|
||||
github.com/go-chi/render v1.0.0
|
||||
github.com/go-pkgz/auth v0.5.0
|
||||
github.com/go-pkgz/lcw v0.2.0
|
||||
github.com/go-pkgz/lgr v0.4.0
|
||||
github.com/go-pkgz/lgr v0.6.2
|
||||
github.com/go-pkgz/mongo v1.1.2
|
||||
github.com/go-pkgz/repeater v1.1.1
|
||||
github.com/go-pkgz/rest v1.4.0
|
||||
github.com/go-pkgz/syncs v1.1.0
|
||||
github.com/golang/mock v1.2.0
|
||||
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c
|
||||
github.com/gorilla/feeds v1.1.0
|
||||
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v0.0.0-20171204182908-b7773ae21874
|
||||
github.com/hashicorp/golang-lru v0.5.1 // indirect
|
||||
github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc
|
||||
github.com/microcosm-cc/bluemonday v0.0.0-20171222152607-542fd4642604
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/rakyll/statik v0.1.3
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.3.0
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16
|
||||
golang.org/x/net v0.0.0-20190107210223-45ffb0cd1ba0
|
||||
golang.org/x/time v0.0.0-20170927054726-6dc17368e09b // indirect
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
||||
gopkg.in/russross/blackfriday.v2 v2.0.0
|
||||
)
|
||||
|
||||
+17
-12
@@ -22,19 +22,23 @@ github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7a
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ=
|
||||
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
|
||||
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/cors v1.0.0 h1:e6x8k7uWbUwYs+aXDoiUzeQFT6l0cygBYyNhD7/1Tg0=
|
||||
github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
|
||||
github.com/go-chi/render v1.0.0 h1:cLJlkaTB4xfx5rWhtoB0BSXsXVJKWFqv08Y3cR1bZKA=
|
||||
github.com/go-chi/render v1.0.0/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/go-pkgz/auth v0.4.2 h1:WY3XzjUieUGxJSjXDU0rLrKt8RcPEaGdtLVRS7M56wU=
|
||||
github.com/go-pkgz/auth v0.4.2/go.mod h1:CWtB8dHmOv+TfF3MUzKwk/YwTLepC2TaDL05A+pFVBM=
|
||||
github.com/go-pkgz/auth v0.5.0 h1:+wqppq35x83PchZNZ7SHHYLI/e8WeETFouujDLsklac=
|
||||
github.com/go-pkgz/auth v0.5.0/go.mod h1:CWtB8dHmOv+TfF3MUzKwk/YwTLepC2TaDL05A+pFVBM=
|
||||
github.com/go-pkgz/lcw v0.2.0 h1:aFoKUG8q0YybId+ThVRQpDMjjuSG4hkLL1EA2xUtruc=
|
||||
github.com/go-pkgz/lcw v0.2.0/go.mod h1:k+PY1CkCMTLXILtFoJOyK65Qqi9rkoTYunFH1vE/C0I=
|
||||
github.com/go-pkgz/lgr v0.2.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
|
||||
github.com/go-pkgz/lgr v0.4.0 h1:s4490VXkaepbkMZBNZgr3rgfUg0G4nOLVa/Yp0hlwyc=
|
||||
github.com/go-pkgz/lgr v0.4.0/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
|
||||
github.com/go-pkgz/lgr v0.6.0 h1:Z9FRhfSyuASiF05iXRj+clTAAl8+1EfLR7SeyLIaGKg=
|
||||
github.com/go-pkgz/lgr v0.6.0/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
|
||||
github.com/go-pkgz/lgr v0.6.1 h1:poohUbv/iguoQ6bzJ5j/Ubl1VcsjU+rzTbbxsKbkGXk=
|
||||
github.com/go-pkgz/lgr v0.6.1/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
|
||||
github.com/go-pkgz/lgr v0.6.2 h1:Twf2YIe2J5tg7mKs+IkDDxrDF7GWlTCl/LzqELWjT5o=
|
||||
github.com/go-pkgz/lgr v0.6.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
|
||||
github.com/go-pkgz/mongo v1.0.0/go.mod h1:R9si/F2aJsjz4MUxhzuppIHY8yLV3YCeuCpgcI50cu4=
|
||||
github.com/go-pkgz/mongo v1.1.2 h1:2Vqn3CWQJkkx4gxxDiQUitAW2FN/CH26lKHkipmpKcc=
|
||||
github.com/go-pkgz/mongo v1.1.2/go.mod h1:0NkWnzpiUxoL5fYZuttCtJrpC67oNDidfYxcdPqHTf0=
|
||||
@@ -45,20 +49,20 @@ github.com/go-pkgz/rest v1.4.0 h1:xNkdMjEL2rNZSHouWjFTH22ncaZ77fopm34RN+eXAwk=
|
||||
github.com/go-pkgz/rest v1.4.0/go.mod h1:COazNj35u3RXAgQNBr6neR599tYP3URiOpsu9p0rOtk=
|
||||
github.com/go-pkgz/syncs v1.1.0 h1:k+dTyUZs1JHsYzo2tuUNrnW0OCwuGuS6ozfXHVspjSY=
|
||||
github.com/go-pkgz/syncs v1.1.0/go.mod h1:bt9lxWRRJ9vOCMGc8Big8ttjYHLKP88ofj1y38UlaHE=
|
||||
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c h1:jWtZjFEUE/Bz0IeIhqCnyZ3HG6KRXSntXe4SjtuTH7c=
|
||||
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/feeds v1.1.0 h1:pcgLJhbdYgaUESnj3AmXPcB7cS3vy63+jC/TI14AGXk=
|
||||
github.com/gorilla/feeds v1.1.0/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce h1:prjrVgOk2Yg6w+PflHoszQNLTUh4kaByUcEWM/9uin4=
|
||||
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v0.0.0-20171204182908-b7773ae21874 h1:em+tTnzgU7N22woTBMcSJAOW7tRHAkK597W+MD/CpK8=
|
||||
github.com/hashicorp/go-multierror v0.0.0-20171204182908-b7773ae21874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
|
||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc h1:0L2sGkaj6MWuV1BfXsrLJ/+XA8RzKKVsYlLVXNkK1Lw=
|
||||
github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
@@ -79,9 +83,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rakyll/statik v0.1.3 h1:H/5HK3yNM7sDzOiMQtC2Q1N69hl+KxzomBBWus662LU=
|
||||
github.com/rakyll/statik v0.1.3/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
@@ -101,8 +106,8 @@ golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb h1:1w588/yEchbPNpa9sEvOcMZYb
|
||||
golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.0.0-20170927054726-6dc17368e09b h1:3X+R0qq1+64izd8es+EttB6qcY+JDlVmAhpRXl7gpzU=
|
||||
golang.org/x/time v0.0.0-20170927054726-6dc17368e09b/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
|
||||
+9
-9
@@ -1,18 +1,18 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
|
||||
install:
|
||||
- go get -u golang.org/x/tools/cmd/goimports
|
||||
- go get -u github.com/golang/lint/golint
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
|
||||
script:
|
||||
- go get -d -t ./...
|
||||
- go vet ./...
|
||||
- golint ./...
|
||||
- go test ./...
|
||||
- >
|
||||
goimports -d -e ./ | grep '.*' && { echo; echo "Aborting due to non-empty goimports output."; exit 1; } || :
|
||||
go_version=$(go version);
|
||||
if [ ${go_version:13:4} = "1.12" ]; then
|
||||
go get -u golang.org/x/tools/cmd/goimports;
|
||||
goimports -d -e ./ | grep '.*' && { echo; echo "Aborting due to non-empty goimports output."; exit 1; } || :;
|
||||
fi
|
||||
|
||||
|
||||
+23
@@ -1,5 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## v4.0.0 (2019-01-10)
|
||||
|
||||
- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8
|
||||
- router: respond with 404 on router with no routes (#362)
|
||||
- router: additional check to ensure wildcard is at the end of a url pattern (#333)
|
||||
- middleware: deprecate use of http.CloseNotifier (#347)
|
||||
- middleware: fix RedirectSlashes to include query params on redirect (#334)
|
||||
- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0
|
||||
|
||||
|
||||
## v3.3.4 (2019-01-07)
|
||||
|
||||
- Minor middleware improvements. No changes to core library/router. Moving v3 into its
|
||||
- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11
|
||||
- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4
|
||||
|
||||
|
||||
## v3.3.3 (2018-08-27)
|
||||
|
||||
- Minor release
|
||||
- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3
|
||||
|
||||
|
||||
## v3.3.2 (2017-12-22)
|
||||
|
||||
- Support to route trailing slashes on mounted sub-routers (#281)
|
||||
|
||||
+27
-32
@@ -3,7 +3,7 @@
|
||||
|
||||
[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis]
|
||||
|
||||
`chi` is a lightweight, idiomatic and composable router for building Go 1.7+ HTTP services. It's
|
||||
`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's
|
||||
especially good at helping you write large REST API services that are kept maintainable as your
|
||||
project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to
|
||||
handle signaling, cancelation and request-scoped values across a handler chain.
|
||||
@@ -31,18 +31,12 @@ included some useful/optional subpackages: [middleware](/middleware), [render](h
|
||||
* **Context control** - built on new `context` package, providing value chaining, cancelations and timeouts
|
||||
* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91))
|
||||
* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown
|
||||
* **No external dependencies** - plain ol' Go 1.7+ stdlib + net/http
|
||||
* **No external dependencies** - plain ol' Go stdlib + net/http
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
* [rest](https://github.com/go-chi/chi/blob/master/_examples/rest/main.go) - REST APIs made easy, productive and maintainable
|
||||
* [logging](https://github.com/go-chi/chi/blob/master/_examples/logging/main.go) - Easy structured logging for any backend
|
||||
* [limits](https://github.com/go-chi/chi/blob/master/_examples/limits/main.go) - Timeouts and Throttling
|
||||
* [todos-resource](https://github.com/go-chi/chi/blob/master/_examples/todos-resource/main.go) - Struct routers/handlers, an example of another code layout style
|
||||
* [versions](https://github.com/go-chi/chi/blob/master/_examples/versions/main.go) - Demo of `chi/render` subpkg
|
||||
* [fileserver](https://github.com/go-chi/chi/blob/master/_examples/fileserver/main.go) - Easily serve static files
|
||||
* [graceful](https://github.com/go-chi/chi/blob/master/_examples/graceful/main.go) - Graceful context signaling and server shutdown
|
||||
See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples.
|
||||
|
||||
|
||||
**As easy as:**
|
||||
@@ -70,8 +64,8 @@ Here is a little preview of how routing looks like with chi. Also take a look at
|
||||
in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in
|
||||
Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)).
|
||||
|
||||
I highly recommend reading the source of the [examples](#examples) listed above, they will show you all the features
|
||||
of chi and serve as a good form of documentation.
|
||||
I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed
|
||||
above, they will show you all the features of chi and serve as a good form of documentation.
|
||||
|
||||
```go
|
||||
import (
|
||||
@@ -232,7 +226,7 @@ type Router interface {
|
||||
}
|
||||
|
||||
// Routes interface adds two methods for router traversal, which is also
|
||||
// used by the `docgen` subpackage to generation documentation for Routers.
|
||||
// used by the github.com/go-chi/docgen package to generate documentation for Routers.
|
||||
type Routes interface {
|
||||
// Routes returns the routing tree in an easily traversable structure.
|
||||
Routes() []Route
|
||||
@@ -261,7 +255,7 @@ friendly with any middleware in the community. This offers much better extensibi
|
||||
of packages and is at the heart of chi's purpose.
|
||||
|
||||
Here is an example of a standard net/http middleware handler using the new request context
|
||||
available in Go 1.7+. This middleware sets a hypothetical user identifier on the request
|
||||
available in Go. This middleware sets a hypothetical user identifier on the request
|
||||
context and calls the next handler in the chain.
|
||||
|
||||
```go
|
||||
@@ -347,6 +341,7 @@ Please see https://github.com/go-chi for additional packages.
|
||||
| package | description |
|
||||
|:---------------------------------------------------|:-------------------------------------------------------------
|
||||
| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) |
|
||||
| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime |
|
||||
| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication |
|
||||
| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing |
|
||||
| [httpcoala](https://github.com/go-chi/httpcoala) | HTTP request coalescer |
|
||||
@@ -374,33 +369,33 @@ and..
|
||||
|
||||
The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark
|
||||
|
||||
Results as of Aug 31, 2017 on Go 1.9.0
|
||||
Results as of Jan 9, 2019 with Go 1.11.4 on Linux X1 Carbon laptop
|
||||
|
||||
```shell
|
||||
BenchmarkChi_Param 3000000 607 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_Param5 2000000 935 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_Param20 1000000 1944 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_ParamWrite 2000000 664 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GithubStatic 2000000 627 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GithubParam 2000000 847 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GithubAll 10000 175556 ns/op 87700 B/op 609 allocs/op
|
||||
BenchmarkChi_GPlusStatic 3000000 566 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GPlusParam 2000000 652 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GPlus2Params 2000000 767 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GPlusAll 200000 9794 ns/op 5616 B/op 39 allocs/op
|
||||
BenchmarkChi_ParseStatic 3000000 590 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_ParseParam 2000000 656 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_Parse2Params 2000000 715 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_ParseAll 100000 18045 ns/op 11232 B/op 78 allocs/op
|
||||
BenchmarkChi_StaticAll 10000 108871 ns/op 67827 B/op 471 allocs/op
|
||||
BenchmarkChi_Param 3000000 475 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_Param5 2000000 696 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_Param20 1000000 1275 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_ParamWrite 3000000 505 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GithubStatic 3000000 508 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GithubParam 2000000 669 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GithubAll 10000 134627 ns/op 87699 B/op 609 allocs/op
|
||||
BenchmarkChi_GPlusStatic 3000000 402 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GPlusParam 3000000 500 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GPlus2Params 3000000 586 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GPlusAll 200000 7237 ns/op 5616 B/op 39 allocs/op
|
||||
BenchmarkChi_ParseStatic 3000000 408 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_ParseParam 3000000 488 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_Parse2Params 3000000 551 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_ParseAll 100000 13508 ns/op 11232 B/op 78 allocs/op
|
||||
BenchmarkChi_StaticAll 20000 81933 ns/op 67826 B/op 471 allocs/op
|
||||
```
|
||||
|
||||
Comparison with other routers: https://gist.github.com/pkieltyka/c089f309abeb179cfc4deaa519956d8c
|
||||
Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc
|
||||
|
||||
NOTE: the allocs in the benchmark above are from the calls to http.Request's
|
||||
`WithContext(context.Context)` method that clones the http.Request, sets the `Context()`
|
||||
on the duplicated (alloc'd) request and returns it the new request object. This is just
|
||||
how setting context on a request in Go 1.7+ works.
|
||||
how setting context on a request in Go works.
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
+7
-7
@@ -84,13 +84,13 @@ func (x *Context) URLParam(key string) string {
|
||||
//
|
||||
// For example,
|
||||
//
|
||||
// func Instrument(next http.Handler) http.Handler {
|
||||
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// next.ServeHTTP(w, r)
|
||||
// routePattern := chi.RouteContext(r.Context()).RoutePattern()
|
||||
// measure(w, r, routePattern)
|
||||
// })
|
||||
// }
|
||||
// func Instrument(next http.Handler) http.Handler {
|
||||
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// next.ServeHTTP(w, r)
|
||||
// routePattern := chi.RouteContext(r.Context()).RoutePattern()
|
||||
// measure(w, r, routePattern)
|
||||
// })
|
||||
// }
|
||||
func (x *Context) RoutePattern() string {
|
||||
routePattern := strings.Join(x.RoutePatterns, "")
|
||||
return strings.Replace(routePattern, "/*/", "/", -1)
|
||||
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
// +build go1.7,!go1.8
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CloseNotify is a middleware that cancels ctx when the underlying
|
||||
// connection has gone away. It can be used to cancel long operations
|
||||
// on the server when the client disconnects before the response is ready.
|
||||
//
|
||||
// Note: this behaviour is standard in Go 1.8+, so the middleware does nothing
|
||||
// on 1.8+ and exists just for backwards compatibility.
|
||||
func CloseNotify(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
cn, ok := w.(http.CloseNotifier)
|
||||
if !ok {
|
||||
panic("chi/middleware: CloseNotify expects http.ResponseWriter to implement http.CloseNotifier interface")
|
||||
}
|
||||
closeNotifyCh := cn.CloseNotify()
|
||||
|
||||
ctx, cancel := context.WithCancel(r.Context())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-closeNotifyCh:
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
// +build go1.8 appengine
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CloseNotify is a middleware that cancels ctx when the underlying
|
||||
// connection has gone away. It can be used to cancel long operations
|
||||
// on the server when the client disconnects before the response is ready.
|
||||
//
|
||||
// Note: this behaviour is standard in Go 1.8+, so the middleware does nothing
|
||||
// on 1.8+ and exists just for backwards compatibility.
|
||||
func CloseNotify(next http.Handler) http.Handler {
|
||||
return next
|
||||
}
|
||||
+167
-104
@@ -11,24 +11,98 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type encoding int
|
||||
var encoders = map[string]EncoderFunc{}
|
||||
|
||||
const (
|
||||
encodingNone encoding = iota
|
||||
encodingGzip
|
||||
encodingDeflate
|
||||
)
|
||||
var encodingPrecedence = []string{"br", "gzip", "deflate"}
|
||||
|
||||
func init() {
|
||||
// TODO:
|
||||
// lzma: Opera.
|
||||
// sdch: Chrome, Android. Gzip output + dictionary header.
|
||||
// br: Brotli, see https://github.com/go-chi/chi/pull/326
|
||||
|
||||
// TODO: Exception for old MSIE browsers that can't handle non-HTML?
|
||||
// https://zoompf.com/blog/2012/02/lose-the-wait-http-compression
|
||||
SetEncoder("gzip", encoderGzip)
|
||||
|
||||
// HTTP 1.1 "deflate" (RFC 2616) stands for DEFLATE data (RFC 1951)
|
||||
// wrapped with zlib (RFC 1950). The zlib wrapper uses Adler-32
|
||||
// checksum compared to CRC-32 used in "gzip" and thus is faster.
|
||||
//
|
||||
// But.. some old browsers (MSIE, Safari 5.1) incorrectly expect
|
||||
// raw DEFLATE data only, without the mentioned zlib wrapper.
|
||||
// Because of this major confusion, most modern browsers try it
|
||||
// both ways, first looking for zlib headers.
|
||||
// Quote by Mark Adler: http://stackoverflow.com/a/9186091/385548
|
||||
//
|
||||
// The list of browsers having problems is quite big, see:
|
||||
// http://zoompf.com/blog/2012/02/lose-the-wait-http-compression
|
||||
// https://web.archive.org/web/20120321182910/http://www.vervestudios.co/projects/compression-tests/results
|
||||
//
|
||||
// That's why we prefer gzip over deflate. It's just more reliable
|
||||
// and not significantly slower than gzip.
|
||||
SetEncoder("deflate", encoderDeflate)
|
||||
|
||||
// NOTE: Not implemented, intentionally:
|
||||
// case "compress": // LZW. Deprecated.
|
||||
// case "bzip2": // Too slow on-the-fly.
|
||||
// case "zopfli": // Too slow on-the-fly.
|
||||
// case "xz": // Too slow on-the-fly.
|
||||
}
|
||||
|
||||
// An EncoderFunc is a function that wraps the provided ResponseWriter with a
|
||||
// streaming compression algorithm and returns it.
|
||||
//
|
||||
// In case of failure, the function should return nil.
|
||||
type EncoderFunc func(w http.ResponseWriter, level int) io.Writer
|
||||
|
||||
// SetEncoder can be used to set the implementation of a compression algorithm.
|
||||
//
|
||||
// The encoding should be a standardised identifier. See:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
|
||||
//
|
||||
// For example, add the Brotli algortithm:
|
||||
//
|
||||
// import brotli_enc "gopkg.in/kothar/brotli-go.v0/enc"
|
||||
//
|
||||
// middleware.SetEncoder("br", func(w http.ResponseWriter, level int) io.Writer {
|
||||
// params := brotli_enc.NewBrotliParams()
|
||||
// params.SetQuality(level)
|
||||
// return brotli_enc.NewBrotliWriter(params, w)
|
||||
// })
|
||||
func SetEncoder(encoding string, fn EncoderFunc) {
|
||||
encoding = strings.ToLower(encoding)
|
||||
if encoding == "" {
|
||||
panic("the encoding can not be empty")
|
||||
}
|
||||
if fn == nil {
|
||||
panic("attempted to set a nil encoder function")
|
||||
}
|
||||
encoders[encoding] = fn
|
||||
|
||||
var e string
|
||||
for _, v := range encodingPrecedence {
|
||||
if v == encoding {
|
||||
e = v
|
||||
}
|
||||
}
|
||||
|
||||
if e == "" {
|
||||
encodingPrecedence = append([]string{e}, encodingPrecedence...)
|
||||
}
|
||||
}
|
||||
|
||||
var defaultContentTypes = map[string]struct{}{
|
||||
"text/html": struct{}{},
|
||||
"text/css": struct{}{},
|
||||
"text/plain": struct{}{},
|
||||
"text/javascript": struct{}{},
|
||||
"application/javascript": struct{}{},
|
||||
"application/x-javascript": struct{}{},
|
||||
"application/json": struct{}{},
|
||||
"application/atom+xml": struct{}{},
|
||||
"application/rss+xml": struct{}{},
|
||||
"text/html": {},
|
||||
"text/css": {},
|
||||
"text/plain": {},
|
||||
"text/javascript": {},
|
||||
"application/javascript": {},
|
||||
"application/x-javascript": {},
|
||||
"application/json": {},
|
||||
"application/atom+xml": {},
|
||||
"application/rss+xml": {},
|
||||
"image/svg+xml": {},
|
||||
}
|
||||
|
||||
// DefaultCompress is a middleware that compresses response
|
||||
@@ -43,6 +117,11 @@ func DefaultCompress(next http.Handler) http.Handler {
|
||||
// body of a given content types to a data format based
|
||||
// on Accept-Encoding request header. It uses a given
|
||||
// compression level.
|
||||
//
|
||||
// NOTE: make sure to set the Content-Type header on your response
|
||||
// otherwise this middleware will not compress the response body. For ex, in
|
||||
// your handler you should set w.Header().Set("Content-Type", http.DetectContentType(yourBody))
|
||||
// or set it manually.
|
||||
func Compress(level int, types ...string) func(next http.Handler) http.Handler {
|
||||
contentTypes := defaultContentTypes
|
||||
if len(types) > 0 {
|
||||
@@ -54,159 +133,143 @@ func Compress(level int, types ...string) func(next http.Handler) http.Handler {
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
mcw := &maybeCompressResponseWriter{
|
||||
encoder, encoding := selectEncoder(r.Header)
|
||||
|
||||
cw := &compressResponseWriter{
|
||||
ResponseWriter: w,
|
||||
w: w,
|
||||
contentTypes: contentTypes,
|
||||
encoding: selectEncoding(r.Header),
|
||||
encoder: encoder,
|
||||
encoding: encoding,
|
||||
level: level,
|
||||
}
|
||||
defer mcw.Close()
|
||||
defer cw.Close()
|
||||
|
||||
next.ServeHTTP(mcw, r)
|
||||
next.ServeHTTP(cw, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func selectEncoding(h http.Header) encoding {
|
||||
enc := h.Get("Accept-Encoding")
|
||||
func selectEncoder(h http.Header) (EncoderFunc, string) {
|
||||
header := h.Get("Accept-Encoding")
|
||||
|
||||
switch {
|
||||
// TODO:
|
||||
// case "br": // Brotli, experimental. Firefox 2016, to-be-in Chromium.
|
||||
// case "lzma": // Opera.
|
||||
// case "sdch": // Chrome, Android. Gzip output + dictionary header.
|
||||
// Parse the names of all accepted algorithms from the header.
|
||||
accepted := strings.Split(strings.ToLower(header), ",")
|
||||
|
||||
case strings.Contains(enc, "gzip"):
|
||||
// TODO: Exception for old MSIE browsers that can't handle non-HTML?
|
||||
// https://zoompf.com/blog/2012/02/lose-the-wait-http-compression
|
||||
return encodingGzip
|
||||
|
||||
case strings.Contains(enc, "deflate"):
|
||||
// HTTP 1.1 "deflate" (RFC 2616) stands for DEFLATE data (RFC 1951)
|
||||
// wrapped with zlib (RFC 1950). The zlib wrapper uses Adler-32
|
||||
// checksum compared to CRC-32 used in "gzip" and thus is faster.
|
||||
//
|
||||
// But.. some old browsers (MSIE, Safari 5.1) incorrectly expect
|
||||
// raw DEFLATE data only, without the mentioned zlib wrapper.
|
||||
// Because of this major confusion, most modern browsers try it
|
||||
// both ways, first looking for zlib headers.
|
||||
// Quote by Mark Adler: http://stackoverflow.com/a/9186091/385548
|
||||
//
|
||||
// The list of browsers having problems is quite big, see:
|
||||
// http://zoompf.com/blog/2012/02/lose-the-wait-http-compression
|
||||
// https://web.archive.org/web/20120321182910/http://www.vervestudios.co/projects/compression-tests/results
|
||||
//
|
||||
// That's why we prefer gzip over deflate. It's just more reliable
|
||||
// and not significantly slower than gzip.
|
||||
return encodingDeflate
|
||||
|
||||
// NOTE: Not implemented, intentionally:
|
||||
// case "compress": // LZW. Deprecated.
|
||||
// case "bzip2": // Too slow on-the-fly.
|
||||
// case "zopfli": // Too slow on-the-fly.
|
||||
// case "xz": // Too slow on-the-fly.
|
||||
// Find supported encoder by accepted list by precedence
|
||||
for _, name := range encodingPrecedence {
|
||||
if fn, ok := encoders[name]; ok && matchAcceptEncoding(accepted, name) {
|
||||
return fn, name
|
||||
}
|
||||
}
|
||||
|
||||
return encodingNone
|
||||
// No encoder found to match the accepted encoding
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
type maybeCompressResponseWriter struct {
|
||||
func matchAcceptEncoding(accepted []string, encoding string) bool {
|
||||
for _, v := range accepted {
|
||||
if strings.Index(v, encoding) >= 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type compressResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
w io.Writer
|
||||
encoding encoding
|
||||
encoder EncoderFunc
|
||||
encoding string
|
||||
contentTypes map[string]struct{}
|
||||
level int
|
||||
wroteHeader bool
|
||||
}
|
||||
|
||||
func (w *maybeCompressResponseWriter) WriteHeader(code int) {
|
||||
if w.wroteHeader {
|
||||
func (cw *compressResponseWriter) WriteHeader(code int) {
|
||||
if cw.wroteHeader {
|
||||
return
|
||||
}
|
||||
w.wroteHeader = true
|
||||
defer w.ResponseWriter.WriteHeader(code)
|
||||
cw.wroteHeader = true
|
||||
defer cw.ResponseWriter.WriteHeader(code)
|
||||
|
||||
// Already compressed data?
|
||||
if w.ResponseWriter.Header().Get("Content-Encoding") != "" {
|
||||
if cw.Header().Get("Content-Encoding") != "" {
|
||||
return
|
||||
}
|
||||
// The content-length after compression is unknown
|
||||
w.ResponseWriter.Header().Del("Content-Length")
|
||||
|
||||
// Parse the first part of the Content-Type response header.
|
||||
contentType := ""
|
||||
parts := strings.Split(w.ResponseWriter.Header().Get("Content-Type"), ";")
|
||||
parts := strings.Split(cw.Header().Get("Content-Type"), ";")
|
||||
if len(parts) > 0 {
|
||||
contentType = parts[0]
|
||||
}
|
||||
|
||||
// Is the content type compressable?
|
||||
if _, ok := w.contentTypes[contentType]; !ok {
|
||||
if _, ok := cw.contentTypes[contentType]; !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Select the compress writer.
|
||||
switch w.encoding {
|
||||
case encodingGzip:
|
||||
gw, err := gzip.NewWriterLevel(w.ResponseWriter, w.level)
|
||||
if err != nil {
|
||||
w.w = w.ResponseWriter
|
||||
return
|
||||
}
|
||||
w.w = gw
|
||||
w.ResponseWriter.Header().Set("Content-Encoding", "gzip")
|
||||
if cw.encoder != nil && cw.encoding != "" {
|
||||
if wr := cw.encoder(cw.ResponseWriter, cw.level); wr != nil {
|
||||
cw.w = wr
|
||||
cw.Header().Set("Content-Encoding", cw.encoding)
|
||||
|
||||
case encodingDeflate:
|
||||
dw, err := flate.NewWriter(w.ResponseWriter, w.level)
|
||||
if err != nil {
|
||||
w.w = w.ResponseWriter
|
||||
return
|
||||
// The content-length after compression is unknown
|
||||
cw.Header().Del("Content-Length")
|
||||
}
|
||||
w.w = dw
|
||||
w.ResponseWriter.Header().Set("Content-Encoding", "deflate")
|
||||
}
|
||||
}
|
||||
|
||||
func (w *maybeCompressResponseWriter) Write(p []byte) (int, error) {
|
||||
if !w.wroteHeader {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
func (cw *compressResponseWriter) Write(p []byte) (int, error) {
|
||||
if !cw.wroteHeader {
|
||||
cw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
return w.w.Write(p)
|
||||
return cw.w.Write(p)
|
||||
}
|
||||
|
||||
func (w *maybeCompressResponseWriter) Flush() {
|
||||
if f, ok := w.w.(http.Flusher); ok {
|
||||
func (cw *compressResponseWriter) Flush() {
|
||||
if f, ok := cw.w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *maybeCompressResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := w.w.(http.Hijacker); ok {
|
||||
func (cw *compressResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := cw.w.(http.Hijacker); ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
return nil, nil, errors.New("chi/middleware: http.Hijacker is unavailable on the writer")
|
||||
}
|
||||
|
||||
func (w *maybeCompressResponseWriter) CloseNotify() <-chan bool {
|
||||
if cn, ok := w.w.(http.CloseNotifier); ok {
|
||||
return cn.CloseNotify()
|
||||
func (cw *compressResponseWriter) Push(target string, opts *http.PushOptions) error {
|
||||
if ps, ok := cw.w.(http.Pusher); ok {
|
||||
return ps.Push(target, opts)
|
||||
}
|
||||
|
||||
// If the underlying writer does not implement http.CloseNotifier, return
|
||||
// a channel that never receives a value. The semantics here is that the
|
||||
// client never disconnnects before the request is processed by the
|
||||
// http.Handler, which is close enough to the default behavior (when
|
||||
// CloseNotify() is not even called).
|
||||
return make(chan bool, 1)
|
||||
return errors.New("chi/middleware: http.Pusher is unavailable on the writer")
|
||||
}
|
||||
|
||||
func (w *maybeCompressResponseWriter) Close() error {
|
||||
if c, ok := w.w.(io.WriteCloser); ok {
|
||||
func (cw *compressResponseWriter) Close() error {
|
||||
if c, ok := cw.w.(io.WriteCloser); ok {
|
||||
return c.Close()
|
||||
}
|
||||
return errors.New("chi/middleware: io.WriteCloser is unavailable on the writer")
|
||||
}
|
||||
|
||||
func encoderGzip(w http.ResponseWriter, level int) io.Writer {
|
||||
gw, err := gzip.NewWriterLevel(w, level)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return gw
|
||||
}
|
||||
|
||||
func encoderDeflate(w http.ResponseWriter, level int) io.Writer {
|
||||
dw, err := flate.NewWriter(w, level)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return dw
|
||||
}
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
// +build go1.8 appengine
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (w *maybeCompressResponseWriter) Push(target string, opts *http.PushOptions) error {
|
||||
if ps, ok := w.w.(http.Pusher); ok {
|
||||
return ps.Push(target, opts)
|
||||
}
|
||||
return errors.New("chi/middleware: http.Pusher is unavailable on the writer")
|
||||
}
|
||||
+6
@@ -26,6 +26,12 @@ func AllowContentType(contentTypes ...string) func(next http.Handler) http.Handl
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.ContentLength == 0 {
|
||||
// skip check for empty content body
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
s := strings.ToLower(strings.TrimSpace(r.Header.Get("Content-Type")))
|
||||
if i := strings.Index(s, ";"); i > -1 {
|
||||
s = s[0:i]
|
||||
|
||||
+22
-18
@@ -16,7 +16,7 @@ var (
|
||||
// DefaultLogger is called by the Logger middleware handler to log each request.
|
||||
// Its made a package-level variable so that it can be reconfigured for custom
|
||||
// logging configurations.
|
||||
DefaultLogger = RequestLogger(&DefaultLogFormatter{Logger: log.New(os.Stdout, "", log.LstdFlags)})
|
||||
DefaultLogger = RequestLogger(&DefaultLogFormatter{Logger: log.New(os.Stdout, "", log.LstdFlags), NoColor: false})
|
||||
)
|
||||
|
||||
// Logger is a middleware that logs the start and end of each request, along
|
||||
@@ -81,29 +81,32 @@ type LoggerInterface interface {
|
||||
|
||||
// DefaultLogFormatter is a simple logger that implements a LogFormatter.
|
||||
type DefaultLogFormatter struct {
|
||||
Logger LoggerInterface
|
||||
Logger LoggerInterface
|
||||
NoColor bool
|
||||
}
|
||||
|
||||
// NewLogEntry creates a new LogEntry for the request.
|
||||
func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry {
|
||||
useColor := !l.NoColor
|
||||
entry := &defaultLogEntry{
|
||||
DefaultLogFormatter: l,
|
||||
request: r,
|
||||
buf: &bytes.Buffer{},
|
||||
useColor: useColor,
|
||||
}
|
||||
|
||||
reqID := GetReqID(r.Context())
|
||||
if reqID != "" {
|
||||
cW(entry.buf, nYellow, "[%s] ", reqID)
|
||||
cW(entry.buf, useColor, nYellow, "[%s] ", reqID)
|
||||
}
|
||||
cW(entry.buf, nCyan, "\"")
|
||||
cW(entry.buf, bMagenta, "%s ", r.Method)
|
||||
cW(entry.buf, useColor, nCyan, "\"")
|
||||
cW(entry.buf, useColor, bMagenta, "%s ", r.Method)
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
cW(entry.buf, nCyan, "%s://%s%s %s\" ", scheme, r.Host, r.RequestURI, r.Proto)
|
||||
cW(entry.buf, useColor, nCyan, "%s://%s%s %s\" ", scheme, r.Host, r.RequestURI, r.Proto)
|
||||
|
||||
entry.buf.WriteString("from ")
|
||||
entry.buf.WriteString(r.RemoteAddr)
|
||||
@@ -114,33 +117,34 @@ func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry {
|
||||
|
||||
type defaultLogEntry struct {
|
||||
*DefaultLogFormatter
|
||||
request *http.Request
|
||||
buf *bytes.Buffer
|
||||
request *http.Request
|
||||
buf *bytes.Buffer
|
||||
useColor bool
|
||||
}
|
||||
|
||||
func (l *defaultLogEntry) Write(status, bytes int, elapsed time.Duration) {
|
||||
switch {
|
||||
case status < 200:
|
||||
cW(l.buf, bBlue, "%03d", status)
|
||||
cW(l.buf, l.useColor, bBlue, "%03d", status)
|
||||
case status < 300:
|
||||
cW(l.buf, bGreen, "%03d", status)
|
||||
cW(l.buf, l.useColor, bGreen, "%03d", status)
|
||||
case status < 400:
|
||||
cW(l.buf, bCyan, "%03d", status)
|
||||
cW(l.buf, l.useColor, bCyan, "%03d", status)
|
||||
case status < 500:
|
||||
cW(l.buf, bYellow, "%03d", status)
|
||||
cW(l.buf, l.useColor, bYellow, "%03d", status)
|
||||
default:
|
||||
cW(l.buf, bRed, "%03d", status)
|
||||
cW(l.buf, l.useColor, bRed, "%03d", status)
|
||||
}
|
||||
|
||||
cW(l.buf, bBlue, " %dB", bytes)
|
||||
cW(l.buf, l.useColor, bBlue, " %dB", bytes)
|
||||
|
||||
l.buf.WriteString(" in ")
|
||||
if elapsed < 500*time.Millisecond {
|
||||
cW(l.buf, nGreen, "%s", elapsed)
|
||||
cW(l.buf, l.useColor, nGreen, "%s", elapsed)
|
||||
} else if elapsed < 5*time.Second {
|
||||
cW(l.buf, nYellow, "%s", elapsed)
|
||||
cW(l.buf, l.useColor, nYellow, "%s", elapsed)
|
||||
} else {
|
||||
cW(l.buf, nRed, "%s", elapsed)
|
||||
cW(l.buf, l.useColor, nRed, "%s", elapsed)
|
||||
}
|
||||
|
||||
l.Logger.Print(l.buf.String())
|
||||
@@ -148,7 +152,7 @@ func (l *defaultLogEntry) Write(status, bytes int, elapsed time.Duration) {
|
||||
|
||||
func (l *defaultLogEntry) Panic(v interface{}, stack []byte) {
|
||||
panicEntry := l.NewLogEntry(l.request).(*defaultLogEntry)
|
||||
cW(panicEntry.buf, bRed, "panic: %+v", v)
|
||||
cW(panicEntry.buf, l.useColor, bRed, "panic: %+v", v)
|
||||
l.Logger.Print(panicEntry.buf.String())
|
||||
l.Logger.Print(string(stack))
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ var epoch = time.Unix(0, 0).Format(time.RFC1123)
|
||||
// Taken from https://github.com/mytrile/nocache
|
||||
var noCacheHeaders = map[string]string{
|
||||
"Expires": epoch,
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate, private, max-age=0",
|
||||
"Cache-Control": "no-cache, no-store, no-transform, must-revalidate, private, max-age=0",
|
||||
"Pragma": "no-cache",
|
||||
"X-Accel-Expires": "0",
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ var xRealIP = http.CanonicalHeaderKey("X-Real-IP")
|
||||
// You should only use this middleware if you can trust the headers passed to
|
||||
// you (in particular, the two headers this middleware uses), for example
|
||||
// because you have placed a reverse proxy like HAProxy or nginx in front of
|
||||
// Goji. If your reverse proxies are configured to pass along arbitrary header
|
||||
// chi. If your reverse proxies are configured to pass along arbitrary header
|
||||
// values from the client, or if you use this middleware without a reverse
|
||||
// proxy, malicious clients will be able to make you very sad (or, depending on
|
||||
// how you're using RemoteAddr, vulnerable to an attack of some sort).
|
||||
|
||||
+7
-3
@@ -17,7 +17,7 @@ import (
|
||||
// Key to use when setting the request ID.
|
||||
type ctxKeyRequestID int
|
||||
|
||||
// RequestIDKey is the key that holds th unique request ID in a request context.
|
||||
// RequestIDKey is the key that holds the unique request ID in a request context.
|
||||
const RequestIDKey ctxKeyRequestID = 0
|
||||
|
||||
var prefix string
|
||||
@@ -62,9 +62,13 @@ func init() {
|
||||
// counter.
|
||||
func RequestID(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
myid := atomic.AddUint64(&reqid, 1)
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, RequestIDKey, fmt.Sprintf("%s-%06d", prefix, myid))
|
||||
requestID := r.Header.Get("X-Request-Id")
|
||||
if requestID == "" {
|
||||
myid := atomic.AddUint64(&reqid, 1)
|
||||
requestID = fmt.Sprintf("%s-%06d", prefix, myid)
|
||||
}
|
||||
ctx = context.WithValue(ctx, RequestIDKey, requestID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
|
||||
+9
-1
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
@@ -28,6 +29,9 @@ func StripSlashes(next http.Handler) http.Handler {
|
||||
|
||||
// RedirectSlashes is a middleware that will match request paths with a trailing
|
||||
// slash and redirect to the same path, less the trailing slash.
|
||||
//
|
||||
// NOTE: RedirectSlashes middleware is *incompatible* with http.FileServer,
|
||||
// see https://github.com/go-chi/chi/issues/343
|
||||
func RedirectSlashes(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
var path string
|
||||
@@ -38,7 +42,11 @@ func RedirectSlashes(next http.Handler) http.Handler {
|
||||
path = r.URL.Path
|
||||
}
|
||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||
path = path[:len(path)-1]
|
||||
if r.URL.RawQuery != "" {
|
||||
path = fmt.Sprintf("%s?%s", path[:len(path)-1], r.URL.RawQuery)
|
||||
} else {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
http.Redirect(w, r, path, 301)
|
||||
return
|
||||
}
|
||||
|
||||
+3
-3
@@ -52,12 +52,12 @@ func init() {
|
||||
}
|
||||
|
||||
// colorWrite
|
||||
func cW(w io.Writer, color []byte, s string, args ...interface{}) {
|
||||
if isTTY {
|
||||
func cW(w io.Writer, useColor bool, color []byte, s string, args ...interface{}) {
|
||||
if isTTY && useColor {
|
||||
w.Write(color)
|
||||
}
|
||||
fmt.Fprintf(w, s, args...)
|
||||
if isTTY {
|
||||
if isTTY && useColor {
|
||||
w.Write(reset)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@ import (
|
||||
//
|
||||
// ie. a route/handler may look like:
|
||||
//
|
||||
// r.Get("/long", func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
// r.Get("/long", func(w http.ResponseWriter, r *http.Request) {
|
||||
// ctx := r.Context()
|
||||
// processTime := time.Duration(rand.Intn(4)+1) * time.Second
|
||||
//
|
||||
// select {
|
||||
|
||||
+47
-12
@@ -10,6 +10,32 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// NewWrapResponseWriter wraps an http.ResponseWriter, returning a proxy that allows you to
|
||||
// hook into various parts of the response process.
|
||||
func NewWrapResponseWriter(w http.ResponseWriter, protoMajor int) WrapResponseWriter {
|
||||
_, fl := w.(http.Flusher)
|
||||
|
||||
bw := basicWriter{ResponseWriter: w}
|
||||
|
||||
if protoMajor == 2 {
|
||||
_, ps := w.(http.Pusher)
|
||||
if fl && ps {
|
||||
return &http2FancyWriter{bw}
|
||||
}
|
||||
} else {
|
||||
_, hj := w.(http.Hijacker)
|
||||
_, rf := w.(io.ReaderFrom)
|
||||
if fl && hj && rf {
|
||||
return &httpFancyWriter{bw}
|
||||
}
|
||||
}
|
||||
if fl {
|
||||
return &flushWriter{bw}
|
||||
}
|
||||
|
||||
return &bw
|
||||
}
|
||||
|
||||
// WrapResponseWriter is a proxy around an http.ResponseWriter that allows you to hook
|
||||
// into various parts of the response process.
|
||||
type WrapResponseWriter interface {
|
||||
@@ -47,6 +73,7 @@ func (b *basicWriter) WriteHeader(code int) {
|
||||
b.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *basicWriter) Write(buf []byte) (int, error) {
|
||||
b.WriteHeader(http.StatusOK)
|
||||
n, err := b.ResponseWriter.Write(buf)
|
||||
@@ -60,20 +87,25 @@ func (b *basicWriter) Write(buf []byte) (int, error) {
|
||||
b.bytes += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (b *basicWriter) maybeWriteHeader() {
|
||||
if !b.wroteHeader {
|
||||
b.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *basicWriter) Status() int {
|
||||
return b.code
|
||||
}
|
||||
|
||||
func (b *basicWriter) BytesWritten() int {
|
||||
return b.bytes
|
||||
}
|
||||
|
||||
func (b *basicWriter) Tee(w io.Writer) {
|
||||
b.tee = w
|
||||
}
|
||||
|
||||
func (b *basicWriter) Unwrap() http.ResponseWriter {
|
||||
return b.ResponseWriter
|
||||
}
|
||||
@@ -83,13 +115,15 @@ type flushWriter struct {
|
||||
}
|
||||
|
||||
func (f *flushWriter) Flush() {
|
||||
f.wroteHeader = true
|
||||
|
||||
fl := f.basicWriter.ResponseWriter.(http.Flusher)
|
||||
fl.Flush()
|
||||
}
|
||||
|
||||
var _ http.Flusher = &flushWriter{}
|
||||
|
||||
// httpFancyWriter is a HTTP writer that additionally satisfies http.CloseNotifier,
|
||||
// httpFancyWriter is a HTTP writer that additionally satisfies
|
||||
// http.Flusher, http.Hijacker, and io.ReaderFrom. It exists for the common case
|
||||
// of wrapping the http.ResponseWriter that package http gives you, in order to
|
||||
// make the proxied object support the full method set of the proxied object.
|
||||
@@ -97,18 +131,22 @@ type httpFancyWriter struct {
|
||||
basicWriter
|
||||
}
|
||||
|
||||
func (f *httpFancyWriter) CloseNotify() <-chan bool {
|
||||
cn := f.basicWriter.ResponseWriter.(http.CloseNotifier)
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
func (f *httpFancyWriter) Flush() {
|
||||
f.wroteHeader = true
|
||||
|
||||
fl := f.basicWriter.ResponseWriter.(http.Flusher)
|
||||
fl.Flush()
|
||||
}
|
||||
|
||||
func (f *httpFancyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
hj := f.basicWriter.ResponseWriter.(http.Hijacker)
|
||||
return hj.Hijack()
|
||||
}
|
||||
|
||||
func (f *http2FancyWriter) Push(target string, opts *http.PushOptions) error {
|
||||
return f.basicWriter.ResponseWriter.(http.Pusher).Push(target, opts)
|
||||
}
|
||||
|
||||
func (f *httpFancyWriter) ReadFrom(r io.Reader) (int64, error) {
|
||||
if f.basicWriter.tee != nil {
|
||||
n, err := io.Copy(&f.basicWriter, r)
|
||||
@@ -122,12 +160,12 @@ func (f *httpFancyWriter) ReadFrom(r io.Reader) (int64, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
var _ http.CloseNotifier = &httpFancyWriter{}
|
||||
var _ http.Flusher = &httpFancyWriter{}
|
||||
var _ http.Hijacker = &httpFancyWriter{}
|
||||
var _ http.Pusher = &http2FancyWriter{}
|
||||
var _ io.ReaderFrom = &httpFancyWriter{}
|
||||
|
||||
// http2FancyWriter is a HTTP2 writer that additionally satisfies http.CloseNotifier,
|
||||
// http2FancyWriter is a HTTP2 writer that additionally satisfies
|
||||
// http.Flusher, and io.ReaderFrom. It exists for the common case
|
||||
// of wrapping the http.ResponseWriter that package http gives you, in order to
|
||||
// make the proxied object support the full method set of the proxied object.
|
||||
@@ -135,14 +173,11 @@ type http2FancyWriter struct {
|
||||
basicWriter
|
||||
}
|
||||
|
||||
func (f *http2FancyWriter) CloseNotify() <-chan bool {
|
||||
cn := f.basicWriter.ResponseWriter.(http.CloseNotifier)
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
func (f *http2FancyWriter) Flush() {
|
||||
f.wroteHeader = true
|
||||
|
||||
fl := f.basicWriter.ResponseWriter.(http.Flusher)
|
||||
fl.Flush()
|
||||
}
|
||||
|
||||
var _ http.CloseNotifier = &http2FancyWriter{}
|
||||
var _ http.Flusher = &http2FancyWriter{}
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
// +build go1.7,!go1.8
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// NewWrapResponseWriter wraps an http.ResponseWriter, returning a proxy that allows you to
|
||||
// hook into various parts of the response process.
|
||||
func NewWrapResponseWriter(w http.ResponseWriter, protoMajor int) WrapResponseWriter {
|
||||
_, cn := w.(http.CloseNotifier)
|
||||
_, fl := w.(http.Flusher)
|
||||
|
||||
bw := basicWriter{ResponseWriter: w}
|
||||
|
||||
if protoMajor == 2 {
|
||||
if cn && fl {
|
||||
return &http2FancyWriter{bw}
|
||||
}
|
||||
} else {
|
||||
_, hj := w.(http.Hijacker)
|
||||
_, rf := w.(io.ReaderFrom)
|
||||
if cn && fl && hj && rf {
|
||||
return &httpFancyWriter{bw}
|
||||
}
|
||||
}
|
||||
if fl {
|
||||
return &flushWriter{bw}
|
||||
}
|
||||
|
||||
return &bw
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
// +build go1.8 appengine
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// NewWrapResponseWriter wraps an http.ResponseWriter, returning a proxy that allows you to
|
||||
// hook into various parts of the response process.
|
||||
func NewWrapResponseWriter(w http.ResponseWriter, protoMajor int) WrapResponseWriter {
|
||||
_, cn := w.(http.CloseNotifier)
|
||||
_, fl := w.(http.Flusher)
|
||||
|
||||
bw := basicWriter{ResponseWriter: w}
|
||||
|
||||
if protoMajor == 2 {
|
||||
_, ps := w.(http.Pusher)
|
||||
if cn && fl && ps {
|
||||
return &http2FancyWriter{bw}
|
||||
}
|
||||
} else {
|
||||
_, hj := w.(http.Hijacker)
|
||||
_, rf := w.(io.ReaderFrom)
|
||||
if cn && fl && hj && rf {
|
||||
return &httpFancyWriter{bw}
|
||||
}
|
||||
}
|
||||
if fl {
|
||||
return &flushWriter{bw}
|
||||
}
|
||||
|
||||
return &bw
|
||||
}
|
||||
|
||||
func (f *http2FancyWriter) Push(target string, opts *http.PushOptions) error {
|
||||
return f.basicWriter.ResponseWriter.(http.Pusher).Push(target, opts)
|
||||
}
|
||||
|
||||
var _ http.Pusher = &http2FancyWriter{}
|
||||
+2
-1
@@ -60,7 +60,8 @@ func NewMux() *Mux {
|
||||
func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Ensure the mux has some routes defined on the mux
|
||||
if mx.handler == nil {
|
||||
panic("chi: attempting to route to a mux with no handlers.")
|
||||
mx.NotFoundHandler().ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if a routing context already exists from a parent router.
|
||||
|
||||
+12
-10
@@ -33,15 +33,15 @@ var mALL = mCONNECT | mDELETE | mGET | mHEAD |
|
||||
mOPTIONS | mPATCH | mPOST | mPUT | mTRACE
|
||||
|
||||
var methodMap = map[string]methodTyp{
|
||||
"CONNECT": mCONNECT,
|
||||
"DELETE": mDELETE,
|
||||
"GET": mGET,
|
||||
"HEAD": mHEAD,
|
||||
"OPTIONS": mOPTIONS,
|
||||
"PATCH": mPATCH,
|
||||
"POST": mPOST,
|
||||
"PUT": mPUT,
|
||||
"TRACE": mTRACE,
|
||||
http.MethodConnect: mCONNECT,
|
||||
http.MethodDelete: mDELETE,
|
||||
http.MethodGet: mGET,
|
||||
http.MethodHead: mHEAD,
|
||||
http.MethodOptions: mOPTIONS,
|
||||
http.MethodPatch: mPATCH,
|
||||
http.MethodPost: mPOST,
|
||||
http.MethodPut: mPUT,
|
||||
http.MethodTrace: mTRACE,
|
||||
}
|
||||
|
||||
// RegisterMethod adds support for custom HTTP method handlers, available
|
||||
@@ -706,7 +706,9 @@ func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) {
|
||||
}
|
||||
|
||||
// Wildcard pattern as finale
|
||||
// TODO: should we panic if there is stuff after the * ???
|
||||
if ws < len(pattern)-1 {
|
||||
panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead")
|
||||
}
|
||||
return ntCatchAll, "*", "", 0, ws, len(pattern)
|
||||
}
|
||||
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
golint:
|
||||
min-confidence: 0
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
maligned:
|
||||
suggest-new: true
|
||||
dupl:
|
||||
threshold: 100
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
misspell:
|
||||
locale: US
|
||||
lll:
|
||||
line-length: 140
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
disabled-checks:
|
||||
- wrapperFunc
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- megacheck
|
||||
- govet
|
||||
- unconvert
|
||||
- megacheck
|
||||
- structcheck
|
||||
- gas
|
||||
- gocyclo
|
||||
- dupl
|
||||
- misspell
|
||||
- unparam
|
||||
- varcheck
|
||||
- deadcode
|
||||
- typecheck
|
||||
- ineffassign
|
||||
- varcheck
|
||||
fast: false
|
||||
|
||||
|
||||
run:
|
||||
# modules-download-mode: vendor
|
||||
skip-dirs:
|
||||
- vendor
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- text: "weak cryptographic primitive"
|
||||
linters:
|
||||
- gosec
|
||||
|
||||
service:
|
||||
golangci-lint-version: 1.16.x
|
||||
+2
-3
@@ -13,7 +13,6 @@ before_install:
|
||||
|
||||
script:
|
||||
- GO111MODULE=on go get ./...
|
||||
- GO111MODULE=on go mod vendor
|
||||
- GO111MODULE=on go test -v -mod=vendor -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1;
|
||||
- golangci-lint run || travis_terminate 1;
|
||||
- GO111MODULE=on go test -v -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1;
|
||||
- golangci-lint run || travis_terminate 1;
|
||||
- $GOPATH/bin/goveralls -coverprofile=profile.cov -service=travis-ci
|
||||
|
||||
+45
-10
@@ -7,14 +7,14 @@
|
||||
## usage
|
||||
|
||||
```go
|
||||
l := lgr.New(lgr.Debug, lgr.CallerFile) // allow debug and caller file info
|
||||
l := lgr.New(lgr.Msec, lgr.Debug, lgr.CallerFile, lgr.CallerFunc) // allow debug and caller info, timestamp with milliseconds
|
||||
l.Logf("INFO some important message, %v", err)
|
||||
l.Logf("DEBUG some less important message, %v", err)
|
||||
```
|
||||
|
||||
output looks like this:
|
||||
```
|
||||
2018/01/07 13:02:34.000 INFO {svc/handler.go:101 h.MyFunc1} some important message, can't open file`
|
||||
2018/01/07 13:02:34.000 INFO {svc/handler.go:101 h.MyFunc1} some important message, can't open file myfile.xyz
|
||||
2018/01/07 13:02:34.015 DEBUG {svc/handler.go:155 h.MyFunc2} some less important message, file is too small`
|
||||
```
|
||||
|
||||
@@ -25,34 +25,69 @@ _Without `lgr.Caller*` it will drop `{caller}` part_
|
||||
### interfaces and default loggers
|
||||
|
||||
- `lgr` package provides a single interface `lgr.L` with a single method `Logf(format string, args ...interface{})`. Function wrapper `lgr.Func` allows to make `lgr.L` from a function directly.
|
||||
- Default logger functionality can be used without `lgr.New`, but just `lgr.Printf`
|
||||
- Default logger functionality can be used without `lgr.New` (see "global logger")
|
||||
- Two predefined loggers available: `lgr.NoOp` (do-nothing logger) and `lgr.Std` (passing directly to stdlib log)
|
||||
|
||||
### options
|
||||
|
||||
`lgr.New` call accepts functional options:
|
||||
|
||||
- `lgr.Debug` - turn debug mode on to allow messages with "DEBUG" level (filtered overwise)
|
||||
- `lgr.Debug` - turn debug mode on to allow messages with "DEBUG" level (filtered otherwise)
|
||||
- `lgr.Out(io.Writer)` - sets the output writer, default `os.Stdout`
|
||||
- `lgr.Err(io.Writer)` - sets the error writer, default `os.Stderr`
|
||||
- `lgr.CallerFile` - adds the caller file info
|
||||
- `lgr.CallerFunc` - adds the caller function info
|
||||
- `lgr.CallerPkg` - adds the caller package
|
||||
- `lgr.LevelBraces` - wraps levels with "[" and "]"
|
||||
- `lgr.Msec` - adds milliseconds to timestamp
|
||||
- `lgr.Out(io.Writer)` - sets the output writer, default `os.Stdout`
|
||||
- `lgr.Err(io.Writer)` - sets the error writer, default `os.Stderr`
|
||||
- `lgr.Format` - sets custom template, overwrite all other formatting modifiers.
|
||||
|
||||
example: `l := lgr.New(lgr.Debug, lgr.Msec)`
|
||||
|
||||
#### formatting templates:
|
||||
|
||||
Several predefined templates provided and can be passed directly to `lgr.Format`, i.e. `lgr.Format(lgr.WithMsec)`
|
||||
|
||||
```
|
||||
Short = `{{.DT.Format "2006/01/02 15:04:05"}} {{.Level}} {{.Message}}`
|
||||
WithMsec = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} {{.Message}}`
|
||||
WithPkg = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerPkg}}) {{.Message}}`
|
||||
ShortDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFile}}:{{.CallerLine}}) {{.Message}}`
|
||||
FuncDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFunc}}) {{.Message}}`
|
||||
FullDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFile}}:{{.CallerLine}} {{.CallerFunc}}) {{.Message}}`
|
||||
```
|
||||
|
||||
User can make a custom template and pass it directly to `lgr.Format`. For example:
|
||||
|
||||
```go
|
||||
lgr.Format(`{{.Level}} - {{.DT.Format "2006-01-02T15:04:05Z07:00"}} - {{.CallerPkg}} - {{.Message}}`)
|
||||
```
|
||||
|
||||
_Note: formatter (predefined or custom) adds measurable overhead - the cost will depend on the version of Go, but is between 30
|
||||
and 50% in recent tests with 1.12. You can validate this in your environment via benchmarks: `go test -bench=. -run=Bench`_
|
||||
|
||||
### levels
|
||||
|
||||
`lgr.Logf` recognizes prefixes like "INFO" or "[INFO]" as levels. The full list of supported levels - "DEBUG", "INFO", "WARN", "ERROR", "PANIC" and "FATAL"
|
||||
`lgr.Logf` recognizes prefixes like "INFO" or "[INFO]" as levels. The full list of supported levels - "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "PANIC" and "FATAL"
|
||||
|
||||
- `DEBUG` will be filtered unless `lgr.Debug` option defined
|
||||
- `TRACE` will be filtered unless `lgr.Trace` option defined
|
||||
- `DEBUG` will be filtered unless `lgr.Debug` or `lgr.Trace` options defined
|
||||
- `INFO` and `WARN` don't have any special behavior attached
|
||||
- `ERROR` sends messages to both out and err writers
|
||||
- `PANIC` and `FATAL` send messages to both out and err writers. In addition sends dump of callers and runtime info to err only, and calls `os.Exit(1)`.
|
||||
- `FATAL` and send messages to both out and err writers and exit(1)
|
||||
- `PANIC` does the same as `FATAL` but in addition sends dump of callers and runtime info to err.
|
||||
|
||||
### adaptors
|
||||
|
||||
`lgr` logger can be converted to `io.Writer` or `*log.Logger`
|
||||
|
||||
- `lgr.ToWriter(l lgr.L, level string) io.Writer` - makes io.Writer forwarding write ops to underlying `lgr.L`
|
||||
- `lgr.ToStdLogger(l lgr.L, level string) *log.Logger` - makes standard logger on top of `lgr.L`
|
||||
|
||||
_`level` parameter is optional, if defined (non-empty) will enforce the level._
|
||||
|
||||
### global logger
|
||||
|
||||
Users **should avoid** global logger and pass the concrete logger as a dependency. However, in some cases a global logger may be needed, for example migration from stdlib `log` to `lgr`. For such cases `log "github.com/go-pkgz/lgr"` can be imported instead of `log` package.
|
||||
|
||||
Global logger provides `lgr.Printf`, `lgr.Print` and `lgr.Fatalf` functions. User can customize the logger by calling `lgr.Setup(options ...)`. The instance of this logger can be retrieved with `lgr.Default()`
|
||||
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package lgr
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Writer holds lgr.L and wraps with io.Writer interface
|
||||
type Writer struct {
|
||||
L
|
||||
level string // if defined added to each message
|
||||
}
|
||||
|
||||
// Write to lgr.L
|
||||
func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
w.Logf(w.level + string(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// ToWriter makes io.Writer for given lgr.L with optional level
|
||||
func ToWriter(l L, level string) *Writer {
|
||||
if level != "" && !strings.HasSuffix(level, " ") {
|
||||
level += " "
|
||||
}
|
||||
return &Writer{l, level}
|
||||
}
|
||||
|
||||
func ToStdLogger(l L, level string) *log.Logger {
|
||||
return log.New(ToWriter(l, level), "", 0)
|
||||
}
|
||||
+5
-7
@@ -2,7 +2,6 @@ package lgr
|
||||
|
||||
import (
|
||||
stdlog "log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var def = New() // default logger doesn't allow DEBUG and doesn't add caller info
|
||||
@@ -15,7 +14,7 @@ type L interface {
|
||||
// Func type is an adapter to allow the use of ordinary functions as Logger.
|
||||
type Func func(format string, args ...interface{})
|
||||
|
||||
// Logf calls f(id)
|
||||
// Logf calls f(format, args...)
|
||||
func (f Func) Logf(format string, args ...interface{}) { f(format, args...) }
|
||||
|
||||
// NoOp logger
|
||||
@@ -26,24 +25,23 @@ var Std = Func(func(format string, args ...interface{}) { stdlog.Printf(format,
|
||||
|
||||
// Printf simplifies replacement of std logger
|
||||
func Printf(format string, args ...interface{}) {
|
||||
def.Logf(format, args...)
|
||||
def.logf(format, args...)
|
||||
}
|
||||
|
||||
// Print simplifies replacement of std logger
|
||||
func Print(line string) {
|
||||
def.Logf(line)
|
||||
def.logf(line)
|
||||
}
|
||||
|
||||
// Fatalf simplifies replacement of std logger
|
||||
func Fatalf(format string, args ...interface{}) {
|
||||
def.Logf(format, args...)
|
||||
os.Exit(1)
|
||||
def.logf(format, args...)
|
||||
def.fatal()
|
||||
}
|
||||
|
||||
// Setup default logger with options
|
||||
func Setup(opts ...Option) {
|
||||
def = New(opts...)
|
||||
def.callerSkip = 2
|
||||
}
|
||||
|
||||
// Default returns pre-constructed def logger (debug off, callers disabled)
|
||||
|
||||
+227
-149
@@ -1,156 +1,296 @@
|
||||
// Package lgr provides a simple logger with some extras. Primary way to log is Logf method.
|
||||
// The logger's output can be customized in 2 ways:
|
||||
// - by setting individual formatting flags, i.e. lgr.New(lgr.Msec, lgr.CallerFunc)
|
||||
// - by passing formatting template, i.e. lgr.New(lgr.Format(lgr.Short))
|
||||
// Leveled output works for messages based on text prefix, i.e. Logf("INFO some message") means INFO level.
|
||||
// Debug and trace levels can be filtered based on lgr.Trace and lgr.Debug options.
|
||||
// ERROR, FATAL and PANIC levels send to err as well. FATAL terminate caller application with os.Exit(1)
|
||||
// and PANIC also prints stack trace.
|
||||
|
||||
package lgr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
var levels = []string{"DEBUG", "INFO", "WARN", "ERROR", "PANIC", "FATAL"}
|
||||
var levels = []string{"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "PANIC", "FATAL"}
|
||||
|
||||
const (
|
||||
Short = `{{.DT.Format "2006/01/02 15:04:05"}} {{.Level}} {{.Message}}`
|
||||
WithMsec = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} {{.Message}}`
|
||||
WithPkg = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerPkg}}) {{.Message}}`
|
||||
ShortDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFile}}:{{.CallerLine}}) {{.Message}}`
|
||||
FuncDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFunc}}) {{.Message}}`
|
||||
FullDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFile}}:{{.CallerLine}} {{.CallerFunc}}) {{.Message}}`
|
||||
)
|
||||
|
||||
// Logger provided simple logger with basic support of levels. Thread safe
|
||||
type Logger struct {
|
||||
stdout, stderr io.Writer
|
||||
dbg bool
|
||||
lock sync.Mutex
|
||||
callerFile bool
|
||||
callerFunc bool
|
||||
callerPkg bool
|
||||
callerSkip int
|
||||
ignoredPkgCallers []string
|
||||
// set with Option calls
|
||||
stdout, stderr io.Writer // destination writes for out and err
|
||||
dbg bool // allows reporting for DEBUG level
|
||||
trace bool // allows reporting for TRACE and DEBUG levels
|
||||
callerFile bool // reports caller file with line number, i.e. foo/bar.go:89
|
||||
callerFunc bool // reports caller function name, i.e. bar.myFunc
|
||||
callerPkg bool // reports caller package name
|
||||
levelBraces bool // encloses level with [], i.e. [INFO]
|
||||
callerDepth int // how many stack frames to skip, relative to the real (reported) frame
|
||||
format string // layout template
|
||||
|
||||
now nowFn
|
||||
fatal panicFn
|
||||
levelBraces bool
|
||||
msec bool
|
||||
// internal use
|
||||
now nowFn
|
||||
fatal panicFn
|
||||
msec bool
|
||||
lock sync.Mutex
|
||||
callerOn bool
|
||||
levelBracesOn bool
|
||||
templ *template.Template
|
||||
}
|
||||
|
||||
// can be redefined internally for testing
|
||||
type nowFn func() time.Time
|
||||
type panicFn func()
|
||||
|
||||
// New makes new leveled logger. Accepts dbg flag turing on info about the caller and allowing DEBUG messages/
|
||||
// Two writers can be passed optionally - first for out and second for err
|
||||
// layout holds all parts to construct the final message with template or with individual flags
|
||||
type layout struct {
|
||||
DT time.Time
|
||||
Level string
|
||||
Message string
|
||||
CallerPkg string
|
||||
CallerFile string
|
||||
CallerFunc string
|
||||
CallerLine int
|
||||
}
|
||||
|
||||
// New makes new leveled logger. By default writes to stdout/stderr.
|
||||
// default format: 2018/01/07 13:02:34.123 DEBUG some message 123
|
||||
func New(options ...Option) *Logger {
|
||||
|
||||
res := Logger{
|
||||
now: time.Now,
|
||||
fatal: func() { os.Exit(1) },
|
||||
stdout: os.Stdout,
|
||||
stderr: os.Stderr,
|
||||
callerSkip: 1,
|
||||
now: time.Now,
|
||||
fatal: func() { os.Exit(1) },
|
||||
stdout: os.Stdout,
|
||||
stderr: os.Stderr,
|
||||
callerDepth: 0,
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(&res)
|
||||
}
|
||||
|
||||
if res.format != "" {
|
||||
// formatter defined
|
||||
var err error
|
||||
res.templ, err = template.New("lgr").Parse(res.format)
|
||||
if err != nil {
|
||||
fmt.Printf("invalid template %s, error %v. switched to %s\n", res.format, err, Short)
|
||||
res.format = Short
|
||||
res.templ = template.Must(template.New("lgrDefault").Parse(Short))
|
||||
}
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
if err = res.templ.Execute(&buf, layout{}); err != nil {
|
||||
fmt.Printf("failed to execute template %s, error %v. switched to %s\n", res.format, err, Short)
|
||||
res.format = Short
|
||||
res.templ = template.Must(template.New("lgrDefault").Parse(Short))
|
||||
}
|
||||
}
|
||||
|
||||
// set *On flags once for optimization on multiple Logf calls
|
||||
res.callerOn = strings.Contains(res.format, "{{.Caller") || res.callerFile || res.callerFunc || res.callerPkg
|
||||
res.levelBracesOn = strings.Contains(res.format, "[{{.Level}}]") || res.levelBraces
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// Logf implements L interface to output with printf style.
|
||||
// Each line prefixed with ts, level and optionally (dbg mode only) by caller info.
|
||||
// DEBUG and TRACE filtered out by dbg and trace flags.
|
||||
// ERROR and FATAL also send the same line to err writer.
|
||||
// FATAL adds runtime stack and os.exit(1), like panic.
|
||||
// FATAL and PANIC adds runtime stack and os.exit(1), like panic.
|
||||
func (l *Logger) Logf(format string, args ...interface{}) {
|
||||
// to align call depth between (*Logger).Logf() and, for example, Printf()
|
||||
l.logf(format, args...)
|
||||
}
|
||||
|
||||
// format timestamp with or without msecs
|
||||
ts := func() (res string) {
|
||||
if l.msec {
|
||||
return l.now().Format("2006/01/02 15:04:05.000")
|
||||
}
|
||||
return l.now().Format("2006/01/02 15:04:05")
|
||||
}
|
||||
func (l *Logger) logf(format string, args ...interface{}) {
|
||||
|
||||
lv, msg := l.extractLevel(fmt.Sprintf(format, args...))
|
||||
if lv == "DEBUG" && !l.dbg {
|
||||
return
|
||||
}
|
||||
var bld strings.Builder
|
||||
bld.WriteString(ts())
|
||||
bld.WriteString(l.formatLevel(lv))
|
||||
bld.WriteString(" ")
|
||||
|
||||
if l.callerFile || l.callerFunc || l.callerPkg {
|
||||
if pc, file, line, ok := runtime.Caller(l.callerSkip); ok {
|
||||
|
||||
funcName, fileInfo := "", ""
|
||||
|
||||
if l.callerFunc {
|
||||
funcNameElems := strings.Split(runtime.FuncForPC(pc).Name(), "/")
|
||||
funcName = funcNameElems[len(funcNameElems)-1]
|
||||
}
|
||||
|
||||
if l.callerFile {
|
||||
fnameElems := strings.Split(file, "/")
|
||||
fileInfo = fmt.Sprintf("%s:%d", strings.Join(fnameElems[len(fnameElems)-2:], "/"), line)
|
||||
if l.callerFunc {
|
||||
fileInfo += " "
|
||||
}
|
||||
}
|
||||
// callerPkg only if no other callers
|
||||
if l.callerPkg && !l.callerFile && !l.callerFunc {
|
||||
file = l.ignoreCaller(file)
|
||||
_, fileInfo = path.Split(path.Dir(file))
|
||||
if l.callerFunc {
|
||||
fileInfo += " "
|
||||
}
|
||||
}
|
||||
srcFileInfo := fmt.Sprintf("{%s%s} ", fileInfo, funcName)
|
||||
bld.WriteString(srcFileInfo)
|
||||
}
|
||||
if lv == "TRACE" && !l.trace {
|
||||
return
|
||||
}
|
||||
|
||||
bld.WriteString(msg) //nolint
|
||||
bld.WriteString("\n") //nolint
|
||||
var ci callerInfo
|
||||
if l.callerOn { // optimization to avoid expensive caller evaluation if caller info not in the template
|
||||
ci = l.reportCaller(l.callerDepth)
|
||||
}
|
||||
|
||||
elems := layout{
|
||||
DT: l.now(),
|
||||
Level: l.formatLevel(lv),
|
||||
Message: strings.TrimSuffix(msg, "\n"), // output adds EOL, trim from the message if passed
|
||||
CallerFunc: ci.FuncName,
|
||||
CallerFile: ci.File,
|
||||
CallerPkg: ci.Pkg,
|
||||
CallerLine: ci.Line,
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if l.format == "" {
|
||||
data = []byte(l.formatWithOptions(elems))
|
||||
} else {
|
||||
buf := bytes.Buffer{}
|
||||
err := l.templ.Execute(&buf, elems) // once constructed, a template may be executed safely in parallel.
|
||||
if err != nil {
|
||||
fmt.Printf("failed to execute template, %v\n", err) // should never happen
|
||||
}
|
||||
data = buf.Bytes()
|
||||
}
|
||||
data = append(data, '\n')
|
||||
|
||||
if l.levelBracesOn { // rearrange space in short levels
|
||||
data = bytes.Replace(data, []byte("[WARN ]"), []byte("[WARN] "), 1)
|
||||
data = bytes.Replace(data, []byte("[INFO ]"), []byte("[INFO] "), 1)
|
||||
}
|
||||
|
||||
l.lock.Lock()
|
||||
msgb := []byte(bld.String())
|
||||
l.stdout.Write(msgb) //nolint
|
||||
_, _ = l.stdout.Write(data)
|
||||
|
||||
// write to err as well for high levels, exit(1) on fatal and panic and dump stack on panic level
|
||||
switch lv {
|
||||
case "PANIC", "FATAL":
|
||||
l.stderr.Write(msgb) //nolint
|
||||
bld.WriteString("\n") //nolint
|
||||
l.stderr.Write(getDump()) //nolint
|
||||
l.fatal()
|
||||
case "ERROR":
|
||||
l.stderr.Write(msgb) //nolint
|
||||
_, _ = l.stderr.Write(data)
|
||||
case "FATAL":
|
||||
_, _ = l.stderr.Write(data)
|
||||
l.fatal()
|
||||
case "PANIC":
|
||||
_, _ = l.stderr.Write(data)
|
||||
_, _ = l.stderr.Write(getDump())
|
||||
l.fatal()
|
||||
}
|
||||
|
||||
l.lock.Unlock()
|
||||
}
|
||||
|
||||
func (l *Logger) ignoreCaller(p string) string {
|
||||
for _, s := range l.ignoredPkgCallers {
|
||||
if strings.Contains(p, "/"+s+"/") {
|
||||
return strings.Replace(p, "/"+s, "", 1)
|
||||
}
|
||||
}
|
||||
return p
|
||||
type callerInfo struct {
|
||||
File string
|
||||
Line int
|
||||
FuncName string
|
||||
Pkg string
|
||||
}
|
||||
|
||||
func (l *Logger) formatLevel(lv string) string {
|
||||
// calldepth 0 identifying the caller of reportCaller()
|
||||
func (l *Logger) reportCaller(calldepth int) (res callerInfo) {
|
||||
|
||||
brace := func(b string) string {
|
||||
if l.levelBraces {
|
||||
return b
|
||||
// caller gets file, line number abd function name via runtime.Callers
|
||||
// file looks like /go/src/github.com/go-pkgz/lgr/logger.go
|
||||
// file is an empty string if not known.
|
||||
// funcName looks like:
|
||||
// main.Test
|
||||
// foo/bar.Test
|
||||
// foo/bar.Test.func1
|
||||
// foo/bar.(*Bar).Test
|
||||
// foo/bar.glob..func1
|
||||
// funcName is an empty string if not known.
|
||||
// line is a zero if not known.
|
||||
caller := func(calldepth int) (file string, line int, funcName string) {
|
||||
pcs := make([]uintptr, 1)
|
||||
n := runtime.Callers(calldepth, pcs)
|
||||
if n != 1 {
|
||||
return "", 0, ""
|
||||
}
|
||||
return ""
|
||||
|
||||
frame, _ := runtime.CallersFrames(pcs).Next()
|
||||
|
||||
return frame.File, frame.Line, frame.Function
|
||||
}
|
||||
|
||||
if lv == "" {
|
||||
return ""
|
||||
// add 5 to adjust stack level because it was called from 3 nested functions added by lgr, i.e. caller,
|
||||
// reportCaller and logf, plus 2 frames by runtime
|
||||
filePath, line, funcName := caller(calldepth + 2 + 3)
|
||||
if (filePath == "") || (line <= 0) || (funcName == "") {
|
||||
return callerInfo{}
|
||||
}
|
||||
|
||||
_, pkgInfo := path.Split(path.Dir(filePath))
|
||||
res.Pkg = pkgInfo
|
||||
|
||||
res.File = filePath
|
||||
if pathElems := strings.Split(filePath, "/"); len(pathElems) > 2 {
|
||||
res.File = strings.Join(pathElems[len(pathElems)-2:], "/")
|
||||
}
|
||||
res.Line = line
|
||||
|
||||
funcNameElems := strings.Split(funcName, "/")
|
||||
res.FuncName = funcNameElems[len(funcNameElems)-1]
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// speed-optimized version of formatter, used with individual options only, i.e. without Format call
|
||||
func (l *Logger) formatWithOptions(elems layout) (res string) {
|
||||
|
||||
orElse := func(flag bool, fnTrue func() string, fnFalse func() string) string {
|
||||
if flag {
|
||||
return fnTrue()
|
||||
}
|
||||
return fnFalse()
|
||||
}
|
||||
nothing := func() string { return "" }
|
||||
|
||||
parts := make([]string, 0, 4)
|
||||
|
||||
parts = append(parts, orElse(l.msec,
|
||||
func() string { return elems.DT.Format("2006/01/02 15:04:05.000") },
|
||||
func() string { return elems.DT.Format("2006/01/02 15:04:05") },
|
||||
))
|
||||
|
||||
parts = append(parts, orElse(l.levelBraces,
|
||||
func() string { return `[` + elems.Level + `]` },
|
||||
func() string { return elems.Level },
|
||||
))
|
||||
|
||||
if l.callerFile || l.callerFunc || l.callerPkg {
|
||||
var callerParts []string
|
||||
v := orElse(l.callerFile, func() string { return elems.CallerFile + ":" + strconv.Itoa(elems.CallerLine) }, nothing)
|
||||
if v != "" {
|
||||
callerParts = append(callerParts, v)
|
||||
}
|
||||
if v := orElse(l.callerFunc, func() string { return elems.CallerFunc }, nothing); v != "" {
|
||||
callerParts = append(callerParts, v)
|
||||
}
|
||||
if v := orElse(l.callerPkg, func() string { return elems.CallerPkg }, nothing); v != "" {
|
||||
callerParts = append(callerParts, v)
|
||||
}
|
||||
parts = append(parts, "{"+strings.Join(callerParts, " ")+"}")
|
||||
}
|
||||
|
||||
parts = append(parts, elems.Message)
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// formatLevel aligns level to 5 chars
|
||||
func (l *Logger) formatLevel(lv string) string {
|
||||
|
||||
spaces := ""
|
||||
if len(lv) == 4 {
|
||||
spaces = " "
|
||||
}
|
||||
return " " + brace("[") + lv + brace("]") + spaces
|
||||
return lv + spaces
|
||||
}
|
||||
|
||||
// extractLevel parses messages with optional level prefix and returns level and the message with stripped level
|
||||
func (l *Logger) extractLevel(line string) (level, msg string) {
|
||||
for _, lv := range levels {
|
||||
if strings.HasPrefix(line, lv) {
|
||||
@@ -173,65 +313,3 @@ func getDump() []byte {
|
||||
}
|
||||
return stacktrace[:length]
|
||||
}
|
||||
|
||||
// Option func type
|
||||
type Option func(l *Logger)
|
||||
|
||||
// Out sets out writer
|
||||
func Out(w io.Writer) Option {
|
||||
return func(l *Logger) {
|
||||
l.stdout = w
|
||||
}
|
||||
}
|
||||
|
||||
// Err sets error writer
|
||||
func Err(w io.Writer) Option {
|
||||
return func(l *Logger) {
|
||||
l.stderr = w
|
||||
}
|
||||
}
|
||||
|
||||
// Debug turn on dbg mode
|
||||
func Debug(l *Logger) {
|
||||
l.dbg = true
|
||||
}
|
||||
|
||||
// CallerFile adds caller info with file, and line number
|
||||
func CallerFile(l *Logger) {
|
||||
l.callerFile = true
|
||||
}
|
||||
|
||||
// CallerFunc adds caller info with function name
|
||||
func CallerFunc(l *Logger) {
|
||||
l.callerFunc = true
|
||||
}
|
||||
|
||||
// CallerPkg adds caller's package name
|
||||
func CallerPkg(l *Logger) {
|
||||
l.callerPkg = true
|
||||
}
|
||||
|
||||
// CallerIgnore sets packages skipped from logging caller
|
||||
func CallerIgnore(ignores ...string) Option {
|
||||
return func(l *Logger) {
|
||||
l.ignoredPkgCallers = ignores
|
||||
}
|
||||
}
|
||||
|
||||
// CallerSkip sets how many trace levels to skip.
|
||||
// by default this value is 1 , i.e. skip logger level only
|
||||
func CallerSkip(n int) Option {
|
||||
return func(l *Logger) {
|
||||
l.callerSkip = n
|
||||
}
|
||||
}
|
||||
|
||||
// LevelBraces adds [] to level
|
||||
func LevelBraces(l *Logger) {
|
||||
l.levelBraces = true
|
||||
}
|
||||
|
||||
// Msec adds .msec to timestamp
|
||||
func Msec(l *Logger) {
|
||||
l.msec = true
|
||||
}
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package lgr
|
||||
|
||||
import "io"
|
||||
|
||||
// Option func type
|
||||
type Option func(l *Logger)
|
||||
|
||||
// Out sets out writer, stdout by default
|
||||
func Out(w io.Writer) Option {
|
||||
return func(l *Logger) {
|
||||
l.stdout = w
|
||||
}
|
||||
}
|
||||
|
||||
// Err sets error writer, stderr by default
|
||||
func Err(w io.Writer) Option {
|
||||
return func(l *Logger) {
|
||||
l.stderr = w
|
||||
}
|
||||
}
|
||||
|
||||
// Debug turn on dbg mode
|
||||
func Debug(l *Logger) {
|
||||
l.dbg = true
|
||||
}
|
||||
|
||||
// Trace turn on trace + dbg mode
|
||||
func Trace(l *Logger) {
|
||||
l.dbg = true
|
||||
l.trace = true
|
||||
}
|
||||
|
||||
// CallerDepth sets number of stack frame skipped for caller reporting, 0 by default
|
||||
func CallerDepth(n int) Option {
|
||||
return func(l *Logger) {
|
||||
l.callerDepth = n
|
||||
}
|
||||
}
|
||||
|
||||
// Format sets output layout, overwrites all options for individual parts, i.e. Caller*, Msec and LevelBraces
|
||||
func Format(f string) Option {
|
||||
return func(l *Logger) {
|
||||
l.format = f
|
||||
}
|
||||
}
|
||||
|
||||
// CallerFunc adds caller info with function name. Ignored if Format option used.
|
||||
func CallerFunc(l *Logger) {
|
||||
l.callerFunc = true
|
||||
}
|
||||
|
||||
// CallerPkg adds caller's package name. Ignored if Format option used.
|
||||
func CallerPkg(l *Logger) {
|
||||
l.callerPkg = true
|
||||
}
|
||||
|
||||
// LevelBraces surrounds level with [], i.e. [INFO]. Ignored if Format option used.
|
||||
func LevelBraces(l *Logger) {
|
||||
l.levelBraces = true
|
||||
}
|
||||
|
||||
// CallerFile adds caller info with file, and line number. Ignored if Format option used.
|
||||
func CallerFile(l *Logger) {
|
||||
l.callerFile = true
|
||||
}
|
||||
|
||||
// Msec adds .msec to timestamp. Ignored if Format option used.
|
||||
func Msec(l *Logger) {
|
||||
l.msec = true
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# This is the official list of GoMock authors for copyright purposes.
|
||||
# This file is distinct from the CONTRIBUTORS files.
|
||||
# See the latter for an explanation.
|
||||
|
||||
# Names should be added to this file as
|
||||
# Name or Organization <email address>
|
||||
# The email address is not required for organizations.
|
||||
|
||||
# Please keep the list sorted.
|
||||
|
||||
Alex Reece <awreece@gmail.com>
|
||||
Google Inc.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# This is the official list of people who can contribute (and typically
|
||||
# have contributed) code to the gomock repository.
|
||||
# The AUTHORS file lists the copyright holders; this file
|
||||
# lists people. For example, Google employees are listed here
|
||||
# but not in AUTHORS, because Google holds the copyright.
|
||||
#
|
||||
# The submission process automatically checks to make sure
|
||||
# that people submitting code are listed in this file (by email address).
|
||||
#
|
||||
# Names should be added to this file only after verifying that
|
||||
# the individual or the individual's organization has agreed to
|
||||
# the appropriate Contributor License Agreement, found here:
|
||||
#
|
||||
# http://code.google.com/legal/individual-cla-v1.0.html
|
||||
# http://code.google.com/legal/corporate-cla-v1.0.html
|
||||
#
|
||||
# The agreement for individuals can be filled out on the web.
|
||||
#
|
||||
# When adding J Random Contributor's name to this file,
|
||||
# either J's name or J's organization's name should be
|
||||
# added to the AUTHORS file, depending on whether the
|
||||
# individual or corporate CLA was used.
|
||||
|
||||
# Names should be added to this file like so:
|
||||
# Name <email address>
|
||||
#
|
||||
# An entry with two email addresses specifies that the
|
||||
# first address should be used in the submit logs and
|
||||
# that the second address should be recognized as the
|
||||
# same person when interacting with Rietveld.
|
||||
|
||||
# Please keep the list sorted.
|
||||
|
||||
Aaron Jacobs <jacobsa@google.com> <aaronjjacobs@gmail.com>
|
||||
Alex Reece <awreece@gmail.com>
|
||||
David Symonds <dsymonds@golang.org>
|
||||
Ryan Barrett <ryanb@google.com>
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
// Copyright 2010 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gomock
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Call represents an expected call to a mock.
|
||||
type Call struct {
|
||||
t TestHelper // for triggering test failures on invalid call setup
|
||||
|
||||
receiver interface{} // the receiver of the method call
|
||||
method string // the name of the method
|
||||
methodType reflect.Type // the type of the method
|
||||
args []Matcher // the args
|
||||
origin string // file and line number of call setup
|
||||
|
||||
preReqs []*Call // prerequisite calls
|
||||
|
||||
// Expectations
|
||||
minCalls, maxCalls int
|
||||
|
||||
numCalls int // actual number made
|
||||
|
||||
// actions are called when this Call is called. Each action gets the args and
|
||||
// can set the return values by returning a non-nil slice. Actions run in the
|
||||
// order they are created.
|
||||
actions []func([]interface{}) []interface{}
|
||||
}
|
||||
|
||||
// newCall creates a *Call. It requires the method type in order to support
|
||||
// unexported methods.
|
||||
func newCall(t TestHelper, receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call {
|
||||
t.Helper()
|
||||
|
||||
// TODO: check arity, types.
|
||||
margs := make([]Matcher, len(args))
|
||||
for i, arg := range args {
|
||||
if m, ok := arg.(Matcher); ok {
|
||||
margs[i] = m
|
||||
} else if arg == nil {
|
||||
// Handle nil specially so that passing a nil interface value
|
||||
// will match the typed nils of concrete args.
|
||||
margs[i] = Nil()
|
||||
} else {
|
||||
margs[i] = Eq(arg)
|
||||
}
|
||||
}
|
||||
|
||||
origin := callerInfo(3)
|
||||
actions := []func([]interface{}) []interface{}{func([]interface{}) []interface{} {
|
||||
// Synthesize the zero value for each of the return args' types.
|
||||
rets := make([]interface{}, methodType.NumOut())
|
||||
for i := 0; i < methodType.NumOut(); i++ {
|
||||
rets[i] = reflect.Zero(methodType.Out(i)).Interface()
|
||||
}
|
||||
return rets
|
||||
}}
|
||||
return &Call{t: t, receiver: receiver, method: method, methodType: methodType,
|
||||
args: margs, origin: origin, minCalls: 1, maxCalls: 1, actions: actions}
|
||||
}
|
||||
|
||||
// AnyTimes allows the expectation to be called 0 or more times
|
||||
func (c *Call) AnyTimes() *Call {
|
||||
c.minCalls, c.maxCalls = 0, 1e8 // close enough to infinity
|
||||
return c
|
||||
}
|
||||
|
||||
// MinTimes requires the call to occur at least n times. If AnyTimes or MaxTimes have not been called, MinTimes also
|
||||
// sets the maximum number of calls to infinity.
|
||||
func (c *Call) MinTimes(n int) *Call {
|
||||
c.minCalls = n
|
||||
if c.maxCalls == 1 {
|
||||
c.maxCalls = 1e8
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// MaxTimes limits the number of calls to n times. If AnyTimes or MinTimes have not been called, MaxTimes also
|
||||
// sets the minimum number of calls to 0.
|
||||
func (c *Call) MaxTimes(n int) *Call {
|
||||
c.maxCalls = n
|
||||
if c.minCalls == 1 {
|
||||
c.minCalls = 0
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn declares the action to run when the call is matched.
|
||||
// The return values from this function are returned by the mocked function.
|
||||
// It takes an interface{} argument to support n-arity functions.
|
||||
func (c *Call) DoAndReturn(f interface{}) *Call {
|
||||
// TODO: Check arity and types here, rather than dying badly elsewhere.
|
||||
v := reflect.ValueOf(f)
|
||||
|
||||
c.addAction(func(args []interface{}) []interface{} {
|
||||
vargs := make([]reflect.Value, len(args))
|
||||
ft := v.Type()
|
||||
for i := 0; i < len(args); i++ {
|
||||
if args[i] != nil {
|
||||
vargs[i] = reflect.ValueOf(args[i])
|
||||
} else {
|
||||
// Use the zero value for the arg.
|
||||
vargs[i] = reflect.Zero(ft.In(i))
|
||||
}
|
||||
}
|
||||
vrets := v.Call(vargs)
|
||||
rets := make([]interface{}, len(vrets))
|
||||
for i, ret := range vrets {
|
||||
rets[i] = ret.Interface()
|
||||
}
|
||||
return rets
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
// Do declares the action to run when the call is matched. The function's
|
||||
// return values are ignored to retain backward compatibility. To use the
|
||||
// return values call DoAndReturn.
|
||||
// It takes an interface{} argument to support n-arity functions.
|
||||
func (c *Call) Do(f interface{}) *Call {
|
||||
// TODO: Check arity and types here, rather than dying badly elsewhere.
|
||||
v := reflect.ValueOf(f)
|
||||
|
||||
c.addAction(func(args []interface{}) []interface{} {
|
||||
vargs := make([]reflect.Value, len(args))
|
||||
ft := v.Type()
|
||||
for i := 0; i < len(args); i++ {
|
||||
if args[i] != nil {
|
||||
vargs[i] = reflect.ValueOf(args[i])
|
||||
} else {
|
||||
// Use the zero value for the arg.
|
||||
vargs[i] = reflect.Zero(ft.In(i))
|
||||
}
|
||||
}
|
||||
v.Call(vargs)
|
||||
return nil
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
// Return declares the values to be returned by the mocked function call.
|
||||
func (c *Call) Return(rets ...interface{}) *Call {
|
||||
c.t.Helper()
|
||||
|
||||
mt := c.methodType
|
||||
if len(rets) != mt.NumOut() {
|
||||
c.t.Fatalf("wrong number of arguments to Return for %T.%v: got %d, want %d [%s]",
|
||||
c.receiver, c.method, len(rets), mt.NumOut(), c.origin)
|
||||
}
|
||||
for i, ret := range rets {
|
||||
if got, want := reflect.TypeOf(ret), mt.Out(i); got == want {
|
||||
// Identical types; nothing to do.
|
||||
} else if got == nil {
|
||||
// Nil needs special handling.
|
||||
switch want.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
// ok
|
||||
default:
|
||||
c.t.Fatalf("argument %d to Return for %T.%v is nil, but %v is not nillable [%s]",
|
||||
i, c.receiver, c.method, want, c.origin)
|
||||
}
|
||||
} else if got.AssignableTo(want) {
|
||||
// Assignable type relation. Make the assignment now so that the generated code
|
||||
// can return the values with a type assertion.
|
||||
v := reflect.New(want).Elem()
|
||||
v.Set(reflect.ValueOf(ret))
|
||||
rets[i] = v.Interface()
|
||||
} else {
|
||||
c.t.Fatalf("wrong type of argument %d to Return for %T.%v: %v is not assignable to %v [%s]",
|
||||
i, c.receiver, c.method, got, want, c.origin)
|
||||
}
|
||||
}
|
||||
|
||||
c.addAction(func([]interface{}) []interface{} {
|
||||
return rets
|
||||
})
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Times declares the exact number of times a function call is expected to be executed.
|
||||
func (c *Call) Times(n int) *Call {
|
||||
c.minCalls, c.maxCalls = n, n
|
||||
return c
|
||||
}
|
||||
|
||||
// SetArg declares an action that will set the nth argument's value,
|
||||
// indirected through a pointer. Or, in the case of a slice, SetArg
|
||||
// will copy value's elements into the nth argument.
|
||||
func (c *Call) SetArg(n int, value interface{}) *Call {
|
||||
c.t.Helper()
|
||||
|
||||
mt := c.methodType
|
||||
// TODO: This will break on variadic methods.
|
||||
// We will need to check those at invocation time.
|
||||
if n < 0 || n >= mt.NumIn() {
|
||||
c.t.Fatalf("SetArg(%d, ...) called for a method with %d args [%s]",
|
||||
n, mt.NumIn(), c.origin)
|
||||
}
|
||||
// Permit setting argument through an interface.
|
||||
// In the interface case, we don't (nay, can't) check the type here.
|
||||
at := mt.In(n)
|
||||
switch at.Kind() {
|
||||
case reflect.Ptr:
|
||||
dt := at.Elem()
|
||||
if vt := reflect.TypeOf(value); !vt.AssignableTo(dt) {
|
||||
c.t.Fatalf("SetArg(%d, ...) argument is a %v, not assignable to %v [%s]",
|
||||
n, vt, dt, c.origin)
|
||||
}
|
||||
case reflect.Interface:
|
||||
// nothing to do
|
||||
case reflect.Slice:
|
||||
// nothing to do
|
||||
default:
|
||||
c.t.Fatalf("SetArg(%d, ...) referring to argument of non-pointer non-interface non-slice type %v [%s]",
|
||||
n, at, c.origin)
|
||||
}
|
||||
|
||||
c.addAction(func(args []interface{}) []interface{} {
|
||||
v := reflect.ValueOf(value)
|
||||
switch reflect.TypeOf(args[n]).Kind() {
|
||||
case reflect.Slice:
|
||||
setSlice(args[n], v)
|
||||
default:
|
||||
reflect.ValueOf(args[n]).Elem().Set(v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
// isPreReq returns true if other is a direct or indirect prerequisite to c.
|
||||
func (c *Call) isPreReq(other *Call) bool {
|
||||
for _, preReq := range c.preReqs {
|
||||
if other == preReq || preReq.isPreReq(other) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// After declares that the call may only match after preReq has been exhausted.
|
||||
func (c *Call) After(preReq *Call) *Call {
|
||||
c.t.Helper()
|
||||
|
||||
if c == preReq {
|
||||
c.t.Fatalf("A call isn't allowed to be its own prerequisite")
|
||||
}
|
||||
if preReq.isPreReq(c) {
|
||||
c.t.Fatalf("Loop in call order: %v is a prerequisite to %v (possibly indirectly).", c, preReq)
|
||||
}
|
||||
|
||||
c.preReqs = append(c.preReqs, preReq)
|
||||
return c
|
||||
}
|
||||
|
||||
// Returns true if the minimum number of calls have been made.
|
||||
func (c *Call) satisfied() bool {
|
||||
return c.numCalls >= c.minCalls
|
||||
}
|
||||
|
||||
// Returns true iff the maximum number of calls have been made.
|
||||
func (c *Call) exhausted() bool {
|
||||
return c.numCalls >= c.maxCalls
|
||||
}
|
||||
|
||||
func (c *Call) String() string {
|
||||
args := make([]string, len(c.args))
|
||||
for i, arg := range c.args {
|
||||
args[i] = arg.String()
|
||||
}
|
||||
arguments := strings.Join(args, ", ")
|
||||
return fmt.Sprintf("%T.%v(%s) %s", c.receiver, c.method, arguments, c.origin)
|
||||
}
|
||||
|
||||
// Tests if the given call matches the expected call.
|
||||
// If yes, returns nil. If no, returns error with message explaining why it does not match.
|
||||
func (c *Call) matches(args []interface{}) error {
|
||||
if !c.methodType.IsVariadic() {
|
||||
if len(args) != len(c.args) {
|
||||
return fmt.Errorf("Expected call at %s has the wrong number of arguments. Got: %d, want: %d",
|
||||
c.origin, len(args), len(c.args))
|
||||
}
|
||||
|
||||
for i, m := range c.args {
|
||||
if !m.Matches(args[i]) {
|
||||
return fmt.Errorf("Expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v",
|
||||
c.origin, strconv.Itoa(i), args[i], m)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(c.args) < c.methodType.NumIn()-1 {
|
||||
return fmt.Errorf("Expected call at %s has the wrong number of matchers. Got: %d, want: %d",
|
||||
c.origin, len(c.args), c.methodType.NumIn()-1)
|
||||
}
|
||||
if len(c.args) != c.methodType.NumIn() && len(args) != len(c.args) {
|
||||
return fmt.Errorf("Expected call at %s has the wrong number of arguments. Got: %d, want: %d",
|
||||
c.origin, len(args), len(c.args))
|
||||
}
|
||||
if len(args) < len(c.args)-1 {
|
||||
return fmt.Errorf("Expected call at %s has the wrong number of arguments. Got: %d, want: greater than or equal to %d",
|
||||
c.origin, len(args), len(c.args)-1)
|
||||
}
|
||||
|
||||
for i, m := range c.args {
|
||||
if i < c.methodType.NumIn()-1 {
|
||||
// Non-variadic args
|
||||
if !m.Matches(args[i]) {
|
||||
return fmt.Errorf("Expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v",
|
||||
c.origin, strconv.Itoa(i), args[i], m)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// The last arg has a possibility of a variadic argument, so let it branch
|
||||
|
||||
// sample: Foo(a int, b int, c ...int)
|
||||
if i < len(c.args) && i < len(args) {
|
||||
if m.Matches(args[i]) {
|
||||
// Got Foo(a, b, c) want Foo(matcherA, matcherB, gomock.Any())
|
||||
// Got Foo(a, b, c) want Foo(matcherA, matcherB, someSliceMatcher)
|
||||
// Got Foo(a, b, c) want Foo(matcherA, matcherB, matcherC)
|
||||
// Got Foo(a, b) want Foo(matcherA, matcherB)
|
||||
// Got Foo(a, b, c, d) want Foo(matcherA, matcherB, matcherC, matcherD)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// The number of actual args don't match the number of matchers,
|
||||
// or the last matcher is a slice and the last arg is not.
|
||||
// If this function still matches it is because the last matcher
|
||||
// matches all the remaining arguments or the lack of any.
|
||||
// Convert the remaining arguments, if any, into a slice of the
|
||||
// expected type.
|
||||
vargsType := c.methodType.In(c.methodType.NumIn() - 1)
|
||||
vargs := reflect.MakeSlice(vargsType, 0, len(args)-i)
|
||||
for _, arg := range args[i:] {
|
||||
vargs = reflect.Append(vargs, reflect.ValueOf(arg))
|
||||
}
|
||||
if m.Matches(vargs.Interface()) {
|
||||
// Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, gomock.Any())
|
||||
// Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, someSliceMatcher)
|
||||
// Got Foo(a, b) want Foo(matcherA, matcherB, gomock.Any())
|
||||
// Got Foo(a, b) want Foo(matcherA, matcherB, someEmptySliceMatcher)
|
||||
break
|
||||
}
|
||||
// Wrong number of matchers or not match. Fail.
|
||||
// Got Foo(a, b) want Foo(matcherA, matcherB, matcherC, matcherD)
|
||||
// Got Foo(a, b, c) want Foo(matcherA, matcherB, matcherC, matcherD)
|
||||
// Got Foo(a, b, c, d) want Foo(matcherA, matcherB, matcherC, matcherD, matcherE)
|
||||
// Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, matcherC, matcherD)
|
||||
// Got Foo(a, b, c) want Foo(matcherA, matcherB)
|
||||
return fmt.Errorf("Expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v",
|
||||
c.origin, strconv.Itoa(i), args[i:], c.args[i])
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Check that all prerequisite calls have been satisfied.
|
||||
for _, preReqCall := range c.preReqs {
|
||||
if !preReqCall.satisfied() {
|
||||
return fmt.Errorf("Expected call at %s doesn't have a prerequisite call satisfied:\n%v\nshould be called before:\n%v",
|
||||
c.origin, preReqCall, c)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the call is not exhausted.
|
||||
if c.exhausted() {
|
||||
return fmt.Errorf("Expected call at %s has already been called the max number of times.", c.origin)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dropPrereqs tells the expected Call to not re-check prerequisite calls any
|
||||
// longer, and to return its current set.
|
||||
func (c *Call) dropPrereqs() (preReqs []*Call) {
|
||||
preReqs = c.preReqs
|
||||
c.preReqs = nil
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Call) call(args []interface{}) []func([]interface{}) []interface{} {
|
||||
c.numCalls++
|
||||
return c.actions
|
||||
}
|
||||
|
||||
// InOrder declares that the given calls should occur in order.
|
||||
func InOrder(calls ...*Call) {
|
||||
for i := 1; i < len(calls); i++ {
|
||||
calls[i].After(calls[i-1])
|
||||
}
|
||||
}
|
||||
|
||||
func setSlice(arg interface{}, v reflect.Value) {
|
||||
va := reflect.ValueOf(arg)
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
va.Index(i).Set(v.Index(i))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Call) addAction(action func([]interface{}) []interface{}) {
|
||||
c.actions = append(c.actions, action)
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// Copyright 2011 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gomock
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// callSet represents a set of expected calls, indexed by receiver and method
|
||||
// name.
|
||||
type callSet struct {
|
||||
// Calls that are still expected.
|
||||
expected map[callSetKey][]*Call
|
||||
// Calls that have been exhausted.
|
||||
exhausted map[callSetKey][]*Call
|
||||
}
|
||||
|
||||
// callSetKey is the key in the maps in callSet
|
||||
type callSetKey struct {
|
||||
receiver interface{}
|
||||
fname string
|
||||
}
|
||||
|
||||
func newCallSet() *callSet {
|
||||
return &callSet{make(map[callSetKey][]*Call), make(map[callSetKey][]*Call)}
|
||||
}
|
||||
|
||||
// Add adds a new expected call.
|
||||
func (cs callSet) Add(call *Call) {
|
||||
key := callSetKey{call.receiver, call.method}
|
||||
m := cs.expected
|
||||
if call.exhausted() {
|
||||
m = cs.exhausted
|
||||
}
|
||||
m[key] = append(m[key], call)
|
||||
}
|
||||
|
||||
// Remove removes an expected call.
|
||||
func (cs callSet) Remove(call *Call) {
|
||||
key := callSetKey{call.receiver, call.method}
|
||||
calls := cs.expected[key]
|
||||
for i, c := range calls {
|
||||
if c == call {
|
||||
// maintain order for remaining calls
|
||||
cs.expected[key] = append(calls[:i], calls[i+1:]...)
|
||||
cs.exhausted[key] = append(cs.exhausted[key], call)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FindMatch searches for a matching call. Returns error with explanation message if no call matched.
|
||||
func (cs callSet) FindMatch(receiver interface{}, method string, args []interface{}) (*Call, error) {
|
||||
key := callSetKey{receiver, method}
|
||||
|
||||
// Search through the expected calls.
|
||||
expected := cs.expected[key]
|
||||
var callsErrors bytes.Buffer
|
||||
for _, call := range expected {
|
||||
err := call.matches(args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(&callsErrors, "\n%v", err)
|
||||
} else {
|
||||
return call, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we haven't found a match then search through the exhausted calls so we
|
||||
// get useful error messages.
|
||||
exhausted := cs.exhausted[key]
|
||||
for _, call := range exhausted {
|
||||
if err := call.matches(args); err != nil {
|
||||
fmt.Fprintf(&callsErrors, "\n%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(expected)+len(exhausted) == 0 {
|
||||
fmt.Fprintf(&callsErrors, "there are no expected calls of the method %q for that receiver", method)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf(callsErrors.String())
|
||||
}
|
||||
|
||||
// Failures returns the calls that are not satisfied.
|
||||
func (cs callSet) Failures() []*Call {
|
||||
failures := make([]*Call, 0, len(cs.expected))
|
||||
for _, calls := range cs.expected {
|
||||
for _, call := range calls {
|
||||
if !call.satisfied() {
|
||||
failures = append(failures, call)
|
||||
}
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
// Copyright 2010 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// GoMock - a mock framework for Go.
|
||||
//
|
||||
// Standard usage:
|
||||
// (1) Define an interface that you wish to mock.
|
||||
// type MyInterface interface {
|
||||
// SomeMethod(x int64, y string)
|
||||
// }
|
||||
// (2) Use mockgen to generate a mock from the interface.
|
||||
// (3) Use the mock in a test:
|
||||
// func TestMyThing(t *testing.T) {
|
||||
// mockCtrl := gomock.NewController(t)
|
||||
// defer mockCtrl.Finish()
|
||||
//
|
||||
// mockObj := something.NewMockMyInterface(mockCtrl)
|
||||
// mockObj.EXPECT().SomeMethod(4, "blah")
|
||||
// // pass mockObj to a real object and play with it.
|
||||
// }
|
||||
//
|
||||
// By default, expected calls are not enforced to run in any particular order.
|
||||
// Call order dependency can be enforced by use of InOrder and/or Call.After.
|
||||
// Call.After can create more varied call order dependencies, but InOrder is
|
||||
// often more convenient.
|
||||
//
|
||||
// The following examples create equivalent call order dependencies.
|
||||
//
|
||||
// Example of using Call.After to chain expected call order:
|
||||
//
|
||||
// firstCall := mockObj.EXPECT().SomeMethod(1, "first")
|
||||
// secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall)
|
||||
// mockObj.EXPECT().SomeMethod(3, "third").After(secondCall)
|
||||
//
|
||||
// Example of using InOrder to declare expected call order:
|
||||
//
|
||||
// gomock.InOrder(
|
||||
// mockObj.EXPECT().SomeMethod(1, "first"),
|
||||
// mockObj.EXPECT().SomeMethod(2, "second"),
|
||||
// mockObj.EXPECT().SomeMethod(3, "third"),
|
||||
// )
|
||||
//
|
||||
// TODO:
|
||||
// - Handle different argument/return types (e.g. ..., chan, map, interface).
|
||||
package gomock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A TestReporter is something that can be used to report test failures.
|
||||
// It is satisfied by the standard library's *testing.T.
|
||||
type TestReporter interface {
|
||||
Errorf(format string, args ...interface{})
|
||||
Fatalf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
// TestHelper is a TestReporter that has the Helper method. It is satisfied
|
||||
// by the standard library's *testing.T.
|
||||
type TestHelper interface {
|
||||
TestReporter
|
||||
Helper()
|
||||
}
|
||||
|
||||
// A Controller represents the top-level control of a mock ecosystem.
|
||||
// It defines the scope and lifetime of mock objects, as well as their expectations.
|
||||
// It is safe to call Controller's methods from multiple goroutines.
|
||||
type Controller struct {
|
||||
// T should only be called within a generated mock. It is not intended to
|
||||
// be used in user code and may be changed in future versions. T is the
|
||||
// TestReporter passed in when creating the Controller via NewController.
|
||||
// If the TestReporter does not implment a TestHelper it will be wrapped
|
||||
// with a nopTestHelper.
|
||||
T TestHelper
|
||||
mu sync.Mutex
|
||||
expectedCalls *callSet
|
||||
finished bool
|
||||
}
|
||||
|
||||
func NewController(t TestReporter) *Controller {
|
||||
h, ok := t.(TestHelper)
|
||||
if !ok {
|
||||
h = nopTestHelper{t}
|
||||
}
|
||||
|
||||
return &Controller{
|
||||
T: h,
|
||||
expectedCalls: newCallSet(),
|
||||
}
|
||||
}
|
||||
|
||||
type cancelReporter struct {
|
||||
TestHelper
|
||||
cancel func()
|
||||
}
|
||||
|
||||
func (r *cancelReporter) Errorf(format string, args ...interface{}) {
|
||||
r.TestHelper.Errorf(format, args...)
|
||||
}
|
||||
func (r *cancelReporter) Fatalf(format string, args ...interface{}) {
|
||||
defer r.cancel()
|
||||
r.TestHelper.Fatalf(format, args...)
|
||||
}
|
||||
|
||||
// WithContext returns a new Controller and a Context, which is cancelled on any
|
||||
// fatal failure.
|
||||
func WithContext(ctx context.Context, t TestReporter) (*Controller, context.Context) {
|
||||
h, ok := t.(TestHelper)
|
||||
if !ok {
|
||||
h = nopTestHelper{t}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return NewController(&cancelReporter{h, cancel}), ctx
|
||||
}
|
||||
|
||||
type nopTestHelper struct {
|
||||
TestReporter
|
||||
}
|
||||
|
||||
func (h nopTestHelper) Helper() {}
|
||||
|
||||
func (ctrl *Controller) RecordCall(receiver interface{}, method string, args ...interface{}) *Call {
|
||||
ctrl.T.Helper()
|
||||
|
||||
recv := reflect.ValueOf(receiver)
|
||||
for i := 0; i < recv.Type().NumMethod(); i++ {
|
||||
if recv.Type().Method(i).Name == method {
|
||||
return ctrl.RecordCallWithMethodType(receiver, method, recv.Method(i).Type(), args...)
|
||||
}
|
||||
}
|
||||
ctrl.T.Fatalf("gomock: failed finding method %s on %T", method, receiver)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (ctrl *Controller) RecordCallWithMethodType(receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call {
|
||||
ctrl.T.Helper()
|
||||
|
||||
call := newCall(ctrl.T, receiver, method, methodType, args...)
|
||||
|
||||
ctrl.mu.Lock()
|
||||
defer ctrl.mu.Unlock()
|
||||
ctrl.expectedCalls.Add(call)
|
||||
|
||||
return call
|
||||
}
|
||||
|
||||
func (ctrl *Controller) Call(receiver interface{}, method string, args ...interface{}) []interface{} {
|
||||
ctrl.T.Helper()
|
||||
|
||||
// Nest this code so we can use defer to make sure the lock is released.
|
||||
actions := func() []func([]interface{}) []interface{} {
|
||||
ctrl.T.Helper()
|
||||
ctrl.mu.Lock()
|
||||
defer ctrl.mu.Unlock()
|
||||
|
||||
expected, err := ctrl.expectedCalls.FindMatch(receiver, method, args)
|
||||
if err != nil {
|
||||
origin := callerInfo(2)
|
||||
ctrl.T.Fatalf("Unexpected call to %T.%v(%v) at %s because: %s", receiver, method, args, origin, err)
|
||||
}
|
||||
|
||||
// Two things happen here:
|
||||
// * the matching call no longer needs to check prerequite calls,
|
||||
// * and the prerequite calls are no longer expected, so remove them.
|
||||
preReqCalls := expected.dropPrereqs()
|
||||
for _, preReqCall := range preReqCalls {
|
||||
ctrl.expectedCalls.Remove(preReqCall)
|
||||
}
|
||||
|
||||
actions := expected.call(args)
|
||||
if expected.exhausted() {
|
||||
ctrl.expectedCalls.Remove(expected)
|
||||
}
|
||||
return actions
|
||||
}()
|
||||
|
||||
var rets []interface{}
|
||||
for _, action := range actions {
|
||||
if r := action(args); r != nil {
|
||||
rets = r
|
||||
}
|
||||
}
|
||||
|
||||
return rets
|
||||
}
|
||||
|
||||
func (ctrl *Controller) Finish() {
|
||||
ctrl.T.Helper()
|
||||
|
||||
ctrl.mu.Lock()
|
||||
defer ctrl.mu.Unlock()
|
||||
|
||||
if ctrl.finished {
|
||||
ctrl.T.Fatalf("Controller.Finish was called more than once. It has to be called exactly once.")
|
||||
}
|
||||
ctrl.finished = true
|
||||
|
||||
// If we're currently panicking, probably because this is a deferred call,
|
||||
// pass through the panic.
|
||||
if err := recover(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Check that all remaining expected calls are satisfied.
|
||||
failures := ctrl.expectedCalls.Failures()
|
||||
for _, call := range failures {
|
||||
ctrl.T.Errorf("missing call(s) to %v", call)
|
||||
}
|
||||
if len(failures) != 0 {
|
||||
ctrl.T.Fatalf("aborting test due to missing call(s)")
|
||||
}
|
||||
}
|
||||
|
||||
func callerInfo(skip int) string {
|
||||
if _, file, line, ok := runtime.Caller(skip + 1); ok {
|
||||
return fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
return "unknown file"
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
// Copyright 2010 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gomock
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// A Matcher is a representation of a class of values.
|
||||
// It is used to represent the valid or expected arguments to a mocked method.
|
||||
type Matcher interface {
|
||||
// Matches returns whether x is a match.
|
||||
Matches(x interface{}) bool
|
||||
|
||||
// String describes what the matcher matches.
|
||||
String() string
|
||||
}
|
||||
|
||||
type anyMatcher struct{}
|
||||
|
||||
func (anyMatcher) Matches(x interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (anyMatcher) String() string {
|
||||
return "is anything"
|
||||
}
|
||||
|
||||
type eqMatcher struct {
|
||||
x interface{}
|
||||
}
|
||||
|
||||
func (e eqMatcher) Matches(x interface{}) bool {
|
||||
return reflect.DeepEqual(e.x, x)
|
||||
}
|
||||
|
||||
func (e eqMatcher) String() string {
|
||||
return fmt.Sprintf("is equal to %v", e.x)
|
||||
}
|
||||
|
||||
type nilMatcher struct{}
|
||||
|
||||
func (nilMatcher) Matches(x interface{}) bool {
|
||||
if x == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(x)
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
|
||||
reflect.Ptr, reflect.Slice:
|
||||
return v.IsNil()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (nilMatcher) String() string {
|
||||
return "is nil"
|
||||
}
|
||||
|
||||
type notMatcher struct {
|
||||
m Matcher
|
||||
}
|
||||
|
||||
func (n notMatcher) Matches(x interface{}) bool {
|
||||
return !n.m.Matches(x)
|
||||
}
|
||||
|
||||
func (n notMatcher) String() string {
|
||||
// TODO: Improve this if we add a NotString method to the Matcher interface.
|
||||
return "not(" + n.m.String() + ")"
|
||||
}
|
||||
|
||||
type assignableToTypeOfMatcher struct {
|
||||
targetType reflect.Type
|
||||
}
|
||||
|
||||
func (m assignableToTypeOfMatcher) Matches(x interface{}) bool {
|
||||
return reflect.TypeOf(x).AssignableTo(m.targetType)
|
||||
}
|
||||
|
||||
func (m assignableToTypeOfMatcher) String() string {
|
||||
return "is assignable to " + m.targetType.Name()
|
||||
}
|
||||
|
||||
// Constructors
|
||||
func Any() Matcher { return anyMatcher{} }
|
||||
func Eq(x interface{}) Matcher { return eqMatcher{x} }
|
||||
func Nil() Matcher { return nilMatcher{} }
|
||||
func Not(x interface{}) Matcher {
|
||||
if m, ok := x.(Matcher); ok {
|
||||
return notMatcher{m}
|
||||
}
|
||||
return notMatcher{Eq(x)}
|
||||
}
|
||||
|
||||
// AssignableToTypeOf is a Matcher that matches if the parameter to the mock
|
||||
// function is assignable to the type of the parameter to this function.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// dbMock.EXPECT().
|
||||
// Insert(gomock.AssignableToTypeOf(&EmployeeRecord{})).
|
||||
// Return(errors.New("DB error"))
|
||||
//
|
||||
func AssignableToTypeOf(x interface{}) Matcher {
|
||||
return assignableToTypeOfMatcher{reflect.TypeOf(x)}
|
||||
}
|
||||
+1
-1
@@ -48,7 +48,7 @@ func main() {
|
||||
// We can use the Contains helpers to check if an error contains
|
||||
// another error. It is safe to do this with a nil error, or with
|
||||
// an error that doesn't even use the errwrap package.
|
||||
if errwrap.Contains(err, ErrNotExist) {
|
||||
if errwrap.Contains(err, "does not exist") {
|
||||
// Do something
|
||||
}
|
||||
if errwrap.ContainsType(err, new(os.PathError)) {
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
module github.com/hashicorp/errwrap
|
||||
+12
-18
@@ -40,35 +40,31 @@ func (c *Cache) Purge() {
|
||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
||||
func (c *Cache) Add(key, value interface{}) (evicted bool) {
|
||||
c.lock.Lock()
|
||||
evicted = c.lru.Add(key, value)
|
||||
c.lock.Unlock()
|
||||
return evicted
|
||||
defer c.lock.Unlock()
|
||||
return c.lru.Add(key, value)
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *Cache) Get(key interface{}) (value interface{}, ok bool) {
|
||||
c.lock.Lock()
|
||||
value, ok = c.lru.Get(key)
|
||||
c.lock.Unlock()
|
||||
return value, ok
|
||||
defer c.lock.Unlock()
|
||||
return c.lru.Get(key)
|
||||
}
|
||||
|
||||
// Contains checks if a key is in the cache, without updating the
|
||||
// recent-ness or deleting it for being stale.
|
||||
func (c *Cache) Contains(key interface{}) bool {
|
||||
c.lock.RLock()
|
||||
containKey := c.lru.Contains(key)
|
||||
c.lock.RUnlock()
|
||||
return containKey
|
||||
defer c.lock.RUnlock()
|
||||
return c.lru.Contains(key)
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating
|
||||
// the "recently used"-ness of the key.
|
||||
func (c *Cache) Peek(key interface{}) (value interface{}, ok bool) {
|
||||
c.lock.RLock()
|
||||
value, ok = c.lru.Peek(key)
|
||||
c.lock.RUnlock()
|
||||
return value, ok
|
||||
defer c.lock.RUnlock()
|
||||
return c.lru.Peek(key)
|
||||
}
|
||||
|
||||
// ContainsOrAdd checks if a key is in the cache without updating the
|
||||
@@ -102,15 +98,13 @@ func (c *Cache) RemoveOldest() {
|
||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
||||
func (c *Cache) Keys() []interface{} {
|
||||
c.lock.RLock()
|
||||
keys := c.lru.Keys()
|
||||
c.lock.RUnlock()
|
||||
return keys
|
||||
defer c.lock.RUnlock()
|
||||
return c.lru.Keys()
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *Cache) Len() int {
|
||||
c.lock.RLock()
|
||||
length := c.lru.Len()
|
||||
c.lock.RUnlock()
|
||||
return length
|
||||
defer c.lock.RUnlock()
|
||||
return c.lru.Len()
|
||||
}
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
module github.com/shurcooL/sanitized_anchor_name
|
||||
+9
-15
@@ -6,6 +6,7 @@
|
||||
package rate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
@@ -212,19 +213,8 @@ func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation {
|
||||
return &r
|
||||
}
|
||||
|
||||
// contextContext is a temporary(?) copy of the context.Context type
|
||||
// to support both Go 1.6 using golang.org/x/net/context and Go 1.7+
|
||||
// with the built-in context package. If people ever stop using Go 1.6
|
||||
// we can remove this.
|
||||
type contextContext interface {
|
||||
Deadline() (deadline time.Time, ok bool)
|
||||
Done() <-chan struct{}
|
||||
Err() error
|
||||
Value(key interface{}) interface{}
|
||||
}
|
||||
|
||||
// Wait is shorthand for WaitN(ctx, 1).
|
||||
func (lim *Limiter) wait(ctx contextContext) (err error) {
|
||||
func (lim *Limiter) Wait(ctx context.Context) (err error) {
|
||||
return lim.WaitN(ctx, 1)
|
||||
}
|
||||
|
||||
@@ -232,7 +222,7 @@ func (lim *Limiter) wait(ctx contextContext) (err error) {
|
||||
// It returns an error if n exceeds the Limiter's burst size, the Context is
|
||||
// canceled, or the expected wait time exceeds the Context's Deadline.
|
||||
// The burst limit is ignored if the rate limit is Inf.
|
||||
func (lim *Limiter) waitN(ctx contextContext, n int) (err error) {
|
||||
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) {
|
||||
if n > lim.burst && lim.limit != Inf {
|
||||
return fmt.Errorf("rate: Wait(n=%d) exceeds limiter's burst %d", n, lim.burst)
|
||||
}
|
||||
@@ -253,8 +243,12 @@ func (lim *Limiter) waitN(ctx contextContext, n int) (err error) {
|
||||
if !r.ok {
|
||||
return fmt.Errorf("rate: Wait(n=%d) would exceed context deadline", n)
|
||||
}
|
||||
// Wait
|
||||
t := time.NewTimer(r.DelayFrom(now))
|
||||
// Wait if necessary
|
||||
delay := r.DelayFrom(now)
|
||||
if delay == 0 {
|
||||
return nil
|
||||
}
|
||||
t := time.NewTimer(delay)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case <-t.C:
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
// Copyright 2017 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.7
|
||||
|
||||
package rate
|
||||
|
||||
import "golang.org/x/net/context"
|
||||
|
||||
// Wait is shorthand for WaitN(ctx, 1).
|
||||
func (lim *Limiter) Wait(ctx context.Context) (err error) {
|
||||
return lim.waitN(ctx, 1)
|
||||
}
|
||||
|
||||
// WaitN blocks until lim permits n events to happen.
|
||||
// It returns an error if n exceeds the Limiter's burst size, the Context is
|
||||
// canceled, or the expected wait time exceeds the Context's Deadline.
|
||||
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) {
|
||||
return lim.waitN(ctx, n)
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
// Copyright 2017 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.7
|
||||
|
||||
package rate
|
||||
|
||||
import "context"
|
||||
|
||||
// Wait is shorthand for WaitN(ctx, 1).
|
||||
func (lim *Limiter) Wait(ctx context.Context) (err error) {
|
||||
return lim.waitN(ctx, 1)
|
||||
}
|
||||
|
||||
// WaitN blocks until lim permits n events to happen.
|
||||
// It returns an error if n exceeds the Limiter's burst size, the Context is
|
||||
// canceled, or the expected wait time exceeds the Context's Deadline.
|
||||
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) {
|
||||
return lim.waitN(ctx, n)
|
||||
}
|
||||
Vendored
+8
-6
@@ -23,7 +23,7 @@ github.com/globalsign/mgo/bson
|
||||
github.com/globalsign/mgo/internal/sasl
|
||||
github.com/globalsign/mgo/internal/scram
|
||||
github.com/globalsign/mgo/internal/json
|
||||
# github.com/go-chi/chi v3.3.2+incompatible
|
||||
# github.com/go-chi/chi v4.0.2+incompatible
|
||||
github.com/go-chi/chi
|
||||
github.com/go-chi/chi/middleware
|
||||
# github.com/go-chi/cors v1.0.0
|
||||
@@ -39,7 +39,7 @@ github.com/go-pkgz/auth/logger
|
||||
github.com/go-pkgz/auth/middleware
|
||||
# github.com/go-pkgz/lcw v0.2.0
|
||||
github.com/go-pkgz/lcw
|
||||
# github.com/go-pkgz/lgr v0.4.0
|
||||
# github.com/go-pkgz/lgr v0.6.2
|
||||
github.com/go-pkgz/lgr
|
||||
# github.com/go-pkgz/mongo v1.1.2
|
||||
github.com/go-pkgz/mongo
|
||||
@@ -52,17 +52,19 @@ github.com/go-pkgz/rest
|
||||
github.com/go-pkgz/rest/logger
|
||||
# github.com/go-pkgz/syncs v1.1.0
|
||||
github.com/go-pkgz/syncs
|
||||
# github.com/golang/mock v1.2.0
|
||||
github.com/golang/mock/gomock
|
||||
# github.com/golang/protobuf v1.2.0
|
||||
github.com/golang/protobuf/proto
|
||||
# github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c
|
||||
github.com/google/uuid
|
||||
# github.com/gorilla/feeds v1.1.0
|
||||
github.com/gorilla/feeds
|
||||
# github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce
|
||||
# github.com/hashicorp/errwrap v1.0.0
|
||||
github.com/hashicorp/errwrap
|
||||
# github.com/hashicorp/go-multierror v0.0.0-20171204182908-b7773ae21874
|
||||
github.com/hashicorp/go-multierror
|
||||
# github.com/hashicorp/golang-lru v0.5.1
|
||||
# github.com/hashicorp/golang-lru v0.5.0
|
||||
github.com/hashicorp/golang-lru
|
||||
github.com/hashicorp/golang-lru/simplelru
|
||||
# github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc
|
||||
@@ -79,7 +81,7 @@ github.com/pkg/errors
|
||||
github.com/pmezard/go-difflib/difflib
|
||||
# github.com/rakyll/statik v0.1.3
|
||||
github.com/rakyll/statik/fs
|
||||
# github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95
|
||||
# github.com/shurcooL/sanitized_anchor_name v1.0.0
|
||||
github.com/shurcooL/sanitized_anchor_name
|
||||
# github.com/stretchr/testify v1.3.0
|
||||
github.com/stretchr/testify/assert
|
||||
@@ -106,7 +108,7 @@ golang.org/x/oauth2/jws
|
||||
golang.org/x/oauth2/jwt
|
||||
# golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb
|
||||
golang.org/x/sys/unix
|
||||
# golang.org/x/time v0.0.0-20170927054726-6dc17368e09b
|
||||
# golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
|
||||
golang.org/x/time/rate
|
||||
# google.golang.org/appengine v1.4.0
|
||||
google.golang.org/appengine
|
||||
|
||||
Reference in New Issue
Block a user