diff --git a/cmd/root.go b/cmd/root.go index 7ec4d441..a1d0c82c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -45,7 +45,7 @@ func init() { persistent := rootCmd.PersistentFlags() persistent.StringVarP(&cfgFile, "config", "c", "", "config file path") - persistent.StringP("database", "d", "./filebrowser.db", "database path") + persistent.StringP("database", "d", "./filebrowser.db", "database path. Possible choices:\n\tbolt: ./bolt.db\n\tsqlite3: sqlite3://test.db\n\tpostgresql: postgres://user:password@192.168.1.10:5432/postgres?sslmode=disable\n\tmysql: mysql://user:password@192.168.1.10:3306/root\n") flags.Bool("noauth", false, "use the noauth auther when using quick setup") flags.String("username", "admin", "username for the first user when using quick config") flags.String("password", "", "hashed password for the first user when using quick config (default \"admin\")") diff --git a/cmd/utils.go b/cmd/utils.go index 2bd9e760..650b162f 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -17,6 +17,7 @@ import ( "github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/storage" "github.com/filebrowser/filebrowser/v2/storage/bolt" + "github.com/filebrowser/filebrowser/v2/storage/sql" ) func checkErr(err error) { @@ -82,27 +83,51 @@ func dbExists(path string) (bool, error) { return false, err } +type Closeable interface { + Close() error +} + +func openBoltDB(path string, cfg pythonConfig) (pythonData, Closeable) { + data := pythonData{hadDB: true} + exists, err := dbExists(path) + + if err != nil { + panic(err) + } else if exists && cfg.noDB { + log.Fatal(path + " already exists") + } else if !exists && !cfg.noDB && !cfg.allowNoDB { + log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.") + } + + data.hadDB = exists + db, err := storm.Open(path) + checkErr(err) + data.store, err = bolt.NewStorage(db) + checkErr(err) + return data, db +} + +func openDB(path string, cfg pythonConfig) (pythonData, Closeable) { + if sql.IsDBPath(path) { + data := pythonData{hadDB: false} + db, err := sql.OpenDB(path) + if err != nil { + log.Fatal("Fail to open database " + path) + } + data.store, err = sql.NewStorage(db) + if err != nil { + log.Fatal("Fail to create database storage for " + path) + } + data.hadDB = sql.HadSetting(db) + return data, db + } + return openBoltDB(path, cfg) +} + func python(fn pythonFunc, cfg pythonConfig) cobraFunc { return func(cmd *cobra.Command, args []string) { - data := pythonData{hadDB: true} - - path := getParam(cmd.Flags(), "database") - exists, err := dbExists(path) - - if err != nil { - panic(err) - } else if exists && cfg.noDB { - log.Fatal(path + " already exists") - } else if !exists && !cfg.noDB && !cfg.allowNoDB { - log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.") - } - - data.hadDB = exists - db, err := storm.Open(path) - checkErr(err) + data, db := openDB(getParam(cmd.Flags(), "database"), cfg) defer db.Close() - data.store, err = bolt.NewStorage(db) - checkErr(err) fn(cmd, args, data) } } diff --git a/go.mod b/go.mod index 03560f19..7b2cd6c2 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,14 @@ require ( github.com/disintegration/imaging v1.6.2 github.com/dsoprea/go-exif/v3 v3.0.0-20201216222538-db167117f483 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 + github.com/go-sql-driver/mysql v1.6.0 github.com/golang-jwt/jwt/v4 v4.4.3 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 + github.com/lib/pq v1.10.9 github.com/maruel/natural v1.1.0 github.com/marusama/semaphore/v2 v2.5.0 + github.com/mattn/go-sqlite3 v1.14.17 github.com/mholt/archiver/v3 v3.5.1 github.com/mitchellh/go-homedir v1.1.0 github.com/pelletier/go-toml/v2 v2.0.6 diff --git a/go.sum b/go.sum index 34ba7f31..de36f890 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= @@ -196,6 +198,8 @@ github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3x github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -204,6 +208,8 @@ github.com/maruel/natural v1.1.0 h1:2z1NgP/Vae+gYrtC0VuvrTJ6U35OuyUqDdfluLqMWuQ= github.com/maruel/natural v1.1.0/go.mod h1:eFVhYCcUOfZFxXoDZam8Ktya72wa79fNC3lc/leA0DQ= github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM= github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= diff --git a/scripts/test-sql.sh b/scripts/test-sql.sh new file mode 100755 index 00000000..5dbc1068 --- /dev/null +++ b/scripts/test-sql.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# stop_docker name +stop_docker() { + sudo docker stop $1 1>/dev/null 2>&1 + sudo docker rm $1 1>/dev/null 2>&1 +} + +# start_mysql name password port +start_mysql() { + stop_docker $1 + sudo docker run --rm --name $1 -e MYSQL_ROOT_PASSWORD=$2 -p $3:3306 -d mysql +} + +# start_postgres name password port +start_postgres() { + stop_docker $1 + sudo docker run --rm --name $1 -e POSTGRES_PASSWORD=$2 -p $3:5432 -d postgres +} + + +test_sqlite() { + rm -f test.db + ./filebrowser -a 0.0.0.0 -d sqlite3://test.db +} + + +test_postgres() { + start_postgres test-postgres postgres 5433 + sleep 30 + ./filebrowser -a 0.0.0.0 -d postgres://postgres:postgres@127.0.0.1:5433/postgres?sslmode=disable +} + + +test_mysql() { + start_mysql test-mysql root 3307 + sleep 60 + ./filebrowser -a 0.0.0.0 -d 'mysql://root:root@127.0.0.1:3307/mysql' +} + +help() { + echo "USAGE: $0 sqlite|mysql|postgres" + exit 1 +} + +if (( $# == 0 )); then + help +fi + +case $1 in + sqlite) + test_sqlite + ;; + mysql) + test_mysql + ;; + postgres) + test_postgres + ;; + *) + help +esac + + diff --git a/storage/sql/auth.go b/storage/sql/auth.go new file mode 100644 index 00000000..c04ba32a --- /dev/null +++ b/storage/sql/auth.go @@ -0,0 +1,47 @@ +package sql + +import ( + "database/sql" + "encoding/json" + "fmt" + + "github.com/filebrowser/filebrowser/v2/auth" + "github.com/filebrowser/filebrowser/v2/errors" + "github.com/filebrowser/filebrowser/v2/settings" +) + +type authBackend struct { + db *sql.DB + dbType string +} + +func (s authBackend) Get(t settings.AuthMethod) (auth.Auther, error) { + var auther auth.Auther + + switch t { + case auth.MethodJSONAuth: + auther = &auth.JSONAuth{} + case auth.MethodProxyAuth: + auther = &auth.ProxyAuth{} + case auth.MethodHookAuth: + auther = &auth.HookAuth{} + case auth.MethodNoAuth: + auther = &auth.NoAuth{} + default: + fmt.Println("ERROR: unknown auth method " + t) + return nil, errors.ErrInvalidAuthMethod + } + return auther, nil +} + +func (s authBackend) Save(a auth.Auther) error { + val, err := json.Marshal(a) + if checkError(err, "Fail to save auth.Auther") { + return err + } + return SetSetting(s.db, s.dbType, "auther", string(val)) +} + +func newAuthBackend(db *sql.DB, dbType string) authBackend { + return authBackend{db: db, dbType: dbType} +} diff --git a/storage/sql/config.go b/storage/sql/config.go new file mode 100644 index 00000000..3106884c --- /dev/null +++ b/storage/sql/config.go @@ -0,0 +1,21 @@ +package sql + +import "os" + +var SettingsTable = "fb_settings" +var UsersTable = "fb_users" +var SharesTable = "fb_shares" + +func getEnv(key string, defaultValue string) string { + val := os.Getenv(key) + if len(val) == 0 { + return defaultValue + } + return val +} + +func init() { + SettingsTable = getEnv("FILEBROWSER_SETTINGS_TABLE", SettingsTable) + UsersTable = getEnv("FILEBROWSER_USERS_TABLE", UsersTable) + SharesTable = getEnv("FILEBROWSER_SHARES_TABLE", SharesTable) +} diff --git a/storage/sql/server.go b/storage/sql/server.go new file mode 100644 index 00000000..cf195d6d --- /dev/null +++ b/storage/sql/server.go @@ -0,0 +1,160 @@ +package sql + +import ( + "encoding/json" + "fmt" + + "github.com/filebrowser/filebrowser/v2/settings" +) + +var defaultServer = settings.Server{ + Port: "8080", + Log: "stdout", + EnableThumbnails: false, + ResizePreview: false, + EnableExec: false, + TypeDetectionByHeader: false, +} + +func cloneServer(server settings.Server) settings.Server { + data, err := json.Marshal(server) + s := settings.Server{} + if checkError(err, "Fail to clone settings.Server") { + return s + } + err = json.Unmarshal(data, &s) + checkError(err, "Fail to decode for settings.Server") + return s +} + +func (s settingsBackend) GetServer() (*settings.Server, error) { + sql := fmt.Sprintf("select %s, value from %s", quoteName(s.dbType, "key"), quoteName(s.dbType, SettingsTable)) + rows, err := s.db.Query(sql) + if checkError(err, "Fail to Query for GetServer") { + return nil, err + } + server := cloneServer(defaultServer) + key := "" + value := "" + + for rows.Next() { + err = rows.Scan(&key, &value) + if checkError(err, "Fail to query settings.Settings") { + continue + } + if key == "Root" { + server.Root = value + } else if key == "BaseURL" { + server.BaseURL = value + } else if key == "Socket" { + server.Socket = value + } else if key == "TLSKey" { + server.TLSKey = value + } else if key == "TLSCert" { + server.TLSCert = value + } else if key == "Port" { + server.Port = value + } else if key == "Address" { + server.Address = value + } else if key == "Log" { + server.Log = value + } else if key == "EnableThumbnails" { + server.EnableThumbnails = boolFromString(value) + } else if key == "ResizePreview" { + server.ResizePreview = boolFromString(value) + } else if key == "EnableExec" { + server.EnableExec = boolFromString(value) + } else if key == "TypeDetectionByHeader" { + server.TypeDetectionByHeader = boolFromString(value) + } else if key == "AuthHook" { + server.AuthHook = value + } + } + return &server, nil +} + +func (s settingsBackend) SaveServer(ss *settings.Server) error { + fields := []string{"Root", "BaseURL", "Socket", "TLSKey", "TLSCert", "Port", "Address", "Log", "EnableThumbnails", "ResizePreview", "EnableExec", "TypeDetectionByHeader", "AuthHook"} + values := []string{ + ss.Root, + ss.BaseURL, + ss.Socket, + ss.TLSKey, + ss.TLSCert, + ss.Port, + ss.Address, + ss.Log, + boolToString(ss.EnableThumbnails), + boolToString(ss.ResizePreview), + boolToString(ss.EnableExec), + boolToString(ss.TypeDetectionByHeader), + ss.AuthHook} + tx, err := s.db.Begin() + if checkError(err, "Fail to begin db transaction") { + return err + } + table := quoteName(s.dbType, SettingsTable) + keyName := quoteName(s.dbType, "key") + p1 := placeHolder(s.dbType, 1) + p2 := placeHolder(s.dbType, 2) + + Insert := func(key string, value string) bool { + insertSql := fmt.Sprintf("INSERT INTO %s (%s, value) VALUES(%s,%s)", table, keyName, p1, p2) + stmt, err := s.db.Prepare(insertSql) + defer stmt.Close() + if checkError(err, "Fail to prepare statement") { + tx.Rollback() + return false + } + _, err = stmt.Exec(key, value) + if checkError(err, "Fail to insert field "+key+" of settings.Server") { + tx.Rollback() + return false + } + return true + } + Update := func(key string, value string) bool { + updateSql := fmt.Sprintf("UPDATE %s SET value=%s WHERE %s=%s", table, p1, keyName, p2) + stmt, err := s.db.Prepare(updateSql) + defer stmt.Close() + if checkError(err, "Fail to prepare statement") { + tx.Rollback() + return false + } + _, err = stmt.Exec(value, key) + if checkError(err, "Fail to update field "+key+" of settings.Server") { + tx.Rollback() + return false + } + return true + } + Exist := func(key string) bool { + querySql := fmt.Sprintf("SELECT count(*) FROM %s WHERE %s=%s", table, keyName, p1) + row := s.db.QueryRow(querySql, key) + count := 0 + err := row.Scan(&count) + if checkError(err, "Fail to Query "+key+" for GetServer") { + return false + } + return count == 1 + } + InsertOrUpdate := func(key string, value string) bool { + if Exist(key) { + return Update(key, value) + } else { + return Insert(key, value) + } + } + + for i, field := range fields { + if !InsertOrUpdate(field, values[i]) { + break + } + } + err = tx.Commit() + if checkError(err, "Fail to commit") { + tx.Rollback() + return err + } + return err +} diff --git a/storage/sql/settings.go b/storage/sql/settings.go new file mode 100644 index 00000000..ece43f6c --- /dev/null +++ b/storage/sql/settings.go @@ -0,0 +1,368 @@ +package sql + +import ( + "database/sql" + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/filebrowser/filebrowser/v2/auth" + "github.com/filebrowser/filebrowser/v2/files" + "github.com/filebrowser/filebrowser/v2/rules" + "github.com/filebrowser/filebrowser/v2/settings" + "github.com/filebrowser/filebrowser/v2/users" +) + +func init() { +} + +type settingsBackend struct { + db *sql.DB + dbType string +} + +func InitSettingsTable(db *sql.DB, dbType string) error { + sql := fmt.Sprintf("create table if not exists %s (%s varchar(128) primary key, value text);", quoteName(dbType, SettingsTable), quoteName(dbType, "key")) + _, err := db.Exec(sql) + checkError(err, "Fail to create table settings") + return err +} + +func bytesToString(data []byte) string { + return base64.RawStdEncoding.EncodeToString(data) +} + +func bytesFromString(s string) ([]byte, error) { + return base64.RawStdEncoding.DecodeString(s) +} + +func userDefaultsFromString(s string) settings.UserDefaults { + if s == "" { + return settings.UserDefaults{} + } + userDefaults := settings.UserDefaults{} + err := json.Unmarshal([]byte(s), &userDefaults) + checkError(err, "Fail to parse settings.UserDefaults") + return userDefaults +} + +func userDefaultsToString(d settings.UserDefaults) string { + data, err := json.Marshal(d) + if checkError(err, "Fail to stringify settings.UserDefaults") { + return "" + } + return string(data) +} + +func brandingFromString(s string) settings.Branding { + if s == "" { + return settings.Branding{} + } + branding := settings.Branding{} + err := json.Unmarshal([]byte(s), &branding) + checkError(err, "Fail to parse settings.Branding") + return branding +} + +func brandingToString(s settings.Branding) string { + data, err := json.Marshal(s) + if checkError(err, "Fail to jsonify settings.Branding") { + return "" + } + return string(data) +} + +func commandsToString(c map[string][]string) string { + data, err := json.Marshal(c) + if checkError(err, "Fail to jsonify commands") { + return "" + } + return string(data) +} + +func commandsFromString(s string) map[string][]string { + c := make(map[string][]string) + if s == "" { + return c + } + err := json.Unmarshal([]byte(s), &c) + checkError(err, "Fail to parse commands") + return c +} + +func stringsFromString(s string) []string { + c := make([]string, 0) + if s == "" { + return c + } + err := json.Unmarshal([]byte(s), &c) + checkError(err, "Fail to parse []string") + return c +} + +func stringsToString(c []string) string { + data, err := json.Marshal(c) + if checkError(err, "Fail to jsonify strings") { + return "" + } + return string(data) +} + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +func boolFromInt(i int) bool { + if i == 0 { + return false + } + return true +} + +func boolFromString(s string) bool { + if s == "0" || s == "" || s == "f" || s == "F" { + return false + } + return true +} + +func boolToString(b bool) string { + if b { + return "1" + } + return "0" +} + +func (s settingsBackend) Get() (*settings.Settings, error) { + sql := fmt.Sprintf("select %s, value from %s;", quoteName(s.dbType, "key"), quoteName(s.dbType, SettingsTable)) + rows, err := s.db.Query(sql) + if checkError(err, "Fail to Query settings.Settings") { + return nil, err + } + key := "" + value := "" + settings1 := cloneSettings(defaultSettings) + for rows.Next() { + err = rows.Scan(&key, &value) + checkError(err, "Fail to query settings.Settings") + if key == "Key" { + val, err := bytesFromString(value) + if !checkError(err, "Fail to parse []byte from string") { + settings1.Key = val + } + } else if key == "Signup" { + settings1.Signup = boolFromString(value) + } else if key == "CreateUserDir" { + settings1.CreateUserDir = boolFromString(value) + } else if key == "UserHomeBasePath" { + settings1.UserHomeBasePath = value + } else if key == "Defaults" { + settings1.Defaults = userDefaultsFromString(value) + } else if key == "AuthMethod" { + settings1.AuthMethod = settings.AuthMethod(value) + } else if key == "Branding" { + settings1.Branding = brandingFromString(value) + } else if key == "Commands" { + settings1.Commands = commandsFromString(value) + } else if key == "Shell" { + settings1.Shell = stringsFromString(value) + } else if key == "Rules" { + settings1.Rules = rulesFromString(value) + } + } + if len(settings1.Key) == 0 { + fmt.Println("The tables may not exist. Please run 'filebrowser config init' first") + return &settings1, nil + } + return &settings1, nil +} + +func (s settingsBackend) Save(ss *settings.Settings) error { + fields := []string{"Key", "Signup", "CreateUserDir", "UserHomeBasePath", "Defaults", "AuthMethod", "Branding", "Commands", "Shell", "Rules"} + values := []string{ + bytesToString(ss.Key), + boolToString(ss.Signup), + boolToString(ss.CreateUserDir), + ss.UserHomeBasePath, + userDefaultsToString(ss.Defaults), + string(ss.AuthMethod), + brandingToString(ss.Branding), + commandsToString(ss.Commands), + stringsToString(ss.Shell), + RulesToString(ss.Rules), + } + tx, err := s.db.Begin() + if checkError(err, "Fail to begin db transaction") { + return err + } + table := quoteName(s.dbType, SettingsTable) + k := quoteName(s.dbType, "key") + p1 := placeHolder(s.dbType, 1) + p2 := placeHolder(s.dbType, 2) + for i, field := range fields { + exists := ContainKey(s.db, s.dbType, field) + sql := fmt.Sprintf("INSERT INTO %s (value, %s) VALUES(%s,%s);", table, k, p1, p2) + if exists { + sql = fmt.Sprintf("UPDATE %s set value = %s where %s = %s;", table, p1, k, p2) + } + stmt, err := s.db.Prepare(sql) + defer stmt.Close() + if checkError(err, "Fail to prepare statement") { + tx.Rollback() + break + } + _, err = stmt.Exec(values[i], field) + if checkError(err, "Fail to insert field "+field+" of settings") { + tx.Rollback() + break + } + } + err = tx.Commit() + if checkError(err, "Fail to commit") { + tx.Rollback() + return err + } + return err +} + +var defaultSettings = settings.Settings{ + Key: []byte(""), + Signup: false, + CreateUserDir: false, + UserHomeBasePath: "/users", + Defaults: settings.UserDefaults{ + Scope: ".", + Locale: "en", + ViewMode: "mosaic", + SingleClick: false, + Sorting: files.Sorting{ + By: "", + Asc: false, + }, + Perm: users.Permissions{ + Admin: false, + Execute: true, + Create: true, + Rename: true, + Modify: true, + Delete: true, + Share: true, + Download: true, + }, + Commands: make([]string, 0), + HideDotfiles: false, + DateFormat: false, + }, + AuthMethod: auth.MethodJSONAuth, + Branding: settings.Branding{ + Name: "", + DisableExternal: false, + Files: "", + Theme: "", + Color: "", + }, + Commands: make(map[string][]string), + Shell: make([]string, 0), + Rules: make([]rules.Rule, 0), +} + +func cloneSettings(s settings.Settings) settings.Settings { + data, err := json.Marshal(s) + s1 := settings.Settings{} + if checkError(err, "Fail to clone settings.Settings") { + return s1 + } + json.Unmarshal(data, &s1) + return s1 +} + +func SetSetting(db *sql.DB, dbType string, key string, value string) error { + t := quoteName(dbType, SettingsTable) + k := quoteName(dbType, "key") + sql := fmt.Sprintf("select count(%s) from %s where %s = '%s';", k, t, k, key) + count := 0 + err := db.QueryRow(sql).Scan(&count) + if checkError(err, "Fail to QueryRow for key="+key) { + return err + } + if count == 0 { + return addSetting(db, dbType, key, value) + } + return updateSetting(db, dbType, key, value) +} + +func GetSetting(db *sql.DB, dbType string, key string) string { + sql := fmt.Sprintf("select value from %s where %s = '%s';", quoteName(dbType, SettingsTable), quoteName(dbType, "key"), key) + value := "" + err := db.QueryRow(sql).Scan(&value) + if checkError(err, "") { + return value + } + return value +} + +func addSetting(db *sql.DB, dbType string, key string, value string) error { + table := quoteName(dbType, SettingsTable) + k := quoteName(dbType, "key") + p1 := placeHolder(dbType, 1) + p2 := placeHolder(dbType, 2) + sql := fmt.Sprintf("insert into %s (%s, value) values(%s, %s);", table, k, p1, p2) + stmt, err := db.Prepare(sql) + if checkError(err, "Fail to prepare sql") { + return err + } + _, err = stmt.Exec(key, value) + checkError(err, "Fail to add settings") + return err +} + +func updateSetting(db *sql.DB, dbType string, key string, value string) error { + sql := fmt.Sprintf( + "update %s set value = %s where %s = %s;", + quoteName(dbType, SettingsTable), + placeHolder(dbType, 1), + quoteName(dbType, "key"), + placeHolder(dbType, 2), + ) + stmt, err := db.Prepare(sql) + if checkError(err, "Fail to prepare sql") { + return err + } + _, err = stmt.Exec(key, value) + checkError(err, "Fail to updateSetting") + return err +} + +func HadSetting(db *sql.DB) bool { + dbType, err := GetDBType(db) + if checkError(err, "Fail to get db type") { + return false + } + key := GetSetting(db, dbType, "Key") + if key == "" { + return false + } + return true +} + +func ContainKey(db *sql.DB, dbType string, key string) bool { + sql := fmt.Sprintf("select value from %s where %s = '%s';", quoteName(dbType, SettingsTable), quoteName(dbType, "key"), key) + value := "" + err := db.QueryRow(sql).Scan(&value) + if checkError(err, "") { + return false + } + return true +} + +func HadSettingOfKey(db *sql.DB, dbType string, key string) bool { + return GetSetting(db, dbType, "Key") == key +} + +func newSettingsBackend(db *sql.DB, dbType string) settingsBackend { + InitSettingsTable(db, dbType) + return settingsBackend{db: db, dbType: dbType} +} diff --git a/storage/sql/share.go b/storage/sql/share.go new file mode 100644 index 00000000..b286647b --- /dev/null +++ b/storage/sql/share.go @@ -0,0 +1,106 @@ +package sql + +import ( + "database/sql" + "fmt" + + "github.com/filebrowser/filebrowser/v2/share" +) + +type shareBackend struct { + db *sql.DB + dbType string +} + +type linkRecord interface { + Scan(dest ...interface{}) error +} + +func InitSharesTable(db *sql.DB, dbType string) error { + sql := fmt.Sprintf("create table if not exists %s (hash text, path text, userid integer, expire integer, passwordhash text, token text)", quoteName(dbType, SharesTable)) + _, err := db.Exec(sql) + checkError(err, "Fail to InitSharesTable") + return err +} + +func parseLink(row linkRecord) (*share.Link, error) { + path := "" + hash := "" + userid := uint(0) + expire := int64(0) + passwordhash := "" + token := "" + err := row.Scan(&path, &hash, &userid, &expire, &passwordhash, &token) + if checkError(err, "Fail to parse record for share.Link") { + return nil, err + } + link := share.Link{} + link.Path = path + link.Hash = hash + link.UserID = userid + link.Expire = expire + link.PasswordHash = passwordhash + link.Token = token + return &link, nil +} + +func queryLinks(db *sql.DB, dbType string, condition string) ([]*share.Link, error) { + sql := fmt.Sprintf("select hash, path, userid, expire, passwordhash, token from %s", quoteName(dbType, SharesTable)) + if len(condition) > 0 { + sql = sql + " where " + condition + } + rows, err := db.Query(sql) + if checkError(err, "Fail to Query links") { + return nil, err + } + var links []*share.Link = []*share.Link{} + for rows.Next() { + link, err := parseLink(rows) + if checkError(err, "Fail to parse record for share.Link") { + continue + } + links = append(links, link) + } + return links, nil +} + +func (s shareBackend) All() ([]*share.Link, error) { + return queryLinks(s.db, s.dbType, "") +} + +func (s shareBackend) FindByUserID(id uint) ([]*share.Link, error) { + condition := fmt.Sprintf("userid=%d", id) + return queryLinks(s.db, s.dbType, condition) +} + +func (s shareBackend) GetByHash(hash string) (*share.Link, error) { + sql := fmt.Sprintf("select hash, path, userid, expire, passwordhash, token from %s where hash='%s'", quoteName(s.dbType, SharesTable), hash) + return parseLink(s.db.QueryRow(sql)) +} + +func (s shareBackend) GetPermanent(path string, id uint) (*share.Link, error) { + sql := fmt.Sprintf("select hash, path, userid, expire, passwordhash, token from %s where path='%s' and userid=%d", quoteName(s.dbType, SharesTable), path, id) + return parseLink(s.db.QueryRow(sql)) +} + +func (s shareBackend) Gets(path string, id uint) ([]*share.Link, error) { + condition := fmt.Sprintf("userid=%d and path='%s'", id, path) + return queryLinks(s.db, s.dbType, condition) +} +func (s shareBackend) Save(l *share.Link) error { + sql := fmt.Sprintf("insert into %s (hash, path, userid, expire, passwordhash, token) values('%s', '%s', %d, %d, '%s', '%s')", quoteName(s.dbType, SharesTable), l.Hash, l.Path, l.UserID, l.Expire, l.PasswordHash, l.Token) + _, err := s.db.Exec(sql) + checkError(err, "Fail to Save share") + return err +} +func (s shareBackend) Delete(hash string) error { + sql := fmt.Sprintf("DELETE FROM %s WHERE hash='%s'", quoteName(s.dbType, SharesTable), hash) + _, err := s.db.Exec(sql) + checkError(err, "Fail to Delete share") + return err +} + +func newShareBackend(db *sql.DB, dbType string) shareBackend { + InitSharesTable(db, dbType) + return shareBackend{db: db, dbType: dbType} +} diff --git a/storage/sql/sql.go b/storage/sql/sql.go new file mode 100644 index 00000000..1273ffdb --- /dev/null +++ b/storage/sql/sql.go @@ -0,0 +1,195 @@ +package sql + +import ( + "database/sql" + "errors" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/filebrowser/filebrowser/v2/auth" + "github.com/filebrowser/filebrowser/v2/settings" + "github.com/filebrowser/filebrowser/v2/share" + "github.com/filebrowser/filebrowser/v2/storage" + "github.com/filebrowser/filebrowser/v2/users" + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" +) + +func init() { +} + +type DBConnectionRecord struct { + db *sql.DB + dbType string + path string +} + +var ( + dbRecords map[string]*DBConnectionRecord = map[string]*DBConnectionRecord{} +) + +// GetDBType used to get the driver type of a sql.DB +// It is based on existing dbRecords +// All sql.DB should opened by OpenDB +func GetDBType(db *sql.DB) (string, error) { + for _, record := range dbRecords { + if record.db == db { + return record.dbType, nil + } + } + return "", errors.New("No such database open by this module") +} + +func getNameQuote(dbType string) string { + if dbType == "mysql" { + return "`" + } + return "\"" +} + +// for mysql, it is “ +// for postgres and sqlite, it is "" +func quoteName(dbType string, name string) string { + q := getNameQuote(dbType) + return q + name + q +} + +// placeholder for sql stmt +// for postgres, it is $1, $2, $3... +// for mysql and sqlite3, it is ?,?,?... +func placeHolder(dbType string, index int) string { + if index <= 0 { + panic("the placeholder index should >= 1") + } + if dbType == "postgres" || dbType == "postgresql" { + return fmt.Sprintf("$%d", index) + } + return "?" +} + +func IsDBPath(path string) bool { + prefixes := []string{"sqlite3", "postgres", "postgresql", "mysql"} + for _, prefix := range prefixes { + if strings.HasPrefix(path, prefix+"://") { + return true + } + } + return false +} + +func OpenDB(path string) (*sql.DB, error) { + if val, ok := dbRecords[path]; ok { + return val.db, nil + } + prefixes := []string{"sqlite3", "postgres", "postgresql", "mysql"} + for _, prefix := range prefixes { + if strings.HasPrefix(path, prefix) { + db, err := connectDB(prefix, path) + if !checkError(err, "Fail to connect database "+path) { + dbRecords[path] = &DBConnectionRecord{db: db, dbType: prefix, path: path} + } + return db, err + } + } + return nil, errors.New("Unsupported db scheme") +} + +type DatabaseResource struct { + scheme string + username string + password string + host string + port int + database string +} + +func ParseDatabasePath(path string) (*DatabaseResource, error) { + pattern := "^(([a-zA-Z0-9]+)://)?(([^:]+)(:(.*))?@)?([a-zA-Z0-9_.]+)(:([0-9]+))?(/([a-zA-Z0-9_-]+))?$" + reg, err := regexp.Compile(pattern) + if checkError(err, "Fail to compile regexp") { + return nil, err + } + matches := reg.FindAllStringSubmatch(path, -1) + if matches == nil || len(matches) == 0 { + return nil, errors.New("Fail to parse database") + } + r := DatabaseResource{} + r.scheme = matches[0][2] + r.username = matches[0][4] + r.password = matches[0][6] + r.host = matches[0][7] + if len(matches[0][9]) > 0 { + port, err := strconv.Atoi(matches[0][9]) + if !checkError(err, "Fail to parse port") { + r.port = port + } + } + r.database = matches[0][11] + return &r, nil +} + +// mysql://user:password@host:port/db => mysql://user:password@tcp(host:port)/db +func transformMysqlPath(path string) (string, error) { + r, err := ParseDatabasePath(path) + if checkError(err, "Fail to parse database path") { + return "", err + } + scheme := r.scheme + if len(scheme) == 0 { + scheme = "mysql" + } + credential := "" + if len(r.username) > 0 && len(r.password) > 0 { + credential = r.username + ":" + r.password + "@" + } else if len(r.username) > 0 { + credential = r.username + "@" + } + host := r.host + port := r.port + if port == 0 { + port = 3306 + } + if len(r.database) == 0 { + return "", errors.New("no database found in path") + } + return fmt.Sprintf("%s://%stcp(%s:%d)/%s", scheme, credential, host, port, r.database), nil +} + +func connectDB(dbType string, path string) (*sql.DB, error) { + if dbType == "sqlite3" && strings.HasPrefix(path, "sqlite3://") { + path = strings.TrimPrefix(path, "sqlite3://") + } else if dbType == "mysql" && strings.HasPrefix(path, "mysql://") { + p, err := transformMysqlPath(path) + if checkError(err, "Fail to parse mysql path") { + return nil, err + } + path = p + path = strings.TrimPrefix(path, "mysql://") + } + db, err := sql.Open(dbType, path) + if err == nil { + return db, nil + } + return nil, err +} + +func NewStorage(db *sql.DB) (*storage.Storage, error) { + dbType, err := GetDBType(db) + checkError(err, "Fail to get database type, maybe this sql.DB is not opened by OpenDB") + + userStore := users.NewStorage(newUsersBackend(db, dbType)) + shareStore := share.NewStorage(newShareBackend(db, dbType)) + settingsStore := settings.NewStorage(newSettingsBackend(db, dbType)) + authStore := auth.NewStorage(newAuthBackend(db, dbType), userStore) + + storage := &storage.Storage{ + Auth: authStore, + Users: userStore, + Share: shareStore, + Settings: settingsStore, + } + return storage, nil +} diff --git a/storage/sql/users.go b/storage/sql/users.go new file mode 100644 index 00000000..fc72ddf5 --- /dev/null +++ b/storage/sql/users.go @@ -0,0 +1,372 @@ +package sql + +import ( + "database/sql" + "encoding/json" + "fmt" + "reflect" + "strings" + + "github.com/filebrowser/filebrowser/v2/errors" + "github.com/filebrowser/filebrowser/v2/files" + "github.com/filebrowser/filebrowser/v2/rules" + "github.com/filebrowser/filebrowser/v2/users" +) + +type usersBackend struct { + db *sql.DB + dbType string +} + +func PermFromString(s string) users.Permissions { + var perm users.Permissions + if s == "" { + return perm + } + err := json.Unmarshal([]byte(s), &perm) + checkError(err, "Fail to parse perm from string") + return perm +} + +func PermToString(perm users.Permissions) string { + data, err := json.Marshal(perm) + if checkError(err, "Fail to stringify users.Permissions") { + return "" + } + return string(data) +} + +func CommandsFromString(s string) []string { + if s == "" { + return make([]string, 0) + } + var commands []string + err := json.Unmarshal([]byte(s), &commands) + checkError(err, "Fail to parse users Commands") + return commands +} + +func CommandsToString(commands []string) string { + data, err := json.Marshal(commands) + if checkError(err, "Fail to stringify users commands") { + return "" + } + return string(data) +} + +func SortingFromString(s string) files.Sorting { + if s == "" { + return files.Sorting{} + } + var sorting files.Sorting + err := json.Unmarshal([]byte(s), &sorting) + checkError(err, "Fail to parse Sorting from string") + return sorting +} + +func SortingToString(sorting files.Sorting) string { + data, err := json.Marshal(sorting) + if checkError(err, "Fail to stringify files.Sorting") { + return "" + } + return string(data) +} + +func rulesFromString(s string) []rules.Rule { + rules := make([]rules.Rule, 0) + if s == "" { + return rules + } + err := json.Unmarshal([]byte(s), &rules) + checkError(err, "Fail to parse Rules from string") + return rules +} + +func RulesToString(rules []rules.Rule) string { + data, err := json.Marshal(rules) + if checkError(err, "Fail to stringify []rules.Rule") { + return "" + } + return string(data) +} + +var adminUser = createAdminUser() + +func createAdminUser() users.User { + userDefault := defaultSettings.Defaults + user := users.User{} + user.Username = "admin" + user.Password = "admin" + user.Scope = userDefault.Scope + user.LockPassword = false + user.ViewMode = userDefault.ViewMode + user.Perm = users.Permissions{ + Admin: true, + Execute: true, + Create: true, + Rename: true, + Modify: true, + Delete: true, + Share: true, + Download: true, + } + user.Commands = userDefault.Commands + user.Sorting = userDefault.Sorting + return user +} + +func InitUserTable(db *sql.DB, dbType string) error { + primaryKey := "integer primary key" + if dbType == "postgres" || dbType == "postgresql" { + primaryKey = "serial primary key" + } else if dbType == "mysql" { + primaryKey = "int unsigned primary key auto_increment" + } + sql := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id %s, username text, password text, scope text, locale text, lockpassword integer, viewmode text, perm text, commands text, sorting text, rules text, hidedotfiles integer, dateformat integer, singleclick integer);", quoteName(dbType, UsersTable), primaryKey) + _, err := db.Exec(sql) + checkError(err, "Fail to create users table") + return err +} + +func newUsersBackend(db *sql.DB, dbType string) usersBackend { + InitUserTable(db, dbType) + return usersBackend{db: db, dbType: dbType} +} + +func (s usersBackend) IsPostgresql() bool { + return s.dbType == "postgres" || s.dbType == "postgresql" +} + +func (s usersBackend) IsMysql() bool { + return s.dbType == "mysql" +} + +func (s usersBackend) IsSqlite() bool { + return s.dbType == "sqlite3" +} + +func (s usersBackend) GetBy(i interface{}) (*users.User, error) { + columns := []string{"id", "username", "password", "scope", "locale", "lockpassword", "viewmode", "perm", "commands", "sorting", "rules", "hidedotfiles", "dateformat", "singleclick"} + columnsStr := strings.Join(columns, ",") + var conditionStr string + switch i.(type) { + case uint: + conditionStr = fmt.Sprintf("id=%v", i) + case int: + conditionStr = fmt.Sprintf("id=%v", i) + case string: + conditionStr = fmt.Sprintf("username='%v'", i) + default: + return nil, errors.ErrInvalidDataType + } + userID := uint(0) + username := "" + password := "" + scope := "" + locale := "" + lockpassword := false + var viewmode users.ViewMode = users.ListViewMode + perm := "" + commands := "" + sorting := "" + rules := "" + hidedotfiles := false + dateformat := false + singleclick := false + user := users.User{} + sql := fmt.Sprintf("SELECT %s FROM %s WHERE %s", columnsStr, quoteName(s.dbType, UsersTable), conditionStr) + err := s.db.QueryRow(sql).Scan(&userID, &username, &password, &scope, &locale, &lockpassword, &viewmode, &perm, &commands, &sorting, &rules, &hidedotfiles, &dateformat, &singleclick) + if checkError(err, "") { + return nil, err + } + user.ID = userID + user.Username = username + user.Password = password + user.Scope = scope + user.Locale = locale + user.LockPassword = lockpassword + user.ViewMode = viewmode + user.Perm = PermFromString(perm) + user.Commands = CommandsFromString(commands) + user.Sorting = SortingFromString(sorting) + user.Rules = rulesFromString(rules) + user.HideDotfiles = hidedotfiles + user.DateFormat = dateformat + user.SingleClick = singleclick + return &user, nil +} + +func (s usersBackend) Gets() ([]*users.User, error) { + sql := fmt.Sprintf("SELECT id, username, password, scope, lockpassword, viewmode, perm,commands,sorting,rules FROM %s", quoteName(s.dbType, UsersTable)) + rows, err := s.db.Query(sql) + if checkError(err, "Fail to Query []*users.User") { + return nil, err + } + var users2 []*users.User = make([]*users.User, 0) + for rows.Next() { + id := 0 + username := "" + password := "" + scope := "" + lockpassword := false + var viewmode users.ViewMode = "list" + perm := "" + commands := "" + sorting := "" + rules := "" + err := rows.Scan(&id, &username, &password, &scope, &lockpassword, &viewmode, &perm, &commands, &sorting, &rules) + if checkError(err, "Fail to parse record for user.User") { + continue + } + user := users.User{} + user.ID = uint(id) + user.Username = username + user.Password = password + user.Scope = scope + user.LockPassword = lockpassword + user.ViewMode = viewmode + user.Perm = PermFromString(perm) + user.Commands = CommandsFromString(commands) + user.Sorting = SortingFromString(sorting) + user.Rules = rulesFromString(rules) + + users2 = append(users2, &user) + } + return users2, nil +} + +func (s usersBackend) updateUser(id uint, user *users.User) error { + lockpassword := 0 + if user.LockPassword { + lockpassword = 1 + } + sql := fmt.Sprintf( + "UPDATE %s SET username='%s',password='%s',scope='%s',lockpassword=%d,viewmode='%s',perm='%s',commands='%s',sorting='%s',rules='%s' WHERE id=%d", + quoteName(s.dbType, UsersTable), + user.Username, + user.Password, + user.Scope, + lockpassword, + user.ViewMode, + PermToString(user.Perm), + CommandsToString(user.Commands), + SortingToString(user.Sorting), + RulesToString(user.Rules), + user.ID, + ) + _, err := s.db.Exec(sql) + checkError(err, "Fail to update user") + return err +} + +func (s usersBackend) insertUser(user *users.User) error { + columnSpec := [][]string{ + {"username", "'%s'"}, + {"password", "'%s'"}, + {"scope", "'%s'"}, + {"locale", "'%s'"}, + {"lockpassword", "%s"}, + {"viewmode", "'%s'"}, + {"perm", "'%s'"}, + {"commands", "'%s'"}, + {"sorting", "'%s'"}, + {"rules", "'%s'"}, + {"hidedotfiles", "%s"}, + {"dateformat", "%s"}, + {"singleclick", "%s"}, + } + columns := []string{} + specs := []string{} + for _, c := range columnSpec { + columns = append(columns, c[0]) + specs = append(specs, c[1]) + } + columnStr := strings.Join(columns, ",") + specStr := strings.Join(specs, ",") + sqlFormat := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", quoteName(s.dbType, UsersTable), columnStr, specStr) + if s.IsPostgresql() { + sqlFormat = sqlFormat + " RETURNING id;" + } + sql := fmt.Sprintf( + sqlFormat, + user.Username, + user.Password, + user.Scope, + user.Locale, + boolToString(user.LockPassword), + user.ViewMode, + PermToString(user.Perm), + CommandsToString(user.Commands), + SortingToString(user.Sorting), + RulesToString(user.Rules), + boolToString(user.HideDotfiles), + boolToString(user.DateFormat), + boolToString(user.SingleClick), + ) + if s.IsPostgresql() { + id := uint(0) + err := s.db.QueryRow(sql).Scan(&id) + if !checkError(err, "Fail to insert user") { + user.ID = id + } + return err + } + res, err := s.db.Exec(sql) + if !checkError(err, "Fail to insert user") { + id, err := res.LastInsertId() + checkError(err, "Fail to get last inserted id") + user.ID = uint(id) + } + return err +} + +func (s usersBackend) Save(user *users.User) error { + userOriginal, err := s.GetBy(user.Username) + checkError(err, "") + if userOriginal != nil { + return s.updateUser(user.ID, user) + } + return s.insertUser(user) +} + +func (s usersBackend) DeleteByID(id uint) error { + sql := fmt.Sprintf("delete from %s where id=%d", quoteName(s.dbType, UsersTable), id) + _, err := s.db.Exec(sql) + checkError(err, "Fail to delete User by id") + return err +} + +func (s usersBackend) DeleteByUsername(username string) error { + sql := fmt.Sprintf("delete from %s where username='%s'", quoteName(s.dbType, UsersTable), username) + _, err := s.db.Exec(sql) + checkError(err, "Fail to delete user by username") + return err +} + +func (s usersBackend) Update(u *users.User, fields ...string) error { + if len(fields) == 0 { + return s.Save(u) + } + var setItems = []string{} + for _, field := range fields { + userField := reflect.ValueOf(u).Elem().FieldByName(field) + if !userField.IsValid() { + continue + } + field = strings.ToLower(field) + val := userField.Interface() + typeStr := reflect.TypeOf(val).Kind().String() + if typeStr == "string" { + setItems = append(setItems, fmt.Sprintf("%s='%s'", field, val)) + } else if typeStr == "bool" { + setItems = append(setItems, fmt.Sprintf("%s=%s", field, boolToString(val.(bool)))) + } else { + // TODO + setItems = append(setItems, fmt.Sprintf("%s=%s", field, val)) + } + } + sql := fmt.Sprintf("UPDATE %s SET %s WHERE id=%d", UsersTable, strings.Join(setItems, ","), u.ID) + _, err := s.db.Exec(sql) + checkError(err, "Fail to update user") + return err +} diff --git a/storage/sql/utils.go b/storage/sql/utils.go new file mode 100644 index 00000000..db6f104e --- /dev/null +++ b/storage/sql/utils.go @@ -0,0 +1,71 @@ +package sql + +import ( + "log" + "path/filepath" + "runtime" + "strings" +) + +func getRuntimeFunctionName(frame uint) string { + pc := make([]uintptr, 1) + count := runtime.Callers(int(frame)+2, pc) + if count == 0 { + return "" + } + f := runtime.FuncForPC(pc[0]) + return f.Name() +} + +func checkError(err error, message string) bool { + if err != nil { + if len(message) > 0 { + funcname := filepath.Base(getRuntimeFunctionName(1)) + log.Printf("ERROR [%s]: %s\n", funcname, err.Error()) + log.Printf("ERROR [%s]: %s\n", funcname, message) + } + return true + } + return false +} + +func logFunction() { + funcname := getRuntimeFunctionName(1) + log.Printf("%s is running\n", funcname) +} + +func reverse(list []string) []string { + var output []string + for i := len(list) - 1; i >= 0; i-- { + output = append(output, list[i]) + } + return output +} + +func logBacktrace() { + funcs := make([]string, 0) + for _, i := range []int{1, 2, 3} { + p := filepath.Base(getRuntimeFunctionName(uint(i))) + if len(p) > 0 { + funcs = append(funcs, p) + } else { + break + } + } + funcs = reverse(funcs) + log.Printf("%s\n", strings.Join(funcs, " -> ")) +} + +func LogBacktrace() { + funcs := make([]string, 0) + for _, i := range []int{1, 2, 3} { + p := filepath.Base(getRuntimeFunctionName(uint(i))) + if len(p) > 0 { + funcs = append(funcs, p) + } else { + break + } + } + funcs = reverse(funcs) + log.Printf("%s\n", strings.Join(funcs, " -> ")) +}