init.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "html/template"
  6. "os"
  7. "path"
  8. "path/filepath"
  9. "strings"
  10. "syscall"
  11. "time"
  12. "github.com/jmoiron/sqlx"
  13. "github.com/jmoiron/sqlx/types"
  14. "github.com/knadh/goyesql/v2"
  15. goyesqlx "github.com/knadh/goyesql/v2/sqlx"
  16. "github.com/knadh/koanf"
  17. "github.com/knadh/koanf/maps"
  18. "github.com/knadh/koanf/parsers/toml"
  19. "github.com/knadh/koanf/providers/confmap"
  20. "github.com/knadh/koanf/providers/file"
  21. "github.com/knadh/koanf/providers/posflag"
  22. "github.com/knadh/listmonk/internal/bounce"
  23. "github.com/knadh/listmonk/internal/bounce/mailbox"
  24. "github.com/knadh/listmonk/internal/i18n"
  25. "github.com/knadh/listmonk/internal/manager"
  26. "github.com/knadh/listmonk/internal/media"
  27. "github.com/knadh/listmonk/internal/media/providers/filesystem"
  28. "github.com/knadh/listmonk/internal/media/providers/s3"
  29. "github.com/knadh/listmonk/internal/messenger"
  30. "github.com/knadh/listmonk/internal/messenger/email"
  31. "github.com/knadh/listmonk/internal/messenger/postback"
  32. "github.com/knadh/listmonk/internal/subimporter"
  33. "github.com/knadh/stuffbin"
  34. "github.com/labstack/echo"
  35. flag "github.com/spf13/pflag"
  36. )
  37. const (
  38. queryFilePath = "queries.sql"
  39. // Root URI of the admin frontend.
  40. adminRoot = "/admin"
  41. )
  42. // constants contains static, constant config values required by the app.
  43. type constants struct {
  44. RootURL string `koanf:"root_url"`
  45. LogoURL string `koanf:"logo_url"`
  46. FaviconURL string `koanf:"favicon_url"`
  47. FromEmail string `koanf:"from_email"`
  48. NotifyEmails []string `koanf:"notify_emails"`
  49. EnablePublicSubPage bool `koanf:"enable_public_subscription_page"`
  50. SendOptinConfirmation bool `koanf:"send_optin_confirmation"`
  51. Lang string `koanf:"lang"`
  52. DBBatchSize int `koanf:"batch_size"`
  53. Privacy struct {
  54. IndividualTracking bool `koanf:"individual_tracking"`
  55. AllowBlocklist bool `koanf:"allow_blocklist"`
  56. AllowExport bool `koanf:"allow_export"`
  57. AllowWipe bool `koanf:"allow_wipe"`
  58. Exportable map[string]bool `koanf:"-"`
  59. DomainBlocklist map[string]bool `koanf:"-"`
  60. } `koanf:"privacy"`
  61. AdminUsername []byte `koanf:"admin_username"`
  62. AdminPassword []byte `koanf:"admin_password"`
  63. UnsubURL string
  64. LinkTrackURL string
  65. ViewTrackURL string
  66. OptinURL string
  67. MessageURL string
  68. MediaProvider string
  69. BounceWebhooksEnabled bool
  70. BounceSESEnabled bool
  71. BounceSendgridEnabled bool
  72. }
  73. func initFlags() {
  74. f := flag.NewFlagSet("config", flag.ContinueOnError)
  75. f.Usage = func() {
  76. // Register --help handler.
  77. fmt.Println(f.FlagUsages())
  78. os.Exit(0)
  79. }
  80. // Register the commandline flags.
  81. f.StringSlice("config", []string{"config.toml"},
  82. "path to one or more config files (will be merged in order)")
  83. f.Bool("install", false, "setup database (first time)")
  84. f.Bool("idempotent", false, "make --install run only if the databse isn't already setup")
  85. f.Bool("upgrade", false, "upgrade database to the current version")
  86. f.Bool("version", false, "current version of the build")
  87. f.Bool("new-config", false, "generate sample config file")
  88. f.String("static-dir", "", "(optional) path to directory with static files")
  89. f.String("i18n-dir", "", "(optional) path to directory with i18n language files")
  90. f.Bool("yes", false, "assume 'yes' to prompts during --install/upgrade")
  91. if err := f.Parse(os.Args[1:]); err != nil {
  92. lo.Fatalf("error loading flags: %v", err)
  93. }
  94. if err := ko.Load(posflag.Provider(f, ".", ko), nil); err != nil {
  95. lo.Fatalf("error loading config: %v", err)
  96. }
  97. }
  98. // initConfigFiles loads the given config files into the koanf instance.
  99. func initConfigFiles(files []string, ko *koanf.Koanf) {
  100. for _, f := range files {
  101. lo.Printf("reading config: %s", f)
  102. if err := ko.Load(file.Provider(f), toml.Parser()); err != nil {
  103. if os.IsNotExist(err) {
  104. lo.Fatal("config file not found. If there isn't one yet, run --new-config to generate one.")
  105. }
  106. lo.Fatalf("error loadng config from file: %v.", err)
  107. }
  108. }
  109. }
  110. // initFileSystem initializes the stuffbin FileSystem to provide
  111. // access to bunded static assets to the app.
  112. func initFS(appDir, frontendDir, staticDir, i18nDir string) stuffbin.FileSystem {
  113. var (
  114. // stuffbin real_path:virtual_alias paths to map local assets on disk
  115. // when there an embedded filestystem is not found.
  116. // These paths are joined with appDir.
  117. appFiles = []string{
  118. "./config.toml.sample:config.toml.sample",
  119. "./queries.sql:queries.sql",
  120. "./schema.sql:schema.sql",
  121. }
  122. frontendFiles = []string{
  123. // Admin frontend's static assets accessible at /admin/* during runtime.
  124. // These paths are sourced from frontendDir.
  125. "./:/admin",
  126. }
  127. staticFiles = []string{
  128. // These paths are joined with staticDir.
  129. "./email-templates:static/email-templates",
  130. "./public:/public",
  131. }
  132. i18nFiles = []string{
  133. // These paths are joined with i18nDir.
  134. "./:/i18n",
  135. }
  136. )
  137. // Get the executable's path.
  138. path, err := os.Executable()
  139. if err != nil {
  140. lo.Fatalf("error getting executable path: %v", err)
  141. }
  142. // Load embedded files in the executable.
  143. hasEmbed := true
  144. fs, err := stuffbin.UnStuff(path)
  145. if err != nil {
  146. hasEmbed = false
  147. // Running in local mode. Load local assets into
  148. // the in-memory stuffbin.FileSystem.
  149. lo.Printf("unable to initialize embedded filesystem (%v). Using local filesystem", err)
  150. fs, err = stuffbin.NewLocalFS("/")
  151. if err != nil {
  152. lo.Fatalf("failed to initialize local file for assets: %v", err)
  153. }
  154. }
  155. // If the embed failed, load app and frontend files from the compile-time paths.
  156. files := []string{}
  157. if !hasEmbed {
  158. files = append(files, joinFSPaths(appDir, appFiles)...)
  159. files = append(files, joinFSPaths(frontendDir, frontendFiles)...)
  160. }
  161. // Irrespective of the embeds, if there are user specified static or i18n paths,
  162. // load files from there and override default files (embedded or picked up from CWD).
  163. if !hasEmbed || i18nDir != "" {
  164. if i18nDir == "" {
  165. // Default dir in cwd.
  166. i18nDir = "i18n"
  167. }
  168. lo.Printf("will load i18n files from: %v", i18nDir)
  169. files = append(files, joinFSPaths(i18nDir, i18nFiles)...)
  170. }
  171. if !hasEmbed || staticDir != "" {
  172. if staticDir == "" {
  173. // Default dir in cwd.
  174. staticDir = "static"
  175. }
  176. lo.Printf("will load static files from: %v", staticDir)
  177. files = append(files, joinFSPaths(staticDir, staticFiles)...)
  178. }
  179. // No additional files to load.
  180. if len(files) == 0 {
  181. return fs
  182. }
  183. // Load files from disk and overlay into the FS.
  184. fStatic, err := stuffbin.NewLocalFS("/", files...)
  185. if err != nil {
  186. lo.Fatalf("failed reading static files from disk: '%s': %v", staticDir, err)
  187. }
  188. if err := fs.Merge(fStatic); err != nil {
  189. lo.Fatalf("error merging static files: '%s': %v", staticDir, err)
  190. }
  191. return fs
  192. }
  193. // initDB initializes the main DB connection pool and parse and loads the app's
  194. // SQL queries into a prepared query map.
  195. func initDB() *sqlx.DB {
  196. var dbCfg dbConf
  197. if err := ko.Unmarshal("db", &dbCfg); err != nil {
  198. lo.Fatalf("error loading db config: %v", err)
  199. }
  200. lo.Printf("connecting to db: %s:%d/%s", dbCfg.Host, dbCfg.Port, dbCfg.DBName)
  201. db, err := connectDB(dbCfg)
  202. if err != nil {
  203. lo.Fatalf("error connecting to DB: %v", err)
  204. }
  205. return db
  206. }
  207. // initQueries loads named SQL queries from the queries file and optionally
  208. // prepares them.
  209. func initQueries(sqlFile string, db *sqlx.DB, fs stuffbin.FileSystem, prepareQueries bool) (goyesql.Queries, *Queries) {
  210. // Load SQL queries.
  211. qB, err := fs.Read(sqlFile)
  212. if err != nil {
  213. lo.Fatalf("error reading SQL file %s: %v", sqlFile, err)
  214. }
  215. qMap, err := goyesql.ParseBytes(qB)
  216. if err != nil {
  217. lo.Fatalf("error parsing SQL queries: %v", err)
  218. }
  219. if !prepareQueries {
  220. return qMap, nil
  221. }
  222. // Prepare queries.
  223. var q Queries
  224. if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil {
  225. lo.Fatalf("error preparing SQL queries: %v", err)
  226. }
  227. return qMap, &q
  228. }
  229. // initSettings loads settings from the DB.
  230. func initSettings(q *sqlx.Stmt) {
  231. var s types.JSONText
  232. if err := q.Get(&s); err != nil {
  233. lo.Fatalf("error reading settings from DB: %s", pqErrMsg(err))
  234. }
  235. // Setting keys are dot separated, eg: app.favicon_url. Unflatten them into
  236. // nested maps {app: {favicon_url}}.
  237. var out map[string]interface{}
  238. if err := json.Unmarshal(s, &out); err != nil {
  239. lo.Fatalf("error unmarshalling settings from DB: %v", err)
  240. }
  241. if err := ko.Load(confmap.Provider(out, "."), nil); err != nil {
  242. lo.Fatalf("error parsing settings from DB: %v", err)
  243. }
  244. }
  245. func initConstants() *constants {
  246. // Read constants.
  247. var c constants
  248. if err := ko.Unmarshal("app", &c); err != nil {
  249. lo.Fatalf("error loading app config: %v", err)
  250. }
  251. if err := ko.Unmarshal("privacy", &c.Privacy); err != nil {
  252. lo.Fatalf("error loading app config: %v", err)
  253. }
  254. c.RootURL = strings.TrimRight(c.RootURL, "/")
  255. c.Lang = ko.String("app.lang")
  256. c.Privacy.Exportable = maps.StringSliceToLookupMap(ko.Strings("privacy.exportable"))
  257. c.MediaProvider = ko.String("upload.provider")
  258. c.Privacy.DomainBlocklist = maps.StringSliceToLookupMap(ko.Strings("privacy.domain_blocklist"))
  259. // Static URLS.
  260. // url.com/subscription/{campaign_uuid}/{subscriber_uuid}
  261. c.UnsubURL = fmt.Sprintf("%s/subscription/%%s/%%s", c.RootURL)
  262. // url.com/subscription/optin/{subscriber_uuid}
  263. c.OptinURL = fmt.Sprintf("%s/subscription/optin/%%s?%%s", c.RootURL)
  264. // url.com/link/{campaign_uuid}/{subscriber_uuid}/{link_uuid}
  265. c.LinkTrackURL = fmt.Sprintf("%s/link/%%s/%%s/%%s", c.RootURL)
  266. // url.com/link/{campaign_uuid}/{subscriber_uuid}
  267. c.MessageURL = fmt.Sprintf("%s/campaign/%%s/%%s", c.RootURL)
  268. // url.com/campaign/{campaign_uuid}/{subscriber_uuid}/px.png
  269. c.ViewTrackURL = fmt.Sprintf("%s/campaign/%%s/%%s/px.png", c.RootURL)
  270. c.BounceWebhooksEnabled = ko.Bool("bounce.webhooks_enabled")
  271. c.BounceSESEnabled = ko.Bool("bounce.ses_enabled")
  272. c.BounceSendgridEnabled = ko.Bool("bounce.sendgrid_enabled")
  273. return &c
  274. }
  275. // initI18n initializes a new i18n instance with the selected language map
  276. // loaded from the filesystem. English is a loaded first as the default map
  277. // and then the selected language is loaded on top of it so that if there are
  278. // missing translations in it, the default English translations show up.
  279. func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n {
  280. i, ok, err := getI18nLang(lang, fs)
  281. if err != nil {
  282. if ok {
  283. lo.Println(err)
  284. } else {
  285. lo.Fatal(err)
  286. }
  287. }
  288. return i
  289. }
  290. // initCampaignManager initializes the campaign manager.
  291. func initCampaignManager(q *Queries, cs *constants, app *App) *manager.Manager {
  292. campNotifCB := func(subject string, data interface{}) error {
  293. return app.sendNotification(cs.NotifyEmails, subject, notifTplCampaign, data)
  294. }
  295. if ko.Int("app.concurrency") < 1 {
  296. lo.Fatal("app.concurrency should be at least 1")
  297. }
  298. if ko.Int("app.message_rate") < 1 {
  299. lo.Fatal("app.message_rate should be at least 1")
  300. }
  301. return manager.New(manager.Config{
  302. BatchSize: ko.Int("app.batch_size"),
  303. Concurrency: ko.Int("app.concurrency"),
  304. MessageRate: ko.Int("app.message_rate"),
  305. MaxSendErrors: ko.Int("app.max_send_errors"),
  306. FromEmail: cs.FromEmail,
  307. IndividualTracking: ko.Bool("privacy.individual_tracking"),
  308. UnsubURL: cs.UnsubURL,
  309. OptinURL: cs.OptinURL,
  310. LinkTrackURL: cs.LinkTrackURL,
  311. ViewTrackURL: cs.ViewTrackURL,
  312. MessageURL: cs.MessageURL,
  313. UnsubHeader: ko.Bool("privacy.unsubscribe_header"),
  314. SlidingWindow: ko.Bool("app.message_sliding_window"),
  315. SlidingWindowDuration: ko.Duration("app.message_sliding_window_duration"),
  316. SlidingWindowRate: ko.Int("app.message_sliding_window_rate"),
  317. }, newManagerStore(q), campNotifCB, app.i18n, lo)
  318. }
  319. // initImporter initializes the bulk subscriber importer.
  320. func initImporter(q *Queries, db *sqlx.DB, app *App) *subimporter.Importer {
  321. return subimporter.New(
  322. subimporter.Options{
  323. DomainBlocklist: app.constants.Privacy.DomainBlocklist,
  324. UpsertStmt: q.UpsertSubscriber.Stmt,
  325. BlocklistStmt: q.UpsertBlocklistSubscriber.Stmt,
  326. UpdateListDateStmt: q.UpdateListsDate.Stmt,
  327. NotifCB: func(subject string, data interface{}) error {
  328. app.sendNotification(app.constants.NotifyEmails, subject, notifTplImport, data)
  329. return nil
  330. },
  331. }, db.DB, app.i18n)
  332. }
  333. // initSMTPMessenger initializes the SMTP messenger.
  334. func initSMTPMessenger(m *manager.Manager) messenger.Messenger {
  335. var (
  336. mapKeys = ko.MapKeys("smtp")
  337. servers = make([]email.Server, 0, len(mapKeys))
  338. )
  339. items := ko.Slices("smtp")
  340. if len(items) == 0 {
  341. lo.Fatalf("no SMTP servers found in config")
  342. }
  343. // Load the config for multipme SMTP servers.
  344. for _, item := range items {
  345. if !item.Bool("enabled") {
  346. continue
  347. }
  348. // Read the SMTP config.
  349. var s email.Server
  350. if err := item.UnmarshalWithConf("", &s, koanf.UnmarshalConf{Tag: "json"}); err != nil {
  351. lo.Fatalf("error reading SMTP config: %v", err)
  352. }
  353. servers = append(servers, s)
  354. lo.Printf("loaded email (SMTP) messenger: %s@%s",
  355. item.String("username"), item.String("host"))
  356. }
  357. if len(servers) == 0 {
  358. lo.Fatalf("no SMTP servers enabled in settings")
  359. }
  360. // Initialize the e-mail messenger with multiple SMTP servers.
  361. msgr, err := email.New(servers...)
  362. if err != nil {
  363. lo.Fatalf("error loading e-mail messenger: %v", err)
  364. }
  365. return msgr
  366. }
  367. // initPostbackMessengers initializes and returns all the enabled
  368. // HTTP postback messenger backends.
  369. func initPostbackMessengers(m *manager.Manager) []messenger.Messenger {
  370. items := ko.Slices("messengers")
  371. if len(items) == 0 {
  372. return nil
  373. }
  374. var out []messenger.Messenger
  375. for _, item := range items {
  376. if !item.Bool("enabled") {
  377. continue
  378. }
  379. // Read the Postback server config.
  380. var (
  381. name = item.String("name")
  382. o postback.Options
  383. )
  384. if err := item.UnmarshalWithConf("", &o, koanf.UnmarshalConf{Tag: "json"}); err != nil {
  385. lo.Fatalf("error reading Postback config: %v", err)
  386. }
  387. // Initialize the Messenger.
  388. p, err := postback.New(o)
  389. if err != nil {
  390. lo.Fatalf("error initializing Postback messenger %s: %v", name, err)
  391. }
  392. out = append(out, p)
  393. lo.Printf("loaded Postback messenger: %s", name)
  394. }
  395. return out
  396. }
  397. // initMediaStore initializes Upload manager with a custom backend.
  398. func initMediaStore() media.Store {
  399. switch provider := ko.String("upload.provider"); provider {
  400. case "s3":
  401. var o s3.Opt
  402. ko.Unmarshal("upload.s3", &o)
  403. up, err := s3.NewS3Store(o)
  404. if err != nil {
  405. lo.Fatalf("error initializing s3 upload provider %s", err)
  406. }
  407. lo.Println("media upload provider: s3")
  408. return up
  409. case "filesystem":
  410. var o filesystem.Opts
  411. ko.Unmarshal("upload.filesystem", &o)
  412. o.RootURL = ko.String("app.root_url")
  413. o.UploadPath = filepath.Clean(o.UploadPath)
  414. o.UploadURI = filepath.Clean(o.UploadURI)
  415. up, err := filesystem.NewDiskStore(o)
  416. if err != nil {
  417. lo.Fatalf("error initializing filesystem upload provider %s", err)
  418. }
  419. lo.Println("media upload provider: filesystem")
  420. return up
  421. default:
  422. lo.Fatalf("unknown provider. select filesystem or s3")
  423. }
  424. return nil
  425. }
  426. // initNotifTemplates compiles and returns e-mail notification templates that are
  427. // used for sending ad-hoc notifications to admins and subscribers.
  428. func initNotifTemplates(path string, fs stuffbin.FileSystem, i *i18n.I18n, cs *constants) *template.Template {
  429. // Register utility functions that the e-mail templates can use.
  430. funcs := template.FuncMap{
  431. "RootURL": func() string {
  432. return cs.RootURL
  433. },
  434. "LogoURL": func() string {
  435. return cs.LogoURL
  436. },
  437. "L": func() *i18n.I18n {
  438. return i
  439. },
  440. }
  441. tpl, err := stuffbin.ParseTemplatesGlob(funcs, fs, "/static/email-templates/*.html")
  442. if err != nil {
  443. lo.Fatalf("error parsing e-mail notif templates: %v", err)
  444. }
  445. return tpl
  446. }
  447. // initBounceManager initializes the bounce manager that scans mailboxes and listens to webhooks
  448. // for incoming bounce events.
  449. func initBounceManager(app *App) *bounce.Manager {
  450. opt := bounce.Opt{
  451. BounceCount: ko.MustInt("bounce.count"),
  452. BounceAction: ko.MustString("bounce.action"),
  453. WebhooksEnabled: ko.Bool("bounce.webhooks_enabled"),
  454. SESEnabled: ko.Bool("bounce.ses_enabled"),
  455. SendgridEnabled: ko.Bool("bounce.sendgrid_enabled"),
  456. SendgridKey: ko.String("bounce.sendgrid_key"),
  457. }
  458. // For now, only one mailbox is supported.
  459. for _, b := range ko.Slices("bounce.mailboxes") {
  460. if !b.Bool("enabled") {
  461. continue
  462. }
  463. var boxOpt mailbox.Opt
  464. if err := b.UnmarshalWithConf("", &boxOpt, koanf.UnmarshalConf{Tag: "json"}); err != nil {
  465. lo.Fatalf("error reading bounce mailbox config: %v", err)
  466. }
  467. opt.MailboxType = b.String("type")
  468. opt.MailboxEnabled = true
  469. opt.Mailbox = boxOpt
  470. break
  471. }
  472. b, err := bounce.New(opt, &bounce.Queries{
  473. RecordQuery: app.queries.RecordBounce,
  474. }, app.log)
  475. if err != nil {
  476. lo.Fatalf("error initializing bounce manager: %v", err)
  477. }
  478. return b
  479. }
  480. // initHTTPServer sets up and runs the app's main HTTP server and blocks forever.
  481. func initHTTPServer(app *App) *echo.Echo {
  482. // Initialize the HTTP server.
  483. var srv = echo.New()
  484. srv.HideBanner = true
  485. // Register app (*App) to be injected into all HTTP handlers.
  486. srv.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  487. return func(c echo.Context) error {
  488. c.Set("app", app)
  489. return next(c)
  490. }
  491. })
  492. // Parse and load user facing templates.
  493. tpl, err := stuffbin.ParseTemplatesGlob(template.FuncMap{
  494. "L": func() *i18n.I18n {
  495. return app.i18n
  496. }}, app.fs, "/public/templates/*.html")
  497. if err != nil {
  498. lo.Fatalf("error parsing public templates: %v", err)
  499. }
  500. srv.Renderer = &tplRenderer{
  501. templates: tpl,
  502. RootURL: app.constants.RootURL,
  503. LogoURL: app.constants.LogoURL,
  504. FaviconURL: app.constants.FaviconURL}
  505. // Initialize the static file server.
  506. fSrv := app.fs.FileServer()
  507. // Public (subscriber) facing static files.
  508. srv.GET("/public/*", echo.WrapHandler(fSrv))
  509. // Admin (frontend) facing static files.
  510. srv.GET("/admin/static/*", echo.WrapHandler(fSrv))
  511. // Public (subscriber) facing media upload files.
  512. if ko.String("upload.provider") == "filesystem" {
  513. srv.Static(ko.String("upload.filesystem.upload_uri"),
  514. ko.String("upload.filesystem.upload_path"))
  515. }
  516. // Register all HTTP handlers.
  517. initHTTPHandlers(srv, app)
  518. // Start the server.
  519. go func() {
  520. if err := srv.Start(ko.String("app.address")); err != nil {
  521. if strings.Contains(err.Error(), "Server closed") {
  522. lo.Println("HTTP server shut down")
  523. } else {
  524. lo.Fatalf("error starting HTTP server: %v", err)
  525. }
  526. }
  527. }()
  528. return srv
  529. }
  530. func awaitReload(sigChan chan os.Signal, closerWait chan bool, closer func()) chan bool {
  531. // The blocking signal handler that main() waits on.
  532. out := make(chan bool)
  533. // Respawn a new process and exit the running one.
  534. respawn := func() {
  535. if err := syscall.Exec(os.Args[0], os.Args, os.Environ()); err != nil {
  536. lo.Fatalf("error spawning process: %v", err)
  537. }
  538. os.Exit(0)
  539. }
  540. // Listen for reload signal.
  541. go func() {
  542. for range sigChan {
  543. lo.Println("reloading on signal ...")
  544. go closer()
  545. select {
  546. case <-closerWait:
  547. // Wait for the closer to finish.
  548. respawn()
  549. case <-time.After(time.Second * 3):
  550. // Or timeout and force close.
  551. respawn()
  552. }
  553. }
  554. }()
  555. return out
  556. }
  557. func joinFSPaths(root string, paths []string) []string {
  558. out := make([]string, 0, len(paths))
  559. for _, p := range paths {
  560. // real_path:stuffbin_alias
  561. f := strings.Split(p, ":")
  562. out = append(out, path.Join(root, f[0])+":"+f[1])
  563. }
  564. return out
  565. }