mirror of https://github.com/stashapp/stash.git
Selective export (#770)
This commit is contained in:
parent
03f5e1a442
commit
03d4826c85
5
Makefile
5
Makefile
|
@ -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
12
go.mod
|
@ -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
160
go.sum
|
@ -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=
|
||||
|
|
|
@ -6,6 +6,10 @@ mutation MetadataExport {
|
|||
metadataExport
|
||||
}
|
||||
|
||||
mutation ExportObjects($input: ExportObjectsInput!) {
|
||||
exportObjects(input: $input)
|
||||
}
|
||||
|
||||
mutation MetadataScan($input: ScanMetadataInput!) {
|
||||
metadataScan(input: $input)
|
||||
}
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -12,4 +12,5 @@ const (
|
|||
movieKey key = 4
|
||||
ContextUser key = 5
|
||||
tagKey key = 6
|
||||
downloadKey key = 7
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
1
tools.go
1
tools.go
|
@ -4,4 +4,5 @@ package main
|
|||
|
||||
import (
|
||||
_ "github.com/99designs/gqlgen"
|
||||
_ "github.com/vektra/mockery/v2"
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
) => {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
const downloadFile = (url: string) => {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.click();
|
||||
};
|
||||
|
||||
export default downloadFile;
|
|
@ -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";
|
||||
|
|
|
@ -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 ./...
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/gorilla/websocket
|
||||
|
||||
go 1.12
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -7,4 +7,6 @@ go:
|
|||
- 1.8.x
|
||||
- 1.9.x
|
||||
- "1.10.x"
|
||||
- "1.11.x"
|
||||
- "1.12.x"
|
||||
- tip
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
module github.com/magiconair/properties
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
module github.com/mitchellh/go-homedir
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/coverage.txt
|
|
@ -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)
|
|
@ -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
|
|
@ -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))
|
||||
}
|
|
@ -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{}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 ./...
|
|
@ -0,0 +1 @@
|
|||
zerolog.io
|
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
remote_theme: rs/gh-readme
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue