mirror of
https://github.com/writefreely/writefreely
synced 2024-11-10 19:34:19 +00:00
1d5c396327
Add --sections flag to app.go according to T657, parse them into a string array (check for invalid arguments and abort) and pass them to Configure(). For now Configure() doesn't do anything with them yet.
668 lines
17 KiB
Go
668 lines
17 KiB
Go
/*
|
|
* Copyright © 2018 A Bunch Tell LLC.
|
|
*
|
|
* This file is part of WriteFreely.
|
|
*
|
|
* WriteFreely is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License, included
|
|
* in the LICENSE file in this source code package.
|
|
*/
|
|
|
|
package writefreely
|
|
|
|
import (
|
|
"database/sql"
|
|
"flag"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/gorilla/schema"
|
|
"github.com/gorilla/sessions"
|
|
"github.com/manifoldco/promptui"
|
|
"github.com/writeas/go-strip-markdown"
|
|
"github.com/writeas/web-core/auth"
|
|
"github.com/writeas/web-core/converter"
|
|
"github.com/writeas/web-core/log"
|
|
"github.com/writeas/writefreely/author"
|
|
"github.com/writeas/writefreely/config"
|
|
"github.com/writeas/writefreely/migrations"
|
|
"github.com/writeas/writefreely/page"
|
|
)
|
|
|
|
const (
|
|
staticDir = "static"
|
|
assumedTitleLen = 80
|
|
postsPerPage = 10
|
|
|
|
serverSoftware = "WriteFreely"
|
|
softwareURL = "https://writefreely.org"
|
|
)
|
|
|
|
var (
|
|
debugging bool
|
|
|
|
// Software version can be set from git env using -ldflags
|
|
softwareVer = "0.9.0"
|
|
|
|
// DEPRECATED VARS
|
|
// TODO: pass app.cfg into GetCollection* calls so we can get these values
|
|
// from Collection methods and we no longer need these.
|
|
hostName string
|
|
isSingleUser bool
|
|
)
|
|
|
|
type app struct {
|
|
router *mux.Router
|
|
db *datastore
|
|
cfg *config.Config
|
|
cfgFile string
|
|
keys *keychain
|
|
sessionStore *sessions.CookieStore
|
|
formDecoder *schema.Decoder
|
|
|
|
timeline *localTimeline
|
|
}
|
|
|
|
// handleViewHome shows page at root path. Will be the Pad if logged in and the
|
|
// catch-all landing page otherwise.
|
|
func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error {
|
|
if app.cfg.App.SingleUser {
|
|
// Render blog index
|
|
return handleViewCollection(app, w, r)
|
|
}
|
|
|
|
// Multi-user instance
|
|
u := getUserSession(app, r)
|
|
if u != nil {
|
|
// User is logged in, so show the Pad
|
|
return handleViewPad(app, w, r)
|
|
}
|
|
|
|
p := struct {
|
|
page.StaticPage
|
|
Flashes []template.HTML
|
|
}{
|
|
StaticPage: pageForReq(app, r),
|
|
}
|
|
|
|
// Get error messages
|
|
session, err := app.sessionStore.Get(r, cookieName)
|
|
if err != nil {
|
|
// Ignore this
|
|
log.Error("Unable to get session in handleViewHome; ignoring: %v", err)
|
|
}
|
|
flashes, _ := getSessionFlashes(app, w, r, session)
|
|
for _, flash := range flashes {
|
|
p.Flashes = append(p.Flashes, template.HTML(flash))
|
|
}
|
|
|
|
// Show landing page
|
|
return renderPage(w, "landing.tmpl", p)
|
|
}
|
|
|
|
func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *template.Template) error {
|
|
p := struct {
|
|
page.StaticPage
|
|
ContentTitle string
|
|
Content template.HTML
|
|
PlainContent string
|
|
Updated string
|
|
|
|
AboutStats *InstanceStats
|
|
}{
|
|
StaticPage: pageForReq(app, r),
|
|
}
|
|
if r.URL.Path == "/about" || r.URL.Path == "/privacy" {
|
|
var c *instanceContent
|
|
var err error
|
|
|
|
if r.URL.Path == "/about" {
|
|
c, err = getAboutPage(app)
|
|
|
|
// Fetch stats
|
|
p.AboutStats = &InstanceStats{}
|
|
p.AboutStats.NumPosts, _ = app.db.GetTotalPosts()
|
|
p.AboutStats.NumBlogs, _ = app.db.GetTotalCollections()
|
|
} else {
|
|
c, err = getPrivacyPage(app)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.ContentTitle = c.Title.String
|
|
p.Content = template.HTML(applyMarkdown([]byte(c.Content), ""))
|
|
p.PlainContent = shortPostDescription(stripmd.Strip(c.Content))
|
|
if !c.Updated.IsZero() {
|
|
p.Updated = c.Updated.Format("January 2, 2006")
|
|
}
|
|
}
|
|
|
|
// Serve templated page
|
|
err := t.ExecuteTemplate(w, "base", p)
|
|
if err != nil {
|
|
log.Error("Unable to render page: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func pageForReq(app *app, r *http.Request) page.StaticPage {
|
|
p := page.StaticPage{
|
|
AppCfg: app.cfg.App,
|
|
Path: r.URL.Path,
|
|
Version: "v" + softwareVer,
|
|
}
|
|
|
|
// Add user information, if given
|
|
var u *User
|
|
accessToken := r.FormValue("t")
|
|
if accessToken != "" {
|
|
userID := app.db.GetUserID(accessToken)
|
|
if userID != -1 {
|
|
var err error
|
|
u, err = app.db.GetUserByID(userID)
|
|
if err == nil {
|
|
p.Username = u.Username
|
|
}
|
|
}
|
|
} else {
|
|
u = getUserSession(app, r)
|
|
if u != nil {
|
|
p.Username = u.Username
|
|
}
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
var shttp = http.NewServeMux()
|
|
var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$")
|
|
|
|
func Serve() {
|
|
// General options usable with other commands
|
|
debugPtr := flag.Bool("debug", false, "Enables debug logging.")
|
|
configFile := flag.String("c", "config.ini", "The configuration file to use")
|
|
|
|
// Setup actions
|
|
createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits")
|
|
doConfig := flag.Bool("config", false, "Run the configuration process")
|
|
configSections := flag.String("sections", "server db app", "Which sections of the configuration to go through (requires --config), " +
|
|
"valid values are any combination of 'server', 'db' and 'app' " +
|
|
"example: writefreely --config --sections \"db app\"")
|
|
genKeys := flag.Bool("gen-keys", false, "Generate encryption and authentication keys")
|
|
createSchema := flag.Bool("init-db", false, "Initialize app database")
|
|
migrate := flag.Bool("migrate", false, "Migrate the database")
|
|
|
|
// Admin actions
|
|
createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password")
|
|
createUser := flag.String("create-user", "", "Create a regular user with the given username:password")
|
|
resetPassUser := flag.String("reset-pass", "", "Reset the given user's password")
|
|
outputVersion := flag.Bool("v", false, "Output the current version")
|
|
flag.Parse()
|
|
|
|
debugging = *debugPtr
|
|
|
|
app := &app{
|
|
cfgFile: *configFile,
|
|
}
|
|
|
|
if *outputVersion {
|
|
fmt.Println(serverSoftware + " " + softwareVer)
|
|
os.Exit(0)
|
|
} else if *createConfig {
|
|
log.Info("Creating configuration...")
|
|
c := config.New()
|
|
log.Info("Saving configuration %s...", app.cfgFile)
|
|
err := config.Save(c, app.cfgFile)
|
|
if err != nil {
|
|
log.Error("Unable to save configuration: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
} else if *doConfig {
|
|
if *configSections == "" {
|
|
*configSections = "server db app"
|
|
}
|
|
configSectionsArray := strings.Split(*configSections, " ")
|
|
|
|
// let's check there aren't any garbage in the list
|
|
for _, element := range configSectionsArray {
|
|
if element != "server" && element != "db" && element != "app" {
|
|
log.Error("Invalid argument to --sections. Valid arguments are only \"server\", \"db\" and \"app\"")
|
|
}
|
|
}
|
|
d, err := config.Configure(app.cfgFile, configSectionsArray)
|
|
if err != nil {
|
|
log.Error("Unable to configure: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
if d.User != nil {
|
|
app.cfg = d.Config
|
|
connectToDatabase(app)
|
|
defer shutdown(app)
|
|
|
|
if !app.db.DatabaseInitialized() {
|
|
err = adminInitDatabase(app)
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
u := &User{
|
|
Username: d.User.Username,
|
|
HashedPass: d.User.HashedPass,
|
|
Created: time.Now().Truncate(time.Second).UTC(),
|
|
}
|
|
|
|
// Create blog
|
|
log.Info("Creating user %s...\n", u.Username)
|
|
err = app.db.CreateUser(u, app.cfg.App.SiteName)
|
|
if err != nil {
|
|
log.Error("Unable to create user: %s", err)
|
|
os.Exit(1)
|
|
}
|
|
log.Info("Done!")
|
|
}
|
|
os.Exit(0)
|
|
} else if *genKeys {
|
|
errStatus := 0
|
|
|
|
// Read keys path from config
|
|
loadConfig(app)
|
|
|
|
// Create keys dir if it doesn't exist yet
|
|
fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir)
|
|
if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) {
|
|
err = os.Mkdir(fullKeysDir, 0700)
|
|
if err != nil {
|
|
log.Error("%s", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Generate keys
|
|
initKeyPaths(app)
|
|
err := generateKey(emailKeyPath)
|
|
if err != nil {
|
|
errStatus = 1
|
|
}
|
|
err = generateKey(cookieAuthKeyPath)
|
|
if err != nil {
|
|
errStatus = 1
|
|
}
|
|
err = generateKey(cookieKeyPath)
|
|
if err != nil {
|
|
errStatus = 1
|
|
}
|
|
|
|
os.Exit(errStatus)
|
|
} else if *createSchema {
|
|
loadConfig(app)
|
|
connectToDatabase(app)
|
|
defer shutdown(app)
|
|
err := adminInitDatabase(app)
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
} else if *createAdmin != "" {
|
|
err := adminCreateUser(app, *createAdmin, true)
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
} else if *createUser != "" {
|
|
err := adminCreateUser(app, *createUser, false)
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
} else if *resetPassUser != "" {
|
|
// Connect to the database
|
|
loadConfig(app)
|
|
connectToDatabase(app)
|
|
defer shutdown(app)
|
|
|
|
// Fetch user
|
|
u, err := app.db.GetUserForAuth(*resetPassUser)
|
|
if err != nil {
|
|
log.Error("Get user: %s", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Prompt for new password
|
|
prompt := promptui.Prompt{
|
|
Templates: &promptui.PromptTemplates{
|
|
Success: "{{ . | bold | faint }}: ",
|
|
},
|
|
Label: "New password",
|
|
Mask: '*',
|
|
}
|
|
newPass, err := prompt.Run()
|
|
if err != nil {
|
|
log.Error("%s", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Do the update
|
|
log.Info("Updating...")
|
|
err = adminResetPassword(app, u, newPass)
|
|
if err != nil {
|
|
log.Error("%s", err)
|
|
os.Exit(1)
|
|
}
|
|
log.Info("Success.")
|
|
os.Exit(0)
|
|
} else if *migrate {
|
|
loadConfig(app)
|
|
connectToDatabase(app)
|
|
defer shutdown(app)
|
|
|
|
err := migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
|
|
if err != nil {
|
|
log.Error("migrate: %s", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
os.Exit(0)
|
|
}
|
|
|
|
log.Info("Initializing...")
|
|
|
|
loadConfig(app)
|
|
|
|
hostName = app.cfg.App.Host
|
|
isSingleUser = app.cfg.App.SingleUser
|
|
app.cfg.Server.Dev = *debugPtr
|
|
|
|
err := initTemplates(app.cfg)
|
|
if err != nil {
|
|
log.Error("load templates: %s", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Load keys
|
|
log.Info("Loading encryption keys...")
|
|
initKeyPaths(app)
|
|
err = initKeys(app)
|
|
if err != nil {
|
|
log.Error("\n%s\n", err)
|
|
}
|
|
|
|
// Initialize modules
|
|
app.sessionStore = initSession(app)
|
|
app.formDecoder = schema.NewDecoder()
|
|
app.formDecoder.RegisterConverter(converter.NullJSONString{}, converter.ConvertJSONNullString)
|
|
app.formDecoder.RegisterConverter(converter.NullJSONBool{}, converter.ConvertJSONNullBool)
|
|
app.formDecoder.RegisterConverter(sql.NullString{}, converter.ConvertSQLNullString)
|
|
app.formDecoder.RegisterConverter(sql.NullBool{}, converter.ConvertSQLNullBool)
|
|
app.formDecoder.RegisterConverter(sql.NullInt64{}, converter.ConvertSQLNullInt64)
|
|
app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64)
|
|
|
|
// Check database configuration
|
|
if app.cfg.Database.Type == driverMySQL && (app.cfg.Database.User == "" || app.cfg.Database.Password == "") {
|
|
log.Error("Database user or password not set.")
|
|
os.Exit(1)
|
|
}
|
|
if app.cfg.Database.Host == "" {
|
|
app.cfg.Database.Host = "localhost"
|
|
}
|
|
if app.cfg.Database.Database == "" {
|
|
app.cfg.Database.Database = "writefreely"
|
|
}
|
|
|
|
connectToDatabase(app)
|
|
defer shutdown(app)
|
|
|
|
// Test database connection
|
|
err = app.db.Ping()
|
|
if err != nil {
|
|
log.Error("Database ping failed: %s", err)
|
|
}
|
|
|
|
r := mux.NewRouter()
|
|
handler := NewHandler(app)
|
|
handler.SetErrorPages(&ErrorPages{
|
|
NotFound: pages["404-general.tmpl"],
|
|
Gone: pages["410.tmpl"],
|
|
InternalServerError: pages["500.tmpl"],
|
|
Blank: pages["blank.tmpl"],
|
|
})
|
|
|
|
// Handle app routes
|
|
initRoutes(handler, r, app.cfg, app.db)
|
|
|
|
// Handle local timeline, if enabled
|
|
if app.cfg.App.LocalTimeline {
|
|
log.Info("Initializing local timeline...")
|
|
initLocalTimeline(app)
|
|
}
|
|
|
|
// Handle static files
|
|
fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir)))
|
|
shttp.Handle("/", fs)
|
|
r.PathPrefix("/").Handler(fs)
|
|
|
|
// Handle shutdown
|
|
c := make(chan os.Signal, 2)
|
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
go func() {
|
|
<-c
|
|
log.Info("Shutting down...")
|
|
shutdown(app)
|
|
log.Info("Done.")
|
|
os.Exit(0)
|
|
}()
|
|
|
|
http.Handle("/", r)
|
|
|
|
// Start web application server
|
|
var bindAddress = app.cfg.Server.Bind
|
|
if bindAddress == "" {
|
|
bindAddress = "localhost"
|
|
}
|
|
if app.cfg.IsSecureStandalone() {
|
|
log.Info("Serving redirects on http://%s:80", bindAddress)
|
|
go func() {
|
|
err = http.ListenAndServe(
|
|
fmt.Sprintf("%s:80", bindAddress), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, app.cfg.App.Host, http.StatusMovedPermanently)
|
|
}))
|
|
log.Error("Unable to start redirect server: %v", err)
|
|
}()
|
|
|
|
log.Info("Serving on https://%s:443", bindAddress)
|
|
log.Info("---")
|
|
err = http.ListenAndServeTLS(
|
|
fmt.Sprintf("%s:443", bindAddress), app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, nil)
|
|
} else {
|
|
log.Info("Serving on http://%s:%d\n", bindAddress, app.cfg.Server.Port)
|
|
log.Info("---")
|
|
err = http.ListenAndServe(fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port), nil)
|
|
}
|
|
if err != nil {
|
|
log.Error("Unable to start: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func loadConfig(app *app) {
|
|
log.Info("Loading %s configuration...", app.cfgFile)
|
|
cfg, err := config.Load(app.cfgFile)
|
|
if err != nil {
|
|
log.Error("Unable to load configuration: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
app.cfg = cfg
|
|
}
|
|
|
|
func connectToDatabase(app *app) {
|
|
log.Info("Connecting to %s database...", app.cfg.Database.Type)
|
|
|
|
var db *sql.DB
|
|
var err error
|
|
if app.cfg.Database.Type == driverMySQL {
|
|
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())))
|
|
db.SetMaxOpenConns(50)
|
|
} else if app.cfg.Database.Type == driverSQLite {
|
|
if !SQLiteEnabled {
|
|
log.Error("Invalid database type '%s'. Binary wasn't compiled with SQLite3 support.", app.cfg.Database.Type)
|
|
os.Exit(1)
|
|
}
|
|
if app.cfg.Database.FileName == "" {
|
|
log.Error("SQLite database filename value in config.ini is empty.")
|
|
os.Exit(1)
|
|
}
|
|
db, err = sql.Open("sqlite3_with_regex", app.cfg.Database.FileName+"?parseTime=true&cached=shared")
|
|
db.SetMaxOpenConns(1)
|
|
} else {
|
|
log.Error("Invalid database type '%s'. Only 'mysql' and 'sqlite3' are supported right now.", app.cfg.Database.Type)
|
|
os.Exit(1)
|
|
}
|
|
if err != nil {
|
|
log.Error("%s", err)
|
|
os.Exit(1)
|
|
}
|
|
app.db = &datastore{db, app.cfg.Database.Type}
|
|
}
|
|
|
|
func shutdown(app *app) {
|
|
log.Info("Closing database connection...")
|
|
app.db.Close()
|
|
}
|
|
|
|
func adminCreateUser(app *app, credStr string, isAdmin bool) error {
|
|
// Create an admin user with --create-admin
|
|
creds := strings.Split(credStr, ":")
|
|
if len(creds) != 2 {
|
|
c := "user"
|
|
if isAdmin {
|
|
c = "admin"
|
|
}
|
|
return fmt.Errorf("usage: writefreely --create-%s username:password", c)
|
|
}
|
|
|
|
loadConfig(app)
|
|
connectToDatabase(app)
|
|
defer shutdown(app)
|
|
|
|
// Ensure an admin / first user doesn't already exist
|
|
firstUser, _ := app.db.GetUserByID(1)
|
|
if isAdmin {
|
|
// Abort if trying to create admin user, but one already exists
|
|
if firstUser != nil {
|
|
return fmt.Errorf("Admin user already exists (%s). Create a regular user with: writefreely --create-user", firstUser.Username)
|
|
}
|
|
} else {
|
|
// Abort if trying to create regular user, but no admin exists yet
|
|
if firstUser == nil {
|
|
return fmt.Errorf("No admin user exists yet. Create an admin first with: writefreely --create-admin")
|
|
}
|
|
}
|
|
|
|
// Create the user
|
|
username := creds[0]
|
|
password := creds[1]
|
|
|
|
// Normalize and validate username
|
|
desiredUsername := username
|
|
username = getSlug(username, "")
|
|
|
|
usernameDesc := username
|
|
if username != desiredUsername {
|
|
usernameDesc += " (originally: " + desiredUsername + ")"
|
|
}
|
|
|
|
if !author.IsValidUsername(app.cfg, username) {
|
|
return fmt.Errorf("Username %s is invalid, reserved, or shorter than configured minimum length (%d characters).", usernameDesc, app.cfg.App.MinUsernameLen)
|
|
}
|
|
|
|
// Hash the password
|
|
hashedPass, err := auth.HashPass([]byte(password))
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to hash password: %v", err)
|
|
}
|
|
|
|
u := &User{
|
|
Username: username,
|
|
HashedPass: hashedPass,
|
|
Created: time.Now().Truncate(time.Second).UTC(),
|
|
}
|
|
|
|
userType := "user"
|
|
if isAdmin {
|
|
userType = "admin"
|
|
}
|
|
log.Info("Creating %s %s...", userType, usernameDesc)
|
|
err = app.db.CreateUser(u, desiredUsername)
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to create user: %s", err)
|
|
}
|
|
log.Info("Done!")
|
|
return nil
|
|
}
|
|
|
|
func adminInitDatabase(app *app) error {
|
|
schemaFileName := "schema.sql"
|
|
if app.cfg.Database.Type == driverSQLite {
|
|
schemaFileName = "sqlite.sql"
|
|
}
|
|
|
|
schema, err := Asset(schemaFileName)
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to load schema file: %v", err)
|
|
}
|
|
|
|
tblReg := regexp.MustCompile("CREATE TABLE (IF NOT EXISTS )?`([a-z_]+)`")
|
|
|
|
queries := strings.Split(string(schema), ";\n")
|
|
for _, q := range queries {
|
|
if strings.TrimSpace(q) == "" {
|
|
continue
|
|
}
|
|
parts := tblReg.FindStringSubmatch(q)
|
|
if len(parts) >= 3 {
|
|
log.Info("Creating table %s...", parts[2])
|
|
} else {
|
|
log.Info("Creating table ??? (Weird query) No match in: %v", parts)
|
|
}
|
|
_, err = app.db.Exec(q)
|
|
if err != nil {
|
|
log.Error("%s", err)
|
|
} else {
|
|
log.Info("Created.")
|
|
}
|
|
}
|
|
|
|
// Set up migrations table
|
|
log.Info("Initializing appmigrations table...")
|
|
err = migrations.SetInitialMigrations(migrations.NewDatastore(app.db.DB, app.db.driverName))
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to set initial migrations: %v", err)
|
|
}
|
|
|
|
log.Info("Running migrations...")
|
|
err = migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
|
|
if err != nil {
|
|
return fmt.Errorf("migrate: %s", err)
|
|
}
|
|
|
|
log.Info("Done.")
|
|
return nil
|
|
}
|