mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2026-10-20 08:16:40 +00:00
461 lines
14 KiB
Go
461 lines
14 KiB
Go
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"os/user"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
. "github.com/mickael-kerjean/filestash/server/pkg/core"
|
|
. "github.com/mickael-kerjean/filestash/server/pkg/env"
|
|
. "github.com/mickael-kerjean/filestash/server/pkg/utils"
|
|
)
|
|
|
|
var Config = NewConfiguration()
|
|
|
|
type Configuration struct {
|
|
state atomic.Pointer[configState]
|
|
}
|
|
|
|
type configState struct {
|
|
forms []Form
|
|
conn []map[string]any
|
|
}
|
|
|
|
type ConfigElement struct {
|
|
key string
|
|
currentElement *FormElement
|
|
cfg *Configuration
|
|
}
|
|
|
|
func InitConfig() error {
|
|
if err := Config.Load(); err != nil {
|
|
return err
|
|
}
|
|
Config.Initialise()
|
|
return nil
|
|
}
|
|
|
|
func NewConfiguration() *Configuration {
|
|
c := &Configuration{}
|
|
c.state.Store(&configState{
|
|
conn: []map[string]any{},
|
|
forms: []Form{
|
|
Form{
|
|
Title: "general",
|
|
Elmnts: []FormElement{
|
|
FormElement{Name: "name", Type: "text", Default: APPNAME, Description: "Name as shown in the UI", Placeholder: "Default: \"" + APPNAME + "\""},
|
|
FormElement{Name: "port", Type: "number", Default: defaultValue(8334, "FILESTASH_PORT"), Description: "Port on which the application is available.", Placeholder: "Default: 8334"},
|
|
FormElement{Name: "host", Type: "text", Description: "The host people need to use to access this server", Placeholder: WhiteLabelText("Eg: \"demo.filestash.app\"", "Eg: \"files.yourcompany.com\"")},
|
|
FormElement{Name: "secret_key", Type: "password", Required: true, Pattern: "[a-zA-Z0-9]{16}", Description: "The key that's used to encrypt and decrypt content. Update this settings will invalidate existing user sessions and shared links, use with caution!"},
|
|
FormElement{Name: "force_ssl", Type: "boolean", Description: "Enable the web security mechanism called 'Strict Transport Security'"},
|
|
FormElement{Name: "editor", Type: "select", Default: "emacs", Opts: []string{"base", "emacs", "vim"}, Description: "Keybinding to be use in the editor. Default: \"emacs\""},
|
|
FormElement{Name: "logout", Type: "text", Default: "", Description: "Redirection URL whenever user click on the logout button"},
|
|
FormElement{Name: "display_hidden", Type: "boolean", Default: false, Description: "Should files starting with a dot be visible by default?"},
|
|
FormElement{Name: "refresh_after_upload", Type: "boolean", Default: false, Description: "Refresh directory listing after upload"},
|
|
FormElement{Name: "open_mode", Type: "select", Default: "single_click", Opts: []string{"single_click", "double_click"}, Description: "How files and folders are opened in the file browser"},
|
|
FormElement{Name: "upload_button", Type: "boolean", Default: false, Description: "Display the upload button on any device"},
|
|
FormElement{Name: "upload_pool_size", Type: "number", Default: 15, Description: "Maximum number of files upload in parallel. Default: 15"},
|
|
FormElement{Name: "upload_chunk_size", Type: "number", Default: 0, Description: "Size of Chunks for Uploads in MB."},
|
|
FormElement{Name: "buffer_size", Type: "select", Default: "medium", Opts: []string{"small", "medium", "large"}, Description: "I/O buffer size for transfers. Larger buffers boost throughput on 20 GbE+ networks but use more memory."},
|
|
FormElement{Name: "filepage_default_view", Type: "select", Default: "grid", Opts: []string{"list", "grid"}, Description: "Default layout for files and folder on the file page"},
|
|
FormElement{Name: "filepage_default_sort", Type: "select", Default: "type", Opts: []string{"type", "date", "name"}, Description: "Default order for files and folder on the file page"},
|
|
FormElement{Name: "cookie_timeout", Type: "number", Default: 60 * 24 * 7, Description: "Authentication Cookie expiration in minutes. Default: 60 * 24 * 7 = 1 week"},
|
|
FormElement{Name: "extended_session", Type: "boolean", Default: false, Description: "Store extra auth data in session"},
|
|
FormElement{Name: "custom_css", Type: "long_text", Default: "", Description: "Setcustom css code for your instance"},
|
|
},
|
|
},
|
|
Form{
|
|
Title: "features",
|
|
Form: []Form{
|
|
Form{
|
|
Title: "api",
|
|
Elmnts: []FormElement{
|
|
FormElement{Name: "enable", Type: "boolean", Default: true, Description: "Enable/Disable the API"},
|
|
},
|
|
},
|
|
Form{
|
|
Title: "share",
|
|
Elmnts: []FormElement{
|
|
FormElement{Name: "enable", Type: "boolean", Default: true, Description: "Enable/Disable the share feature"},
|
|
FormElement{Name: "default_access", Type: "select", Default: "editor", Opts: []string{"editor", "viewer"}, Description: "Default access for shared links"},
|
|
FormElement{Name: "redirect", Type: "text", Placeholder: "redirection URL", Description: "When set, shared links will perform a redirection to another link. Example: https://example.com?full_path={{path}}"},
|
|
},
|
|
},
|
|
Form{
|
|
Title: "protection",
|
|
Elmnts: []FormElement{
|
|
FormElement{Name: "iframe", Type: "text", Default: "", Description: "list of domains who can use the application from an iframe. eg: https://example.com"},
|
|
FormElement{Name: "enable_chromecast", Type: "boolean", Default: true, Description: "Enable users to stream content on a chromecast device. This feature requires the browser to access google's server to download the chromecast SDK."},
|
|
FormElement{Name: "signature", Type: "text", Default: "", Description: "Enforce signature when using URL parameters in the authentication process"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Form{
|
|
Title: "log",
|
|
Elmnts: []FormElement{
|
|
FormElement{Name: "enable", Type: "enable", Target: []string{"log_level"}, Default: true},
|
|
FormElement{Name: "level", Type: "select", Default: defaultValue("INFO", "LOG_LEVEL"), Opts: []string{"DEBUG", "INFO", "WARNING", "ERROR"}, Id: "log_level", Description: "Default: \"INFO\". This setting determines the level of detail at which log events are written to the log file"},
|
|
FormElement{Name: "telemetry", Type: "boolean", Default: false, Description: "We won't share anything with any third party. This will only to be used to improve our software"},
|
|
},
|
|
},
|
|
Form{
|
|
Title: "email",
|
|
Elmnts: []FormElement{
|
|
FormElement{Name: "server", Type: "text", Default: "smtp.gmail.com", Description: "Address of the SMTP email server.", Placeholder: "Default: smtp.gmail.com"},
|
|
FormElement{Name: "port", Type: "number", Default: 587, Description: "Port of the SMTP email server. Eg: 587", Placeholder: "Default: 587"},
|
|
FormElement{Name: "username", Type: "text", Description: "The username for authenticating to the SMTP server.", Placeholder: "Eg: username@gmail.com"},
|
|
FormElement{Name: "password", Type: "password", Description: "The password associated with the SMTP username.", Placeholder: "Eg: Your google password"},
|
|
FormElement{Name: "from", Type: "text", Description: "Email address visible on sent messages.", Placeholder: "Eg: username@gmail.com"},
|
|
},
|
|
},
|
|
Form{
|
|
Title: "auth",
|
|
Elmnts: []FormElement{
|
|
FormElement{Name: "admin", Type: "bcrypt", Default: "", Description: "Password of the admin section."},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
return c
|
|
}
|
|
|
|
func (this *Configuration) Connections() []map[string]any {
|
|
return this.state.Load().conn
|
|
}
|
|
|
|
func (this *Configuration) Load() error {
|
|
cFile, err := LoadConfig()
|
|
if err != nil {
|
|
Log.Error("config::load %s", err)
|
|
return err
|
|
}
|
|
|
|
// Extract enabled backends
|
|
var d struct {
|
|
Connections []map[string]any `json:"connections"`
|
|
}
|
|
json.Unmarshal(cFile, &d)
|
|
conn := []map[string]any{}
|
|
if d.Connections != nil {
|
|
conn = d.Connections
|
|
}
|
|
|
|
// Hydrate Config with data coming from the config file
|
|
var raw map[string]any
|
|
json.Unmarshal(cFile, &raw)
|
|
for {
|
|
old := this.state.Load()
|
|
next := &configState{forms: cloneForms(old.forms), conn: conn}
|
|
for path, value := range flattenJSON("", raw) {
|
|
el := find(&next.forms, path, true)
|
|
if el != nil && el.Value != value {
|
|
el.Value = value
|
|
}
|
|
}
|
|
if this.state.CompareAndSwap(old, next) {
|
|
break
|
|
}
|
|
}
|
|
|
|
Log.SetVisibility(this.Get("log.level").String())
|
|
for _, fn := range changeListeners {
|
|
fn()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func flattenJSON(prefix string, m map[string]any) map[string]any {
|
|
out := map[string]any{}
|
|
for k, v := range m {
|
|
key := k
|
|
if prefix != "" {
|
|
key = prefix + "." + k
|
|
}
|
|
switch val := v.(type) {
|
|
case map[string]any:
|
|
for nk, nv := range flattenJSON(key, val) {
|
|
out[nk] = nv
|
|
}
|
|
case []any:
|
|
default:
|
|
out[key] = val
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (this *Configuration) Initialise() {
|
|
shouldSave := false
|
|
if env := os.Getenv("ADMIN_PASSWORD"); env != "" {
|
|
shouldSave = true
|
|
this.Get("auth.admin").Set(env)
|
|
}
|
|
if env := os.Getenv("APPLICATION_URL"); env != "" {
|
|
shouldSave = true
|
|
_ = this.Get("general.host").Set(env).String()
|
|
}
|
|
if this.Get("general.secret_key").String() == "" {
|
|
shouldSave = true
|
|
this.Get("general.secret_key").Set(RandomString(16))
|
|
}
|
|
if shouldSave {
|
|
this.Save()
|
|
}
|
|
InitSecretDerivate(this.Get("general.secret_key").String())
|
|
}
|
|
|
|
func (this *Configuration) Save() {
|
|
s := this.state.Load()
|
|
formBytes, err := FormToJSON(Form{Form: s.forms}, func(el FormElement) any { return el.Value })
|
|
conn, _ := json.Marshal(s.conn)
|
|
if err != nil {
|
|
Log.Error("config::save marshal %s", err.Error())
|
|
return
|
|
}
|
|
var buf bytes.Buffer
|
|
buf.WriteByte('{')
|
|
inner := formBytes[1 : len(formBytes)-1]
|
|
if len(inner) > 0 {
|
|
buf.Write(inner)
|
|
buf.WriteByte(',')
|
|
}
|
|
buf.WriteString(`"connections":`)
|
|
buf.Write(conn)
|
|
buf.WriteByte('}')
|
|
if err := SaveConfig(PrettyPrint(buf.Bytes())); err != nil {
|
|
Log.Error("config::save %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func (this *Configuration) Get(key string) ConfigElement {
|
|
s := this.state.Load()
|
|
el := find(&s.forms, key, false)
|
|
if el == nil {
|
|
for {
|
|
old := this.state.Load()
|
|
next := &configState{forms: cloneForms(old.forms), conn: old.conn}
|
|
el = find(&next.forms, key, true)
|
|
if this.state.CompareAndSwap(old, next) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return ConfigElement{currentElement: el, key: key, cfg: this}
|
|
}
|
|
|
|
func (this ConfigElement) Schema(fn func(*FormElement) *FormElement) ConfigElement {
|
|
current := this.cfg.state.Load()
|
|
if current != nil {
|
|
if el := find(¤t.forms, this.key, false); el != nil {
|
|
probe := *el
|
|
if res := fn(&probe); res != nil && el.Type == res.Type && el.Value == res.Value {
|
|
this.currentElement = el
|
|
return this
|
|
}
|
|
}
|
|
}
|
|
for {
|
|
next := &configState{forms: cloneForms(current.forms), conn: current.conn}
|
|
el := fn(find(&next.forms, this.key, true))
|
|
if this.cfg.state.CompareAndSwap(current, next) {
|
|
this.currentElement = el
|
|
break
|
|
}
|
|
current = this.cfg.state.Load()
|
|
}
|
|
return this
|
|
}
|
|
|
|
func (this ConfigElement) Default(value interface{}) ConfigElement {
|
|
shouldSave := false
|
|
for {
|
|
old := this.cfg.state.Load()
|
|
next := &configState{forms: cloneForms(old.forms), conn: old.conn}
|
|
el := find(&next.forms, this.key, true)
|
|
shouldSave = el.Default == nil
|
|
if !shouldSave {
|
|
if el.Default != value {
|
|
Log.Debug("Attempt to set multiple default config value => %+v", el)
|
|
}
|
|
this.currentElement = el
|
|
break
|
|
}
|
|
el.Default = value
|
|
if this.cfg.state.CompareAndSwap(old, next) {
|
|
this.currentElement = el
|
|
break
|
|
}
|
|
}
|
|
if shouldSave {
|
|
this.cfg.Save()
|
|
}
|
|
return this
|
|
}
|
|
|
|
func (this ConfigElement) Set(value interface{}) ConfigElement {
|
|
changed := false
|
|
for {
|
|
old := this.cfg.state.Load()
|
|
next := &configState{forms: cloneForms(old.forms), conn: old.conn}
|
|
el := find(&next.forms, this.key, true)
|
|
changed = el.Value != value
|
|
if !changed {
|
|
this.currentElement = el
|
|
break
|
|
}
|
|
el.Value = value
|
|
if this.cfg.state.CompareAndSwap(old, next) {
|
|
this.currentElement = el
|
|
break
|
|
}
|
|
}
|
|
if changed {
|
|
this.cfg.Save()
|
|
}
|
|
return this
|
|
}
|
|
|
|
func (this ConfigElement) String() string {
|
|
switch v := this.Interface().(type) {
|
|
case string:
|
|
return v
|
|
case []byte:
|
|
return string(v)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (this ConfigElement) Int() int {
|
|
switch v := this.Interface().(type) {
|
|
case float64:
|
|
return int(v)
|
|
case int64:
|
|
return int(v)
|
|
case int:
|
|
return v
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (this ConfigElement) Bool() bool {
|
|
if v, ok := this.Interface().(bool); ok {
|
|
return v
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (this ConfigElement) Interface() interface{} {
|
|
if this.currentElement == nil {
|
|
return nil
|
|
} else if this.currentElement.Value == nil {
|
|
return this.currentElement.Default
|
|
}
|
|
return this.currentElement.Value
|
|
}
|
|
|
|
func (this *Configuration) MarshalJSON() ([]byte, error) {
|
|
username := "n/a"
|
|
if u, err := user.Current(); err == nil {
|
|
if u.Username != "" {
|
|
username = u.Username
|
|
} else {
|
|
username = u.Name
|
|
}
|
|
}
|
|
return Form{Form: append(this.state.Load().forms, Form{
|
|
Title: "constant",
|
|
Elmnts: []FormElement{
|
|
{Name: "user", Type: "boolean", ReadOnly: true, Value: username},
|
|
{Name: "license", Type: "text", ReadOnly: true, Value: LICENSE},
|
|
},
|
|
})}.MarshalJSON()
|
|
}
|
|
|
|
func defaultValue[T string | int | bool](dval T, envName string) T {
|
|
if val := os.Getenv(envName); val != "" {
|
|
switch any(dval).(type) {
|
|
case int:
|
|
if n, err := strconv.Atoi(val); err == nil {
|
|
return any(n).(T)
|
|
}
|
|
case bool:
|
|
if b, err := strconv.ParseBool(val); err == nil {
|
|
return any(b).(T)
|
|
}
|
|
default:
|
|
return any(val).(T)
|
|
}
|
|
}
|
|
return dval
|
|
}
|
|
|
|
func cloneForms(forms []Form) []Form {
|
|
out := make([]Form, len(forms))
|
|
for i := range forms {
|
|
out[i] = Form{
|
|
Title: forms[i].Title,
|
|
Form: cloneForms(forms[i].Form),
|
|
Elmnts: append([]FormElement{}, forms[i].Elmnts...),
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func find(forms *[]Form, key string, create bool) *FormElement {
|
|
lastDot := strings.LastIndexByte(key, '.')
|
|
if lastDot < 0 {
|
|
return nil
|
|
}
|
|
name := key[lastDot+1:]
|
|
cur := forms
|
|
for start := 0; ; {
|
|
rel := strings.IndexByte(key[start:lastDot], '.')
|
|
last := rel < 0
|
|
end := lastDot
|
|
if !last {
|
|
end = start + rel
|
|
}
|
|
title := key[start:end]
|
|
var form *Form
|
|
list := *cur
|
|
for i := range list {
|
|
if list[i].Title == title {
|
|
form = &list[i]
|
|
break
|
|
}
|
|
}
|
|
if form == nil {
|
|
if !create {
|
|
return nil
|
|
}
|
|
*cur = append(*cur, Form{Title: title})
|
|
form = &(*cur)[len(*cur)-1]
|
|
}
|
|
if last {
|
|
for j := range form.Elmnts {
|
|
if form.Elmnts[j].Name == name {
|
|
return &form.Elmnts[j]
|
|
}
|
|
}
|
|
if !create {
|
|
return nil
|
|
}
|
|
form.Elmnts = append(form.Elmnts, FormElement{Name: name, Type: "hidden"})
|
|
return &form.Elmnts[len(form.Elmnts)-1]
|
|
}
|
|
cur = &form.Form
|
|
start = end + 1
|
|
}
|
|
}
|
|
|
|
var changeListeners []func()
|
|
|
|
func RegisterChange(fn func()) {
|
|
changeListeners = append(changeListeners, fn)
|
|
}
|