listmonk/cmd/install.go

240 lines
7 KiB
Go
Raw Normal View History

2018-10-25 13:51:47 +00:00
package main
import (
"encoding/json"
2018-10-25 13:51:47 +00:00
"fmt"
"io/ioutil"
"os"
"regexp"
"strings"
2018-10-25 13:51:47 +00:00
2020-03-07 15:07:48 +00:00
"github.com/gofrs/uuid"
2018-10-25 13:51:47 +00:00
"github.com/jmoiron/sqlx"
"github.com/knadh/listmonk/models"
"github.com/knadh/stuffbin"
"github.com/lib/pq"
2018-10-25 13:51:47 +00:00
)
// install runs the first time setup of creating and
// migrating the database and creating the super user.
func install(lastVer string, db *sqlx.DB, fs stuffbin.FileSystem, prompt, idempotent bool) {
qMap := readQueries(queryFilePath, db, fs)
fmt.Println("")
if !idempotent {
fmt.Println("** first time installation **")
fmt.Printf("** IMPORTANT: This will wipe existing listmonk tables and types in the DB '%s' **",
ko.String("db.database"))
} else {
fmt.Println("** first time (idempotent) installation **")
}
fmt.Println("")
2018-10-25 13:51:47 +00:00
2019-11-30 11:25:14 +00:00
if prompt {
var ok string
fmt.Print("continue (y/N)? ")
2019-11-30 11:25:14 +00:00
if _, err := fmt.Scanf("%s", &ok); err != nil {
lo.Fatalf("error reading value from terminal: %v", err)
2019-11-30 11:25:14 +00:00
}
if strings.ToLower(ok) != "y" {
fmt.Println("install cancelled.")
2019-11-30 11:25:14 +00:00
return
}
2018-10-25 13:51:47 +00:00
}
// If idempotence is on, check if the DB is already setup.
if idempotent {
if _, err := db.Exec("SELECT count(*) FROM settings"); err != nil {
// If "settings" doesn't exist, assume it's a fresh install.
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code != "42P01" {
lo.Fatalf("error checking existing DB schema: %v", err)
}
} else {
lo.Println("skipping install as database appears to be already setup")
os.Exit(0)
}
}
2018-10-25 13:51:47 +00:00
// Migrate the tables.
if err := installSchema(lastVer, db, fs); err != nil {
lo.Fatalf("error migrating DB schema: %v", err)
2018-10-25 13:51:47 +00:00
}
// Load the queries.
q := prepareQueries(qMap, db, ko)
2018-10-25 13:51:47 +00:00
// Sample list.
var (
defList int
optinList int
)
if err := q.CreateList.Get(&defList,
2020-03-07 15:07:48 +00:00
uuid.Must(uuid.NewV4()),
2018-10-25 13:51:47 +00:00
"Default list",
models.ListTypePrivate,
models.ListOptinSingle,
2018-10-25 13:51:47 +00:00
pq.StringArray{"test"},
"",
2018-10-25 13:51:47 +00:00
); err != nil {
lo.Fatalf("error creating list: %v", err)
2018-10-25 13:51:47 +00:00
}
2020-03-07 15:07:48 +00:00
if err := q.CreateList.Get(&optinList, uuid.Must(uuid.NewV4()),
"Opt-in list",
models.ListTypePublic,
models.ListOptinDouble,
pq.StringArray{"test"},
"",
); err != nil {
lo.Fatalf("error creating list: %v", err)
}
2018-10-25 13:51:47 +00:00
// Sample subscriber.
if _, err := q.UpsertSubscriber.Exec(
2020-03-07 15:07:48 +00:00
uuid.Must(uuid.NewV4()),
"john@example.com",
"John Doe",
`{"type": "known", "good": true, "city": "Bengaluru"}`,
pq.Int64Array{int64(defList)},
models.SubscriptionStatusUnconfirmed,
true); err != nil {
lo.Fatalf("Error creating subscriber: %v", err)
}
if _, err := q.UpsertSubscriber.Exec(
2020-03-07 15:07:48 +00:00
uuid.Must(uuid.NewV4()),
"anon@example.com",
"Anon Doe",
`{"type": "unknown", "good": true, "city": "Bengaluru"}`,
pq.Int64Array{int64(optinList)},
models.SubscriptionStatusUnconfirmed,
true); err != nil {
lo.Fatalf("error creating subscriber: %v", err)
2018-10-25 13:51:47 +00:00
}
// Default campaign template.
campTpl, err := fs.Get("/static/email-templates/default.tpl")
2018-10-25 13:51:47 +00:00
if err != nil {
lo.Fatalf("error reading default e-mail template: %v", err)
2018-10-25 13:51:47 +00:00
}
var campTplID int
if err := q.CreateTemplate.Get(&campTplID, "Default campaign template", models.TemplateTypeCampaign, "", campTpl.ReadBytes()); err != nil {
lo.Fatalf("error creating default campaign template: %v", err)
2018-10-25 13:51:47 +00:00
}
if _, err := q.SetDefaultTemplate.Exec(campTplID); err != nil {
lo.Fatalf("error setting default template: %v", err)
}
2022-12-25 10:37:12 +00:00
// Default campaign archive template.
archiveTpl, err := fs.Get("/static/email-templates/default-archive.tpl")
if err != nil {
lo.Fatalf("error reading default archive template: %v", err)
}
var archiveTplID int
if err := q.CreateTemplate.Get(&archiveTplID, "Default archive template", models.TemplateTypeCampaign, "", archiveTpl.ReadBytes()); err != nil {
lo.Fatalf("error creating default campaign template: %v", err)
}
// Sample campaign.
2020-03-07 15:07:48 +00:00
if _, err := q.CreateCampaign.Exec(uuid.Must(uuid.NewV4()),
models.CampaignTypeRegular,
"Test campaign",
"Welcome to listmonk",
"No Reply <noreply@yoursite.com>",
`<h3>Hi {{ .Subscriber.FirstName }}!</h3>
<p>This is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.</p>
<p>Here is a <a href="https://listmonk.app@TrackLink">tracked link</a>.</p>
<p>Use the link icon in the editor toolbar or when writing raw HTML or Markdown,
simply suffix @TrackLink to the end of a URL to turn it into a tracking link. Example:</p>
<pre>&lt;a href=&quot;https:/&zwnj;/listmonk.app&#064;TrackLink&quot;&gt;&lt;/a&gt;</pre>
<p>For help, refer to the <a href="https://listmonk.app/docs">documentation</a>.</p>
`,
nil,
"richtext",
2020-08-01 11:15:29 +00:00
nil,
json.RawMessage("[]"),
pq.StringArray{"test-campaign"},
emailMsgr,
campTplID,
pq.Int64Array{1},
false,
2022-12-25 10:37:12 +00:00
archiveTplID,
`{"name": "Subscriber"}`,
nil,
); err != nil {
lo.Fatalf("error creating sample campaign: %v", err)
2018-10-25 13:51:47 +00:00
}
// Sample tx template.
txTpl, err := fs.Get("/static/email-templates/sample-tx.tpl")
if err != nil {
lo.Fatalf("error reading default e-mail template: %v", err)
}
if _, err := q.CreateTemplate.Exec("Sample transactional template", models.TemplateTypeTx, "Welcome {{ .Subscriber.Name }}", txTpl.ReadBytes()); err != nil {
lo.Fatalf("error creating sample transactional template: %v", err)
}
lo.Printf("setup complete")
lo.Printf(`run the program and access the dashboard at %s`, ko.MustString("app.address"))
2018-10-25 13:51:47 +00:00
}
// installSchema executes the SQL schema and creates the necessary tables and types.
func installSchema(curVer string, db *sqlx.DB, fs stuffbin.FileSystem) error {
q, err := fs.Read("/schema.sql")
2018-10-25 13:51:47 +00:00
if err != nil {
return err
}
if _, err := db.Exec(string(q)); err != nil {
2018-10-25 13:51:47 +00:00
return err
}
// Insert the current migration version.
return recordMigrationVersion(curVer, db)
}
// recordMigrationVersion inserts the given version (of DB migration) into the
// `migrations` array in the settings table.
func recordMigrationVersion(ver string, db *sqlx.DB) error {
_, err := db.Exec(fmt.Sprintf(`INSERT INTO settings (key, value)
VALUES('migrations', '["%s"]'::JSONB)
ON CONFLICT (key) DO UPDATE SET value = settings.value || EXCLUDED.value`, ver))
return err
2018-10-25 13:51:47 +00:00
}
func newConfigFile(path string) error {
if _, err := os.Stat(path); !os.IsNotExist(err) {
return fmt.Errorf("%s exists. Remove it to generate a new one.", path)
}
// Initialize the static file system into which all
// required static assets (.sql, .js files etc.) are loaded.
fs := initFS(appDir, "", "", "")
b, err := fs.Read("config.toml.sample")
if err != nil {
return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err)
}
// Generate a random admin password.
pwd, err := generateRandomString(16)
if err == nil {
b = regexp.MustCompile(`admin_password\s+?=\s+?(.*)`).
ReplaceAll(b, []byte(fmt.Sprintf(`admin_password = "%s"`, pwd)))
}
return ioutil.WriteFile(path, b, 0644)
}
2020-11-10 16:51:25 +00:00
// checkSchema checks if the DB schema is installed.
func checkSchema(db *sqlx.DB) (bool, error) {
if _, err := db.Exec(`SELECT id FROM templates LIMIT 1`); err != nil {
if isTableNotExistErr(err) {
return false, nil
}
return false, err
}
return true, nil
}