Files
2024-03-13 00:18:24 +11:00

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)
}
}