Selective export (#770)

This commit is contained in:
WithoutPants 2020-09-15 17:28:53 +10:00 committed by GitHub
parent 03f5e1a442
commit 03d4826c85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
280 changed files with 40619 additions and 13035 deletions

View File

@ -92,6 +92,11 @@ test:
it:
go test -mod=vendor -tags=integration ./...
# generates test mocks
.PHONY: generate-test-mocks
generate-test-mocks:
go run -mod=vendor github.com/vektra/mockery/v2 --dir ./pkg/models --name '.*ReaderWriter' --outpkg mocks --output ./pkg/models/mocks
# installs UI dependencies. Run when first cloning repository, or if UI
# dependencies have changed
.PHONY: pre-ui

12
go.mod
View File

@ -12,7 +12,7 @@ require (
github.com/golang-migrate/migrate/v4 v4.3.1
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.0
github.com/gorilla/websocket v1.4.2
github.com/h2non/filetype v1.0.8
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
@ -24,14 +24,16 @@ require (
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
github.com/sirupsen/logrus v1.4.2
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.4.0
github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.5.1
github.com/tidwall/gjson v1.6.0
github.com/vektah/gqlparser v1.1.2
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2
github.com/vektra/mockery v1.1.2 // indirect
github.com/vektra/mockery/v2 v2.2.1
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
golang.org/x/image v0.0.0-20190802002840-cff245a6509b
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v2 v2.2.4
)
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999

160
go.sum
View File

@ -3,7 +3,18 @@ cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.36.0/go.mod h1:RUoy9p/M4ge0HzT8L+SDZ8jg+Q6fth0CiBuhFJpSV40=
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
@ -12,6 +23,7 @@ github.com/99designs/gqlgen v0.9.0/go.mod h1:HrrG7ic9EgLPsULxsZh/Ti+p0HNWgR3XRuv
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
@ -32,11 +44,16 @@ github.com/antchfx/xpath v1.1.6 h1:6sVh6hB5T6phw1pFpHRQ+C4bd8sNI+O58flqtg7h0R0=
github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/bmatcuk/doublestar/v2 v2.0.1 h1:EFT91DmIMRcrUEcYUW7AqSAwKvNzP5+CoDmNVBbcQOU=
github.com/bmatcuk/doublestar/v2 v2.0.1/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
@ -53,11 +70,14 @@ github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1
github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg=
github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=
@ -100,6 +120,7 @@ github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev
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-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
@ -338,9 +359,11 @@ github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4er
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@ -348,14 +371,18 @@ github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
@ -373,6 +400,8 @@ github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTM
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@ -381,16 +410,31 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
github.com/h2non/filetype v1.0.8 h1:le8gpf+FQA0/DlDABbtisA1KiTS0Xi+YSC/E8yY3Y14=
github.com/h2non/filetype v1.0.8/go.mod h1:isekKqOuhMj+s/7r3rIeTErIRy4Rub5uBWHfvMusLMU=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
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-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@ -407,9 +451,11 @@ github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
@ -438,6 +484,8 @@ github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8=
github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
@ -463,6 +511,7 @@ github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kN
github.com/markbates/sigtx v1.0.0/go.mod h1:QF1Hv6Ic6Ca6W+T+DL0Y/ypborFKyvUY9HmuCD4VeTc=
github.com/markbates/willie v1.0.9/go.mod h1:fsrFVWl91+gXpx/6dv715j7i11fYPfZ9ZGfH0DQzY7w=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@ -473,15 +522,27 @@ github.com/mattn/go-sqlite3 v1.13.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mongodb/mongo-go-driver v0.3.0/go.mod h1:NK/HWDIIZkaYsnYa0hmtP443T5ELr0KDecmIioVuuyU=
github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba/go.mod h1:RKgILGEJq24YyJ2ban8EO0RUVSJlF1pGsEvoLEACr/Q=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@ -503,6 +564,7 @@ github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKw
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@ -511,6 +573,7 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
@ -534,10 +597,18 @@ github.com/rogpeppe/go-internal v1.1.0 h1:g0fH8RicVgNl+zVZDCDfbdWxAWoAEJyI7I3TZY
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2 h1:J7U/N7eRtzjhs26d6GqMh2HBuXP8/Z64Densiiieafo=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8=
github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@ -566,6 +637,7 @@ github.com/shurcooL/octicon v0.0.0-20180602230221-c42b0e3b24d9/go.mod h1:eWdoE5J
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
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/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
@ -578,6 +650,8 @@ github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
@ -590,6 +664,8 @@ github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@ -599,6 +675,8 @@ github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaN
github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
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=
@ -608,6 +686,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
@ -627,16 +707,23 @@ github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWp
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/vektra/mockery v1.1.2 h1:uc0Yn67rJpjt8U/mAZimdCKn9AeA97BOkjpmtBSlfP4=
github.com/vektra/mockery v1.1.2/go.mod h1:VcfZjKaFOPO+MpN4ZvwPjs4c48lkq1o3Ym8yHZJu0jU=
github.com/vektra/mockery/v2 v2.2.1 h1:EYgPvxyYkm/0JKs62qlVc9pO+ljb8biPbDWabk5/PmI=
github.com/vektra/mockery/v2 v2.2.1/go.mod h1:rBZUbbhMbiSX1WlCGsOgAi6xjuJGxB7KKbnoL0XNYW8=
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
@ -650,6 +737,7 @@ golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20181024171144-74cb1d3d52f4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181025113841-85e1b3f9139a/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -662,17 +750,37 @@ golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2 h1:FNSSV4jv1PrPsiM2iKGpqLPPgYACqh9Muav7Pollk1k=
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -688,6 +796,7 @@ golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
@ -697,8 +806,13 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd h1:QPwSajcTUrFriMF1nJ3XzgoqakqQEsnZf9LdXdi2nkI=
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
@ -709,6 +823,7 @@ golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAG
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -717,7 +832,9 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -742,10 +859,15 @@ golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0=
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190426135247-a129542de9ae h1:mQLHiymj/JXKnnjc62tb7nD5pZLs940/sXJu+Xp3DBA=
golang.org/x/sys v0.0.0-20190426135247-a129542de9ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -794,16 +916,39 @@ golang.org/x/tools v0.0.0-20190219185102-9394956cfdc5/go.mod h1:E6PF97AdD6v0s+fP
golang.org/x/tools v0.0.0-20190221204921-83362c3779f5/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425222832-ad9eeb80039a/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200323144430-8dcfad9e016e h1:ssd5ulOvVWlh4kDSUF2SqzmMeWfjmwDXM+uGw/aQjRE=
golang.org/x/tools v0.0.0-20200323144430-8dcfad9e016e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -811,6 +956,7 @@ google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO50
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -818,13 +964,20 @@ google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
@ -836,6 +989,8 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
@ -843,11 +998,16 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=

View File

@ -6,6 +6,10 @@ mutation MetadataExport {
metadataExport
}
mutation ExportObjects($input: ExportObjectsInput!) {
exportObjects(input: $input)
}
mutation MetadataScan($input: ScanMetadataInput!) {
metadataScan(input: $input)
}

View File

@ -158,6 +158,9 @@ type Mutation {
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult!
"""Returns a link to download the result"""
exportObjects(input: ExportObjectsInput!): String
"""Start an import. Returns the job ID"""
metadataImport: String!
"""Start an export. Returns the job ID"""

View File

@ -50,3 +50,18 @@ type MetadataUpdateStatus {
status: String!
message: String!
}
input ExportObjectTypeInput {
ids: [String!]
all: Boolean
}
input ExportObjectsInput {
scenes: ExportObjectTypeInput
studios: ExportObjectTypeInput
performers: ExportObjectTypeInput
tags: ExportObjectTypeInput
movies: ExportObjectTypeInput
galleries: ExportObjectTypeInput
includeDependencies: Boolean
}

View File

@ -12,4 +12,5 @@ const (
movieKey key = 4
ContextUser key = 5
tagKey key = 6
downloadKey key = 7
)

View File

@ -2,8 +2,10 @@ package api
import (
"context"
"time"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models"
)
@ -22,6 +24,27 @@ func (r *mutationResolver) MetadataExport(ctx context.Context) (string, error) {
return "todo", nil
}
func (r *mutationResolver) ExportObjects(ctx context.Context, input models.ExportObjectsInput) (*string, error) {
t := manager.CreateExportTask(config.GetVideoFileNamingAlgorithm(), input)
wg, err := manager.GetInstance().RunSingleTask(t)
if err != nil {
return nil, err
}
wg.Wait()
if t.DownloadHash != "" {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
// generate timestamp
suffix := time.Now().Format("20060102-150405")
ret := baseURL + "/downloads/" + t.DownloadHash + "/export" + suffix + ".zip"
return &ret, nil
}
return nil, nil
}
func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) {
manager.GetInstance().Generate(input)
return "todo", nil

View File

@ -0,0 +1,41 @@
package api
import (
"context"
"net/http"
"github.com/go-chi/chi"
"github.com/stashapp/stash/pkg/manager"
)
type downloadsRoutes struct{}
func (rs downloadsRoutes) Routes() chi.Router {
r := chi.NewRouter()
r.Route("/{downloadHash}", func(r chi.Router) {
r.Use(downloadCtx)
r.Get("/{filename}", rs.file)
})
return r
}
func (rs downloadsRoutes) file(w http.ResponseWriter, r *http.Request) {
hash := r.Context().Value(downloadKey).(string)
if hash == "" {
http.Error(w, http.StatusText(404), 404)
return
}
manager.GetInstance().DownloadStore.Serve(hash, w, r)
}
func downloadCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
downloadHash := chi.URLParam(r, "downloadHash")
ctx := context.WithValue(r.Context(), downloadKey, downloadHash)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@ -155,6 +155,7 @@ func Start() {
r.Mount("/studio", studioRoutes{}.Routes())
r.Mount("/movie", movieRoutes{}.Routes())
r.Mount("/tag", tagRoutes{}.Routes())
r.Mount("/downloads", downloadsRoutes{}.Routes())
r.HandleFunc("/css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")

109
pkg/manager/downloads.go Normal file
View File

@ -0,0 +1,109 @@
package manager
import (
"net/http"
"os"
"sync"
"time"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/utils"
)
// DownloadStore manages single-use generated files for the UI to download.
type DownloadStore struct {
m map[string]*storeFile
mutex sync.Mutex
}
type storeFile struct {
path string
contentType string
keep bool
wg sync.WaitGroup
once sync.Once
}
func NewDownloadStore() *DownloadStore {
return &DownloadStore{
m: make(map[string]*storeFile),
}
}
func (s *DownloadStore) RegisterFile(fp string, contentType string, keep bool) string {
const keyLength = 4
const attempts = 100
// keep generating random keys until we get a free one
// prevent infinite loop by only attempting a finite amount of times
var hash string
generate := true
a := 0
s.mutex.Lock()
for generate && a < attempts {
hash = utils.GenerateRandomKey(keyLength)
_, generate = s.m[hash]
a = a + 1
}
s.m[hash] = &storeFile{
path: fp,
contentType: contentType,
keep: keep,
}
s.mutex.Unlock()
return hash
}
func (s *DownloadStore) Serve(hash string, w http.ResponseWriter, r *http.Request) {
s.mutex.Lock()
f, ok := s.m[hash]
if !ok {
s.mutex.Unlock()
http.NotFound(w, r)
return
}
if !f.keep {
s.waitAndRemoveFile(hash, &w, r)
}
s.mutex.Unlock()
if f.contentType != "" {
w.Header().Add("Content-Type", f.contentType)
}
http.ServeFile(w, r, f.path)
}
func (s *DownloadStore) waitAndRemoveFile(hash string, w *http.ResponseWriter, r *http.Request) {
f := s.m[hash]
notify := r.Context().Done()
f.wg.Add(1)
go func() {
<-notify
s.mutex.Lock()
defer s.mutex.Unlock()
f.wg.Done()
}()
go f.once.Do(func() {
// leave it up for 30 seconds after the first request to allow for multiple requests
time.Sleep(30 * time.Second)
f.wg.Wait()
s.mutex.Lock()
defer s.mutex.Unlock()
delete(s.m, hash)
err := os.Remove(f.path)
if err != nil {
logger.Errorf("error removing %s after downloading: %s", f.path, err.Error())
}
})
}

View File

@ -2,62 +2,65 @@ package manager
import (
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/manager/paths"
)
type jsonUtils struct{}
type jsonUtils struct {
json paths.JSONPaths
}
func (jp *jsonUtils) getMappings() (*jsonschema.Mappings, error) {
return jsonschema.LoadMappingsFile(instance.Paths.JSON.MappingsFile)
return jsonschema.LoadMappingsFile(jp.json.MappingsFile)
}
func (jp *jsonUtils) saveMappings(mappings *jsonschema.Mappings) error {
return jsonschema.SaveMappingsFile(instance.Paths.JSON.MappingsFile, mappings)
return jsonschema.SaveMappingsFile(jp.json.MappingsFile, mappings)
}
func (jp *jsonUtils) getScraped() ([]jsonschema.ScrapedItem, error) {
return jsonschema.LoadScrapedFile(instance.Paths.JSON.ScrapedFile)
return jsonschema.LoadScrapedFile(jp.json.ScrapedFile)
}
func (jp *jsonUtils) saveScaped(scraped []jsonschema.ScrapedItem) error {
return jsonschema.SaveScrapedFile(instance.Paths.JSON.ScrapedFile, scraped)
return jsonschema.SaveScrapedFile(jp.json.ScrapedFile, scraped)
}
func (jp *jsonUtils) getPerformer(checksum string) (*jsonschema.Performer, error) {
return jsonschema.LoadPerformerFile(instance.Paths.JSON.PerformerJSONPath(checksum))
return jsonschema.LoadPerformerFile(jp.json.PerformerJSONPath(checksum))
}
func (jp *jsonUtils) savePerformer(checksum string, performer *jsonschema.Performer) error {
return jsonschema.SavePerformerFile(instance.Paths.JSON.PerformerJSONPath(checksum), performer)
return jsonschema.SavePerformerFile(jp.json.PerformerJSONPath(checksum), performer)
}
func (jp *jsonUtils) getStudio(checksum string) (*jsonschema.Studio, error) {
return jsonschema.LoadStudioFile(instance.Paths.JSON.StudioJSONPath(checksum))
return jsonschema.LoadStudioFile(jp.json.StudioJSONPath(checksum))
}
func (jp *jsonUtils) saveStudio(checksum string, studio *jsonschema.Studio) error {
return jsonschema.SaveStudioFile(instance.Paths.JSON.StudioJSONPath(checksum), studio)
return jsonschema.SaveStudioFile(jp.json.StudioJSONPath(checksum), studio)
}
func (jp *jsonUtils) getTag(checksum string) (*jsonschema.Tag, error) {
return jsonschema.LoadTagFile(instance.Paths.JSON.TagJSONPath(checksum))
return jsonschema.LoadTagFile(jp.json.TagJSONPath(checksum))
}
func (jp *jsonUtils) saveTag(checksum string, tag *jsonschema.Tag) error {
return jsonschema.SaveTagFile(instance.Paths.JSON.TagJSONPath(checksum), tag)
return jsonschema.SaveTagFile(jp.json.TagJSONPath(checksum), tag)
}
func (jp *jsonUtils) getMovie(checksum string) (*jsonschema.Movie, error) {
return jsonschema.LoadMovieFile(instance.Paths.JSON.MovieJSONPath(checksum))
return jsonschema.LoadMovieFile(jp.json.MovieJSONPath(checksum))
}
func (jp *jsonUtils) saveMovie(checksum string, movie *jsonschema.Movie) error {
return jsonschema.SaveMovieFile(instance.Paths.JSON.MovieJSONPath(checksum), movie)
return jsonschema.SaveMovieFile(jp.json.MovieJSONPath(checksum), movie)
}
func (jp *jsonUtils) getScene(checksum string) (*jsonschema.Scene, error) {
return jsonschema.LoadSceneFile(instance.Paths.JSON.SceneJSONPath(checksum))
return jsonschema.LoadSceneFile(jp.json.SceneJSONPath(checksum))
}
func (jp *jsonUtils) saveScene(checksum string, scene *jsonschema.Scene) error {
return jsonschema.SaveSceneFile(instance.Paths.JSON.SceneJSONPath(checksum), scene)
return jsonschema.SaveSceneFile(jp.json.SceneJSONPath(checksum), scene)
}

View File

@ -18,13 +18,14 @@ import (
type singleton struct {
Status TaskStatus
Paths *paths.Paths
JSON *jsonUtils
FFMPEGPath string
FFProbePath string
PluginCache *plugin.Cache
ScraperCache *scraper.Cache
DownloadStore *DownloadStore
}
var instance *singleton
@ -51,14 +52,19 @@ func Initialize() *singleton {
instance = &singleton{
Status: TaskStatus{Status: Idle, Progress: -1},
Paths: paths.NewPaths(),
JSON: &jsonUtils{},
PluginCache: initPluginCache(),
ScraperCache: initScraperCache(),
DownloadStore: NewDownloadStore(),
}
instance.RefreshConfig()
// clear the downloads and tmp directories
utils.EmptyDir(instance.Paths.Generated.Downloads)
utils.EmptyDir(instance.Paths.Generated.Tmp)
initFFMPEG()
})
@ -188,12 +194,12 @@ func initScraperCache() *scraper.Cache {
func (s *singleton) RefreshConfig() {
s.Paths = paths.NewPaths()
if config.IsValid() {
_ = utils.EnsureDir(s.Paths.Generated.Screenshots)
_ = utils.EnsureDir(s.Paths.Generated.Vtt)
_ = utils.EnsureDir(s.Paths.Generated.Markers)
_ = utils.EnsureDir(s.Paths.Generated.Transcodes)
paths.EnsureJSONDirs()
utils.EnsureDir(s.Paths.Generated.Screenshots)
utils.EnsureDir(s.Paths.Generated.Vtt)
utils.EnsureDir(s.Paths.Generated.Markers)
utils.EnsureDir(s.Paths.Generated.Transcodes)
utils.EnsureDir(s.Paths.Generated.Downloads)
paths.EnsureJSONDirs(config.GetMetadataPath())
}
}

View File

@ -1,6 +1,7 @@
package manager
import (
"errors"
"path/filepath"
"strconv"
"strings"
@ -170,12 +171,32 @@ func (s *singleton) Export() {
var wg sync.WaitGroup
wg.Add(1)
task := ExportTask{fileNamingAlgorithm: config.GetVideoFileNamingAlgorithm()}
task := ExportTask{full: true, fileNamingAlgorithm: config.GetVideoFileNamingAlgorithm()}
go task.Start(&wg)
wg.Wait()
}()
}
func (s *singleton) RunSingleTask(t Task) (*sync.WaitGroup, error) {
if s.Status.Status != Idle {
return nil, errors.New("task already running")
}
s.Status.SetStatus(t.GetStatus())
s.Status.indefiniteProgress()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer s.returnToIdleState()
go t.Start(&wg)
wg.Wait()
}()
return &wg, nil
}
func setGeneratePreviewOptionsInput(optionsInput *models.GeneratePreviewOptionsInput) {
if optionsInput.PreviewSegments == nil {
val := config.GetPreviewSegments()

View File

@ -1,13 +1,13 @@
package paths
import (
"github.com/stashapp/stash/pkg/utils"
"path/filepath"
"github.com/stashapp/stash/pkg/utils"
)
type Paths struct {
Generated *generatedPaths
JSON *jsonPaths
Gallery *galleryPaths
Scene *scenePaths
@ -17,7 +17,6 @@ type Paths struct {
func NewPaths() *Paths {
p := Paths{}
p.Generated = newGeneratedPaths()
p.JSON = newJSONPaths()
p.Gallery = newGalleryPaths()
p.Scene = newScenePaths(p)

View File

@ -1,9 +1,11 @@
package paths
import (
"io/ioutil"
"path/filepath"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/utils"
"path/filepath"
)
type generatedPaths struct {
@ -11,6 +13,7 @@ type generatedPaths struct {
Vtt string
Markers string
Transcodes string
Downloads string
Tmp string
}
@ -20,6 +23,7 @@ func newGeneratedPaths() *generatedPaths {
gp.Vtt = filepath.Join(config.GetGeneratedPath(), "vtt")
gp.Markers = filepath.Join(config.GetGeneratedPath(), "markers")
gp.Transcodes = filepath.Join(config.GetGeneratedPath(), "transcodes")
gp.Downloads = filepath.Join(config.GetGeneratedPath(), "downloads")
gp.Tmp = filepath.Join(config.GetGeneratedPath(), "tmp")
return &gp
}
@ -29,13 +33,25 @@ func (gp *generatedPaths) GetTmpPath(fileName string) string {
}
func (gp *generatedPaths) EnsureTmpDir() {
_ = utils.EnsureDir(gp.Tmp)
utils.EnsureDir(gp.Tmp)
}
func (gp *generatedPaths) EmptyTmpDir() {
_ = utils.EmptyDir(gp.Tmp)
utils.EmptyDir(gp.Tmp)
}
func (gp *generatedPaths) RemoveTmpDir() {
_ = utils.RemoveDir(gp.Tmp)
utils.RemoveDir(gp.Tmp)
}
func (gp *generatedPaths) TempDir(pattern string) (string, error) {
gp.EnsureTmpDir()
ret, err := ioutil.TempDir(gp.Tmp, pattern)
if err != nil {
return "", err
}
utils.EmptyDir(ret)
return ret, nil
}

View File

@ -3,11 +3,10 @@ package paths
import (
"path/filepath"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/utils"
)
type jsonPaths struct {
type JSONPaths struct {
Metadata string
MappingsFile string
@ -21,27 +20,27 @@ type jsonPaths struct {
Movies string
}
func newJSONPaths() *jsonPaths {
jp := jsonPaths{}
jp.Metadata = config.GetMetadataPath()
jp.MappingsFile = filepath.Join(config.GetMetadataPath(), "mappings.json")
jp.ScrapedFile = filepath.Join(config.GetMetadataPath(), "scraped.json")
jp.Performers = filepath.Join(config.GetMetadataPath(), "performers")
jp.Scenes = filepath.Join(config.GetMetadataPath(), "scenes")
jp.Galleries = filepath.Join(config.GetMetadataPath(), "galleries")
jp.Studios = filepath.Join(config.GetMetadataPath(), "studios")
jp.Movies = filepath.Join(config.GetMetadataPath(), "movies")
jp.Tags = filepath.Join(config.GetMetadataPath(), "tags")
func newJSONPaths(baseDir string) *JSONPaths {
jp := JSONPaths{}
jp.Metadata = baseDir
jp.MappingsFile = filepath.Join(baseDir, "mappings.json")
jp.ScrapedFile = filepath.Join(baseDir, "scraped.json")
jp.Performers = filepath.Join(baseDir, "performers")
jp.Scenes = filepath.Join(baseDir, "scenes")
jp.Galleries = filepath.Join(baseDir, "galleries")
jp.Studios = filepath.Join(baseDir, "studios")
jp.Movies = filepath.Join(baseDir, "movies")
jp.Tags = filepath.Join(baseDir, "tags")
return &jp
}
func GetJSONPaths() *jsonPaths {
jp := newJSONPaths()
func GetJSONPaths(baseDir string) *JSONPaths {
jp := newJSONPaths(baseDir)
return jp
}
func EnsureJSONDirs() {
jsonPaths := GetJSONPaths()
func EnsureJSONDirs(baseDir string) {
jsonPaths := GetJSONPaths(baseDir)
utils.EnsureDir(jsonPaths.Metadata)
utils.EnsureDir(jsonPaths.Scenes)
utils.EnsureDir(jsonPaths.Galleries)
@ -51,22 +50,22 @@ func EnsureJSONDirs() {
utils.EnsureDir(jsonPaths.Tags)
}
func (jp *jsonPaths) PerformerJSONPath(checksum string) string {
func (jp *JSONPaths) PerformerJSONPath(checksum string) string {
return filepath.Join(jp.Performers, checksum+".json")
}
func (jp *jsonPaths) SceneJSONPath(checksum string) string {
func (jp *JSONPaths) SceneJSONPath(checksum string) string {
return filepath.Join(jp.Scenes, checksum+".json")
}
func (jp *jsonPaths) StudioJSONPath(checksum string) string {
func (jp *JSONPaths) StudioJSONPath(checksum string) string {
return filepath.Join(jp.Studios, checksum+".json")
}
func (jp *jsonPaths) TagJSONPath(checksum string) string {
func (jp *JSONPaths) TagJSONPath(checksum string) string {
return filepath.Join(jp.Tags, checksum+".json")
}
func (jp *jsonPaths) MovieJSONPath(checksum string) string {
func (jp *JSONPaths) MovieJSONPath(checksum string) string {
return filepath.Join(jp.Movies, checksum+".json")
}

View File

@ -4,4 +4,5 @@ import "sync"
type Task interface {
Start(wg *sync.WaitGroup)
GetStatus() JobStatus
}

File diff suppressed because it is too large Load Diff

View File

@ -13,11 +13,14 @@ import (
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/manager/paths"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
type ImportTask struct {
json jsonUtils
Mappings *jsonschema.Mappings
Scraped []jsonschema.ScrapedItem
fileNamingAlgorithm models.HashAlgorithm
@ -26,12 +29,18 @@ type ImportTask struct {
func (t *ImportTask) Start(wg *sync.WaitGroup) {
defer wg.Done()
t.Mappings, _ = instance.JSON.getMappings()
baseDir := config.GetMetadataPath()
t.json = jsonUtils{
json: *paths.GetJSONPaths(baseDir),
}
t.Mappings, _ = t.json.getMappings()
if t.Mappings == nil {
logger.Error("missing mappings json")
return
}
scraped, _ := instance.JSON.getScraped()
scraped, _ := t.json.getScraped()
if scraped == nil {
logger.Warn("missing scraped json")
}
@ -62,7 +71,7 @@ func (t *ImportTask) ImportPerformers(ctx context.Context) {
for i, mappingJSON := range t.Mappings.Performers {
index := i + 1
performerJSON, err := instance.JSON.getPerformer(mappingJSON.Checksum)
performerJSON, err := t.json.getPerformer(mappingJSON.Checksum)
if err != nil {
logger.Errorf("[performers] failed to read json: %s", err.Error())
continue
@ -175,7 +184,7 @@ func (t *ImportTask) ImportStudios(ctx context.Context) {
for i, mappingJSON := range t.Mappings.Studios {
index := i + 1
studioJSON, err := instance.JSON.getStudio(mappingJSON.Checksum)
studioJSON, err := t.json.getStudio(mappingJSON.Checksum)
if err != nil {
logger.Errorf("[studios] failed to read json: %s", err.Error())
continue
@ -298,7 +307,7 @@ func (t *ImportTask) ImportMovies(ctx context.Context) {
for i, mappingJSON := range t.Mappings.Movies {
index := i + 1
movieJSON, err := instance.JSON.getMovie(mappingJSON.Checksum)
movieJSON, err := t.json.getMovie(mappingJSON.Checksum)
if err != nil {
logger.Errorf("[movies] failed to read json: %s", err.Error())
continue
@ -431,7 +440,7 @@ func (t *ImportTask) ImportTags(ctx context.Context) {
for i, mappingJSON := range t.Mappings.Tags {
index := i + 1
tagJSON, err := instance.JSON.getTag(mappingJSON.Checksum)
tagJSON, err := t.json.getTag(mappingJSON.Checksum)
if err != nil {
logger.Errorf("[tags] failed to read json: %s", err.Error())
continue
@ -547,7 +556,7 @@ func (t *ImportTask) ImportScenes(ctx context.Context) {
logger.Progressf("[scenes] %d of %d", index, len(t.Mappings.Scenes))
sceneJSON, err := instance.JSON.getScene(mappingJSON.Checksum)
sceneJSON, err := t.json.getScene(mappingJSON.Checksum)
if err != nil {
logger.Infof("[scenes] <%s> json parse failure: %s", mappingJSON.Checksum, err.Error())
continue

53
pkg/models/gallery.go Normal file
View File

@ -0,0 +1,53 @@
package models
import (
"github.com/jmoiron/sqlx"
)
type GalleryReader interface {
// Find(id int) (*Gallery, error)
FindMany(ids []int) ([]*Gallery, error)
// FindByChecksum(checksum string) (*Gallery, error)
// FindByPath(path string) (*Gallery, error)
FindBySceneID(sceneID int) (*Gallery, error)
// ValidGalleriesForScenePath(scenePath string) ([]*Gallery, error)
// Count() (int, error)
All() ([]*Gallery, error)
// Query(galleryFilter *GalleryFilterType, findFilter *FindFilterType) ([]*Gallery, int)
}
type GalleryWriter interface {
// Create(newGallery Gallery) (*Gallery, error)
// Update(updatedGallery Gallery) (*Gallery, error)
// Destroy(id int) error
// ClearGalleryId(sceneID int) error
}
type GalleryReaderWriter interface {
GalleryReader
GalleryWriter
}
func NewGalleryReaderWriter(tx *sqlx.Tx) GalleryReaderWriter {
return &galleryReaderWriter{
tx: tx,
qb: NewGalleryQueryBuilder(),
}
}
type galleryReaderWriter struct {
tx *sqlx.Tx
qb GalleryQueryBuilder
}
func (t *galleryReaderWriter) FindMany(ids []int) ([]*Gallery, error) {
return t.qb.FindMany(ids)
}
func (t *galleryReaderWriter) All() ([]*Gallery, error) {
return t.qb.All()
}
func (t *galleryReaderWriter) FindBySceneID(sceneID int) (*Gallery, error) {
return t.qb.FindBySceneID(sceneID, t.tx)
}

52
pkg/models/join.go Normal file
View File

@ -0,0 +1,52 @@
package models
import (
"github.com/jmoiron/sqlx"
)
type JoinReader interface {
// GetScenePerformers(sceneID int) ([]PerformersScenes, error)
GetSceneMovies(sceneID int) ([]MoviesScenes, error)
// GetSceneTags(sceneID int) ([]ScenesTags, error)
}
type JoinWriter interface {
// CreatePerformersScenes(newJoins []PerformersScenes) error
// AddPerformerScene(sceneID int, performerID int) (bool, error)
// UpdatePerformersScenes(sceneID int, updatedJoins []PerformersScenes) error
// DestroyPerformersScenes(sceneID int) error
// CreateMoviesScenes(newJoins []MoviesScenes) error
// AddMoviesScene(sceneID int, movieID int, sceneIdx *int) (bool, error)
// UpdateMoviesScenes(sceneID int, updatedJoins []MoviesScenes) error
// DestroyMoviesScenes(sceneID int) error
// CreateScenesTags(newJoins []ScenesTags) error
// UpdateScenesTags(sceneID int, updatedJoins []ScenesTags) error
// AddSceneTag(sceneID int, tagID int) (bool, error)
// DestroyScenesTags(sceneID int) error
// CreateSceneMarkersTags(newJoins []SceneMarkersTags) error
// UpdateSceneMarkersTags(sceneMarkerID int, updatedJoins []SceneMarkersTags) error
// DestroySceneMarkersTags(sceneMarkerID int, updatedJoins []SceneMarkersTags) error
// DestroyScenesGalleries(sceneID int) error
// DestroyScenesMarkers(sceneID int) error
}
type JoinReaderWriter interface {
JoinReader
JoinWriter
}
func NewJoinReaderWriter(tx *sqlx.Tx) JoinReaderWriter {
return &joinReaderWriter{
tx: tx,
qb: NewJoinsQueryBuilder(),
}
}
type joinReaderWriter struct {
tx *sqlx.Tx
qb JoinsQueryBuilder
}
func (t *joinReaderWriter) GetSceneMovies(sceneID int) ([]MoviesScenes, error) {
return t.qb.GetSceneMovies(sceneID, t.tx)
}

View File

@ -0,0 +1,82 @@
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
models "github.com/stashapp/stash/pkg/models"
mock "github.com/stretchr/testify/mock"
)
// GalleryReaderWriter is an autogenerated mock type for the GalleryReaderWriter type
type GalleryReaderWriter struct {
mock.Mock
}
// All provides a mock function with given fields:
func (_m *GalleryReaderWriter) All() ([]*models.Gallery, error) {
ret := _m.Called()
var r0 []*models.Gallery
if rf, ok := ret.Get(0).(func() []*models.Gallery); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Gallery)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindBySceneID provides a mock function with given fields: sceneID
func (_m *GalleryReaderWriter) FindBySceneID(sceneID int) (*models.Gallery, error) {
ret := _m.Called(sceneID)
var r0 *models.Gallery
if rf, ok := ret.Get(0).(func(int) *models.Gallery); ok {
r0 = rf(sceneID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Gallery)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(sceneID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindMany provides a mock function with given fields: ids
func (_m *GalleryReaderWriter) FindMany(ids []int) ([]*models.Gallery, error) {
ret := _m.Called(ids)
var r0 []*models.Gallery
if rf, ok := ret.Get(0).(func([]int) []*models.Gallery); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Gallery)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]int) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,36 @@
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
models "github.com/stashapp/stash/pkg/models"
mock "github.com/stretchr/testify/mock"
)
// JoinReaderWriter is an autogenerated mock type for the JoinReaderWriter type
type JoinReaderWriter struct {
mock.Mock
}
// GetSceneMovies provides a mock function with given fields: sceneID
func (_m *JoinReaderWriter) GetSceneMovies(sceneID int) ([]models.MoviesScenes, error) {
ret := _m.Called(sceneID)
var r0 []models.MoviesScenes
if rf, ok := ret.Get(0).(func(int) []models.MoviesScenes); ok {
r0 = rf(sceneID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]models.MoviesScenes)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(sceneID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,128 @@
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
models "github.com/stashapp/stash/pkg/models"
mock "github.com/stretchr/testify/mock"
)
// MovieReaderWriter is an autogenerated mock type for the MovieReaderWriter type
type MovieReaderWriter struct {
mock.Mock
}
// All provides a mock function with given fields:
func (_m *MovieReaderWriter) All() ([]*models.Movie, error) {
ret := _m.Called()
var r0 []*models.Movie
if rf, ok := ret.Get(0).(func() []*models.Movie); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Movie)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Find provides a mock function with given fields: id
func (_m *MovieReaderWriter) Find(id int) (*models.Movie, error) {
ret := _m.Called(id)
var r0 *models.Movie
if rf, ok := ret.Get(0).(func(int) *models.Movie); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Movie)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindMany provides a mock function with given fields: ids
func (_m *MovieReaderWriter) FindMany(ids []int) ([]*models.Movie, error) {
ret := _m.Called(ids)
var r0 []*models.Movie
if rf, ok := ret.Get(0).(func([]int) []*models.Movie); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Movie)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]int) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetBackImage provides a mock function with given fields: movieID
func (_m *MovieReaderWriter) GetBackImage(movieID int) ([]byte, error) {
ret := _m.Called(movieID)
var r0 []byte
if rf, ok := ret.Get(0).(func(int) []byte); ok {
r0 = rf(movieID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(movieID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFrontImage provides a mock function with given fields: movieID
func (_m *MovieReaderWriter) GetFrontImage(movieID int) ([]byte, error) {
ret := _m.Called(movieID)
var r0 []byte
if rf, ok := ret.Get(0).(func(int) []byte); ok {
r0 = rf(movieID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(movieID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,128 @@
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
models "github.com/stashapp/stash/pkg/models"
mock "github.com/stretchr/testify/mock"
)
// PerformerReaderWriter is an autogenerated mock type for the PerformerReaderWriter type
type PerformerReaderWriter struct {
mock.Mock
}
// All provides a mock function with given fields:
func (_m *PerformerReaderWriter) All() ([]*models.Performer, error) {
ret := _m.Called()
var r0 []*models.Performer
if rf, ok := ret.Get(0).(func() []*models.Performer); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Performer)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindBySceneID provides a mock function with given fields: sceneID
func (_m *PerformerReaderWriter) FindBySceneID(sceneID int) ([]*models.Performer, error) {
ret := _m.Called(sceneID)
var r0 []*models.Performer
if rf, ok := ret.Get(0).(func(int) []*models.Performer); ok {
r0 = rf(sceneID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Performer)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(sceneID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindMany provides a mock function with given fields: ids
func (_m *PerformerReaderWriter) FindMany(ids []int) ([]*models.Performer, error) {
ret := _m.Called(ids)
var r0 []*models.Performer
if rf, ok := ret.Get(0).(func([]int) []*models.Performer); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Performer)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]int) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindNamesBySceneID provides a mock function with given fields: sceneID
func (_m *PerformerReaderWriter) FindNamesBySceneID(sceneID int) ([]*models.Performer, error) {
ret := _m.Called(sceneID)
var r0 []*models.Performer
if rf, ok := ret.Get(0).(func(int) []*models.Performer); ok {
r0 = rf(sceneID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Performer)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(sceneID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPerformerImage provides a mock function with given fields: performerID
func (_m *PerformerReaderWriter) GetPerformerImage(performerID int) ([]byte, error) {
ret := _m.Called(performerID)
var r0 []byte
if rf, ok := ret.Get(0).(func(int) []byte); ok {
r0 = rf(performerID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(performerID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,36 @@
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
models "github.com/stashapp/stash/pkg/models"
mock "github.com/stretchr/testify/mock"
)
// SceneMarkerReaderWriter is an autogenerated mock type for the SceneMarkerReaderWriter type
type SceneMarkerReaderWriter struct {
mock.Mock
}
// FindBySceneID provides a mock function with given fields: sceneID
func (_m *SceneMarkerReaderWriter) FindBySceneID(sceneID int) ([]*models.SceneMarker, error) {
ret := _m.Called(sceneID)
var r0 []*models.SceneMarker
if rf, ok := ret.Get(0).(func(int) []*models.SceneMarker); ok {
r0 = rf(sceneID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.SceneMarker)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(sceneID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,82 @@
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
models "github.com/stashapp/stash/pkg/models"
mock "github.com/stretchr/testify/mock"
)
// SceneReaderWriter is an autogenerated mock type for the SceneReaderWriter type
type SceneReaderWriter struct {
mock.Mock
}
// All provides a mock function with given fields:
func (_m *SceneReaderWriter) All() ([]*models.Scene, error) {
ret := _m.Called()
var r0 []*models.Scene
if rf, ok := ret.Get(0).(func() []*models.Scene); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Scene)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindMany provides a mock function with given fields: ids
func (_m *SceneReaderWriter) FindMany(ids []int) ([]*models.Scene, error) {
ret := _m.Called(ids)
var r0 []*models.Scene
if rf, ok := ret.Get(0).(func([]int) []*models.Scene); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Scene)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]int) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSceneCover provides a mock function with given fields: sceneID
func (_m *SceneReaderWriter) GetSceneCover(sceneID int) ([]byte, error) {
ret := _m.Called(sceneID)
var r0 []byte
if rf, ok := ret.Get(0).(func(int) []byte); ok {
r0 = rf(sceneID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(sceneID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,105 @@
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
models "github.com/stashapp/stash/pkg/models"
mock "github.com/stretchr/testify/mock"
)
// StudioReaderWriter is an autogenerated mock type for the StudioReaderWriter type
type StudioReaderWriter struct {
mock.Mock
}
// All provides a mock function with given fields:
func (_m *StudioReaderWriter) All() ([]*models.Studio, error) {
ret := _m.Called()
var r0 []*models.Studio
if rf, ok := ret.Get(0).(func() []*models.Studio); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Studio)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Find provides a mock function with given fields: id
func (_m *StudioReaderWriter) Find(id int) (*models.Studio, error) {
ret := _m.Called(id)
var r0 *models.Studio
if rf, ok := ret.Get(0).(func(int) *models.Studio); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Studio)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindMany provides a mock function with given fields: ids
func (_m *StudioReaderWriter) FindMany(ids []int) ([]*models.Studio, error) {
ret := _m.Called(ids)
var r0 []*models.Studio
if rf, ok := ret.Get(0).(func([]int) []*models.Studio); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Studio)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]int) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetStudioImage provides a mock function with given fields: studioID
func (_m *StudioReaderWriter) GetStudioImage(studioID int) ([]byte, error) {
ret := _m.Called(studioID)
var r0 []byte
if rf, ok := ret.Get(0).(func(int) []byte); ok {
r0 = rf(studioID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(studioID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,151 @@
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
models "github.com/stashapp/stash/pkg/models"
mock "github.com/stretchr/testify/mock"
)
// TagReaderWriter is an autogenerated mock type for the TagReaderWriter type
type TagReaderWriter struct {
mock.Mock
}
// All provides a mock function with given fields:
func (_m *TagReaderWriter) All() ([]*models.Tag, error) {
ret := _m.Called()
var r0 []*models.Tag
if rf, ok := ret.Get(0).(func() []*models.Tag); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Tag)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Find provides a mock function with given fields: id
func (_m *TagReaderWriter) Find(id int) (*models.Tag, error) {
ret := _m.Called(id)
var r0 *models.Tag
if rf, ok := ret.Get(0).(func(int) *models.Tag); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Tag)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindBySceneID provides a mock function with given fields: sceneID
func (_m *TagReaderWriter) FindBySceneID(sceneID int) ([]*models.Tag, error) {
ret := _m.Called(sceneID)
var r0 []*models.Tag
if rf, ok := ret.Get(0).(func(int) []*models.Tag); ok {
r0 = rf(sceneID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Tag)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(sceneID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindBySceneMarkerID provides a mock function with given fields: sceneMarkerID
func (_m *TagReaderWriter) FindBySceneMarkerID(sceneMarkerID int) ([]*models.Tag, error) {
ret := _m.Called(sceneMarkerID)
var r0 []*models.Tag
if rf, ok := ret.Get(0).(func(int) []*models.Tag); ok {
r0 = rf(sceneMarkerID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Tag)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(sceneMarkerID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindMany provides a mock function with given fields: ids
func (_m *TagReaderWriter) FindMany(ids []int) ([]*models.Tag, error) {
ret := _m.Called(ids)
var r0 []*models.Tag
if rf, ok := ret.Get(0).(func([]int) []*models.Tag); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Tag)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]int) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTagImage provides a mock function with given fields: tagID
func (_m *TagReaderWriter) GetTagImage(tagID int) ([]byte, error) {
ret := _m.Called(tagID)
var r0 []byte
if rf, ok := ret.Get(0).(func(int) []byte); ok {
r0 = rf(tagID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(tagID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,17 @@
package modelstest
import "database/sql"
func NullString(v string) sql.NullString {
return sql.NullString{
String: v,
Valid: true,
}
}
func NullInt64(v int64) sql.NullInt64 {
return sql.NullInt64{
Int64: v,
Valid: true,
}
}

63
pkg/models/movie.go Normal file
View File

@ -0,0 +1,63 @@
package models
import (
"github.com/jmoiron/sqlx"
)
type MovieReader interface {
Find(id int) (*Movie, error)
FindMany(ids []int) ([]*Movie, error)
// FindBySceneID(sceneID int) ([]*Movie, error)
// FindByName(name string, nocase bool) (*Movie, error)
// FindByNames(names []string, nocase bool) ([]*Movie, error)
All() ([]*Movie, error)
// AllSlim() ([]*Movie, error)
// Query(movieFilter *MovieFilterType, findFilter *FindFilterType) ([]*Movie, int)
GetFrontImage(movieID int) ([]byte, error)
GetBackImage(movieID int) ([]byte, error)
}
type MovieWriter interface {
// Create(newMovie Movie) (*Movie, error)
// Update(updatedMovie MoviePartial) (*Movie, error)
// Destroy(id string) error
// UpdateMovieImages(movieID int, frontImage []byte, backImage []byte) error
// DestroyMovieImages(movieID int) error
}
type MovieReaderWriter interface {
MovieReader
MovieWriter
}
func NewMovieReaderWriter(tx *sqlx.Tx) MovieReaderWriter {
return &movieReaderWriter{
tx: tx,
qb: NewMovieQueryBuilder(),
}
}
type movieReaderWriter struct {
tx *sqlx.Tx
qb MovieQueryBuilder
}
func (t *movieReaderWriter) Find(id int) (*Movie, error) {
return t.qb.Find(id, t.tx)
}
func (t *movieReaderWriter) FindMany(ids []int) ([]*Movie, error) {
return t.qb.FindMany(ids)
}
func (t *movieReaderWriter) All() ([]*Movie, error) {
return t.qb.All()
}
func (t *movieReaderWriter) GetFrontImage(movieID int) ([]byte, error) {
return t.qb.GetFrontImage(movieID, t.tx)
}
func (t *movieReaderWriter) GetBackImage(movieID int) ([]byte, error) {
return t.qb.GetBackImage(movieID, t.tx)
}

63
pkg/models/performer.go Normal file
View File

@ -0,0 +1,63 @@
package models
import (
"github.com/jmoiron/sqlx"
)
type PerformerReader interface {
// Find(id int) (*Performer, error)
FindMany(ids []int) ([]*Performer, error)
FindBySceneID(sceneID int) ([]*Performer, error)
FindNamesBySceneID(sceneID int) ([]*Performer, error)
// FindByNames(names []string, nocase bool) ([]*Performer, error)
// Count() (int, error)
All() ([]*Performer, error)
// AllSlim() ([]*Performer, error)
// Query(performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]*Performer, int)
GetPerformerImage(performerID int) ([]byte, error)
}
type PerformerWriter interface {
// Create(newPerformer Performer) (*Performer, error)
// Update(updatedPerformer Performer) (*Performer, error)
// Destroy(id string) error
// UpdatePerformerImage(performerID int, image []byte) error
// DestroyPerformerImage(performerID int) error
}
type PerformerReaderWriter interface {
PerformerReader
PerformerWriter
}
func NewPerformerReaderWriter(tx *sqlx.Tx) PerformerReaderWriter {
return &performerReaderWriter{
tx: tx,
qb: NewPerformerQueryBuilder(),
}
}
type performerReaderWriter struct {
tx *sqlx.Tx
qb PerformerQueryBuilder
}
func (t *performerReaderWriter) FindMany(ids []int) ([]*Performer, error) {
return t.qb.FindMany(ids)
}
func (t *performerReaderWriter) All() ([]*Performer, error) {
return t.qb.All()
}
func (t *performerReaderWriter) GetPerformerImage(performerID int) ([]byte, error) {
return t.qb.GetPerformerImage(performerID, t.tx)
}
func (t *performerReaderWriter) FindBySceneID(id int) ([]*Performer, error) {
return t.qb.FindBySceneID(id, t.tx)
}
func (t *performerReaderWriter) FindNamesBySceneID(sceneID int) ([]*Performer, error) {
return t.qb.FindNameBySceneID(sceneID, t.tx)
}

View File

@ -2,6 +2,7 @@ package models
import (
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
@ -71,6 +72,24 @@ func (qb *MovieQueryBuilder) Find(id int, tx *sqlx.Tx) (*Movie, error) {
return qb.queryMovie(query, args, tx)
}
func (qb *MovieQueryBuilder) FindMany(ids []int) ([]*Movie, error) {
var movies []*Movie
for _, id := range ids {
movie, err := qb.Find(id, nil)
if err != nil {
return nil, err
}
if movie == nil {
return nil, fmt.Errorf("movie with id %d not found", id)
}
movies = append(movies, movie)
}
return movies, nil
}
func (qb *MovieQueryBuilder) FindBySceneID(sceneID int, tx *sqlx.Tx) ([]*Movie, error) {
query := `
SELECT movies.* FROM movies

View File

@ -2,6 +2,7 @@ package models
import (
"database/sql"
"fmt"
"strconv"
"time"
@ -76,6 +77,24 @@ func (qb *PerformerQueryBuilder) Find(id int) (*Performer, error) {
return results[0], nil
}
func (qb *PerformerQueryBuilder) FindMany(ids []int) ([]*Performer, error) {
var performers []*Performer
for _, id := range ids {
performer, err := qb.Find(id)
if err != nil {
return nil, err
}
if performer == nil {
return nil, fmt.Errorf("performer with id %d not found", id)
}
performers = append(performers, performer)
}
return performers, nil
}
func (qb *PerformerQueryBuilder) FindBySceneID(sceneID int, tx *sqlx.Tx) ([]*Performer, error) {
query := selectAll("performers") + `
LEFT JOIN performers_scenes as scenes_join on scenes_join.performer_id = performers.id

View File

@ -2,6 +2,7 @@ package models
import (
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
@ -74,6 +75,24 @@ func (qb *StudioQueryBuilder) Find(id int, tx *sqlx.Tx) (*Studio, error) {
return qb.queryStudio(query, args, tx)
}
func (qb *StudioQueryBuilder) FindMany(ids []int) ([]*Studio, error) {
var studios []*Studio
for _, id := range ids {
studio, err := qb.Find(id, nil)
if err != nil {
return nil, err
}
if studio == nil {
return nil, fmt.Errorf("studio with id %d not found", id)
}
studios = append(studios, studio)
}
return studios, nil
}
func (qb *StudioQueryBuilder) FindChildren(id int, tx *sqlx.Tx) ([]*Studio, error) {
query := "SELECT studios.* FROM studios WHERE studios.parent_id = ?"
args := []interface{}{id}

View File

@ -3,6 +3,7 @@ package models
import (
"database/sql"
"errors"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
@ -89,6 +90,24 @@ func (qb *TagQueryBuilder) Find(id int, tx *sqlx.Tx) (*Tag, error) {
return qb.queryTag(query, args, tx)
}
func (qb *TagQueryBuilder) FindMany(ids []int) ([]*Tag, error) {
var tags []*Tag
for _, id := range ids {
tag, err := qb.Find(id, nil)
if err != nil {
return nil, err
}
if tag == nil {
return nil, fmt.Errorf("tag with id %d not found", id)
}
tags = append(tags, tag)
}
return tags, nil
}
func (qb *TagQueryBuilder) FindBySceneID(sceneID int, tx *sqlx.Tx) ([]*Tag, error) {
query := `
SELECT tags.* FROM tags

73
pkg/models/scene.go Normal file
View File

@ -0,0 +1,73 @@
package models
import (
"github.com/jmoiron/sqlx"
)
type SceneReader interface {
// Find(id int) (*Scene, error)
FindMany(ids []int) ([]*Scene, error)
// FindByChecksum(checksum string) (*Scene, error)
// FindByOSHash(oshash string) (*Scene, error)
// FindByPath(path string) (*Scene, error)
// FindByPerformerID(performerID int) ([]*Scene, error)
// CountByPerformerID(performerID int) (int, error)
// FindByStudioID(studioID int) ([]*Scene, error)
// FindByMovieID(movieID int) ([]*Scene, error)
// CountByMovieID(movieID int) (int, error)
// Count() (int, error)
// SizeCount() (string, error)
// CountByStudioID(studioID int) (int, error)
// CountByTagID(tagID int) (int, error)
// CountMissingChecksum() (int, error)
// CountMissingOSHash() (int, error)
// Wall(q *string) ([]*Scene, error)
All() ([]*Scene, error)
// Query(sceneFilter *SceneFilterType, findFilter *FindFilterType) ([]*Scene, int)
// QueryAllByPathRegex(regex string) ([]*Scene, error)
// QueryByPathRegex(findFilter *FindFilterType) ([]*Scene, int)
GetSceneCover(sceneID int) ([]byte, error)
}
type SceneWriter interface {
// Create(newScene Scene) (*Scene, error)
// Update(updatedScene ScenePartial) (*Scene, error)
// IncrementOCounter(id int) (int, error)
// DecrementOCounter(id int) (int, error)
// ResetOCounter(id int) (int, error)
// Destroy(id string) error
// UpdateFormat(id int, format string) error
// UpdateOSHash(id int, oshash string) error
// UpdateChecksum(id int, checksum string) error
// UpdateSceneCover(sceneID int, cover []byte) error
// DestroySceneCover(sceneID int) error
}
type SceneReaderWriter interface {
SceneReader
SceneWriter
}
func NewSceneReaderWriter(tx *sqlx.Tx) SceneReaderWriter {
return &sceneReaderWriter{
tx: tx,
qb: NewSceneQueryBuilder(),
}
}
type sceneReaderWriter struct {
tx *sqlx.Tx
qb SceneQueryBuilder
}
func (t *sceneReaderWriter) FindMany(ids []int) ([]*Scene, error) {
return t.qb.FindMany(ids)
}
func (t *sceneReaderWriter) All() ([]*Scene, error) {
return t.qb.All()
}
func (t *sceneReaderWriter) GetSceneCover(sceneID int) ([]byte, error) {
return t.qb.GetSceneCover(sceneID, t.tx)
}

View File

@ -0,0 +1,42 @@
package models
import (
"github.com/jmoiron/sqlx"
)
type SceneMarkerReader interface {
// Find(id int) (*SceneMarker, error)
// FindMany(ids []int) ([]*SceneMarker, error)
FindBySceneID(sceneID int) ([]*SceneMarker, error)
// CountByTagID(tagID int) (int, error)
// GetMarkerStrings(q *string, sort *string) ([]*MarkerStringsResultType, error)
// Wall(q *string) ([]*SceneMarker, error)
// Query(sceneMarkerFilter *SceneMarkerFilterType, findFilter *FindFilterType) ([]*SceneMarker, int)
}
type SceneMarkerWriter interface {
// Create(newSceneMarker SceneMarker) (*SceneMarker, error)
// Update(updatedSceneMarker SceneMarker) (*SceneMarker, error)
// Destroy(id string) error
}
type SceneMarkerReaderWriter interface {
SceneMarkerReader
SceneMarkerWriter
}
func NewSceneMarkerReaderWriter(tx *sqlx.Tx) SceneMarkerReaderWriter {
return &sceneMarkerReaderWriter{
tx: tx,
qb: NewSceneMarkerQueryBuilder(),
}
}
type sceneMarkerReaderWriter struct {
tx *sqlx.Tx
qb SceneMarkerQueryBuilder
}
func (t *sceneMarkerReaderWriter) FindBySceneID(sceneID int) ([]*SceneMarker, error) {
return t.qb.FindBySceneID(sceneID, t.tx)
}

59
pkg/models/studio.go Normal file
View File

@ -0,0 +1,59 @@
package models
import (
"github.com/jmoiron/sqlx"
)
type StudioReader interface {
Find(id int) (*Studio, error)
FindMany(ids []int) ([]*Studio, error)
// FindChildren(id int) ([]*Studio, error)
// FindBySceneID(sceneID int) (*Studio, error)
// FindByName(name string, nocase bool) (*Studio, error)
// Count() (int, error)
All() ([]*Studio, error)
// AllSlim() ([]*Studio, error)
// Query(studioFilter *StudioFilterType, findFilter *FindFilterType) ([]*Studio, int)
GetStudioImage(studioID int) ([]byte, error)
}
type StudioWriter interface {
// Create(newStudio Studio) (*Studio, error)
// Update(updatedStudio StudioPartial) (*Studio, error)
// Destroy(id string) error
// UpdateStudioImage(studioID int, image []byte) error
// DestroyStudioImage(studioID int) error
}
type StudioReaderWriter interface {
StudioReader
StudioWriter
}
func NewStudioReaderWriter(tx *sqlx.Tx) StudioReaderWriter {
return &studioReaderWriter{
tx: tx,
qb: NewStudioQueryBuilder(),
}
}
type studioReaderWriter struct {
tx *sqlx.Tx
qb StudioQueryBuilder
}
func (t *studioReaderWriter) Find(id int) (*Studio, error) {
return t.qb.Find(id, t.tx)
}
func (t *studioReaderWriter) FindMany(ids []int) ([]*Studio, error) {
return t.qb.FindMany(ids)
}
func (t *studioReaderWriter) All() ([]*Studio, error) {
return t.qb.All()
}
func (t *studioReaderWriter) GetStudioImage(studioID int) ([]byte, error) {
return t.qb.GetStudioImage(studioID, t.tx)
}

68
pkg/models/tag.go Normal file
View File

@ -0,0 +1,68 @@
package models
import (
"github.com/jmoiron/sqlx"
)
type TagReader interface {
Find(id int) (*Tag, error)
FindMany(ids []int) ([]*Tag, error)
FindBySceneID(sceneID int) ([]*Tag, error)
FindBySceneMarkerID(sceneMarkerID int) ([]*Tag, error)
// FindByName(name string, nocase bool) (*Tag, error)
// FindByNames(names []string, nocase bool) ([]*Tag, error)
// Count() (int, error)
All() ([]*Tag, error)
// AllSlim() ([]*Tag, error)
// Query(tagFilter *TagFilterType, findFilter *FindFilterType) ([]*Tag, int, error)
GetTagImage(tagID int) ([]byte, error)
}
type TagWriter interface {
// Create(newTag Tag) (*Tag, error)
// Update(updatedTag Tag) (*Tag, error)
// Destroy(id string) error
// UpdateTagImage(tagID int, image []byte) error
// DestroyTagImage(tagID int) error
}
type TagReaderWriter interface {
TagReader
TagWriter
}
func NewTagReaderWriter(tx *sqlx.Tx) TagReaderWriter {
return &tagReaderWriter{
tx: tx,
qb: NewTagQueryBuilder(),
}
}
type tagReaderWriter struct {
tx *sqlx.Tx
qb TagQueryBuilder
}
func (t *tagReaderWriter) Find(id int) (*Tag, error) {
return t.qb.Find(id, t.tx)
}
func (t *tagReaderWriter) FindMany(ids []int) ([]*Tag, error) {
return t.qb.FindMany(ids)
}
func (t *tagReaderWriter) All() ([]*Tag, error) {
return t.qb.All()
}
func (t *tagReaderWriter) FindBySceneMarkerID(sceneMarkerID int) ([]*Tag, error) {
return t.qb.FindBySceneMarkerID(sceneMarkerID, t.tx)
}
func (t *tagReaderWriter) GetTagImage(tagID int) ([]byte, error) {
return t.qb.GetTagImage(tagID, t.tx)
}
func (t *tagReaderWriter) FindBySceneID(sceneID int) ([]*Tag, error) {
return t.qb.FindBySceneID(sceneID, t.tx)
}

76
pkg/movie/export.go Normal file
View File

@ -0,0 +1,76 @@
package movie
import (
"fmt"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
// ToJSON converts a Movie into its JSON equivalent.
func ToJSON(reader models.MovieReader, studioReader models.StudioReader, movie *models.Movie) (*jsonschema.Movie, error) {
newMovieJSON := jsonschema.Movie{
CreatedAt: models.JSONTime{Time: movie.CreatedAt.Timestamp},
UpdatedAt: models.JSONTime{Time: movie.UpdatedAt.Timestamp},
}
if movie.Name.Valid {
newMovieJSON.Name = movie.Name.String
}
if movie.Aliases.Valid {
newMovieJSON.Aliases = movie.Aliases.String
}
if movie.Date.Valid {
newMovieJSON.Date = utils.GetYMDFromDatabaseDate(movie.Date.String)
}
if movie.Rating.Valid {
newMovieJSON.Rating = int(movie.Rating.Int64)
}
if movie.Duration.Valid {
newMovieJSON.Duration = int(movie.Duration.Int64)
}
if movie.Director.Valid {
newMovieJSON.Director = movie.Director.String
}
if movie.Synopsis.Valid {
newMovieJSON.Synopsis = movie.Synopsis.String
}
if movie.URL.Valid {
newMovieJSON.URL = movie.URL.String
}
if movie.StudioID.Valid {
studio, err := studioReader.Find(int(movie.StudioID.Int64))
if err != nil {
return nil, fmt.Errorf("error getting movie studio: %s", err.Error())
}
if studio != nil {
newMovieJSON.Studio = studio.Name.String
}
}
frontImage, err := reader.GetFrontImage(movie.ID)
if err != nil {
return nil, fmt.Errorf("error getting movie front image: %s", err.Error())
}
if len(frontImage) > 0 {
newMovieJSON.FrontImage = utils.GetBase64StringFromData(frontImage)
}
backImage, err := reader.GetBackImage(movie.ID)
if err != nil {
return nil, fmt.Errorf("error getting movie back image: %s", err.Error())
}
if len(backImage) > 0 {
newMovieJSON.BackImage = utils.GetBase64StringFromData(backImage)
}
return &newMovieJSON, nil
}

222
pkg/movie/export_test.go Normal file
View File

@ -0,0 +1,222 @@
package movie
import (
"database/sql"
"errors"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stashapp/stash/pkg/models/modelstest"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
const (
movieID = 1
emptyID = 2
errFrontImageID = 3
errBackImageID = 4
errStudioMovieID = 5
missingStudioMovieID = 6
)
const (
studioID = 1
missingStudioID = 2
errStudioID = 3
)
const movieName = "testMovie"
const movieAliases = "aliases"
var date = models.SQLiteDate{
String: "2001-01-01",
Valid: true,
}
const rating = 5
const duration = 100
const director = "director"
const synopsis = "synopsis"
const url = "url"
const studioName = "studio"
const frontImage = "ZnJvbnRJbWFnZUJ5dGVz"
const backImage = "YmFja0ltYWdlQnl0ZXM="
var frontImageBytes = []byte("frontImageBytes")
var backImageBytes = []byte("backImageBytes")
var studio models.Studio = models.Studio{
Name: modelstest.NullString(studioName),
}
var createTime time.Time = time.Date(2001, 01, 01, 0, 0, 0, 0, time.UTC)
var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.UTC)
func createFullMovie(id int, studioID int) models.Movie {
return models.Movie{
ID: id,
Name: modelstest.NullString(movieName),
Aliases: modelstest.NullString(movieAliases),
Date: date,
Rating: sql.NullInt64{
Int64: rating,
Valid: true,
},
Duration: sql.NullInt64{
Int64: duration,
Valid: true,
},
Director: modelstest.NullString(director),
Synopsis: modelstest.NullString(synopsis),
URL: modelstest.NullString(url),
StudioID: sql.NullInt64{
Int64: int64(studioID),
Valid: true,
},
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
}
}
func createEmptyMovie(id int) models.Movie {
return models.Movie{
ID: id,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
}
}
func createFullJSONMovie(studio, frontImage, backImage string) *jsonschema.Movie {
return &jsonschema.Movie{
Name: movieName,
Aliases: movieAliases,
Date: date.String,
Rating: rating,
Duration: duration,
Director: director,
Synopsis: synopsis,
URL: url,
Studio: studio,
FrontImage: frontImage,
BackImage: backImage,
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
}
}
func createEmptyJSONMovie() *jsonschema.Movie {
return &jsonschema.Movie{
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
}
}
type testScenario struct {
movie models.Movie
expected *jsonschema.Movie
err bool
}
var scenarios []testScenario
func initTestTable() {
scenarios = []testScenario{
testScenario{
createFullMovie(movieID, studioID),
createFullJSONMovie(studioName, frontImage, backImage),
false,
},
testScenario{
createEmptyMovie(emptyID),
createEmptyJSONMovie(),
false,
},
testScenario{
createFullMovie(errFrontImageID, studioID),
nil,
true,
},
testScenario{
createFullMovie(errBackImageID, studioID),
nil,
true,
},
testScenario{
createFullMovie(errStudioMovieID, errStudioID),
nil,
true,
},
testScenario{
createFullMovie(missingStudioMovieID, missingStudioID),
createFullJSONMovie("", frontImage, backImage),
false,
},
}
}
func TestToJSON(t *testing.T) {
initTestTable()
mockMovieReader := &mocks.MovieReaderWriter{}
imageErr := errors.New("error getting image")
mockMovieReader.On("GetFrontImage", movieID).Return(frontImageBytes, nil).Once()
mockMovieReader.On("GetFrontImage", missingStudioMovieID).Return(frontImageBytes, nil).Once()
mockMovieReader.On("GetFrontImage", emptyID).Return(nil, nil).Once().Maybe()
mockMovieReader.On("GetFrontImage", errFrontImageID).Return(nil, imageErr).Once()
mockMovieReader.On("GetFrontImage", errBackImageID).Return(frontImageBytes, nil).Once()
mockMovieReader.On("GetBackImage", movieID).Return(backImageBytes, nil).Once()
mockMovieReader.On("GetBackImage", missingStudioMovieID).Return(backImageBytes, nil).Once()
mockMovieReader.On("GetBackImage", emptyID).Return(nil, nil).Once()
mockMovieReader.On("GetBackImage", errBackImageID).Return(nil, imageErr).Once()
mockMovieReader.On("GetBackImage", errFrontImageID).Return(backImageBytes, nil).Maybe()
mockMovieReader.On("GetBackImage", errStudioMovieID).Return(backImageBytes, nil).Maybe()
mockStudioReader := &mocks.StudioReaderWriter{}
studioErr := errors.New("error getting studio")
mockStudioReader.On("Find", studioID).Return(&studio, nil)
mockStudioReader.On("Find", missingStudioID).Return(nil, nil)
mockStudioReader.On("Find", errStudioID).Return(nil, studioErr)
for i, s := range scenarios {
movie := s.movie
json, err := ToJSON(mockMovieReader, mockStudioReader, &movie)
if !s.err && err != nil {
t.Errorf("[%d] unexpected error: %s", i, err.Error())
} else if s.err && err == nil {
t.Errorf("[%d] expected error not returned", i)
} else {
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
mockMovieReader.AssertExpectations(t)
mockStudioReader.AssertExpectations(t)
}

100
pkg/performer/export.go Normal file
View File

@ -0,0 +1,100 @@
package performer
import (
"fmt"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
// ToJSON converts a Performer object into its JSON equivalent.
func ToJSON(reader models.PerformerReader, performer *models.Performer) (*jsonschema.Performer, error) {
newPerformerJSON := jsonschema.Performer{
CreatedAt: models.JSONTime{Time: performer.CreatedAt.Timestamp},
UpdatedAt: models.JSONTime{Time: performer.UpdatedAt.Timestamp},
}
if performer.Name.Valid {
newPerformerJSON.Name = performer.Name.String
}
if performer.Gender.Valid {
newPerformerJSON.Gender = performer.Gender.String
}
if performer.URL.Valid {
newPerformerJSON.URL = performer.URL.String
}
if performer.Birthdate.Valid {
newPerformerJSON.Birthdate = utils.GetYMDFromDatabaseDate(performer.Birthdate.String)
}
if performer.Ethnicity.Valid {
newPerformerJSON.Ethnicity = performer.Ethnicity.String
}
if performer.Country.Valid {
newPerformerJSON.Country = performer.Country.String
}
if performer.EyeColor.Valid {
newPerformerJSON.EyeColor = performer.EyeColor.String
}
if performer.Height.Valid {
newPerformerJSON.Height = performer.Height.String
}
if performer.Measurements.Valid {
newPerformerJSON.Measurements = performer.Measurements.String
}
if performer.FakeTits.Valid {
newPerformerJSON.FakeTits = performer.FakeTits.String
}
if performer.CareerLength.Valid {
newPerformerJSON.CareerLength = performer.CareerLength.String
}
if performer.Tattoos.Valid {
newPerformerJSON.Tattoos = performer.Tattoos.String
}
if performer.Piercings.Valid {
newPerformerJSON.Piercings = performer.Piercings.String
}
if performer.Aliases.Valid {
newPerformerJSON.Aliases = performer.Aliases.String
}
if performer.Twitter.Valid {
newPerformerJSON.Twitter = performer.Twitter.String
}
if performer.Instagram.Valid {
newPerformerJSON.Instagram = performer.Instagram.String
}
if performer.Favorite.Valid {
newPerformerJSON.Favorite = performer.Favorite.Bool
}
image, err := reader.GetPerformerImage(performer.ID)
if err != nil {
return nil, fmt.Errorf("error getting performers image: %s", err.Error())
}
if len(image) > 0 {
newPerformerJSON.Image = utils.GetBase64StringFromData(image)
}
return &newPerformerJSON, nil
}
func GetIDs(performers []*models.Performer) []int {
var results []int
for _, performer := range performers {
results = append(results, performer.ID)
}
return results
}
func GetNames(performers []*models.Performer) []string {
var results []string
for _, performer := range performers {
if performer.Name.Valid {
results = append(results, performer.Name.String)
}
}
return results
}

View File

@ -0,0 +1,189 @@
package performer
import (
"database/sql"
"errors"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stashapp/stash/pkg/models/modelstest"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
const (
performerID = 1
noImageID = 2
errImageID = 3
)
const (
performerName = "testPerformer"
url = "url"
aliases = "aliases"
careerLength = "careerLength"
country = "country"
ethnicity = "ethnicity"
eyeColor = "eyeColor"
fakeTits = "fakeTits"
gender = "gender"
height = "height"
instagram = "instagram"
measurements = "measurements"
piercings = "piercings"
tattoos = "tattoos"
twitter = "twitter"
)
var imageBytes = []byte("imageBytes")
const image = "aW1hZ2VCeXRlcw=="
var birthDate = models.SQLiteDate{
String: "2001-01-01",
Valid: true,
}
var createTime time.Time = time.Date(2001, 01, 01, 0, 0, 0, 0, time.UTC)
var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.UTC)
func createFullPerformer(id int) models.Performer {
return models.Performer{
ID: id,
Name: modelstest.NullString(performerName),
URL: modelstest.NullString(url),
Aliases: modelstest.NullString(aliases),
Birthdate: birthDate,
CareerLength: modelstest.NullString(careerLength),
Country: modelstest.NullString(country),
Ethnicity: modelstest.NullString(ethnicity),
EyeColor: modelstest.NullString(eyeColor),
FakeTits: modelstest.NullString(fakeTits),
Favorite: sql.NullBool{
Bool: true,
Valid: true,
},
Gender: modelstest.NullString(gender),
Height: modelstest.NullString(height),
Instagram: modelstest.NullString(instagram),
Measurements: modelstest.NullString(measurements),
Piercings: modelstest.NullString(piercings),
Tattoos: modelstest.NullString(tattoos),
Twitter: modelstest.NullString(twitter),
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
}
}
func createEmptyPerformer(id int) models.Performer {
return models.Performer{
ID: id,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
}
}
func createFullJSONPerformer(image string) *jsonschema.Performer {
return &jsonschema.Performer{
Name: performerName,
URL: url,
Aliases: aliases,
Birthdate: birthDate.String,
CareerLength: careerLength,
Country: country,
Ethnicity: ethnicity,
EyeColor: eyeColor,
FakeTits: fakeTits,
Favorite: true,
Gender: gender,
Height: height,
Instagram: instagram,
Measurements: measurements,
Piercings: piercings,
Tattoos: tattoos,
Twitter: twitter,
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
Image: image,
}
}
func createEmptyJSONPerformer() *jsonschema.Performer {
return &jsonschema.Performer{
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
}
}
type testScenario struct {
input models.Performer
expected *jsonschema.Performer
err bool
}
var scenarios []testScenario
func initTestTable() {
scenarios = []testScenario{
testScenario{
createFullPerformer(performerID),
createFullJSONPerformer(image),
false,
},
testScenario{
createEmptyPerformer(noImageID),
createEmptyJSONPerformer(),
false,
},
testScenario{
createFullPerformer(errImageID),
nil,
true,
},
}
}
func TestToJSON(t *testing.T) {
initTestTable()
mockPerformerReader := &mocks.PerformerReaderWriter{}
imageErr := errors.New("error getting image")
mockPerformerReader.On("GetPerformerImage", performerID).Return(imageBytes, nil).Once()
mockPerformerReader.On("GetPerformerImage", noImageID).Return(nil, nil).Once()
mockPerformerReader.On("GetPerformerImage", errImageID).Return(nil, imageErr).Once()
for i, s := range scenarios {
tag := s.input
json, err := ToJSON(mockPerformerReader, &tag)
if !s.err && err != nil {
t.Errorf("[%d] unexpected error: %s", i, err.Error())
} else if s.err && err == nil {
t.Errorf("[%d] expected error not returned", i)
} else {
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
mockPerformerReader.AssertExpectations(t)
}

298
pkg/scene/export.go Normal file
View File

@ -0,0 +1,298 @@
package scene
import (
"fmt"
"math"
"strconv"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
// ToBasicJSON converts a scene object into its JSON object equivalent. It
// does not convert the relationships to other objects, with the exception
// of cover image.
func ToBasicJSON(reader models.SceneReader, scene *models.Scene) (*jsonschema.Scene, error) {
newSceneJSON := jsonschema.Scene{
CreatedAt: models.JSONTime{Time: scene.CreatedAt.Timestamp},
UpdatedAt: models.JSONTime{Time: scene.UpdatedAt.Timestamp},
}
if scene.Checksum.Valid {
newSceneJSON.Checksum = scene.Checksum.String
}
if scene.OSHash.Valid {
newSceneJSON.OSHash = scene.OSHash.String
}
if scene.Title.Valid {
newSceneJSON.Title = scene.Title.String
}
if scene.URL.Valid {
newSceneJSON.URL = scene.URL.String
}
if scene.Date.Valid {
newSceneJSON.Date = utils.GetYMDFromDatabaseDate(scene.Date.String)
}
if scene.Rating.Valid {
newSceneJSON.Rating = int(scene.Rating.Int64)
}
newSceneJSON.OCounter = scene.OCounter
if scene.Details.Valid {
newSceneJSON.Details = scene.Details.String
}
newSceneJSON.File = getSceneFileJSON(scene)
cover, err := reader.GetSceneCover(scene.ID)
if err != nil {
return nil, fmt.Errorf("error getting scene cover: %s", err.Error())
}
if len(cover) > 0 {
newSceneJSON.Cover = utils.GetBase64StringFromData(cover)
}
return &newSceneJSON, nil
}
func getSceneFileJSON(scene *models.Scene) *jsonschema.SceneFile {
ret := &jsonschema.SceneFile{}
if scene.Size.Valid {
ret.Size = scene.Size.String
}
if scene.Duration.Valid {
ret.Duration = getDecimalString(scene.Duration.Float64)
}
if scene.VideoCodec.Valid {
ret.VideoCodec = scene.VideoCodec.String
}
if scene.AudioCodec.Valid {
ret.AudioCodec = scene.AudioCodec.String
}
if scene.Format.Valid {
ret.Format = scene.Format.String
}
if scene.Width.Valid {
ret.Width = int(scene.Width.Int64)
}
if scene.Height.Valid {
ret.Height = int(scene.Height.Int64)
}
if scene.Framerate.Valid {
ret.Framerate = getDecimalString(scene.Framerate.Float64)
}
if scene.Bitrate.Valid {
ret.Bitrate = int(scene.Bitrate.Int64)
}
return ret
}
// GetStudioName returns the name of the provided scene's studio. It returns an
// empty string if there is no studio assigned to the scene.
func GetStudioName(reader models.StudioReader, scene *models.Scene) (string, error) {
if scene.StudioID.Valid {
studio, err := reader.Find(int(scene.StudioID.Int64))
if err != nil {
return "", err
}
if studio != nil {
return studio.Name.String, nil
}
}
return "", nil
}
// GetGalleryChecksum returns the checksum of the provided scene. It returns an
// empty string if there is no gallery assigned to the scene.
func GetGalleryChecksum(reader models.GalleryReader, scene *models.Scene) (string, error) {
gallery, err := reader.FindBySceneID(scene.ID)
if err != nil {
return "", fmt.Errorf("error getting scene gallery: %s", err.Error())
}
if gallery != nil {
return gallery.Checksum, nil
}
return "", nil
}
// GetTagNames returns a slice of tag names corresponding to the provided
// scene's tags.
func GetTagNames(reader models.TagReader, scene *models.Scene) ([]string, error) {
tags, err := reader.FindBySceneID(scene.ID)
if err != nil {
return nil, fmt.Errorf("error getting scene tags: %s", err.Error())
}
return getTagNames(tags), nil
}
func getTagNames(tags []*models.Tag) []string {
var results []string
for _, tag := range tags {
if tag.Name != "" {
results = append(results, tag.Name)
}
}
return results
}
// GetDependentTagIDs returns a slice of unique tag IDs that this scene references.
func GetDependentTagIDs(tags models.TagReader, joins models.JoinReader, markerReader models.SceneMarkerReader, scene *models.Scene) ([]int, error) {
var ret []int
t, err := tags.FindBySceneID(scene.ID)
if err != nil {
return nil, err
}
for _, tt := range t {
ret = utils.IntAppendUnique(ret, tt.ID)
}
sm, err := markerReader.FindBySceneID(scene.ID)
if err != nil {
return nil, err
}
for _, smm := range sm {
ret = utils.IntAppendUnique(ret, smm.PrimaryTagID)
smmt, err := tags.FindBySceneMarkerID(smm.ID)
if err != nil {
return nil, fmt.Errorf("invalid tags for scene marker: %s", err.Error())
}
for _, smmtt := range smmt {
ret = utils.IntAppendUnique(ret, smmtt.ID)
}
}
return ret, nil
}
// GetSceneMoviesJSON returns a slice of SceneMovie JSON representation objects
// corresponding to the provided scene's scene movie relationships.
func GetSceneMoviesJSON(movieReader models.MovieReader, joinReader models.JoinReader, scene *models.Scene) ([]jsonschema.SceneMovie, error) {
sceneMovies, err := joinReader.GetSceneMovies(scene.ID)
if err != nil {
return nil, fmt.Errorf("error getting scene movies: %s", err.Error())
}
var results []jsonschema.SceneMovie
for _, sceneMovie := range sceneMovies {
movie, err := movieReader.Find(sceneMovie.MovieID)
if err != nil {
return nil, fmt.Errorf("error getting movie: %s", err.Error())
}
if movie.Name.Valid {
sceneMovieJSON := jsonschema.SceneMovie{
MovieName: movie.Name.String,
SceneIndex: int(sceneMovie.SceneIndex.Int64),
}
results = append(results, sceneMovieJSON)
}
}
return results, nil
}
// GetDependentMovieIDs returns a slice of movie IDs that this scene references.
func GetDependentMovieIDs(joins models.JoinReader, scene *models.Scene) ([]int, error) {
var ret []int
m, err := joins.GetSceneMovies(scene.ID)
if err != nil {
return nil, err
}
for _, mm := range m {
ret = append(ret, mm.MovieID)
}
return ret, nil
}
// GetSceneMarkersJSON returns a slice of SceneMarker JSON representation
// objects corresponding to the provided scene's markers.
func GetSceneMarkersJSON(markerReader models.SceneMarkerReader, tagReader models.TagReader, scene *models.Scene) ([]jsonschema.SceneMarker, error) {
sceneMarkers, err := markerReader.FindBySceneID(scene.ID)
if err != nil {
return nil, fmt.Errorf("error getting scene markers: %s", err.Error())
}
var results []jsonschema.SceneMarker
for _, sceneMarker := range sceneMarkers {
primaryTag, err := tagReader.Find(sceneMarker.PrimaryTagID)
if err != nil {
return nil, fmt.Errorf("invalid primary tag for scene marker: %s", err.Error())
}
sceneMarkerTags, err := tagReader.FindBySceneMarkerID(sceneMarker.ID)
if err != nil {
return nil, fmt.Errorf("invalid tags for scene marker: %s", err.Error())
}
sceneMarkerJSON := jsonschema.SceneMarker{
Title: sceneMarker.Title,
Seconds: getDecimalString(sceneMarker.Seconds),
PrimaryTag: primaryTag.Name,
Tags: getTagNames(sceneMarkerTags),
CreatedAt: models.JSONTime{Time: sceneMarker.CreatedAt.Timestamp},
UpdatedAt: models.JSONTime{Time: sceneMarker.UpdatedAt.Timestamp},
}
results = append(results, sceneMarkerJSON)
}
return results, nil
}
func getDecimalString(num float64) string {
if num == 0 {
return ""
}
precision := getPrecision(num)
if precision == 0 {
precision = 1
}
return fmt.Sprintf("%."+strconv.Itoa(precision)+"f", num)
}
func getPrecision(num float64) int {
if num == 0 {
return 0
}
e := 1.0
p := 0
for (math.Round(num*e) / e) != num {
e *= 10
p++
}
return p
}

667
pkg/scene/export_test.go Normal file
View File

@ -0,0 +1,667 @@
package scene
import (
"database/sql"
"errors"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stashapp/stash/pkg/models/modelstest"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
const (
sceneID = 1
noImageID = 2
errImageID = 3
studioID = 4
missingStudioID = 5
errStudioID = 6
noGalleryID = 7
errGalleryID = 8
noTagsID = 11
errTagsID = 12
noMoviesID = 13
errMoviesID = 14
errFindMovieID = 15
noMarkersID = 16
errMarkersID = 17
errFindPrimaryTagID = 18
errFindByMarkerID = 19
)
const (
url = "url"
checksum = "checksum"
oshash = "oshash"
title = "title"
date = "2001-01-01"
rating = 5
ocounter = 2
details = "details"
size = "size"
duration = 1.23
durationStr = "1.23"
videoCodec = "videoCodec"
audioCodec = "audioCodec"
format = "format"
width = 100
height = 100
framerate = 3.21
framerateStr = "3.21"
bitrate = 1
)
const (
studioName = "studioName"
galleryChecksum = "galleryChecksum"
validMovie1 = 1
validMovie2 = 2
invalidMovie = 3
movie1Name = "movie1Name"
movie2Name = "movie2Name"
movie1Scene = 1
movie2Scene = 2
)
var names = []string{
"name1",
"name2",
}
var imageBytes = []byte("imageBytes")
const image = "aW1hZ2VCeXRlcw=="
var createTime time.Time = time.Date(2001, 01, 01, 0, 0, 0, 0, time.UTC)
var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.UTC)
func createFullScene(id int) models.Scene {
return models.Scene{
ID: id,
Title: modelstest.NullString(title),
AudioCodec: modelstest.NullString(audioCodec),
Bitrate: modelstest.NullInt64(bitrate),
Checksum: modelstest.NullString(checksum),
Date: models.SQLiteDate{
String: date,
Valid: true,
},
Details: modelstest.NullString(details),
Duration: sql.NullFloat64{
Float64: duration,
Valid: true,
},
Format: modelstest.NullString(format),
Framerate: sql.NullFloat64{
Float64: framerate,
Valid: true,
},
Height: modelstest.NullInt64(height),
OCounter: ocounter,
OSHash: modelstest.NullString(oshash),
Rating: modelstest.NullInt64(rating),
Size: modelstest.NullString(size),
VideoCodec: modelstest.NullString(videoCodec),
Width: modelstest.NullInt64(width),
URL: modelstest.NullString(url),
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
}
}
func createEmptyScene(id int) models.Scene {
return models.Scene{
ID: id,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
}
}
func createFullJSONScene(image string) *jsonschema.Scene {
return &jsonschema.Scene{
Title: title,
Checksum: checksum,
Date: date,
Details: details,
OCounter: ocounter,
OSHash: oshash,
Rating: rating,
URL: url,
File: &jsonschema.SceneFile{
AudioCodec: audioCodec,
Bitrate: bitrate,
Duration: durationStr,
Format: format,
Framerate: framerateStr,
Height: height,
Size: size,
VideoCodec: videoCodec,
Width: width,
},
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
Cover: image,
}
}
func createEmptyJSONScene() *jsonschema.Scene {
return &jsonschema.Scene{
File: &jsonschema.SceneFile{},
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
}
}
type basicTestScenario struct {
input models.Scene
expected *jsonschema.Scene
err bool
}
var scenarios = []basicTestScenario{
{
createFullScene(sceneID),
createFullJSONScene(image),
false,
},
{
createEmptyScene(noImageID),
createEmptyJSONScene(),
false,
},
{
createFullScene(errImageID),
nil,
true,
},
}
func TestToJSON(t *testing.T) {
mockSceneReader := &mocks.SceneReaderWriter{}
imageErr := errors.New("error getting image")
mockSceneReader.On("GetSceneCover", sceneID).Return(imageBytes, nil).Once()
mockSceneReader.On("GetSceneCover", noImageID).Return(nil, nil).Once()
mockSceneReader.On("GetSceneCover", errImageID).Return(nil, imageErr).Once()
for i, s := range scenarios {
scene := s.input
json, err := ToBasicJSON(mockSceneReader, &scene)
if !s.err && err != nil {
t.Errorf("[%d] unexpected error: %s", i, err.Error())
} else if s.err && err == nil {
t.Errorf("[%d] expected error not returned", i)
} else {
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
mockSceneReader.AssertExpectations(t)
}
func createStudioScene(studioID int) models.Scene {
return models.Scene{
StudioID: modelstest.NullInt64(int64(studioID)),
}
}
type stringTestScenario struct {
input models.Scene
expected string
err bool
}
var getStudioScenarios = []stringTestScenario{
{
createStudioScene(studioID),
studioName,
false,
},
{
createStudioScene(missingStudioID),
"",
false,
},
{
createStudioScene(errStudioID),
"",
true,
},
}
func TestGetStudioName(t *testing.T) {
mockStudioReader := &mocks.StudioReaderWriter{}
studioErr := errors.New("error getting image")
mockStudioReader.On("Find", studioID).Return(&models.Studio{
Name: modelstest.NullString(studioName),
}, nil).Once()
mockStudioReader.On("Find", missingStudioID).Return(nil, nil).Once()
mockStudioReader.On("Find", errStudioID).Return(nil, studioErr).Once()
for i, s := range getStudioScenarios {
scene := s.input
json, err := GetStudioName(mockStudioReader, &scene)
if !s.err && err != nil {
t.Errorf("[%d] unexpected error: %s", i, err.Error())
} else if s.err && err == nil {
t.Errorf("[%d] expected error not returned", i)
} else {
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
mockStudioReader.AssertExpectations(t)
}
var getGalleryChecksumScenarios = []stringTestScenario{
{
createEmptyScene(sceneID),
galleryChecksum,
false,
},
{
createEmptyScene(noGalleryID),
"",
false,
},
{
createEmptyScene(errGalleryID),
"",
true,
},
}
func TestGetGalleryChecksum(t *testing.T) {
mockGalleryReader := &mocks.GalleryReaderWriter{}
galleryErr := errors.New("error getting gallery")
mockGalleryReader.On("FindBySceneID", sceneID).Return(&models.Gallery{
Checksum: galleryChecksum,
}, nil).Once()
mockGalleryReader.On("FindBySceneID", noGalleryID).Return(nil, nil).Once()
mockGalleryReader.On("FindBySceneID", errGalleryID).Return(nil, galleryErr).Once()
for i, s := range getGalleryChecksumScenarios {
scene := s.input
json, err := GetGalleryChecksum(mockGalleryReader, &scene)
if !s.err && err != nil {
t.Errorf("[%d] unexpected error: %s", i, err.Error())
} else if s.err && err == nil {
t.Errorf("[%d] expected error not returned", i)
} else {
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
mockGalleryReader.AssertExpectations(t)
}
type stringSliceTestScenario struct {
input models.Scene
expected []string
err bool
}
var getTagNamesScenarios = []stringSliceTestScenario{
{
createEmptyScene(sceneID),
names,
false,
},
{
createEmptyScene(noTagsID),
nil,
false,
},
{
createEmptyScene(errTagsID),
nil,
true,
},
}
func getTags(names []string) []*models.Tag {
var ret []*models.Tag
for _, n := range names {
ret = append(ret, &models.Tag{
Name: n,
})
}
return ret
}
func TestGetTagNames(t *testing.T) {
mockTagReader := &mocks.TagReaderWriter{}
tagErr := errors.New("error getting tag")
mockTagReader.On("FindBySceneID", sceneID).Return(getTags(names), nil).Once()
mockTagReader.On("FindBySceneID", noTagsID).Return(nil, nil).Once()
mockTagReader.On("FindBySceneID", errTagsID).Return(nil, tagErr).Once()
for i, s := range getTagNamesScenarios {
scene := s.input
json, err := GetTagNames(mockTagReader, &scene)
if !s.err && err != nil {
t.Errorf("[%d] unexpected error: %s", i, err.Error())
} else if s.err && err == nil {
t.Errorf("[%d] expected error not returned", i)
} else {
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
mockTagReader.AssertExpectations(t)
}
type sceneMoviesTestScenario struct {
input models.Scene
expected []jsonschema.SceneMovie
err bool
}
var getSceneMoviesJSONScenarios = []sceneMoviesTestScenario{
{
createEmptyScene(sceneID),
[]jsonschema.SceneMovie{
{
MovieName: movie1Name,
SceneIndex: movie1Scene,
},
{
MovieName: movie2Name,
SceneIndex: movie2Scene,
},
},
false,
},
{
createEmptyScene(noMoviesID),
nil,
false,
},
{
createEmptyScene(errMoviesID),
nil,
true,
},
{
createEmptyScene(errFindMovieID),
nil,
true,
},
}
var validMovies = []models.MoviesScenes{
{
MovieID: validMovie1,
SceneIndex: modelstest.NullInt64(movie1Scene),
},
{
MovieID: validMovie2,
SceneIndex: modelstest.NullInt64(movie2Scene),
},
}
var invalidMovies = []models.MoviesScenes{
{
MovieID: invalidMovie,
SceneIndex: modelstest.NullInt64(movie1Scene),
},
}
func TestGetSceneMoviesJSON(t *testing.T) {
mockMovieReader := &mocks.MovieReaderWriter{}
mockJoinReader := &mocks.JoinReaderWriter{}
joinErr := errors.New("error getting scene movies")
movieErr := errors.New("error getting movie")
mockJoinReader.On("GetSceneMovies", sceneID).Return(validMovies, nil).Once()
mockJoinReader.On("GetSceneMovies", noMoviesID).Return(nil, nil).Once()
mockJoinReader.On("GetSceneMovies", errMoviesID).Return(nil, joinErr).Once()
mockJoinReader.On("GetSceneMovies", errFindMovieID).Return(invalidMovies, nil).Once()
mockMovieReader.On("Find", validMovie1).Return(&models.Movie{
Name: modelstest.NullString(movie1Name),
}, nil).Once()
mockMovieReader.On("Find", validMovie2).Return(&models.Movie{
Name: modelstest.NullString(movie2Name),
}, nil).Once()
mockMovieReader.On("Find", invalidMovie).Return(nil, movieErr).Once()
for i, s := range getSceneMoviesJSONScenarios {
scene := s.input
json, err := GetSceneMoviesJSON(mockMovieReader, mockJoinReader, &scene)
if !s.err && err != nil {
t.Errorf("[%d] unexpected error: %s", i, err.Error())
} else if s.err && err == nil {
t.Errorf("[%d] expected error not returned", i)
} else {
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
mockMovieReader.AssertExpectations(t)
}
const (
validMarkerID1 = 1
validMarkerID2 = 2
invalidMarkerID1 = 3
invalidMarkerID2 = 4
validTagID1 = 1
validTagID2 = 2
validTagName1 = "validTagName1"
validTagName2 = "validTagName2"
invalidTagID = 3
markerTitle1 = "markerTitle1"
markerTitle2 = "markerTitle2"
markerSeconds1 = 1.0
markerSeconds2 = 2.3
markerSeconds1Str = "1.0"
markerSeconds2Str = "2.3"
)
type sceneMarkersTestScenario struct {
input models.Scene
expected []jsonschema.SceneMarker
err bool
}
var getSceneMarkersJSONScenarios = []sceneMarkersTestScenario{
{
createEmptyScene(sceneID),
[]jsonschema.SceneMarker{
{
Title: markerTitle1,
PrimaryTag: validTagName1,
Seconds: markerSeconds1Str,
Tags: []string{
validTagName1,
validTagName2,
},
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
},
{
Title: markerTitle2,
PrimaryTag: validTagName2,
Seconds: markerSeconds2Str,
Tags: []string{
validTagName2,
},
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
},
},
false,
},
{
createEmptyScene(noMarkersID),
nil,
false,
},
{
createEmptyScene(errMarkersID),
nil,
true,
},
{
createEmptyScene(errFindPrimaryTagID),
nil,
true,
},
{
createEmptyScene(errFindByMarkerID),
nil,
true,
},
}
var validMarkers = []*models.SceneMarker{
{
ID: validMarkerID1,
Title: markerTitle1,
PrimaryTagID: validTagID1,
Seconds: markerSeconds1,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
},
{
ID: validMarkerID2,
Title: markerTitle2,
PrimaryTagID: validTagID2,
Seconds: markerSeconds2,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
},
}
var invalidMarkers1 = []*models.SceneMarker{
{
ID: invalidMarkerID1,
PrimaryTagID: invalidTagID,
},
}
var invalidMarkers2 = []*models.SceneMarker{
{
ID: invalidMarkerID2,
PrimaryTagID: validTagID1,
},
}
func TestGetSceneMarkersJSON(t *testing.T) {
mockTagReader := &mocks.TagReaderWriter{}
mockMarkerReader := &mocks.SceneMarkerReaderWriter{}
markersErr := errors.New("error getting scene markers")
tagErr := errors.New("error getting tags")
mockMarkerReader.On("FindBySceneID", sceneID).Return(validMarkers, nil).Once()
mockMarkerReader.On("FindBySceneID", noMarkersID).Return(nil, nil).Once()
mockMarkerReader.On("FindBySceneID", errMarkersID).Return(nil, markersErr).Once()
mockMarkerReader.On("FindBySceneID", errFindPrimaryTagID).Return(invalidMarkers1, nil).Once()
mockMarkerReader.On("FindBySceneID", errFindByMarkerID).Return(invalidMarkers2, nil).Once()
mockTagReader.On("Find", validTagID1).Return(&models.Tag{
Name: validTagName1,
}, nil)
mockTagReader.On("Find", validTagID2).Return(&models.Tag{
Name: validTagName2,
}, nil)
mockTagReader.On("Find", invalidTagID).Return(nil, tagErr)
mockTagReader.On("FindBySceneMarkerID", validMarkerID1).Return([]*models.Tag{
{
Name: validTagName1,
},
{
Name: validTagName2,
},
}, nil)
mockTagReader.On("FindBySceneMarkerID", validMarkerID2).Return([]*models.Tag{
{
Name: validTagName2,
},
}, nil)
mockTagReader.On("FindBySceneMarkerID", invalidMarkerID2).Return(nil, tagErr).Once()
for i, s := range getSceneMarkersJSONScenarios {
scene := s.input
json, err := GetSceneMarkersJSON(mockMarkerReader, mockTagReader, &scene)
if !s.err && err != nil {
t.Errorf("[%d] unexpected error: %s", i, err.Error())
} else if s.err && err == nil {
t.Errorf("[%d] expected error not returned", i)
} else {
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
mockTagReader.AssertExpectations(t)
}

47
pkg/studio/export.go Normal file
View File

@ -0,0 +1,47 @@
package studio
import (
"fmt"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
// ToJSON converts a Studio object into its JSON equivalent.
func ToJSON(reader models.StudioReader, studio *models.Studio) (*jsonschema.Studio, error) {
newStudioJSON := jsonschema.Studio{
CreatedAt: models.JSONTime{Time: studio.CreatedAt.Timestamp},
UpdatedAt: models.JSONTime{Time: studio.UpdatedAt.Timestamp},
}
if studio.Name.Valid {
newStudioJSON.Name = studio.Name.String
}
if studio.URL.Valid {
newStudioJSON.URL = studio.URL.String
}
if studio.ParentID.Valid {
parent, err := reader.Find(int(studio.ParentID.Int64))
if err != nil {
return nil, fmt.Errorf("error getting parent studio: %s", err.Error())
}
if parent != nil {
newStudioJSON.ParentStudio = parent.Name.String
}
}
image, err := reader.GetStudioImage(studio.ID)
if err != nil {
return nil, fmt.Errorf("error getting studio image: %s", err.Error())
}
if len(image) > 0 {
newStudioJSON.Image = utils.GetBase64StringFromData(image)
}
return &newStudioJSON, nil
}

168
pkg/studio/export_test.go Normal file
View File

@ -0,0 +1,168 @@
package studio
import (
"errors"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stashapp/stash/pkg/models/modelstest"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
const (
studioID = 1
noImageID = 2
errImageID = 3
missingParentStudioID = 4
errStudioID = 5
parentStudioID = 10
missingStudioID = 11
errParentStudioID = 12
)
const studioName = "testStudio"
const url = "url"
const parentStudioName = "parentStudio"
var parentStudio models.Studio = models.Studio{
Name: modelstest.NullString(parentStudioName),
}
var imageBytes = []byte("imageBytes")
const image = "aW1hZ2VCeXRlcw=="
var createTime time.Time = time.Date(2001, 01, 01, 0, 0, 0, 0, time.UTC)
var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.UTC)
func createFullStudio(id int, parentID int) models.Studio {
return models.Studio{
ID: id,
Name: modelstest.NullString(studioName),
URL: modelstest.NullString(url),
ParentID: modelstest.NullInt64(int64(parentID)),
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
}
}
func createEmptyStudio(id int) models.Studio {
return models.Studio{
ID: id,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
}
}
func createFullJSONStudio(parentStudio, image string) *jsonschema.Studio {
return &jsonschema.Studio{
Name: studioName,
URL: url,
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
ParentStudio: parentStudio,
Image: image,
}
}
func createEmptyJSONStudio() *jsonschema.Studio {
return &jsonschema.Studio{
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
}
}
type testScenario struct {
input models.Studio
expected *jsonschema.Studio
err bool
}
var scenarios []testScenario
func initTestTable() {
scenarios = []testScenario{
testScenario{
createFullStudio(studioID, parentStudioID),
createFullJSONStudio(parentStudioName, image),
false,
},
testScenario{
createEmptyStudio(noImageID),
createEmptyJSONStudio(),
false,
},
testScenario{
createFullStudio(errImageID, parentStudioID),
nil,
true,
},
testScenario{
createFullStudio(missingParentStudioID, missingStudioID),
createFullJSONStudio("", image),
false,
},
testScenario{
createFullStudio(errStudioID, errParentStudioID),
nil,
true,
},
}
}
func TestToJSON(t *testing.T) {
initTestTable()
mockStudioReader := &mocks.StudioReaderWriter{}
imageErr := errors.New("error getting image")
mockStudioReader.On("GetStudioImage", studioID).Return(imageBytes, nil).Once()
mockStudioReader.On("GetStudioImage", noImageID).Return(nil, nil).Once()
mockStudioReader.On("GetStudioImage", errImageID).Return(nil, imageErr).Once()
mockStudioReader.On("GetStudioImage", missingParentStudioID).Return(imageBytes, nil).Maybe()
mockStudioReader.On("GetStudioImage", errStudioID).Return(imageBytes, nil).Maybe()
parentStudioErr := errors.New("error getting parent studio")
mockStudioReader.On("Find", parentStudioID).Return(&parentStudio, nil)
mockStudioReader.On("Find", missingStudioID).Return(nil, nil)
mockStudioReader.On("Find", errParentStudioID).Return(nil, parentStudioErr)
for i, s := range scenarios {
studio := s.input
json, err := ToJSON(mockStudioReader, &studio)
if !s.err && err != nil {
t.Errorf("[%d] unexpected error: %s", i, err.Error())
} else if s.err && err == nil {
t.Errorf("[%d] expected error not returned", i)
} else {
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
mockStudioReader.AssertExpectations(t)
}

29
pkg/tag/export.go Normal file
View File

@ -0,0 +1,29 @@
package tag
import (
"fmt"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
// ToJSON converts a Tag object into its JSON equivalent.
func ToJSON(reader models.TagReader, tag *models.Tag) (*jsonschema.Tag, error) {
newTagJSON := jsonschema.Tag{
Name: tag.Name,
CreatedAt: models.JSONTime{Time: tag.CreatedAt.Timestamp},
UpdatedAt: models.JSONTime{Time: tag.UpdatedAt.Timestamp},
}
image, err := reader.GetTagImage(tag.ID)
if err != nil {
return nil, fmt.Errorf("error getting tag image: %s", err.Error())
}
if len(image) > 0 {
newTagJSON.Image = utils.GetBase64StringFromData(image)
}
return &newTagJSON, nil
}

105
pkg/tag/export_test.go Normal file
View File

@ -0,0 +1,105 @@
package tag
import (
"errors"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
const (
tagID = 1
noImageID = 2
errImageID = 3
)
const tagName = "testTag"
var createTime time.Time = time.Date(2001, 01, 01, 0, 0, 0, 0, time.UTC)
var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.UTC)
func createTag(id int) models.Tag {
return models.Tag{
ID: id,
Name: tagName,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
}
}
func createJSONTag(image string) *jsonschema.Tag {
return &jsonschema.Tag{
Name: tagName,
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
Image: image,
}
}
type testScenario struct {
tag models.Tag
expected *jsonschema.Tag
err bool
}
var scenarios []testScenario
func initTestTable() {
scenarios = []testScenario{
testScenario{
createTag(tagID),
createJSONTag("PHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMjAwIgogICBoZWlnaHQ9IjIwMCIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC40IHI5OTM5IgogICBzb2RpcG9kaTpkb2NuYW1lPSJ0YWcuc3ZnIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzNCIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9ImJhc2UiCiAgICAgcGFnZWNvbG9yPSIjMDAwMDAwIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMSIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjEiCiAgICAgaW5rc2NhcGU6Y3g9IjE4MS43Nzc3MSIKICAgICBpbmtzY2FwZTpjeT0iMjc5LjcyMzc2IgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9ImZhbHNlIgogICAgIGZpdC1tYXJnaW4tdG9wPSIwIgogICAgIGZpdC1tYXJnaW4tbGVmdD0iMCIKICAgICBmaXQtbWFyZ2luLXJpZ2h0PSIwIgogICAgIGZpdC1tYXJnaW4tYm90dG9tPSIwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDE3IgogICAgIGlua3NjYXBlOndpbmRvdy14PSItOCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTgiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTU3Ljg0MzU4LC01MjQuNjk1MjIpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDI5ODciCiAgICAgICBkPSJtIDIyOS45NDMxNCw2NjkuMjY1NDkgLTM2LjA4NDY2LC0zNi4wODQ2NiBjIC00LjY4NjUzLC00LjY4NjUzIC00LjY4NjUzLC0xMi4yODQ2OCAwLC0xNi45NzEyMSBsIDM2LjA4NDY2LC0zNi4wODQ2NyBhIDEyLjAwMDQ1MywxMi4wMDA0NTMgMCAwIDEgOC40ODU2LC0zLjUxNDggbCA3NC45MTQ0MywwIGMgNi42Mjc2MSwwIDEyLjAwMDQxLDUuMzcyOCAxMi4wMDA0MSwxMi4wMDA0MSBsIDAsNzIuMTY5MzMgYyAwLDYuNjI3NjEgLTUuMzcyOCwxMi4wMDA0MSAtMTIuMDAwNDEsMTIuMDAwNDEgbCAtNzQuOTE0NDMsMCBhIDEyLjAwMDQ1MywxMi4wMDA0NTMgMCAwIDEgLTguNDg1NiwtMy41MTQ4MSB6IG0gLTEzLjQ1NjM5LC01My4wNTU4NyBjIC00LjY4NjUzLDQuNjg2NTMgLTQuNjg2NTMsMTIuMjg0NjggMCwxNi45NzEyMSA0LjY4NjUyLDQuNjg2NTIgMTIuMjg0NjcsNC42ODY1MiAxNi45NzEyLDAgNC42ODY1MywtNC42ODY1MyA0LjY4NjUzLC0xMi4yODQ2OCAwLC0xNi45NzEyMSAtNC42ODY1MywtNC42ODY1MiAtMTIuMjg0NjgsLTQuNjg2NTIgLTE2Ljk3MTIsMCB6IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjEiIC8+CiAgPC9nPgo8L3N2Zz4="),
false,
},
testScenario{
createTag(noImageID),
createJSONTag(""),
false,
},
testScenario{
createTag(errImageID),
nil,
true,
},
}
}
func TestToJSON(t *testing.T) {
initTestTable()
mockTagReader := &mocks.TagReaderWriter{}
imageErr := errors.New("error getting image")
mockTagReader.On("GetTagImage", tagID).Return(models.DefaultTagImage, nil).Once()
mockTagReader.On("GetTagImage", noImageID).Return(nil, nil).Once()
mockTagReader.On("GetTagImage", errImageID).Return(nil, imageErr).Once()
for i, s := range scenarios {
tag := s.tag
json, err := ToJSON(mockTagReader, &tag)
if !s.err && err != nil {
t.Errorf("[%d] unexpected error: %s", i, err.Error())
} else if s.err && err == nil {
t.Errorf("[%d] expected error not returned", i)
} else {
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
mockTagReader.AssertExpectations(t)
}

View File

@ -0,0 +1,39 @@
package utils
// IntIndex returns the first index of the provided int value in the provided
// int slice. It returns -1 if it is not found.
func IntIndex(vs []int, t int) int {
for i, v := range vs {
if v == t {
return i
}
}
return -1
}
// IntInclude returns true if the provided int value exists in the provided int
// slice.
func IntInclude(vs []int, t int) bool {
return IntIndex(vs, t) >= 0
}
// IntAppendUnique appends toAdd to the vs int slice if toAdd does not already
// exist in the slice. It returns the new or unchanged int slice.
func IntAppendUnique(vs []int, toAdd int) []int {
if IntInclude(vs, toAdd) {
return vs
}
return append(vs, toAdd)
}
// IntAppendUniques appends a slice of int values to the vs int slice. It only
// appends values that do not already exist in the slice. It returns the new or
// unchanged int slice.
func IntAppendUniques(vs []int, toAdd []int) []int {
for _, v := range toAdd {
vs = IntAppendUnique(vs, v)
}
return vs
}

View File

@ -4,4 +4,5 @@ package main
import (
_ "github.com/99designs/gqlgen"
_ "github.com/vektra/mockery/v2"
)

View File

@ -0,0 +1,73 @@
import React, { useState } from "react";
import { Form } from "react-bootstrap";
import { mutateExportObjects } from "src/core/StashService";
import { Modal } from "src/components/Shared";
import { useToast } from "src/hooks";
import { downloadFile } from "src/utils";
interface ISceneExportDialogProps {
selectedIds?: string[];
all?: boolean;
onClose: () => void;
}
export const SceneExportDialog: React.FC<ISceneExportDialogProps> = (
props: ISceneExportDialogProps
) => {
const [includeDependencies, setIncludeDependencies] = useState(true);
// Network state
const [isRunning, setIsRunning] = useState(false);
const Toast = useToast();
async function onExport() {
try {
setIsRunning(true);
const ret = await mutateExportObjects({
scenes: {
ids: props.selectedIds,
all: props.all,
},
includeDependencies,
});
// download the result
if (ret.data && ret.data.exportObjects) {
const link = ret.data.exportObjects;
downloadFile(link);
}
} catch (e) {
Toast.error(e);
} finally {
setIsRunning(false);
props.onClose();
}
}
return (
<Modal
show
icon="cogs"
header="Generate"
accept={{ onClick: onExport, text: "Export" }}
cancel={{
onClick: () => props.onClose(),
text: "Cancel",
variant: "secondary",
}}
isRunning={isRunning}
>
<Form>
<Form.Group>
<Form.Check
id="include-dependencies"
checked={includeDependencies}
label="Include related performers/movies/tags/studio in export"
onChange={() => setIncludeDependencies(!includeDependencies)}
/>
</Form.Group>
</Form>
</Modal>
);
};

View File

@ -16,6 +16,7 @@ import { SceneListTable } from "./SceneListTable";
import { EditScenesDialog } from "./EditScenesDialog";
import { DeleteScenesDialog } from "./DeleteScenesDialog";
import { SceneGenerateDialog } from "./SceneGenerateDialog";
import { SceneExportDialog } from "./SceneExportDialog";
interface ISceneList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
@ -28,6 +29,8 @@ export const SceneList: React.FC<ISceneList> = ({
}) => {
const history = useHistory();
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
const otherOperations = [
{
@ -39,6 +42,15 @@ export const SceneList: React.FC<ISceneList> = ({
onClick: generate,
isDisplayed: showWhenSelected,
},
{
text: "Export...",
onClick: onExport,
isDisplayed: showWhenSelected,
},
{
text: "Export all...",
onClick: onExportAll,
},
];
const addKeybinds = (
@ -96,6 +108,16 @@ export const SceneList: React.FC<ISceneList> = ({
setIsGenerateDialogOpen(true);
}
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
async function onExportAll() {
setIsExportAll(true);
setIsExportDialogOpen(true);
}
function maybeRenderSceneGenerateDialog(selectedIds: Set<string>) {
if (isGenerateDialogOpen) {
return (
@ -111,6 +133,22 @@ export const SceneList: React.FC<ISceneList> = ({
}
}
function maybeRenderSceneExportDialog(selectedIds: Set<string>) {
if (isExportDialogOpen) {
return (
<>
<SceneExportDialog
selectedIds={Array.from(selectedIds.values())}
all={isExportAll}
onClose={() => {
setIsExportDialogOpen(false);
}}
/>
</>
);
}
}
function renderEditScenesDialog(
selectedScenes: SlimSceneDataFragment[],
onClose: (applied: boolean) => void
@ -187,6 +225,7 @@ export const SceneList: React.FC<ISceneList> = ({
return (
<>
{maybeRenderSceneGenerateDialog(selectedIds)}
{maybeRenderSceneExportDialog(selectedIds)}
{renderScenes(result, filter, selectedIds, zoomIndex)}
</>
);

View File

@ -552,6 +552,12 @@ export const mutateMetadataExport = () =>
mutation: GQL.MetadataExportDocument,
});
export const mutateExportObjects = (input: GQL.ExportObjectsInput) =>
client.mutate<GQL.ExportObjectsMutation>({
mutation: GQL.ExportObjectsDocument,
variables: { input },
});
export const mutateMetadataImport = () =>
client.mutate<GQL.MetadataImportMutation>({
mutation: GQL.MetadataImportDocument,

View File

@ -607,8 +607,8 @@ export const useTagsList = (
result?.data?.findTags?.count ?? 0,
});
export const showWhenSelected = (
_result: FindScenesQueryResult,
export const showWhenSelected = <T extends IQueryResult>(
_result: T,
_filter: ListFilterModel,
selectedIds: Set<string>
) => {

View File

@ -0,0 +1,7 @@
const downloadFile = (url: string) => {
const a = document.createElement("a");
a.href = url;
a.click();
};
export default downloadFile;

View File

@ -10,3 +10,4 @@ export { default as SessionUtils } from "./session";
export { default as flattenMessages } from "./flattenMessages";
export { default as getISOCountry } from "./country";
export { default as useFocus } from "./focus";
export { default as downloadFile } from "./download";

View File

@ -1,19 +0,0 @@
language: go
sudo: false
matrix:
include:
- go: 1.7.x
- go: 1.8.x
- go: 1.9.x
- go: 1.10.x
- go: 1.11.x
- go: tip
allow_failures:
- go: tip
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .)
- go vet $(go list ./... | grep -v /vendor/)
- go test -v -race ./...

View File

@ -1,14 +1,14 @@
# Gorilla WebSocket
[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket)
[![CircleCI](https://circleci.com/gh/gorilla/websocket.svg?style=svg)](https://circleci.com/gh/gorilla/websocket)
Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
[![Build Status](https://travis-ci.org/gorilla/websocket.svg?branch=master)](https://travis-ci.org/gorilla/websocket)
[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket)
### Documentation
* [API Reference](http://godoc.org/github.com/gorilla/websocket)
* [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc)
* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat)
* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command)
* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo)
@ -27,7 +27,7 @@ package API is stable.
### Protocol Compliance
The Gorilla WebSocket package passes the server tests in the [Autobahn Test
Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn
Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn
subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn).
### Gorilla WebSocket compared with other packages
@ -40,7 +40,7 @@ subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn
</tr>
<tr>
<tr><td colspan="3"><a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a> Features</td></tr>
<tr><td>Passes <a href="http://autobahn.ws/testsuite/">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr>
<tr><td>Passes <a href="https://github.com/crossbario/autobahn-testsuite">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr>
<tr><td>Receive <a href="https://tools.ietf.org/html/rfc6455#section-5.4">fragmented</a> message<td>Yes</td><td><a href="https://code.google.com/p/go/issues/detail?id=7632">No</a>, see note 1</td></tr>
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.1">close</a> message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=4588">No</a></td></tr>
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.2">pings</a> and receive <a href="https://tools.ietf.org/html/rfc6455#section-5.5.3">pongs</a></td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td>No</td></tr>

View File

@ -70,7 +70,7 @@ type Dialer struct {
// HandshakeTimeout specifies the duration for the handshake to complete.
HandshakeTimeout time.Duration
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer
// size is zero, then a useful default size is used. The I/O buffer sizes
// do not limit the size of the messages that can be sent or received.
ReadBufferSize, WriteBufferSize int
@ -140,7 +140,7 @@ var nilDialer = *DefaultDialer
// Use the response.Header to get the selected subprotocol
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
//
// The context will be used in the request and in the Dialer
// The context will be used in the request and in the Dialer.
//
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
// non-nil *http.Response so that callers can handle redirects, authentication,

View File

@ -244,7 +244,7 @@ type Conn struct {
subprotocol string
// Write fields
mu chan bool // used as mutex to protect write to conn
mu chan struct{} // used as mutex to protect write to conn
writeBuf []byte // frame is constructed in this buffer.
writePool BufferPool
writeBufSize int
@ -263,7 +263,9 @@ type Conn struct {
reader io.ReadCloser // the current reader returned to the application
readErr error
br *bufio.Reader
readRemaining int64 // bytes remaining in current frame.
// bytes remaining in current frame.
// set setReadRemaining to safely update this value and prevent overflow
readRemaining int64
readFinal bool // true the current message has more frames.
readLength int64 // Message size.
readLimit int64 // Maximum message size.
@ -300,8 +302,8 @@ func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int,
writeBuf = make([]byte, writeBufferSize)
}
mu := make(chan bool, 1)
mu <- true
mu := make(chan struct{}, 1)
mu <- struct{}{}
c := &Conn{
isServer: isServer,
br: br,
@ -320,6 +322,17 @@ func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int,
return c
}
// setReadRemaining tracks the number of bytes remaining on the connection. If n
// overflows, an ErrReadLimit is returned.
func (c *Conn) setReadRemaining(n int64) error {
if n < 0 {
return ErrReadLimit
}
c.readRemaining = n
return nil
}
// Subprotocol returns the negotiated protocol for the connection.
func (c *Conn) Subprotocol() string {
return c.subprotocol
@ -364,7 +377,7 @@ func (c *Conn) read(n int) ([]byte, error) {
func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error {
<-c.mu
defer func() { c.mu <- true }()
defer func() { c.mu <- struct{}{} }()
c.writeErrMu.Lock()
err := c.writeErr
@ -416,7 +429,7 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
maskBytes(key, 0, buf[6:])
}
d := time.Hour * 1000
d := 1000 * time.Hour
if !deadline.IsZero() {
d = deadline.Sub(time.Now())
if d < 0 {
@ -431,7 +444,7 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
case <-timer.C:
return errWriteTimeout
}
defer func() { c.mu <- true }()
defer func() { c.mu <- struct{}{} }()
c.writeErrMu.Lock()
err := c.writeErr
@ -451,7 +464,8 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
return err
}
func (c *Conn) prepWrite(messageType int) error {
// beginMessage prepares a connection and message writer for a new message.
func (c *Conn) beginMessage(mw *messageWriter, messageType int) error {
// Close previous writer if not already closed by the application. It's
// probably better to return an error in this situation, but we cannot
// change this without breaking existing applications.
@ -471,6 +485,10 @@ func (c *Conn) prepWrite(messageType int) error {
return err
}
mw.c = c
mw.frameType = messageType
mw.pos = maxFrameHeaderSize
if c.writeBuf == nil {
wpd, ok := c.writePool.Get().(writePoolData)
if ok {
@ -491,16 +509,11 @@ func (c *Conn) prepWrite(messageType int) error {
// All message types (TextMessage, BinaryMessage, CloseMessage, PingMessage and
// PongMessage) are supported.
func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
if err := c.prepWrite(messageType); err != nil {
var mw messageWriter
if err := c.beginMessage(&mw, messageType); err != nil {
return nil, err
}
mw := &messageWriter{
c: c,
frameType: messageType,
pos: maxFrameHeaderSize,
}
c.writer = mw
c.writer = &mw
if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) {
w := c.newCompressionWriter(c.writer, c.compressionLevel)
mw.compress = true
@ -517,10 +530,16 @@ type messageWriter struct {
err error
}
func (w *messageWriter) fatal(err error) error {
func (w *messageWriter) endMessage(err error) error {
if w.err != nil {
return err
}
c := w.c
w.err = err
w.c.writer = nil
c.writer = nil
if c.writePool != nil {
c.writePool.Put(writePoolData{buf: c.writeBuf})
c.writeBuf = nil
}
return err
}
@ -534,7 +553,7 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
// Check for invalid control frames.
if isControl(w.frameType) &&
(!final || length > maxControlFramePayloadSize) {
return w.fatal(errInvalidControlFrame)
return w.endMessage(errInvalidControlFrame)
}
b0 := byte(w.frameType)
@ -579,7 +598,7 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
copy(c.writeBuf[maxFrameHeaderSize-4:], key[:])
maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos])
if len(extra) > 0 {
return c.writeFatal(errors.New("websocket: internal error, extra used in client mode"))
return w.endMessage(c.writeFatal(errors.New("websocket: internal error, extra used in client mode")))
}
}
@ -600,15 +619,11 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
c.isWriting = false
if err != nil {
return w.fatal(err)
return w.endMessage(err)
}
if final {
c.writer = nil
if c.writePool != nil {
c.writePool.Put(writePoolData{buf: c.writeBuf})
c.writeBuf = nil
}
w.endMessage(errWriteClosed)
return nil
}
@ -706,11 +721,7 @@ func (w *messageWriter) Close() error {
if w.err != nil {
return w.err
}
if err := w.flushFrame(true, nil); err != nil {
return err
}
w.err = errWriteClosed
return nil
return w.flushFrame(true, nil)
}
// WritePreparedMessage writes prepared message into connection.
@ -742,10 +753,10 @@ func (c *Conn) WriteMessage(messageType int, data []byte) error {
if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) {
// Fast path with no allocations and single frame.
if err := c.prepWrite(messageType); err != nil {
var mw messageWriter
if err := c.beginMessage(&mw, messageType); err != nil {
return err
}
mw := messageWriter{c: c, frameType: messageType, pos: maxFrameHeaderSize}
n := copy(c.writeBuf[mw.pos:], data)
mw.pos += n
data = data[n:]
@ -792,7 +803,7 @@ func (c *Conn) advanceFrame() (int, error) {
final := p[0]&finalBit != 0
frameType := int(p[0] & 0xf)
mask := p[1]&maskBit != 0
c.readRemaining = int64(p[1] & 0x7f)
c.setReadRemaining(int64(p[1] & 0x7f))
c.readDecompress = false
if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 {
@ -826,7 +837,17 @@ func (c *Conn) advanceFrame() (int, error) {
return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType))
}
// 3. Read and parse frame length.
// 3. Read and parse frame length as per
// https://tools.ietf.org/html/rfc6455#section-5.2
//
// The length of the "Payload data", in bytes: if 0-125, that is the payload
// length.
// - If 126, the following 2 bytes interpreted as a 16-bit unsigned
// integer are the payload length.
// - If 127, the following 8 bytes interpreted as
// a 64-bit unsigned integer (the most significant bit MUST be 0) are the
// payload length. Multibyte length quantities are expressed in network byte
// order.
switch c.readRemaining {
case 126:
@ -834,13 +855,19 @@ func (c *Conn) advanceFrame() (int, error) {
if err != nil {
return noFrame, err
}
c.readRemaining = int64(binary.BigEndian.Uint16(p))
if err := c.setReadRemaining(int64(binary.BigEndian.Uint16(p))); err != nil {
return noFrame, err
}
case 127:
p, err := c.read(8)
if err != nil {
return noFrame, err
}
c.readRemaining = int64(binary.BigEndian.Uint64(p))
if err := c.setReadRemaining(int64(binary.BigEndian.Uint64(p))); err != nil {
return noFrame, err
}
}
// 4. Handle frame masking.
@ -863,6 +890,12 @@ func (c *Conn) advanceFrame() (int, error) {
if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage {
c.readLength += c.readRemaining
// Don't allow readLength to overflow in the presence of a large readRemaining
// counter.
if c.readLength < 0 {
return noFrame, ErrReadLimit
}
if c.readLimit > 0 && c.readLength > c.readLimit {
c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait))
return noFrame, ErrReadLimit
@ -876,7 +909,7 @@ func (c *Conn) advanceFrame() (int, error) {
var payload []byte
if c.readRemaining > 0 {
payload, err = c.read(int(c.readRemaining))
c.readRemaining = 0
c.setReadRemaining(0)
if err != nil {
return noFrame, err
}
@ -949,6 +982,7 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
c.readErr = hideTempErr(err)
break
}
if frameType == TextMessage || frameType == BinaryMessage {
c.messageReader = &messageReader{c}
c.reader = c.messageReader
@ -989,7 +1023,9 @@ func (r *messageReader) Read(b []byte) (int, error) {
if c.isServer {
c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n])
}
c.readRemaining -= int64(n)
rem := c.readRemaining
rem -= int64(n)
c.setReadRemaining(rem)
if c.readRemaining > 0 && c.readErr == io.EOF {
c.readErr = errUnexpectedEOF
}
@ -1041,7 +1077,7 @@ func (c *Conn) SetReadDeadline(t time.Time) error {
return c.conn.SetReadDeadline(t)
}
// SetReadLimit sets the maximum size for a message read from the peer. If a
// SetReadLimit sets the maximum size in bytes for a message read from the peer. If a
// message exceeds the limit, the connection sends a close message to the peer
// and returns ErrReadLimit to the application.
func (c *Conn) SetReadLimit(limit int64) {

View File

@ -151,6 +151,53 @@
// checking. The application is responsible for checking the Origin header
// before calling the Upgrade function.
//
// Buffers
//
// Connections buffer network input and output to reduce the number
// of system calls when reading or writing messages.
//
// Write buffers are also used for constructing WebSocket frames. See RFC 6455,
// Section 5 for a discussion of message framing. A WebSocket frame header is
// written to the network each time a write buffer is flushed to the network.
// Decreasing the size of the write buffer can increase the amount of framing
// overhead on the connection.
//
// The buffer sizes in bytes are specified by the ReadBufferSize and
// WriteBufferSize fields in the Dialer and Upgrader. The Dialer uses a default
// size of 4096 when a buffer size field is set to zero. The Upgrader reuses
// buffers created by the HTTP server when a buffer size field is set to zero.
// The HTTP server buffers have a size of 4096 at the time of this writing.
//
// The buffer sizes do not limit the size of a message that can be read or
// written by a connection.
//
// Buffers are held for the lifetime of the connection by default. If the
// Dialer or Upgrader WriteBufferPool field is set, then a connection holds the
// write buffer only when writing a message.
//
// Applications should tune the buffer sizes to balance memory use and
// performance. Increasing the buffer size uses more memory, but can reduce the
// number of system calls to read or write the network. In the case of writing,
// increasing the buffer size can reduce the number of frame headers written to
// the network.
//
// Some guidelines for setting buffer parameters are:
//
// Limit the buffer sizes to the maximum expected message size. Buffers larger
// than the largest message do not provide any benefit.
//
// Depending on the distribution of message sizes, setting the buffer size to
// a value less than the maximum expected message size can greatly reduce memory
// use with a small impact on performance. Here's an example: If 99% of the
// messages are smaller than 256 bytes and the maximum message size is 512
// bytes, then a buffer size of 256 bytes will result in 1.01 more system calls
// than a buffer size of 512 bytes. The memory savings is 50%.
//
// A write buffer pool is useful when the application has a modest number
// writes over a large number of connections. when buffers are pooled, a larger
// buffer size has a reduced impact on total memory use and has the benefit of
// reducing system calls and frame overhead.
//
// Compression EXPERIMENTAL
//
// Per message compression extensions (RFC 7692) are experimentally supported

3
vendor/github.com/gorilla/websocket/go.mod generated vendored Normal file
View File

@ -0,0 +1,3 @@
module github.com/gorilla/websocket
go 1.12

0
vendor/github.com/gorilla/websocket/go.sum generated vendored Normal file
View File

42
vendor/github.com/gorilla/websocket/join.go generated vendored Normal file
View File

@ -0,0 +1,42 @@
// Copyright 2019 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"io"
"strings"
)
// JoinMessages concatenates received messages to create a single io.Reader.
// The string term is appended to each message. The returned reader does not
// support concurrent calls to the Read method.
func JoinMessages(c *Conn, term string) io.Reader {
return &joinReader{c: c, term: term}
}
type joinReader struct {
c *Conn
term string
r io.Reader
}
func (r *joinReader) Read(p []byte) (int, error) {
if r.r == nil {
var err error
_, r.r, err = r.c.NextReader()
if err != nil {
return 0, err
}
if r.term != "" {
r.r = io.MultiReader(r.r, strings.NewReader(r.term))
}
}
n, err := r.r.Read(p)
if err == io.EOF {
err = nil
r.r = nil
}
return n, err
}

View File

@ -73,8 +73,8 @@ func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) {
// Prepare a frame using a 'fake' connection.
// TODO: Refactor code in conn.go to allow more direct construction of
// the frame.
mu := make(chan bool, 1)
mu <- true
mu := make(chan struct{}, 1)
mu <- struct{}{}
var nc prepareConn
c := &Conn{
conn: &nc,

View File

@ -22,18 +22,18 @@ func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) {
func init() {
proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) {
return &httpProxyDialer{proxyURL: proxyURL, fowardDial: forwardDialer.Dial}, nil
return &httpProxyDialer{proxyURL: proxyURL, forwardDial: forwardDialer.Dial}, nil
})
}
type httpProxyDialer struct {
proxyURL *url.URL
fowardDial func(network, addr string) (net.Conn, error)
forwardDial func(network, addr string) (net.Conn, error)
}
func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) {
hostPort, _ := hostPortNoPort(hpd.proxyURL)
conn, err := hpd.fowardDial(network, hostPort)
conn, err := hpd.forwardDial(network, hostPort)
if err != nil {
return nil, err
}

View File

@ -27,7 +27,7 @@ type Upgrader struct {
// HandshakeTimeout specifies the duration for the handshake to complete.
HandshakeTimeout time.Duration
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer
// size is zero, then buffers allocated by the HTTP server are used. The
// I/O buffer sizes do not limit the size of the messages that can be sent
// or received.
@ -153,7 +153,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
challengeKey := r.Header.Get("Sec-Websocket-Key")
if challengeKey == "" {
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-WebSocket-Key' header is missing or blank")
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header is missing or blank")
}
subprotocol := u.selectSubprotocol(r, responseHeader)

View File

@ -31,68 +31,113 @@ func generateChallengeKey() (string, error) {
return base64.StdEncoding.EncodeToString(p), nil
}
// Octet types from RFC 2616.
var octetTypes [256]byte
const (
isTokenOctet = 1 << iota
isSpaceOctet
)
func init() {
// From RFC 2616
//
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
// CR = <US-ASCII CR, carriage return (13)>
// LF = <US-ASCII LF, linefeed (10)>
// SP = <US-ASCII SP, space (32)>
// HT = <US-ASCII HT, horizontal-tab (9)>
// <"> = <US-ASCII double-quote mark (34)>
// CRLF = CR LF
// LWS = [CRLF] 1*( SP | HT )
// TEXT = <any OCTET except CTLs, but including LWS>
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
// token = 1*<any CHAR except CTLs or separators>
// qdtext = <any TEXT except <">>
for c := 0; c < 256; c++ {
var t byte
isCtl := c <= 31 || c == 127
isChar := 0 <= c && c <= 127
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
t |= isSpaceOctet
}
if isChar && !isCtl && !isSeparator {
t |= isTokenOctet
}
octetTypes[c] = t
}
// Token octets per RFC 2616.
var isTokenOctet = [256]bool{
'!': true,
'#': true,
'$': true,
'%': true,
'&': true,
'\'': true,
'*': true,
'+': true,
'-': true,
'.': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'W': true,
'V': true,
'X': true,
'Y': true,
'Z': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'|': true,
'~': true,
}
// skipSpace returns a slice of the string s with all leading RFC 2616 linear
// whitespace removed.
func skipSpace(s string) (rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isSpaceOctet == 0 {
if b := s[i]; b != ' ' && b != '\t' {
break
}
}
return s[i:]
}
// nextToken returns the leading RFC 2616 token of s and the string following
// the token.
func nextToken(s string) (token, rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isTokenOctet == 0 {
if !isTokenOctet[s[i]] {
break
}
}
return s[:i], s[i:]
}
// nextTokenOrQuoted returns the leading token or quoted string per RFC 2616
// and the string following the token or quoted string.
func nextTokenOrQuoted(s string) (value string, rest string) {
if !strings.HasPrefix(s, "\"") {
return nextToken(s)
@ -128,7 +173,8 @@ func nextTokenOrQuoted(s string) (value string, rest string) {
return "", ""
}
// equalASCIIFold returns true if s is equal to t with ASCII case folding.
// equalASCIIFold returns true if s is equal to t with ASCII case folding as
// defined in RFC 4790.
func equalASCIIFold(s, t string) bool {
for s != "" && t != "" {
sr, size := utf8.DecodeRuneInString(s)

View File

@ -7,4 +7,6 @@ go:
- 1.8.x
- 1.9.x
- "1.10.x"
- "1.11.x"
- "1.12.x"
- tip

View File

@ -1,5 +1,13 @@
## Changelog
### [1.8.1](https://github.com/magiconair/properties/tree/v1.8.1) - 10 May 2019
* [PR #26](https://github.com/magiconair/properties/pull/35): Close body always after request
This patch ensures that in `LoadURL` the response body is always closed.
Thanks to [@liubog2008](https://github.com/liubog2008) for the patch.
### [1.8](https://github.com/magiconair/properties/tree/v1.8) - 15 May 2018
* [PR #26](https://github.com/magiconair/properties/pull/26): Disable expansion during loading

View File

@ -1,6 +1,6 @@
[![](https://img.shields.io/github/tag/magiconair/properties.svg?style=flat-square&label=release)](https://github.com/magiconair/properties/releases)
[![Travis CI Status](https://img.shields.io/travis/magiconair/properties.svg?branch=master&style=flat-square&label=travis)](https://travis-ci.org/magiconair/properties)
[![Codeship CI Status](https://img.shields.io/codeship/16aaf660-f615-0135-b8f0-7e33b70920c0/master.svg?label=codeship&style=flat-square)](https://app.codeship.com/projects/274177")
[![CircleCI Status](https://img.shields.io/circleci/project/github/magiconair/properties.svg?label=circle+ci&style=flat-square)](https://circleci.com/gh/magiconair/properties)
[![License](https://img.shields.io/badge/License-BSD%202--Clause-orange.svg?style=flat-square)](https://raw.githubusercontent.com/magiconair/properties/master/LICENSE)
[![GoDoc](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square)](http://godoc.org/github.com/magiconair/properties)
@ -30,7 +30,7 @@ changed from `panic` to `log.Fatal` but this is configurable and custom
error handling functions can be provided. See the package documentation for
details.
Read the full documentation on [GoDoc](https://godoc.org/github.com/magiconair/properties) [![GoDoc](https://godoc.org/github.com/magiconair/properties?status.png)](https://godoc.org/github.com/magiconair/properties)
Read the full documentation on [![GoDoc](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square)](http://godoc.org/github.com/magiconair/properties)
## Getting Started

1
vendor/github.com/magiconair/properties/go.mod generated vendored Normal file
View File

@ -0,0 +1 @@
module github.com/magiconair/properties

View File

@ -115,6 +115,7 @@ func (l *Loader) LoadURL(url string) (*Properties, error) {
if err != nil {
return nil, fmt.Errorf("properties: error fetching %q. %s", url, err)
}
defer resp.Body.Close()
if resp.StatusCode == 404 && l.IgnoreMissing {
LogPrintf("properties: %s returned %d. skipping", url, resp.StatusCode)
@ -129,7 +130,6 @@ func (l *Loader) LoadURL(url string) (*Properties, error) {
if err != nil {
return nil, fmt.Errorf("properties: %s error reading response. %s", url, err)
}
defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
var enc Encoding

21
vendor/github.com/mitchellh/go-homedir/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 Mitchell Hashimoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

14
vendor/github.com/mitchellh/go-homedir/README.md generated vendored Normal file
View File

@ -0,0 +1,14 @@
# go-homedir
This is a Go library for detecting the user's home directory without
the use of cgo, so the library can be used in cross-compilation environments.
Usage is incredibly simple, just call `homedir.Dir()` to get the home directory
for a user, and `homedir.Expand()` to expand the `~` in a path to the home
directory.
**Why not just use `os/user`?** The built-in `os/user` package requires
cgo on Darwin systems. This means that any Go code that uses that package
cannot cross compile. But 99% of the time the use for `os/user` is just to
retrieve the home directory, which we can do for the current user without
cgo. This library does that, enabling cross-compilation.

1
vendor/github.com/mitchellh/go-homedir/go.mod generated vendored Normal file
View File

@ -0,0 +1 @@
module github.com/mitchellh/go-homedir

167
vendor/github.com/mitchellh/go-homedir/homedir.go generated vendored Normal file
View File

@ -0,0 +1,167 @@
package homedir
import (
"bytes"
"errors"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
)
// DisableCache will disable caching of the home directory. Caching is enabled
// by default.
var DisableCache bool
var homedirCache string
var cacheLock sync.RWMutex
// Dir returns the home directory for the executing user.
//
// This uses an OS-specific method for discovering the home directory.
// An error is returned if a home directory cannot be detected.
func Dir() (string, error) {
if !DisableCache {
cacheLock.RLock()
cached := homedirCache
cacheLock.RUnlock()
if cached != "" {
return cached, nil
}
}
cacheLock.Lock()
defer cacheLock.Unlock()
var result string
var err error
if runtime.GOOS == "windows" {
result, err = dirWindows()
} else {
// Unix-like system, so just assume Unix
result, err = dirUnix()
}
if err != nil {
return "", err
}
homedirCache = result
return result, nil
}
// Expand expands the path to include the home directory if the path
// is prefixed with `~`. If it isn't prefixed with `~`, the path is
// returned as-is.
func Expand(path string) (string, error) {
if len(path) == 0 {
return path, nil
}
if path[0] != '~' {
return path, nil
}
if len(path) > 1 && path[1] != '/' && path[1] != '\\' {
return "", errors.New("cannot expand user-specific home dir")
}
dir, err := Dir()
if err != nil {
return "", err
}
return filepath.Join(dir, path[1:]), nil
}
// Reset clears the cache, forcing the next call to Dir to re-detect
// the home directory. This generally never has to be called, but can be
// useful in tests if you're modifying the home directory via the HOME
// env var or something.
func Reset() {
cacheLock.Lock()
defer cacheLock.Unlock()
homedirCache = ""
}
func dirUnix() (string, error) {
homeEnv := "HOME"
if runtime.GOOS == "plan9" {
// On plan9, env vars are lowercase.
homeEnv = "home"
}
// First prefer the HOME environmental variable
if home := os.Getenv(homeEnv); home != "" {
return home, nil
}
var stdout bytes.Buffer
// If that fails, try OS specific commands
if runtime.GOOS == "darwin" {
cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`)
cmd.Stdout = &stdout
if err := cmd.Run(); err == nil {
result := strings.TrimSpace(stdout.String())
if result != "" {
return result, nil
}
}
} else {
cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid()))
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
// If the error is ErrNotFound, we ignore it. Otherwise, return it.
if err != exec.ErrNotFound {
return "", err
}
} else {
if passwd := strings.TrimSpace(stdout.String()); passwd != "" {
// username:password:uid:gid:gecos:home:shell
passwdParts := strings.SplitN(passwd, ":", 7)
if len(passwdParts) > 5 {
return passwdParts[5], nil
}
}
}
}
// If all else fails, try the shell
stdout.Reset()
cmd := exec.Command("sh", "-c", "cd && pwd")
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return "", err
}
result := strings.TrimSpace(stdout.String())
if result == "" {
return "", errors.New("blank output when reading home directory")
}
return result, nil
}
func dirWindows() (string, error) {
// First prefer the HOME environmental variable
if home := os.Getenv("HOME"); home != "" {
return home, nil
}
// Prefer standard environment variable USERPROFILE
if home := os.Getenv("USERPROFILE"); home != "" {
return home, nil
}
drive := os.Getenv("HOMEDRIVE")
path := os.Getenv("HOMEPATH")
home := drive + path
if drive == "" || path == "" {
return "", errors.New("HOMEDRIVE, HOMEPATH, or USERPROFILE are blank")
}
return home, nil
}

1
vendor/github.com/modern-go/concurrent/.gitignore generated vendored Normal file
View File

@ -0,0 +1 @@
/coverage.txt

14
vendor/github.com/modern-go/concurrent/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,14 @@
language: go
go:
- 1.8.x
- 1.x
before_install:
- go get -t -v ./...
script:
- ./test.sh
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@ -1,2 +1,49 @@
# concurrent
concurrency utilities
[![Sourcegraph](https://sourcegraph.com/github.com/modern-go/concurrent/-/badge.svg)](https://sourcegraph.com/github.com/modern-go/concurrent?badge)
[![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/modern-go/concurrent)
[![Build Status](https://travis-ci.org/modern-go/concurrent.svg?branch=master)](https://travis-ci.org/modern-go/concurrent)
[![codecov](https://codecov.io/gh/modern-go/concurrent/branch/master/graph/badge.svg)](https://codecov.io/gh/modern-go/concurrent)
[![rcard](https://goreportcard.com/badge/github.com/modern-go/concurrent)](https://goreportcard.com/report/github.com/modern-go/concurrent)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://raw.githubusercontent.com/modern-go/concurrent/master/LICENSE)
* concurrent.Map: backport sync.Map for go below 1.9
* concurrent.Executor: goroutine with explicit ownership and cancellable
# concurrent.Map
because sync.Map is only available in go 1.9, we can use concurrent.Map to make code portable
```go
m := concurrent.NewMap()
m.Store("hello", "world")
elem, found := m.Load("hello")
// elem will be "world"
// found will be true
```
# concurrent.Executor
```go
executor := concurrent.NewUnboundedExecutor()
executor.Go(func(ctx context.Context) {
everyMillisecond := time.NewTicker(time.Millisecond)
for {
select {
case <-ctx.Done():
fmt.Println("goroutine exited")
return
case <-everyMillisecond.C:
// do something
}
}
})
time.Sleep(time.Second)
executor.StopAndWaitForever()
fmt.Println("executor stopped")
```
attach goroutine to executor instance, so that we can
* cancel it by stop the executor with Stop/StopAndWait/StopAndWaitForever
* handle panic by callback: the default behavior will no longer crash your application

View File

@ -2,6 +2,13 @@ package concurrent
import "context"
// Executor replace go keyword to start a new goroutine
// the goroutine should cancel itself if the context passed in has been cancelled
// the goroutine started by the executor, is owned by the executor
// we can cancel all executors owned by the executor just by stop the executor itself
// however Executor interface does not Stop method, the one starting and owning executor
// should use the concrete type of executor, instead of this interface.
type Executor interface {
// Go starts a new goroutine controlled by the context
Go(handler func(ctx context.Context))
}

View File

@ -4,10 +4,12 @@ package concurrent
import "sync"
// Map is a wrapper for sync.Map introduced in go1.9
type Map struct {
sync.Map
}
// NewMap creates a thread safe Map
func NewMap() *Map {
return &Map{}
}

View File

@ -4,17 +4,20 @@ package concurrent
import "sync"
// Map implements a thread safe map for go version below 1.9 using mutex
type Map struct {
lock sync.RWMutex
data map[interface{}]interface{}
}
// NewMap creates a thread safe map
func NewMap() *Map {
return &Map{
data: make(map[interface{}]interface{}, 32),
}
}
// Load is same as sync.Map Load
func (m *Map) Load(key interface{}) (elem interface{}, found bool) {
m.lock.RLock()
elem, found = m.data[key]
@ -22,9 +25,9 @@ func (m *Map) Load(key interface{}) (elem interface{}, found bool) {
return
}
// Load is same as sync.Map Store
func (m *Map) Store(key interface{}, elem interface{}) {
m.lock.Lock()
m.data[key] = elem
m.lock.Unlock()
}

13
vendor/github.com/modern-go/concurrent/log.go generated vendored Normal file
View File

@ -0,0 +1,13 @@
package concurrent
import (
"os"
"log"
"io/ioutil"
)
// ErrorLogger is used to print out error, can be set to writer other than stderr
var ErrorLogger = log.New(os.Stderr, "", 0)
// InfoLogger is used to print informational message, default to off
var InfoLogger = log.New(ioutil.Discard, "", 0)

12
vendor/github.com/modern-go/concurrent/test.sh generated vendored Normal file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
echo "" > coverage.txt
for d in $(go list ./... | grep -v vendor); do
go test -coverprofile=profile.out -coverpkg=github.com/modern-go/concurrent $d
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out
fi
done

View File

@ -4,33 +4,37 @@ import (
"context"
"fmt"
"runtime"
"runtime/debug"
"sync"
"time"
"runtime/debug"
"reflect"
)
var LogInfo = func(event string, properties ...interface{}) {
// HandlePanic logs goroutine panic by default
var HandlePanic = func(recovered interface{}, funcName string) {
ErrorLogger.Println(fmt.Sprintf("%s panic: %v", funcName, recovered))
ErrorLogger.Println(string(debug.Stack()))
}
var LogPanic = func(recovered interface{}, properties ...interface{}) interface{} {
fmt.Println(fmt.Sprintf("paniced: %v", recovered))
debug.PrintStack()
return recovered
}
const StopSignal = "STOP!"
// UnboundedExecutor is a executor without limits on counts of alive goroutines
// it tracks the goroutine started by it, and can cancel them when shutdown
type UnboundedExecutor struct {
ctx context.Context
cancel context.CancelFunc
activeGoroutinesMutex *sync.Mutex
activeGoroutines map[string]int
HandlePanic func(recovered interface{}, funcName string)
}
// GlobalUnboundedExecutor has the life cycle of the program itself
// any goroutine want to be shutdown before main exit can be started from this executor
// GlobalUnboundedExecutor expects the main function to call stop
// it does not magically knows the main function exits
var GlobalUnboundedExecutor = NewUnboundedExecutor()
// NewUnboundedExecutor creates a new UnboundedExecutor,
// UnboundedExecutor can not be created by &UnboundedExecutor{}
// HandlePanic can be set with a callback to override global HandlePanic
func NewUnboundedExecutor() *UnboundedExecutor {
ctx, cancel := context.WithCancel(context.TODO())
return &UnboundedExecutor{
@ -41,8 +45,13 @@ func NewUnboundedExecutor() *UnboundedExecutor {
}
}
// Go starts a new goroutine and tracks its lifecycle.
// Panic will be recovered and logged automatically, except for StopSignal
func (executor *UnboundedExecutor) Go(handler func(ctx context.Context)) {
_, file, line, _ := runtime.Caller(1)
pc := reflect.ValueOf(handler).Pointer()
f := runtime.FuncForPC(pc)
funcName := f.Name()
file, line := f.FileLine(pc)
executor.activeGoroutinesMutex.Lock()
defer executor.activeGoroutinesMutex.Unlock()
startFrom := fmt.Sprintf("%s:%d", file, line)
@ -50,46 +59,57 @@ func (executor *UnboundedExecutor) Go(handler func(ctx context.Context)) {
go func() {
defer func() {
recovered := recover()
if recovered != nil && recovered != StopSignal {
LogPanic(recovered)
// if you want to quit a goroutine without trigger HandlePanic
// use runtime.Goexit() to quit
if recovered != nil {
if executor.HandlePanic == nil {
HandlePanic(recovered, funcName)
} else {
executor.HandlePanic(recovered, funcName)
}
}
executor.activeGoroutinesMutex.Lock()
defer executor.activeGoroutinesMutex.Unlock()
executor.activeGoroutines[startFrom] -= 1
executor.activeGoroutinesMutex.Unlock()
}()
handler(executor.ctx)
}()
}
// Stop cancel all goroutines started by this executor without wait
func (executor *UnboundedExecutor) Stop() {
executor.cancel()
}
// StopAndWaitForever cancel all goroutines started by this executor and
// wait until all goroutines exited
func (executor *UnboundedExecutor) StopAndWaitForever() {
executor.StopAndWait(context.Background())
}
// StopAndWait cancel all goroutines started by this executor and wait.
// Wait can be cancelled by the context passed in.
func (executor *UnboundedExecutor) StopAndWait(ctx context.Context) {
executor.cancel()
for {
fiveSeconds := time.NewTimer(time.Millisecond * 100)
oneHundredMilliseconds := time.NewTimer(time.Millisecond * 100)
select {
case <-fiveSeconds.C:
case <-ctx.Done():
case <-oneHundredMilliseconds.C:
if executor.checkNoActiveGoroutines() {
return
}
if executor.checkGoroutines() {
case <-ctx.Done():
return
}
}
}
func (executor *UnboundedExecutor) checkGoroutines() bool {
func (executor *UnboundedExecutor) checkNoActiveGoroutines() bool {
executor.activeGoroutinesMutex.Lock()
defer executor.activeGoroutinesMutex.Unlock()
for startFrom, count := range executor.activeGoroutines {
if count > 0 {
LogInfo("event!unbounded_executor.still waiting goroutines to quit",
InfoLogger.Println("UnboundedExecutor is still waiting goroutines to quit",
"startFrom", startFrom,
"count", count)
return false

View File

@ -4,6 +4,7 @@ import (
"reflect"
"runtime"
"strings"
"sync"
"unsafe"
)
@ -15,10 +16,17 @@ func typelinks1() [][]unsafe.Pointer
//go:linkname typelinks2 reflect.typelinks
func typelinks2() (sections []unsafe.Pointer, offset [][]int32)
var types = map[string]reflect.Type{}
var packages = map[string]map[string]reflect.Type{}
// initOnce guards initialization of types and packages
var initOnce sync.Once
var types map[string]reflect.Type
var packages map[string]map[string]reflect.Type
// discoverTypes initializes types and packages
func discoverTypes() {
types = make(map[string]reflect.Type)
packages = make(map[string]map[string]reflect.Type)
func init() {
ver := runtime.Version()
if ver == "go1.5" || strings.HasPrefix(ver, "go1.5.") {
loadGo15Types()
@ -90,11 +98,13 @@ type emptyInterface struct {
// TypeByName return the type by its name, just like Class.forName in java
func TypeByName(typeName string) Type {
initOnce.Do(discoverTypes)
return Type2(types[typeName])
}
// TypeByPackageName return the type by its package and name
func TypeByPackageName(pkgPath string, name string) Type {
initOnce.Do(discoverTypes)
pkgTypes := packages[pkgPath]
if pkgTypes == nil {
return nil

25
vendor/github.com/rs/zerolog/.gitignore generated vendored Normal file
View File

@ -0,0 +1,25 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
tmp
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

15
vendor/github.com/rs/zerolog/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,15 @@
language: go
go:
- "1.7"
- "1.8"
- "1.9"
- "1.10"
- "1.11"
- "1.12"
- "master"
matrix:
allow_failures:
- go: "master"
script:
- go test -v -race -cpu=1,2,4 -bench . -benchmem ./...
- go test -v -tags binary_log -race -cpu=1,2,4 -bench . -benchmem ./...

1
vendor/github.com/rs/zerolog/CNAME generated vendored Normal file
View File

@ -0,0 +1 @@
zerolog.io

21
vendor/github.com/rs/zerolog/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Olivier Poitrey
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

595
vendor/github.com/rs/zerolog/README.md generated vendored Normal file
View File

@ -0,0 +1,595 @@
# Zero Allocation JSON Logger
[![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/rs/zerolog) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/zerolog/master/LICENSE) [![Build Status](https://travis-ci.org/rs/zerolog.svg?branch=master)](https://travis-ci.org/rs/zerolog) [![Coverage](http://gocover.io/_badge/github.com/rs/zerolog)](http://gocover.io/github.com/rs/zerolog)
The zerolog package provides a fast and simple logger dedicated to JSON output.
Zerolog's API is designed to provide both a great developer experience and stunning [performance](#benchmarks). Its unique chaining API allows zerolog to write JSON (or CBOR) log events by avoiding allocations and reflection.
Uber's [zap](https://godoc.org/go.uber.org/zap) library pioneered this approach. Zerolog is taking this concept to the next level with a simpler to use API and even better performance.
To keep the code base and the API simple, zerolog focuses on efficient structured logging only. Pretty logging on the console is made possible using the provided (but inefficient) [`zerolog.ConsoleWriter`](#pretty-logging).
![Pretty Logging Image](pretty.png)
## Who uses zerolog
Find out [who uses zerolog](https://github.com/rs/zerolog/wiki/Who-uses-zerolog) and add your company / project to the list.
## Features
* Blazing fast
* Low to zero allocation
* Level logging
* Sampling
* Hooks
* Contextual fields
* `context.Context` integration
* `net/http` helpers
* JSON and CBOR encoding formats
* Pretty logging for development
## Installation
```bash
go get -u github.com/rs/zerolog/log
```
## Getting Started
### Simple Logging Example
For simple logging, import the global logger package **github.com/rs/zerolog/log**
```go
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// UNIX Time is faster and smaller than most timestamps
// If you set zerolog.TimeFieldFormat to an empty string,
// logs will write with UNIX time
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Print("hello world")
}
// Output: {"time":1516134303,"level":"debug","message":"hello world"}
```
> Note: By default log writes to `os.Stderr`
> Note: The default log level for `log.Print` is *debug*
### Contextual Logging
**zerolog** allows data to be added to log messages in the form of key:value pairs. The data added to the message adds "context" about the log event that can be critical for debugging as well as myriad other purposes. An example of this is below:
```go
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Debug().
Str("Scale", "833 cents").
Float64("Interval", 833.09).
Msg("Fibonacci is everywhere")
log.Debug().
Str("Name", "Tom").
Send()
}
// Output: {"level":"debug","Scale":"833 cents","Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere"}
// Output: {"level":"debug","Name":"Tom","time":1562212768}
```
> You'll note in the above example that when adding contextual fields, the fields are strongly typed. You can find the full list of supported fields [here](#standard-types)
### Leveled Logging
#### Simple Leveled Logging Example
```go
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Info().Msg("hello world")
}
// Output: {"time":1516134303,"level":"info","message":"hello world"}
```
> It is very important to note that when using the **zerolog** chaining API, as shown above (`log.Info().Msg("hello world"`), the chain must have either the `Msg` or `Msgf` method call. If you forget to add either of these, the log will not occur and there is no compile time error to alert you of this.
**zerolog** allows for logging at the following levels (from highest to lowest):
* panic (`zerolog.PanicLevel`, 5)
* fatal (`zerolog.FatalLevel`, 4)
* error (`zerolog.ErrorLevel`, 3)
* warn (`zerolog.WarnLevel`, 2)
* info (`zerolog.InfoLevel`, 1)
* debug (`zerolog.DebugLevel`, 0)
* trace (`zerolog.TraceLevel`, -1)
You can set the Global logging level to any of these options using the `SetGlobalLevel` function in the zerolog package, passing in one of the given constants above, e.g. `zerolog.InfoLevel` would be the "info" level. Whichever level is chosen, all logs with a level greater than or equal to that level will be written. To turn off logging entirely, pass the `zerolog.Disabled` constant.
#### Setting Global Log Level
This example uses command-line flags to demonstrate various outputs depending on the chosen log level.
```go
package main
import (
"flag"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
debug := flag.Bool("debug", false, "sets log level to debug")
flag.Parse()
// Default level for this example is info, unless debug flag is present
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if *debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
log.Debug().Msg("This message appears only when log level set to Debug")
log.Info().Msg("This message appears when log level set to Debug or Info")
if e := log.Debug(); e.Enabled() {
// Compute log output only if enabled.
value := "bar"
e.Str("foo", value).Msg("some debug message")
}
}
```
Info Output (no flag)
```bash
$ ./logLevelExample
{"time":1516387492,"level":"info","message":"This message appears when log level set to Debug or Info"}
```
Debug Output (debug flag set)
```bash
$ ./logLevelExample -debug
{"time":1516387573,"level":"debug","message":"This message appears only when log level set to Debug"}
{"time":1516387573,"level":"info","message":"This message appears when log level set to Debug or Info"}
{"time":1516387573,"level":"debug","foo":"bar","message":"some debug message"}
```
#### Logging without Level or Message
You may choose to log without a specific level by using the `Log` method. You may also write without a message by setting an empty string in the `msg string` parameter of the `Msg` method. Both are demonstrated in the example below.
```go
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Log().
Str("foo", "bar").
Msg("")
}
// Output: {"time":1494567715,"foo":"bar"}
```
#### Logging Fatal Messages
```go
package main
import (
"errors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
err := errors.New("A repo man spends his life getting into tense situations")
service := "myservice"
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Fatal().
Err(err).
Str("service", service).
Msgf("Cannot start %s", service)
}
// Output: {"time":1516133263,"level":"fatal","error":"A repo man spends his life getting into tense situations","service":"myservice","message":"Cannot start myservice"}
// exit status 1
```
> NOTE: Using `Msgf` generates one allocation even when the logger is disabled.
### Create logger instance to manage different outputs
```go
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
logger.Info().Str("foo", "bar").Msg("hello world")
// Output: {"level":"info","time":1494567715,"message":"hello world","foo":"bar"}
```
### Sub-loggers let you chain loggers with additional context
```go
sublogger := log.With().
Str("component", "foo").
Logger()
sublogger.Info().Msg("hello world")
// Output: {"level":"info","time":1494567715,"message":"hello world","component":"foo"}
```
### Pretty logging
To log a human-friendly, colorized output, use `zerolog.ConsoleWriter`:
```go
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
log.Info().Str("foo", "bar").Msg("Hello world")
// Output: 3:04PM INF Hello World foo=bar
```
To customize the configuration and formatting:
```go
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
output.FormatLevel = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
}
output.FormatMessage = func(i interface{}) string {
return fmt.Sprintf("***%s****", i)
}
output.FormatFieldName = func(i interface{}) string {
return fmt.Sprintf("%s:", i)
}
output.FormatFieldValue = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("%s", i))
}
log := zerolog.New(output).With().Timestamp().Logger()
log.Info().Str("foo", "bar").Msg("Hello World")
// Output: 2006-01-02T15:04:05Z07:00 | INFO | ***Hello World**** foo:BAR
```
### Sub dictionary
```go
log.Info().
Str("foo", "bar").
Dict("dict", zerolog.Dict().
Str("bar", "baz").
Int("n", 1),
).Msg("hello world")
// Output: {"level":"info","time":1494567715,"foo":"bar","dict":{"bar":"baz","n":1},"message":"hello world"}
```
### Customize automatic field names
```go
zerolog.TimestampFieldName = "t"
zerolog.LevelFieldName = "l"
zerolog.MessageFieldName = "m"
log.Info().Msg("hello world")
// Output: {"l":"info","t":1494567715,"m":"hello world"}
```
### Add contextual fields to the global logger
```go
log.Logger = log.With().Str("foo", "bar").Logger()
```
### Add file and line number to log
```go
log.Logger = log.With().Caller().Logger()
log.Info().Msg("hello world")
// Output: {"level": "info", "message": "hello world", "caller": "/go/src/your_project/some_file:21"}
```
### Thread-safe, lock-free, non-blocking writer
If your writer might be slow or not thread-safe and you need your log producers to never get slowed down by a slow writer, you can use a `diode.Writer` as follow:
```go
wr := diode.NewWriter(os.Stdout, 1000, 10*time.Millisecond, func(missed int) {
fmt.Printf("Logger Dropped %d messages", missed)
})
log := zerolog.New(w)
log.Print("test")
```
You will need to install `code.cloudfoundry.org/go-diodes` to use this feature.
### Log Sampling
```go
sampled := log.Sample(&zerolog.BasicSampler{N: 10})
sampled.Info().Msg("will be logged every 10 messages")
// Output: {"time":1494567715,"level":"info","message":"will be logged every 10 messages"}
```
More advanced sampling:
```go
// Will let 5 debug messages per period of 1 second.
// Over 5 debug message, 1 every 100 debug messages are logged.
// Other levels are not sampled.
sampled := log.Sample(zerolog.LevelSampler{
DebugSampler: &zerolog.BurstSampler{
Burst: 5,
Period: 1*time.Second,
NextSampler: &zerolog.BasicSampler{N: 100},
},
})
sampled.Debug().Msg("hello world")
// Output: {"time":1494567715,"level":"debug","message":"hello world"}
```
### Hooks
```go
type SeverityHook struct{}
func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
if level != zerolog.NoLevel {
e.Str("severity", level.String())
}
}
hooked := log.Hook(SeverityHook{})
hooked.Warn().Msg("")
// Output: {"level":"warn","severity":"warn"}
```
### Pass a sub-logger by context
```go
ctx := log.With().Str("component", "module").Logger().WithContext(ctx)
log.Ctx(ctx).Info().Msg("hello world")
// Output: {"component":"module","level":"info","message":"hello world"}
```
### Set as standard logger output
```go
log := zerolog.New(os.Stdout).With().
Str("foo", "bar").
Logger()
stdlog.SetFlags(0)
stdlog.SetOutput(log)
stdlog.Print("hello world")
// Output: {"foo":"bar","message":"hello world"}
```
### Integration with `net/http`
The `github.com/rs/zerolog/hlog` package provides some helpers to integrate zerolog with `http.Handler`.
In this example we use [alice](https://github.com/justinas/alice) to install logger for better readability.
```go
log := zerolog.New(os.Stdout).With().
Timestamp().
Str("role", "my-service").
Str("host", host).
Logger()
c := alice.New()
// Install the logger handler with default output on the console
c = c.Append(hlog.NewHandler(log))
// Install some provided extra handler to set some request's context fields.
// Thanks to those handler, all our logs will come with some pre-populated fields.
c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
hlog.FromRequest(r).Info().
Str("method", r.Method).
Str("url", r.URL.String()).
Int("status", status).
Int("size", size).
Dur("duration", duration).
Msg("")
}))
c = c.Append(hlog.RemoteAddrHandler("ip"))
c = c.Append(hlog.UserAgentHandler("user_agent"))
c = c.Append(hlog.RefererHandler("referer"))
c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id"))
// Here is your final handler
h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get the logger from the request's context. You can safely assume it
// will be always there: if the handler is removed, hlog.FromRequest
// will return a no-op logger.
hlog.FromRequest(r).Info().
Str("user", "current user").
Str("status", "ok").
Msg("Something happened")
// Output: {"level":"info","time":"2001-02-03T04:05:06Z","role":"my-service","host":"local-hostname","req_id":"b4g0l5t6tfid6dtrapu0","user":"current user","status":"ok","message":"Something happened"}
}))
http.Handle("/", h)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal().Err(err).Msg("Startup failed")
}
```
## Global Settings
Some settings can be changed and will by applied to all loggers:
* `log.Logger`: You can set this value to customize the global logger (the one used by package level methods).
* `zerolog.SetGlobalLevel`: Can raise the minimum level of all loggers. Set this to `zerolog.Disabled` to disable logging altogether (quiet mode).
* `zerolog.DisableSampling`: If argument is `true`, all sampled loggers will stop sampling and issue 100% of their log events.
* `zerolog.TimestampFieldName`: Can be set to customize `Timestamp` field name.
* `zerolog.LevelFieldName`: Can be set to customize level field name.
* `zerolog.MessageFieldName`: Can be set to customize message field name.
* `zerolog.ErrorFieldName`: Can be set to customize `Err` field name.
* `zerolog.TimeFieldFormat`: Can be set to customize `Time` field value formatting. If set with `zerolog.TimeFormatUnix`, `zerolog.TimeFormatUnixMs` or `zerolog.TimeFormatUnixMicro`, times are formated as UNIX timestamp.
* `zerolog.DurationFieldUnit`: Can be set to customize the unit for time.Duration type fields added by `Dur` (default: `time.Millisecond`).
* `zerolog.DurationFieldInteger`: If set to `true`, `Dur` fields are formatted as integers instead of floats (default: `false`).
* `zerolog.ErrorHandler`: Called whenever zerolog fails to write an event on its output. If not set, an error is printed on the stderr. This handler must be thread safe and non-blocking.
## Field Types
### Standard Types
* `Str`
* `Bool`
* `Int`, `Int8`, `Int16`, `Int32`, `Int64`
* `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
* `Float32`, `Float64`
### Advanced Fields
* `Err`: Takes an `error` and render it as a string using the `zerolog.ErrorFieldName` field name.
* `Timestamp`: Insert a timestamp field with `zerolog.TimestampFieldName` field name and formatted using `zerolog.TimeFieldFormat`.
* `Time`: Adds a field with the time formated with the `zerolog.TimeFieldFormat`.
* `Dur`: Adds a field with a `time.Duration`.
* `Dict`: Adds a sub-key/value as a field of the event.
* `Interface`: Uses reflection to marshal the type.
## Binary Encoding
In addition to the default JSON encoding, `zerolog` can produce binary logs using [CBOR](http://cbor.io) encoding. The choice of encoding can be decided at compile time using the build tag `binary_log` as follows:
```bash
go build -tags binary_log .
```
To Decode binary encoded log files you can use any CBOR decoder. One has been tested to work
with zerolog library is [CSD](https://github.com/toravir/csd/).
## Related Projects
* [grpc-zerolog](https://github.com/cheapRoc/grpc-zerolog): Implementation of `grpclog.LoggerV2` interface using `zerolog`
## Benchmarks
See [logbench](http://hackemist.com/logbench/) for more comprehensive and up-to-date benchmarks.
All operations are allocation free (those numbers *include* JSON encoding):
```text
BenchmarkLogEmpty-8 100000000 19.1 ns/op 0 B/op 0 allocs/op
BenchmarkDisabled-8 500000000 4.07 ns/op 0 B/op 0 allocs/op
BenchmarkInfo-8 30000000 42.5 ns/op 0 B/op 0 allocs/op
BenchmarkContextFields-8 30000000 44.9 ns/op 0 B/op 0 allocs/op
BenchmarkLogFields-8 10000000 184 ns/op 0 B/op 0 allocs/op
```
There are a few Go logging benchmarks and comparisons that include zerolog.
* [imkira/go-loggers-bench](https://github.com/imkira/go-loggers-bench)
* [uber-common/zap](https://github.com/uber-go/zap#performance)
Using Uber's zap comparison benchmark:
Log a message and 10 fields:
| Library | Time | Bytes Allocated | Objects Allocated |
| :--- | :---: | :---: | :---: |
| zerolog | 767 ns/op | 552 B/op | 6 allocs/op |
| :zap: zap | 848 ns/op | 704 B/op | 2 allocs/op |
| :zap: zap (sugared) | 1363 ns/op | 1610 B/op | 20 allocs/op |
| go-kit | 3614 ns/op | 2895 B/op | 66 allocs/op |
| lion | 5392 ns/op | 5807 B/op | 63 allocs/op |
| logrus | 5661 ns/op | 6092 B/op | 78 allocs/op |
| apex/log | 15332 ns/op | 3832 B/op | 65 allocs/op |
| log15 | 20657 ns/op | 5632 B/op | 93 allocs/op |
Log a message with a logger that already has 10 fields of context:
| Library | Time | Bytes Allocated | Objects Allocated |
| :--- | :---: | :---: | :---: |
| zerolog | 52 ns/op | 0 B/op | 0 allocs/op |
| :zap: zap | 283 ns/op | 0 B/op | 0 allocs/op |
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
| lion | 2702 ns/op | 4074 B/op | 38 allocs/op |
| go-kit | 3378 ns/op | 3046 B/op | 52 allocs/op |
| logrus | 4309 ns/op | 4564 B/op | 63 allocs/op |
| apex/log | 13456 ns/op | 2898 B/op | 51 allocs/op |
| log15 | 14179 ns/op | 2642 B/op | 44 allocs/op |
Log a static string, without any context or `printf`-style templating:
| Library | Time | Bytes Allocated | Objects Allocated |
| :--- | :---: | :---: | :---: |
| zerolog | 50 ns/op | 0 B/op | 0 allocs/op |
| :zap: zap | 236 ns/op | 0 B/op | 0 allocs/op |
| standard library | 453 ns/op | 80 B/op | 2 allocs/op |
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
| go-kit | 508 ns/op | 656 B/op | 13 allocs/op |
| lion | 771 ns/op | 1224 B/op | 10 allocs/op |
| logrus | 1244 ns/op | 1505 B/op | 27 allocs/op |
| apex/log | 2751 ns/op | 584 B/op | 11 allocs/op |
| log15 | 5181 ns/op | 1592 B/op | 26 allocs/op |
## Caveats
Note that zerolog does no de-duplication of fields. Using the same key multiple times creates multiple keys in final JSON:
```go
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
logger.Info().
Timestamp().
Msg("dup")
// Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}
```
In this case, many consumers will take the last value, but this is not guaranteed; check yours if in doubt.

1
vendor/github.com/rs/zerolog/_config.yml generated vendored Normal file
View File

@ -0,0 +1 @@
remote_theme: rs/gh-readme

233
vendor/github.com/rs/zerolog/array.go generated vendored Normal file
View File

@ -0,0 +1,233 @@
package zerolog
import (
"net"
"sync"
"time"
)
var arrayPool = &sync.Pool{
New: func() interface{} {
return &Array{
buf: make([]byte, 0, 500),
}
},
}
// Array is used to prepopulate an array of items
// which can be re-used to add to log messages.
type Array struct {
buf []byte
}
func putArray(a *Array) {
// Proper usage of a sync.Pool requires each entry to have approximately
// the same memory cost. To obtain this property when the stored type
// contains a variably-sized buffer, we add a hard limit on the maximum buffer
// to place back in the pool.
//
// See https://golang.org/issue/23199
const maxSize = 1 << 16 // 64KiB
if cap(a.buf) > maxSize {
return
}
arrayPool.Put(a)
}
// Arr creates an array to be added to an Event or Context.
func Arr() *Array {
a := arrayPool.Get().(*Array)
a.buf = a.buf[:0]
return a
}
// MarshalZerologArray method here is no-op - since data is
// already in the needed format.
func (*Array) MarshalZerologArray(*Array) {
}
func (a *Array) write(dst []byte) []byte {
dst = enc.AppendArrayStart(dst)
if len(a.buf) > 0 {
dst = append(append(dst, a.buf...))
}
dst = enc.AppendArrayEnd(dst)
putArray(a)
return dst
}
// Object marshals an object that implement the LogObjectMarshaler
// interface and append append it to the array.
func (a *Array) Object(obj LogObjectMarshaler) *Array {
e := Dict()
obj.MarshalZerologObject(e)
e.buf = enc.AppendEndMarker(e.buf)
a.buf = append(enc.AppendArrayDelim(a.buf), e.buf...)
putEvent(e)
return a
}
// Str append append the val as a string to the array.
func (a *Array) Str(val string) *Array {
a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), val)
return a
}
// Bytes append append the val as a string to the array.
func (a *Array) Bytes(val []byte) *Array {
a.buf = enc.AppendBytes(enc.AppendArrayDelim(a.buf), val)
return a
}
// Hex append append the val as a hex string to the array.
func (a *Array) Hex(val []byte) *Array {
a.buf = enc.AppendHex(enc.AppendArrayDelim(a.buf), val)
return a
}
// RawJSON adds already encoded JSON to the array.
func (a *Array) RawJSON(val []byte) *Array {
a.buf = appendJSON(enc.AppendArrayDelim(a.buf), val)
return a
}
// Err serializes and appends the err to the array.
func (a *Array) Err(err error) *Array {
switch m := ErrorMarshalFunc(err).(type) {
case LogObjectMarshaler:
e := newEvent(nil, 0)
e.buf = e.buf[:0]
e.appendObject(m)
a.buf = append(enc.AppendArrayDelim(a.buf), e.buf...)
putEvent(e)
case error:
if m == nil || isNilValue(m) {
a.buf = enc.AppendNil(enc.AppendArrayDelim(a.buf))
} else {
a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m.Error())
}
case string:
a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m)
default:
a.buf = enc.AppendInterface(enc.AppendArrayDelim(a.buf), m)
}
return a
}
// Bool append append the val as a bool to the array.
func (a *Array) Bool(b bool) *Array {
a.buf = enc.AppendBool(enc.AppendArrayDelim(a.buf), b)
return a
}
// Int append append i as a int to the array.
func (a *Array) Int(i int) *Array {
a.buf = enc.AppendInt(enc.AppendArrayDelim(a.buf), i)
return a
}
// Int8 append append i as a int8 to the array.
func (a *Array) Int8(i int8) *Array {
a.buf = enc.AppendInt8(enc.AppendArrayDelim(a.buf), i)
return a
}
// Int16 append append i as a int16 to the array.
func (a *Array) Int16(i int16) *Array {
a.buf = enc.AppendInt16(enc.AppendArrayDelim(a.buf), i)
return a
}
// Int32 append append i as a int32 to the array.
func (a *Array) Int32(i int32) *Array {
a.buf = enc.AppendInt32(enc.AppendArrayDelim(a.buf), i)
return a
}
// Int64 append append i as a int64 to the array.
func (a *Array) Int64(i int64) *Array {
a.buf = enc.AppendInt64(enc.AppendArrayDelim(a.buf), i)
return a
}
// Uint append append i as a uint to the array.
func (a *Array) Uint(i uint) *Array {
a.buf = enc.AppendUint(enc.AppendArrayDelim(a.buf), i)
return a
}
// Uint8 append append i as a uint8 to the array.
func (a *Array) Uint8(i uint8) *Array {
a.buf = enc.AppendUint8(enc.AppendArrayDelim(a.buf), i)
return a
}
// Uint16 append append i as a uint16 to the array.
func (a *Array) Uint16(i uint16) *Array {
a.buf = enc.AppendUint16(enc.AppendArrayDelim(a.buf), i)
return a
}
// Uint32 append append i as a uint32 to the array.
func (a *Array) Uint32(i uint32) *Array {
a.buf = enc.AppendUint32(enc.AppendArrayDelim(a.buf), i)
return a
}
// Uint64 append append i as a uint64 to the array.
func (a *Array) Uint64(i uint64) *Array {
a.buf = enc.AppendUint64(enc.AppendArrayDelim(a.buf), i)
return a
}
// Float32 append append f as a float32 to the array.
func (a *Array) Float32(f float32) *Array {
a.buf = enc.AppendFloat32(enc.AppendArrayDelim(a.buf), f)
return a
}
// Float64 append append f as a float64 to the array.
func (a *Array) Float64(f float64) *Array {
a.buf = enc.AppendFloat64(enc.AppendArrayDelim(a.buf), f)
return a
}
// Time append append t formated as string using zerolog.TimeFieldFormat.
func (a *Array) Time(t time.Time) *Array {
a.buf = enc.AppendTime(enc.AppendArrayDelim(a.buf), t, TimeFieldFormat)
return a
}
// Dur append append d to the array.
func (a *Array) Dur(d time.Duration) *Array {
a.buf = enc.AppendDuration(enc.AppendArrayDelim(a.buf), d, DurationFieldUnit, DurationFieldInteger)
return a
}
// Interface append append i marshaled using reflection.
func (a *Array) Interface(i interface{}) *Array {
if obj, ok := i.(LogObjectMarshaler); ok {
return a.Object(obj)
}
a.buf = enc.AppendInterface(enc.AppendArrayDelim(a.buf), i)
return a
}
// IPAddr adds IPv4 or IPv6 address to the array
func (a *Array) IPAddr(ip net.IP) *Array {
a.buf = enc.AppendIPAddr(enc.AppendArrayDelim(a.buf), ip)
return a
}
// IPPrefix adds IPv4 or IPv6 Prefix (IP + mask) to the array
func (a *Array) IPPrefix(pfx net.IPNet) *Array {
a.buf = enc.AppendIPPrefix(enc.AppendArrayDelim(a.buf), pfx)
return a
}
// MACAddr adds a MAC (Ethernet) address to the array
func (a *Array) MACAddr(ha net.HardwareAddr) *Array {
a.buf = enc.AppendMACAddr(enc.AppendArrayDelim(a.buf), ha)
return a
}

397
vendor/github.com/rs/zerolog/console.go generated vendored Normal file
View File

@ -0,0 +1,397 @@
package zerolog
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
"sync"
"time"
)
const (
colorBlack = iota + 30
colorRed
colorGreen
colorYellow
colorBlue
colorMagenta
colorCyan
colorWhite
colorBold = 1
colorDarkGray = 90
)
var (
consoleBufPool = sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 100))
},
}
)
const (
consoleDefaultTimeFormat = time.Kitchen
)
// Formatter transforms the input into a formatted string.
type Formatter func(interface{}) string
// ConsoleWriter parses the JSON input and writes it in an
// (optionally) colorized, human-friendly format to Out.
type ConsoleWriter struct {
// Out is the output destination.
Out io.Writer
// NoColor disables the colorized output.
NoColor bool
// TimeFormat specifies the format for timestamp in output.
TimeFormat string
// PartsOrder defines the order of parts in output.
PartsOrder []string
FormatTimestamp Formatter
FormatLevel Formatter
FormatCaller Formatter
FormatMessage Formatter
FormatFieldName Formatter
FormatFieldValue Formatter
FormatErrFieldName Formatter
FormatErrFieldValue Formatter
}
// NewConsoleWriter creates and initializes a new ConsoleWriter.
func NewConsoleWriter(options ...func(w *ConsoleWriter)) ConsoleWriter {
w := ConsoleWriter{
Out: os.Stdout,
TimeFormat: consoleDefaultTimeFormat,
PartsOrder: consoleDefaultPartsOrder(),
}
for _, opt := range options {
opt(&w)
}
return w
}
// Write transforms the JSON input with formatters and appends to w.Out.
func (w ConsoleWriter) Write(p []byte) (n int, err error) {
if w.PartsOrder == nil {
w.PartsOrder = consoleDefaultPartsOrder()
}
var buf = consoleBufPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
consoleBufPool.Put(buf)
}()
var evt map[string]interface{}
p = decodeIfBinaryToBytes(p)
d := json.NewDecoder(bytes.NewReader(p))
d.UseNumber()
err = d.Decode(&evt)
if err != nil {
return n, fmt.Errorf("cannot decode event: %s", err)
}
for _, p := range w.PartsOrder {
w.writePart(buf, evt, p)
}
w.writeFields(evt, buf)
err = buf.WriteByte('\n')
if err != nil {
return n, err
}
_, err = buf.WriteTo(w.Out)
return len(p), err
}
// writeFields appends formatted key-value pairs to buf.
func (w ConsoleWriter) writeFields(evt map[string]interface{}, buf *bytes.Buffer) {
var fields = make([]string, 0, len(evt))
for field := range evt {
switch field {
case LevelFieldName, TimestampFieldName, MessageFieldName, CallerFieldName:
continue
}
fields = append(fields, field)
}
sort.Strings(fields)
if len(fields) > 0 {
buf.WriteByte(' ')
}
// Move the "error" field to the front
ei := sort.Search(len(fields), func(i int) bool { return fields[i] >= ErrorFieldName })
if ei < len(fields) && fields[ei] == ErrorFieldName {
fields[ei] = ""
fields = append([]string{ErrorFieldName}, fields...)
var xfields = make([]string, 0, len(fields))
for _, field := range fields {
if field == "" { // Skip empty fields
continue
}
xfields = append(xfields, field)
}
fields = xfields
}
for i, field := range fields {
var fn Formatter
var fv Formatter
if field == ErrorFieldName {
if w.FormatErrFieldName == nil {
fn = consoleDefaultFormatErrFieldName(w.NoColor)
} else {
fn = w.FormatErrFieldName
}
if w.FormatErrFieldValue == nil {
fv = consoleDefaultFormatErrFieldValue(w.NoColor)
} else {
fv = w.FormatErrFieldValue
}
} else {
if w.FormatFieldName == nil {
fn = consoleDefaultFormatFieldName(w.NoColor)
} else {
fn = w.FormatFieldName
}
if w.FormatFieldValue == nil {
fv = consoleDefaultFormatFieldValue
} else {
fv = w.FormatFieldValue
}
}
buf.WriteString(fn(field))
switch fValue := evt[field].(type) {
case string:
if needsQuote(fValue) {
buf.WriteString(fv(strconv.Quote(fValue)))
} else {
buf.WriteString(fv(fValue))
}
case json.Number:
buf.WriteString(fv(fValue))
default:
b, err := json.Marshal(fValue)
if err != nil {
fmt.Fprintf(buf, colorize("[error: %v]", colorRed, w.NoColor), err)
} else {
fmt.Fprint(buf, fv(b))
}
}
if i < len(fields)-1 { // Skip space for last field
buf.WriteByte(' ')
}
}
}
// writePart appends a formatted part to buf.
func (w ConsoleWriter) writePart(buf *bytes.Buffer, evt map[string]interface{}, p string) {
var f Formatter
switch p {
case LevelFieldName:
if w.FormatLevel == nil {
f = consoleDefaultFormatLevel(w.NoColor)
} else {
f = w.FormatLevel
}
case TimestampFieldName:
if w.FormatTimestamp == nil {
f = consoleDefaultFormatTimestamp(w.TimeFormat, w.NoColor)
} else {
f = w.FormatTimestamp
}
case MessageFieldName:
if w.FormatMessage == nil {
f = consoleDefaultFormatMessage
} else {
f = w.FormatMessage
}
case CallerFieldName:
if w.FormatCaller == nil {
f = consoleDefaultFormatCaller(w.NoColor)
} else {
f = w.FormatCaller
}
default:
if w.FormatFieldValue == nil {
f = consoleDefaultFormatFieldValue
} else {
f = w.FormatFieldValue
}
}
var s = f(evt[p])
if len(s) > 0 {
buf.WriteString(s)
if p != w.PartsOrder[len(w.PartsOrder)-1] { // Skip space for last part
buf.WriteByte(' ')
}
}
}
// needsQuote returns true when the string s should be quoted in output.
func needsQuote(s string) bool {
for i := range s {
if s[i] < 0x20 || s[i] > 0x7e || s[i] == ' ' || s[i] == '\\' || s[i] == '"' {
return true
}
}
return false
}
// colorize returns the string s wrapped in ANSI code c, unless disabled is true.
func colorize(s interface{}, c int, disabled bool) string {
if disabled {
return fmt.Sprintf("%s", s)
}
return fmt.Sprintf("\x1b[%dm%v\x1b[0m", c, s)
}
// ----- DEFAULT FORMATTERS ---------------------------------------------------
func consoleDefaultPartsOrder() []string {
return []string{
TimestampFieldName,
LevelFieldName,
CallerFieldName,
MessageFieldName,
}
}
func consoleDefaultFormatTimestamp(timeFormat string, noColor bool) Formatter {
if timeFormat == "" {
timeFormat = consoleDefaultTimeFormat
}
return func(i interface{}) string {
t := "<nil>"
switch tt := i.(type) {
case string:
ts, err := time.Parse(TimeFieldFormat, tt)
if err != nil {
t = tt
} else {
t = ts.Format(timeFormat)
}
case json.Number:
i, err := tt.Int64()
if err != nil {
t = tt.String()
} else {
var sec, nsec int64 = i, 0
switch TimeFieldFormat {
case TimeFormatUnixMs:
nsec = int64(time.Duration(i) * time.Millisecond)
sec = 0
case TimeFormatUnixMicro:
nsec = int64(time.Duration(i) * time.Microsecond)
sec = 0
}
ts := time.Unix(sec, nsec).UTC()
t = ts.Format(timeFormat)
}
}
return colorize(t, colorDarkGray, noColor)
}
}
func consoleDefaultFormatLevel(noColor bool) Formatter {
return func(i interface{}) string {
var l string
if ll, ok := i.(string); ok {
switch ll {
case "trace":
l = colorize("TRC", colorMagenta, noColor)
case "debug":
l = colorize("DBG", colorYellow, noColor)
case "info":
l = colorize("INF", colorGreen, noColor)
case "warn":
l = colorize("WRN", colorRed, noColor)
case "error":
l = colorize(colorize("ERR", colorRed, noColor), colorBold, noColor)
case "fatal":
l = colorize(colorize("FTL", colorRed, noColor), colorBold, noColor)
case "panic":
l = colorize(colorize("PNC", colorRed, noColor), colorBold, noColor)
default:
l = colorize("???", colorBold, noColor)
}
} else {
if i == nil {
l = colorize("???", colorBold, noColor)
} else {
l = strings.ToUpper(fmt.Sprintf("%s", i))[0:3]
}
}
return l
}
}
func consoleDefaultFormatCaller(noColor bool) Formatter {
return func(i interface{}) string {
var c string
if cc, ok := i.(string); ok {
c = cc
}
if len(c) > 0 {
cwd, err := os.Getwd()
if err == nil {
c = strings.TrimPrefix(c, cwd)
c = strings.TrimPrefix(c, "/")
}
c = colorize(c, colorBold, noColor) + colorize(" >", colorCyan, noColor)
}
return c
}
}
func consoleDefaultFormatMessage(i interface{}) string {
if i == nil {
return ""
}
return fmt.Sprintf("%s", i)
}
func consoleDefaultFormatFieldName(noColor bool) Formatter {
return func(i interface{}) string {
return colorize(fmt.Sprintf("%s=", i), colorCyan, noColor)
}
}
func consoleDefaultFormatFieldValue(i interface{}) string {
return fmt.Sprintf("%s", i)
}
func consoleDefaultFormatErrFieldName(noColor bool) Formatter {
return func(i interface{}) string {
return colorize(fmt.Sprintf("%s=", i), colorRed, noColor)
}
}
func consoleDefaultFormatErrFieldValue(noColor bool) Formatter {
return func(i interface{}) string {
return colorize(fmt.Sprintf("%s", i), colorRed, noColor)
}
}

427
vendor/github.com/rs/zerolog/context.go generated vendored Normal file
View File

@ -0,0 +1,427 @@
package zerolog
import (
"io/ioutil"
"math"
"net"
"time"
)
// Context configures a new sub-logger with contextual fields.
type Context struct {
l Logger
}
// Logger returns the logger with the context previously set.
func (c Context) Logger() Logger {
return c.l
}
// Fields is a helper function to use a map to set fields using type assertion.
func (c Context) Fields(fields map[string]interface{}) Context {
c.l.context = appendFields(c.l.context, fields)
return c
}
// Dict adds the field key with the dict to the logger context.
func (c Context) Dict(key string, dict *Event) Context {
dict.buf = enc.AppendEndMarker(dict.buf)
c.l.context = append(enc.AppendKey(c.l.context, key), dict.buf...)
putEvent(dict)
return c
}
// Array adds the field key with an array to the event context.
// Use zerolog.Arr() to create the array or pass a type that
// implement the LogArrayMarshaler interface.
func (c Context) Array(key string, arr LogArrayMarshaler) Context {
c.l.context = enc.AppendKey(c.l.context, key)
if arr, ok := arr.(*Array); ok {
c.l.context = arr.write(c.l.context)
return c
}
var a *Array
if aa, ok := arr.(*Array); ok {
a = aa
} else {
a = Arr()
arr.MarshalZerologArray(a)
}
c.l.context = a.write(c.l.context)
return c
}
// Object marshals an object that implement the LogObjectMarshaler interface.
func (c Context) Object(key string, obj LogObjectMarshaler) Context {
e := newEvent(levelWriterAdapter{ioutil.Discard}, 0)
e.Object(key, obj)
c.l.context = enc.AppendObjectData(c.l.context, e.buf)
putEvent(e)
return c
}
// EmbedObject marshals and Embeds an object that implement the LogObjectMarshaler interface.
func (c Context) EmbedObject(obj LogObjectMarshaler) Context {
e := newEvent(levelWriterAdapter{ioutil.Discard}, 0)
e.EmbedObject(obj)
c.l.context = enc.AppendObjectData(c.l.context, e.buf)
putEvent(e)
return c
}
// Str adds the field key with val as a string to the logger context.
func (c Context) Str(key, val string) Context {
c.l.context = enc.AppendString(enc.AppendKey(c.l.context, key), val)
return c
}
// Strs adds the field key with val as a string to the logger context.
func (c Context) Strs(key string, vals []string) Context {
c.l.context = enc.AppendStrings(enc.AppendKey(c.l.context, key), vals)
return c
}
// Bytes adds the field key with val as a []byte to the logger context.
func (c Context) Bytes(key string, val []byte) Context {
c.l.context = enc.AppendBytes(enc.AppendKey(c.l.context, key), val)
return c
}
// Hex adds the field key with val as a hex string to the logger context.
func (c Context) Hex(key string, val []byte) Context {
c.l.context = enc.AppendHex(enc.AppendKey(c.l.context, key), val)
return c
}
// RawJSON adds already encoded JSON to context.
//
// No sanity check is performed on b; it must not contain carriage returns and
// be valid JSON.
func (c Context) RawJSON(key string, b []byte) Context {
c.l.context = appendJSON(enc.AppendKey(c.l.context, key), b)
return c
}
// AnErr adds the field key with serialized err to the logger context.
func (c Context) AnErr(key string, err error) Context {
switch m := ErrorMarshalFunc(err).(type) {
case nil:
return c
case LogObjectMarshaler:
return c.Object(key, m)
case error:
if m == nil || isNilValue(m) {
return c
} else {
return c.Str(key, m.Error())
}
case string:
return c.Str(key, m)
default:
return c.Interface(key, m)
}
}
// Errs adds the field key with errs as an array of serialized errors to the
// logger context.
func (c Context) Errs(key string, errs []error) Context {
arr := Arr()
for _, err := range errs {
switch m := ErrorMarshalFunc(err).(type) {
case LogObjectMarshaler:
arr = arr.Object(m)
case error:
if m == nil || isNilValue(m) {
arr = arr.Interface(nil)
} else {
arr = arr.Str(m.Error())
}
case string:
arr = arr.Str(m)
default:
arr = arr.Interface(m)
}
}
return c.Array(key, arr)
}
// Err adds the field "error" with serialized err to the logger context.
func (c Context) Err(err error) Context {
return c.AnErr(ErrorFieldName, err)
}
// Bool adds the field key with val as a bool to the logger context.
func (c Context) Bool(key string, b bool) Context {
c.l.context = enc.AppendBool(enc.AppendKey(c.l.context, key), b)
return c
}
// Bools adds the field key with val as a []bool to the logger context.
func (c Context) Bools(key string, b []bool) Context {
c.l.context = enc.AppendBools(enc.AppendKey(c.l.context, key), b)
return c
}
// Int adds the field key with i as a int to the logger context.
func (c Context) Int(key string, i int) Context {
c.l.context = enc.AppendInt(enc.AppendKey(c.l.context, key), i)
return c
}
// Ints adds the field key with i as a []int to the logger context.
func (c Context) Ints(key string, i []int) Context {
c.l.context = enc.AppendInts(enc.AppendKey(c.l.context, key), i)
return c
}
// Int8 adds the field key with i as a int8 to the logger context.
func (c Context) Int8(key string, i int8) Context {
c.l.context = enc.AppendInt8(enc.AppendKey(c.l.context, key), i)
return c
}
// Ints8 adds the field key with i as a []int8 to the logger context.
func (c Context) Ints8(key string, i []int8) Context {
c.l.context = enc.AppendInts8(enc.AppendKey(c.l.context, key), i)
return c
}
// Int16 adds the field key with i as a int16 to the logger context.
func (c Context) Int16(key string, i int16) Context {
c.l.context = enc.AppendInt16(enc.AppendKey(c.l.context, key), i)
return c
}
// Ints16 adds the field key with i as a []int16 to the logger context.
func (c Context) Ints16(key string, i []int16) Context {
c.l.context = enc.AppendInts16(enc.AppendKey(c.l.context, key), i)
return c
}
// Int32 adds the field key with i as a int32 to the logger context.
func (c Context) Int32(key string, i int32) Context {
c.l.context = enc.AppendInt32(enc.AppendKey(c.l.context, key), i)
return c
}
// Ints32 adds the field key with i as a []int32 to the logger context.
func (c Context) Ints32(key string, i []int32) Context {
c.l.context = enc.AppendInts32(enc.AppendKey(c.l.context, key), i)
return c
}
// Int64 adds the field key with i as a int64 to the logger context.
func (c Context) Int64(key string, i int64) Context {
c.l.context = enc.AppendInt64(enc.AppendKey(c.l.context, key), i)
return c
}
// Ints64 adds the field key with i as a []int64 to the logger context.
func (c Context) Ints64(key string, i []int64) Context {
c.l.context = enc.AppendInts64(enc.AppendKey(c.l.context, key), i)
return c
}
// Uint adds the field key with i as a uint to the logger context.
func (c Context) Uint(key string, i uint) Context {
c.l.context = enc.AppendUint(enc.AppendKey(c.l.context, key), i)
return c
}
// Uints adds the field key with i as a []uint to the logger context.
func (c Context) Uints(key string, i []uint) Context {
c.l.context = enc.AppendUints(enc.AppendKey(c.l.context, key), i)
return c
}
// Uint8 adds the field key with i as a uint8 to the logger context.
func (c Context) Uint8(key string, i uint8) Context {
c.l.context = enc.AppendUint8(enc.AppendKey(c.l.context, key), i)
return c
}
// Uints8 adds the field key with i as a []uint8 to the logger context.
func (c Context) Uints8(key string, i []uint8) Context {
c.l.context = enc.AppendUints8(enc.AppendKey(c.l.context, key), i)
return c
}
// Uint16 adds the field key with i as a uint16 to the logger context.
func (c Context) Uint16(key string, i uint16) Context {
c.l.context = enc.AppendUint16(enc.AppendKey(c.l.context, key), i)
return c
}
// Uints16 adds the field key with i as a []uint16 to the logger context.
func (c Context) Uints16(key string, i []uint16) Context {
c.l.context = enc.AppendUints16(enc.AppendKey(c.l.context, key), i)
return c
}
// Uint32 adds the field key with i as a uint32 to the logger context.
func (c Context) Uint32(key string, i uint32) Context {
c.l.context = enc.AppendUint32(enc.AppendKey(c.l.context, key), i)
return c
}
// Uints32 adds the field key with i as a []uint32 to the logger context.
func (c Context) Uints32(key string, i []uint32) Context {
c.l.context = enc.AppendUints32(enc.AppendKey(c.l.context, key), i)
return c
}
// Uint64 adds the field key with i as a uint64 to the logger context.
func (c Context) Uint64(key string, i uint64) Context {
c.l.context = enc.AppendUint64(enc.AppendKey(c.l.context, key), i)
return c
}
// Uints64 adds the field key with i as a []uint64 to the logger context.
func (c Context) Uints64(key string, i []uint64) Context {
c.l.context = enc.AppendUints64(enc.AppendKey(c.l.context, key), i)
return c
}
// Float32 adds the field key with f as a float32 to the logger context.
func (c Context) Float32(key string, f float32) Context {
c.l.context = enc.AppendFloat32(enc.AppendKey(c.l.context, key), f)
return c
}
// Floats32 adds the field key with f as a []float32 to the logger context.
func (c Context) Floats32(key string, f []float32) Context {
c.l.context = enc.AppendFloats32(enc.AppendKey(c.l.context, key), f)
return c
}
// Float64 adds the field key with f as a float64 to the logger context.
func (c Context) Float64(key string, f float64) Context {
c.l.context = enc.AppendFloat64(enc.AppendKey(c.l.context, key), f)
return c
}
// Floats64 adds the field key with f as a []float64 to the logger context.
func (c Context) Floats64(key string, f []float64) Context {
c.l.context = enc.AppendFloats64(enc.AppendKey(c.l.context, key), f)
return c
}
type timestampHook struct{}
func (ts timestampHook) Run(e *Event, level Level, msg string) {
e.Timestamp()
}
var th = timestampHook{}
// Timestamp adds the current local time as UNIX timestamp to the logger context with the "time" key.
// To customize the key name, change zerolog.TimestampFieldName.
//
// NOTE: It won't dedupe the "time" key if the *Context has one already.
func (c Context) Timestamp() Context {
c.l = c.l.Hook(th)
return c
}
// Time adds the field key with t formated as string using zerolog.TimeFieldFormat.
func (c Context) Time(key string, t time.Time) Context {
c.l.context = enc.AppendTime(enc.AppendKey(c.l.context, key), t, TimeFieldFormat)
return c
}
// Times adds the field key with t formated as string using zerolog.TimeFieldFormat.
func (c Context) Times(key string, t []time.Time) Context {
c.l.context = enc.AppendTimes(enc.AppendKey(c.l.context, key), t, TimeFieldFormat)
return c
}
// Dur adds the fields key with d divided by unit and stored as a float.
func (c Context) Dur(key string, d time.Duration) Context {
c.l.context = enc.AppendDuration(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldInteger)
return c
}
// Durs adds the fields key with d divided by unit and stored as a float.
func (c Context) Durs(key string, d []time.Duration) Context {
c.l.context = enc.AppendDurations(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldInteger)
return c
}
// Interface adds the field key with obj marshaled using reflection.
func (c Context) Interface(key string, i interface{}) Context {
c.l.context = enc.AppendInterface(enc.AppendKey(c.l.context, key), i)
return c
}
type callerHook struct {
callerSkipFrameCount int
}
func newCallerHook(skipFrameCount int) callerHook {
return callerHook{callerSkipFrameCount: skipFrameCount}
}
func (ch callerHook) Run(e *Event, level Level, msg string) {
switch ch.callerSkipFrameCount {
case useGlobalSkipFrameCount:
// Extra frames to skip (added by hook infra).
e.caller(CallerSkipFrameCount + contextCallerSkipFrameCount)
default:
// Extra frames to skip (added by hook infra).
e.caller(ch.callerSkipFrameCount + contextCallerSkipFrameCount)
}
}
// useGlobalSkipFrameCount acts as a flag to informat callerHook.Run
// to use the global CallerSkipFrameCount.
const useGlobalSkipFrameCount = math.MinInt32
// ch is the default caller hook using the global CallerSkipFrameCount.
var ch = newCallerHook(useGlobalSkipFrameCount)
// Caller adds the file:line of the caller with the zerolog.CallerFieldName key.
func (c Context) Caller() Context {
c.l = c.l.Hook(ch)
return c
}
// CallerWithSkipFrameCount adds the file:line of the caller with the zerolog.CallerFieldName key.
// The specified skipFrameCount int will override the global CallerSkipFrameCount for this context's respective logger.
// If set to -1 the global CallerSkipFrameCount will be used.
func (c Context) CallerWithSkipFrameCount(skipFrameCount int) Context {
c.l = c.l.Hook(newCallerHook(skipFrameCount))
return c
}
type stackTraceHook struct{}
func (sh stackTraceHook) Run(e *Event, level Level, msg string) {
e.Stack()
}
var sh = stackTraceHook{}
// Stack enables stack trace printing for the error passed to Err().
func (c Context) Stack() Context {
c.l = c.l.Hook(sh)
return c
}
// IPAddr adds IPv4 or IPv6 Address to the context
func (c Context) IPAddr(key string, ip net.IP) Context {
c.l.context = enc.AppendIPAddr(enc.AppendKey(c.l.context, key), ip)
return c
}
// IPPrefix adds IPv4 or IPv6 Prefix (address and mask) to the context
func (c Context) IPPrefix(key string, pfx net.IPNet) Context {
c.l.context = enc.AppendIPPrefix(enc.AppendKey(c.l.context, key), pfx)
return c
}
// MACAddr adds MAC address to the context
func (c Context) MACAddr(key string, ha net.HardwareAddr) Context {
c.l.context = enc.AppendMACAddr(enc.AppendKey(c.l.context, key), ha)
return c
}

48
vendor/github.com/rs/zerolog/ctx.go generated vendored Normal file
View File

@ -0,0 +1,48 @@
package zerolog
import (
"context"
)
var disabledLogger *Logger
func init() {
SetGlobalLevel(TraceLevel)
l := Nop()
disabledLogger = &l
}
type ctxKey struct{}
// WithContext returns a copy of ctx with l associated. If an instance of Logger
// is already in the context, the context is not updated.
//
// For instance, to add a field to an existing logger in the context, use this
// notation:
//
// ctx := r.Context()
// l := zerolog.Ctx(ctx)
// l.UpdateContext(func(c Context) Context {
// return c.Str("bar", "baz")
// })
func (l *Logger) WithContext(ctx context.Context) context.Context {
if lp, ok := ctx.Value(ctxKey{}).(*Logger); ok {
if lp == l {
// Do not store same logger.
return ctx
}
} else if l.level == Disabled {
// Do not store disabled logger.
return ctx
}
return context.WithValue(ctx, ctxKey{}, l)
}
// Ctx returns the Logger associated with the ctx. If no logger
// is associated, a disabled logger is returned.
func Ctx(ctx context.Context) *Logger {
if l, ok := ctx.Value(ctxKey{}).(*Logger); ok {
return l
}
return disabledLogger
}

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