1
0
mirror of https://github.com/mxpv/podsync.git synced 2024-05-11 05:55:04 +00:00

Refactor database storage

This commit is contained in:
Maksym Pavlenko
2018-11-24 11:58:08 -08:00
parent 3f331844e8
commit 73ee7900e9
14 changed files with 820 additions and 345 deletions

View File

@@ -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
}

16
go.mod
View File

@@ -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
)

39
go.sum
View File

@@ -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=

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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
}

24
pkg/feeds/id_gen.go Normal file
View File

@@ -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()
}

View File

@@ -1,8 +1,9 @@
package model
import (
"github.com/mxpv/podsync/pkg/api"
"time"
"github.com/mxpv/podsync/pkg/api"
)
type Pledge struct {

184
pkg/storage/pg.go Normal file
View File

@@ -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()
}

View File

@@ -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;
COMMIT;
END;
`

226
pkg/storage/pg_test.go Normal file
View File

@@ -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
}

View File

@@ -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}
}

View File

@@ -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)
}

View File

@@ -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",