mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 08:22:24 +01:00
feature (plg_backend_psql): storage connector for postgres
This commit is contained in:
parent
fa3cc86fed
commit
e4c46aa750
5 changed files with 403 additions and 0 deletions
154
server/plugin/plg_backend_psql/index.go
Normal file
154
server/plugin/plg_backend_psql/index.go
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
package plg_backend_psql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
. "github.com/mickael-kerjean/filestash/server/common"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PSQL struct {
|
||||||
|
db *sql.DB
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Backend.Register("psql", PSQL{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this PSQL) Init(params map[string]string, app *App) (IBackend, error) {
|
||||||
|
host := params["host"]
|
||||||
|
port := withDefault(params["port"], "5432")
|
||||||
|
user := params["user"]
|
||||||
|
password := params["password"]
|
||||||
|
dbname := withDefault(params["dbname"], "postgres")
|
||||||
|
sslmode := withDefault(params["sslmode"], "disable")
|
||||||
|
|
||||||
|
if host == "" || user == "" || password == "" {
|
||||||
|
return nil, ErrNotValid
|
||||||
|
}
|
||||||
|
db, err := sql.Open(
|
||||||
|
"postgres",
|
||||||
|
fmt.Sprintf(
|
||||||
|
"host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||||
|
host, port, user, password, dbname, sslmode,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if err := db.Ping(); err != nil {
|
||||||
|
Log.Debug("plg_backend_psql::init err=%s", err.Error())
|
||||||
|
return nil, ErrNotValid
|
||||||
|
}
|
||||||
|
return PSQL{
|
||||||
|
db: db,
|
||||||
|
ctx: app.Context,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func withDefault(val string, def string) string {
|
||||||
|
if val == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this PSQL) LoginForm() Form {
|
||||||
|
return Form{
|
||||||
|
Elmnts: []FormElement{
|
||||||
|
FormElement{
|
||||||
|
Name: "type",
|
||||||
|
Type: "hidden",
|
||||||
|
Value: "psql",
|
||||||
|
},
|
||||||
|
FormElement{
|
||||||
|
Name: "host",
|
||||||
|
Type: "text",
|
||||||
|
Placeholder: "Host",
|
||||||
|
},
|
||||||
|
FormElement{
|
||||||
|
Name: "port",
|
||||||
|
Type: "number",
|
||||||
|
Placeholder: "Port",
|
||||||
|
},
|
||||||
|
FormElement{
|
||||||
|
Name: "user",
|
||||||
|
Type: "text",
|
||||||
|
Placeholder: "User",
|
||||||
|
},
|
||||||
|
FormElement{
|
||||||
|
Name: "password",
|
||||||
|
Type: "password",
|
||||||
|
Placeholder: "Password",
|
||||||
|
},
|
||||||
|
FormElement{
|
||||||
|
Name: "dbname",
|
||||||
|
Type: "text",
|
||||||
|
Placeholder: "DB Name",
|
||||||
|
},
|
||||||
|
FormElement{
|
||||||
|
Name: "sslmode",
|
||||||
|
Type: "text",
|
||||||
|
Placeholder: "SSL Mode",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this PSQL) Touch(path string) error { // TODO
|
||||||
|
this.db.Close()
|
||||||
|
return ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this PSQL) Save(path string, file io.Reader) error { // TODO
|
||||||
|
this.db.Close()
|
||||||
|
return ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this PSQL) Rm(path string) error { // TODO
|
||||||
|
this.db.Close()
|
||||||
|
return ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this PSQL) Mkdir(path string) error {
|
||||||
|
this.db.Close()
|
||||||
|
return ErrNotValid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this PSQL) Mv(from string, to string) error {
|
||||||
|
this.db.Close()
|
||||||
|
return ErrNotValid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this PSQL) Meta(path string) Metadata {
|
||||||
|
location, _ := getPath(path)
|
||||||
|
return Metadata{
|
||||||
|
CanCreateDirectory: NewBool(false),
|
||||||
|
CanCreateFile: func(l Location) *bool {
|
||||||
|
if l.table == "" {
|
||||||
|
return NewBool(false)
|
||||||
|
}
|
||||||
|
return NewBool(true)
|
||||||
|
}(location),
|
||||||
|
CanRename: NewBool(false),
|
||||||
|
CanDelete: func(l Location) *bool {
|
||||||
|
if l.table == "" {
|
||||||
|
return NewBool(false)
|
||||||
|
}
|
||||||
|
return NewBool(true)
|
||||||
|
}(location),
|
||||||
|
CanMove: NewBool(false),
|
||||||
|
CanUpload: func(l Location) *bool {
|
||||||
|
if l.row == "" {
|
||||||
|
return NewBool(false)
|
||||||
|
}
|
||||||
|
return NewBool(true)
|
||||||
|
}(location),
|
||||||
|
RefreshOnCreate: NewBool(true),
|
||||||
|
HideExtension: NewBool(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
62
server/plugin/plg_backend_psql/index_cat.go
Normal file
62
server/plugin/plg_backend_psql/index_cat.go
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
package plg_backend_psql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
. "github.com/mickael-kerjean/filestash/server/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (this PSQL) Cat(path string) (io.ReadCloser, error) {
|
||||||
|
defer this.db.Close()
|
||||||
|
l, err := getPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
columnName, err := getKey(this.ctx, this.db, l.table)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rows, err := this.db.QueryContext(this.ctx, `
|
||||||
|
SELECT *
|
||||||
|
FROM `+l.table+`
|
||||||
|
WHERE `+columnName+`='`+l.row+`'
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
c, err := rows.Columns()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t, err := rows.ColumnTypes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
col := make([]any, len(c))
|
||||||
|
for rows.Next() {
|
||||||
|
if i != 0 {
|
||||||
|
return nil, ErrNotValid
|
||||||
|
}
|
||||||
|
pcol := make([]any, len(c))
|
||||||
|
for i, _ := range pcol {
|
||||||
|
pcol[i] = &col[i]
|
||||||
|
}
|
||||||
|
if err := rows.Scan(pcol...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
forms := make([]FormElement, len(c))
|
||||||
|
for i, _ := range c {
|
||||||
|
f := formType(t[i].ScanType(), c[i])
|
||||||
|
f.Name = c[i]
|
||||||
|
f.Value = col[i]
|
||||||
|
forms[i] = f
|
||||||
|
}
|
||||||
|
b, err := Form{Elmnts: forms}.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewReadCloserFromBytes(b), nil
|
||||||
|
}
|
||||||
64
server/plugin/plg_backend_psql/index_ls.go
Normal file
64
server/plugin/plg_backend_psql/index_ls.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package plg_backend_psql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
. "github.com/mickael-kerjean/filestash/server/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (this PSQL) Ls(path string) ([]os.FileInfo, error) {
|
||||||
|
defer this.db.Close()
|
||||||
|
l, err := getPath(path)
|
||||||
|
if err != nil {
|
||||||
|
Log.Debug("pl_backend_psql::ls method=getPath err=%s", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if l.table == "" {
|
||||||
|
rows, err := this.db.QueryContext(this.ctx, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
|
||||||
|
if err != nil {
|
||||||
|
Log.Debug("plg_backend_psql::ls method=query err=%s", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
out := []os.FileInfo{}
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
if err := rows.Scan(&name); err != nil {
|
||||||
|
Log.Debug("plg_backend_psql::ls method=scan err=%s", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, File{
|
||||||
|
FName: name,
|
||||||
|
FType: "directory",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
} else if l.row == "" {
|
||||||
|
key, err := getKey(this.ctx, this.db, l.table)
|
||||||
|
if err != nil {
|
||||||
|
Log.Debug("plg_backend_psql::ls method=getKey err=%s", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rows, err := this.db.QueryContext(this.ctx, "SELECT "+key+" FROM "+l.table+" LIMIT 500000")
|
||||||
|
if err != nil {
|
||||||
|
Log.Debug("plg_backend_psql::ls method=query err=%s", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
out := []os.FileInfo{}
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
if err := rows.Scan(&name); err != nil {
|
||||||
|
Log.Debug("plg_backend_psql::ls method=scan err=%s", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, File{
|
||||||
|
FName: name + ".form",
|
||||||
|
FType: "file",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
Log.Stdout("plg_backend_psql::ls err=invalid location=%v", l)
|
||||||
|
return []os.FileInfo{}, ErrNotValid
|
||||||
|
}
|
||||||
12
server/plugin/plg_backend_psql/types.go
Normal file
12
server/plugin/plg_backend_psql/types.go
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
package plg_backend_psql
|
||||||
|
|
||||||
|
type Column struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Constraint string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Location struct {
|
||||||
|
table string
|
||||||
|
row string
|
||||||
|
}
|
||||||
111
server/plugin/plg_backend_psql/utils.go
Normal file
111
server/plugin/plg_backend_psql/utils.go
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package plg_backend_psql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
. "github.com/mickael-kerjean/filestash/server/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getPath(path string) (Location, error) {
|
||||||
|
l := Location{}
|
||||||
|
for i, chunk := range strings.Split(path, "/") {
|
||||||
|
if i == 0 {
|
||||||
|
if chunk != "" {
|
||||||
|
return l, ErrNotValid
|
||||||
|
}
|
||||||
|
} else if i == 1 {
|
||||||
|
l.table = chunk
|
||||||
|
} else if i == 2 {
|
||||||
|
l.row = strings.TrimSuffix(chunk, ".form")
|
||||||
|
} else {
|
||||||
|
return l, ErrNotValid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getColumns(ctx context.Context, db *sql.DB, table string) ([]Column, error) {
|
||||||
|
rows, err := db.QueryContext(ctx, `
|
||||||
|
SELECT c.column_name, c.data_type, tc.constraint_type
|
||||||
|
FROM information_schema.table_constraints tc
|
||||||
|
JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name)
|
||||||
|
JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema
|
||||||
|
AND tc.table_name = c.table_name AND ccu.column_name = c.column_name
|
||||||
|
WHERE tc.table_name = $1
|
||||||
|
`, table)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
columns := []Column{}
|
||||||
|
for rows.Next() {
|
||||||
|
var c Column
|
||||||
|
if err := rows.Scan(&c.Name, &c.Type, &c.Type); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
columns = append(columns, c)
|
||||||
|
}
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKey(ctx context.Context, db *sql.DB, table string) (string, error) {
|
||||||
|
columns, err := getColumns(ctx, db, table)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
key := ""
|
||||||
|
score := 0
|
||||||
|
for _, column := range columns {
|
||||||
|
if c := calculateScore(column); c > score {
|
||||||
|
key = column.Name
|
||||||
|
score = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if key == "" {
|
||||||
|
return "", ErrNotValid
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateScore(column Column) int {
|
||||||
|
scoreType := 0
|
||||||
|
scoreName := 1
|
||||||
|
switch column.Type {
|
||||||
|
case "PRIMARY KEY":
|
||||||
|
scoreType = 3
|
||||||
|
case "UNIQUE":
|
||||||
|
scoreType = 2
|
||||||
|
}
|
||||||
|
switch strings.ToLower(column.Name) {
|
||||||
|
case "name":
|
||||||
|
scoreName = 2
|
||||||
|
case "label":
|
||||||
|
scoreName = 2
|
||||||
|
case "email":
|
||||||
|
scoreName = 5
|
||||||
|
}
|
||||||
|
return scoreType * scoreName
|
||||||
|
}
|
||||||
|
|
||||||
|
func formType(rt reflect.Type, label string) FormElement {
|
||||||
|
switch rt.String() {
|
||||||
|
case "bool":
|
||||||
|
return FormElement{
|
||||||
|
Type: "boolean",
|
||||||
|
}
|
||||||
|
case "time.Time":
|
||||||
|
return FormElement{
|
||||||
|
Type: "datetime",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(label), "password") {
|
||||||
|
return FormElement{
|
||||||
|
Type: "password",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FormElement{
|
||||||
|
Type: "text",
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue