Skip to content

Commit 2aa7309

Browse files
authored
Merge pull request #317 from Contextualist/feat-s3
Support S3-compatible provider as storage backend
2 parents 7fc950f + edb64f9 commit 2aa7309

File tree

13 files changed

+366
-40
lines changed

13 files changed

+366
-40
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ Minimal configuration would look like this:
6767
```toml
6868
[server]
6969
port = 8080
70-
data_dir = "/data/podsync/"
70+
71+
[storage]
72+
[storage.local]
73+
data_dir = "/data/podsync/"
7174

7275
[tokens]
7376
youtube = "PASTE YOUR API KEY HERE"

cloud_formation.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,11 @@ Resources:
169169
tee /home/ec2-user/podsync/config.toml <<EOF
170170
[server]
171171
port = ${PodsyncPort}
172-
data_dir = "/home/ec2-user/podsync/data"
173172
hostname = "http://$publichost:${PodsyncPort}"
173+
174+
[storage]
175+
[storage.local]
176+
data_dir = "/home/ec2-user/podsync/data"
174177

175178
[tokens]
176179
youtube = "${YouTubeApiKey}"

cmd/podsync/config.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import (
99
"github.com/hashicorp/go-multierror"
1010
"github.com/pelletier/go-toml"
1111
"github.com/pkg/errors"
12+
log "github.com/sirupsen/logrus"
1213

1314
"github.com/mxpv/podsync/pkg/db"
1415
"github.com/mxpv/podsync/pkg/feed"
16+
"github.com/mxpv/podsync/pkg/fs"
1517
"github.com/mxpv/podsync/pkg/model"
1618
"github.com/mxpv/podsync/pkg/ytdl"
1719
"github.com/mxpv/podsync/services/web"
@@ -20,6 +22,8 @@ import (
2022
type Config struct {
2123
// Server is the web server configuration
2224
Server web.Config `toml:"server"`
25+
// S3 is the optional configuration for S3-compatible storage provider
26+
Storage fs.Config `toml:"storage"`
2327
// Log is the optional logging configuration
2428
Log Log `toml:"log"`
2529
// Database configuration
@@ -74,8 +78,17 @@ func LoadConfig(path string) (*Config, error) {
7478
func (c *Config) validate() error {
7579
var result *multierror.Error
7680

77-
if c.Server.DataDir == "" {
78-
result = multierror.Append(result, errors.New("data directory is required"))
81+
if c.Server.DataDir != "" {
82+
log.Warnf(`server.data_dir is deprecated, and will be removed in a future release. Use the following config instead:
83+
84+
[storage]
85+
[storage.local]
86+
data_dir = "%s"
87+
88+
`, c.Server.DataDir)
89+
if c.Storage.Local.DataDir == "" {
90+
c.Storage.Local.DataDir = c.Server.DataDir
91+
}
7992
}
8093

8194
if c.Server.Path != "" {
@@ -85,6 +98,19 @@ func (c *Config) validate() error {
8598
}
8699
}
87100

101+
switch c.Storage.Type {
102+
case "local":
103+
if c.Storage.Local.DataDir == "" {
104+
result = multierror.Append(result, errors.New("data directory is required for local storage"))
105+
}
106+
case "s3":
107+
if c.Storage.S3.EndpointURL == "" || c.Storage.S3.Region == "" || c.Storage.S3.Bucket == "" {
108+
result = multierror.Append(result, errors.New("S3 storage requires endpoint_url, region and bucket to be set"))
109+
}
110+
default:
111+
result = multierror.Append(result, errors.Errorf("unknown storage type: %s", c.Storage.Type))
112+
}
113+
88114
if len(c.Feeds) == 0 {
89115
result = multierror.Append(result, errors.New("at least one feed must be specified"))
90116
}
@@ -107,6 +133,10 @@ func (c *Config) applyDefaults(configPath string) {
107133
}
108134
}
109135

136+
if c.Storage.Type == "" {
137+
c.Storage.Type = "local"
138+
}
139+
110140
if c.Log.Filename != "" {
111141
if c.Log.MaxSize == 0 {
112142
c.Log.MaxSize = model.DefaultLogMaxSize

cmd/podsync/main.go

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
type Opts struct {
2828
ConfigPath string `long:"config" short:"c" default:"config.toml" env:"PODSYNC_CONFIG_PATH"`
29+
Headless bool `long:"headless"`
2930
Debug bool `long:"debug"`
3031
NoBanner bool `long:"no-banner"`
3132
}
@@ -59,8 +60,6 @@ func main() {
5960
ctx, cancel := context.WithCancel(context.Background())
6061
defer cancel()
6162

62-
group, ctx := errgroup.WithContext(ctx)
63-
6463
// Parse args
6564
opts := Opts{}
6665
_, err := flags.Parse(&opts)
@@ -110,8 +109,21 @@ func main() {
110109
if err != nil {
111110
log.WithError(err).Fatal("failed to open database")
112111
}
113-
114-
storage, err := fs.NewLocal(cfg.Server.DataDir)
112+
defer func() {
113+
if err := database.Close(); err != nil {
114+
log.WithError(err).Error("failed to close database")
115+
}
116+
}()
117+
118+
var storage fs.Storage
119+
switch cfg.Storage.Type {
120+
case "local":
121+
storage, err = fs.NewLocal(cfg.Storage.Local.DataDir)
122+
case "s3":
123+
storage, err = fs.NewS3(cfg.Storage.S3)
124+
default:
125+
log.Fatalf("unknown storage type: %s", cfg.Storage.Type)
126+
}
115127
if err != nil {
116128
log.WithError(err).Fatal("failed to open storage")
117129
}
@@ -133,6 +145,24 @@ func main() {
133145
log.WithError(err).Fatal("failed to create updater")
134146
}
135147

148+
// In Headless mode, do one round of feed updates and quit
149+
if opts.Headless {
150+
for _, feed := range cfg.Feeds {
151+
if err := manager.Update(ctx, feed); err != nil {
152+
log.WithError(err).Errorf("failed to update feed: %s", feed.URL)
153+
}
154+
}
155+
return
156+
}
157+
158+
group, ctx := errgroup.WithContext(ctx)
159+
defer func() {
160+
if err := group.Wait(); err != nil && (err != context.Canceled && err != http.ErrServerClosed) {
161+
log.WithError(err).Error("wait error")
162+
}
163+
log.Info("gracefully stopped")
164+
}()
165+
136166
// Queue of feeds to update
137167
updates := make(chan *feed.Config, 16)
138168
defer close(updates)
@@ -191,6 +221,10 @@ func main() {
191221
}
192222
})
193223

224+
if cfg.Storage.Type == "s3" {
225+
return // S3 content is hosted externally
226+
}
227+
194228
// Run web server
195229
srv := web.New(cfg.Server, storage)
196230

@@ -222,14 +256,4 @@ func main() {
222256
}
223257
}
224258
})
225-
226-
if err := group.Wait(); err != nil && (err != context.Canceled && err != http.ErrServerClosed) {
227-
log.WithError(err).Error("wait error")
228-
}
229-
230-
if err := database.Close(); err != nil {
231-
log.WithError(err).Error("failed to close database")
232-
}
233-
234-
log.Info("gracefully stopped")
235259
}

config.toml.example

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,21 @@ hostname = "https://my.test.host:4443"
1111
bind_address = "172.20.10.2"
1212
# Specify path for reverse proxy and only [A-Za-z0-9]
1313
path = "test"
14-
data_dir = "/app/data" # Don't change if you run podsync via docker
14+
15+
# Configure where to store the episode data
16+
[storage]
17+
# Could be "local" (default) for the local file system, or "s3" for a S3-compatible storage provider (e.g. AWS S3)
18+
type = "local"
19+
20+
[storage.local]
21+
data_dir = "/app/data" # Don't change if you run podsync via docker
22+
23+
# To configure for a S3 provider, set key and secret in environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, respectively;
24+
# then fillout the API endpoint, region, and bucket below.
25+
[storage.s3]
26+
endpoint = "https://s3.us-west-2.amazonaws.com"
27+
region = "us-west-2"
28+
bucket = "example-bucket-name"
1529

1630
# API keys to be used to access Youtube and Vimeo.
1731
# These can be either specified as string parameter or array of string (so those will be rotated).

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module github.com/mxpv/podsync
22

33
require (
44
github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec
5+
github.com/aws/aws-sdk-go v1.44.30
56
github.com/dgraph-io/badger v1.6.2
67
github.com/eduncan911/podcast v1.4.2
78
github.com/gilliek/go-opml v1.0.0
@@ -20,7 +21,6 @@ require (
2021
google.golang.org/api v0.0.0-20180718221112-efcb5f25ac56
2122
google.golang.org/appengine v1.1.0 // indirect
2223
gopkg.in/natefinch/lumberjack.v2 v2.0.0
23-
gopkg.in/yaml.v2 v2.2.8 // indirect
2424
)
2525

2626
go 1.13

go.sum

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
77
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
88
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
99
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
10+
github.com/aws/aws-sdk-go v1.44.30 h1:w7sTp6jFWRaZCDg08fUx8X4IOU4sgbCR+e+1qSmf+bc=
11+
github.com/aws/aws-sdk-go v1.44.30/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
1012
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
1113
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
1214
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@@ -43,6 +45,10 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
4345
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
4446
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
4547
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
48+
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
49+
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
50+
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
51+
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
4652
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
4753
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
4854
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -91,8 +97,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
9197
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
9298
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
9399
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
94-
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
95100
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
101+
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
102+
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
96103
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd h1:QQhib242ErYDSMitlBm8V7wYCm/1a25hV8qMadIKLPA=
97104
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
98105
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -106,11 +113,16 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
106113
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
107114
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
108115
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
109-
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
110116
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
117+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
118+
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
119+
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
111120
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
121+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
112122
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
113123
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
124+
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
125+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
114126
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
115127
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
116128
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=

pkg/fs/local.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import (
1111
log "github.com/sirupsen/logrus"
1212
)
1313

14+
// LocalConfig is the storage configuration for local file system
15+
type LocalConfig struct {
16+
DataDir string `yaml:"data_dir"`
17+
}
18+
1419
// Local implements local file storage
1520
type Local struct {
1621
rootDir string
@@ -65,3 +70,18 @@ func (l *Local) copyFile(source io.Reader, destinationPath string) (int64, error
6570

6671
return written, nil
6772
}
73+
74+
func (l *Local) Size(_ctx context.Context, name string) (int64, error) {
75+
file, err := l.Open(name)
76+
if err != nil {
77+
return 0, err
78+
}
79+
defer file.Close()
80+
81+
stat, err := file.Stat()
82+
if err != nil {
83+
return 0, err
84+
}
85+
86+
return stat.Size(), nil
87+
}

pkg/fs/local_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func TestLocal_Size(t *testing.T) {
5252
_, err = stor.Create(testCtx, "1/test", bytes.NewBuffer([]byte{1, 5, 7, 8, 3}))
5353
assert.NoError(t, err)
5454

55-
sz, err := Size(stor, "1/test")
55+
sz, err := stor.Size(testCtx, "1/test")
5656
assert.NoError(t, err)
5757
assert.EqualValues(t, 5, sz)
5858
}
@@ -61,7 +61,7 @@ func TestLocal_NoSize(t *testing.T) {
6161
stor, err := NewLocal("")
6262
assert.NoError(t, err)
6363

64-
_, err = Size(stor, "1/test")
64+
_, err = stor.Size(testCtx, "1/test")
6565
assert.True(t, os.IsNotExist(err))
6666
}
6767

@@ -80,7 +80,7 @@ func TestLocal_Delete(t *testing.T) {
8080
err = stor.Delete(testCtx, "1/test")
8181
assert.NoError(t, err)
8282

83-
_, err = Size(stor, "1/test")
83+
_, err = stor.Size(testCtx, "1/test")
8484
assert.True(t, os.IsNotExist(err))
8585

8686
_, err = os.Stat(filepath.Join(tmpDir, "1", "test"))

0 commit comments

Comments
 (0)