mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-29 00:55:51 +08:00
994 lines
26 KiB
Go
994 lines
26 KiB
Go
package plg_backend_mysql
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
_ "github.com/go-sql-driver/mysql"
|
|
. "github.com/mickael-kerjean/filestash/server/common"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Mysql struct {
|
|
params map[string]string
|
|
db *sql.DB
|
|
}
|
|
|
|
func init() {
|
|
Backend.Register("mysql", Mysql{})
|
|
}
|
|
|
|
func (this Mysql) Init(params map[string]string, app *App) (IBackend, error) {
|
|
if params["host"] == "" {
|
|
params["host"] = "127.0.0.1"
|
|
}
|
|
if params["port"] == "" {
|
|
params["port"] = "3306"
|
|
}
|
|
|
|
db, err := sql.Open(
|
|
"mysql",
|
|
fmt.Sprintf(
|
|
"%s:%s@tcp(%s:%s)/",
|
|
params["username"],
|
|
params["password"],
|
|
params["host"],
|
|
params["port"],
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Mysql{
|
|
params: params,
|
|
db: db,
|
|
}, nil
|
|
}
|
|
|
|
func (this Mysql) LoginForm() Form {
|
|
return Form{
|
|
Elmnts: []FormElement{
|
|
FormElement{
|
|
Name: "type",
|
|
Type: "hidden",
|
|
Value: "mysql",
|
|
},
|
|
FormElement{
|
|
Name: "host",
|
|
Type: "text",
|
|
Placeholder: "Host",
|
|
},
|
|
FormElement{
|
|
Name: "username",
|
|
Type: "text",
|
|
Placeholder: "Username",
|
|
},
|
|
FormElement{
|
|
Name: "password",
|
|
Type: "password",
|
|
Placeholder: "Password",
|
|
},
|
|
FormElement{
|
|
Name: "port",
|
|
Type: "number",
|
|
Placeholder: "Port",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (this Mysql) Ls(path string) ([]os.FileInfo, error) {
|
|
defer this.db.Close()
|
|
location, err := NewDBLocation(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
files := make([]os.FileInfo, 0)
|
|
|
|
if location.db == "" { // first level folder = a list all the available databases
|
|
rows, err := this.db.Query("SELECT s.schema_name, t.update_time, t.create_time FROM information_schema.SCHEMATA as s LEFT JOIN ( SELECT table_schema, MAX(update_time) as update_time, MAX(create_time) as create_time FROM information_schema.tables GROUP BY table_schema ) as t ON s.schema_name = t.table_schema ORDER BY schema_name")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var name string
|
|
var create string
|
|
var rcreate sql.RawBytes
|
|
var update string
|
|
var rupdate sql.RawBytes
|
|
|
|
if err := rows.Scan(&name, &rcreate, &rupdate); err != nil {
|
|
return nil, err
|
|
}
|
|
create = string(rcreate)
|
|
update = string(rupdate)
|
|
|
|
files = append(files, File{
|
|
FName: name,
|
|
FType: "directory",
|
|
FTime: func() int64 {
|
|
var t time.Time
|
|
var err error
|
|
if create == "" && update == "" {
|
|
return 0
|
|
} else if update == "" {
|
|
if t, err = time.Parse("2006-01-02 15:04:05", create); err != nil {
|
|
return 0
|
|
}
|
|
return t.Unix()
|
|
}
|
|
if t, err = time.Parse("2006-01-02 15:04:05", update); err != nil {
|
|
return 0
|
|
}
|
|
return t.Unix()
|
|
}(),
|
|
})
|
|
}
|
|
return files, nil
|
|
} else if location.table == "" { // second level folder = a list of all the tables available in a database
|
|
rows, err := this.db.Query("SELECT table_name, create_time, update_time FROM information_schema.tables WHERE table_schema = ?", location.db)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var name string
|
|
var create string
|
|
var rcreate sql.RawBytes
|
|
var update string
|
|
var rupdate sql.RawBytes
|
|
|
|
if err := rows.Scan(&name, &rcreate, &rupdate); err != nil {
|
|
return nil, err
|
|
}
|
|
create = string(rcreate)
|
|
update = string(rupdate)
|
|
|
|
files = append(files, File{
|
|
FName: name,
|
|
FType: "directory",
|
|
FTime: func() int64 {
|
|
var t time.Time
|
|
var err error
|
|
if create == "" && update == "" {
|
|
return 0
|
|
} else if update == "" {
|
|
if t, err = time.Parse("2006-01-02 15:04:05", create); err != nil {
|
|
return 0
|
|
}
|
|
return t.Unix()
|
|
}
|
|
if t, err = time.Parse("2006-01-02 15:04:05", update); err != nil {
|
|
return 0
|
|
}
|
|
return t.Unix()
|
|
}(),
|
|
})
|
|
}
|
|
return files, nil
|
|
} else if location.row == "" { // third level folder = a list of all the available rows within the selected table
|
|
sqlFields, err := FindQuerySelection(this.db, location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
extractSingleName := func(s QuerySelection) string {
|
|
return s.Name
|
|
}
|
|
extractName := func(s []QuerySelection) []string {
|
|
t := make([]string, 0, len(s))
|
|
for i := range s {
|
|
t = append(t, extractSingleName(s[i]))
|
|
}
|
|
return t
|
|
}
|
|
extractNamePlus := func(s []QuerySelection) []string {
|
|
t := make([]string, 0, len(s))
|
|
for i := range s {
|
|
t = append(t, "IFNULL("+extractSingleName(s[i])+", '')")
|
|
}
|
|
return t
|
|
}
|
|
|
|
rows, err := this.db.Query(fmt.Sprintf(
|
|
"SELECT CONCAT(%s) as filename %sFROM %s.%s %s LIMIT 500000",
|
|
func() string {
|
|
q := strings.Join(extractNamePlus(sqlFields.Select), ", ' - ', ")
|
|
if len(sqlFields.Esthetics) != 0 {
|
|
q += ", ' - ', " + strings.Join(extractNamePlus(sqlFields.Esthetics), ", ' ', ")
|
|
}
|
|
return q
|
|
}(),
|
|
func() string {
|
|
if extractSingleName(sqlFields.Date) != "" {
|
|
return ", " + extractSingleName(sqlFields.Date) + " as date "
|
|
}
|
|
return ""
|
|
}(),
|
|
location.db,
|
|
location.table,
|
|
func() string {
|
|
if len(sqlFields.Order) != 0 {
|
|
return "ORDER BY " + strings.Join(extractName(sqlFields.Order), ", ") + " DESC "
|
|
}
|
|
return ""
|
|
}(),
|
|
))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for rows.Next() {
|
|
var name_raw sql.RawBytes
|
|
var date sql.RawBytes
|
|
if extractSingleName(sqlFields.Date) == "" {
|
|
if err := rows.Scan(&name_raw); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if err := rows.Scan(&name_raw, &date); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
files = append(files, File{
|
|
FName: string(name_raw) + ".form",
|
|
FType: "file",
|
|
FSize: -1,
|
|
FTime: func() int64 {
|
|
t, err := time.Parse("2006-01-02", fmt.Sprintf("%s", date))
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return t.Unix()
|
|
}(),
|
|
})
|
|
}
|
|
return files, nil
|
|
}
|
|
return nil, ErrNotValid
|
|
}
|
|
|
|
func (this Mysql) Cat(path string) (io.ReadCloser, error) {
|
|
defer this.db.Close()
|
|
location, err := NewDBLocation(path)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if location.db == "" || location.table == "" || location.row == "" {
|
|
return nil, ErrNotValid
|
|
}
|
|
|
|
// STEP 1: Perform the database query
|
|
fields, err := FindQuerySelection(this.db, location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
whereSQL, whereParams := sqlWhereClause(fields, location)
|
|
query := fmt.Sprintf(
|
|
"SELECT * FROM %s.%s WHERE %s",
|
|
location.db,
|
|
location.table,
|
|
whereSQL,
|
|
)
|
|
|
|
rows, err := this.db.Query(query, whereParams...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
columnsName, err := rows.Columns()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// STEP 2: find potential foreign key on given results
|
|
// those will be shown as a list of possible choice
|
|
columnsChoice, err := FindForeignKeysChoices(this.db, location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// STEP 3: Encode the result of the query into a form object
|
|
var forms []FormElement = []FormElement{}
|
|
|
|
dummy := make([]interface{}, len(columnsName))
|
|
columnPointers := make([]interface{}, len(columnsName))
|
|
for i := range columnsName {
|
|
columnPointers[i] = &dummy[i]
|
|
}
|
|
for rows.Next() {
|
|
if err := rows.Scan(columnPointers...); err != nil {
|
|
return nil, err
|
|
}
|
|
break
|
|
}
|
|
for i := range columnsName {
|
|
if pval, ok := columnPointers[i].(*interface{}); ok {
|
|
if pval == nil {
|
|
continue
|
|
}
|
|
el := FormElement{
|
|
Name: columnsName[i],
|
|
Type: "text",
|
|
}
|
|
|
|
switch fields.All[columnsName[i]].Type {
|
|
case "int":
|
|
el.Value = fmt.Sprintf("%d", *pval)
|
|
el.Type = "number"
|
|
case "integer":
|
|
el.Value = fmt.Sprintf("%d", *pval)
|
|
el.Type = "number"
|
|
case "decimal":
|
|
el.Value = fmt.Sprintf("%d", *pval)
|
|
el.Type = "number"
|
|
case "dec":
|
|
el.Value = fmt.Sprintf("%d", *pval)
|
|
el.Type = "number"
|
|
case "float":
|
|
el.Value = fmt.Sprintf("%d", *pval)
|
|
el.Type = "number"
|
|
case "double":
|
|
el.Value = fmt.Sprintf("%d", *pval)
|
|
el.Type = "number"
|
|
case "tinyint":
|
|
el.Value = fmt.Sprintf("%d", *pval)
|
|
el.Type = "number"
|
|
case "smallint":
|
|
el.Value = fmt.Sprintf("%d", *pval)
|
|
el.Type = "number"
|
|
case "mediumint":
|
|
el.Value = fmt.Sprintf("%d", *pval)
|
|
el.Type = "number"
|
|
case "bigint":
|
|
el.Value = fmt.Sprintf("%d", *pval)
|
|
el.Type = "number"
|
|
case "enum":
|
|
el.Type = "select"
|
|
reg := regexp.MustCompile(`^'(.*)'$`)
|
|
el.Opts = func() []string {
|
|
r := strings.Split(strings.TrimSuffix(strings.TrimPrefix(fields.All[columnsName[i]].RawType, "enum("), ")"), ",")
|
|
for i := 0; i < len(r); i++ {
|
|
r[i] = reg.ReplaceAllString(r[i], `$1`)
|
|
}
|
|
return r
|
|
}()
|
|
el.Value = fmt.Sprintf("%s", *pval)
|
|
case "datetime":
|
|
el.Value = strings.Replace(fmt.Sprintf("%s", *pval), " ", "T", 1)
|
|
el.Type = "datetime"
|
|
case "timestamp":
|
|
el.Value = strings.Replace(fmt.Sprintf("%s", *pval), " ", "T", 1)
|
|
el.Type = "datetime"
|
|
case "date":
|
|
el.Value = fmt.Sprintf("%s", *pval)
|
|
el.Type = "date"
|
|
case "text":
|
|
el.Value = fmt.Sprintf("%s", *pval)
|
|
el.Type = "long_text"
|
|
case "longblob":
|
|
el.Value = fmt.Sprintf("%s", *pval)
|
|
el.Type = "file"
|
|
case "mediumblob":
|
|
el.Value = fmt.Sprintf("%s", *pval)
|
|
el.Type = "file"
|
|
case "tinnyblob":
|
|
el.Value = fmt.Sprintf("%s", *pval)
|
|
el.Type = "file"
|
|
default:
|
|
el.Value = fmt.Sprintf("%s", *pval)
|
|
}
|
|
if *pval == nil {
|
|
el.Value = ""
|
|
}
|
|
|
|
if choices, ok := columnsChoice[columnsName[i]]; ok {
|
|
el.Type = "text"
|
|
el.MultiValue = false
|
|
el.Datalist = choices
|
|
|
|
if l, err := FindWhoOwns(this.db, DBLocation{location.db, location.table, columnsName[i]}); err == nil {
|
|
el.Description = fmt.Sprintf(
|
|
"Relates to object in %s",
|
|
generateLink(this.params["path"], l, el.Value),
|
|
)
|
|
}
|
|
} else if key := fields.All[columnsName[i]].Key; key == "PRI" {
|
|
locations, err := FindWhoIsUsing(this.db, location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(locations) > 0 {
|
|
text := []string{}
|
|
for i := 0; i < len(locations); i++ {
|
|
text = append(
|
|
text,
|
|
fmt.Sprintf(
|
|
"%s (%d)",
|
|
generateLink(this.params["path"], DBLocation{locations[i].db, locations[i].table, locations[i].row}, el.Value),
|
|
FindHowManyOccurenceOfaValue(this.db, locations[i], el.Value),
|
|
),
|
|
)
|
|
}
|
|
el.Description = "Used in " + strings.Join(text, ", ")
|
|
}
|
|
}
|
|
forms = append(forms, el)
|
|
}
|
|
}
|
|
|
|
// STEP 3: Send the form back to the user
|
|
b, err := Form{Elmnts: forms}.MarshalJSON()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewReadCloserFromBytes(b), nil
|
|
}
|
|
|
|
func (this Mysql) Mkdir(path string) error {
|
|
defer this.db.Close()
|
|
location, err := NewDBLocation(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if location.db != "" && location.table == "" && location.row == "" {
|
|
_, err = this.db.Exec(fmt.Sprintf("CREATE DATABASE %s", strings.TrimPrefix(location.db, "CREATE DATABASE ")))
|
|
return err
|
|
}
|
|
return ErrNotAllowed
|
|
}
|
|
|
|
func (this Mysql) Rm(path string) error {
|
|
defer this.db.Close()
|
|
location, err := NewDBLocation(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if location.db == "" {
|
|
return ErrNotValid
|
|
} else if location.table == "" {
|
|
_, err := this.db.Exec(fmt.Sprintf("DROP DATABASE %s", location.db))
|
|
return err
|
|
} else if location.row == "" {
|
|
_, err := this.db.Exec(fmt.Sprintf("DROP TABLE %s.%s", location.db, location.table))
|
|
return err
|
|
}
|
|
fields, err := FindQuerySelection(this.db, location)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
whereSQL, whereParams := sqlWhereClause(fields, location)
|
|
query := fmt.Sprintf(
|
|
"DELETE FROM %s.%s WHERE %s",
|
|
location.db,
|
|
location.table,
|
|
whereSQL,
|
|
)
|
|
_, err = this.db.Exec(query, whereParams...)
|
|
return err
|
|
}
|
|
|
|
func (this Mysql) Mv(from string, to string) error {
|
|
defer this.db.Close()
|
|
return ErrNotValid
|
|
}
|
|
|
|
func (this Mysql) Touch(path string) error {
|
|
defer this.db.Close()
|
|
location, err := NewDBLocation(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if location.db == "" {
|
|
return ErrNotValid
|
|
} else if location.table == "" {
|
|
return ErrNotValid
|
|
} else if location.row == "" {
|
|
return ErrNotValid
|
|
}
|
|
|
|
fields, err := FindQuerySelection(this.db, location)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
query := fmt.Sprintf(
|
|
"INSERT INTO %s.%s (%s) VALUES(%s)",
|
|
location.db,
|
|
location.table,
|
|
func() string {
|
|
values := []string{}
|
|
for i := range fields.Select {
|
|
values = append(values, fields.Select[i].Name)
|
|
}
|
|
return strings.Join(values, ",")
|
|
}(),
|
|
func() string {
|
|
values := make([]string, len(fields.Select))
|
|
for i := range values {
|
|
values[i] = "?"
|
|
}
|
|
return strings.Join(values, ",")
|
|
}(),
|
|
)
|
|
queryValues := func() []interface{} {
|
|
valuesOfQuery := make([]interface{}, 0, len(fields.Select))
|
|
valuesFromInput := strings.Split(location.row, " - ")
|
|
for i := range fields.Select {
|
|
if i < len(valuesFromInput) {
|
|
valuesOfQuery = append(valuesOfQuery, valuesFromInput[i])
|
|
} else {
|
|
if t := fields.Select[i].Type; t == "int" || t == "integer" || t == "dec" || t == "double" || t == "float" || t == "smallint" || t == "mediumint" || t == "bigint" {
|
|
valuesOfQuery = append(valuesOfQuery, 0)
|
|
} else if t == "datetime" || t == "date" || t == "timestamp" {
|
|
valuesOfQuery = append(valuesOfQuery, time.Now())
|
|
} else {
|
|
valuesOfQuery = append(valuesOfQuery, "")
|
|
}
|
|
}
|
|
}
|
|
return valuesOfQuery
|
|
}()
|
|
_, err = this.db.Exec(query, queryValues...)
|
|
return err
|
|
}
|
|
|
|
type SqlKeyParams struct {
|
|
Key string
|
|
Value interface{}
|
|
}
|
|
|
|
func (this Mysql) Save(path string, file io.Reader) error {
|
|
defer this.db.Close()
|
|
location, err := NewDBLocation(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if location.db == "" || location.table == "" || location.row == "" {
|
|
return ErrNotValid
|
|
}
|
|
sqlFields, err := FindQuerySelection(this.db, location)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var data map[string]FormElement
|
|
if err := json.NewDecoder(file).Decode(&data); err != nil {
|
|
return err
|
|
}
|
|
var d []SqlKeyParams = make([]SqlKeyParams, 0)
|
|
for key, value := range data {
|
|
d = append(d, SqlKeyParams{key, value.Value})
|
|
}
|
|
|
|
whereSQL, whereParams := sqlWhereClause(sqlFields, location)
|
|
setParams := make([]interface{}, 0, len(data))
|
|
for _, v := range d {
|
|
setParams = append(setParams, v.Value)
|
|
}
|
|
|
|
_, err = this.db.Exec(fmt.Sprintf(
|
|
"UPDATE %s.%s SET %s WHERE %s",
|
|
location.db,
|
|
location.table,
|
|
func() string {
|
|
a := make([]string, 0, len(data))
|
|
for _, v := range d {
|
|
a = append(a, fmt.Sprintf("%s = ?", v.Key))
|
|
}
|
|
return strings.Join(a, ", ")
|
|
}(),
|
|
whereSQL,
|
|
), append(setParams, whereParams...)...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (this Mysql) Meta(path string) Metadata {
|
|
location, _ := NewDBLocation(path)
|
|
return Metadata{
|
|
CanCreateDirectory: func(l DBLocation) *bool {
|
|
if l.db == "" && l.table == "" && l.row == "" {
|
|
return NewBool(true)
|
|
}
|
|
return NewBool(false)
|
|
}(location),
|
|
CanCreateFile: func(l DBLocation) *bool {
|
|
if l.table == "" || l.db == "" {
|
|
return NewBool(false)
|
|
}
|
|
return NewBool(true)
|
|
}(location),
|
|
CanRename: NewBool(false),
|
|
CanMove: NewBool(false),
|
|
RefreshOnCreate: NewBool(true),
|
|
HideExtension: NewBool(true),
|
|
}
|
|
}
|
|
|
|
type DBLocation struct {
|
|
db string
|
|
table string
|
|
row string
|
|
}
|
|
|
|
func NewDBLocation(path string) (DBLocation, error) {
|
|
var location DBLocation
|
|
|
|
p := strings.Split(strings.Trim(path, "/"), "/")
|
|
isValid := func(str string) bool { // https://dev.mysql.com/doc/refman/8.0/en/identifiers.html
|
|
return regexp.MustCompile(`^[0-9,a-z,A-Z$_]*$`).MatchString(str)
|
|
}
|
|
|
|
if lPath := len(p); lPath == 0 {
|
|
return DBLocation{}, nil
|
|
} else if lPath == 1 {
|
|
location = DBLocation{
|
|
db: p[0],
|
|
}
|
|
if isValid(p[0]) == false {
|
|
return location, ErrNotValid
|
|
}
|
|
return location, nil
|
|
} else if lPath == 2 {
|
|
location = DBLocation{
|
|
db: p[0],
|
|
table: p[1],
|
|
}
|
|
if isValid(p[0]) == false || isValid(p[1]) == false {
|
|
return location, ErrNotValid
|
|
}
|
|
return location, nil
|
|
} else if lPath == 3 {
|
|
location = DBLocation{
|
|
db: p[0],
|
|
table: p[1],
|
|
row: strings.TrimSuffix(p[2], ".form"),
|
|
}
|
|
if isValid(p[0]) == false || isValid(p[1]) == false {
|
|
return location, ErrNotValid
|
|
}
|
|
return location, nil
|
|
}
|
|
return DBLocation{}, ErrNotValid
|
|
}
|
|
|
|
type QuerySelection struct {
|
|
Name string
|
|
Type string
|
|
RawType string
|
|
Size int
|
|
Key string
|
|
Nullable bool
|
|
}
|
|
|
|
type SqlFields struct {
|
|
Order []QuerySelection
|
|
Select []QuerySelection
|
|
Esthetics []QuerySelection
|
|
Date QuerySelection
|
|
All map[string]QuerySelection
|
|
}
|
|
|
|
func sqlWhereClause(s SqlFields, location DBLocation) (string, []interface{}) {
|
|
where := []string{}
|
|
queryParams := make([]interface{}, 0)
|
|
|
|
for i := range s.Select {
|
|
where = append(where, fmt.Sprintf("%s = ?", s.Select[i].Name))
|
|
}
|
|
for i, value := range strings.Split(location.row, " - ") {
|
|
if i < len(s.Select) {
|
|
queryParams = append(queryParams, value)
|
|
}
|
|
}
|
|
return strings.Join(where, " AND "), queryParams
|
|
}
|
|
|
|
func FindQuerySelection(db *sql.DB, location DBLocation) (SqlFields, error) {
|
|
var queryCandidates []QuerySelection = make([]QuerySelection, 0)
|
|
var fields SqlFields = SqlFields{
|
|
Order: make([]QuerySelection, 0),
|
|
Select: make([]QuerySelection, 0),
|
|
Esthetics: make([]QuerySelection, 0),
|
|
All: make(map[string]QuerySelection, 0),
|
|
}
|
|
if location.db == "" || location.table == "" {
|
|
return fields, ErrNotValid
|
|
}
|
|
|
|
// STEP 1: extract possible values from the available schema
|
|
rows, err := db.Query("SELECT IS_NULLABLE, DATA_TYPE, COLUMN_TYPE, COLUMN_NAME, COLUMN_KEY FROM information_schema.COLUMNS WHERE table_schema = ? && table_name = ?", location.db, location.table)
|
|
if err != nil {
|
|
return fields, err
|
|
}
|
|
for rows.Next() {
|
|
var data_type string
|
|
var column_type string
|
|
var column_name string
|
|
var column_key string
|
|
var is_nullable string
|
|
|
|
if err := rows.Scan(&is_nullable, &data_type, &column_type, &column_name, &column_key); err != nil {
|
|
return fields, err
|
|
}
|
|
q := QuerySelection{
|
|
Name: column_name,
|
|
Type: data_type,
|
|
Size: func() int {
|
|
if strings.Contains(column_type, "(") && strings.Contains(column_type, ")") {
|
|
c := regexp.MustCompile("[0-9]+").FindAllString(column_type, -1)
|
|
if len(c) == 1 {
|
|
if i, err := strconv.Atoi(c[0]); err == nil {
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}(),
|
|
Nullable: func() bool {
|
|
if is_nullable == "YES" {
|
|
return true
|
|
}
|
|
return false
|
|
}(),
|
|
RawType: column_type,
|
|
Key: column_key,
|
|
}
|
|
fields.All[column_name] = q
|
|
queryCandidates = append(queryCandidates, q)
|
|
}
|
|
if len(queryCandidates) == 0 {
|
|
return fields, ErrNotValid
|
|
}
|
|
|
|
// STEP 2: filter out unwanted fields from the schema
|
|
for i := 0; i < len(queryCandidates); i++ {
|
|
if queryCandidates[i].Key == "PRI" || queryCandidates[i].Key == "UNI" {
|
|
fields.Select = append(fields.Select, queryCandidates[i])
|
|
if queryCandidates[i].Type == "date" {
|
|
fields.Order = append(fields.Order, queryCandidates[i])
|
|
}
|
|
} else if queryCandidates[i].Type == "varchar" {
|
|
fields.Esthetics = append(fields.Esthetics, queryCandidates[i])
|
|
}
|
|
|
|
if queryCandidates[i].Type == "date" && queryCandidates[i].Nullable == false {
|
|
fields.Date = queryCandidates[i]
|
|
}
|
|
}
|
|
|
|
// STEP 3: Ensure the current selection is workable
|
|
if len(fields.Select) == 0 {
|
|
// worst case scenario with no defined keys in the schema, we populate the selection with:
|
|
// - strategy 1: finding a field that can do the job (essentially COUNT(*) == DISTINCT(COUNT(*)))
|
|
// - strategy 2: the key is a set of all the available fields (worst worst case)
|
|
// This can be fairly slow on large tables but that's the cost to pay for bad database design
|
|
sort.SliceStable(queryCandidates, func(i, j int) bool {
|
|
if queryCandidates[i].Type == "varchar" && queryCandidates[i].Type != queryCandidates[j].Type {
|
|
return true
|
|
} else if queryCandidates[j].Type == "varchar" && queryCandidates[i].Type != queryCandidates[j].Type {
|
|
return false
|
|
} else if queryCandidates[i].Type == "char" && queryCandidates[i].Type != queryCandidates[j].Type {
|
|
return true
|
|
} else if queryCandidates[j].Type == "char" && queryCandidates[i].Type != queryCandidates[j].Type {
|
|
return false
|
|
}
|
|
return queryCandidates[i].Size < queryCandidates[j].Size
|
|
})
|
|
var size int = 0
|
|
var i int = 0
|
|
for i = range queryCandidates {
|
|
query := fmt.Sprintf(
|
|
"SELECT COUNT(%s), COUNT(DISTINCT(%s)) FROM %s.%s",
|
|
queryCandidates[i].Name,
|
|
queryCandidates[i].Name,
|
|
location.db,
|
|
location.table,
|
|
)
|
|
size += queryCandidates[i].Size
|
|
var count_all int
|
|
var count_distinct int
|
|
if err := db.QueryRow(query).Scan(&count_all, &count_distinct); err != nil {
|
|
return fields, err
|
|
}
|
|
if count_all == count_distinct {
|
|
fields.Select = append(fields.Select, queryCandidates[i])
|
|
fields.Esthetics = func() []QuerySelection {
|
|
var i int
|
|
esthetics := make([]QuerySelection, 0, len(fields.Esthetics))
|
|
for i = range fields.Esthetics {
|
|
if fields.Esthetics[i].Name != queryCandidates[i].Name {
|
|
esthetics = append(esthetics, queryCandidates[i])
|
|
}
|
|
}
|
|
return esthetics
|
|
}()
|
|
break
|
|
}
|
|
}
|
|
if i == len(queryCandidates)-1 {
|
|
if size > 200 {
|
|
return fields, NewError("This table doesn't have any defined keys.", 405)
|
|
}
|
|
fields.Select = queryCandidates
|
|
fields.Esthetics = make([]QuerySelection, 0)
|
|
}
|
|
}
|
|
|
|
// STEP 4: organise our finding into a data structure that's usable
|
|
sortQuerySelection := func(s []QuerySelection) func(i, j int) bool {
|
|
calculateScore := func(q QuerySelection) int {
|
|
score := 0
|
|
if q.Key == "UNI" {
|
|
score = 4
|
|
} else if q.Key == "PRI" {
|
|
score = 5
|
|
} else {
|
|
return 0
|
|
}
|
|
if lowerName := strings.ToLower(q.Name); lowerName == "id" || lowerName == "gid" || lowerName == "uid" {
|
|
score -= 2
|
|
}
|
|
if q.Type == "varchar" || q.Type == "char" {
|
|
score += 1
|
|
} else if q.Type == "date" {
|
|
score -= 1
|
|
}
|
|
return score
|
|
}
|
|
return func(i, j int) bool {
|
|
return calculateScore(s[i]) > calculateScore(s[j])
|
|
}
|
|
}
|
|
sort.SliceStable(fields.Select, sortQuerySelection(fields.Select))
|
|
sort.SliceStable(fields.Order, sortQuerySelection(fields.Order))
|
|
fields.Date.Name = func() string {
|
|
if len(fields.Order) == 0 {
|
|
return fields.Date.Name
|
|
}
|
|
return fields.Order[0].Name
|
|
}()
|
|
fields.Esthetics = func() []QuerySelection { // fields whose only value is to make our generated field look good
|
|
var size int = 0
|
|
var i int
|
|
for i = range fields.Select {
|
|
size += fields.Select[i].Size
|
|
}
|
|
for i = range fields.Esthetics {
|
|
s := fields.Esthetics[i].Size
|
|
if size+s > 100 {
|
|
break
|
|
}
|
|
size += s
|
|
}
|
|
if i+1 > len(fields.Esthetics) {
|
|
return fields.Esthetics
|
|
}
|
|
return fields.Esthetics[:i+1]
|
|
}()
|
|
|
|
return fields, nil
|
|
}
|
|
|
|
func (this Mysql) Close() error {
|
|
return this.db.Close()
|
|
}
|
|
|
|
func FindForeignKeysChoices(db *sql.DB, location DBLocation) (map[string][]string, error) {
|
|
choices := make(map[string][]string, 0)
|
|
rows, err := db.Query("SELECT column_name, referenced_table_name, referenced_column_name FROM information_schema.key_column_usage WHERE table_schema = ? AND table_name = ? AND referenced_column_name IS NOT NULL", location.db, location.table)
|
|
if err != nil {
|
|
return choices, err
|
|
}
|
|
for rows.Next() {
|
|
var column_name string
|
|
var referenced_table_schema string
|
|
var referenced_column_name string
|
|
if err := rows.Scan(&column_name, &referenced_table_schema, &referenced_column_name); err != nil {
|
|
return choices, err
|
|
}
|
|
r, err := db.Query(fmt.Sprintf("SELECT DISTINCT(%s) FROM %s.%s LIMIT 10000", column_name, location.db, location.table))
|
|
if err != nil {
|
|
return choices, err
|
|
}
|
|
var res []string = make([]string, 0)
|
|
for r.Next() {
|
|
var value string
|
|
r.Scan(&value)
|
|
res = append(res, value)
|
|
}
|
|
choices[column_name] = res
|
|
}
|
|
return choices, nil
|
|
}
|
|
|
|
func FindWhoIsUsing(db *sql.DB, location DBLocation) ([]DBLocation, error) {
|
|
locations := make([]DBLocation, 0)
|
|
rows, err := db.Query("SELECT table_schema, table_name, column_name FROM information_schema.key_column_usage WHERE referenced_table_schema = ? AND referenced_table_name = ? AND column_name IS NOT NULL", location.db, location.table)
|
|
if err != nil {
|
|
return locations, err
|
|
}
|
|
for rows.Next() {
|
|
var table_schema string
|
|
var table_name string
|
|
var column_name string
|
|
if err := rows.Scan(&table_schema, &table_name, &column_name); err != nil {
|
|
return locations, err
|
|
}
|
|
locations = append(locations, DBLocation{
|
|
db: table_schema,
|
|
table: table_name,
|
|
row: column_name,
|
|
})
|
|
}
|
|
return locations, nil
|
|
}
|
|
|
|
func FindWhoOwns(db *sql.DB, location DBLocation) (DBLocation, error) {
|
|
var referenced_table_schema string
|
|
var referenced_table_name string
|
|
var referenced_column_name string
|
|
|
|
if err := db.QueryRow(
|
|
fmt.Sprintf("SELECT referenced_table_schema, referenced_table_name, referenced_column_name FROM information_schema.key_column_usage WHERE table_schema = ? AND table_name = ? AND column_name = ? AND referenced_column_name IS NOT NULL"),
|
|
location.db,
|
|
location.table,
|
|
location.row,
|
|
).Scan(&referenced_table_schema, &referenced_table_name, &referenced_column_name); err != nil {
|
|
return DBLocation{}, err
|
|
}
|
|
return DBLocation{referenced_table_schema, referenced_table_name, referenced_column_name}, nil
|
|
}
|
|
|
|
func FindHowManyOccurenceOfaValue(db *sql.DB, location DBLocation, value interface{}) int {
|
|
var count int
|
|
if err := db.QueryRow(
|
|
fmt.Sprintf("SELECT COUNT(*) FROM %s.%s WHERE %s = ?", location.db, location.table, location.row),
|
|
value,
|
|
).Scan(&count); err != nil {
|
|
return 0
|
|
}
|
|
return count
|
|
}
|
|
|
|
func generateLink(chroot string, l DBLocation, value interface{}) string {
|
|
chrootLocation, err := NewDBLocation(chroot)
|
|
if err != nil {
|
|
return fmt.Sprintf("'%s'", l.table)
|
|
}
|
|
|
|
if chrootLocation.db == "" {
|
|
return fmt.Sprintf(
|
|
"[%s](/files/%s/%s/%s)",
|
|
l.table,
|
|
l.db,
|
|
l.table,
|
|
func() string {
|
|
if l.row == "" {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("?q=%s%%3D%s", l.row, value)
|
|
}(),
|
|
)
|
|
} else if chrootLocation.table == "" {
|
|
return fmt.Sprintf(
|
|
"[%s](/files/%s/%s)",
|
|
l.table,
|
|
l.table,
|
|
func() string {
|
|
if l.row == "" {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("?q=%s%%3D%s", l.row, value)
|
|
}(),
|
|
)
|
|
} else {
|
|
return fmt.Sprintf("'%s'", l.table)
|
|
}
|
|
}
|