A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 
 

761 righe
19 KiB

  1. /*
  2. * Copyright © 2018-2019 A Bunch Tell LLC.
  3. *
  4. * This file is part of WriteFreely.
  5. *
  6. * WriteFreely is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License, included
  8. * in the LICENSE file in this source code package.
  9. */
  10. package writefreely
  11. import (
  12. "database/sql"
  13. "fmt"
  14. "html/template"
  15. "io/ioutil"
  16. "net/http"
  17. "net/url"
  18. "os"
  19. "os/signal"
  20. "path/filepath"
  21. "regexp"
  22. "strings"
  23. "syscall"
  24. "time"
  25. "github.com/gorilla/mux"
  26. "github.com/gorilla/schema"
  27. "github.com/gorilla/sessions"
  28. "github.com/manifoldco/promptui"
  29. "github.com/writeas/go-strip-markdown"
  30. "github.com/writeas/impart"
  31. "github.com/writeas/web-core/auth"
  32. "github.com/writeas/web-core/converter"
  33. "github.com/writeas/web-core/log"
  34. "github.com/writeas/writefreely/author"
  35. "github.com/writeas/writefreely/config"
  36. "github.com/writeas/writefreely/key"
  37. "github.com/writeas/writefreely/migrations"
  38. "github.com/writeas/writefreely/page"
  39. )
  40. const (
  41. staticDir = "static"
  42. assumedTitleLen = 80
  43. postsPerPage = 10
  44. serverSoftware = "WriteFreely"
  45. softwareURL = "https://writefreely.org"
  46. )
  47. var (
  48. debugging bool
  49. // Software version can be set from git env using -ldflags
  50. softwareVer = "0.10.0"
  51. // DEPRECATED VARS
  52. isSingleUser bool
  53. )
  54. // App holds data and configuration for an individual WriteFreely instance.
  55. type App struct {
  56. router *mux.Router
  57. shttp *http.ServeMux
  58. db *datastore
  59. cfg *config.Config
  60. cfgFile string
  61. keys *key.Keychain
  62. sessionStore *sessions.CookieStore
  63. formDecoder *schema.Decoder
  64. timeline *localTimeline
  65. }
  66. // DB returns the App's datastore
  67. func (app *App) DB() *datastore {
  68. return app.db
  69. }
  70. // Router returns the App's router
  71. func (app *App) Router() *mux.Router {
  72. return app.router
  73. }
  74. // Config returns the App's current configuration.
  75. func (app *App) Config() *config.Config {
  76. return app.cfg
  77. }
  78. // SetConfig updates the App's Config to the given value.
  79. func (app *App) SetConfig(cfg *config.Config) {
  80. app.cfg = cfg
  81. }
  82. // SetKeys updates the App's Keychain to the given value.
  83. func (app *App) SetKeys(k *key.Keychain) {
  84. app.keys = k
  85. }
  86. // Apper is the interface for getting data into and out of a WriteFreely
  87. // instance (or "App").
  88. //
  89. // App returns the App for the current instance.
  90. //
  91. // LoadConfig reads an app configuration into the App, returning any error
  92. // encountered.
  93. //
  94. // SaveConfig persists the current App configuration.
  95. //
  96. // LoadKeys reads the App's encryption keys and loads them into its
  97. // key.Keychain.
  98. type Apper interface {
  99. App() *App
  100. LoadConfig() error
  101. SaveConfig(*config.Config) error
  102. LoadKeys() error
  103. }
  104. // App returns the App
  105. func (app *App) App() *App {
  106. return app
  107. }
  108. // LoadConfig loads and parses a config file.
  109. func (app *App) LoadConfig() error {
  110. log.Info("Loading %s configuration...", app.cfgFile)
  111. cfg, err := config.Load(app.cfgFile)
  112. if err != nil {
  113. log.Error("Unable to load configuration: %v", err)
  114. os.Exit(1)
  115. return err
  116. }
  117. app.cfg = cfg
  118. return nil
  119. }
  120. // SaveConfig saves the given Config to disk -- namely, to the App's cfgFile.
  121. func (app *App) SaveConfig(c *config.Config) error {
  122. return config.Save(c, app.cfgFile)
  123. }
  124. // LoadKeys reads all needed keys from disk into the App. In order to use the
  125. // configured `Server.KeysParentDir`, you must call initKeyPaths(App) before
  126. // this.
  127. func (app *App) LoadKeys() error {
  128. var err error
  129. app.keys = &key.Keychain{}
  130. if debugging {
  131. log.Info(" %s", emailKeyPath)
  132. }
  133. app.keys.EmailKey, err = ioutil.ReadFile(emailKeyPath)
  134. if err != nil {
  135. return err
  136. }
  137. if debugging {
  138. log.Info(" %s", cookieAuthKeyPath)
  139. }
  140. app.keys.CookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath)
  141. if err != nil {
  142. return err
  143. }
  144. if debugging {
  145. log.Info(" %s", cookieKeyPath)
  146. }
  147. app.keys.CookieKey, err = ioutil.ReadFile(cookieKeyPath)
  148. if err != nil {
  149. return err
  150. }
  151. return nil
  152. }
  153. // handleViewHome shows page at root path. Will be the Pad if logged in and the
  154. // catch-all landing page otherwise.
  155. func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error {
  156. if app.cfg.App.SingleUser {
  157. // Render blog index
  158. return handleViewCollection(app, w, r)
  159. }
  160. // Multi-user instance
  161. forceLanding := r.FormValue("landing") == "1"
  162. if !forceLanding {
  163. // Show correct page based on user auth status and configured landing path
  164. u := getUserSession(app, r)
  165. if u != nil {
  166. // User is logged in, so show the Pad
  167. return handleViewPad(app, w, r)
  168. }
  169. if land := app.cfg.App.LandingPath(); land != "/" {
  170. return impart.HTTPError{http.StatusFound, land}
  171. }
  172. }
  173. p := struct {
  174. page.StaticPage
  175. Flashes []template.HTML
  176. Banner template.HTML
  177. Content template.HTML
  178. ForcedLanding bool
  179. }{
  180. StaticPage: pageForReq(app, r),
  181. ForcedLanding: forceLanding,
  182. }
  183. banner, err := getLandingBanner(app)
  184. if err != nil {
  185. log.Error("unable to get landing banner: %v", err)
  186. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get banner: %v", err)}
  187. }
  188. p.Banner = template.HTML(applyMarkdown([]byte(banner.Content), ""))
  189. content, err := getLandingBody(app)
  190. if err != nil {
  191. log.Error("unable to get landing content: %v", err)
  192. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get content: %v", err)}
  193. }
  194. p.Content = template.HTML(applyMarkdown([]byte(content.Content), ""))
  195. // Get error messages
  196. session, err := app.sessionStore.Get(r, cookieName)
  197. if err != nil {
  198. // Ignore this
  199. log.Error("Unable to get session in handleViewHome; ignoring: %v", err)
  200. }
  201. flashes, _ := getSessionFlashes(app, w, r, session)
  202. for _, flash := range flashes {
  203. p.Flashes = append(p.Flashes, template.HTML(flash))
  204. }
  205. // Show landing page
  206. return renderPage(w, "landing.tmpl", p)
  207. }
  208. func handleTemplatedPage(app *App, w http.ResponseWriter, r *http.Request, t *template.Template) error {
  209. p := struct {
  210. page.StaticPage
  211. ContentTitle string
  212. Content template.HTML
  213. PlainContent string
  214. Updated string
  215. AboutStats *InstanceStats
  216. }{
  217. StaticPage: pageForReq(app, r),
  218. }
  219. if r.URL.Path == "/about" || r.URL.Path == "/privacy" {
  220. var c *instanceContent
  221. var err error
  222. if r.URL.Path == "/about" {
  223. c, err = getAboutPage(app)
  224. // Fetch stats
  225. p.AboutStats = &InstanceStats{}
  226. p.AboutStats.NumPosts, _ = app.db.GetTotalPosts()
  227. p.AboutStats.NumBlogs, _ = app.db.GetTotalCollections()
  228. } else {
  229. c, err = getPrivacyPage(app)
  230. }
  231. if err != nil {
  232. return err
  233. }
  234. p.ContentTitle = c.Title.String
  235. p.Content = template.HTML(applyMarkdown([]byte(c.Content), ""))
  236. p.PlainContent = shortPostDescription(stripmd.Strip(c.Content))
  237. if !c.Updated.IsZero() {
  238. p.Updated = c.Updated.Format("January 2, 2006")
  239. }
  240. }
  241. // Serve templated page
  242. err := t.ExecuteTemplate(w, "base", p)
  243. if err != nil {
  244. log.Error("Unable to render page: %v", err)
  245. }
  246. return nil
  247. }
  248. func pageForReq(app *App, r *http.Request) page.StaticPage {
  249. p := page.StaticPage{
  250. AppCfg: app.cfg.App,
  251. Path: r.URL.Path,
  252. Version: "v" + softwareVer,
  253. }
  254. // Add user information, if given
  255. var u *User
  256. accessToken := r.FormValue("t")
  257. if accessToken != "" {
  258. userID := app.db.GetUserID(accessToken)
  259. if userID != -1 {
  260. var err error
  261. u, err = app.db.GetUserByID(userID)
  262. if err == nil {
  263. p.Username = u.Username
  264. }
  265. }
  266. } else {
  267. u = getUserSession(app, r)
  268. if u != nil {
  269. p.Username = u.Username
  270. }
  271. }
  272. p.CanViewReader = !app.cfg.App.Private || u != nil
  273. return p
  274. }
  275. var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$")
  276. // Initialize loads the app configuration and initializes templates, keys,
  277. // session, route handlers, and the database connection.
  278. func Initialize(apper Apper, debug bool) (*App, error) {
  279. debugging = debug
  280. apper.LoadConfig()
  281. // Load templates
  282. err := InitTemplates(apper.App().Config())
  283. if err != nil {
  284. return nil, fmt.Errorf("load templates: %s", err)
  285. }
  286. // Load keys and set up session
  287. initKeyPaths(apper.App()) // TODO: find a better way to do this, since it's unneeded in all Apper implementations
  288. err = InitKeys(apper)
  289. if err != nil {
  290. return nil, fmt.Errorf("init keys: %s", err)
  291. }
  292. apper.App().InitSession()
  293. apper.App().InitDecoder()
  294. err = ConnectToDatabase(apper.App())
  295. if err != nil {
  296. return nil, fmt.Errorf("connect to DB: %s", err)
  297. }
  298. // Handle local timeline, if enabled
  299. if apper.App().cfg.App.LocalTimeline {
  300. log.Info("Initializing local timeline...")
  301. initLocalTimeline(apper.App())
  302. }
  303. return apper.App(), nil
  304. }
  305. func Serve(app *App, r *mux.Router) {
  306. log.Info("Going to serve...")
  307. isSingleUser = app.cfg.App.SingleUser
  308. app.cfg.Server.Dev = debugging
  309. // Handle shutdown
  310. c := make(chan os.Signal, 2)
  311. signal.Notify(c, os.Interrupt, syscall.SIGTERM)
  312. go func() {
  313. <-c
  314. log.Info("Shutting down...")
  315. shutdown(app)
  316. log.Info("Done.")
  317. os.Exit(0)
  318. }()
  319. // Start web application server
  320. var bindAddress = app.cfg.Server.Bind
  321. if bindAddress == "" {
  322. bindAddress = "localhost"
  323. }
  324. var err error
  325. if app.cfg.IsSecureStandalone() {
  326. log.Info("Serving redirects on http://%s:80", bindAddress)
  327. go func() {
  328. err = http.ListenAndServe(
  329. fmt.Sprintf("%s:80", bindAddress), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  330. http.Redirect(w, r, app.cfg.App.Host, http.StatusMovedPermanently)
  331. }))
  332. log.Error("Unable to start redirect server: %v", err)
  333. }()
  334. log.Info("Serving on https://%s:443", bindAddress)
  335. log.Info("---")
  336. err = http.ListenAndServeTLS(
  337. fmt.Sprintf("%s:443", bindAddress), app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, r)
  338. } else {
  339. log.Info("Serving on http://%s:%d\n", bindAddress, app.cfg.Server.Port)
  340. log.Info("---")
  341. err = http.ListenAndServe(fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port), r)
  342. }
  343. if err != nil {
  344. log.Error("Unable to start: %v", err)
  345. os.Exit(1)
  346. }
  347. }
  348. func (app *App) InitDecoder() {
  349. // TODO: do this at the package level, instead of the App level
  350. // Initialize modules
  351. app.formDecoder = schema.NewDecoder()
  352. app.formDecoder.RegisterConverter(converter.NullJSONString{}, converter.ConvertJSONNullString)
  353. app.formDecoder.RegisterConverter(converter.NullJSONBool{}, converter.ConvertJSONNullBool)
  354. app.formDecoder.RegisterConverter(sql.NullString{}, converter.ConvertSQLNullString)
  355. app.formDecoder.RegisterConverter(sql.NullBool{}, converter.ConvertSQLNullBool)
  356. app.formDecoder.RegisterConverter(sql.NullInt64{}, converter.ConvertSQLNullInt64)
  357. app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64)
  358. }
  359. // ConnectToDatabase validates and connects to the configured database, then
  360. // tests the connection.
  361. func ConnectToDatabase(app *App) error {
  362. // Check database configuration
  363. if app.cfg.Database.Type == driverMySQL && (app.cfg.Database.User == "" || app.cfg.Database.Password == "") {
  364. return fmt.Errorf("Database user or password not set.")
  365. }
  366. if app.cfg.Database.Host == "" {
  367. app.cfg.Database.Host = "localhost"
  368. }
  369. if app.cfg.Database.Database == "" {
  370. app.cfg.Database.Database = "writefreely"
  371. }
  372. // TODO: check err
  373. connectToDatabase(app)
  374. // Test database connection
  375. err := app.db.Ping()
  376. if err != nil {
  377. return fmt.Errorf("Database ping failed: %s", err)
  378. }
  379. return nil
  380. }
  381. // OutputVersion prints out the version of the application.
  382. func OutputVersion() {
  383. fmt.Println(serverSoftware + " " + softwareVer)
  384. }
  385. // NewApp creates a new app instance.
  386. func NewApp(cfgFile string) *App {
  387. return &App{
  388. cfgFile: cfgFile,
  389. }
  390. }
  391. // CreateConfig creates a default configuration and saves it to the app's cfgFile.
  392. func CreateConfig(app *App) error {
  393. log.Info("Creating configuration...")
  394. c := config.New()
  395. log.Info("Saving configuration %s...", app.cfgFile)
  396. err := config.Save(c, app.cfgFile)
  397. if err != nil {
  398. return fmt.Errorf("Unable to save configuration: %v", err)
  399. }
  400. return nil
  401. }
  402. // DoConfig runs the interactive configuration process.
  403. func DoConfig(app *App, configSections string) {
  404. if configSections == "" {
  405. configSections = "server db app"
  406. }
  407. // let's check there aren't any garbage in the list
  408. configSectionsArray := strings.Split(configSections, " ")
  409. for _, element := range configSectionsArray {
  410. if element != "server" && element != "db" && element != "app" {
  411. log.Error("Invalid argument to --sections. Valid arguments are only \"server\", \"db\" and \"app\"")
  412. os.Exit(1)
  413. }
  414. }
  415. d, err := config.Configure(app.cfgFile, configSections)
  416. if err != nil {
  417. log.Error("Unable to configure: %v", err)
  418. os.Exit(1)
  419. }
  420. app.cfg = d.Config
  421. connectToDatabase(app)
  422. defer shutdown(app)
  423. if !app.db.DatabaseInitialized() {
  424. err = adminInitDatabase(app)
  425. if err != nil {
  426. log.Error(err.Error())
  427. os.Exit(1)
  428. }
  429. } else {
  430. log.Info("Database already initialized.")
  431. }
  432. if d.User != nil {
  433. u := &User{
  434. Username: d.User.Username,
  435. HashedPass: d.User.HashedPass,
  436. Created: time.Now().Truncate(time.Second).UTC(),
  437. }
  438. // Create blog
  439. log.Info("Creating user %s...\n", u.Username)
  440. err = app.db.CreateUser(u, app.cfg.App.SiteName)
  441. if err != nil {
  442. log.Error("Unable to create user: %s", err)
  443. os.Exit(1)
  444. }
  445. log.Info("Done!")
  446. }
  447. os.Exit(0)
  448. }
  449. // GenerateKeyFiles creates app encryption keys and saves them into the configured KeysParentDir.
  450. func GenerateKeyFiles(app *App) error {
  451. // Read keys path from config
  452. app.LoadConfig()
  453. // Create keys dir if it doesn't exist yet
  454. fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir)
  455. if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) {
  456. err = os.Mkdir(fullKeysDir, 0700)
  457. if err != nil {
  458. return err
  459. }
  460. }
  461. // Generate keys
  462. initKeyPaths(app)
  463. // TODO: use something like https://github.com/hashicorp/go-multierror to return errors
  464. var keyErrs error
  465. err := generateKey(emailKeyPath)
  466. if err != nil {
  467. keyErrs = err
  468. }
  469. err = generateKey(cookieAuthKeyPath)
  470. if err != nil {
  471. keyErrs = err
  472. }
  473. err = generateKey(cookieKeyPath)
  474. if err != nil {
  475. keyErrs = err
  476. }
  477. return keyErrs
  478. }
  479. // CreateSchema creates all database tables needed for the application.
  480. func CreateSchema(apper Apper) error {
  481. apper.LoadConfig()
  482. connectToDatabase(apper.App())
  483. defer shutdown(apper.App())
  484. err := adminInitDatabase(apper.App())
  485. if err != nil {
  486. return err
  487. }
  488. return nil
  489. }
  490. // Migrate runs all necessary database migrations.
  491. func Migrate(apper Apper) error {
  492. apper.LoadConfig()
  493. connectToDatabase(apper.App())
  494. defer shutdown(apper.App())
  495. err := migrations.Migrate(migrations.NewDatastore(apper.App().db.DB, apper.App().db.driverName))
  496. if err != nil {
  497. return fmt.Errorf("migrate: %s", err)
  498. }
  499. return nil
  500. }
  501. // ResetPassword runs the interactive password reset process.
  502. func ResetPassword(apper Apper, username string) error {
  503. // Connect to the database
  504. apper.LoadConfig()
  505. connectToDatabase(apper.App())
  506. defer shutdown(apper.App())
  507. // Fetch user
  508. u, err := apper.App().db.GetUserForAuth(username)
  509. if err != nil {
  510. log.Error("Get user: %s", err)
  511. os.Exit(1)
  512. }
  513. // Prompt for new password
  514. prompt := promptui.Prompt{
  515. Templates: &promptui.PromptTemplates{
  516. Success: "{{ . | bold | faint }}: ",
  517. },
  518. Label: "New password",
  519. Mask: '*',
  520. }
  521. newPass, err := prompt.Run()
  522. if err != nil {
  523. log.Error("%s", err)
  524. os.Exit(1)
  525. }
  526. // Do the update
  527. log.Info("Updating...")
  528. err = adminResetPassword(apper.App(), u, newPass)
  529. if err != nil {
  530. log.Error("%s", err)
  531. os.Exit(1)
  532. }
  533. log.Info("Success.")
  534. return nil
  535. }
  536. func connectToDatabase(app *App) {
  537. log.Info("Connecting to %s database...", app.cfg.Database.Type)
  538. var db *sql.DB
  539. var err error
  540. if app.cfg.Database.Type == driverMySQL {
  541. db, err = sql.Open(app.cfg.Database.Type, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database, url.QueryEscape(time.Local.String())))
  542. db.SetMaxOpenConns(50)
  543. } else if app.cfg.Database.Type == driverSQLite {
  544. if !SQLiteEnabled {
  545. log.Error("Invalid database type '%s'. Binary wasn't compiled with SQLite3 support.", app.cfg.Database.Type)
  546. os.Exit(1)
  547. }
  548. if app.cfg.Database.FileName == "" {
  549. log.Error("SQLite database filename value in config.ini is empty.")
  550. os.Exit(1)
  551. }
  552. db, err = sql.Open("sqlite3_with_regex", app.cfg.Database.FileName+"?parseTime=true&cached=shared")
  553. db.SetMaxOpenConns(1)
  554. } else {
  555. log.Error("Invalid database type '%s'. Only 'mysql' and 'sqlite3' are supported right now.", app.cfg.Database.Type)
  556. os.Exit(1)
  557. }
  558. if err != nil {
  559. log.Error("%s", err)
  560. os.Exit(1)
  561. }
  562. app.db = &datastore{db, app.cfg.Database.Type}
  563. }
  564. func shutdown(app *App) {
  565. log.Info("Closing database connection...")
  566. app.db.Close()
  567. }
  568. // CreateUser creates a new admin or normal user from the given credentials.
  569. func CreateUser(apper Apper, username, password string, isAdmin bool) error {
  570. // Create an admin user with --create-admin
  571. apper.LoadConfig()
  572. connectToDatabase(apper.App())
  573. defer shutdown(apper.App())
  574. // Ensure an admin / first user doesn't already exist
  575. firstUser, _ := apper.App().db.GetUserByID(1)
  576. if isAdmin {
  577. // Abort if trying to create admin user, but one already exists
  578. if firstUser != nil {
  579. return fmt.Errorf("Admin user already exists (%s). Create a regular user with: writefreely --create-user", firstUser.Username)
  580. }
  581. } else {
  582. // Abort if trying to create regular user, but no admin exists yet
  583. if firstUser == nil {
  584. return fmt.Errorf("No admin user exists yet. Create an admin first with: writefreely --create-admin")
  585. }
  586. }
  587. // Create the user
  588. // Normalize and validate username
  589. desiredUsername := username
  590. username = getSlug(username, "")
  591. usernameDesc := username
  592. if username != desiredUsername {
  593. usernameDesc += " (originally: " + desiredUsername + ")"
  594. }
  595. if !author.IsValidUsername(apper.App().cfg, username) {
  596. return fmt.Errorf("Username %s is invalid, reserved, or shorter than configured minimum length (%d characters).", usernameDesc, apper.App().cfg.App.MinUsernameLen)
  597. }
  598. // Hash the password
  599. hashedPass, err := auth.HashPass([]byte(password))
  600. if err != nil {
  601. return fmt.Errorf("Unable to hash password: %v", err)
  602. }
  603. u := &User{
  604. Username: username,
  605. HashedPass: hashedPass,
  606. Created: time.Now().Truncate(time.Second).UTC(),
  607. }
  608. userType := "user"
  609. if isAdmin {
  610. userType = "admin"
  611. }
  612. log.Info("Creating %s %s...", userType, usernameDesc)
  613. err = apper.App().db.CreateUser(u, desiredUsername)
  614. if err != nil {
  615. return fmt.Errorf("Unable to create user: %s", err)
  616. }
  617. log.Info("Done!")
  618. return nil
  619. }
  620. func adminInitDatabase(app *App) error {
  621. schemaFileName := "schema.sql"
  622. if app.cfg.Database.Type == driverSQLite {
  623. schemaFileName = "sqlite.sql"
  624. }
  625. schema, err := Asset(schemaFileName)
  626. if err != nil {
  627. return fmt.Errorf("Unable to load schema file: %v", err)
  628. }
  629. tblReg := regexp.MustCompile("CREATE TABLE (IF NOT EXISTS )?`([a-z_]+)`")
  630. queries := strings.Split(string(schema), ";\n")
  631. for _, q := range queries {
  632. if strings.TrimSpace(q) == "" {
  633. continue
  634. }
  635. parts := tblReg.FindStringSubmatch(q)
  636. if len(parts) >= 3 {
  637. log.Info("Creating table %s...", parts[2])
  638. } else {
  639. log.Info("Creating table ??? (Weird query) No match in: %v", parts)
  640. }
  641. _, err = app.db.Exec(q)
  642. if err != nil {
  643. log.Error("%s", err)
  644. } else {
  645. log.Info("Created.")
  646. }
  647. }
  648. // Set up migrations table
  649. log.Info("Initializing appmigrations table...")
  650. err = migrations.SetInitialMigrations(migrations.NewDatastore(app.db.DB, app.db.driverName))
  651. if err != nil {
  652. return fmt.Errorf("Unable to set initial migrations: %v", err)
  653. }
  654. log.Info("Running migrations...")
  655. err = migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
  656. if err != nil {
  657. return fmt.Errorf("migrate: %s", err)
  658. }
  659. log.Info("Done.")
  660. return nil
  661. }