mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-10-31 01:58:11 +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)
 | |
| 	}
 | |
| }
 | 
