From fefe2d82a4fdddaddd485fd2f843e677327aa845 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sun, 31 Jan 2021 23:12:50 +0000 Subject: [PATCH 01/14] rebase 12/02, use go1.16rc1 in make, remove ioutil, start switching to io/fs for file i/o ioutil's contents are now in io and os. Eventually jfa-go's files will be embedded in the binary with go1.16's new embed feature. Using io/fs will provide abstraction for accessing these files, and allow for both embedded and non-embedded versions. Also, internal paths to things like email templates, etc. will be prefixed with "jfa-go:" to indicate to use the app's own Filesystem instead of reading the file normally. This also allows for custom files to continue to be used as they are currently. --- Makefile | 6 +-- config.go | 37 ++++++++++------ config/config-base.json | 2 +- email.go | 28 ++++++------ go.sum | 45 ++++++++++++++++++++ main.go | 63 ++++++++++++++++----------- pwreset.go | 3 +- setup.go | 6 +-- static.go | 30 +++++++++++++ storage.go | 94 ++++++----------------------------------- 10 files changed, 170 insertions(+), 144 deletions(-) create mode 100644 static.go diff --git a/Makefile b/Makefile index 71108ff..9365aff 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ ts-debug: cp -r ts build/data/web/js swagger: - go get github.com/swaggo/swag/cmd/swag + go1.16rc1 get github.com/swaggo/swag/cmd/swag swag init -g main.go version: @@ -51,10 +51,10 @@ version: compile: $(info Downloading deps) - go mod download + go1.16rc1 mod download $(info Building) mkdir -p build - CGO_ENABLED=0 go build -o build/jfa-go *.go + CGO_ENABLED=0 go1.16rc1 build -o build/jfa-go *.go compress: upx --lzma build/jfa-go diff --git a/config.go b/config.go index e9db6ea..89e56ab 100644 --- a/config.go +++ b/config.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io/fs" "path/filepath" "strconv" "strings" @@ -11,6 +12,14 @@ import ( var emailEnabled = false +func (app *appContext) GetPath(sect, key string) (fs.FS, string) { + val := app.config.Section(sect).Key(key).MustString("") + if strings.HasPrefix(val, "jfa-go:") { + return app.localFS, strings.TrimPrefix(val, "jfa-go:") + } + return app.systemFS, val +} + func (app *appContext) loadConfig() error { var err error app.config, err = ini.Load(app.configPath) @@ -31,26 +40,26 @@ func (app *appContext) loadConfig() error { app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/") app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false))) - app.config.Section("password_resets").Key("email_html").SetValue(app.config.Section("password_resets").Key("email_html").MustString(filepath.Join(app.localPath, "email.html"))) - app.config.Section("password_resets").Key("email_text").SetValue(app.config.Section("password_resets").Key("email_text").MustString(filepath.Join(app.localPath, "email.txt"))) + app.config.Section("password_resets").Key("email_html").SetValue(app.config.Section("password_resets").Key("email_html").MustString("jfa-go:" + "email.html")) + app.config.Section("password_resets").Key("email_text").SetValue(app.config.Section("password_resets").Key("email_text").MustString("jfa-go:" + "email.txt")) - app.config.Section("invite_emails").Key("email_html").SetValue(app.config.Section("invite_emails").Key("email_html").MustString(filepath.Join(app.localPath, "invite-email.html"))) - app.config.Section("invite_emails").Key("email_text").SetValue(app.config.Section("invite_emails").Key("email_text").MustString(filepath.Join(app.localPath, "invite-email.txt"))) + app.config.Section("invite_emails").Key("email_html").SetValue(app.config.Section("invite_emails").Key("email_html").MustString("jfa-go:" + "invite-email.html")) + app.config.Section("invite_emails").Key("email_text").SetValue(app.config.Section("invite_emails").Key("email_text").MustString("jfa-go:" + "invite-email.txt")) - app.config.Section("email_confirmation").Key("email_html").SetValue(app.config.Section("email_confirmation").Key("email_html").MustString(filepath.Join(app.localPath, "confirmation.html"))) - app.config.Section("email_confirmation").Key("email_text").SetValue(app.config.Section("email_confirmation").Key("email_text").MustString(filepath.Join(app.localPath, "confirmation.txt"))) + app.config.Section("email_confirmation").Key("email_html").SetValue(app.config.Section("email_confirmation").Key("email_html").MustString("jfa-go:" + "confirmation.html")) + app.config.Section("email_confirmation").Key("email_text").SetValue(app.config.Section("email_confirmation").Key("email_text").MustString("jfa-go:" + "confirmation.txt")) - app.config.Section("notifications").Key("expiry_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(app.localPath, "expired.html"))) - app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(app.localPath, "expired.txt"))) + app.config.Section("notifications").Key("expiry_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString("jfa-go:" + "expired.html")) + app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString("jfa-go:" + "expired.txt")) - app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.localPath, "created.html"))) - app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString(filepath.Join(app.localPath, "created.txt"))) + app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString("jfa-go:" + "created.html")) + app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString("jfa-go:" + "created.txt")) - app.config.Section("deletion").Key("email_html").SetValue(app.config.Section("deletion").Key("email_html").MustString(filepath.Join(app.localPath, "deleted.html"))) - app.config.Section("deletion").Key("email_text").SetValue(app.config.Section("deletion").Key("email_text").MustString(filepath.Join(app.localPath, "deleted.txt"))) + app.config.Section("deletion").Key("email_html").SetValue(app.config.Section("deletion").Key("email_html").MustString("jfa-go:" + "deleted.html")) + app.config.Section("deletion").Key("email_text").SetValue(app.config.Section("deletion").Key("email_text").MustString("jfa-go:" + "deleted.txt")) - app.config.Section("welcome_email").Key("email_html").SetValue(app.config.Section("welcome_email").Key("email_html").MustString(filepath.Join(app.localPath, "welcome.html"))) - app.config.Section("welcome_email").Key("email_text").SetValue(app.config.Section("welcome_email").Key("email_text").MustString(filepath.Join(app.localPath, "welcome.txt"))) + app.config.Section("welcome_email").Key("email_html").SetValue(app.config.Section("welcome_email").Key("email_html").MustString("jfa-go:" + "welcome.html")) + app.config.Section("welcome_email").Key("email_text").SetValue(app.config.Section("welcome_email").Key("email_text").MustString("jfa-go:" + "welcome.txt")) app.config.Section("jellyfin").Key("version").SetValue(VERSION) app.config.Section("jellyfin").Key("device").SetValue("jfa-go") diff --git a/config/config-base.json b/config/config-base.json index e72821c..837ed3f 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -65,7 +65,7 @@ ["emby", "Emby"] ], "value": "jellyfin", - "description": "Note: Emby integration works is missing some features, such as Password Resets." + "description": "Note: Emby integration works but is missing some features, such as Password Resets." }, "substitute_jellyfin_strings": { "name": "Substitute occurrences of \"Jellyfin\"", diff --git a/email.go b/email.go index 2c1f95b..6cfc788 100644 --- a/email.go +++ b/email.go @@ -162,8 +162,8 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key) for _, key := range []string{"html", "text"} { - fpath := app.config.Section("email_confirmation").Key("email_" + key).String() - tpl, err := template.ParseFiles(fpath) + filesystem, fpath := app.GetPath("email_confirmation", "email_"+key) + tpl, err := template.ParseFS(filesystem, fpath) if err != nil { return nil, err } @@ -199,8 +199,8 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont inviteLink = fmt.Sprintf("%s/%s", inviteLink, code) for _, key := range []string{"html", "text"} { - fpath := app.config.Section("invite_emails").Key("email_" + key).String() - tpl, err := template.ParseFiles(fpath) + filesystem, fpath := app.GetPath("invite_emails", "email_"+key) + tpl, err := template.ParseFS(filesystem, fpath) if err != nil { return nil, err } @@ -232,8 +232,8 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont } expiry := app.formatDatetime(invite.ValidTill) for _, key := range []string{"html", "text"} { - fpath := app.config.Section("notifications").Key("expiry_" + key).String() - tpl, err := template.ParseFiles(fpath) + filesystem, fpath := app.GetPath("notifications", "expiry_"+key) + tpl, err := template.ParseFS(filesystem, fpath) if err != nil { return nil, err } @@ -267,8 +267,8 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite tplAddress = address } for _, key := range []string{"html", "text"} { - fpath := app.config.Section("notifications").Key("created_" + key).String() - tpl, err := template.ParseFiles(fpath) + filesystem, fpath := app.GetPath("notifications", "created_"+key) + tpl, err := template.ParseFS(filesystem, fpath) if err != nil { return nil, err } @@ -302,8 +302,8 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Ema d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern) message := app.config.Section("email").Key("message").String() for _, key := range []string{"html", "text"} { - fpath := app.config.Section("password_resets").Key("email_" + key).String() - tpl, err := template.ParseFiles(fpath) + filesystem, fpath := app.GetPath("password_resets", "email_"+key) + tpl, err := template.ParseFS(filesystem, fpath) if err != nil { return nil, err } @@ -335,8 +335,8 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")), } for _, key := range []string{"html", "text"} { - fpath := app.config.Section("deletion").Key("email_" + key).String() - tpl, err := template.ParseFiles(fpath) + filesystem, fpath := app.GetPath("deletion", "email_"+key) + tpl, err := template.ParseFS(filesystem, fpath) if err != nil { return nil, err } @@ -363,8 +363,8 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext) (*Ema subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")), } for _, key := range []string{"html", "text"} { - fpath := app.config.Section("welcome_email").Key("email_" + key).String() - tpl, err := template.ParseFiles(fpath) + filesystem, fpath := app.GetPath("welcome_email", "email_"+key) + tpl, err := template.ParseFS(filesystem, fpath) if err != nil { return nil, err } diff --git a/go.sum b/go.sum index b2b2fdf..fd39922 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ +cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= @@ -9,30 +11,41 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk= github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanw/esbuild v0.8.44 h1:9svHk3MxC3T8ThKkUJ71GcPXYGMhxhO5iCfg2hrU0PU= github.com/evanw/esbuild v0.8.44/go.mod h1:y2AFBAGVelPqPodpdtxWWqe6n2jYf5FrsJbligmRmuw= +github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= +github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc= github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0= github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0= @@ -102,6 +115,7 @@ github.com/go-openapi/swag v0.19.12 h1:Bc0bnY2c3AoF7Gc+IMIAQQsD8fLHjHpc19wXvYuay github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= github.com/go-openapi/swag v0.19.13 h1:233UVgMy1DlmCYYfOiFpta6e2urloh+sEs5id6lyzog= github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= @@ -117,8 +131,11 @@ github.com/go-playground/validator/v10 v10.4.0 h1:72qIR/m8ybvL8L5TIyfgrigqkrw7kV github.com/go-playground/validator/v10 v10.4.0/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -139,7 +156,9 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -164,10 +183,13 @@ github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9 h1:GQE1iatYDRrIidq4Zf/ github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9/go.mod h1:4ZxfWkxwtc7dBeifERVVWRy9F9rTU9p0yCDgeCtlius= github.com/knz/strtime v0.0.0-20200924090105-187c67f2bf5e h1:ViPE0JEOvtw5I0EGUiFSr2VNKGNU+3oBT+oHbDXHbxk= github.com/knz/strtime v0.0.0-20200924090105-187c67f2bf5e/go.mod h1:4ZxfWkxwtc7dBeifERVVWRy9F9rTU9p0yCDgeCtlius= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5 h1:hyz3dwM5QLc1Rfoz4FuWJQG5BN7tc6K1MndAUnGpQr4= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= @@ -218,6 +240,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLD github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858 h1:lgbJiJQx8bXo+eM88AFdd0VxUvaTLzCBXpK+H9poJ+Y= github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858/go.mod h1:y02HeaN0visd95W6cEX2NXDv5sCwyqfzucWTdDGEwYY= @@ -225,20 +248,25 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= @@ -286,6 +314,7 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -297,10 +326,13 @@ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rB golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU= golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -335,12 +367,14 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLD golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -374,6 +408,7 @@ golang.org/x/sys v0.0.0-20210123111255-9b0068b26619 h1:yLLDsUUPDliIQpKl7BjVb1igw golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -422,14 +457,18 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -444,9 +483,13 @@ google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -467,6 +510,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index 6c745fc..e36d3a3 100644 --- a/main.go +++ b/main.go @@ -7,8 +7,9 @@ import ( "encoding/json" "flag" "fmt" + "html/template" "io" - "io/ioutil" + "io/fs" "log" "mime" "net" @@ -58,7 +59,9 @@ type appContext struct { configBasePath string configBase settings dataPath string - localPath string + systemFS fs.FS + localFS fs.FS + webFS httpFS cssClass string jellyfinLogin bool users []User @@ -82,8 +85,8 @@ type appContext struct { func (app *appContext) loadHTML(router *gin.Engine) { customPath := app.config.Section("files").Key("html_templates").MustString("") - templatePath := filepath.Join(app.localPath, "html") - htmlFiles, err := ioutil.ReadDir(templatePath) + templatePath := "html" + htmlFiles, err := fs.ReadDir(app.localFS, templatePath) if err != nil { app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath) return @@ -98,7 +101,11 @@ func (app *appContext) loadHTML(router *gin.Engine) { loadFiles[i] = filepath.Join(filepath.Join(customPath, f.Name())) } } - router.LoadHTMLFiles(loadFiles...) + tmpl, err := template.ParseFS(app.localFS, loadFiles...) + if err != nil { + app.err.Fatalf("Failed to load templates: %v", err) + } + router.SetHTMLTemplate(tmpl) } func generateSecret(length int) (string, error) { @@ -194,7 +201,14 @@ func start(asDaemon, firstCall bool) { app.dataPath = filepath.Join(userConfigDir, "jfa-go") app.configPath = filepath.Join(app.dataPath, "config.ini") executable, _ := os.Executable() - app.localPath = filepath.Join(filepath.Dir(executable), "data") + app.localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data")) + app.systemFS = os.DirFS("/") + wfs := os.DirFS(filepath.Join(filepath.Dir(executable), "data", "web")) + app.webFS = httpFS{ + hfs: http.FS(wfs), + fs: wfs, + } + app.webFS.fs = wfs app.info = log.New(os.Stdout, "[INFO] ", log.Ltime) app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile) @@ -251,23 +265,19 @@ func start(asDaemon, firstCall bool) { } if _, err := os.Stat(app.configPath); os.IsNotExist(err) { firstRun = true - dConfigPath := filepath.Join(app.localPath, "config-default.ini") - var dConfig *os.File - dConfig, err = os.Open(dConfigPath) + dConfig, err := fs.ReadFile(app.localFS, "config-default.ini") if err != nil { - app.err.Fatalf("Couldn't find default config file \"%s\"", dConfigPath) + app.err.Fatalf("Couldn't find default config file") } - defer dConfig.Close() - var nConfig *os.File nConfig, err := os.Create(app.configPath) if err != nil { app.err.Printf("Couldn't open config file for writing: \"%s\"", app.configPath) app.err.Fatalf("Error: %s", err) } defer nConfig.Close() - _, err = io.Copy(nConfig, dConfig) + _, err = nConfig.Write(dConfig) if err != nil { - app.err.Fatalf("Couldn't copy default config. To do this manually, copy\n%s\nto\n%s", dConfigPath, app.configPath) + app.err.Fatalf("Couldn't copy default config.") } app.info.Printf("Copied default configuration to \"%s\"", app.configPath) } @@ -288,7 +298,7 @@ func start(asDaemon, firstCall bool) { app.info.Print(aurora.Magenta("\n\nWARNING: Don't use debug mode in production, as it exposes pprof on the network.\n\n")) app.debug = log.New(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile) } else { - app.debug = log.New(ioutil.Discard, "", 0) + app.debug = log.New(io.Discard, "", 0) } if asDaemon { @@ -330,10 +340,10 @@ func start(asDaemon, firstCall bool) { }() } - app.storage.lang.CommonPath = filepath.Join(app.localPath, "lang", "common") - app.storage.lang.FormPath = filepath.Join(app.localPath, "lang", "form") - app.storage.lang.AdminPath = filepath.Join(app.localPath, "lang", "admin") - app.storage.lang.EmailPath = filepath.Join(app.localPath, "lang", "email") + app.storage.lang.CommonPath = filepath.Join("lang", "common") + app.storage.lang.FormPath = filepath.Join("lang", "form") + app.storage.lang.AdminPath = filepath.Join("lang", "admin") + app.storage.lang.EmailPath = filepath.Join("lang", "email") err := app.storage.loadLang() if err != nil { app.info.Fatalf("Failed to load language files: %+v\n", err) @@ -413,8 +423,8 @@ func start(asDaemon, firstCall bool) { } - app.configBasePath = filepath.Join(app.localPath, "config-base.json") - configBase, _ := ioutil.ReadFile(app.configBasePath) + app.configBasePath = "config-base.json" + configBase, _ := fs.ReadFile(app.localFS, app.configBasePath) json.Unmarshal(configBase, &app.configBase) themes := map[string]string{ @@ -562,7 +572,7 @@ func start(asDaemon, firstCall bool) { } else { debugMode = false address = "0.0.0.0:8056" - app.storage.lang.SetupPath = filepath.Join(app.localPath, "lang", "setup") + app.storage.lang.SetupPath = filepath.Join("lang", "setup") err := app.storage.loadLangSetup() if err != nil { app.info.Fatalf("Failed to load language files: %+v\n", err) @@ -583,14 +593,17 @@ func start(asDaemon, firstCall bool) { setGinLogger(router, debugMode) router.Use(gin.Recovery()) + // Move to router.go routePrefixes := []string{app.URLBase} if app.URLBase != "" { routePrefixes = append(routePrefixes, "") } for _, p := range routePrefixes { - router.Use(static.Serve(p+"/", static.LocalFile(filepath.Join(app.localPath, "web"), false))) + router.Use(static.Serve(p+"/", app.webFS)) } + // app.loadHTML(router) + router.Use(static.Serve("/", app.webFS)) router.NoRoute(app.NoRouteHandler) if debugMode { app.debug.Println("Loading pprof") @@ -600,6 +613,7 @@ func start(asDaemon, firstCall bool) { router.GET(p+"/lang/:page", app.GetLanguages) } if !firstRun { + // Move to router for _, p := range routePrefixes { router.GET(p+"/", app.AdminPage) router.GET(p+"/accounts", app.AdminPage) @@ -608,7 +622,7 @@ func start(asDaemon, firstCall bool) { router.GET(p+"/token/login", app.getTokenLogin) router.GET(p+"/token/refresh", app.getTokenRefresh) router.POST(p+"/newUser", app.NewUser) - router.Use(static.Serve(p+"/invite/", static.LocalFile(filepath.Join(app.localPath, "web"), false))) + router.Use(static.Serve(p+"/invite/", app.webFS)) router.GET(p+"/invite/:invCode", app.InviteProxy) } if *SWAGGER { @@ -650,7 +664,6 @@ func start(asDaemon, firstCall bool) { router.POST("/config", app.ModifyConfig) app.info.Printf("Loading setup @ %s", address) } - SRV = &http.Server{ Addr: address, Handler: router, diff --git a/pwreset.go b/pwreset.go index 401ddd1..98e9fad 100644 --- a/pwreset.go +++ b/pwreset.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "io/ioutil" "os" "strings" "time" @@ -53,7 +52,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) { } if event.Op&fsnotify.Write == fsnotify.Write && strings.Contains(event.Name, "passwordreset") { var pwr PasswordReset - data, err := ioutil.ReadFile(event.Name) + data, err := os.ReadFile(event.Name) if err != nil { return } diff --git a/setup.go b/setup.go index 083669e..a79857e 100644 --- a/setup.go +++ b/setup.go @@ -2,7 +2,7 @@ package main import ( "encoding/json" - "io/ioutil" + "os" "path/filepath" "strings" @@ -76,7 +76,7 @@ func (st *Storage) loadLangSetup() error { load := func(fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := setupLang{} - f, err := ioutil.ReadFile(filepath.Join(st.lang.SetupPath, fname)) + f, err := os.ReadFile(filepath.Join(st.lang.SetupPath, fname)) if err != nil { return err } @@ -112,7 +112,7 @@ func (st *Storage) loadLangSetup() error { return err } english = st.lang.Setup["en-us"] - files, err := ioutil.ReadDir(st.lang.SetupPath) + files, err := os.ReadDir(st.lang.SetupPath) if err != nil { return err } diff --git a/static.go b/static.go new file mode 100644 index 0000000..3ef9436 --- /dev/null +++ b/static.go @@ -0,0 +1,30 @@ +package main + +import ( + "io/fs" + "net/http" + "strings" +) + +type httpFS struct { + hfs http.FileSystem + fs fs.FS +} + +func (f httpFS) Open(name string) (http.File, error) { + return f.hfs.Open(name) +} + +func (f httpFS) Exists(prefix string, filepath string) bool { + if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) { + stats, err := fs.Stat(f.fs, p) + if err != nil { + return false + } + if stats.IsDir() { + return false + } + return true + } + return false +} diff --git a/storage.go b/storage.go index f0562f7..3f3e716 100644 --- a/storage.go +++ b/storage.go @@ -2,8 +2,8 @@ package main import ( "encoding/json" - "io/ioutil" "log" + "os" "path/filepath" "strconv" "strings" @@ -42,7 +42,7 @@ type Invite struct { Notify map[string]map[string]bool `json:"notify"` Profile string `json:"profile"` Label string `json:"label,omitempty"` - Keys []string `json"keys,omitempty"` + Keys []string `json"keys,omitempty"` } type Lang struct { @@ -126,7 +126,7 @@ func (st *Storage) loadLangCommon() error { load := func(fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := commonLang{} - f, err := ioutil.ReadFile(filepath.Join(st.lang.CommonPath, fname)) + f, err := os.ReadFile(filepath.Join(st.lang.CommonPath, fname)) if err != nil { return err } @@ -148,7 +148,7 @@ func (st *Storage) loadLangCommon() error { return err } english = st.lang.Common["en-us"] - files, err := ioutil.ReadDir(st.lang.CommonPath) + files, err := os.ReadDir(st.lang.CommonPath) if err != nil { return err } @@ -169,7 +169,7 @@ func (st *Storage) loadLangAdmin() error { load := func(fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := adminLang{} - f, err := ioutil.ReadFile(filepath.Join(st.lang.AdminPath, fname)) + f, err := os.ReadFile(filepath.Join(st.lang.AdminPath, fname)) if err != nil { return err } @@ -199,7 +199,7 @@ func (st *Storage) loadLangAdmin() error { return err } english = st.lang.Admin["en-us"] - files, err := ioutil.ReadDir(st.lang.AdminPath) + files, err := os.ReadDir(st.lang.AdminPath) if err != nil { return err } @@ -220,7 +220,7 @@ func (st *Storage) loadLangForm() error { load := func(fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := formLang{} - f, err := ioutil.ReadFile(filepath.Join(st.lang.FormPath, fname)) + f, err := os.ReadFile(filepath.Join(st.lang.FormPath, fname)) if err != nil { return err } @@ -255,7 +255,7 @@ func (st *Storage) loadLangForm() error { return err } english = st.lang.Form["en-us"] - files, err := ioutil.ReadDir(st.lang.FormPath) + files, err := os.ReadDir(st.lang.FormPath) if err != nil { return err } @@ -276,7 +276,7 @@ func (st *Storage) loadLangEmail() error { load := func(fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := emailLang{} - f, err := ioutil.ReadFile(filepath.Join(st.lang.EmailPath, fname)) + f, err := os.ReadFile(filepath.Join(st.lang.EmailPath, fname)) if err != nil { return err } @@ -304,7 +304,7 @@ func (st *Storage) loadLangEmail() error { return err } english = st.lang.Email["en-us"] - files, err := ioutil.ReadDir(st.lang.EmailPath) + files, err := os.ReadDir(st.lang.EmailPath) if err != nil { return err } @@ -329,76 +329,6 @@ func (st *Storage) storeInvites() error { return storeJSON(st.invite_path, st.invites) } -// func (st *Storage) loadLang() error { -// loadData := func(path string, stringJson bool) (map[string]string, map[string]map[string]interface{}, error) { -// files, err := ioutil.ReadDir(path) -// outString := map[string]string{} -// out := map[string]map[string]interface{}{} -// if err != nil { -// return nil, nil, err -// } -// for _, f := range files { -// index := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name())) -// var data map[string]interface{} -// var file []byte -// var err error -// file, err = ioutil.ReadFile(filepath.Join(path, f.Name())) -// if err != nil { -// file = []byte("{}") -// } -// // Replace Jellyfin with something if necessary -// if substituteStrings != "" { -// fileString := strings.ReplaceAll(string(file), "Jellyfin", substituteStrings) -// file = []byte(fileString) -// } -// err = json.Unmarshal(file, &data) -// if err != nil { -// log.Printf("ERROR: Failed to read \"%s\": %s", path, err) -// return nil, nil, err -// } -// if stringJson { -// stringJSON, err := json.Marshal(data) -// if err != nil { -// return nil, nil, err -// } -// outString[index] = string(stringJSON) -// } -// out[index] = data -// -// } -// return outString, out, nil -// } -// _, form, err := loadData(st.lang.FormPath, false) -// if err != nil { -// return err -// } -// for index, lang := range form { -// validationStrings := lang["validationStrings"].(map[string]interface{}) -// vS, err := json.Marshal(validationStrings) -// if err != nil { -// return err -// } -// lang["validationStrings"] = string(vS) -// form[index] = lang -// } -// st.lang.Form = form -// adminJSON, admin, err := loadData(st.lang.AdminPath, true) -// st.lang.Admin = admin -// st.lang.AdminJSON = adminJSON -// -// _, emails, err := loadData(st.lang.EmailPath, false) -// fixedEmails := map[string]map[string]map[string]interface{}{} -// for lang, e := range emails { -// f := map[string]map[string]interface{}{} -// for field, vals := range e { -// f[field] = vals.(map[string]interface{}) -// } -// fixedEmails[lang] = f -// } -// st.lang.Email = fixedEmails -// return err -// } - func (st *Storage) loadEmails() error { return loadJSON(st.emails_path, &st.emails) } @@ -495,7 +425,7 @@ func (st *Storage) migrateToProfile() error { func loadJSON(path string, obj interface{}) error { var file []byte var err error - file, err = ioutil.ReadFile(path) + file, err = os.ReadFile(path) if err != nil { file = []byte("{}") } @@ -511,7 +441,7 @@ func storeJSON(path string, obj interface{}) error { if err != nil { return err } - err = ioutil.WriteFile(path, data, 0644) + err = os.WriteFile(path, data, 0644) if err != nil { log.Printf("ERROR: Failed to write to \"%s\": %s", path, err) } From 0330540f87c0c02616a75883cf267c1c3a2eb0d6 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 1 Feb 2021 17:39:19 +0000 Subject: [PATCH 02/14] Use fs for language, add lang_files option The local app translations are loaded, and then if [files]/lang_files is provided (a directory containing custom translations), any found inside it are loaded over top. This makes customizing much easier. --- config/config-base.json | 8 ++ go.mod | 3 +- go.sum | 42 ++++++++++ main.go | 18 +++-- setup.go | 40 +++++---- static.go | 4 +- storage.go | 175 ++++++++++++++++++++++++++-------------- 7 files changed, 207 insertions(+), 83 deletions(-) diff --git a/config/config-base.json b/config/config-base.json index 837ed3f..6731a6e 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -860,6 +860,14 @@ "type": "text", "value": "", "description": "Path to directory containing custom versions of web ui pages. See wiki for more info." + }, + "lang_files": { + "name": "Custom language files directory", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Useful if you want to customize the text in jfa-go. Should follow the same structure as the 'lang' directory, which you can see on GitHub." } } } diff --git a/go.mod b/go.mod index 42f2a71..0153944 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,8 @@ require ( golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect golang.org/x/text v0.3.5 // indirect - golang.org/x/tools v0.1.0 // indirect + golang.org/x/tools v0.1.1-0.20210129181147-0cef57b5b584 // indirect + golang.org/x/tools/gopls v0.0.0-20210201165201-19db92ec3be1 // indirect google.golang.org/protobuf v1.25.0 // indirect gopkg.in/ini.v1 v1.62.0 ) diff --git a/go.sum b/go.sum index fd39922..5d58702 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -158,14 +159,22 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/safehtml v0.0.2 h1:ZOt2VXg4x24bW0m2jtzAOkhoXV0iM8vNKc0paByCZqM= +github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hrfee/jfa-go/jfapi v0.0.0-20210109010027-4aae65518089 h1:WRk+JAywI8V4u+PBQpdvXBX73yCZxgnLwyIiX7xL+Xc= github.com/hrfee/jfa-go/jfapi v0.0.0-20210109010027-4aae65518089/go.mod h1:Al1Rd1JGtpS+3KnK8t7+J0CZVDbT86QJrXHR6kZijds= +github.com/jba/templatecheck v0.5.0 h1:sZwNjXG3xNApuwKmgUWEo2JuxmG0sgNaELl0zwRQ9x8= +github.com/jba/templatecheck v0.5.0/go.mod h1:/1k7EajoSErFI9GLHAsiIJEaNLt3ALKNw2TV7z2SYv4= github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e h1:OGunVjqY7y4U4laftpEHv+mvZBlr7UGimJXKEGQtg48= github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY= github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible h1:CL0ooBNfbNyJTJATno+m0h+zM5bW6v7fKlboKUGP/dI= @@ -179,6 +188,8 @@ github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGn github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9 h1:GQE1iatYDRrIidq4Zf/9ZzKWyrTk2sXOYc1JADbkAjQ= github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9/go.mod h1:4ZxfWkxwtc7dBeifERVVWRy9F9rTU9p0yCDgeCtlius= github.com/knz/strtime v0.0.0-20200924090105-187c67f2bf5e h1:ViPE0JEOvtw5I0EGUiFSr2VNKGNU+3oBT+oHbDXHbxk= @@ -248,21 +259,31 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0= +github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sanity-io/litter v1.3.0 h1:5ZO+weUsqdSWMUng5JnpkW/Oz8iTXiIdeumhQr1sSjs= +github.com/sanity-io/litter v1.3.0/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -313,6 +334,7 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -332,8 +354,11 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -347,6 +372,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200923182212-328152dc79b1 h1:Iu68XRPd67wN4aRGGWwwq6bZo/25jR6uu52l/j2KkUE= @@ -373,6 +399,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -429,6 +456,7 @@ golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59 h1:QjA/9ArTfVTLfEhClDCG7SGrZkZixxWpwNCDiwJfh88= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200923182640-463111b69878 h1:VUw1+Jf6KJPf82mbTQMia6HCnNMv2BbAipkEZ4KTcqQ= golang.org/x/tools v0.0.0-20200923182640-463111b69878/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= @@ -450,10 +478,15 @@ golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b h1:Ych5r0Z6MLML1fgf5hTg9p5 golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e h1:t96dS3DO8DGjawSLJL/HIdz8CycAd2v07XxqB3UPTi0= golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee h1:5xKxdl/RhlelmSPaxyVeq5PYSmJ4H14yeQT58qP1F6o= golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1-0.20210129181147-0cef57b5b584 h1:JAI5SUo/oOtQXK4jvtjJMlwF5opt8qBUpxGa86SJ6zU= +golang.org/x/tools v0.1.1-0.20210129181147-0cef57b5b584/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools/gopls v0.0.0-20210201165201-19db92ec3be1 h1:YRvjnCA/wBOOkQAEKC5ts16/1IJ+NEO9eevhfDF5ues= +golang.org/x/tools/gopls v0.0.0-20210201165201-19db92ec3be1/go.mod h1:DWl5nefYvX46i2mLQVu6Ud0ycJQ3HNPbQKzUHT3VUek= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -483,8 +516,11 @@ google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 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= @@ -515,3 +551,9 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2020.1.6 h1:W18jzjh8mfPez+AwGLxmOImucz/IFjpNlrKVnaj2YVc= +honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzEYLhQB2YY= +mvdan.cc/gofumpt v0.1.0 h1:hsVv+Y9UsZ/mFZTxJZuHVI6shSQCtzZ11h1JEFPAZLw= +mvdan.cc/gofumpt v0.1.0/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= +mvdan.cc/xurls/v2 v2.2.0 h1:NSZPykBXJFCetGZykLAxaL6SIpvbVy/UFEniIfHAa8A= +mvdan.cc/xurls/v2 v2.2.0/go.mod h1:EV1RMtya9D6G5DMYPGD8zTQzaHet6Jh8gFlRgGRJeO8= diff --git a/main.go b/main.go index e36d3a3..7e1c5ed 100644 --- a/main.go +++ b/main.go @@ -61,6 +61,7 @@ type appContext struct { dataPath string systemFS fs.FS localFS fs.FS + langFS fs.FS webFS httpFS cssClass string jellyfinLogin bool @@ -202,6 +203,7 @@ func start(asDaemon, firstCall bool) { app.configPath = filepath.Join(app.dataPath, "config.ini") executable, _ := os.Executable() app.localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data")) + app.langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data", "lang")) app.systemFS = os.DirFS("/") wfs := os.DirFS(filepath.Join(filepath.Dir(executable), "data", "web")) app.webFS = httpFS{ @@ -340,11 +342,17 @@ func start(asDaemon, firstCall bool) { }() } - app.storage.lang.CommonPath = filepath.Join("lang", "common") - app.storage.lang.FormPath = filepath.Join("lang", "form") - app.storage.lang.AdminPath = filepath.Join("lang", "admin") - app.storage.lang.EmailPath = filepath.Join("lang", "email") - err := app.storage.loadLang() + app.storage.lang.CommonPath = "common" + app.storage.lang.FormPath = "form" + app.storage.lang.AdminPath = "admin" + app.storage.lang.EmailPath = "email" + externalLang := app.config.Section("files").Key("lang_files").MustString("") + var err error + if externalLang == "" { + err = app.storage.loadLang(app.langFS) + } else { + err = app.storage.loadLang(app.langFS, os.DirFS(externalLang)) + } if err != nil { app.info.Fatalf("Failed to load language files: %+v\n", err) } diff --git a/setup.go b/setup.go index a79857e..307ca1f 100644 --- a/setup.go +++ b/setup.go @@ -2,7 +2,7 @@ package main import ( "encoding/json" - "os" + "io/fs" "path/filepath" "strings" @@ -70,13 +70,14 @@ func (app *appContext) TestJF(gc *gin.Context) { gc.JSON(200, map[string]bool{"success": true}) } -func (st *Storage) loadLangSetup() error { +// The first filesystem passed should be the localFS, to ensure the local lang files are loaded first. +func (st *Storage) loadLangSetup(filesystems ...fs.FS) error { st.lang.Setup = map[string]setupLang{} var english setupLang - load := func(fname string) error { + load := func(filesystem fs.FS, fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := setupLang{} - f, err := os.ReadFile(filepath.Join(st.lang.SetupPath, fname)) + f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.SetupPath, fname)) if err != nil { return err } @@ -107,20 +108,29 @@ func (st *Storage) loadLangSetup() error { st.lang.Setup[index] = lang return nil } - err := load("en-us.json") - if err != nil { - return err + engFound := false + var err error + for _, filesystem := range filesystems { + err = load(filesystem, "en-us.json") + if err == nil { + engFound = true + } } - english = st.lang.Setup["en-us"] - files, err := os.ReadDir(st.lang.SetupPath) - if err != nil { + if !engFound { return err } - for _, f := range files { - if f.Name() != "en-us.json" { - err = load(f.Name()) - if err != nil { - return err + english = st.lang.Setup["en-us"] + for _, filesystem := range filesystems { + files, err := fs.ReadDir(filesystem, st.lang.SetupPath) + if err != nil { + return err + } + for _, f := range files { + if f.Name() != "en-us.json" { + err = load(filesystem, f.Name()) + if err != nil { + return err + } } } } diff --git a/static.go b/static.go index 3ef9436..346eef5 100644 --- a/static.go +++ b/static.go @@ -6,8 +6,10 @@ import ( "strings" ) +// Since the gin-static middleware uses a version of http.Filesystem with an extra Exists() func, we extend it here. + type httpFS struct { - hfs http.FileSystem + hfs http.FileSystem // Created by converting fs.FS using http.FS() fs fs.FS } diff --git a/storage.go b/storage.go index 3f3e716..b89c8f3 100644 --- a/storage.go +++ b/storage.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "io/fs" "log" "os" "path/filepath" @@ -62,20 +63,20 @@ type Lang struct { Setup setupLangs } -func (st *Storage) loadLang() (err error) { - err = st.loadLangCommon() +func (st *Storage) loadLang(filesystems ...fs.FS) (err error) { + err = st.loadLangCommon(filesystems...) if err != nil { return } - err = st.loadLangAdmin() + err = st.loadLangAdmin(filesystems...) if err != nil { return } - err = st.loadLangForm() + err = st.loadLangForm(filesystems...) if err != nil { return } - err = st.loadLangEmail() + err = st.loadLangEmail(filesystems...) return } @@ -120,13 +121,13 @@ func patchQuantityStrings(english, other *map[string]quantityString) { } } -func (st *Storage) loadLangCommon() error { +func (st *Storage) loadLangCommon(filesystems ...fs.FS) error { st.lang.Common = map[string]commonLang{} var english commonLang - load := func(fname string) error { + load := func(filesystem fs.FS, fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := commonLang{} - f, err := os.ReadFile(filepath.Join(st.lang.CommonPath, fname)) + f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.CommonPath, fname)) if err != nil { return err } @@ -143,33 +144,46 @@ func (st *Storage) loadLangCommon() error { st.lang.Common[index] = lang return nil } - err := load("en-us.json") - if err != nil { - return err + engFound := false + var err error + for _, filesystem := range filesystems { + err = load(filesystem, "en-us.json") + if err == nil { + engFound = true + } } - english = st.lang.Common["en-us"] - files, err := os.ReadDir(st.lang.CommonPath) - if err != nil { + if !engFound { return err } - for _, f := range files { - if f.Name() != "en-us.json" { - err = load(f.Name()) - if err != nil { - return err + english = st.lang.Common["en-us"] + commonLoaded := false + for _, filesystem := range filesystems { + files, err := fs.ReadDir(filesystem, st.lang.CommonPath) + if err != nil { + continue + } + for _, f := range files { + if f.Name() != "en-us.json" { + err = load(filesystem, f.Name()) + if err == nil { + commonLoaded = true + } } } } + if !commonLoaded { + return err + } return nil } -func (st *Storage) loadLangAdmin() error { +func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error { st.lang.Admin = map[string]adminLang{} var english adminLang - load := func(fname string) error { + load := func(filesystem fs.FS, fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := adminLang{} - f, err := os.ReadFile(filepath.Join(st.lang.AdminPath, fname)) + f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.AdminPath, fname)) if err != nil { return err } @@ -194,33 +208,46 @@ func (st *Storage) loadLangAdmin() error { st.lang.Admin[index] = lang return nil } - err := load("en-us.json") - if err != nil { - return err + engFound := false + var err error + for _, filesystem := range filesystems { + err = load(filesystem, "en-us.json") + if err == nil { + engFound = true + } } - english = st.lang.Admin["en-us"] - files, err := os.ReadDir(st.lang.AdminPath) - if err != nil { + if !engFound { return err } - for _, f := range files { - if f.Name() != "en-us.json" { - err = load(f.Name()) - if err != nil { - return err + english = st.lang.Admin["en-us"] + adminLoaded := false + for _, filesystem := range filesystems { + files, err := fs.ReadDir(filesystem, st.lang.AdminPath) + if err != nil { + continue + } + for _, f := range files { + if f.Name() != "en-us.json" { + err = load(filesystem, f.Name()) + if err == nil { + adminLoaded = true + } } } } + if !adminLoaded { + return err + } return nil } -func (st *Storage) loadLangForm() error { +func (st *Storage) loadLangForm(filesystems ...fs.FS) error { st.lang.Form = map[string]formLang{} var english formLang - load := func(fname string) error { + load := func(filesystem fs.FS, fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := formLang{} - f, err := os.ReadFile(filepath.Join(st.lang.FormPath, fname)) + f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.FormPath, fname)) if err != nil { return err } @@ -250,33 +277,46 @@ func (st *Storage) loadLangForm() error { st.lang.Form[index] = lang return nil } - err := load("en-us.json") - if err != nil { - return err + engFound := false + var err error + for _, filesystem := range filesystems { + err = load(filesystem, "en-us.json") + if err == nil { + engFound = true + } } - english = st.lang.Form["en-us"] - files, err := os.ReadDir(st.lang.FormPath) - if err != nil { + if !engFound { return err } - for _, f := range files { - if f.Name() != "en-us.json" { - err = load(f.Name()) - if err != nil { - return err + english = st.lang.Form["en-us"] + formLoaded := false + for _, filesystem := range filesystems { + files, err := fs.ReadDir(filesystem, st.lang.FormPath) + if err != nil { + continue + } + for _, f := range files { + if f.Name() != "en-us.json" { + err = load(filesystem, f.Name()) + if err == nil { + formLoaded = true + } } } } + if !formLoaded { + return err + } return nil } -func (st *Storage) loadLangEmail() error { +func (st *Storage) loadLangEmail(filesystems ...fs.FS) error { st.lang.Email = map[string]emailLang{} var english emailLang - load := func(fname string) error { + load := func(filesystem fs.FS, fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := emailLang{} - f, err := os.ReadFile(filepath.Join(st.lang.EmailPath, fname)) + f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.EmailPath, fname)) if err != nil { return err } @@ -299,23 +339,36 @@ func (st *Storage) loadLangEmail() error { st.lang.Email[index] = lang return nil } - err := load("en-us.json") - if err != nil { - return err + engFound := false + var err error + for _, filesystem := range filesystems { + err = load(filesystem, "en-us.json") + if err == nil { + engFound = true + } } - english = st.lang.Email["en-us"] - files, err := os.ReadDir(st.lang.EmailPath) - if err != nil { + if !engFound { return err } - for _, f := range files { - if f.Name() != "en-us.json" { - err = load(f.Name()) - if err != nil { - return err + english = st.lang.Email["en-us"] + emailLoaded := false + for _, filesystem := range filesystems { + files, err := fs.ReadDir(filesystem, st.lang.EmailPath) + if err != nil { + continue + } + for _, f := range files { + if f.Name() != "en-us.json" { + err = load(filesystem, f.Name()) + if err == nil { + emailLoaded = true + } } } } + if !emailLoaded { + return err + } return nil } From 815bdc35acf0946a61dd73edb8e14628669dd714 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 1 Feb 2021 18:41:45 +0000 Subject: [PATCH 03/14] fully self-contained paths are pretty janky, but it works. Also, [files]/lang_files now must be the path to a directory CONTAINING a "lang/" directory. I'll work around this at a later date. --- Makefile | 6 ++--- config.go | 2 +- config/config-base.json | 2 +- go.mod | 4 +--- go.sum | 30 ------------------------- main.go | 50 ++++++++++++++++++++++------------------- setup.go | 8 +++++-- static.go | 6 +++-- 8 files changed, 43 insertions(+), 65 deletions(-) diff --git a/Makefile b/Makefile index 9365aff..135ca51 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ compile: go1.16rc1 mod download $(info Building) mkdir -p build - CGO_ENABLED=0 go1.16rc1 build -o build/jfa-go *.go + cd build && CGO_ENABLED=0 go1.16rc1 build -o ./jfa-go ../*.go compress: upx --lzma build/jfa-go @@ -79,5 +79,5 @@ copy: install: cp -r build $(DESTDIR)/jfa-go -all: configuration npm email version typescript bundle-css swagger compile copy -debug: configuration npm email version ts-debug bundle-css swagger compile copy +all: configuration npm email version typescript bundle-css swagger copy compile +debug: configuration npm email version ts-debug bundle-css swagger copy compile diff --git a/config.go b/config.go index 89e56ab..624cdb1 100644 --- a/config.go +++ b/config.go @@ -15,7 +15,7 @@ var emailEnabled = false func (app *appContext) GetPath(sect, key string) (fs.FS, string) { val := app.config.Section(sect).Key(key).MustString("") if strings.HasPrefix(val, "jfa-go:") { - return app.localFS, strings.TrimPrefix(val, "jfa-go:") + return localFS, "build/data/" + strings.TrimPrefix(val, "jfa-go:") } return app.systemFS, val } diff --git a/config/config-base.json b/config/config-base.json index 6731a6e..0547651 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -867,7 +867,7 @@ "requires_restart": true, "type": "text", "value": "", - "description": "Useful if you want to customize the text in jfa-go. Should follow the same structure as the 'lang' directory, which you can see on GitHub." + "description": "The path to a directory CONTAINING a 'lang/' directory, which follow the same form as the internal one. See GitHub for more info." } } } diff --git a/go.mod b/go.mod index 0153944..393aaf3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/hrfee/jfa-go -go 1.14 +go 1.16 replace github.com/hrfee/jfa-go/docs => ./docs @@ -51,8 +51,6 @@ require ( golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect golang.org/x/text v0.3.5 // indirect - golang.org/x/tools v0.1.1-0.20210129181147-0cef57b5b584 // indirect - golang.org/x/tools/gopls v0.0.0-20210201165201-19db92ec3be1 // indirect google.golang.org/protobuf v1.25.0 // indirect gopkg.in/ini.v1 v1.62.0 ) diff --git a/go.sum b/go.sum index 5d58702..0dba103 100644 --- a/go.sum +++ b/go.sum @@ -453,40 +453,10 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59 h1:QjA/9ArTfVTLfEhClDCG7SGrZkZixxWpwNCDiwJfh88= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200923182640-463111b69878 h1:VUw1+Jf6KJPf82mbTQMia6HCnNMv2BbAipkEZ4KTcqQ= -golang.org/x/tools v0.0.0-20200923182640-463111b69878/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20200924182824-0f1c53950d78 h1:3JUoxVhcskhsIDEc7vg0MUUEpmPPN5TfG+E97z/Fn90= -golang.org/x/tools v0.0.0-20200924182824-0f1c53950d78/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20200929191002-f1e51e6b9437 h1:XSFqH8m531iIGazX5lrUC9j3slbwsZ1GFByqdUrLqmI= -golang.org/x/tools v0.0.0-20200929191002-f1e51e6b9437/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201008184944-d01b322e6f06 h1:w9ail9jFLaySAm61Zjhciu0LQ5i8YTy2pimlNLx4uuk= -golang.org/x/tools v0.0.0-20201008184944-d01b322e6f06/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201017001424-6003fad69a88 h1:ZB1XYzdDo7c/O48jzjMkvIjnC120Z9/CwgDWhePjQdQ= -golang.org/x/tools v0.0.0-20201017001424-6003fad69a88/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9 h1:sEvmEcJVKBNUvgCUClbUQeHOAa9U0I2Ce1BooMvVCY4= -golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201103235415-b653051172e4 h1:Qe0EMgvVYb6tmJhJHljCj3gS96hvSTkGNaIzp/ivq10= -golang.org/x/tools v0.0.0-20201103235415-b653051172e4/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201113202037-1643af1435f3 h1:7R7+wzd5VuLvCNyHZ/MG511kkoP/DBEzkbh8qUsFbY8= -golang.org/x/tools v0.0.0-20201113202037-1643af1435f3/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b h1:Ych5r0Z6MLML1fgf5hTg9p5bV56Xqx9xv9hLgMBATWs= -golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e h1:t96dS3DO8DGjawSLJL/HIdz8CycAd2v07XxqB3UPTi0= golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee h1:5xKxdl/RhlelmSPaxyVeq5PYSmJ4H14yeQT58qP1F6o= -golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1-0.20210129181147-0cef57b5b584 h1:JAI5SUo/oOtQXK4jvtjJMlwF5opt8qBUpxGa86SJ6zU= -golang.org/x/tools v0.1.1-0.20210129181147-0cef57b5b584/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools/gopls v0.0.0-20210201165201-19db92ec3be1 h1:YRvjnCA/wBOOkQAEKC5ts16/1IJ+NEO9eevhfDF5ues= -golang.org/x/tools/gopls v0.0.0-20210201165201-19db92ec3be1/go.mod h1:DWl5nefYvX46i2mLQVu6Ud0ycJQ3HNPbQKzUHT3VUek= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index 7e1c5ed..0cd9a8a 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "context" "crypto/rand" + "embed" "encoding/base64" "encoding/json" "flag" @@ -51,6 +52,12 @@ type User struct { Password string `json:"password"` } +//go:embed build/data build/data/html build/data/web build/data/web/css build/data/web/js build/data/web/js/ts +var localFS embed.FS + +//go:embed lang/common lang/admin lang/email lang/form lang/setup +var langFS embed.FS + // contains everything the application needs, essentially. Wouldn't do this in the future. type appContext struct { // defaults *Config @@ -60,8 +67,6 @@ type appContext struct { configBase settings dataPath string systemFS fs.FS - localFS fs.FS - langFS fs.FS webFS httpFS cssClass string jellyfinLogin bool @@ -86,8 +91,8 @@ type appContext struct { func (app *appContext) loadHTML(router *gin.Engine) { customPath := app.config.Section("files").Key("html_templates").MustString("") - templatePath := "html" - htmlFiles, err := fs.ReadDir(app.localFS, templatePath) + templatePath := "build/data/html" + htmlFiles, err := fs.ReadDir(localFS, templatePath) if err != nil { app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath) return @@ -102,7 +107,7 @@ func (app *appContext) loadHTML(router *gin.Engine) { loadFiles[i] = filepath.Join(filepath.Join(customPath, f.Name())) } } - tmpl, err := template.ParseFS(app.localFS, loadFiles...) + tmpl, err := template.ParseFS(localFS, loadFiles...) if err != nil { app.err.Fatalf("Failed to load templates: %v", err) } @@ -201,16 +206,15 @@ func start(asDaemon, firstCall bool) { userConfigDir, _ := os.UserConfigDir() app.dataPath = filepath.Join(userConfigDir, "jfa-go") app.configPath = filepath.Join(app.dataPath, "config.ini") - executable, _ := os.Executable() - app.localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data")) - app.langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data", "lang")) + // executable, _ := os.Executable() + // localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data")) + // langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data", "lang")) app.systemFS = os.DirFS("/") - wfs := os.DirFS(filepath.Join(filepath.Dir(executable), "data", "web")) + // wfs := os.DirFS(filepath.Join(filepath.Dir(executable), "data", "web")) app.webFS = httpFS{ - hfs: http.FS(wfs), - fs: wfs, + hfs: http.FS(localFS), + fs: localFS, } - app.webFS.fs = wfs app.info = log.New(os.Stdout, "[INFO] ", log.Ltime) app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile) @@ -267,7 +271,7 @@ func start(asDaemon, firstCall bool) { } if _, err := os.Stat(app.configPath); os.IsNotExist(err) { firstRun = true - dConfig, err := fs.ReadFile(app.localFS, "config-default.ini") + dConfig, err := fs.ReadFile(localFS, "build/data/config-default.ini") if err != nil { app.err.Fatalf("Couldn't find default config file") } @@ -342,16 +346,16 @@ func start(asDaemon, firstCall bool) { }() } - app.storage.lang.CommonPath = "common" - app.storage.lang.FormPath = "form" - app.storage.lang.AdminPath = "admin" - app.storage.lang.EmailPath = "email" + app.storage.lang.CommonPath = "lang/common" + app.storage.lang.FormPath = "lang/form" + app.storage.lang.AdminPath = "lang/admin" + app.storage.lang.EmailPath = "lang/email" externalLang := app.config.Section("files").Key("lang_files").MustString("") var err error if externalLang == "" { - err = app.storage.loadLang(app.langFS) + err = app.storage.loadLang(langFS) } else { - err = app.storage.loadLang(app.langFS, os.DirFS(externalLang)) + err = app.storage.loadLang(langFS, os.DirFS(externalLang)) } if err != nil { app.info.Fatalf("Failed to load language files: %+v\n", err) @@ -431,8 +435,8 @@ func start(asDaemon, firstCall bool) { } - app.configBasePath = "config-base.json" - configBase, _ := fs.ReadFile(app.localFS, app.configBasePath) + app.configBasePath = "build/data/config-base.json" + configBase, _ := fs.ReadFile(localFS, app.configBasePath) json.Unmarshal(configBase, &app.configBase) themes := map[string]string{ @@ -580,8 +584,8 @@ func start(asDaemon, firstCall bool) { } else { debugMode = false address = "0.0.0.0:8056" - app.storage.lang.SetupPath = filepath.Join("lang", "setup") - err := app.storage.loadLangSetup() + app.storage.lang.SetupPath = "lang/setup" + err := app.storage.loadLangSetup(langFS) if err != nil { app.info.Fatalf("Failed to load language files: %+v\n", err) } diff --git a/setup.go b/setup.go index 307ca1f..a1771f8 100644 --- a/setup.go +++ b/setup.go @@ -120,6 +120,7 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error { return err } english = st.lang.Setup["en-us"] + setupLoaded := false for _, filesystem := range filesystems { files, err := fs.ReadDir(filesystem, st.lang.SetupPath) if err != nil { @@ -128,11 +129,14 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error { for _, f := range files { if f.Name() != "en-us.json" { err = load(filesystem, f.Name()) - if err != nil { - return err + if err == nil { + setupLoaded = true } } } } + if !setupLoaded { + return err + } return nil } diff --git a/static.go b/static.go index 346eef5..ea620f5 100644 --- a/static.go +++ b/static.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io/fs" "net/http" "strings" @@ -14,12 +15,13 @@ type httpFS struct { } func (f httpFS) Open(name string) (http.File, error) { - return f.hfs.Open(name) + fmt.Println("build/data/web" + name) + return f.hfs.Open("build/data/web" + name) } func (f httpFS) Exists(prefix string, filepath string) bool { if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) { - stats, err := fs.Stat(f.fs, p) + stats, err := fs.Stat(f.fs, "build/data/web/"+p) if err != nil { return false } From 72cf3e2240d3c980313f0830febcf3630a6b937b Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 1 Feb 2021 20:25:20 +0000 Subject: [PATCH 04/14] add external/internal data options "make all" will build with internal data, whereas "make debug"/"make all-external" will make an external "data/" directory. --- .drone.yml | 4 ++-- .gitignore | 1 + Makefile | 55 +++++++++++++++++++++++++++++++----------------------- config.go | 2 +- embed.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ main.go | 18 ++++++------------ static.go | 6 ++---- version.py | 2 +- 8 files changed, 91 insertions(+), 43 deletions(-) create mode 100755 embed.py diff --git a/.drone.yml b/.drone.yml index 09090f9..959c69e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -141,13 +141,13 @@ depends_on: - arm64-docker - armhf-docker --- -name: jfa-go-git +name: jfa-go-1.16-git kind: pipeline type: docker steps: - name: build - image: golang:latest + image: golang:1.16rc commands: - apt update -y - apt install build-essential python3-pip curl software-properties-common sed upx -y diff --git a/.gitignore b/.gitignore index 71a8ff1..062b8b5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ dist/ build/ data/ version.go +embed.go notes docs/* lang/langtostruct.py diff --git a/Makefile b/Makefile index 135ca51..926dba7 100644 --- a/Makefile +++ b/Makefile @@ -16,31 +16,31 @@ npm: configuration: $(info Fixing config-base) - -mkdir -p build/data - python3 config/fixconfig.py -i config/config-base.json -o build/data/config-base.json + -mkdir -p data + python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json $(info Generating config-default.ini) - python3 config/generate_ini.py -i config/config-base.json -o build/data/config-default.ini + python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini email: $(info Generating email html) - python3 mail/generate.py -o build/data/ + python3 mail/generate.py -o data/ typescript: $(info compiling typescript) - -mkdir -p build/data/web/js - -$(ESBUILD) --bundle ts/admin.ts --outfile=./build/data/web/js/admin.js --minify - -$(ESBUILD) --bundle ts/form.ts --outfile=./build/data/web/js/form.js --minify - -$(ESBUILD) --bundle ts/setup.ts --outfile=./build/data/web/js/setup.js --minify + -mkdir -p data/web/js + -$(ESBUILD) --bundle ts/admin.ts --outfile=./data/web/js/admin.js --minify + -$(ESBUILD) --bundle ts/form.ts --outfile=./data/web/js/form.js --minify + -$(ESBUILD) --bundle ts/setup.ts --outfile=./data/web/js/setup.js --minify ts-debug: $(info compiling typescript w/ sourcemaps) - -mkdir -p build/data/web/js - -$(ESBUILD) --bundle ts/admin.ts --sourcemap --outfile=./build/data/web/js/admin.js - -$(ESBUILD) --bundle ts/form.ts --sourcemap --outfile=./build/data/web/js/form.js - -$(ESBUILD) --bundle ts/setup.ts --sourcemap --outfile=./build/data/web/js/setup.js - -rm -r build/data/web/js/ts + -mkdir -p data/web/js + -$(ESBUILD) --bundle ts/admin.ts --sourcemap --outfile=./data/web/js/admin.js + -$(ESBUILD) --bundle ts/form.ts --sourcemap --outfile=./data/web/js/form.js + -$(ESBUILD) --bundle ts/setup.ts --sourcemap --outfile=./data/web/js/setup.js + -rm -r data/web/js/ts $(info copying typescript) - cp -r ts build/data/web/js + cp -r ts data/web/js swagger: go1.16rc1 get github.com/swaggo/swag/cmd/swag @@ -60,24 +60,33 @@ compress: upx --lzma build/jfa-go bundle-css: - -mkdir -p build/data/web/css + -mkdir -p data/web/css $(info bundling css) - $(ESBUILD) --bundle css/base.css --outfile=build/data/web/css/bundle.css --external:remixicon.css --minify + $(ESBUILD) --bundle css/base.css --outfile=data/web/css/bundle.css --external:remixicon.css --minify copy: $(info copying fonts) - cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 build/data/web/css/ + cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 data/web/css/ $(info copying html) - cp -r html build/data/ + cp -r html data/ $(info copying static data) - -mkdir -p build/data/web - cp -r static/* build/data/web/ + -mkdir -p data/web + cp -r static/* data/web/ $(info copying language files) - cp -r lang build/data/ + cp -r lang data/ +embed: + python embed.py internal + +noembed: + python embed.py external + -mkdir -p build + $(info copying internal data into build/) + cp -r data build/ install: cp -r build $(DESTDIR)/jfa-go -all: configuration npm email version typescript bundle-css swagger copy compile -debug: configuration npm email version ts-debug bundle-css swagger copy compile +all: configuration npm email version typescript bundle-css swagger copy embed compile +all-external: configuration npm email version ts-debug bundle-css swagger copy noembed compile +debug: configuration npm email version ts-debug bundle-css swagger copy noembed compile diff --git a/config.go b/config.go index 624cdb1..4c6a48e 100644 --- a/config.go +++ b/config.go @@ -15,7 +15,7 @@ var emailEnabled = false func (app *appContext) GetPath(sect, key string) (fs.FS, string) { val := app.config.Section(sect).Key(key).MustString("") if strings.HasPrefix(val, "jfa-go:") { - return localFS, "build/data/" + strings.TrimPrefix(val, "jfa-go:") + return localFS, "data/" + strings.TrimPrefix(val, "jfa-go:") } return app.systemFS, val } diff --git a/embed.py b/embed.py new file mode 100755 index 0000000..21fd16f --- /dev/null +++ b/embed.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +import sys +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("embed", metavar="||", type=str) +trues = ["true", "internal", "yes", "y"] +falses = ["false", "external", "no", "n"] + +EMBED = parser.parse_args().embed + +with open("embed.go", "w") as f: + if EMBED in trues: + f.write("""package main +import ( + "embed" + "log" +) + +//go:embed data data/html data/web data/web/css data/web/js +var localFS embed.FS + +//go:embed lang/common lang/admin lang/email lang/form lang/setup +var langFS embed.FS + +func loadLocalFS() { + log.Println("Using internal storage") +}""") + elif EMBED in falses: + f.write("""package main +import ( + "io/fs" + "os" + "log" + "path/filepath" +) + +var localFS fs.FS +var langFS fs.FS + +func loadLocalFS() { + log.Println("Using external storage") + executable, _ := os.Executable() + localFS = os.DirFS(filepath.Dir(executable)) + langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data")) +}""") diff --git a/main.go b/main.go index 0cd9a8a..ea6362e 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "context" "crypto/rand" - "embed" "encoding/base64" "encoding/json" "flag" @@ -52,13 +51,7 @@ type User struct { Password string `json:"password"` } -//go:embed build/data build/data/html build/data/web build/data/web/css build/data/web/js build/data/web/js/ts -var localFS embed.FS - -//go:embed lang/common lang/admin lang/email lang/form lang/setup -var langFS embed.FS - -// contains everything the application needs, essentially. Wouldn't do this in the future. +// contains (almost) everything the application needs, essentially. This was a dumb design decision imo. type appContext struct { // defaults *Config config *ini.File @@ -91,7 +84,7 @@ type appContext struct { func (app *appContext) loadHTML(router *gin.Engine) { customPath := app.config.Section("files").Key("html_templates").MustString("") - templatePath := "build/data/html" + templatePath := "data/html" htmlFiles, err := fs.ReadDir(localFS, templatePath) if err != nil { app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath) @@ -201,7 +194,7 @@ func start(asDaemon, firstCall bool) { /* set default config, data and local paths also, confusing naming here. data_path is not the internal 'data' directory, rather the users .config/jfa-go folder. - local_path is the internal 'data' directory. + localFS/data is the internal 'data' directory. */ userConfigDir, _ := os.UserConfigDir() app.dataPath = filepath.Join(userConfigDir, "jfa-go") @@ -271,7 +264,7 @@ func start(asDaemon, firstCall bool) { } if _, err := os.Stat(app.configPath); os.IsNotExist(err) { firstRun = true - dConfig, err := fs.ReadFile(localFS, "build/data/config-default.ini") + dConfig, err := fs.ReadFile(localFS, "data/config-default.ini") if err != nil { app.err.Fatalf("Couldn't find default config file") } @@ -435,7 +428,7 @@ func start(asDaemon, firstCall bool) { } - app.configBasePath = "build/data/config-base.json" + app.configBasePath = "data/config-base.json" configBase, _ := fs.ReadFile(localFS, app.configBasePath) json.Unmarshal(configBase, &app.configBase) @@ -782,6 +775,7 @@ func main() { if flagPassed("test") { TEST = true } + loadLocalFS() if flagPassed("start") { args := []string{} for i, f := range os.Args { diff --git a/static.go b/static.go index ea620f5..41c1c00 100644 --- a/static.go +++ b/static.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "io/fs" "net/http" "strings" @@ -15,13 +14,12 @@ type httpFS struct { } func (f httpFS) Open(name string) (http.File, error) { - fmt.Println("build/data/web" + name) - return f.hfs.Open("build/data/web" + name) + return f.hfs.Open("data/web" + name) } func (f httpFS) Exists(prefix string, filepath string) bool { if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) { - stats, err := fs.Stat(f.fs, "build/data/web/"+p) + stats, err := fs.Stat(f.fs, "data/web/"+p) if err != nil { return false } diff --git a/version.py b/version.py index 387e23e..3e9b9ed 100644 --- a/version.py +++ b/version.py @@ -1,6 +1,6 @@ import subprocess import sys -import os + try: version = sys.argv[1].replace('v', '') except IndexError: From cb12c6f4419331eeaa5cdf56fb479ab09d13a366 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 1 Feb 2021 20:28:01 +0000 Subject: [PATCH 05/14] update goreleaser --- .goreleaser.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index 723dc70..ccc5746 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -26,6 +26,7 @@ before: - npx esbuild --bundle ts/setup.ts --outfile=./data/web/js/setup.js --minify - go get -u github.com/swaggo/swag/cmd/swag - swag init -g main.go + - python3 embed.py internal builds: - dir: ./ env: From ee37588959da0b2ae1c407f727aa80f78f388cf1 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 1 Feb 2021 20:30:38 +0000 Subject: [PATCH 06/14] drone image --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 959c69e..060a407 100644 --- a/.drone.yml +++ b/.drone.yml @@ -147,7 +147,7 @@ type: docker steps: - name: build - image: golang:1.16rc + image: golang:1.16rc1-buster commands: - apt update -y - apt install build-essential python3-pip curl software-properties-common sed upx -y From 98a9e20cc05007b98e566a0ed701fced9c67d252 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 1 Feb 2021 20:55:17 +0000 Subject: [PATCH 07/14] Fix docker build, add GOBINARY flag for make GOBINARY defaults to "go", but if you want to build on a normal system, you'll likely set it to go1.16rc1 with "make all GOBINARY=go1.16rc1". --- .goreleaser.yml | 4 ---- Dockerfile | 6 +++--- Makefile | 7 ++++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index ccc5746..d0acc5e 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -45,10 +45,6 @@ archives: linux: Linux windows: Windows amd64: x86_64 - files: - - data/* - - data/**/* - - data/**/**/* checksum: name_template: 'checksums.txt' snapshot: diff --git a/Dockerfile b/Dockerfile index 284851d..988343d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:latest AS build +FROM golang:1.16rc1-buster AS build COPY . /opt/build @@ -6,10 +6,10 @@ RUN apt update -y \ && apt install build-essential python3-pip curl software-properties-common sed upx -y \ && (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \ && apt install nodejs \ - && (cd /opt/build; make all GOESBUILD=on; make compress) \ + && (cd /opt/build; make all-external GOESBUILD=on; make compress) \ && sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' /opt/build/build/data/html/setup.html -FROM golang:latest +FROM golang:1.16rc1-buster COPY --from=build /opt/build/build /opt/jfa-go diff --git a/Makefile b/Makefile index 926dba7..8613bce 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ ifeq ($(GOESBUILD), on) else ESBUILD := npx esbuild endif +GOBINARY ?= go npm: $(info installing npm dependencies) @@ -43,7 +44,7 @@ ts-debug: cp -r ts data/web/js swagger: - go1.16rc1 get github.com/swaggo/swag/cmd/swag + $(GOBINARY) get github.com/swaggo/swag/cmd/swag swag init -g main.go version: @@ -51,10 +52,10 @@ version: compile: $(info Downloading deps) - go1.16rc1 mod download + $(GOBINARY) mod download $(info Building) mkdir -p build - cd build && CGO_ENABLED=0 go1.16rc1 build -o ./jfa-go ../*.go + cd build && CGO_ENABLED=0 $(GOBINARY) build -o ./jfa-go ../*.go compress: upx --lzma build/jfa-go From e6775cd2d1bbab96479143320f67ac102d5c1627 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 2 Feb 2021 15:19:43 +0000 Subject: [PATCH 08/14] use embed.fs wrapper for langFS so lang/ is not needed in paths [files]lang_files is now the path to the lang directory, not path to a directory containing it. --- config/config-base.json | 2 +- embed.py | 22 +++++++++++++++++----- main.go | 10 +++++----- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/config/config-base.json b/config/config-base.json index 0547651..b75f871 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -867,7 +867,7 @@ "requires_restart": true, "type": "text", "value": "", - "description": "The path to a directory CONTAINING a 'lang/' directory, which follow the same form as the internal one. See GitHub for more info." + "description": "The path to a directory which following the same form as the internal 'lang/' directory. See GitHub for more info." } } } diff --git a/embed.py b/embed.py index 21fd16f..b066c77 100755 --- a/embed.py +++ b/embed.py @@ -13,18 +13,30 @@ with open("embed.go", "w") as f: if EMBED in trues: f.write("""package main import ( - "embed" - "log" + "embed" + "io/fs" + "log" ) //go:embed data data/html data/web data/web/css data/web/js var localFS embed.FS //go:embed lang/common lang/admin lang/email lang/form lang/setup -var langFS embed.FS +var lFS embed.FS + +var langFS LangFS + +type LangFS struct { + fs embed.FS +} + +func (l LangFS) Open(name string) (fs.File, error) { return l.fs.Open("lang/" + name) } +func (l LangFS) ReadDir(name string) ([]fs.DirEntry, error) { return l.fs.ReadDir("lang/" + name) } +func (l LangFS) ReadFile(name string) ([]byte, error) { return l.fs.ReadFile("lang/" + name) } func loadLocalFS() { - log.Println("Using internal storage") + langFS = LangFS{lFS} + log.Println("Using internal storage") }""") elif EMBED in falses: f.write("""package main @@ -42,5 +54,5 @@ func loadLocalFS() { log.Println("Using external storage") executable, _ := os.Executable() localFS = os.DirFS(filepath.Dir(executable)) - langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data")) + langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data", "lang")) }""") diff --git a/main.go b/main.go index ea6362e..ed3ce47 100644 --- a/main.go +++ b/main.go @@ -339,10 +339,10 @@ func start(asDaemon, firstCall bool) { }() } - app.storage.lang.CommonPath = "lang/common" - app.storage.lang.FormPath = "lang/form" - app.storage.lang.AdminPath = "lang/admin" - app.storage.lang.EmailPath = "lang/email" + app.storage.lang.CommonPath = "common" + app.storage.lang.FormPath = "form" + app.storage.lang.AdminPath = "admin" + app.storage.lang.EmailPath = "email" externalLang := app.config.Section("files").Key("lang_files").MustString("") var err error if externalLang == "" { @@ -577,7 +577,7 @@ func start(asDaemon, firstCall bool) { } else { debugMode = false address = "0.0.0.0:8056" - app.storage.lang.SetupPath = "lang/setup" + app.storage.lang.SetupPath = "setup" err := app.storage.loadLangSetup(langFS) if err != nil { app.info.Fatalf("Failed to load language files: %+v\n", err) From aaed272bf27c5e2cf5f990f2d562f09cc199c201 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 2 Feb 2021 15:44:30 +0000 Subject: [PATCH 09/14] use embed.fs wrapper on data --- config.go | 2 +- embed.py | 23 +++++++++++++---------- main.go | 6 +++--- static.go | 4 ++-- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/config.go b/config.go index 4c6a48e..4957d99 100644 --- a/config.go +++ b/config.go @@ -15,7 +15,7 @@ var emailEnabled = false func (app *appContext) GetPath(sect, key string) (fs.FS, string) { val := app.config.Section(sect).Key(key).MustString("") if strings.HasPrefix(val, "jfa-go:") { - return localFS, "data/" + strings.TrimPrefix(val, "jfa-go:") + return localFS, strings.TrimPrefix(val, "jfa-go:") } return app.systemFS, val } diff --git a/embed.py b/embed.py index b066c77..80a2a08 100755 --- a/embed.py +++ b/embed.py @@ -19,23 +19,26 @@ import ( ) //go:embed data data/html data/web data/web/css data/web/js -var localFS embed.FS +var loFS embed.FS //go:embed lang/common lang/admin lang/email lang/form lang/setup -var lFS embed.FS +var laFS embed.FS -var langFS LangFS +var langFS rewriteFS +var localFS rewriteFS -type LangFS struct { - fs embed.FS +type rewriteFS struct { + fs embed.FS + prefix string } -func (l LangFS) Open(name string) (fs.File, error) { return l.fs.Open("lang/" + name) } -func (l LangFS) ReadDir(name string) ([]fs.DirEntry, error) { return l.fs.ReadDir("lang/" + name) } -func (l LangFS) ReadFile(name string) ([]byte, error) { return l.fs.ReadFile("lang/" + name) } +func (l rewriteFS) Open(name string) (fs.File, error) { return l.fs.Open(l.prefix + name) } +func (l rewriteFS) ReadDir(name string) ([]fs.DirEntry, error) { return l.fs.ReadDir(l.prefix + name) } +func (l rewriteFS) ReadFile(name string) ([]byte, error) { return l.fs.ReadFile(l.prefix + name) } func loadLocalFS() { - langFS = LangFS{lFS} + langFS = rewriteFS{laFS, "lang/"} + localFS = rewriteFS{loFS, "data/"} log.Println("Using internal storage") }""") elif EMBED in falses: @@ -53,6 +56,6 @@ var langFS fs.FS func loadLocalFS() { log.Println("Using external storage") executable, _ := os.Executable() - localFS = os.DirFS(filepath.Dir(executable)) + localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data")) langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data", "lang")) }""") diff --git a/main.go b/main.go index ed3ce47..a5433dc 100644 --- a/main.go +++ b/main.go @@ -84,7 +84,7 @@ type appContext struct { func (app *appContext) loadHTML(router *gin.Engine) { customPath := app.config.Section("files").Key("html_templates").MustString("") - templatePath := "data/html" + templatePath := "html" htmlFiles, err := fs.ReadDir(localFS, templatePath) if err != nil { app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath) @@ -264,7 +264,7 @@ func start(asDaemon, firstCall bool) { } if _, err := os.Stat(app.configPath); os.IsNotExist(err) { firstRun = true - dConfig, err := fs.ReadFile(localFS, "data/config-default.ini") + dConfig, err := fs.ReadFile(localFS, "config-default.ini") if err != nil { app.err.Fatalf("Couldn't find default config file") } @@ -428,7 +428,7 @@ func start(asDaemon, firstCall bool) { } - app.configBasePath = "data/config-base.json" + app.configBasePath = "config-base.json" configBase, _ := fs.ReadFile(localFS, app.configBasePath) json.Unmarshal(configBase, &app.configBase) diff --git a/static.go b/static.go index 41c1c00..6102d83 100644 --- a/static.go +++ b/static.go @@ -14,12 +14,12 @@ type httpFS struct { } func (f httpFS) Open(name string) (http.File, error) { - return f.hfs.Open("data/web" + name) + return f.hfs.Open("web" + name) } func (f httpFS) Exists(prefix string, filepath string) bool { if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) { - stats, err := fs.Stat(f.fs, "data/web/"+p) + stats, err := fs.Stat(f.fs, "web/"+p) if err != nil { return false } From ea99966057d7bdfd96d5f23129ee1e53be7c5fb5 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 2 Feb 2021 18:09:02 +0000 Subject: [PATCH 10/14] refactor, move route loading to router.go --- README.md | 5 +- api.go | 3 + email.go | 258 +++++++++++++++++----------------------------- embed.py | 4 +- main.go | 186 +++++---------------------------- package-lock.json | 6 +- package.json | 1 + router.go | 156 ++++++++++++++++++++++++++++ views.go | 22 ++-- 9 files changed, 299 insertions(+), 342 deletions(-) create mode 100644 router.go diff --git a/README.md b/README.md index 3658e49..78378b8 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,8 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/) or [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/). -For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.pw/view/hrfee/jfa-go)), and extract `jfa-go` and `data` to the same directory. -* For linux users, you can place them inside `/opt/jfa-go` and then run -`sudo ln -s /opt/jfa-go/jfa-go /usr/bin/jfa-go` to place it in your PATH. +For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.pw/view/hrfee/jfa-go)), and extract the `jfa-go` executable to somewhere useful. +* For \*nix/macOS users, `chmod +x jfa-go` then place it somewhere in your PATH like `/usr/bin`. Run the executable to start. diff --git a/api.go b/api.go index 3885c44..cf051e2 100644 --- a/api.go +++ b/api.go @@ -1330,6 +1330,9 @@ func (app *appContext) GetLanguages(gc *gin.Context) { gc.JSON(200, resp) } +// @Summary Restarts the program. No response means success. +// @Router /restart [post] +// @tags Other func (app *appContext) restart(gc *gin.Context) { app.info.Println("Restarting...") err := app.Restart() diff --git a/email.go b/email.go index 6cfc788..b75197c 100644 --- a/email.go +++ b/email.go @@ -153,38 +153,47 @@ func (emailer *Emailer) NewSMTP(server string, port int, username, password stri } } -func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext) (*Email, error) { - email := &Email{ - subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")), - } - message := app.config.Section("email").Key("message").String() - inviteLink := app.config.Section("invite_emails").Key("url_base").String() - inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key) - +func (emailer *Emailer) construct(app *appContext, section, keyFragment string, data map[string]interface{}) (html, text string, err error) { + var tpl *template.Template for _, key := range []string{"html", "text"} { - filesystem, fpath := app.GetPath("email_confirmation", "email_"+key) - tpl, err := template.ParseFS(filesystem, fpath) + filesystem, fpath := app.GetPath(section, keyFragment+key) + tpl, err = template.ParseFS(filesystem, fpath) if err != nil { - return nil, err + return } var tplData bytes.Buffer - err = tpl.Execute(&tplData, map[string]string{ - "helloUser": emailer.lang.Strings.format("helloUser", username), - "clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"), - "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), - "urlVal": inviteLink, - "confirmEmail": emailer.lang.EmailConfirmation.get("confirmEmail"), - "message": message, - }) + err = tpl.Execute(&tplData, data) if err != nil { - return nil, err + return } if key == "html" { - email.html = tplData.String() + html = tplData.String() } else { - email.text = tplData.String() + text = tplData.String() } } + return +} + +func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext) (*Email, error) { + email := &Email{ + subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")), + } + message := app.config.Section("email").Key("message").String() + inviteLink := app.config.Section("invite_emails").Key("url_base").String() + inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key) + var err error + email.html, email.text, err = emailer.construct(app, "email_confirmation", "email_", map[string]interface{}{ + "helloUser": emailer.lang.Strings.format("helloUser", username), + "clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"), + "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), + "urlVal": inviteLink, + "confirmEmail": emailer.lang.EmailConfirmation.get("confirmEmail"), + "message": message, + }) + if err != nil { + return nil, err + } return email, nil } @@ -197,31 +206,18 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont message := app.config.Section("email").Key("message").String() inviteLink := app.config.Section("invite_emails").Key("url_base").String() inviteLink = fmt.Sprintf("%s/%s", inviteLink, code) - - for _, key := range []string{"html", "text"} { - filesystem, fpath := app.GetPath("invite_emails", "email_"+key) - tpl, err := template.ParseFS(filesystem, fpath) - if err != nil { - return nil, err - } - var tplData bytes.Buffer - err = tpl.Execute(&tplData, map[string]string{ - "hello": emailer.lang.InviteEmail.get("hello"), - "youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"), - "toJoin": emailer.lang.InviteEmail.get("toJoin"), - "inviteExpiry": emailer.lang.InviteEmail.format("inviteExpiry", d, t, expiresIn), - "linkButton": emailer.lang.InviteEmail.get("linkButton"), - "invite_link": inviteLink, - "message": message, - }) - if err != nil { - return nil, err - } - if key == "html" { - email.html = tplData.String() - } else { - email.text = tplData.String() - } + var err error + email.html, email.text, err = emailer.construct(app, "invite_emails", "email_", map[string]interface{}{ + "hello": emailer.lang.InviteEmail.get("hello"), + "youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"), + "toJoin": emailer.lang.InviteEmail.get("toJoin"), + "inviteExpiry": emailer.lang.InviteEmail.format("inviteExpiry", d, t, expiresIn), + "linkButton": emailer.lang.InviteEmail.get("linkButton"), + "invite_link": inviteLink, + "message": message, + }) + if err != nil { + return nil, err } return email, nil } @@ -231,26 +227,14 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont subject: emailer.lang.InviteExpiry.get("title"), } expiry := app.formatDatetime(invite.ValidTill) - for _, key := range []string{"html", "text"} { - filesystem, fpath := app.GetPath("notifications", "expiry_"+key) - tpl, err := template.ParseFS(filesystem, fpath) - if err != nil { - return nil, err - } - var tplData bytes.Buffer - err = tpl.Execute(&tplData, map[string]string{ - "inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"), - "expiredAt": emailer.lang.InviteExpiry.format("expiredAt", "\""+code+"\"", expiry), - "notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"), - }) - if err != nil { - return nil, err - } - if key == "html" { - email.html = tplData.String() - } else { - email.text = tplData.String() - } + var err error + email.html, email.text, err = emailer.construct(app, "notifications", "expiry_", map[string]interface{}{ + "inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"), + "expiredAt": emailer.lang.InviteExpiry.format("expiredAt", "\""+code+"\"", expiry), + "notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"), + }) + if err != nil { + return nil, err } return email, nil } @@ -266,31 +250,19 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite } else { tplAddress = address } - for _, key := range []string{"html", "text"} { - filesystem, fpath := app.GetPath("notifications", "created_"+key) - tpl, err := template.ParseFS(filesystem, fpath) - if err != nil { - return nil, err - } - var tplData bytes.Buffer - err = tpl.Execute(&tplData, map[string]string{ - "aUserWasCreated": emailer.lang.UserCreated.format("aUserWasCreated", "\""+code+"\""), - "name": emailer.lang.Strings.get("name"), - "address": emailer.lang.Strings.get("emailAddress"), - "time": emailer.lang.UserCreated.get("time"), - "nameVal": username, - "addressVal": tplAddress, - "timeVal": created, - "notificationNotice": emailer.lang.UserCreated.get("notificationNotice"), - }) - if err != nil { - return nil, err - } - if key == "html" { - email.html = tplData.String() - } else { - email.text = tplData.String() - } + var err error + email.html, email.text, err = emailer.construct(app, "notifications", "created_", map[string]interface{}{ + "aUserWasCreated": emailer.lang.UserCreated.format("aUserWasCreated", "\""+code+"\""), + "name": emailer.lang.Strings.get("name"), + "address": emailer.lang.Strings.get("emailAddress"), + "time": emailer.lang.UserCreated.get("time"), + "nameVal": username, + "addressVal": tplAddress, + "timeVal": created, + "notificationNotice": emailer.lang.UserCreated.get("notificationNotice"), + }) + if err != nil { + return nil, err } return email, nil } @@ -301,31 +273,19 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Ema } d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern) message := app.config.Section("email").Key("message").String() - for _, key := range []string{"html", "text"} { - filesystem, fpath := app.GetPath("password_resets", "email_"+key) - tpl, err := template.ParseFS(filesystem, fpath) - if err != nil { - return nil, err - } - var tplData bytes.Buffer - err = tpl.Execute(&tplData, map[string]string{ - "helloUser": emailer.lang.Strings.format("helloUser", pwr.Username), - "someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"), - "ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"), - "codeExpiry": emailer.lang.PasswordReset.format("codeExpiry", d, t, expiresIn), - "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), - "pin": emailer.lang.PasswordReset.get("pin"), - "pinVal": pwr.Pin, - "message": message, - }) - if err != nil { - return nil, err - } - if key == "html" { - email.html = tplData.String() - } else { - email.text = tplData.String() - } + var err error + email.html, email.text, err = emailer.construct(app, "password_resets", "email_", map[string]interface{}{ + "helloUser": emailer.lang.Strings.format("helloUser", pwr.Username), + "someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"), + "ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"), + "codeExpiry": emailer.lang.PasswordReset.format("codeExpiry", d, t, expiresIn), + "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), + "pin": emailer.lang.PasswordReset.get("pin"), + "pinVal": pwr.Pin, + "message": message, + }) + if err != nil { + return nil, err } return email, nil } @@ -334,26 +294,14 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email email := &Email{ subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")), } - for _, key := range []string{"html", "text"} { - filesystem, fpath := app.GetPath("deletion", "email_"+key) - tpl, err := template.ParseFS(filesystem, fpath) - if err != nil { - return nil, err - } - var tplData bytes.Buffer - err = tpl.Execute(&tplData, map[string]string{ - "yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"), - "reason": emailer.lang.UserDeleted.get("reason"), - "reasonVal": reason, - }) - if err != nil { - return nil, err - } - if key == "html" { - email.html = tplData.String() - } else { - email.text = tplData.String() - } + var err error + email.html, email.text, err = emailer.construct(app, "deletion", "email_", map[string]interface{}{ + "yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"), + "reason": emailer.lang.UserDeleted.get("reason"), + "reasonVal": reason, + }) + if err != nil { + return nil, err } return email, nil } @@ -362,30 +310,18 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext) (*Ema email := &Email{ subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")), } - for _, key := range []string{"html", "text"} { - filesystem, fpath := app.GetPath("welcome_email", "email_"+key) - tpl, err := template.ParseFS(filesystem, fpath) - if err != nil { - return nil, err - } - var tplData bytes.Buffer - err = tpl.Execute(&tplData, map[string]string{ - "welcome": emailer.lang.WelcomeEmail.get("welcome"), - "youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"), - "jellyfinURL": emailer.lang.WelcomeEmail.get("jellyfinURL"), - "jellyfinURLVal": app.config.Section("jellyfin").Key("public_server").String(), - "username": emailer.lang.Strings.get("username"), - "usernameVal": username, - "message": app.config.Section("email").Key("message").String(), - }) - if err != nil { - return nil, err - } - if key == "html" { - email.html = tplData.String() - } else { - email.text = tplData.String() - } + var err error + email.html, email.text, err = emailer.construct(app, "welcome_email", "email_", map[string]interface{}{ + "welcome": emailer.lang.WelcomeEmail.get("welcome"), + "youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"), + "jellyfinURL": emailer.lang.WelcomeEmail.get("jellyfinURL"), + "jellyfinURLVal": app.config.Section("jellyfin").Key("public_server").String(), + "username": emailer.lang.Strings.get("username"), + "usernameVal": username, + "message": app.config.Section("email").Key("message").String(), + }) + if err != nil { + return nil, err } return email, nil } diff --git a/embed.py b/embed.py index 80a2a08..5d4586c 100755 --- a/embed.py +++ b/embed.py @@ -36,7 +36,7 @@ func (l rewriteFS) Open(name string) (fs.File, error) { return l.fs.Ope func (l rewriteFS) ReadDir(name string) ([]fs.DirEntry, error) { return l.fs.ReadDir(l.prefix + name) } func (l rewriteFS) ReadFile(name string) ([]byte, error) { return l.fs.ReadFile(l.prefix + name) } -func loadLocalFS() { +func loadFilesystems() { langFS = rewriteFS{laFS, "lang/"} localFS = rewriteFS{loFS, "data/"} log.Println("Using internal storage") @@ -53,7 +53,7 @@ import ( var localFS fs.FS var langFS fs.FS -func loadLocalFS() { +func loadFilesystems() { log.Println("Using external storage") executable, _ := os.Executable() localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data")) diff --git a/main.go b/main.go index a5433dc..5c62827 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,6 @@ import ( "encoding/json" "flag" "fmt" - "html/template" "io" "io/fs" "log" @@ -23,20 +22,27 @@ import ( "strings" "time" - "github.com/gin-contrib/pprof" - "github.com/gin-contrib/static" - "github.com/gin-gonic/gin" "github.com/hrfee/jfa-go/common" _ "github.com/hrfee/jfa-go/docs" "github.com/hrfee/jfa-go/mediabrowser" "github.com/hrfee/jfa-go/ombi" "github.com/lithammer/shortuuid/v3" "github.com/logrusorgru/aurora/v3" - swaggerFiles "github.com/swaggo/files" - ginSwagger "github.com/swaggo/gin-swagger" "gopkg.in/ini.v1" ) +var ( + PLATFORM string = runtime.GOOS + SOCK string = "jfa-go.sock" + SRV *http.Server + RESTART chan bool + DATA, CONFIG, HOST *string + PORT *int + DEBUG *bool + TEST bool + SWAGGER *bool +) + var serverTypes = map[string]string{ "jellyfin": "Jellyfin", "emby": "Emby (experimental)", @@ -82,31 +88,6 @@ type appContext struct { URLBase string } -func (app *appContext) loadHTML(router *gin.Engine) { - customPath := app.config.Section("files").Key("html_templates").MustString("") - templatePath := "html" - htmlFiles, err := fs.ReadDir(localFS, templatePath) - if err != nil { - app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath) - return - } - loadFiles := make([]string, len(htmlFiles)) - for i, f := range htmlFiles { - if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) { - app.debug.Printf("Using default \"%s\"", f.Name()) - loadFiles[i] = filepath.Join(templatePath, f.Name()) - } else { - app.info.Printf("Using custom \"%s\"", f.Name()) - loadFiles[i] = filepath.Join(filepath.Join(customPath, f.Name())) - } - } - tmpl, err := template.ParseFS(localFS, loadFiles...) - if err != nil { - app.err.Fatalf("Failed to load templates: %v", err) - } - router.SetHTMLTemplate(tmpl) -} - func generateSecret(length int) (string, error) { bytes := make([]byte, length) _, err := rand.Read(bytes) @@ -116,46 +97,6 @@ func generateSecret(length int) (string, error) { return base64.URLEncoding.EncodeToString(bytes), err } -func setGinLogger(router *gin.Engine, debugMode bool) { - if debugMode { - router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { - return fmt.Sprintf("[GIN/DEBUG] %s: %s(%s) => %d in %s; %s\n", - param.TimeStamp.Format("15:04:05"), - param.Method, - param.Path, - param.StatusCode, - param.Latency, - func() string { - if param.ErrorMessage != "" { - return "Error: " + param.ErrorMessage - } - return "" - }(), - ) - })) - } else { - router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { - return fmt.Sprintf("[GIN] %s(%s) => %d\n", - param.Method, - param.Path, - param.StatusCode, - ) - })) - } -} - -var ( - PLATFORM string = runtime.GOOS - SOCK string = "jfa-go.sock" - SRV *http.Server - RESTART chan bool - DATA, CONFIG, HOST *string - PORT *int - DEBUG *bool - TEST bool - SWAGGER *bool -) - func test(app *appContext) { fmt.Printf("\n\n----\n\n") settings := map[string]interface{}{ @@ -192,18 +133,16 @@ func start(asDaemon, firstCall bool) { app := new(appContext) /* - set default config, data and local paths - also, confusing naming here. data_path is not the internal 'data' directory, rather the users .config/jfa-go folder. - localFS/data is the internal 'data' directory. + set default config and data paths + data: Contains invites.json, emails.json, user_profile.json, etc. + config: config.ini. Usually in data, but can be changed via -config. + localFS is jfa-go's internal data. On external builds, the directory is named "data" and placed next to the executable. */ userConfigDir, _ := os.UserConfigDir() app.dataPath = filepath.Join(userConfigDir, "jfa-go") app.configPath = filepath.Join(app.dataPath, "config.ini") - // executable, _ := os.Executable() - // localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data")) - // langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data", "lang")) app.systemFS = os.DirFS("/") - // wfs := os.DirFS(filepath.Join(filepath.Dir(executable), "data", "web")) + // gin-static doesn't just take a plain http.FileSystem, so we implement it's ServeFileSystem. See static.go. app.webFS = httpFS{ hfs: http.FS(localFS), fs: localFS, @@ -587,92 +526,15 @@ func start(asDaemon, firstCall bool) { // workaround for potentially broken windows mime types mime.AddExtensionType(".js", "application/javascript") + app.info.Println("Initializing router") + router := app.loadRouter(address, debugMode) app.info.Println("Loading routes") - if debugMode { - gin.SetMode(gin.DebugMode) - } else { - gin.SetMode(gin.ReleaseMode) - } - router := gin.New() - - setGinLogger(router, debugMode) - - router.Use(gin.Recovery()) - // Move to router.go - routePrefixes := []string{app.URLBase} - if app.URLBase != "" { - routePrefixes = append(routePrefixes, "") - } - for _, p := range routePrefixes { - router.Use(static.Serve(p+"/", app.webFS)) - } - // - app.loadHTML(router) - router.Use(static.Serve("/", app.webFS)) - router.NoRoute(app.NoRouteHandler) - if debugMode { - app.debug.Println("Loading pprof") - pprof.Register(router) - } - for _, p := range routePrefixes { - router.GET(p+"/lang/:page", app.GetLanguages) - } if !firstRun { - // Move to router - for _, p := range routePrefixes { - router.GET(p+"/", app.AdminPage) - router.GET(p+"/accounts", app.AdminPage) - router.GET(p+"/settings", app.AdminPage) - router.GET(p+"/lang/:page/:file", app.ServeLang) - router.GET(p+"/token/login", app.getTokenLogin) - router.GET(p+"/token/refresh", app.getTokenRefresh) - router.POST(p+"/newUser", app.NewUser) - router.Use(static.Serve(p+"/invite/", app.webFS)) - router.GET(p+"/invite/:invCode", app.InviteProxy) - } - if *SWAGGER { - app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n")) - for _, p := range routePrefixes { - router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) - } - } - api := router.Group("/", app.webAuth()) - for _, p := range routePrefixes { - router.POST(p+"/logout", app.Logout) - api.DELETE(p+"/users", app.DeleteUser) - api.GET(p+"/users", app.GetUsers) - api.POST(p+"/users", app.NewUserAdmin) - api.POST(p+"/invites", app.GenerateInvite) - api.GET(p+"/invites", app.GetInvites) - api.DELETE(p+"/invites", app.DeleteInvite) - api.POST(p+"/invites/profile", app.SetProfile) - api.GET(p+"/profiles", app.GetProfiles) - api.POST(p+"/profiles/default", app.SetDefaultProfile) - api.POST(p+"/profiles", app.CreateProfile) - api.DELETE(p+"/profiles", app.DeleteProfile) - api.POST(p+"/invites/notify", app.SetNotify) - api.POST(p+"/users/emails", app.ModifyEmails) - // api.POST(p + "/setDefaults", app.SetDefaults) - api.POST(p+"/users/settings", app.ApplySettings) - api.GET(p+"/config", app.GetConfig) - api.POST(p+"/config", app.ModifyConfig) - api.POST(p+"/restart", app.restart) - if app.config.Section("ombi").Key("enabled").MustBool(false) { - api.GET(p+"/ombi/users", app.OmbiUsers) - api.POST(p+"/ombi/defaults", app.SetOmbiDefaults) - } - } - app.info.Printf("Starting router @ %s", address) + app.loadRoutes(router) } else { - router.GET("/", app.ServeSetup) - router.POST("/jellyfin/test", app.TestJF) - router.POST("/config", app.ModifyConfig) + app.loadSetup(router) app.info.Printf("Loading setup @ %s", address) } - SRV = &http.Server{ - Addr: address, - Handler: router, - } go func() { if app.config.Section("advanced").Key("tls").MustBool(false) { cert := app.config.Section("advanced").Key("tls_cert").MustString("") @@ -694,9 +556,9 @@ func start(asDaemon, firstCall bool) { } }() for range RESTART { - cntx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - if err := SRV.Shutdown(cntx); err != nil { + if err := SRV.Shutdown(ctx); err != nil { app.err.Fatalf("Server shutdown error: %s", err) } return @@ -775,7 +637,7 @@ func main() { if flagPassed("test") { TEST = true } - loadLocalFS() + loadFilesystems() if flagPassed("start") { args := []string{} for i, f := range os.Args { diff --git a/package-lock.json b/package-lock.json index a16ccc7..3affa71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -228,9 +228,9 @@ "integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY=" }, "esbuild": { - "version": "0.7.22", - "resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.7.22.tgz", - "integrity": "sha1-kUm5A/gSi3xFp1QEbCQZnXa74I4=" + "version": "0.8.44", + "resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.44.tgz", + "integrity": "sha1-KnT0j+IFeQgcnY/pm+b7jShIyIc=" }, "escalade": { "version": "3.1.1", diff --git a/package.json b/package.json index 2ea8a8d..1b2feb6 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "homepage": "https://github.com/hrfee/jfa-go#readme", "dependencies": { "a17t": "^0.4.0", + "esbuild": "^0.8.44", "lodash": "^4.17.19", "mjml": "^4.8.0", "remixicon": "^2.5.0", diff --git a/router.go b/router.go new file mode 100644 index 0000000..b6720c3 --- /dev/null +++ b/router.go @@ -0,0 +1,156 @@ +package main + +import ( + "fmt" + "html/template" + "io/fs" + "net/http" + "os" + "path/filepath" + + "github.com/gin-contrib/pprof" + "github.com/gin-contrib/static" + "github.com/gin-gonic/gin" + "github.com/logrusorgru/aurora/v3" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +// loads HTML templates. If [files]/html_templates is set, alternative files inside the directory are loaded in place of the internal templates. +func (app *appContext) loadHTML(router *gin.Engine) { + customPath := app.config.Section("files").Key("html_templates").MustString("") + templatePath := "html" + htmlFiles, err := fs.ReadDir(localFS, templatePath) + if err != nil { + app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath) + return + } + loadFiles := make([]string, len(htmlFiles)) + for i, f := range htmlFiles { + if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) { + app.debug.Printf("Using default \"%s\"", f.Name()) + loadFiles[i] = filepath.Join(templatePath, f.Name()) + } else { + app.info.Printf("Using custom \"%s\"", f.Name()) + loadFiles[i] = filepath.Join(filepath.Join(customPath, f.Name())) + } + } + tmpl, err := template.ParseFS(localFS, loadFiles...) + if err != nil { + app.err.Fatalf("Failed to load templates: %v", err) + } + router.SetHTMLTemplate(tmpl) +} + +// sets gin logger. +func setGinLogger(router *gin.Engine, debugMode bool) { + if debugMode { + router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + return fmt.Sprintf("[GIN/DEBUG] %s: %s(%s) => %d in %s; %s\n", + param.TimeStamp.Format("15:04:05"), + param.Method, + param.Path, + param.StatusCode, + param.Latency, + func() string { + if param.ErrorMessage != "" { + return "Error: " + param.ErrorMessage + } + return "" + }(), + ) + })) + } else { + router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + return fmt.Sprintf("[GIN] %s(%s) => %d\n", + param.Method, + param.Path, + param.StatusCode, + ) + })) + } +} + +func (app *appContext) loadRouter(address string, debug bool) *gin.Engine { + if debug { + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) + } + router := gin.New() + + setGinLogger(router, debug) + + router.Use(gin.Recovery()) + app.loadHTML(router) + router.Use(static.Serve("/", app.webFS)) + router.NoRoute(app.NoRouteHandler) + if debug { + app.debug.Println("Loading pprof") + pprof.Register(router) + } + SRV = &http.Server{ + Addr: address, + Handler: router, + } + return router +} + +func (app *appContext) loadRoutes(router *gin.Engine) { + routePrefixes := []string{app.URLBase} + if app.URLBase != "" { + routePrefixes = append(routePrefixes, "") + } + for _, p := range routePrefixes { + router.GET(p+"/lang/:page", app.GetLanguages) + router.Use(static.Serve(p+"/", app.webFS)) + router.GET(p+"/", app.AdminPage) + router.GET(p+"/accounts", app.AdminPage) + router.GET(p+"/settings", app.AdminPage) + router.GET(p+"/lang/:page/:file", app.ServeLang) + router.GET(p+"/token/login", app.getTokenLogin) + router.GET(p+"/token/refresh", app.getTokenRefresh) + router.POST(p+"/newUser", app.NewUser) + router.Use(static.Serve(p+"/invite/", app.webFS)) + router.GET(p+"/invite/:invCode", app.InviteProxy) + } + if *SWAGGER { + app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n")) + for _, p := range routePrefixes { + router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + } + } + api := router.Group("/", app.webAuth()) + for _, p := range routePrefixes { + router.POST(p+"/logout", app.Logout) + api.DELETE(p+"/users", app.DeleteUser) + api.GET(p+"/users", app.GetUsers) + api.POST(p+"/users", app.NewUserAdmin) + api.POST(p+"/invites", app.GenerateInvite) + api.GET(p+"/invites", app.GetInvites) + api.DELETE(p+"/invites", app.DeleteInvite) + api.POST(p+"/invites/profile", app.SetProfile) + api.GET(p+"/profiles", app.GetProfiles) + api.POST(p+"/profiles/default", app.SetDefaultProfile) + api.POST(p+"/profiles", app.CreateProfile) + api.DELETE(p+"/profiles", app.DeleteProfile) + api.POST(p+"/invites/notify", app.SetNotify) + api.POST(p+"/users/emails", app.ModifyEmails) + // api.POST(p + "/setDefaults", app.SetDefaults) + api.POST(p+"/users/settings", app.ApplySettings) + api.GET(p+"/config", app.GetConfig) + api.POST(p+"/config", app.ModifyConfig) + api.POST(p+"/restart", app.restart) + if app.config.Section("ombi").Key("enabled").MustBool(false) { + api.GET(p+"/ombi/users", app.OmbiUsers) + api.POST(p+"/ombi/defaults", app.SetOmbiDefaults) + } + } +} + +func (app *appContext) loadSetup(router *gin.Engine) { + router.GET("/lang/:page", app.GetLanguages) + router.GET("/", app.ServeSetup) + router.POST("/jellyfin/test", app.TestJF) + router.POST("/config", app.ModifyConfig) +} diff --git a/views.go b/views.go index 717dd2c..65097e4 100644 --- a/views.go +++ b/views.go @@ -52,14 +52,19 @@ func (app *appContext) pushResources(gc *gin.Context, admin bool) { gc.Header("Link", cssHeader) } -func (app *appContext) AdminPage(gc *gin.Context) { - app.pushResources(gc, true) +func (app *appContext) getLang(gc *gin.Context, chosen string) string { lang := gc.Query("lang") if lang == "" { - lang = app.storage.lang.chosenAdminLang + lang = chosen } else if _, ok := app.storage.lang.Admin[lang]; !ok { - lang = app.storage.lang.chosenAdminLang + lang = chosen } + return lang +} + +func (app *appContext) AdminPage(gc *gin.Context) { + app.pushResources(gc, true) + lang := app.getLang(gc, app.storage.lang.chosenAdminLang) emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) @@ -82,12 +87,7 @@ func (app *appContext) AdminPage(gc *gin.Context) { func (app *appContext) InviteProxy(gc *gin.Context) { app.pushResources(gc, false) code := gc.Param("invCode") - lang := gc.Query("lang") - if lang == "" { - lang = app.storage.lang.chosenFormLang - } else if _, ok := app.storage.lang.Form[lang]; !ok { - lang = app.storage.lang.chosenFormLang - } + lang := app.getLang(gc, app.storage.lang.chosenFormLang) /* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */ // if app.checkInvite(code, false, "") { inv, ok := app.storage.invites[code] @@ -98,7 +98,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { }) return } - if key := gc.Query("key"); key != "" { + if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) { validKey := false keyIndex := -1 for i, k := range inv.Keys { From 873afb47cd8ad44147e9c065ebbf8b5bf0e1ef00 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 5 Feb 2021 13:01:45 +0000 Subject: [PATCH 11/14] strip debug symbols in makefile --- Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8613bce..33bb3d2 100644 --- a/Makefile +++ b/Makefile @@ -51,6 +51,13 @@ version: python3 version.py auto version.go compile: + $(info Downloading deps) + $(GOBINARY) mod download + $(info Building) + mkdir -p build + cd build && CGO_ENABLED=0 $(GOBINARY) build -ldflags="-s -w" -o ./jfa-go ../*.go + +compile-debug: $(info Downloading deps) $(GOBINARY) mod download $(info Building) @@ -90,4 +97,4 @@ install: all: configuration npm email version typescript bundle-css swagger copy embed compile all-external: configuration npm email version ts-debug bundle-css swagger copy noembed compile -debug: configuration npm email version ts-debug bundle-css swagger copy noembed compile +debug: configuration npm email version ts-debug bundle-css swagger copy noembed compile-debug From a6a7710a798021e71dab41fc99daa3a4a8766236 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 8 Feb 2021 12:03:22 +0000 Subject: [PATCH 12/14] use filepath.Join wrapper for different embed and os path styles If using internal, "/" is used as a separator always, and with external, filepath.Join is used. --- embed.py | 7 +++++++ go.mod | 6 +++--- go.sum | 8 ++++++++ router.go | 2 +- setup.go | 2 +- storage.go | 8 ++++---- 6 files changed, 24 insertions(+), 9 deletions(-) diff --git a/embed.py b/embed.py index 5d4586c..d420031 100755 --- a/embed.py +++ b/embed.py @@ -35,6 +35,11 @@ type rewriteFS struct { func (l rewriteFS) Open(name string) (fs.File, error) { return l.fs.Open(l.prefix + name) } func (l rewriteFS) ReadDir(name string) ([]fs.DirEntry, error) { return l.fs.ReadDir(l.prefix + name) } func (l rewriteFS) ReadFile(name string) ([]byte, error) { return l.fs.ReadFile(l.prefix + name) } +func FSJoin(elem ...string) string { + out := "" + for _, v := range elem { out += v + "/" } + return out[:len(out)-1] +} func loadFilesystems() { langFS = rewriteFS{laFS, "lang/"} @@ -53,6 +58,8 @@ import ( var localFS fs.FS var langFS fs.FS +func FSJoin(elem ...string) string { return filepath.Join(elem...) } + func loadFilesystems() { log.Println("Using external storage") executable, _ := os.Executable() diff --git a/go.mod b/go.mod index 393aaf3..acf5aa8 100644 --- a/go.mod +++ b/go.mod @@ -20,8 +20,7 @@ require ( github.com/gin-gonic/gin v1.6.3 github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/spec v0.20.1 // indirect - github.com/go-openapi/swag v0.19.13 // indirect + github.com/go-openapi/spec v0.20.3 // indirect github.com/go-playground/validator/v10 v10.4.1 // indirect github.com/gofrs/uuid v3.3.0+incompatible // indirect github.com/golang/protobuf v1.4.3 @@ -36,7 +35,7 @@ require ( github.com/lithammer/shortuuid/v3 v3.0.4 github.com/logrusorgru/aurora/v3 v3.0.0 github.com/mailgun/mailgun-go/v4 v4.3.0 - github.com/mailru/easyjson v0.7.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858 @@ -51,6 +50,7 @@ require ( golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect golang.org/x/text v0.3.5 // indirect + golang.org/x/tools v0.1.0 // indirect google.golang.org/protobuf v1.25.0 // indirect gopkg.in/ini.v1 v1.62.0 ) diff --git a/go.sum b/go.sum index 0dba103..9619579 100644 --- a/go.sum +++ b/go.sum @@ -102,6 +102,8 @@ github.com/go-openapi/spec v0.20.0 h1:HGLc8AJ7ynOxwv0Lq4TsnwLsWMawHAYiJIFzbcML86 github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= github.com/go-openapi/spec v0.20.1 h1:5WNKTzPguDN+79wbJw2UE2q+eX+gUmEFsIKSvnSQJlc= github.com/go-openapi/spec v0.20.1/go.mod h1:93x7oh+d+FQsmsieroS4cmR3u0p/ywH649a3qwC9OsQ= +github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ= +github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= @@ -116,6 +118,8 @@ github.com/go-openapi/swag v0.19.12 h1:Bc0bnY2c3AoF7Gc+IMIAQQsD8fLHjHpc19wXvYuay github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= github.com/go-openapi/swag v0.19.13 h1:233UVgMy1DlmCYYfOiFpta6e2urloh+sEs5id6lyzog= github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= @@ -235,6 +239,8 @@ github.com/mailru/easyjson v0.7.3 h1:M6wcO9gFHCIPynXGu4iA+NMs//FCgFUWR2jxqV3/+Xk github.com/mailru/easyjson v0.7.3/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -457,6 +463,8 @@ golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e h1:t96dS3DO8DGjawSLJL/HIdz8CycAd2v07XxqB3UPTi0= golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/router.go b/router.go index b6720c3..cdbd93a 100644 --- a/router.go +++ b/router.go @@ -29,7 +29,7 @@ func (app *appContext) loadHTML(router *gin.Engine) { for i, f := range htmlFiles { if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) { app.debug.Printf("Using default \"%s\"", f.Name()) - loadFiles[i] = filepath.Join(templatePath, f.Name()) + loadFiles[i] = FSJoin(templatePath, f.Name()) } else { app.info.Printf("Using custom \"%s\"", f.Name()) loadFiles[i] = filepath.Join(filepath.Join(customPath, f.Name())) diff --git a/setup.go b/setup.go index a1771f8..1e29bc6 100644 --- a/setup.go +++ b/setup.go @@ -77,7 +77,7 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error { load := func(filesystem fs.FS, fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := setupLang{} - f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.SetupPath, fname)) + f, err := fs.ReadFile(filesystem, FSJoin(st.lang.SetupPath, fname)) if err != nil { return err } diff --git a/storage.go b/storage.go index b89c8f3..5100a81 100644 --- a/storage.go +++ b/storage.go @@ -127,7 +127,7 @@ func (st *Storage) loadLangCommon(filesystems ...fs.FS) error { load := func(filesystem fs.FS, fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := commonLang{} - f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.CommonPath, fname)) + f, err := fs.ReadFile(filesystem, FSJoin(st.lang.CommonPath, fname)) if err != nil { return err } @@ -183,7 +183,7 @@ func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error { load := func(filesystem fs.FS, fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := adminLang{} - f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.AdminPath, fname)) + f, err := fs.ReadFile(filesystem, FSJoin(st.lang.AdminPath, fname)) if err != nil { return err } @@ -247,7 +247,7 @@ func (st *Storage) loadLangForm(filesystems ...fs.FS) error { load := func(filesystem fs.FS, fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := formLang{} - f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.FormPath, fname)) + f, err := fs.ReadFile(filesystem, FSJoin(st.lang.FormPath, fname)) if err != nil { return err } @@ -316,7 +316,7 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error { load := func(filesystem fs.FS, fname string) error { index := strings.TrimSuffix(fname, filepath.Ext(fname)) lang := emailLang{} - f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.EmailPath, fname)) + f, err := fs.ReadFile(filesystem, FSJoin(st.lang.EmailPath, fname)) if err != nil { return err } From 988829a6db58c82c8f54079e2e9bb2ae4cd36e07 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 12 Feb 2021 15:22:21 +0000 Subject: [PATCH 13/14] dont build docker on go1.16 branch --- .drone.yml | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/.drone.yml b/.drone.yml index 060a407..b7a1d45 100644 --- a/.drone.yml +++ b/.drone.yml @@ -22,6 +22,8 @@ steps: trigger: event: - tag + branch: + - main --- name: amd64-docker kind: pipeline @@ -42,6 +44,8 @@ steps: trigger: event: - tag + branch: + - main --- name: arm64-docker kind: pipeline @@ -69,6 +73,8 @@ steps: trigger: event: - tag + branch: + - main volumes: - name: ssh_key host: @@ -100,6 +106,8 @@ steps: trigger: event: - tag + branch: + - main volumes: - name: ssh_key host: @@ -132,6 +140,8 @@ steps: trigger: event: - tag + branch: + - main volumes: - name: ssh_key host: @@ -141,13 +151,13 @@ depends_on: - arm64-docker - armhf-docker --- -name: jfa-go-1.16-git +name: jfa-go-git kind: pipeline type: docker steps: - name: build - image: golang:1.16rc1-buster + image: golang:latest commands: - apt update -y - apt install build-essential python3-pip curl software-properties-common sed upx -y @@ -185,6 +195,13 @@ steps: repo: hrfee/jfa-go tags: manifest-unstable-amd64 +trigger: + branch: + - main + event: + exclude: + - pull_request + --- name: arm64-docker-git kind: pipeline @@ -213,7 +230,6 @@ steps: trigger: branch: - main - - go1.16 event: exclude: - pull_request @@ -250,7 +266,6 @@ steps: trigger: branch: - main - - go1.16 event: exclude: - pull_request @@ -291,7 +306,6 @@ depends_on: trigger: branch: - main - - go1.16 event: exclude: - pull_request From c3fb00a307a9a47d5e4880c362ea1317bdf11ee6 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 12 Feb 2021 15:37:19 +0000 Subject: [PATCH 14/14] wrong go version container --- .drone.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index b7a1d45..70004d9 100644 --- a/.drone.yml +++ b/.drone.yml @@ -9,7 +9,7 @@ steps: commands: - git fetch --tags - name: release - image: golang:latest + image: golang:1.16rc1-buster environment: GITHUB_TOKEN: from_secret: github_token @@ -157,7 +157,7 @@ type: docker steps: - name: build - image: golang:latest + image: golang:1.16rc1-buster commands: - apt update -y - apt install build-essential python3-pip curl software-properties-common sed upx -y @@ -321,7 +321,7 @@ type: docker steps: - name: build - image: golang:latest + image: golang:1.16rc1-buster commands: - apt update -y - apt install build-essential python3-pip curl software-properties-common sed upx -y