diff --git a/cmd/app/main.go b/cmd/app/main.go index 933ce7c..dcdfb8d 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -4,23 +4,19 @@ import ( "context" "fmt" "log" - "net" "net/http" "os" "os/signal" - "strings" "syscall" - "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" - "github.com/go-pg/pg" "github.com/mxpv/podsync/pkg/api" "github.com/mxpv/podsync/pkg/builders" "github.com/mxpv/podsync/pkg/config" "github.com/mxpv/podsync/pkg/feeds" "github.com/mxpv/podsync/pkg/handler" "github.com/mxpv/podsync/pkg/stats" + "github.com/mxpv/podsync/pkg/storage" "github.com/mxpv/podsync/pkg/support" - "github.com/pkg/errors" ) func main() { @@ -30,14 +26,14 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - // Create core sevices + // Create core services cfg, err := config.ReadConfiguration() if err != nil { panic(err) } - database, err := createPg(cfg.PostgresConnectionURL) + database, err := storage.NewPG(cfg.PostgresConnectionURL, true) if err != nil { panic(err) } @@ -62,7 +58,7 @@ func main() { } feed, err := feeds.NewFeedService( - feeds.WithPostgres(database), + feeds.WithStorage(database), feeds.WithStats(statistics), feeds.WithBuilder(api.ProviderYoutube, youtube), feeds.WithBuilder(api.ProviderVimeo, vimeo), @@ -88,35 +84,9 @@ func main() { log.Printf("shutting down server") - srv.Shutdown(ctx) - database.Close() - statistics.Close() + _ = srv.Shutdown(ctx) + _ = database.Close() + _ = statistics.Close() log.Printf("server gracefully stopped") } - -func createPg(connectionURL string) (*pg.DB, error) { - opts, err := pg.ParseURL(connectionURL) - if err != nil { - return nil, err - } - - // If host format is "projection:region:host", than use Google SQL Proxy - // See https://github.com/go-pg/pg/issues/576 - if strings.Count(opts.Addr, ":") == 2 { - log.Print("using GCP SQL proxy") - opts.Dialer = func(network, addr string) (net.Conn, error) { - return proxy.Dial(addr) - } - } - - db := pg.Connect(opts) - - // Check database connectivity - if _, err := db.ExecOne("SELECT 1"); err != nil { - db.Close() - return nil, errors.Wrap(err, "failed to check database connectivity") - } - - return db, nil -} diff --git a/go.mod b/go.mod index 5c14b68..154da83 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,13 @@ module github.com/mxpv/podsync require ( cloud.google.com/go v0.25.0 // indirect github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec + github.com/BurntSushi/toml v0.3.1 // indirect github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170929212804-61590edac4c7 github.com/boj/redistore v0.0.0-20160128113310-fc113767cd6b // indirect github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 // indirect github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20180621172731-4e5d6d543851 // indirect github.com/davecgh/go-spew v1.1.0 // indirect - github.com/fsnotify/fsnotify v1.4.7 // indirect + github.com/eduncan911/podcast v1.3.0 // indirect github.com/garyburd/redigo v1.6.0 // indirect github.com/gin-contrib/sessions v0.0.0-20170731012558-a71ea9167c61 github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect @@ -16,7 +17,6 @@ require ( github.com/go-pg/pg v6.14.2+incompatible github.com/go-redis/redis v6.12.0+incompatible github.com/golang/mock v1.1.1 - github.com/golang/protobuf v1.1.0 // indirect github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect github.com/gorilla/sessions v1.1.1 // indirect github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce // indirect @@ -24,9 +24,12 @@ require ( github.com/kidstuff/mongostore v0.0.0-20180412085134-db2a8b4fac1f // indirect github.com/magiconair/properties v1.8.0 // indirect github.com/mattn/go-isatty v0.0.3 // indirect + github.com/memcachier/mc v2.0.1+incompatible // indirect github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699 // indirect github.com/mxpv/patreon-go v0.0.0-20171031001022-1d2f253ac700 github.com/mxpv/podcast v0.0.0-20170823220358-fe328ad87d18 + github.com/onsi/ginkgo v1.7.0 // indirect + github.com/onsi/gomega v1.4.3 // indirect github.com/pelletier/go-toml v1.2.0 // indirect github.com/pkg/errors v0.8.0 github.com/pmezard/go-difflib v1.0.0 // indirect @@ -36,16 +39,17 @@ require ( github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec // indirect github.com/spf13/pflag v1.0.1 // indirect github.com/spf13/viper v1.0.2 + github.com/stretchr/objx v0.1.1 // indirect github.com/stretchr/testify v1.2.2 + github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf // indirect github.com/ugorji/go v1.1.1 // indirect github.com/ventu-io/go-shortid v0.0.0-20171029131806-771a37caa5cf - golang.org/x/net v0.0.0-20180719001425-81d44fd177a9 + golang.org/x/net v0.0.0-20180906233101-161cd47e91fd golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd - golang.org/x/sys v0.0.0-20180715085529-ac767d655b30 // indirect - golang.org/x/text v0.3.0 // indirect + golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect google.golang.org/api v0.0.0-20180718221112-efcb5f25ac56 google.golang.org/appengine v1.1.0 // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 // indirect gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect - gopkg.in/yaml.v2 v2.2.1 // indirect ) diff --git a/go.sum b/go.sum index 7336eae..12cb380 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go v0.25.0 h1:6vD6xZTc8Jo6To8gHxFDRVsMvWFDgY3rugNszcDalN8= cloud.google.com/go v0.25.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec h1:1VPruZMM1WQC7POhjxbZOWK564cuFz1hlpwYW6ocM4E= github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec/go.mod h1:+hWo/MWgY8VtjZvdrYM2nPRMaK40zX2iPsH/qD0+Xs0= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170929212804-61590edac4c7 h1:Clo7QBZv+fHzjCgVp4ELlbIsY5rScCmj+4VCfoMfqtQ= github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170929212804-61590edac4c7/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo= github.com/boj/redistore v0.0.0-20160128113310-fc113767cd6b h1:PfxLkkgJYE095CKZji++BNwZjxWfoAF21WFPzkzOZEs= @@ -12,6 +14,8 @@ github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20180621172731-4e5d6d github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20180621172731-4e5d6d543851/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eduncan911/podcast v1.3.0 h1:lVCar1J39mMNWR2SbGzPjeUbCKEkQ6/pt/7beQqK6fk= +github.com/eduncan911/podcast v1.3.0/go.mod h1:C7Q04QZtv7LW/1X67mc1zwsktpZ68kbxsUS3CYWniJg= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc= @@ -28,8 +32,8 @@ github.com/go-redis/redis v6.12.0+incompatible h1:s+64XI+z/RXqGHz2fQSgRJOEwqqSXe github.com/go-redis/redis v6.12.0+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= @@ -40,6 +44,8 @@ github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0Pr github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k= github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/kidstuff/mongostore v0.0.0-20180412085134-db2a8b4fac1f h1:84d0qxD9AiuBNpeK5TkYwTKKNezsYxIVn8nWh0pq51E= @@ -48,12 +54,19 @@ github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDe github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/memcachier/mc v2.0.1+incompatible h1:s8EDz0xrJLP8goitwZOoq1vA/sm0fPS4X3KAF0nyhWQ= +github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699 h1:KXZJFdun9knAVAR8tg/aHJEr5DgtcbqyvzacK+CDCaI= github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mxpv/patreon-go v0.0.0-20171031001022-1d2f253ac700 h1:39PjdU78pNilVLU9tjWVDGt/rziIKKkKHuzWbH1kBbw= github.com/mxpv/patreon-go v0.0.0-20171031001022-1d2f253ac700/go.mod h1:ksYjm2GAbGlgIP7jO9Q5/AdyE4MwwEbgQ+lFMx3hyiM= github.com/mxpv/podcast v0.0.0-20170823220358-fe328ad87d18 h1:YYsu49Y42JA+CSs9+z2MGBdGxb5jklpagLp5QPJ6BwQ= github.com/mxpv/podcast v0.0.0-20170823220358-fe328ad87d18/go.mod h1:bKrqwMF8O3PciTG92w0992h/d7Aj7CuIF5uTNEl3pNY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 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/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= @@ -72,28 +85,42 @@ github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso= github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= +github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w= github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= github.com/ventu-io/go-shortid v0.0.0-20171029131806-771a37caa5cf h1:cgAKVljim9RJRcJNGjnBUajXj1FupBSdWwW4JaQG7vk= github.com/ventu-io/go-shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:6rZqAOk/eYX5FJyjQJ6Z3RBSN389IXX2ijwW4FcggaM= -golang.org/x/net v0.0.0-20180719001425-81d44fd177a9 h1:BN4Q1JAtkPK2SJ//1vLij2bjj2ZPrBi6w1VEpdevHDA= -golang.org/x/net v0.0.0-20180719001425-81d44fd177a9/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd h1:QQhib242ErYDSMitlBm8V7wYCm/1a25hV8qMadIKLPA= golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sys v0.0.0-20180715085529-ac767d655b30 h1:4bYUqrXBoiI7UFQeibUwFhvcHfaEeL75O3lOcZa964o= -golang.org/x/sys v0.0.0-20180715085529-ac767d655b30/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= google.golang.org/api v0.0.0-20180718221112-efcb5f25ac56 h1:iDRbkenn0VZEo05mHiCtN6/EfbZj7x1Rg+tPjB5HiQc= google.golang.org/api v0.0.0-20180718221112-efcb5f25ac56/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/feeds/feeds.go b/pkg/feeds/feeds.go index 66bbb09..2523af4 100644 --- a/pkg/feeds/feeds.go +++ b/pkg/feeds/feeds.go @@ -5,12 +5,11 @@ import ( "log" "time" - "github.com/go-pg/pg" itunes "github.com/mxpv/podcast" + "github.com/pkg/errors" + "github.com/mxpv/podsync/pkg/api" "github.com/mxpv/podsync/pkg/model" - "github.com/pkg/errors" - "github.com/ventu-io/go-shortid" ) const ( @@ -27,11 +26,18 @@ type builder interface { Build(feed *model.Feed) (podcast *itunes.Podcast, err error) } +type storage interface { + SaveFeed(feed *model.Feed) error + GetFeed(hashID string) (*model.Feed, error) + GetMetadata(hashID string) (*model.Feed, error) + Downgrade(userID string, featureLevel int) error +} + type Service struct { - sid *shortid.Shortid - stats stats - db *pg.DB - builders map[api.Provider]builder + generator IDGen + stats stats + db storage + builders map[api.Provider]builder } func (s Service) makeFeed(req *api.CreateFeedRequest, identity *api.Identity) (*model.Feed, error) { @@ -65,7 +71,7 @@ func (s Service) makeFeed(req *api.CreateFeedRequest, identity *api.Identity) (* } // Generate short id - hashId, err := s.sid.Generate() + hashId, err := s.generator.Generate() if err != nil { return nil, errors.Wrap(err, "failed to generate id for feed") } @@ -87,34 +93,15 @@ func (s Service) CreateFeed(req *api.CreateFeedRequest, identity *api.Identity) return "", fmt.Errorf("failed to get builder for URL: %s", req.URL) } - // Save to database - _, err = s.db.Model(feed).Insert() - if err != nil { - return "", errors.Wrap(err, "failed to save feed to database") + if err := s.db.SaveFeed(feed); err != nil { + return "", err } return feed.HashID, nil } func (s Service) QueryFeed(hashID string) (*model.Feed, error) { - lastAccess := time.Now().UTC() - - feed := &model.Feed{} - res, err := s.db.Model(feed). - Set("last_access = ?", lastAccess). - Where("hash_id = ?", hashID). - Returning("*"). - Update() - - if err != nil { - return nil, errors.Wrapf(err, "failed to query feed: %s", hashID) - } - - if res.RowsAffected() != 1 { - return nil, api.ErrNotFound - } - - return feed, nil + return s.db.GetFeed(hashID) } func (s Service) BuildFeed(hashID string) (*itunes.Podcast, error) { @@ -146,13 +133,7 @@ func (s Service) BuildFeed(hashID string) (*itunes.Podcast, error) { } func (s Service) GetMetadata(hashID string) (*api.Metadata, error) { - feed := &model.Feed{} - err := s.db. - Model(feed). - Where("hash_id = ?", hashID). - Column("provider", "format", "quality", "user_id"). - Select() - + feed, err := s.db.GetMetadata(hashID) if err != nil { return nil, err } @@ -173,64 +154,19 @@ func (s Service) GetMetadata(hashID string) (*api.Metadata, error) { func (s Service) Downgrade(patronID string, featureLevel int) error { log.Printf("Downgrading patron '%s' to feature level %d", patronID, featureLevel) - if featureLevel > api.ExtendedFeatures { - return nil + if err := s.db.Downgrade(patronID, featureLevel); err != nil { + log.Printf("! downgrade failed") + return err } - if featureLevel == api.ExtendedFeatures { - const maxPages = 150 - res, err := s.db. - Model(&model.Feed{}). - Set("page_size = ?", maxPages). - Where("user_id = ? AND page_size > ?", patronID, maxPages). - Update() - - if err != nil { - log.Printf("! failed to reduce page sizes for patron '%s': %v", patronID, err) - return err - } - - res, err = s.db. - Model(&model.Feed{}). - Set("feature_level = ?", api.ExtendedFeatures). - Where("user_id = ?", patronID, maxPages). - Update() - - if err != nil { - log.Printf("! failed to downgrade patron '%s' to feature level %d: %v", patronID, featureLevel, err) - return err - } - - log.Printf("Updated %d feed(s) of user '%s' to feature level %d", res.RowsAffected(), patronID, featureLevel) - return nil - } - - if featureLevel == api.DefaultFeatures { - res, err := s.db. - Model(&model.Feed{}). - Set("page_size = ?", 50). - Set("feature_level = ?", api.DefaultFeatures). - Set("format = ?", api.FormatVideo). - Set("quality = ?", api.QualityHigh). - Where("user_id = ?", patronID). - Update() - - if err != nil { - log.Printf("! failed to downgrade patron '%s' to feature level %d: %v", patronID, featureLevel, err) - return err - } - - log.Printf("Updated %d feed(s) of user '%s' to feature level %d", res.RowsAffected(), patronID, featureLevel) - return nil - } - - return errors.New("unsupported downgrade type") + log.Printf("updated user '%s' to feature level %d", patronID, featureLevel) + return nil } type feedOption func(*Service) //noinspection GoExportedFuncWithUnexportedType -func WithPostgres(db *pg.DB) feedOption { +func WithStorage(db storage) feedOption { return func(service *Service) { service.db = db } @@ -251,14 +187,14 @@ func WithStats(m stats) feedOption { } func NewFeedService(opts ...feedOption) (*Service, error) { - sid, err := shortid.New(1, shortid.DefaultABC, uint64(time.Now().UnixNano())) + idGen, err := NewIDGen() if err != nil { return nil, err } svc := &Service{ - sid: sid, - builders: make(map[api.Provider]builder), + generator: idGen, + builders: make(map[api.Provider]builder), } for _, fn := range opts { diff --git a/pkg/feeds/feeds_mock_test.go b/pkg/feeds/feeds_mock_test.go index 2b76a61..21ac12a 100644 --- a/pkg/feeds/feeds_mock_test.go +++ b/pkg/feeds/feeds_mock_test.go @@ -1,6 +1,7 @@ // Code generated by MockGen. DO NOT EDIT. // Source: feeds.go +// Package feeds is a generated GoMock package. package feeds import ( @@ -29,34 +30,34 @@ func NewMockstats(ctrl *gomock.Controller) *Mockstats { } // EXPECT returns an object that allows the caller to indicate expected use -func (_m *Mockstats) EXPECT() *MockstatsMockRecorder { - return _m.recorder +func (m *Mockstats) EXPECT() *MockstatsMockRecorder { + return m.recorder } // Inc mocks base method -func (_m *Mockstats) Inc(metric string, hashID string) (int64, error) { - ret := _m.ctrl.Call(_m, "Inc", metric, hashID) +func (m *Mockstats) Inc(metric, hashID string) (int64, error) { + ret := m.ctrl.Call(m, "Inc", metric, hashID) ret0, _ := ret[0].(int64) ret1, _ := ret[1].(error) return ret0, ret1 } // Inc indicates an expected call of Inc -func (_mr *MockstatsMockRecorder) Inc(arg0, arg1 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Inc", reflect.TypeOf((*Mockstats)(nil).Inc), arg0, arg1) +func (mr *MockstatsMockRecorder) Inc(metric, hashID interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Inc", reflect.TypeOf((*Mockstats)(nil).Inc), metric, hashID) } // Get mocks base method -func (_m *Mockstats) Get(metric string, hashID string) (int64, error) { - ret := _m.ctrl.Call(_m, "Get", metric, hashID) +func (m *Mockstats) Get(metric, hashID string) (int64, error) { + ret := m.ctrl.Call(m, "Get", metric, hashID) ret0, _ := ret[0].(int64) ret1, _ := ret[1].(error) return ret0, ret1 } // Get indicates an expected call of Get -func (_mr *MockstatsMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Get", reflect.TypeOf((*Mockstats)(nil).Get), arg0, arg1) +func (mr *MockstatsMockRecorder) Get(metric, hashID interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*Mockstats)(nil).Get), metric, hashID) } // Mockbuilder is a mock of builder interface @@ -78,19 +79,92 @@ func NewMockbuilder(ctrl *gomock.Controller) *Mockbuilder { } // EXPECT returns an object that allows the caller to indicate expected use -func (_m *Mockbuilder) EXPECT() *MockbuilderMockRecorder { - return _m.recorder +func (m *Mockbuilder) EXPECT() *MockbuilderMockRecorder { + return m.recorder } // Build mocks base method -func (_m *Mockbuilder) Build(feed *model.Feed) (*podcast.Podcast, error) { - ret := _m.ctrl.Call(_m, "Build", feed) +func (m *Mockbuilder) Build(feed *model.Feed) (*podcast.Podcast, error) { + ret := m.ctrl.Call(m, "Build", feed) ret0, _ := ret[0].(*podcast.Podcast) ret1, _ := ret[1].(error) return ret0, ret1 } // Build indicates an expected call of Build -func (_mr *MockbuilderMockRecorder) Build(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Build", reflect.TypeOf((*Mockbuilder)(nil).Build), arg0) +func (mr *MockbuilderMockRecorder) Build(feed interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*Mockbuilder)(nil).Build), feed) +} + +// Mockstorage is a mock of storage interface +type Mockstorage struct { + ctrl *gomock.Controller + recorder *MockstorageMockRecorder +} + +// MockstorageMockRecorder is the mock recorder for Mockstorage +type MockstorageMockRecorder struct { + mock *Mockstorage +} + +// NewMockstorage creates a new mock instance +func NewMockstorage(ctrl *gomock.Controller) *Mockstorage { + mock := &Mockstorage{ctrl: ctrl} + mock.recorder = &MockstorageMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *Mockstorage) EXPECT() *MockstorageMockRecorder { + return m.recorder +} + +// SaveFeed mocks base method +func (m *Mockstorage) SaveFeed(feed *model.Feed) error { + ret := m.ctrl.Call(m, "SaveFeed", feed) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveFeed indicates an expected call of SaveFeed +func (mr *MockstorageMockRecorder) SaveFeed(feed interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveFeed", reflect.TypeOf((*Mockstorage)(nil).SaveFeed), feed) +} + +// GetFeed mocks base method +func (m *Mockstorage) GetFeed(hashID string) (*model.Feed, error) { + ret := m.ctrl.Call(m, "GetFeed", hashID) + ret0, _ := ret[0].(*model.Feed) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFeed indicates an expected call of GetFeed +func (mr *MockstorageMockRecorder) GetFeed(hashID interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeed", reflect.TypeOf((*Mockstorage)(nil).GetFeed), hashID) +} + +// GetMetadata mocks base method +func (m *Mockstorage) GetMetadata(hashID string) (*model.Feed, error) { + ret := m.ctrl.Call(m, "GetMetadata", hashID) + ret0, _ := ret[0].(*model.Feed) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMetadata indicates an expected call of GetMetadata +func (mr *MockstorageMockRecorder) GetMetadata(hashID interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMetadata", reflect.TypeOf((*Mockstorage)(nil).GetMetadata), hashID) +} + +// Downgrade mocks base method +func (m *Mockstorage) Downgrade(userID string, featureLevel int) error { + ret := m.ctrl.Call(m, "Downgrade", userID, featureLevel) + ret0, _ := ret[0].(error) + return ret0 +} + +// Downgrade indicates an expected call of Downgrade +func (mr *MockstorageMockRecorder) Downgrade(userID, featureLevel interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Downgrade", reflect.TypeOf((*Mockstorage)(nil).Downgrade), userID, featureLevel) } diff --git a/pkg/feeds/feeds_test.go b/pkg/feeds/feeds_test.go index e36da5f..0edb5d1 100644 --- a/pkg/feeds/feeds_test.go +++ b/pkg/feeds/feeds_test.go @@ -5,12 +5,12 @@ package feeds import ( "testing" - "github.com/go-pg/pg" "github.com/golang/mock/gomock" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "github.com/mxpv/podsync/pkg/api" "github.com/mxpv/podsync/pkg/model" - "github.com/stretchr/testify/require" - "github.com/ventu-io/go-shortid" ) var feed = &model.Feed{ @@ -27,10 +27,15 @@ func TestService_CreateFeed(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + db := NewMockstorage(ctrl) + db.EXPECT().SaveFeed(gomock.Any()).Times(1).Return(nil) + + gen, _ := NewIDGen() + s := Service{ - sid: shortid.GetDefault(), - db: createDatabase(t), - builders: map[api.Provider]builder{api.ProviderYoutube: nil}, + generator: gen, + db: db, + builders: map[api.Provider]builder{api.ProviderYoutube: nil}, } req := &api.CreateFeedRequest{ @@ -53,8 +58,10 @@ func TestService_makeFeed(t *testing.T) { Format: api.FormatAudio, } + gen, _ := NewIDGen() + s := Service{ - sid: shortid.GetDefault(), + generator: gen, } feed, err := s.makeFeed(req, &api.Identity{}) @@ -76,6 +83,18 @@ func TestService_makeFeed(t *testing.T) { require.Equal(t, api.FormatAudio, feed.Format) } +func TestService_QueryFeed(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + db := NewMockstorage(ctrl) + db.EXPECT().GetFeed("123").Times(1).Return(nil, nil) + + s := Service{db: db} + _, err := s.QueryFeed("123") + require.NoError(t, err) +} + func TestService_GetFeed(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -83,7 +102,10 @@ func TestService_GetFeed(t *testing.T) { stats := NewMockstats(ctrl) stats.EXPECT().Inc(MetricQueries, feed.HashID).Return(int64(10), nil) - s := Service{db: createDatabase(t), stats: stats} + stor := NewMockstorage(ctrl) + stor.EXPECT().GetFeed(feed.HashID).Times(1).Return(feed, nil) + + s := Service{db: stor, stats: stats} _, err := s.BuildFeed(feed.HashID) require.NoError(t, err) @@ -106,126 +128,41 @@ func TestService_BuildFeedQuotaCheck(t *testing.T) { stats := NewMockstats(ctrl) stats.EXPECT().Inc(MetricQueries, f.HashID).Return(int64(api.ExtendedPaginationQueryLimit)+1, nil) - s := Service{db: createDatabase(t), stats: stats} + stor := NewMockstorage(ctrl) + stor.EXPECT().GetFeed(f.HashID).Times(1).Return(f, nil) - err := s.db.Insert(f) - require.NoError(t, err) + s := Service{db: stor, stats: stats} - _, err = s.BuildFeed(f.HashID) + _, err := s.BuildFeed(f.HashID) require.Equal(t, api.ErrQuotaExceeded, err) } func TestService_WrongID(t *testing.T) { - s := Service{db: createDatabase(t)} + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + stor := NewMockstorage(ctrl) + stor.EXPECT().GetFeed(gomock.Any()).Times(1).Return(nil, errors.New("not found")) + + s := Service{db: stor} _, err := s.BuildFeed("invalid_feed_id") require.Error(t, err) } -func TestService_UpdateLastAccess(t *testing.T) { - s := Service{db: createDatabase(t)} - - feed1, err := s.QueryFeed(feed.HashID) - require.NoError(t, err) - - feed2, err := s.QueryFeed(feed.HashID) - require.NoError(t, err) - - require.True(t, feed2.LastAccess.After(feed1.LastAccess)) -} - func TestService_GetMetadata(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + stor := NewMockstorage(ctrl) + stor.EXPECT().GetMetadata(feed.HashID).Times(1).Return(feed, nil) + stats := NewMockstats(ctrl) stats.EXPECT().Inc(MetricDownloads, feed.HashID).Return(int64(10), nil) - s := Service{ - db: createDatabase(t), - stats: stats, - } + s := Service{db: stor, stats: stats} m, err := s.GetMetadata(feed.HashID) require.NoError(t, err) require.Equal(t, int64(10), m.Downloads) } - -func TestService_DowngradeToAnonymous(t *testing.T) { - s := Service{db: createDatabase(t)} - - feed := &model.Feed{ - HashID: "123456", - UserID: "123456", - ItemID: "123456", - Provider: api.ProviderVimeo, - LinkType: api.LinkTypeGroup, - PageSize: 150, - Quality: api.QualityLow, - Format: api.FormatAudio, - FeatureLevel: api.ExtendedFeatures, - } - - err := s.db.Insert(feed) - require.NoError(t, err) - - err = s.Downgrade(feed.UserID, api.DefaultFeatures) - require.NoError(t, err) - - downgraded := &model.Feed{FeedID: feed.FeedID} - err = s.db.Select(downgraded) - require.NoError(t, err) - - require.Equal(t, 50, downgraded.PageSize) - require.Equal(t, api.QualityHigh, downgraded.Quality) - require.Equal(t, api.FormatVideo, downgraded.Format) - require.Equal(t, api.DefaultFeatures, downgraded.FeatureLevel) -} - -func TestService_DowngradeToExtendedFeatures(t *testing.T) { - s := Service{db: createDatabase(t)} - - feed := &model.Feed{ - HashID: "123456", - UserID: "123456", - ItemID: "123456", - Provider: api.ProviderVimeo, - LinkType: api.LinkTypeGroup, - PageSize: 500, - Quality: api.QualityLow, - Format: api.FormatAudio, - FeatureLevel: api.ExtendedFeatures, - } - - err := s.db.Insert(feed) - require.NoError(t, err) - - err = s.Downgrade(feed.UserID, api.ExtendedFeatures) - require.NoError(t, err) - - downgraded := &model.Feed{FeedID: feed.FeedID} - err = s.db.Select(downgraded) - require.NoError(t, err) - - require.Equal(t, 150, downgraded.PageSize) - require.Equal(t, feed.Quality, downgraded.Quality) - require.Equal(t, feed.Format, downgraded.Format) - require.Equal(t, api.ExtendedFeatures, downgraded.FeatureLevel) -} - -func createDatabase(t *testing.T) *pg.DB { - opts, err := pg.ParseURL("postgres://postgres:@localhost/podsync?sslmode=disable") - if err != nil { - require.NoError(t, err) - } - - db := pg.Connect(opts) - - _, err = db.Model(&model.Feed{}).Where("1=1").Delete() - require.NoError(t, err) - - err = db.Insert(feed) - require.NoError(t, err) - - return db -} diff --git a/pkg/feeds/id_gen.go b/pkg/feeds/id_gen.go new file mode 100644 index 0000000..1083368 --- /dev/null +++ b/pkg/feeds/id_gen.go @@ -0,0 +1,24 @@ +package feeds + +import ( + "time" + + "github.com/ventu-io/go-shortid" +) + +type IDGen struct { + sid *shortid.Shortid +} + +func NewIDGen() (IDGen, error) { + sid, err := shortid.New(1, shortid.DefaultABC, uint64(time.Now().UnixNano())) + if err != nil { + return IDGen{}, err + } + + return IDGen{sid}, nil +} + +func (id IDGen) Generate() (string, error) { + return id.sid.Generate() +} \ No newline at end of file diff --git a/pkg/model/models.go b/pkg/model/model.go similarity index 99% rename from pkg/model/models.go rename to pkg/model/model.go index 48bc0e5..3252f32 100644 --- a/pkg/model/models.go +++ b/pkg/model/model.go @@ -1,8 +1,9 @@ package model import ( - "github.com/mxpv/podsync/pkg/api" "time" + + "github.com/mxpv/podsync/pkg/api" ) type Pledge struct { diff --git a/pkg/storage/pg.go b/pkg/storage/pg.go new file mode 100644 index 0000000..306886c --- /dev/null +++ b/pkg/storage/pg.go @@ -0,0 +1,184 @@ +package storage + +import ( + "log" + "net" + "strings" + "time" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" + "github.com/go-pg/pg" + "github.com/pkg/errors" + + "github.com/mxpv/podsync/pkg/api" + "github.com/mxpv/podsync/pkg/model" +) + +type Postgres struct { + db *pg.DB +} + +func NewPG(connectionURL string, ping bool) (Postgres, error) { + opts, err := pg.ParseURL(connectionURL) + if err != nil { + return Postgres{}, err + } + + // If host format is "projection:region:host", than use Google SQL Proxy + // See https://github.com/go-pg/pg/issues/576 + if strings.Count(opts.Addr, ":") == 2 { + log.Print("using GCP SQL proxy") + opts.Dialer = func(network, addr string) (net.Conn, error) { + return proxy.Dial(addr) + } + } + + db := pg.Connect(opts) + + // Check database connectivity + if ping { + if _, err := db.ExecOne("SELECT 1"); err != nil { + _ = db.Close() + return Postgres{}, errors.Wrap(err, "failed to check database connectivity") + } + } + + return Postgres{db: db}, nil +} + +func (p Postgres) SaveFeed(feed *model.Feed) error { + _, err := p.db.Model(feed).Insert() + if err != nil { + return errors.Wrap(err, "failed to save feed to database") + } + return err +} + +func (p Postgres) GetFeed(hashID string) (*model.Feed, error) { + lastAccess := time.Now().UTC() + + feed := &model.Feed{} + res, err := p.db.Model(feed). + Set("last_access = ?", lastAccess). + Where("hash_id = ?", hashID). + Returning("*"). + Update() + + if err != nil { + return nil, errors.Wrapf(err, "failed to query feed: %s", hashID) + } + + if res.RowsAffected() != 1 { + return nil, api.ErrNotFound + } + + return feed, nil +} + +func (p Postgres) GetMetadata(hashID string) (*model.Feed, error) { + feed := &model.Feed{} + err := p.db. + Model(feed). + Where("hash_id = ?", hashID). + Column("provider", "format", "quality", "user_id"). + Select() + + if err != nil { + return nil, err + } + + return feed, nil +} + +func (p Postgres) Downgrade(patronID string, featureLevel int) error { + if featureLevel > api.ExtendedFeatures { + return nil + } + + if featureLevel == api.ExtendedFeatures { + const maxPages = 150 + _, err := p.db. + Model(&model.Feed{}). + Set("page_size = ?", maxPages). + Where("user_id = ? AND page_size > ?", patronID, maxPages). + Update() + + if err != nil { + return errors.Wrapf(err, "failed to reduce page sizes for patron '%s'", patronID) + } + + _, err = p.db. + Model(&model.Feed{}). + Set("feature_level = ?", api.ExtendedFeatures). + Where("user_id = ?", patronID, maxPages). + Update() + + if err != nil { + return errors.Wrapf(err, "failed to downgrade patron '%s' to feature level %d", patronID, featureLevel) + } + + return nil + } + + if featureLevel == api.DefaultFeatures { + _, err := p.db. + Model(&model.Feed{}). + Set("page_size = ?", 50). + Set("feature_level = ?", api.DefaultFeatures). + Set("format = ?", api.FormatVideo). + Set("quality = ?", api.QualityHigh). + Where("user_id = ?", patronID). + Update() + + if err != nil { + return errors.Wrapf(err, "failed to downgrade patron '%s' to feature level %d", patronID, featureLevel) + } + + return nil + } + + return errors.New("unsupported downgrade type") +} + +func (p Postgres) AddPledge(pledge *model.Pledge) error { + return p.db.Insert(pledge) +} + +func (p Postgres) UpdatePledge(patronID string, pledge *model.Pledge) error { + updateColumns := []string{ + "declined_since", + "amount_cents", + "total_historical_amount_cents", + "outstanding_payment_amount_cents", + "is_paused", + } + + res, err := p.db.Model(pledge).Column(updateColumns...).Where("patron_id = ?", patronID).Update() + if err != nil { + return errors.Wrapf(err, "failed to update pledge %d for user %s: %v", pledge.PledgeID, patronID, err) + } + + if res.RowsAffected() != 1 { + return errors.Wrapf(err, "unexpected number of updated rows: %d for user %s", res.RowsAffected(), patronID) + } + + return nil +} + +func (p Postgres) DeletePledge(pledge *model.Pledge) error { + err := p.db.Delete(pledge) + if err == pg.ErrNoRows { + return nil + } + + return err +} + +func (p Postgres) GetPledge(patronID string) (*model.Pledge, error) { + pledge := &model.Pledge{} + return pledge, p.db.Model(pledge).Where("patron_id = ?", patronID).Limit(1).Select() +} + +func (p Postgres) Close() error { + return p.db.Close() +} \ No newline at end of file diff --git a/pkg/model/pg.sql b/pkg/storage/pg_sql.go similarity index 94% rename from pkg/model/pg.sql rename to pkg/storage/pg_sql.go index 162900b..b666ec2 100644 --- a/pkg/model/pg.sql +++ b/pkg/storage/pg_sql.go @@ -1,3 +1,7 @@ +package storage + +//noinspection SpellCheckingInspection +const pgsql = ` BEGIN; -- Pledges @@ -55,4 +59,6 @@ CREATE TABLE IF NOT EXISTS feeds ( CREATE INDEX IF NOT EXISTS feeds_hash_id_idx ON feeds(hash_id); CREATE INDEX IF NOT EXISTS feeds_user_id_idx ON feeds(user_id); -COMMIT; \ No newline at end of file +COMMIT; +END; +` \ No newline at end of file diff --git a/pkg/storage/pg_test.go b/pkg/storage/pg_test.go new file mode 100644 index 0000000..62e2798 --- /dev/null +++ b/pkg/storage/pg_test.go @@ -0,0 +1,226 @@ +package storage + +import ( + "testing" + "time" + + "github.com/go-pg/pg" + "github.com/stretchr/testify/require" + + "github.com/mxpv/podsync/pkg/api" + "github.com/mxpv/podsync/pkg/model" +) + +var ( + testPledge = &model.Pledge{PledgeID: 12345, AmountCents: 400, PatronID: 1, CreatedAt: time.Now()} + testFeed = &model.Feed{FeedID: 1, HashID: "3", UserID: "3", ItemID: "4", LinkType: api.LinkTypeChannel, Provider: api.ProviderVimeo, Format: api.FormatAudio ,Quality: api.QualityLow} +) + +func TestPostgres_SaveFeed(t *testing.T) { + stor := createPG(t) + defer func() { _ = stor.Close() }() + + err := stor.SaveFeed(testFeed) + require.NoError(t, err) + + find := &model.Feed{FeedID: 1} + err = stor.db.Model(find).Select() + require.NoError(t, err) + + require.Equal(t, testFeed.FeedID, find.FeedID) + require.Equal(t, testFeed.HashID, find.HashID) + require.Equal(t, testFeed.UserID, find.UserID) + require.Equal(t, testFeed.ItemID, find.ItemID) + require.Equal(t, testFeed.LinkType, find.LinkType) + require.Equal(t, testFeed.Provider, find.Provider) +} + +func TestPostgres_GetFeed(t *testing.T) { + stor := createPG(t) + defer func() { _ = stor.Close() }() + + err := stor.SaveFeed(testFeed) + require.NoError(t, err) + + find, err := stor.GetFeed(testFeed.HashID) + require.NoError(t, err) + + require.Equal(t, testFeed.FeedID, find.FeedID) + require.Equal(t, testFeed.HashID, find.HashID) + require.Equal(t, testFeed.UserID, find.UserID) + require.Equal(t, testFeed.ItemID, find.ItemID) + require.Equal(t, testFeed.LinkType, find.LinkType) + require.Equal(t, testFeed.Provider, find.Provider) +} + +func TestService_UpdateLastAccess(t *testing.T) { + stor := createPG(t) + defer func() { _ = stor.Close() }() + + err := stor.db.Insert(testFeed) + require.NoError(t, err) + + feed1, err := stor.GetFeed(testFeed.HashID) + require.NoError(t, err) + + feed2, err := stor.GetFeed(testFeed.HashID) + require.NoError(t, err) + + require.True(t, feed2.LastAccess.After(feed1.LastAccess)) +} + +func TestPostgres_GetMetadata(t *testing.T) { + stor := createPG(t) + defer func() { _ = stor.Close() }() + + err := stor.SaveFeed(testFeed) + require.NoError(t, err) + + find, err := stor.GetMetadata(testFeed.HashID) + require.NoError(t, err) + + require.Equal(t, testFeed.UserID, find.UserID) + require.Equal(t, testFeed.Provider, find.Provider) + require.Equal(t, testFeed.Quality, find.Quality) + require.Equal(t, testFeed.Format, find.Format) +} + +func TestService_DowngradeToAnonymous(t *testing.T) { + stor := createPG(t) + defer func() { _ = stor.Close() }() + + feed := &model.Feed{ + HashID: "123456", + UserID: "123456", + ItemID: "123456", + Provider: api.ProviderVimeo, + LinkType: api.LinkTypeGroup, + PageSize: 150, + Quality: api.QualityLow, + Format: api.FormatAudio, + FeatureLevel: api.ExtendedFeatures, + } + + err := stor.db.Insert(feed) + require.NoError(t, err) + + err = stor.Downgrade(feed.UserID, api.DefaultFeatures) + require.NoError(t, err) + + downgraded := &model.Feed{FeedID: feed.FeedID} + err = stor.db.Select(downgraded) + require.NoError(t, err) + + require.Equal(t, 50, downgraded.PageSize) + require.Equal(t, api.QualityHigh, downgraded.Quality) + require.Equal(t, api.FormatVideo, downgraded.Format) + require.Equal(t, api.DefaultFeatures, downgraded.FeatureLevel) +} + +func TestService_DowngradeToExtendedFeatures(t *testing.T) { + stor := createPG(t) + defer func() { _ = stor.Close() }() + + feed := &model.Feed{ + HashID: "123456", + UserID: "123456", + ItemID: "123456", + Provider: api.ProviderVimeo, + LinkType: api.LinkTypeGroup, + PageSize: 500, + Quality: api.QualityLow, + Format: api.FormatAudio, + FeatureLevel: api.ExtendedFeatures, + } + + err := stor.db.Insert(feed) + require.NoError(t, err) + + err = stor.Downgrade(feed.UserID, api.ExtendedFeatures) + require.NoError(t, err) + + downgraded := &model.Feed{FeedID: feed.FeedID} + err = stor.db.Select(downgraded) + require.NoError(t, err) + + require.Equal(t, 150, downgraded.PageSize) + require.Equal(t, feed.Quality, downgraded.Quality) + require.Equal(t, feed.Format, downgraded.Format) + require.Equal(t, api.ExtendedFeatures, downgraded.FeatureLevel) +} + +func TestPostgres_AddPledge(t *testing.T) { + stor := createPG(t) + defer func() { _ = stor.Close() }() + + err := stor.AddPledge(testPledge) + require.NoError(t, err) + + pledge := &model.Pledge{PledgeID: 12345} + err = stor.db.Select(pledge) + require.NoError(t, err) + + require.Equal(t, int64(12345), pledge.PledgeID) + require.Equal(t, 400, pledge.AmountCents) +} + +func TestPostgres_UpdatePledge(t *testing.T) { + stor := createPG(t) + defer func() { _ = stor.Close() }() + + err := stor.AddPledge(testPledge) + require.NoError(t, err) + + err = stor.UpdatePledge("1", &model.Pledge{AmountCents: 999}) + require.NoError(t, err) + + pledge := &model.Pledge{PledgeID: 12345} + err = stor.db.Select(pledge) + require.NoError(t, err) + require.Equal(t, 999, pledge.AmountCents) +} + +func TestPostgres_DeletePledge(t *testing.T) { + stor := createPG(t) + defer func() { _ = stor.Close() }() + + err := stor.AddPledge(testPledge) + require.NoError(t, err) + + err = stor.DeletePledge(testPledge) + require.NoError(t, err) + + err = stor.db.Select(&model.Pledge{PledgeID: 12345}) + require.Equal(t, pg.ErrNoRows, err) +} + +func TestPostgres_GetPledge(t *testing.T) { + stor := createPG(t) + defer func() { _ = stor.Close() }() + + err := stor.AddPledge(testPledge) + require.NoError(t, err) + + pledge, err := stor.GetPledge("1") + require.NoError(t, err) + require.Equal(t, 400, pledge.AmountCents) + require.Equal(t, int64(12345), pledge.PledgeID) +} + +// docker run -it --rm -p 5432:5432 -e POSTGRES_DB=podsync postgres +func createPG(t *testing.T) Postgres { + const localConnectionString = "postgres://postgres:@localhost/podsync?sslmode=disable" + + postgres, err := NewPG(localConnectionString, false) + require.NoError(t, err) + + _, err = postgres.db.Exec(pgsql) + require.NoError(t, err) + + for _, obj := range []interface{}{&model.Pledge{}, &model.Feed{}} { + _, err = postgres.db.Model(obj).Where("1=1").Delete() + require.NoError(t, err) + } + + return postgres +} diff --git a/pkg/support/patreon.go b/pkg/support/patreon.go index 002ffec..03cd4c0 100644 --- a/pkg/support/patreon.go +++ b/pkg/support/patreon.go @@ -5,22 +5,29 @@ import ( "log" "strconv" - "github.com/go-pg/pg" "github.com/mxpv/patreon-go" + "github.com/pkg/errors" + "github.com/mxpv/podsync/pkg/api" "github.com/mxpv/podsync/pkg/model" - "github.com/pkg/errors" ) const ( creatorID = "2822191" ) -type Patreon struct { - db *pg.DB +type storage interface { + AddPledge(pledge *model.Pledge) error + UpdatePledge(patronID string, pledge *model.Pledge) error + DeletePledge(pledge *model.Pledge) error + GetPledge(patronID string) (*model.Pledge, error) } -func (h Patreon) toModel(pledge *patreon.Pledge) (*model.Pledge, error) { +type Patreon struct { + db storage +} + +func ToModel(pledge *patreon.Pledge) (*model.Pledge, error) { pledgeID, err := strconv.ParseInt(pledge.ID, 10, 64) if err != nil { return nil, errors.Wrapf(err, "failed to parse pledge id: %s", pledge.ID) @@ -63,53 +70,32 @@ func (h Patreon) toModel(pledge *patreon.Pledge) (*model.Pledge, error) { } func (h Patreon) Hook(pledge *patreon.Pledge, event string) error { - obj, err := h.toModel(pledge) + obj, err := ToModel(pledge) if err != nil { return err } switch event { case patreon.EventCreatePledge: - return h.db.Insert(obj) + return h.db.AddPledge(obj) case patreon.EventUpdatePledge: // Update comes with different PledgeID from Patreon, so do update by user ID patronID := pledge.Relationships.Patron.Data.ID - updateColumns := []string{ - "declined_since", - "amount_cents", - "total_historical_amount_cents", - "outstanding_payment_amount_cents", - "is_paused", - } - - res, err := h.db.Model(obj).Column(updateColumns...).Where("patron_id = ?patron_id").Update() - if err != nil { - log.Printf("! failed to update pledge %s for user %s: %v", pledge.ID, patronID, err) + if err := h.db.UpdatePledge(patronID, obj); err != nil { return err } - if res.RowsAffected() != 1 { - log.Printf("! unexpected number of updated rows: %d for user %s", res.RowsAffected(), patronID) - return errors.New("unexpected update result") - } - return nil case patreon.EventDeletePledge: - err := h.db.Delete(obj) - if err == pg.ErrNoRows { - return nil - } - - return err + return h.db.DeletePledge(obj) default: return fmt.Errorf("unknown event: %s", event) } } func (h Patreon) FindPledge(patronID string) (*model.Pledge, error) { - p := &model.Pledge{} - return p, h.db.Model(p).Where("patron_id = ?", patronID).Limit(1).Select() + return h.db.GetPledge(patronID) } func (h Patreon) GetFeatureLevelByID(patronID string) (level int) { @@ -152,6 +138,6 @@ func (h Patreon) GetFeatureLevelFromAmount(amount int) int { return api.DefaultFeatures } -func NewPatreon(db *pg.DB) *Patreon { +func NewPatreon(db storage) *Patreon { return &Patreon{db: db} } diff --git a/pkg/support/patreon_mock_test.go b/pkg/support/patreon_mock_test.go new file mode 100644 index 0000000..5cb0aa9 --- /dev/null +++ b/pkg/support/patreon_mock_test.go @@ -0,0 +1,83 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: patreon.go + +// Package support is a generated GoMock package. +package support + +import ( + gomock "github.com/golang/mock/gomock" + model "github.com/mxpv/podsync/pkg/model" + reflect "reflect" +) + +// Mockstorage is a mock of storage interface +type Mockstorage struct { + ctrl *gomock.Controller + recorder *MockstorageMockRecorder +} + +// MockstorageMockRecorder is the mock recorder for Mockstorage +type MockstorageMockRecorder struct { + mock *Mockstorage +} + +// NewMockstorage creates a new mock instance +func NewMockstorage(ctrl *gomock.Controller) *Mockstorage { + mock := &Mockstorage{ctrl: ctrl} + mock.recorder = &MockstorageMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *Mockstorage) EXPECT() *MockstorageMockRecorder { + return m.recorder +} + +// AddPledge mocks base method +func (m *Mockstorage) AddPledge(pledge *model.Pledge) error { + ret := m.ctrl.Call(m, "AddPledge", pledge) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddPledge indicates an expected call of AddPledge +func (mr *MockstorageMockRecorder) AddPledge(pledge interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPledge", reflect.TypeOf((*Mockstorage)(nil).AddPledge), pledge) +} + +// UpdatePledge mocks base method +func (m *Mockstorage) UpdatePledge(patronID string, pledge *model.Pledge) error { + ret := m.ctrl.Call(m, "UpdatePledge", patronID, pledge) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdatePledge indicates an expected call of UpdatePledge +func (mr *MockstorageMockRecorder) UpdatePledge(patronID, pledge interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePledge", reflect.TypeOf((*Mockstorage)(nil).UpdatePledge), patronID, pledge) +} + +// DeletePledge mocks base method +func (m *Mockstorage) DeletePledge(pledge *model.Pledge) error { + ret := m.ctrl.Call(m, "DeletePledge", pledge) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePledge indicates an expected call of DeletePledge +func (mr *MockstorageMockRecorder) DeletePledge(pledge interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePledge", reflect.TypeOf((*Mockstorage)(nil).DeletePledge), pledge) +} + +// GetPledge mocks base method +func (m *Mockstorage) GetPledge(patronID string) (*model.Pledge, error) { + ret := m.ctrl.Call(m, "GetPledge", patronID) + ret0, _ := ret[0].(*model.Pledge) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPledge indicates an expected call of GetPledge +func (mr *MockstorageMockRecorder) GetPledge(patronID interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPledge", reflect.TypeOf((*Mockstorage)(nil).GetPledge), patronID) +} diff --git a/pkg/support/patreon_test.go b/pkg/support/patreon_test.go index 3443acf..6c820d9 100644 --- a/pkg/support/patreon_test.go +++ b/pkg/support/patreon_test.go @@ -1,96 +1,113 @@ +//go:generate mockgen -source=patreon.go -destination=patreon_mock_test.go -package=support + package support import ( "testing" "time" - "github.com/go-pg/pg" + "github.com/golang/mock/gomock" "github.com/mxpv/patreon-go" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "github.com/mxpv/podsync/pkg/api" "github.com/mxpv/podsync/pkg/model" - "github.com/stretchr/testify/require" ) -func TestCreate(t *testing.T) { +func TestToModel(t *testing.T) { pledge := createPledge() - hook := createHandler(t) - err := hook.Hook(pledge, patreon.EventCreatePledge) + modelPledge, err := ToModel(pledge) require.NoError(t, err) - model := &model.Pledge{PledgeID: 12345} - err = hook.db.Select(model) + require.Equal(t, modelPledge.PledgeID, int64(12345)) + require.Equal(t, modelPledge.AmountCents, 400) + require.Equal(t, modelPledge.PatronID, int64(67890)) + require.NotNil(t, modelPledge.CreatedAt) +} + +func TestCreate(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + pledge := createPledge() + expected, _ := ToModel(pledge) + + storage := NewMockstorage(ctrl) + storage.EXPECT().AddPledge(gomock.Eq(expected)).Times(1).Return(nil) + + hook := Patreon{db: storage} + + err := hook.Hook(pledge, patreon.EventCreatePledge) require.NoError(t, err) - require.Equal(t, pledge.Attributes.AmountCents, model.AmountCents) } func TestUpdate(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + pledge := createPledge() + expected, _ := ToModel(pledge) - hook := createHandler(t) - err := hook.Hook(pledge, patreon.EventCreatePledge) + storage := NewMockstorage(ctrl) + storage.EXPECT().UpdatePledge("67890", gomock.Eq(expected)) + + hook := Patreon{db: storage} + err := hook.Hook(pledge, patreon.EventUpdatePledge) require.NoError(t, err) - - pledge.Attributes.AmountCents = 999 - - err = hook.Hook(pledge, patreon.EventUpdatePledge) - require.NoError(t, err) - - model := &model.Pledge{PledgeID: 12345} - err = hook.db.Select(model) - require.NoError(t, err) - require.Equal(t, 999, model.AmountCents) } func TestDelete(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + pledge := createPledge() - hook := createHandler(t) + expected, _ := ToModel(pledge) - err := hook.Hook(pledge, patreon.EventCreatePledge) - require.NoError(t, err) + storage := NewMockstorage(ctrl) + storage.EXPECT().DeletePledge(expected) - err = hook.Hook(pledge, patreon.EventDeletePledge) + hook := Patreon{db: storage} + err := hook.Hook(pledge, patreon.EventDeletePledge) require.NoError(t, err) } func TestFindPledge(t *testing.T) { - pledge := createPledge() - hook := createHandler(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() - err := hook.Hook(pledge, patreon.EventCreatePledge) - require.NoError(t, err) + expected := &model.Pledge{} - res, err := hook.FindPledge("67890") + storage := NewMockstorage(ctrl) + storage.EXPECT().GetPledge("123").Times(1).Return(expected, nil) + + hook := Patreon{db: storage} + res, err := hook.FindPledge("123") require.NoError(t, err) - require.Equal(t, res.AmountCents, pledge.Attributes.AmountCents) + require.Equal(t, expected, res) } func TestGetFeatureLevel(t *testing.T) { - pledge := createPledge() - hook := createHandler(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() - err := hook.Hook(pledge, patreon.EventCreatePledge) + pledge := createPledge() + storage := NewMockstorage(ctrl) + + ret, err := ToModel(pledge) require.NoError(t, err) + storage.EXPECT().GetPledge(pledge.Relationships.Patron.Data.ID).Return(ret, nil) + storage.EXPECT().GetPledge("xyz").Return(nil, errors.New("not found")) + + hook := Patreon{db: storage} + require.Equal(t, api.PodcasterFeature, hook.GetFeatureLevelByID(creatorID)) require.Equal(t, api.DefaultFeatures, hook.GetFeatureLevelByID("xyz")) require.Equal(t, api.ExtendedPagination, hook.GetFeatureLevelByID(pledge.Relationships.Patron.Data.ID)) } -func createHandler(t *testing.T) *Patreon { - opts, err := pg.ParseURL("postgres://postgres:@localhost/podsync?sslmode=disable") - if err != nil { - require.NoError(t, err) - } - - db := pg.Connect(opts) - - _, err = db.Model(&model.Pledge{}).Where("1=1").Delete() - require.NoError(t, err) - - return NewPatreon(db) -} - func createPledge() *patreon.Pledge { pledge := &patreon.Pledge{ ID: "12345",