diff --git a/.drone.yml b/.drone.yml index 877b954..4325d2e 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 @@ -64,7 +64,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 @@ -130,7 +130,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 @@ -144,4 +144,3 @@ trigger: event: include: - pull_request - 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/.goreleaser.yml b/.goreleaser.yml index 723dc70..d0acc5e 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: @@ -44,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 b302296..6e23448 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN apt update -y \ && apt install build-essential python3-pip curl software-properties-common sed -y \ && (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \ && apt install nodejs \ - && (cd /opt/build; make configuration npm email version typescript bundle-css swagger copy GOESBUILD=on) \ + && (cd /opt/build; make configuration npm email version typescript bundle-css swagger noembed copy GOESBUILD=on) \ && 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 diff --git a/Makefile b/Makefile index 71108ff..33bb3d2 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ ifeq ($(GOESBUILD), on) else ESBUILD := npx esbuild endif +GOBINARY ?= go npm: $(info installing npm dependencies) @@ -16,34 +17,34 @@ 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: - go get github.com/swaggo/swag/cmd/swag + $(GOBINARY) get github.com/swaggo/swag/cmd/swag swag init -g main.go version: @@ -51,33 +52,49 @@ version: compile: $(info Downloading deps) - go mod download + $(GOBINARY) mod download $(info Building) mkdir -p build - CGO_ENABLED=0 go build -o build/jfa-go *.go + cd build && CGO_ENABLED=0 $(GOBINARY) build -ldflags="-s -w" -o ./jfa-go ../*.go + +compile-debug: + $(info Downloading deps) + $(GOBINARY) mod download + $(info Building) + mkdir -p build + cd build && CGO_ENABLED=0 $(GOBINARY) build -o ./jfa-go ../*.go 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 compile copy -debug: configuration npm email version ts-debug bundle-css swagger compile copy +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 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/config.go b/config.go index e9db6ea..4957d99 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 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 e8eb9c7..a64625b 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\"", @@ -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": "The path to a directory which following the same form as the internal 'lang/' directory. See GitHub for more info." } } } diff --git a/email.go b/email.go index 2c1f95b..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"} { - fpath := app.config.Section("email_confirmation").Key("email_" + key).String() - tpl, err := template.ParseFiles(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"} { - fpath := app.config.Section("invite_emails").Key("email_" + key).String() - tpl, err := template.ParseFiles(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"} { - fpath := app.config.Section("notifications").Key("expiry_" + key).String() - tpl, err := template.ParseFiles(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"} { - fpath := app.config.Section("notifications").Key("created_" + key).String() - tpl, err := template.ParseFiles(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"} { - fpath := app.config.Section("password_resets").Key("email_" + key).String() - tpl, err := template.ParseFiles(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"} { - fpath := app.config.Section("deletion").Key("email_" + key).String() - tpl, err := template.ParseFiles(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"} { - fpath := app.config.Section("welcome_email").Key("email_" + key).String() - tpl, err := template.ParseFiles(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 new file mode 100755 index 0000000..d420031 --- /dev/null +++ b/embed.py @@ -0,0 +1,68 @@ +#!/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" + "io/fs" + "log" +) + +//go:embed data data/html data/web data/web/css data/web/js +var loFS embed.FS + +//go:embed lang/common lang/admin lang/email lang/form lang/setup +var laFS embed.FS + +var langFS rewriteFS +var localFS rewriteFS + +type rewriteFS struct { + fs embed.FS + prefix string +} + +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/"} + localFS = rewriteFS{loFS, "data/"} + 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 FSJoin(elem ...string) string { return filepath.Join(elem...) } + +func loadFilesystems() { + log.Println("Using external storage") + executable, _ := os.Executable() + localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data")) + langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data", "lang")) +}""") diff --git a/go.mod b/go.mod index 0d05d99..c0fe2a4 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 @@ -17,8 +17,7 @@ require ( github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e github.com/gin-gonic/gin v1.6.3 github.com/go-chi/chi v4.1.2+incompatible // 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/golang/protobuf v1.4.3 // indirect github.com/google/uuid v1.1.2 // indirect @@ -31,6 +30,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.7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 @@ -38,9 +38,7 @@ require ( github.com/swaggo/swag v1.7.0 // indirect github.com/ugorji/go v1.2.0 // indirect golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect - 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 052e469..d156336 100644 --- a/go.sum +++ b/go.sum @@ -11,7 +11,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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/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= @@ -30,7 +29,6 @@ github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQD 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= @@ -48,42 +46,32 @@ github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmC github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4= github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg= github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= -github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo= github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/spec v0.19.14 h1:r4fbYFo6N4ZelmSX8G6p+cv/hZRXzcuqQIADGT1iNKM= github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA= -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= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.11 h1:RFTu/dlFySpyVvJDfp/7674JY4SDglYWKztbiIGFpmc= github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY= -github.com/go-openapi/swag v0.19.12 h1:Bc0bnY2c3AoF7Gc+IMIAQQsD8fLHjHpc19wXvYuayQI= -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= @@ -92,7 +80,6 @@ github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTM github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 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= @@ -101,7 +88,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -118,7 +104,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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/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= 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= @@ -131,7 +116,6 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -157,35 +141,31 @@ github.com/mailgun/mailgun-go/v4 v4.3.0/go.mod h1:fWuBI2iaS/pSSyo6+EBpHjatQO3lV8 github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -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-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/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/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/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/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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -200,40 +180,32 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd 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= -github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0= github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI= github.com/swaggo/gin-swagger v1.3.0 h1:eOmp7r57oUgZPw2dJOjcGNMse9cvXcI4tTqBcnZtPsI= github.com/swaggo/gin-swagger v1.3.0/go.mod h1:oy1BRA6WvgtCp848lhxce7BnWH4C8Bxa0m5SkWx+cS0= github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= -github.com/swaggo/swag v1.6.7 h1:e8GC2xDllJZr3omJkm9YfmK0Y56+rMO3cg0JBKNz09s= github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc= github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E= github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.13/go.mod h1:jxau1n+/wyTGLQoCkjok9r5zFa/FxT6eI5HiHKQszjc= github.com/ugorji/go v1.2.0 h1:6eXlzYLLwZwXroJx9NyqbYcbv/d93twiOzQLDewE6qM= github.com/ugorji/go v1.2.0/go.mod h1:1ny++pKMXhLWrwWV5Nf+CbOuZJhMoaFD+0GMFfd8fEc= github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.13/go.mod h1:oNVt3Dq+FO91WNQ/9JnHKQP2QJxTzoN7wCBFCq1OeuU= github.com/ugorji/go/codec v1.2.0 h1:As6RccOIlbm9wHuWYMlB30dErcI+4WiKWsYsmPkyrUw= github.com/ugorji/go/codec v1.2.0/go.mod h1:dXvG35r7zTX6QImXOSFhGMmKtX+wJ7VTWzGvYQGIjBs= -github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 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= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/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= @@ -254,14 +226,9 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= -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/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -279,23 +246,16 @@ golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/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/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= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -307,10 +267,8 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/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-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= @@ -333,7 +291,6 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= @@ -350,9 +307,7 @@ gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 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= diff --git a/main.go b/main.go index 6c745fc..5c62827 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "flag" "fmt" "io" - "io/ioutil" + "io/fs" "log" "mime" "net" @@ -22,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)", @@ -50,7 +57,7 @@ type User struct { Password string `json:"password"` } -// 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 @@ -58,7 +65,8 @@ type appContext struct { configBasePath string configBase settings dataPath string - localPath string + systemFS fs.FS + webFS httpFS cssClass string jellyfinLogin bool users []User @@ -80,27 +88,6 @@ type appContext struct { URLBase string } -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) - 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())) - } - } - router.LoadHTMLFiles(loadFiles...) -} - func generateSecret(length int) (string, error) { bytes := make([]byte, length) _, err := rand.Read(bytes) @@ -110,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{}{ @@ -186,15 +133,20 @@ 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. - local_path 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() - app.localPath = filepath.Join(filepath.Dir(executable), "data") + app.systemFS = os.DirFS("/") + // 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, + } app.info = log.New(os.Stdout, "[INFO] ", log.Ltime) app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile) @@ -251,23 +203,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(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 +236,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,11 +278,17 @@ 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") - 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(langFS) + } else { + err = app.storage.loadLang(langFS, os.DirFS(externalLang)) + } if err != nil { app.info.Fatalf("Failed to load language files: %+v\n", err) } @@ -413,8 +367,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(localFS, app.configBasePath) json.Unmarshal(configBase, &app.configBase) themes := map[string]string{ @@ -562,8 +516,8 @@ func start(asDaemon, firstCall bool) { } else { debugMode = false address = "0.0.0.0:8056" - app.storage.lang.SetupPath = filepath.Join(app.localPath, "lang", "setup") - err := app.storage.loadLangSetup() + app.storage.lang.SetupPath = "setup" + err := app.storage.loadLangSetup(langFS) if err != nil { app.info.Fatalf("Failed to load language files: %+v\n", err) } @@ -572,89 +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()) - 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))) - } - app.loadHTML(router) - 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 { - 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/", static.LocalFile(filepath.Join(app.localPath, "web"), false))) - 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("") @@ -676,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 @@ -757,6 +637,7 @@ func main() { if flagPassed("test") { TEST = true } + loadFilesystems() if flagPassed("start") { args := []string{} for i, f := range os.Args { diff --git a/package-lock.json b/package-lock.json index 5b9e0a0..49977e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -227,11 +227,6 @@ "resolved": "https://registry.npm.taobao.org/entities/download/entities-1.1.2.tgz?cache=0&sync_timestamp=1602897347667&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fentities%2Fdownload%2Fentities-1.1.2.tgz", "integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY=" }, - "esbuild": { - "version": "0.8.46", - "resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.46.tgz", - "integrity": "sha1-j8cjDOMBmxLiVTOZ8MA4dacpwms=" - }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npm.taobao.org/escalade/download/escalade-3.1.1.tgz?cache=0&sync_timestamp=1602567437752&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fescalade%2Fdownload%2Fescalade-3.1.1.tgz", diff --git a/package.json b/package.json index b9425a4..2ea8a8d 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "homepage": "https://github.com/hrfee/jfa-go#readme", "dependencies": { "a17t": "^0.4.0", - "esbuild": "^0.8.46", "lodash": "^4.17.19", "mjml": "^4.8.0", "remixicon": "^2.5.0", 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/router.go b/router.go new file mode 100644 index 0000000..cdbd93a --- /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] = FSJoin(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/setup.go b/setup.go index 083669e..1e29bc6 100644 --- a/setup.go +++ b/setup.go @@ -2,7 +2,7 @@ package main import ( "encoding/json" - "io/ioutil" + "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 := ioutil.ReadFile(filepath.Join(st.lang.SetupPath, fname)) + f, err := fs.ReadFile(filesystem, FSJoin(st.lang.SetupPath, fname)) if err != nil { return err } @@ -107,22 +108,35 @@ 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 := ioutil.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"] + setupLoaded := false + 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 { + setupLoaded = true + } } } } + if !setupLoaded { + return err + } return nil } diff --git a/static.go b/static.go new file mode 100644 index 0000000..6102d83 --- /dev/null +++ b/static.go @@ -0,0 +1,32 @@ +package main + +import ( + "io/fs" + "net/http" + "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 // Created by converting fs.FS using http.FS() + fs fs.FS +} + +func (f httpFS) Open(name string) (http.File, error) { + 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, "web/"+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..5100a81 100644 --- a/storage.go +++ b/storage.go @@ -2,8 +2,9 @@ package main import ( "encoding/json" - "io/ioutil" + "io/fs" "log" + "os" "path/filepath" "strconv" "strings" @@ -42,7 +43,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 { @@ -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 := ioutil.ReadFile(filepath.Join(st.lang.CommonPath, fname)) + f, err := fs.ReadFile(filesystem, FSJoin(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 := ioutil.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 := ioutil.ReadFile(filepath.Join(st.lang.AdminPath, fname)) + f, err := fs.ReadFile(filesystem, FSJoin(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 := ioutil.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 := ioutil.ReadFile(filepath.Join(st.lang.FormPath, fname)) + f, err := fs.ReadFile(filesystem, FSJoin(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 := ioutil.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 := ioutil.ReadFile(filepath.Join(st.lang.EmailPath, fname)) + f, err := fs.ReadFile(filesystem, FSJoin(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 := ioutil.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 } @@ -329,76 +382,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 +478,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 +494,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) } 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: 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 {