Make server out of SS script
This commit is contained in:
2835
assets/css/pico.css
Normal file
2835
assets/css/pico.css
Normal file
File diff suppressed because it is too large
Load Diff
6
assets/efs.go
Normal file
6
assets/efs.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package assets
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed css
|
||||||
|
var Assets embed.FS
|
||||||
2835
assets/pico.css
Normal file
2835
assets/pico.css
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
@@ -19,13 +20,19 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
err := server()
|
||||||
|
assertNoErr(err)
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
file, err := os.Open(*csvPath)
|
file, err := os.Open(*csvPath)
|
||||||
assertNoErr(err)
|
assertNoErr(err)
|
||||||
records, err := csv.NewReader(file).ReadAll()
|
records, err := csv.NewReader(file).ReadAll()
|
||||||
assertNoErr(err)
|
assertNoErr(err)
|
||||||
|
|
||||||
db := openDB(*mysqlConnString)
|
db, err := openDB(*mysqlConnString)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
bobDB := bob.NewDB(db)
|
bobDB := bob.NewDB(db)
|
||||||
|
|
||||||
err = os.RemoveAll("aips-report.csv")
|
err = os.RemoveAll("aips-report.csv")
|
||||||
@@ -37,7 +44,7 @@ func main() {
|
|||||||
reportData := make([][]string, len(records))
|
reportData := make([][]string, len(records))
|
||||||
for idx, record := range records {
|
for idx, record := range records {
|
||||||
q := models.LocationsPackages.Query()
|
q := models.LocationsPackages.Query()
|
||||||
like := fmt.Sprintf("%%%s%%", record[0])
|
like := fmt.Sprintf("%%%s%%", record)
|
||||||
q.Apply(models.SelectWhere.LocationsPackages.CurrentPath.Like(like))
|
q.Apply(models.SelectWhere.LocationsPackages.CurrentPath.Like(like))
|
||||||
res, err := q.All(context.Background(), bobDB)
|
res, err := q.All(context.Background(), bobDB)
|
||||||
assertNoErr(err)
|
assertNoErr(err)
|
||||||
@@ -62,10 +69,10 @@ func assertNoErr(err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func openDB(connStr string) *sql.DB {
|
func openDB(connStr string) (*sql.DB, error) {
|
||||||
db, err := sql.Open("mysql", connStr)
|
db, err := sql.Open("mysql", connStr)
|
||||||
assertNoErr(err)
|
if err != nil {
|
||||||
err = db.Ping()
|
return nil, err
|
||||||
assertNoErr(err)
|
}
|
||||||
return db
|
return db, db.Ping()
|
||||||
}
|
}
|
||||||
|
|||||||
157
cmd/ss/server.go
Normal file
157
cmd/ss/server.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/stephenafamo/bob"
|
||||||
|
"gitlab.artefactual.com/dcosme/am-scripts/assets"
|
||||||
|
"gitlab.artefactual.com/dcosme/am-scripts/database/mcp/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func server() error {
|
||||||
|
fileServer := http.FileServer(http.FS(assets.Assets))
|
||||||
|
http.Handle("/assets/", http.StripPrefix("/assets/", fileServer))
|
||||||
|
state := &State{
|
||||||
|
Theme: ThemeLight,
|
||||||
|
Mysql: &Service{
|
||||||
|
Name: "Mysql",
|
||||||
|
Status: "Disconnected",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
Home(state).Render(w)
|
||||||
|
})
|
||||||
|
http.HandleFunc("/upload", state.uploadInput)
|
||||||
|
http.HandleFunc("/mysql", state.connectDB)
|
||||||
|
http.HandleFunc("/report", state.report)
|
||||||
|
|
||||||
|
slog.Info("listening", "addr", "4001")
|
||||||
|
err := http.ListenAndServe(":4001", http.DefaultServeMux)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *State) report(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mu := sync.Mutex{}
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
err := func() error {
|
||||||
|
if len(state.Input) == 0 && state.Report == nil {
|
||||||
|
return errors.New("need input before creating report")
|
||||||
|
}
|
||||||
|
if state.Mysql.Status != "Connected" {
|
||||||
|
return errors.New("need database online")
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("creating report")
|
||||||
|
for idx, record := range state.Input {
|
||||||
|
q := models.LocationsPackages.Query()
|
||||||
|
like := fmt.Sprintf("%%%s%%", record)
|
||||||
|
q.Apply(models.SelectWhere.LocationsPackages.CurrentPath.Like(like))
|
||||||
|
res, err := q.All(context.Background(), state.db)
|
||||||
|
assertNoErr(err)
|
||||||
|
|
||||||
|
row := []string{record}
|
||||||
|
for _, r := range res {
|
||||||
|
slog.Info("AIP Found: " + r.CurrentPath)
|
||||||
|
row = append(row, r.CurrentPath)
|
||||||
|
}
|
||||||
|
state.Report[idx] = row
|
||||||
|
}
|
||||||
|
|
||||||
|
return Report(state).Render(w)
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
state.Err = err
|
||||||
|
slog.Error(err.Error())
|
||||||
|
Report(state).Render(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *State) connectDB(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := func() error {
|
||||||
|
state.Mysql.Err = nil
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
connString := r.PostFormValue("mysql")
|
||||||
|
slog.Info("connecting mysql", "conn", connString)
|
||||||
|
db, err := openDB(connString)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", connString, err)
|
||||||
|
}
|
||||||
|
state.db = bob.NewDB(db)
|
||||||
|
slog.Info("connected to database")
|
||||||
|
state.Mysql.Status = "Connected"
|
||||||
|
state.Mysql.URL = connString
|
||||||
|
|
||||||
|
return Home(state).Render(w)
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
state.Mysql.Err = err
|
||||||
|
slog.Error(err.Error())
|
||||||
|
Home(state).Render(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *State) uploadInput(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := func() error {
|
||||||
|
type InputFile struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Contents string `json:"contents"`
|
||||||
|
Mime string `json:"mime"`
|
||||||
|
}
|
||||||
|
type UploadPayload struct {
|
||||||
|
InputFile []InputFile `json:"input-file"`
|
||||||
|
}
|
||||||
|
var payload UploadPayload
|
||||||
|
err := bind(&payload, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(payload.InputFile) != 1 {
|
||||||
|
return fmt.Errorf("exactly one file is allowed to be uploaded, got: %d", len(payload.InputFile))
|
||||||
|
}
|
||||||
|
file := payload.InputFile[0]
|
||||||
|
|
||||||
|
raw := file.Contents
|
||||||
|
content, err := base64.StdEncoding.DecodeString(raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
|
||||||
|
slog.Info("file uploaded", "name", file.Name)
|
||||||
|
state.Input = lines
|
||||||
|
state.Report = make([][]string, len(lines))
|
||||||
|
|
||||||
|
return Home(state).Render(w)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(err.Error())
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bind(v any, r *http.Request) error {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
json.Unmarshal(body, v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
164
cmd/ss/ui.go
Normal file
164
cmd/ss/ui.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/stephenafamo/bob"
|
||||||
|
. "maragu.dev/gomponents"
|
||||||
|
ds "maragu.dev/gomponents-datastar"
|
||||||
|
. "maragu.dev/gomponents/components"
|
||||||
|
. "maragu.dev/gomponents/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
Name string
|
||||||
|
URL string
|
||||||
|
Status string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
Input []string
|
||||||
|
Mysql *Service
|
||||||
|
Theme Theme
|
||||||
|
Report [][]string
|
||||||
|
Err error
|
||||||
|
db bob.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type Theme string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ThemeLight = "light"
|
||||||
|
ThemeDark = "dark"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Home(s *State) Node {
|
||||||
|
main := Div(
|
||||||
|
Class("grid"),
|
||||||
|
input(s),
|
||||||
|
mysql(s),
|
||||||
|
)
|
||||||
|
return layout("Home", s, main)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Report(s *State) Node {
|
||||||
|
if s.Report == nil {
|
||||||
|
return layout("Report", s, Div(
|
||||||
|
err(s.Err),
|
||||||
|
Button(Text("Launch script")),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
table := Table(
|
||||||
|
Map(s.Report, func(row []string) Node {
|
||||||
|
return Tr(
|
||||||
|
Map(row, func(cell string) Node {
|
||||||
|
return Td(Text(cell))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return layout("Report", s, table)
|
||||||
|
}
|
||||||
|
|
||||||
|
func input(s *State) Node {
|
||||||
|
if len(s.Input) == 0 {
|
||||||
|
return Article(
|
||||||
|
Header(Text("Input")),
|
||||||
|
P(Text("AIP Names")),
|
||||||
|
Label(
|
||||||
|
Input(
|
||||||
|
Type("file"),
|
||||||
|
Accept(".csv"),
|
||||||
|
ds.Bind("input-file"),
|
||||||
|
),
|
||||||
|
Small(Text("only .csv files accepted - max size 1 MIB")),
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Type("submit"),
|
||||||
|
Text("Upload Input file"),
|
||||||
|
ds.On("click", "@post('/upload')"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return Article(
|
||||||
|
Header(Text(fmt.Sprintf("Input file has %d names", len(s.Input)))),
|
||||||
|
Map(s.Input, func(id string) Node {
|
||||||
|
return P(Text(id))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mysql(s *State) Node {
|
||||||
|
if s.Mysql.Status == "Disconnected" {
|
||||||
|
return Article(
|
||||||
|
Header(Text("Mysql Config")),
|
||||||
|
Form(
|
||||||
|
Action("/mysql"),
|
||||||
|
Method("post"),
|
||||||
|
FieldSet(
|
||||||
|
Role("group"),
|
||||||
|
Input(
|
||||||
|
Type("text"),
|
||||||
|
Name("mysql"),
|
||||||
|
Placeholder("place mysql connection string here"),
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
Type("submit"),
|
||||||
|
Value("connect"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Footer(err(s.Mysql.Err)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Article(
|
||||||
|
Header(Text("Mysql Config")),
|
||||||
|
P(Text(fmt.Sprintf("Mysql Status: %s", s.Mysql.Status))),
|
||||||
|
P(Text(fmt.Sprintf("Mysql URL: %s", s.Mysql.URL))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func err(err error) Node {
|
||||||
|
if err != nil {
|
||||||
|
return P(Text(err.Error()))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func layout(title string, s *State, children ...Node) Node {
|
||||||
|
return HTML5(HTML5Props{
|
||||||
|
Title: "Migrate - " + title,
|
||||||
|
Language: "en",
|
||||||
|
Head: []Node{
|
||||||
|
Link(Rel("stylesheet"), Href("/assets/css/pico.css")),
|
||||||
|
Script(Type("module"), Src("https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.7/bundles/datastar.js")),
|
||||||
|
},
|
||||||
|
Body: []Node{
|
||||||
|
Class("container"),
|
||||||
|
Header(
|
||||||
|
H1(Text("AIPs Report")),
|
||||||
|
Nav(
|
||||||
|
Ul(
|
||||||
|
navElement("/", "Config"),
|
||||||
|
navElement("/report", "Report"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Main(Group(children)),
|
||||||
|
Footer(),
|
||||||
|
},
|
||||||
|
HTMLAttrs: []Node{
|
||||||
|
Data("theme", string(s.Theme)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func navElement(path, name string) Node {
|
||||||
|
return Li(A(Href(path), Text(name)))
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
@@ -5,6 +5,8 @@ go 1.26.1
|
|||||||
require (
|
require (
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/stephenafamo/bob v0.42.0
|
github.com/stephenafamo/bob v0.42.0
|
||||||
|
maragu.dev/gomponents v1.2.0
|
||||||
|
maragu.dev/gomponents-datastar v0.3.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -200,5 +200,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
|||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
maragu.dev/gomponents v1.2.0 h1:H7/N5htz1GCnhu0HB1GasluWeU2rJZOYztVEyN61iTc=
|
||||||
|
maragu.dev/gomponents v1.2.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
|
||||||
|
maragu.dev/gomponents-datastar v0.3.3 h1:vGPoEy61xbdBgndfOsde0ILHhw8hL1nwgzNNYScxRFU=
|
||||||
|
maragu.dev/gomponents-datastar v0.3.3/go.mod h1:cx6Bx+DRz9nAEfrSW1yLZAI1mcM/uvXmJCbe3a66J/I=
|
||||||
mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
|
mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
|
||||||
mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
|
mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
|
||||||
|
|||||||
Reference in New Issue
Block a user