ags-upload

Insert AGS files to a database
git clone git://src.adamsgaard.dk/ags-upload # fast
git clone https://src.adamsgaard.dk/ags-upload.git # slow
Log | Files | Refs Back to index

commit d7aff58399bcb2d8e0573c9e027229bb9d0fd3f3
parent 12b840e26e1e7437fea2415dc2794eb9a1c932cd
Author: Anders Damsgaard <anders@adamsgaard.dk>
Date:   Wed,  8 Oct 2025 11:40:06 +0200

main.go: parse cpt data

Diffstat:
Mcmd/main.go | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
1 file changed, 123 insertions(+), 48 deletions(-)

diff --git a/cmd/main.go b/cmd/main.go @@ -9,7 +9,8 @@ import ( "net/http" "os" "strings" - "time" + "strconv" + //"time" "github.com/gin-gonic/gin" "gorm.io/driver/postgres" @@ -26,11 +27,24 @@ type CptInfo struct { Contractor string // PROJ_CONT } -func ParseAGS(r io.Reader) (*CptInfo, error) { +type Cpt struct { // group SCPG - data + ID uint `gorm:"primaryKey"` + InfoId uint //foreign key from CptInfo + LocationId string // LOCA_ID + TestReference string // SCPG_TESN + Depth float64 // SCPT_DPTH + ConeRes float64 // SCPT_RES + SideFric float64 // SCPT_FRES + Pore1 float64 // SCPT_PWP1 + Pore2 float64 // SCPT_PWP2 + Pore3 float64 // SCPT_PWP3 + FrictionRatio float64 // SCPT_FRR +} +func ParseAGSProjectAndSCPT(r io.Reader) (*CptInfo, []Cpt, error) { norm, err := dos2unix(r) if err != nil { - return nil, fmt.Errorf("read: %w", err) + return nil, nil, fmt.Errorf("read: %w", err) } cr := csv.NewReader(norm) @@ -38,69 +52,113 @@ func ParseAGS(r io.Reader) (*CptInfo, error) { cr.LazyQuotes = true var ( - inPROJ bool - headerIndex map[string]int + curGroup string + headersByGrp = map[string]map[string]int{} // GROUP -> header index map + project *CptInfo + cpts []Cpt ) + get := func(group string, data []string, name string) string { + hm := headersByGrp[group] + if hm == nil { + return "" + } + if idx, ok := hm[strings.ToUpper(name)]; ok && idx >= 0 && idx < len(data) { + return data[idx] + } + return "" + } + parseF64 := func(s string) float64 { + if s == "" { + return 0 + } + // Optional: handle decimal commas + s = strings.ReplaceAll(s, ",", ".") + f, _ := strconv.ParseFloat(s, 64) + return f + } + for { rec, err := cr.Read() if err == io.EOF { break } if err != nil { - return nil, fmt.Errorf("csv: %w", err) + return nil, nil, fmt.Errorf("csv: %w", err) } if len(rec) == 0 { continue } - for i := range rec { rec[i] = strings.TrimSpace(rec[i]) + // Some exporters put empty quotes => "" — leave as empty string } - switch strings.ToUpper(rec[0]) { + tag := strings.ToUpper(rec[0]) + switch tag { case "GROUP": - inPROJ = len(rec) > 1 && strings.EqualFold(rec[1], "PROJ") - headerIndex = nil + if len(rec) > 1 { + curGroup = strings.ToUpper(strings.TrimSpace(rec[1])) + } else { + curGroup = "" + } case "HEADING": - if !inPROJ { + if curGroup == "" { continue } - headerIndex = make(map[string]int) + m := make(map[string]int, len(rec)-1) for i := 1; i < len(rec); i++ { key := strings.ToUpper(strings.TrimSpace(rec[i])) - headerIndex[key] = i - 1 // positions in the "DATA" slice after skipping the first token + m[key] = i - 1 // position in DATA after skipping tag } + headersByGrp[curGroup] = m + case "DATA": - if !inPROJ || headerIndex == nil { + if curGroup == "" { continue } - data := rec[1:] // align with headerIndex positions + data := rec[1:] - get := func(h string) string { - if idx, ok := headerIndex[strings.ToUpper(h)]; ok && idx >= 0 && idx < len(data) { - return data[idx] + switch curGroup { + case "PROJ": + if project != nil { + // If multiple PROJ rows exist, keep the first (typical). + continue + } + project = &CptInfo{ + SourceId: get("PROJ", data, "PROJ_ID"), + Name: get("PROJ", data, "PROJ_NAME"), + Location: get("PROJ", data, "PROJ_LOC"), + Client: get("PROJ", data, "PROJ_CLNT"), + Contractor: get("PROJ", data, "PROJ_CONT"), } - return "" - } - p := &CptInfo{ - SourceId: get("PROJ_ID"), - Name: get("PROJ_NAME"), - Location: get("PROJ_LOC"), - Client: get("PROJ_CLNT"), - Contractor: get("PROJ_CONT"), + case "SCPT": + cpts = append(cpts, Cpt{ + LocationId: get("SCPT", data, "LOCA_ID"), + TestReference: get("SCPT", data, "SCPG_TESN"), + Depth: parseF64(get("SCPT", data, "SCPT_DPTH")), + ConeRes: parseF64(get("SCPT", data, "SCPT_RES")), + SideFric: parseF64(get("SCPT", data, "SCPT_FRES")), + Pore1: parseF64(get("SCPT", data, "SCPT_PWP1")), + Pore2: parseF64(get("SCPT", data, "SCPT_PWP2")), + Pore3: parseF64(get("SCPT", data, "SCPT_PWP3")), + FrictionRatio: parseF64(get("SCPT", data, "SCPT_FRR")), + }) + default: + // ignore other groups for now } - return p, nil + // ignore UNIT, TYPE, etc. default: continue } } - return nil, fmt.Errorf("no data found") + return project, cpts, nil } + func dos2unix(r io.Reader) (io.Reader, error) { all, err := io.ReadAll(r) if err != nil { @@ -135,44 +193,61 @@ func main() { log.Fatal(err) } + if err := db.AutoMigrate(&Cpt{}); err != nil { + log.Fatal(err) + } + r := gin.Default() // ~32 MB file cap for multipart r.MaxMultipartMemory = 32 << 20 r.POST("/ingest/ags", func(c *gin.Context) { - reader, cleanup, err := getAGSReader(c.Request) + file, _, err := c.Request.FormFile("file") if err != nil { - c.String(http.StatusBadRequest, "upload error: %v", err) + c.String(400, "missing multipart file: %v", err) return } - if cleanup != nil { - defer cleanup() - } + defer file.Close() - p, err := ParseAGS(reader) + proj, cpts, err := ParseAGSProjectAndSCPT(file) if err != nil { - c.String(http.StatusBadRequest, "parse error: %v", err) + c.String(400, "parse error: %v", err) return } - err = db. - Where("source_id = ?", p.SourceId). - Assign(p). - FirstOrCreate(p).Error + err = db.Transaction(func(tx *gorm.DB) error { + + // Upsert project by SourceId (make SourceId unique if you rely on it) + if proj != nil { + if err := tx. + Where("source_id = ?", proj.SourceId). + Assign(proj). + FirstOrCreate(proj).Error; err != nil { + return err + } + } + + // If you later derive InfoId from a SC* info table, set it here before insert. + if len(cpts) > 0 { + // Optional: add a foreign key to project if you want (e.g., ProjectID) + // for i := range cpts { cpts[i].ProjectID = proj.ID } + + if err := tx.CreateInBatches(cpts, 2000).Error; err != nil { + return err + } + } + + return nil + }) if err != nil { - c.String(http.StatusInternalServerError, "db error: %v", err) + c.String(500, "db error: %v", err) return } - c.JSON(http.StatusCreated, gin.H{ - "id": p.ID, - "sourceId": p.SourceId, - "name": p.Name, - "location": p.Location, - "client": p.Client, - "contractor": p.Contractor, - "savedAt": time.Now().Format(time.RFC3339), + c.JSON(201, gin.H{ + "project": proj, + "cpts": len(cpts), }) })