- SQL injection protection with regex and prepared statements - Handled new website - Refactored ws - CSV export handle - modified HTML, CSS and JS
This commit is contained in:
parent
be699390b6
commit
4239c1dbb1
@ -23,12 +23,13 @@ db_name="database"
|
||||
db_username="username"
|
||||
db_password="password"
|
||||
db_table="qrz_test"
|
||||
cron="@every 1h"
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
./qrz -configfile qrz.ini
|
||||
./qrz -configfile qrz.ini -port 8080
|
||||
```
|
||||
|
||||
## License
|
||||
|
@ -11,9 +11,12 @@ import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
var version string
|
||||
|
||||
func main() {
|
||||
var config config.Config
|
||||
config.GetConfig()
|
||||
config.Version = version
|
||||
|
||||
err := database.Initialize(&config)
|
||||
defer config.Db.Close()
|
||||
|
@ -4,5 +4,4 @@ db_name="database"
|
||||
db_username="username"
|
||||
db_password="password"
|
||||
db_table="qrz_test"
|
||||
port=8080
|
||||
cron="@every 1h"
|
@ -12,10 +12,14 @@ import (
|
||||
// GetConfig fetch configuration
|
||||
func (config *Config) GetConfig() error {
|
||||
var configfile string
|
||||
var nofeed bool
|
||||
var port int
|
||||
|
||||
flag.Usage = utils.Usage
|
||||
|
||||
flag.StringVar(&configfile, "configfile", "qrz.ini", "config file to use with qrz section")
|
||||
flag.IntVar(&port, "port", 8080, "web port to use")
|
||||
flag.BoolVar(&nofeed, "nofeed", false, "no feed database table with entries at first launch")
|
||||
flag.Parse()
|
||||
|
||||
cfg, err := ini.Load(configfile)
|
||||
@ -25,7 +29,8 @@ func (config *Config) GetConfig() error {
|
||||
|
||||
qrzsection := cfg.Section("qrz")
|
||||
|
||||
config.Port = qrzsection.Key("port").MustInt(8080)
|
||||
config.Port = port
|
||||
config.NoFeed = nofeed
|
||||
config.DbHostname = qrzsection.Key("db_hostname").MustString("localhost")
|
||||
config.DbName = qrzsection.Key("db_name").MustString("database")
|
||||
config.DbUsername = qrzsection.Key("db_username").MustString("username")
|
||||
@ -34,37 +39,69 @@ func (config *Config) GetConfig() error {
|
||||
config.DbTable = qrzsection.Key("db_table").MustString("qrz")
|
||||
config.Cron = qrzsection.Key("cron").MustString("@every 1h")
|
||||
|
||||
config.DbSchema = fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s` (`id` int(8) NOT NULL AUTO_INCREMENT, `qrz` varchar(25) NOT NULL, `name` varchar(25) DEFAULT NULL, `address` varchar(50) DEFAULT NULL, `city` varchar(50) DEFAULT NULL, `zipcode` varchar(5) DEFAULT NULL, `dept` varchar(50) DEFAULT NULL, `country` varchar(25) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `qrz` (`qrz`,`name`,`city`,`dept`) USING BTREE, KEY `test` (`country`), FULLTEXT KEY `city` (`city`), FULLTEXT KEY `dept` (`dept`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;", config.DbTable)
|
||||
config.DbSchema = fmt.Sprintf(`
|
||||
CREATE TABLE IF NOT EXISTS %s
|
||||
(
|
||||
id int(8) NOT NULL AUTO_INCREMENT,
|
||||
qrz varchar(25) NOT NULL,
|
||||
name varchar(25) DEFAULT NULL,
|
||||
address varchar(50) DEFAULT NULL,
|
||||
city varchar(50) DEFAULT NULL,
|
||||
zipcode varchar(5) DEFAULT NULL,
|
||||
dept varchar(50) DEFAULT NULL,
|
||||
country varchar(25) DEFAULT NULL,
|
||||
dmrid varchar(25) DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY qrz (qrz,name,city,dept) USING BTREE,
|
||||
KEY test (country),
|
||||
FULLTEXT KEY city (city),
|
||||
FULLTEXT KEY dept (dept)
|
||||
)
|
||||
ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
|
||||
config.DbTable)
|
||||
|
||||
config.Statements.DbInsert = "INSERT IGNORE INTO %s (qrz,name,city,dept,country) VALUES ('%s','%s','%s','%s','%s');"
|
||||
config.DbStatements.Insert = fmt.Sprintf(
|
||||
`INSERT IGNORE INTO %s (qrz, dmrid, name, city, dept, country)
|
||||
VALUES (?,?,?,?,?,?);`,
|
||||
config.DbTable)
|
||||
config.DbStatements.ExportCSV = fmt.Sprintf(
|
||||
`SELECT qrz, name, city, dept, country
|
||||
FROM %s;`,
|
||||
config.DbTable)
|
||||
|
||||
config.Statements.DbGetQrz = "SELECT qrz FROM %s;"
|
||||
config.DbStatements.Countries = fmt.Sprintf(
|
||||
`SELECT country
|
||||
FROM %s
|
||||
GROUP BY country;`,
|
||||
config.DbTable)
|
||||
|
||||
config.Statements.DbCheck = "SELECT COUNT(*) FROM %s WHERE qrz = '%s';"
|
||||
|
||||
config.URLBase = "http://groupe-frs.hamstation.eu/index_qrz_liste_%s.htm"
|
||||
config.URLBase = `http://groupe-frs.hamstation.eu/index_qrz_liste_%s.php`
|
||||
|
||||
config.QrzGroups = []string{"01", "03", "09", "103", "104", "107", "119", "13", "14", "146", "147", "15", "156", "16", "161", "163", "18", "188", "2", "214", "233", "25", "26", "29", "30", "31", "32", "34", "43", "44", "49", "54", "64", "66", "76", "79", "84", "97", "98"}
|
||||
//config.QrzGroups = map[string]string{"France": "14"}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config is the global config of g2g
|
||||
// Config is the global config of qrz
|
||||
type Config struct {
|
||||
Port int
|
||||
Db *sqlx.DB
|
||||
|
||||
DbHostname string
|
||||
DbName string
|
||||
DbUsername string
|
||||
DbPassword string
|
||||
DbSchema string
|
||||
DbTable string
|
||||
Statements struct {
|
||||
DbInsert string
|
||||
DbGetQrz string
|
||||
DbCheck string
|
||||
DbStatements struct {
|
||||
Insert string
|
||||
ExportCSV string
|
||||
Countries string
|
||||
}
|
||||
URLBase string
|
||||
QrzGroups []string
|
||||
Cron string
|
||||
Db *sqlx.DB
|
||||
Port int
|
||||
NoFeed bool
|
||||
Version string
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package qrz
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@ -11,7 +12,6 @@ import (
|
||||
"git.paulbsd.com/paulbsd/qrz/src/config"
|
||||
"github.com/antchfx/htmlquery"
|
||||
"github.com/robfig/cron"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
)
|
||||
|
||||
// InitCronConfig create task schedules
|
||||
@ -74,12 +74,13 @@ func getBody(url string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
/*
|
||||
// For old website support 23/05/2020
|
||||
dec := charmap.ISO8859_1.NewDecoder()
|
||||
output, _ := dec.Bytes(body)
|
||||
bodyString := string(output)
|
||||
|
||||
return bodyString, nil
|
||||
*/
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// getFrsEntries get FRS entries from html body
|
||||
@ -94,14 +95,25 @@ func getFrsEntries(config config.Config, body string) (frsentries map[string]Frs
|
||||
|
||||
for _, n := range htmlquery.Find(htmlpage, "//tr") {
|
||||
td := htmlquery.Find(n, "//td")
|
||||
if re.MatchString(htmlquery.InnerText(td[0])) {
|
||||
if len(td) > 2 {
|
||||
if re.MatchString(htmlquery.InnerText(td[1])) {
|
||||
frs := FrsEntry{
|
||||
QRZ: strings.Replace(htmlquery.InnerText(td[0]), "'", "\\'", -1),
|
||||
Name: strings.Replace(htmlquery.InnerText(td[1]), "'", "\\'", -1),
|
||||
City: strings.Replace(htmlquery.InnerText(td[2]), "'", "\\'", -1),
|
||||
Dept: strings.Replace(htmlquery.InnerText(td[3]), "'", "\\'", -1),
|
||||
Country: strings.Replace(htmlquery.InnerText(td[4]), "'", "\\'", -1)}
|
||||
QRZ: strings.TrimLeft(htmlquery.InnerText(td[1]), " "),
|
||||
DMRID: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[0]), " "), Valid: true},
|
||||
Name: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[2]), " "), Valid: true},
|
||||
City: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[3]), " "), Valid: true},
|
||||
Dept: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[4]), " "), Valid: true},
|
||||
Country: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[5]), " "), Valid: true}}
|
||||
frsentries[frs.QRZ] = frs
|
||||
} else if re.MatchString(htmlquery.InnerText(td[0])) {
|
||||
frs := FrsEntry{
|
||||
QRZ: strings.TrimLeft(htmlquery.InnerText(td[0]), " "),
|
||||
Name: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[1]), " "), Valid: true},
|
||||
City: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[2]), " "), Valid: true},
|
||||
Dept: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[3]), " "), Valid: true},
|
||||
Country: sql.NullString{String: strings.TrimLeft(htmlquery.InnerText(td[4]), " "), Valid: true}}
|
||||
frsentries[frs.QRZ] = frs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,24 +121,16 @@ func getFrsEntries(config config.Config, body string) (frsentries map[string]Frs
|
||||
}
|
||||
|
||||
// getCurrentEntries fetch existing entries from database
|
||||
func getCurrentEntries(config config.Config) (existingQRZ []string, err error) {
|
||||
q := fmt.Sprintf(config.Statements.DbGetQrz, config.DbTable)
|
||||
rows, err := config.Db.Query(q)
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var i string
|
||||
rows.Scan(&i)
|
||||
existingQRZ = append(existingQRZ, i)
|
||||
}
|
||||
func getCurrentEntries(config config.Config) (existingQRZ []FrsEntry, err error) {
|
||||
err = config.Db.Select(&existingQRZ, fmt.Sprintf("SELECT * FROM %s;", config.DbTable))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// discardExistingEntries remove existing entries from original map[string]FrsEntry
|
||||
func discardExistingEntries(config config.Config, frsPeople *map[string]FrsEntry, existingQRZ []string) (err error) {
|
||||
func discardExistingEntries(config config.Config, frsPeople *map[string]FrsEntry, existingQRZ []FrsEntry) (err error) {
|
||||
for _, entry := range existingQRZ {
|
||||
delete(*frsPeople, entry)
|
||||
delete(*frsPeople, entry.QRZ)
|
||||
}
|
||||
|
||||
return
|
||||
@ -139,9 +143,8 @@ func insertFrsEntryToDB(config config.Config, frsPeople map[string]FrsEntry) (er
|
||||
|
||||
fmt.Println(fmt.Sprintf("Starting inserts of %d entries", len(frsPeople)))
|
||||
|
||||
for _, j := range frsPeople {
|
||||
query := fmt.Sprintf(config.Statements.DbInsert, config.DbTable, j.QRZ, j.Name, j.City, j.Dept, j.Country)
|
||||
tx.MustExec(query)
|
||||
for _, frs := range frsPeople {
|
||||
tx.MustExec(config.DbStatements.Insert, frs.QRZ, frs.DMRID, frs.Name, frs.City, frs.Dept, frs.Country)
|
||||
qrzNum++
|
||||
}
|
||||
|
||||
@ -156,9 +159,13 @@ func insertFrsEntryToDB(config config.Config, frsPeople map[string]FrsEntry) (er
|
||||
|
||||
// FrsEntry describe FRS people
|
||||
type FrsEntry struct {
|
||||
ID int `db:"id"`
|
||||
QRZ string `db:"qrz"`
|
||||
Name string `db:"name"`
|
||||
City string `db:"city"`
|
||||
Dept string `db:"dept"`
|
||||
Country string `db:"country"`
|
||||
DMRID sql.NullString `db:"dmrid"`
|
||||
Name sql.NullString `db:"name"`
|
||||
Address sql.NullString `db:"address"`
|
||||
City sql.NullString `db:"city"`
|
||||
ZipCode sql.NullString `db:"zipcode"`
|
||||
Dept sql.NullString `db:"dept"`
|
||||
Country sql.NullString `db:"country"`
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.paulbsd.com/paulbsd/qrz/src/config"
|
||||
@ -12,6 +13,7 @@ import (
|
||||
"git.paulbsd.com/paulbsd/qrz/src/static"
|
||||
"git.paulbsd.com/paulbsd/qrz/src/templates"
|
||||
"github.com/gobuffalo/packr/v2"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
@ -35,12 +37,21 @@ func RunServer(config config.Config) (err error) {
|
||||
e.POST("/qrzws", func(c echo.Context) (err error) {
|
||||
res, err := Run(c, config)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
return c.String(http.StatusInternalServerError, "Erreur 500 ta mère")
|
||||
}
|
||||
return c.JSON(http.StatusOK, res)
|
||||
})
|
||||
e.GET("/export_frs.csv", func(c echo.Context) (err error) {
|
||||
data, mime, err := RunCSVExport(c, config)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
return c.Blob(http.StatusOK, mime, data)
|
||||
})
|
||||
|
||||
if !config.NoFeed {
|
||||
go qrz.Run(config)
|
||||
}
|
||||
e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", config.Port)))
|
||||
return
|
||||
}
|
||||
@ -49,15 +60,16 @@ func RunServer(config config.Config) (err error) {
|
||||
func Run(c echo.Context, config config.Config) (res QrzDatatableOutput, err error) {
|
||||
qrzinputjson := new(QrzDatatableInput)
|
||||
var count int
|
||||
var countfiltered int
|
||||
var counttotal int
|
||||
|
||||
if err = c.Bind(qrzinputjson); err != nil {
|
||||
return
|
||||
}
|
||||
query, querycountfiltered, querycounttotal, err := BuildQuery(config, *qrzinputjson)
|
||||
|
||||
rows, err := config.Db.Queryx(query)
|
||||
rows, err := BuildQuery(config, *qrzinputjson)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
for rows.Next() {
|
||||
var line []string
|
||||
results := make(map[string]interface{})
|
||||
@ -71,55 +83,87 @@ func Run(c echo.Context, config config.Config) (res QrzDatatableOutput, err erro
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
rowscountfiltered, err := config.Db.Queryx(querycountfiltered)
|
||||
rowscountfiltered.Next()
|
||||
err = rowscountfiltered.Scan(&countfiltered)
|
||||
rowscountfiltered.Close()
|
||||
|
||||
rowscounttotal, err := config.Db.Queryx(querycounttotal)
|
||||
rowscounttotal.Next()
|
||||
err = rowscounttotal.Scan(&counttotal)
|
||||
rowscounttotal.Close()
|
||||
|
||||
res.RecordsFiltered = countfiltered
|
||||
res.RecordsTotal = counttotal
|
||||
res.RecordsFiltered, err = BuildQueryCountFiltered(config, *qrzinputjson)
|
||||
res.RecordsTotal, err = BuildQueryCountTotal(config, *qrzinputjson)
|
||||
res.Draw = qrzinputjson.Draw
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// BuildQuery builds query for SQL engine
|
||||
func BuildQuery(config config.Config, qrzdt QrzDatatableInput) (query string, querycountfiltered string, querycounttotal string, err error) {
|
||||
selectstatement, err := SetSelectStatement(config, qrzdt)
|
||||
orderstatement, err := SetOrderStatement(config, qrzdt)
|
||||
limitstatement, err := SetLimitStatement(config, qrzdt)
|
||||
searchstatement, err := SetSearchStatement(config, qrzdt)
|
||||
// BuildQuery builds main query
|
||||
func BuildQuery(config config.Config, qrzdt QrzDatatableInput) (rows *sqlx.Rows, err error) {
|
||||
var selectstatement, orderstatement, limitstatement, searchstatement string
|
||||
selectstatement, err = SetSelectStatement(config, qrzdt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orderstatement, err = SetOrderStatement(config, qrzdt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitstatement, err = SetLimitStatement(config, qrzdt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
searchstatement, err = SetSearchStatement(config, qrzdt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err = config.Db.Queryx(fmt.Sprintf("SELECT %s FROM %s WHERE %s %s %s;", selectstatement, config.DbTable, searchstatement, orderstatement, limitstatement))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// BuildQueryCountFiltered builds query for counting filtered
|
||||
func BuildQueryCountFiltered(config config.Config, qrzdt QrzDatatableInput) (cnt int, err error) {
|
||||
searchstatement, err := SetSearchStatement(config, qrzdt)
|
||||
err = config.Db.Get(&cnt, fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE %s;", config.DbTable, searchstatement))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// BuildQueryCountTotal builds query for counting totals
|
||||
func BuildQueryCountTotal(config config.Config, qrzdt QrzDatatableInput) (cnt int, err error) {
|
||||
err = config.Db.Get(&cnt, fmt.Sprintf("SELECT COUNT(*) FROM %s;", config.DbTable))
|
||||
|
||||
query = fmt.Sprintf("SELECT %s FROM %s %s %s %s;", selectstatement, config.DbTable, searchstatement, orderstatement, limitstatement)
|
||||
querycountfiltered = fmt.Sprintf("SELECT COUNT(*) FROM %s %s;", config.DbTable, searchstatement)
|
||||
querycounttotal = fmt.Sprintf("SELECT COUNT(*) FROM %s;", config.DbTable)
|
||||
return
|
||||
}
|
||||
|
||||
// SetSelectStatement build the sql select statement part
|
||||
func SetSelectStatement(config config.Config, qrzdt QrzDatatableInput) (selectstatement string, err error) {
|
||||
var cols []string
|
||||
colre := regexp.MustCompile(`^[a-z]+$`)
|
||||
if len(qrzdt.Columns) > 0 {
|
||||
for _, col := range qrzdt.Columns {
|
||||
valid := colre.MatchString(col.Name)
|
||||
if valid {
|
||||
cols = append(cols, col.Name)
|
||||
} else {
|
||||
err = fmt.Errorf("String does not match prerequisites")
|
||||
return
|
||||
}
|
||||
}
|
||||
selectstatement = strings.Join(cols, ",")
|
||||
} else {
|
||||
selectstatement = "*"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetOrderStatement build the sql order statement part
|
||||
func SetOrderStatement(config config.Config, qrzdt QrzDatatableInput) (orderstmt string, err error) {
|
||||
var orderstmts []string
|
||||
colre := regexp.MustCompile(`^[a-z]+$`)
|
||||
orderre := regexp.MustCompile(`^(ASC|asc|DESC|desc)$`)
|
||||
for _, col := range qrzdt.Order {
|
||||
orderstmts = append(orderstmts, fmt.Sprintf("%s %s", qrzdt.Columns[col.Column].Name, strings.ToUpper(col.Dir)))
|
||||
if colre.MatchString(qrzdt.Columns[col.Column].Name) && orderre.MatchString(col.Dir) {
|
||||
orderstmts = append(orderstmts, fmt.Sprintf("%s %s", qrzdt.Columns[col.Column].Name, col.Dir))
|
||||
} else {
|
||||
err = fmt.Errorf("Order statements does not match prerequisites")
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(orderstmts) > 0 {
|
||||
orderstmt = fmt.Sprintf("ORDER BY %s", strings.Join(orderstmts, ", "))
|
||||
@ -129,10 +173,16 @@ func SetOrderStatement(config config.Config, qrzdt QrzDatatableInput) (orderstmt
|
||||
|
||||
// SetLimitStatement build the sql limit statement part
|
||||
func SetLimitStatement(config config.Config, qrzdt QrzDatatableInput) (limitstmt string, err error) {
|
||||
intre := regexp.MustCompile(`^[0-9]+$`)
|
||||
if qrzdt.Length < 1 {
|
||||
qrzdt.Length = 50
|
||||
}
|
||||
if intre.MatchString(fmt.Sprintf("%d", qrzdt.Length)) && intre.MatchString(fmt.Sprintf("%d", qrzdt.Start)) {
|
||||
limitstmt = fmt.Sprintf("LIMIT %d OFFSET %d", qrzdt.Length, qrzdt.Start)
|
||||
} else {
|
||||
err = fmt.Errorf("Limit statements does not match prerequisites")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -140,28 +190,37 @@ func SetLimitStatement(config config.Config, qrzdt QrzDatatableInput) (limitstmt
|
||||
func SetSearchStatement(config config.Config, qrzdt QrzDatatableInput) (searchstmt string, err error) {
|
||||
var searchstmtslice []string
|
||||
if len(qrzdt.Columns) > 0 {
|
||||
searchstmtslice = append(searchstmtslice, "WHERE")
|
||||
for id, i := range qrzdt.Columns {
|
||||
searchstmtslice = append(searchstmtslice, fmt.Sprintf("%s LIKE '%%%s%%'", i.Name, qrzdt.Search.Value))
|
||||
if id < len(qrzdt.Columns)-1 {
|
||||
searchstmtslice = append(searchstmtslice, "OR")
|
||||
}
|
||||
}
|
||||
searchstmt = strings.Join(searchstmtslice, " ")
|
||||
|
||||
} else {
|
||||
searchstmtslice = []string{"1=1"}
|
||||
}
|
||||
searchstmt = strings.Join(searchstmtslice, " ")
|
||||
return
|
||||
}
|
||||
|
||||
// getCurrentEntries fetch existing entries from database
|
||||
func getCurrentEntries(config config.Config) (existingQRZ []string, err error) {
|
||||
q := fmt.Sprintf(config.Statements.DbGetQrz, config.DbTable)
|
||||
rows, err := config.Db.Query(q)
|
||||
|
||||
// RunCSVExport runs the main loop
|
||||
func RunCSVExport(c echo.Context, config config.Config) (data []byte, mime string, err error) {
|
||||
mime = "text/csv"
|
||||
rows, err := config.Db.Queryx(fmt.Sprintf(config.DbStatements.ExportCSV, config.DbTable))
|
||||
var res []string
|
||||
for rows.Next() {
|
||||
var qrzelem string
|
||||
rows.Scan(&qrzelem)
|
||||
existingQRZ = append(existingQRZ, qrzelem)
|
||||
var l []string
|
||||
results := make(map[string]interface{})
|
||||
err = rows.MapScan(results)
|
||||
colslice, _ := rows.Columns()
|
||||
for _, column := range colslice {
|
||||
l = append(l, fmt.Sprintf("%s", results[column]))
|
||||
}
|
||||
line := strings.Join(l, ",")
|
||||
res = append(res, line)
|
||||
}
|
||||
data = []byte(strings.Join(res, "\n"))
|
||||
|
||||
return
|
||||
}
|
||||
|
7
static/css/bootstrap.min.css
vendored
Normal file
7
static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
static/js/bootstrap.min.js
vendored
Normal file
7
static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
static/js/main.js
Normal file
3
static/js/main.js
Normal file
@ -0,0 +1,3 @@
|
||||
function export_frs() {
|
||||
var w = window.location.href = "/export_frs.csv";
|
||||
}
|
@ -17,19 +17,31 @@ $(document).ready(function() {
|
||||
},
|
||||
"stateSave": true,
|
||||
"ajax": {
|
||||
"url": "qrzws",
|
||||
"url": "/qrzws",
|
||||
"type": "POST",
|
||||
"contentType": "application/json",
|
||||
"data": function (d) {
|
||||
return JSON.stringify(d);
|
||||
},
|
||||
},
|
||||
"columns": [
|
||||
{ "name": "qrz" },
|
||||
{ "name": "name" },
|
||||
{ "name": "city" },
|
||||
{ "name": "dept" },
|
||||
{ "name": "country" },
|
||||
"columns": [{
|
||||
"name": "qrz"
|
||||
},
|
||||
{
|
||||
"name": "name"
|
||||
},
|
||||
{
|
||||
"name": "city"
|
||||
},
|
||||
{
|
||||
"name": "dept"
|
||||
},
|
||||
{
|
||||
"name": "country"
|
||||
},
|
||||
{
|
||||
"name": "dmrid"
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
@ -1,16 +1,21 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="static/css/main.css" media="screen">
|
||||
<link rel="stylesheet" type="text/css" href="static/css/jquery.dataTables.min.css" media="screen">
|
||||
<link rel="stylesheet" type="text/css" href="static/css/font-awesome/all.css">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href="static/css/bootstrap.min.css">
|
||||
|
||||
<script type="text/javascript" src="static/js/jquery.js"></script>
|
||||
<script type="text/javascript" src="static/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" src="static/js/bootstrap.min.js"></script>
|
||||
<script type="text/javascript" src="static/js/main_table.js"></script>
|
||||
<script type="text/javascript" src="static/js/main.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1 class="jumbotron-heading">FRS QRZ database</h1>
|
||||
<p>Mirror of <a href="http://groupe-frs.hamstation.eu">http://groupe-frs.hamstation.eu</a> list of users</p>
|
||||
<div>
|
||||
<table id="main_table" class="display cell-border">
|
||||
<thead class="thead-dark">
|
||||
@ -20,11 +25,16 @@
|
||||
<td>City</td>
|
||||
<td>Department</td>
|
||||
<td>Country</td>
|
||||
<td>DMRID</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<input type="button" value="Export CSV" onclick="export_frs()">
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user