// Package console holds the FAQ plugin's commands: faq:import-winter, a copy // of a WinterCMS site's FAQs. package console import ( "bufio" "bytes" "context" "encoding/json" "fmt" "os" "strconv" "strings" "time" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/lagoon" "gorm.io/gorm" ) func flagValue(in bonfire.Input, name string) string { v, ok := in.Flag(name) if !ok { return "" } return strings.TrimSpace(v) } func flagOn(in bonfire.Input, name string) bool { switch strings.ToLower(flagValue(in, name)) { case "true", "1", "yes": return true } return false } // lookupDB returns the app database. The generated main.go publishes no // database for plugin commands, so when none is published it opens // database.dsn and publishes it the way lagoon's migrate does. The returned // func closes a pool this call opened; it is a no-op otherwise. func lookupDB(ctx context.Context, app *backpack.App) (*gorm.DB, func(), error) { if app == nil { return nil, nil, fmt.Errorf("golem15.faq: app is nil") } if db, ok := app.Lookup[*gorm.DB](); ok && db != nil { return db, func() {}, nil } if lagoon.DSN(app.Config) == "" { return nil, nil, fmt.Errorf("golem15.faq: database is not configured (set SUMMER_DATABASE__DSN)") } sqlDB, gdb, err := lagoon.OpenFromApp(ctx, app) if err != nil { return nil, nil, err } if err := lagoon.Publish(app, sqlDB, gdb); err != nil { _ = sqlDB.Close() return nil, nil, err } return gdb, func() { _ = sqlDB.Close() }, nil } // readJSONLines decodes one JSON object per non-blank line of file. func readJSONLines(file string) ([]map[string]json.RawMessage, error) { f, err := os.Open(file) if err != nil { return nil, fmt.Errorf("File not found: %s", file) } defer f.Close() var rows []map[string]json.RawMessage scanner := bufio.NewScanner(f) scanner.Buffer(make([]byte, 1<<20), 64<<20) n := 0 for scanner.Scan() { n++ line := bytes.TrimSpace(scanner.Bytes()) if len(line) == 0 { continue } var row map[string]json.RawMessage if err := json.Unmarshal(line, &row); err != nil { return nil, fmt.Errorf("%s line %d: %w", file, n, err) } rows = append(rows, row) } if err := scanner.Err(); err != nil { return nil, err } return rows, nil } // isNull reports a missing or null JSON value. func isNull(raw json.RawMessage) bool { t := bytes.TrimSpace(raw) return len(t) == 0 || bytes.Equal(t, []byte("null")) } // jsonText reads a string, number or boolean as text; nil for null. func jsonText(raw json.RawMessage) (*string, error) { if isNull(raw) { return nil, nil } var v any if err := json.Unmarshal(raw, &v); err != nil { return nil, err } var s string switch x := v.(type) { case string: s = x case float64: s = strconv.FormatFloat(x, 'f', -1, 64) case bool: s = strconv.FormatBool(x) default: return nil, fmt.Errorf("want text, got %s", raw) } return &s, nil } // jsonUint reads a positive integer given as a number or a string; 0 for // null. func jsonUint(raw json.RawMessage) (uint, error) { s, err := jsonText(raw) if err != nil || s == nil || strings.TrimSpace(*s) == "" { return 0, err } n, err := strconv.ParseUint(strings.TrimSpace(*s), 10, 32) if err != nil { return 0, fmt.Errorf("want an id, got %s", raw) } return uint(n), nil } // jsonBool reads MySQL's 0 and 1, a JSON boolean or "true"/"false"; def for // null. func jsonBool(raw json.RawMessage, def bool) (bool, error) { s, err := jsonText(raw) if err != nil || s == nil { return def, err } switch strings.ToLower(strings.TrimSpace(*s)) { case "1", "true": return true, nil case "0", "false", "": return false, nil } return false, fmt.Errorf("want a boolean, got %s", raw) } // jsonTime reads a MySQL DATETIME text (with or without fractions) or an // RFC 3339 time as UTC; nil for null. func jsonTime(raw json.RawMessage) (*time.Time, error) { s, err := jsonText(raw) if err != nil || s == nil || strings.TrimSpace(*s) == "" { return nil, err } text := strings.TrimSpace(*s) for _, layout := range []string{"2006-01-02 15:04:05.999999", "2006-01-02 15:04:05", time.RFC3339Nano} { if t, err := time.ParseInLocation(layout, text, time.UTC); err == nil { t = t.UTC() return &t, nil } } return nil, fmt.Errorf("want a date and time, got %s", raw) }