From ddd1b83b273354194ab5d429fc8390cea47cac11 Mon Sep 17 00:00:00 2001 From: Mickael KERJEAN Date: Wed, 24 Oct 2018 11:52:20 +1100 Subject: [PATCH] feature (plugin): API to develop plugin --- docker/prod/Dockerfile | 4 + server/common/app.go | 40 --- .../backend/nothing.go => common/backend.go} | 39 ++- server/common/config.go | 314 ++++++++---------- server/common/config_test.go | 77 +++++ server/common/constants.go | 2 + server/common/crypto.go | 1 + server/common/crypto_test.go | 7 +- server/common/default.go | 33 ++ server/common/files.go | 14 + server/common/helpers.go | 39 --- server/common/log.go | 104 ++++-- server/common/mime.go | 37 +++ server/common/plugin.go | 11 + server/common/types.go | 1 + server/common/utils.go | 17 + server/ctrl/files.go | 17 +- server/ctrl/session.go | 3 +- server/ctrl/share.go | 6 +- server/ctrl/static.go | 6 +- server/ctrl/webdav.go | 12 +- server/main.go | 10 +- server/middleware/http.go | 16 +- server/model/backend/custombackend.go | 42 --- server/model/backend/dropbox.go | 16 +- server/model/backend/ftp.go | 4 +- server/model/backend/gdrive.go | 29 +- server/model/backend/git.go | 6 +- server/model/backend/s3.go | 4 +- server/model/backend/sftp.go | 6 +- server/model/backend/webdav.go | 6 +- server/model/files.go | 58 ++-- server/model/share.go | 18 +- server/plugin/example/build.sh | 3 + server/plugin/example/index.go | 24 ++ server/plugin/image/build.sh | 3 + server/plugin/image/index.go | 109 ++++++ .../images => plugin/image/lib}/raw.c | 0 .../images => plugin/image/lib}/raw.go | 2 +- .../images => plugin/image/lib}/raw.h | 0 .../images => plugin/image/lib}/resizer.c | 0 .../images => plugin/image/lib}/resizer.go | 2 +- .../images => plugin/image/lib}/resizer.h | 0 server/plugin/index.go | 68 ++++ server/services/pipeline.go | 99 ------ server/services/webdav.go | 28 -- 46 files changed, 782 insertions(+), 555 deletions(-) rename server/{model/backend/nothing.go => common/backend.go} (51%) create mode 100644 server/common/config_test.go create mode 100644 server/common/default.go delete mode 100644 server/common/helpers.go create mode 100644 server/common/mime.go create mode 100644 server/common/plugin.go delete mode 100644 server/model/backend/custombackend.go create mode 100755 server/plugin/example/build.sh create mode 100644 server/plugin/example/index.go create mode 100755 server/plugin/image/build.sh create mode 100644 server/plugin/image/index.go rename server/{services/images => plugin/image/lib}/raw.c (100%) rename server/{services/images => plugin/image/lib}/raw.go (98%) rename server/{services/images => plugin/image/lib}/raw.h (100%) rename server/{services/images => plugin/image/lib}/resizer.c (100%) rename server/{services/images => plugin/image/lib}/resizer.go (98%) rename server/{services/images => plugin/image/lib}/resizer.h (100%) create mode 100644 server/plugin/index.go delete mode 100644 server/services/pipeline.go delete mode 100644 server/services/webdav.go diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 5f98bcf4..a5c5a693 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -42,6 +42,10 @@ RUN mkdir -p /tmp/go/src/github.com/mickael-kerjean/ && \ cd ../ && \ GOPATH=/tmp/go go build -o ./dist/nuage ./server/main.go && \ ################# + # Compile Plugins + cd /tmp/go/src/github.com/mickael-kerjean/nuage/server/plugin && + cd image && ./build.sh && cd ../ && + ################# # Finalise the build apk --no-cache add ca-certificates && \ mv dist /app && \ diff --git a/server/common/app.go b/server/common/app.go index 51710dfe..d74312ef 100644 --- a/server/common/app.go +++ b/server/common/app.go @@ -1,48 +1,8 @@ package common -import ( - "net" - "net/http" - "os" - "path/filepath" - "time" -) - type App struct { Config *Config - Helpers *Helpers Backend IBackend Body map[string]interface{} Session map[string]string } - -func GetCurrentDir() string { - ex, _ := os.Executable() - return filepath.Dir(ex) -} - -var HTTPClient = http.Client{ - Timeout: 5 * time.Hour, - Transport: &http.Transport{ - Dial: (&net.Dialer{ - Timeout: 10 * time.Second, - KeepAlive: 10 * time.Second, - }).Dial, - TLSHandshakeTimeout: 5 * time.Second, - IdleConnTimeout: 60 * time.Second, - ResponseHeaderTimeout: 60 * time.Second, - }, -} - -var HTTP = http.Client{ - Timeout: 800 * time.Millisecond, - Transport: &http.Transport{ - Dial: (&net.Dialer{ - Timeout: 500 * time.Millisecond, - KeepAlive: 500 * time.Millisecond, - }).Dial, - TLSHandshakeTimeout: 500 * time.Millisecond, - IdleConnTimeout: 500 * time.Millisecond, - ResponseHeaderTimeout: 500 * time.Millisecond, - }, -} diff --git a/server/model/backend/nothing.go b/server/common/backend.go similarity index 51% rename from server/model/backend/nothing.go rename to server/common/backend.go index edd534ae..882a8bf6 100644 --- a/server/model/backend/nothing.go +++ b/server/common/backend.go @@ -1,27 +1,52 @@ -package backend +package common import ( - . "github.com/mickael-kerjean/nuage/server/common" "io" "os" "strings" ) -type Nothing struct { +const BACKEND_NIL = "_nothing_" + +var Backend = NewDriver() + +func NewDriver() Driver { + return Driver{make(map[string]IBackend)} } -func NewNothing(params map[string]string, app *App) (*Nothing, error) { +type Driver struct { + ds map[string]IBackend +} + +func (d *Driver) Register(name string, driver IBackend) { + if driver == nil { + panic("backend: register invalid nil backend") + } + if d.ds[name] != nil { + panic("backend: register already exist") + } + d.ds[name] = driver +} + +func (d *Driver) Get(name string) IBackend { + b := d.ds[name] + if b == nil || name == BACKEND_NIL { + return Nothing{} + } + return b +} + +type Nothing struct {} + +func (b Nothing) Init(params map[string]string, app *App) (IBackend, error) { return &Nothing{}, nil } - func (b Nothing) Info() string { return "N/A" } - func (b Nothing) Ls(path string) ([]os.FileInfo, error) { return nil, NewError("", 401) } - func (b Nothing) Cat(path string) (io.Reader, error) { return strings.NewReader(""), NewError("", 401) } diff --git a/server/common/config.go b/server/common/config.go index 4ff83637..c4c61067 100644 --- a/server/common/config.go +++ b/server/common/config.go @@ -1,198 +1,188 @@ package common import ( + "bytes" "encoding/json" - "github.com/fsnotify/fsnotify" - "log" - "os" + "io" + "io/ioutil" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" "path/filepath" + "sync" + "os" ) -const ( - CONFIG_PATH = "data/config/" - APP_VERSION = "v0.3" -) +var configPath string = filepath.Join(GetCurrentDir(), CONFIG_PATH + "config.json") + +func init() { + c := NewConfig() + // Let's initialise all our json config stuff + // For some reasons the file will be written bottom up so we start from the end moving up to the top + + // Connections + if c.Get("connections.0.type").Interface() == nil { + c.Get("connections.-1").Set(map[string]interface{}{"type": "webdav", "label": "Webdav"}) + c.Get("connections.-1").Set(map[string]interface{}{"type": "ftp", "label": "FTP"}) + c.Get("connections.-1").Set(map[string]interface{}{"type": "sftp", "label": "SFTP"}) + c.Get("connections.-1").Set(map[string]interface{}{"type": "git", "label": "GIT"}) + c.Get("connections.-1").Set(map[string]interface{}{"type": "s3", "label": "S3"}) + c.Get("connections.-1").Set(map[string]interface{}{"type": "dropbox", "label": "Dropbox"}) + c.Get("connections.-1").Set(map[string]interface{}{"type": "gdrive", "label": "Drive"}) + } + + // OAuth credentials + c.Get("oauth").Default("") + + // Log + c.Get("log.telemetry").Default(true) + c.Get("log.level").Default("INFO") + c.Get("log.enable").Default(true) + + // Email + c.Get("email.from").Default("username@gmail.com") + c.Get("email.password").Default("password") + c.Get("email.username").Default("username@gmail.com") + c.Get("email.port").Default(587) + c.Get("email.server").Default("smtp.google.com") + + // General + c.Get("general.remember_me").Default(true) + c.Get("general.auto_connect").Default(false) + c.Get("general.display_hidden").Default(true) + c.Get("general.fork_button").Default(true) + c.Get("general.editor").Default("emacs") + if c.Get("general.secret_key").String() == "" { + c.Get("general.secret_key").Default(RandomString(16)) + } + if env := os.Getenv("APPLICATION_URL"); env != "" { + c.Get("general.host").Set(env) + } else { + c.Get("general.host").Default("http://127.0.0.1:8334") + } + c.Get("general.port").Default(8334) + c.Get("general.name").Default("Nuage") +} func NewConfig() *Config { - c := Config{} - c.Initialise() - return &c + a := Config{} + return a.load() } - type Config struct { - General struct { - Name string `json:"name"` - Port int `json:"port"` - Host string `json:"host"` - SecretKey string `json:"secret_key"` - Editor string `json:"editor"` - ForkButton bool `json:"fork_button"` - DisplayHidden bool `json:"display_hidden"` - AutoConnect bool `json:"auto_connect"` - RememberMe *bool `json:"remember_me"` - } `json:"general"` - Log struct { - Enable bool `json:"enable"` - Level string `json:"level"` - Telemetry bool `json:"telemetry"` - } `json:"log"` - Email struct { - Server string `json:"server"` - Port int `json:"port"` - Username string `json:"username"` - Password string `json:"password"` - From string `json:"from"` - } `json:"email"` - OAuthProvider struct { - Dropbox struct { - ClientID string `json:"client_id"` - } `json:"dropbox"` - GoogleDrive struct { - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - } `json:"gdrive"` - Custom struct { - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - } `json:"custom"` - } `json:"oauth"` - Connections []struct { - Type string `json:"type"` - Label string `json:"label"` - Hostname *string `json:"hostname,omitempty"` - Username *string `json:"username,omitempty"` - Password *string `json:"password,omitempty"` - Url *string `json:"url,omitempty"` - Advanced *bool `json:"advanced,omitempty"` - Port *string `json:"port,omitempty"` - Path *string `json:"path,omitempty"` - Passphrase *string `json:"passphrase,omitempty"` - Conn *string `json:"conn,omitempty"` - SecretAccessKey *string `json:"secret_access_key,omitempty"` - AccessKeyId *string `json:"access_key_id,omitempty"` - Endpoint *string `json:"endpoint,omitempty"` - Commit *string `json:"commit,omitempty"` - Branch *string `json:"branch,omitempty"` - AuthorEmail *string `json:"author_email,omitempty"` - AuthorName *string `json:"author_name,omitempty"` - CommitterEmail *string `json:"committer_email,omitempty"` - CommitterName *string `json:"committter_name,omitempty"` - } `json:"connections"` - Runtime struct { - Dirname string - ConfigPath string - FirstSetup bool - } `json:"-"` - MimeTypes map[string]string `json:"-"` + mu sync.Mutex + path *string + json string + reader gjson.Result } -func (c *Config) Initialise() { - c.Runtime.Dirname = GetCurrentDir() - c.Runtime.ConfigPath = filepath.Join(c.Runtime.Dirname, CONFIG_PATH) - os.MkdirAll(c.Runtime.ConfigPath, os.ModePerm) - if err := c.loadConfig(filepath.Join(c.Runtime.ConfigPath, "config.json")); err != nil { - log.Println("> Can't load configuration file: ", err) +func (this *Config) load() *Config { + if f, err := os.OpenFile(configPath, os.O_RDONLY, os.ModePerm); err == nil { + j, _ := ioutil.ReadAll(f) + this.json = string(j) + f.Close() + } else { + this.json = `{}` } - if err := c.loadMimeType(filepath.Join(c.Runtime.ConfigPath, "mime.json")); err != nil { - log.Println("> Can't load mimetype config") + if gjson.Valid(this.json) == true { + this.reader = gjson.Parse(this.json) } - go c.ChangeListener() + return this } -func (c *Config) loadConfig(path string) error { - file, err := os.Open(path) - defer file.Close() - if err != nil { - c = &Config{} - log.Println("can't load config file: ", err) - return err - } - decoder := json.NewDecoder(file) - err = decoder.Decode(&c) - if err != nil { - return err - } - c.populateDefault(path) - return nil +func (this *Config) Get(path string) *Config { + this.path = &path + return this } -func (c *Config) ChangeListener() { - watcher, err := fsnotify.NewWatcher() - if err != nil { - log.Fatal(err) +func (this *Config) Default(value interface{}) *Config { + if this.path == nil { + return this } - defer watcher.Close() - done := make(chan bool) - go func() { - for { - select { - case event := <-watcher.Events: - if event.Op&fsnotify.Write == fsnotify.Write { - config_path := filepath.Join(c.Runtime.ConfigPath, "config.json") - if err = c.loadConfig(config_path); err != nil { - log.Println("can't load config file") - } else { - c.populateDefault(config_path) - } - } - } - } - }() - _ = watcher.Add(c.Runtime.ConfigPath) - <-done + + if val := this.reader.Get(*this.path).Value(); val == nil { + this.mu.Lock() + this.json, _ = sjson.Set(this.json, *this.path, value) + this.reader = gjson.Parse(this.json) + this.save() + this.mu.Unlock() + } + return this } -func (c *Config) populateDefault(path string) { - if c.General.Port == 0 { - c.General.Port = 8334 +func (this *Config) Set(value interface{}) *Config { + if this.path == nil { + return this } - if c.General.Name == "" { - c.General.Name = "Nuage" + + this.mu.Lock() + this.json, _ = sjson.Set(this.json, *this.path, value) + this.reader = gjson.Parse(this.json) + this.save() + this.mu.Unlock() + return this +} + +func (this Config) String() string { + return this.reader.Get(*this.path).String() +} + +func (this Config) Int() int { + val := this.reader.Get(*this.path).Value() + switch val.(type) { + case float64: return int(val.(float64)) + case int64: return int(val.(int64)) + case int: return val.(int) } - if c.General.SecretKey == "" { - c.General.SecretKey = RandomString(16) - j, err := json.MarshalIndent(c, "", " ") - if err == nil { - f, err := os.OpenFile(path, os.O_WRONLY, os.ModePerm) - if err == nil { - f.Write(j) - f.Close() - } - } + return 0 +} + +func (this Config) Bool() bool { + val := this.reader.Get(*this.path).Value() + switch val.(type) { + case bool: return val.(bool) } - if c.OAuthProvider.Dropbox.ClientID == "" { - c.OAuthProvider.Dropbox.ClientID = os.Getenv("DROPBOX_CLIENT_ID") + return false +} + +func (this Config) Interface() interface{} { + return this.reader.Get(*this.path).Value() +} + +func (this Config) save() { + if gjson.Valid(this.json) == false { + return } - if c.OAuthProvider.GoogleDrive.ClientID == "" { - c.OAuthProvider.GoogleDrive.ClientID = os.Getenv("GDRIVE_CLIENT_ID") - } - if c.OAuthProvider.GoogleDrive.ClientSecret == "" { - c.OAuthProvider.GoogleDrive.ClientSecret = os.Getenv("GDRIVE_CLIENT_SECRET") - } - if c.General.Host == "" { - c.General.Host = os.Getenv("APPLICATION_URL") + if f, err := os.OpenFile(configPath, os.O_WRONLY|os.O_CREATE, os.ModePerm); err == nil { + buf := bytes.NewBuffer(PrettyPrint([]byte(this.json))) + io.Copy(f, buf) + f.Close() } } -func (c *Config) Export() (string, error) { +func (this Config) Scan(p interface{}) error { + content := this.reader.Get(*this.path).String() + + return json.Unmarshal([]byte(content), &p) +} + +func (this Config) Export() (string, error) { publicConf := struct { Editor string `json:"editor"` ForkButton bool `json:"fork_button"` DisplayHidden bool `json:"display_hidden"` AutoConnect bool `json:"auto_connect"` Name string `json:"name"` - RememberMe *bool `json:"remember_me"` + RememberMe bool `json:"remember_me"` Connections interface{} `json:"connections"` MimeTypes map[string]string `json:"mime"` }{ - Editor: c.General.Editor, - ForkButton: c.General.ForkButton, - DisplayHidden: c.General.DisplayHidden, - AutoConnect: c.General.AutoConnect, - Connections: c.Connections, - MimeTypes: c.MimeTypes, - Name: c.General.Name, - RememberMe: c.General.RememberMe, + Editor: this.Get("general.editor").String(), + ForkButton: this.Get("general.fork_button").Bool(), + DisplayHidden: this.Get("general.display_hidden").Bool(), + AutoConnect: this.Get("general.auto_connect").Bool(), + Name: this.Get("general.name").String(), + RememberMe: this.Get("general.remember_me").Bool(), + Connections: this.Get("connections").Interface(), + MimeTypes: AllMimeTypes(), } j, err := json.Marshal(publicConf) if err != nil { @@ -200,13 +190,3 @@ func (c *Config) Export() (string, error) { } return string(j), nil } - -func (c *Config) loadMimeType(path string) error { - file, err := os.Open(path) - defer file.Close() - if err != nil { - return err - } - decoder := json.NewDecoder(file) - return decoder.Decode(&c.MimeTypes) -} diff --git a/server/common/config_test.go b/server/common/config_test.go new file mode 100644 index 00000000..8ffe6e89 --- /dev/null +++ b/server/common/config_test.go @@ -0,0 +1,77 @@ +package common + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestConfigGet(t *testing.T) { + assert.Equal(t, nil, NewConfig().Get("foo").Interface()) + assert.Equal(t, nil, NewConfig().Get("foo.bar").Interface()) +} + +func TestConfigDefault(t *testing.T) { + c := NewConfig() + assert.Equal(t, "test", c.Get("foo.bar").Default("test").String()) + assert.Equal(t, "test", c.Get("foo.bar").String()) + assert.Equal(t, "test", c.Get("foo.bar").Default("nope").String()) + assert.Equal(t, "nope", c.Get("foo.bar.test").Default("nope").String()) +} + +func TestConfigTypeCase(t *testing.T) { + assert.Equal(t, nil, NewConfig().Get("foo.bar.nil").Default(nil).Interface()) + assert.Equal(t, true, NewConfig().Get("foo.bar").Default(true).Bool()) + assert.Equal(t, 10, NewConfig().Get("foo.bar").Default(10).Int()) + assert.Equal(t, "test", NewConfig().Get("foo.bar").Default("test").String()) +} + +func TestConfigSet(t *testing.T) { + c := NewConfig() + assert.Equal(t, "test", c.Get("foo.bar").Set("test").String()) + assert.Equal(t, "valu", c.Get("foo.bar").Set("valu").String()) + assert.Equal(t, "valu", c.Get("foo.bar.test.bar.foo").Set("valu").String()) +} + +func TestConfigScan(t *testing.T) { + c := NewConfig() + c.Get("foo.bar").Default("test") + c.Get("foo.bar2").Default(32) + c.Get("foo.bar3").Default(true) + + var data struct { + Bar string `json:"bar"` + Bar2 int `json:"bar2"` + Bar3 bool `json:"bar3"` + } + c.Get("foo").Scan(&data) + assert.Equal(t, "test", data.Bar) + assert.Equal(t, 32, data.Bar2) + assert.Equal(t, true, data.Bar3) +} + +func TestConfigSlice(t *testing.T) { + c := NewConfig() + + c.Get("connections.-1").Set(map[string]interface{}{"type": "test0", "label": "test0"}) + c.Get("connections.-1").Set(map[string]interface{}{"type": "test1", "label": "Test1"}) + + var data []struct { + Type string `json:"type"` + Label string `json:"label"` + } + c.Get("connections").Scan(&data) + assert.Equal(t, 2, len(data)) + assert.Equal(t, "test0", data[0].Type) + assert.Equal(t, "test0", data[0].Label) +} + +func BenchmarkGetConfigElement(b *testing.B) { + c := NewConfig() + c.Get("foo.bar.test.foo").Set("test") + c.Get("foo.bar.test.bar.foo").Set("valu") + + for n := 0; n < b.N; n++ { + c.Get("foo").String() + } +} + diff --git a/server/common/constants.go b/server/common/constants.go index f230f7ba..c4644fa5 100644 --- a/server/common/constants.go +++ b/server/common/constants.go @@ -1,6 +1,8 @@ package common const ( + APP_VERSION = "v0.3" + CONFIG_PATH = "data/config/" COOKIE_NAME_AUTH = "auth" COOKIE_NAME_PROOF = "proof" COOKIE_PATH = "/api/" diff --git a/server/common/crypto.go b/server/common/crypto.go index 918478d5..54dc8d42 100644 --- a/server/common/crypto.go +++ b/server/common/crypto.go @@ -138,6 +138,7 @@ func verify(something []byte) ([]byte, error) { return something, nil } +// Create a unique ID that can be use to identify different session func GenerateID(params map[string]string) string { p := "type =>" + params["type"] p += "host =>" + params["host"] diff --git a/server/common/crypto_test.go b/server/common/crypto_test.go index f3ea20f8..0b76ea5b 100644 --- a/server/common/crypto_test.go +++ b/server/common/crypto_test.go @@ -35,8 +35,9 @@ func TestIDGeneration(t *testing.T) { func TestStringGeneration(t *testing.T) { str := QuickString(10) - str1 := QuickString(10) - str2 := QuickString(10) + str1 := QuickString(16) + str2 := QuickString(24) assert.Equal(t, len(str), 10) - t.Log(str, str1, str2) + assert.Equal(t, len(str1), 16) + assert.Equal(t, len(str2), 24) } diff --git a/server/common/default.go b/server/common/default.go new file mode 100644 index 00000000..a8f4201a --- /dev/null +++ b/server/common/default.go @@ -0,0 +1,33 @@ +package common + +import ( + "net/http" + "net" + "time" +) + +var HTTPClient = http.Client{ + Timeout: 5 * time.Hour, + Transport: &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 10 * time.Second, + KeepAlive: 10 * time.Second, + }).Dial, + TLSHandshakeTimeout: 5 * time.Second, + IdleConnTimeout: 60 * time.Second, + ResponseHeaderTimeout: 60 * time.Second, + }, +} + +var HTTP = http.Client{ + Timeout: 800 * time.Millisecond, + Transport: &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 500 * time.Millisecond, + KeepAlive: 500 * time.Millisecond, + }).Dial, + TLSHandshakeTimeout: 500 * time.Millisecond, + IdleConnTimeout: 500 * time.Millisecond, + ResponseHeaderTimeout: 500 * time.Millisecond, + }, +} diff --git a/server/common/files.go b/server/common/files.go index f1674e58..b9d8ccd9 100644 --- a/server/common/files.go +++ b/server/common/files.go @@ -1,5 +1,19 @@ package common +import ( + "os" + "path/filepath" +) + +func GetCurrentDir() string { + ex, _ := os.Executable() + return filepath.Dir(ex) +} + +func GetAbsolutePath(p string) string { + return filepath.Join(GetCurrentDir(), p) +} + func IsDirectory(path string) bool { if string(path[len(path)-1]) != "/" { return false diff --git a/server/common/helpers.go b/server/common/helpers.go deleted file mode 100644 index 8c27f127..00000000 --- a/server/common/helpers.go +++ /dev/null @@ -1,39 +0,0 @@ -package common - -import ( - "path/filepath" - "strings" -) - -type Helpers struct { - AbsolutePath func(p string) string - MimeType func(p string) string -} - -func NewHelpers(config *Config) *Helpers { - return &Helpers{ - MimeType: mimeType(config), - AbsolutePath: absolutePath(config), - } -} - -func absolutePath(c *Config) func(p string) string { - return func(p string) string { - return filepath.Join(c.Runtime.Dirname, p) - } -} - -func mimeType(c *Config) func(p string) string { - return func(p string) string { - ext := filepath.Ext(p) - if ext != "" { - ext = ext[1:] - } - ext = strings.ToLower(ext) - mType := c.MimeTypes[ext] - if mType == "" { - return "application/octet-stream" - } - return mType - } -} diff --git a/server/common/log.go b/server/common/log.go index 5b2c4901..c5c95f0e 100644 --- a/server/common/log.go +++ b/server/common/log.go @@ -2,13 +2,7 @@ package common import ( "time" - "log" -) - -const ( - LOG_INFO = "INFO" - LOG_WARNING = "WARNING" - LOG_ERROR = "ERROR" + slog "log" ) type LogEntry struct { @@ -27,34 +21,76 @@ type LogEntry struct { Backend string `json:"backend"` } -func Log(ctx *App, str string, level string){ - if ctx.Config.Log.Enable == false { - return - } +type log struct{ + enable bool - shouldDisplay := func(r string, l string) bool { - levels := []string{"DEBUG", "INFO", "WARNING", "ERROR"} + debug bool + info bool + warn bool + error bool +} - configLevel := -1 - currentLevel := 0 - - for i:=0; i <= len(levels); i++ { - if levels[i] == l { - currentLevel = i - } - if levels[i] == r { - configLevel = i - break - } - } - - if currentLevel <= configLevel { - return true - } - return false - }(ctx.Config.Log.Level, level) - - if shouldDisplay { - log.Printf("%s %s\n", level, str) +func (l *log) Info(str string) { + if l.info && l.enable { + slog.Printf("INFO %s\n", str) } } + +func (l *log) Warning(str string) { + if l.warn && l.enable { + slog.Printf("WARNING %s\n", str) + } +} + +func (l *log) Error(str string) { + if l.error && l.enable { + slog.Printf("ERROR %s\n", str) + } +} + +func (l *log) Debug(str string) { + if l.debug && l.enable { + slog.Printf("DEBUG %s\n", str) + } +} + +func (l *log) SetVisibility(str string) { + switch str { + case "WARN": + l.debug = false + l.info = false + l.warn = true + l.error = true + case "ERROR": + l.debug = false + l.info = false + l.warn = false + l.error = true + case "DEBUG": + l.debug = true + l.info = true + l.warn = true + l.error = true + case "INFO": + l.debug = false + l.info = true + l.warn = true + l.error = true + default: + l.debug = false + l.info = true + l.warn = true + l.error = true + } +} + +func(l *log) Enable(val bool) { + l.enable = val +} + +var Log = func () log { + l := log{} + l.SetVisibility("DEBUG") + l.Enable(true) + return l +}() diff --git a/server/common/mime.go b/server/common/mime.go new file mode 100644 index 00000000..a17a2f1b --- /dev/null +++ b/server/common/mime.go @@ -0,0 +1,37 @@ +package common + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +var MimeTypes map[string]string + +func init(){ + path := filepath.Join(GetCurrentDir(), CONFIG_PATH + "mime.json") + if f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm); err == nil { + j, _ := ioutil.ReadAll(f) + json.Unmarshal(j, &MimeTypes) + f.Close() + } +} + +func GetMimeType(p string) string { + ext := filepath.Ext(p) + if ext != "" { + ext = ext[1:] + } + ext = strings.ToLower(ext) + mType := MimeTypes[ext] + if mType == "" { + return "application/octet-stream" + } + return mType +} + +func AllMimeTypes() map[string]string { + return MimeTypes +} diff --git a/server/common/plugin.go b/server/common/plugin.go new file mode 100644 index 00000000..9b933128 --- /dev/null +++ b/server/common/plugin.go @@ -0,0 +1,11 @@ +package common + +type Plugin struct { + Type string + Call interface{} + Priority int +} + +const ( + PROCESS_FILE_CONTENT_BEFORE_SEND = "PROCESS_FILE_CONTENT_BEFORE_SEND" +) diff --git a/server/common/types.go b/server/common/types.go index 7f6b7925..3725238a 100644 --- a/server/common/types.go +++ b/server/common/types.go @@ -7,6 +7,7 @@ import ( ) type IBackend interface { + Init(params map[string]string, app *App) (IBackend, error) Ls(path string) ([]os.FileInfo, error) Cat(path string) (io.Reader, error) Mkdir(path string) error diff --git a/server/common/utils.go b/server/common/utils.go index c024e135..1263731f 100644 --- a/server/common/utils.go +++ b/server/common/utils.go @@ -1,5 +1,10 @@ package common +import ( + "bytes" + "encoding/json" +) + func NewBool(t bool) *bool { return &t } @@ -21,6 +26,7 @@ func NewBoolFromInterface(val interface{}) bool { default: return false } } + func NewInt64pFromInterface(val interface{}) *int64 { switch val.(type) { case int64: @@ -32,6 +38,7 @@ func NewInt64pFromInterface(val interface{}) *int64 { default: return nil } } + func NewStringpFromInterface(val interface{}) *string { switch val.(type) { case string: @@ -49,3 +56,13 @@ func NewStringFromInterface(val interface{}) string { default: return "" } } + +func PrettyPrint(json_dirty []byte) []byte { + var json_pretty bytes.Buffer + error := json.Indent(&json_pretty, json_dirty, "", " ") + if error != nil { + return json_dirty + } + json_pretty.Write([]byte("\n")) + return json_pretty.Bytes() +} diff --git a/server/ctrl/files.go b/server/ctrl/files.go index 6faa4e1f..c0db91b4 100644 --- a/server/ctrl/files.go +++ b/server/ctrl/files.go @@ -2,8 +2,8 @@ package ctrl import ( . "github.com/mickael-kerjean/nuage/server/common" - "github.com/mickael-kerjean/nuage/server/services" "github.com/mickael-kerjean/nuage/server/model" + "github.com/mickael-kerjean/nuage/server/plugin" "io" "net/http" "path/filepath" @@ -110,10 +110,17 @@ func FileCat(ctx App, res http.ResponseWriter, req *http.Request) { return } - file, err = services.ProcessFileBeforeSend(file, &ctx, req, &res) - if err != nil { - SendErrorResult(res, err) - return + mType := GetMimeType(req.URL.Query().Get("path")) + res.Header().Set("Content-Type", mType) + + for _, obj := range plugin.ProcessFileContentBeforeSend() { + if obj == nil { + continue + } + if file, err = obj(file, &ctx, &res, req); err != nil { + SendErrorResult(res, err) + return + } } io.Copy(res, file) } diff --git a/server/ctrl/session.go b/server/ctrl/session.go index 20631b81..916bd745 100644 --- a/server/ctrl/session.go +++ b/server/ctrl/session.go @@ -69,8 +69,7 @@ func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) { SendErrorResult(res, NewError(err.Error(), 500)) return } - obfuscate, err := EncryptString(ctx.Config.General.SecretKey, string(s)) - + obfuscate, err := EncryptString(ctx.Config.Get("general.secret_key").String(), string(s)) if err != nil { SendErrorResult(res, NewError(err.Error(), 500)) return diff --git a/server/ctrl/share.go b/server/ctrl/share.go index c4ede5e6..99783cc0 100644 --- a/server/ctrl/share.go +++ b/server/ctrl/share.go @@ -49,7 +49,7 @@ func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) { return "" } var data map[string]string - str, err := DecryptString(ctx.Config.General.SecretKey, c.Value) + str, err := DecryptString(ctx.Config.Get("general.secret_key").String(), c.Value) if err != nil { return "" } @@ -78,7 +78,7 @@ func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) { if err != nil { return "" } - obfuscate, err := EncryptString(ctx.Config.General.SecretKey, string(s)) + obfuscate, err := EncryptString(ctx.Config.Get("general.secret_key").String(), string(s)) if err != nil { return "" } @@ -156,7 +156,7 @@ func ShareVerifyProof(ctx App, res http.ResponseWriter, req *http.Request) { Name: COOKIE_NAME_PROOF, Value: func(p []model.Proof) string { j, _ := json.Marshal(p) - str, _ := EncryptString(ctx.Config.General.SecretKey, string(j)) + str, _ := EncryptString(ctx.Config.Get("general.secret_key").String(), string(j)) return str }(verifiedProof), Path: COOKIE_PATH, diff --git a/server/ctrl/static.go b/server/ctrl/static.go index c1574cae..39901e0e 100644 --- a/server/ctrl/static.go +++ b/server/ctrl/static.go @@ -22,7 +22,7 @@ func StaticHandler(_path string, ctx App) http.Handler { return } - absPath := ctx.Helpers.AbsolutePath(_path) + absPath := GetAbsolutePath(_path) fsrv := http.FileServer(http.Dir(absPath)) _, err := os.Open(path.Join(absPath, req.URL.Path+".gz")) if err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") { @@ -45,11 +45,11 @@ func DefaultHandler(_path string, ctx App) http.Handler { SecureHeader(&header) p := _path - if _, err := os.Open(path.Join(ctx.Config.Runtime.Dirname, p+".gz")); err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") { + if _, err := os.Open(path.Join(GetCurrentDir(), p+".gz")); err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") { res.Header().Set("Content-Encoding", "gzip") p += ".gz" } - http.ServeFile(res, req, ctx.Helpers.AbsolutePath(p)) + http.ServeFile(res, req, GetAbsolutePath(p)) }) } diff --git a/server/ctrl/webdav.go b/server/ctrl/webdav.go index d4396d28..0804e4f6 100644 --- a/server/ctrl/webdav.go +++ b/server/ctrl/webdav.go @@ -2,13 +2,13 @@ package ctrl import ( "net/http" - "log" . "github.com/mickael-kerjean/nuage/server/common" . "github.com/mickael-kerjean/nuage/server/middleware" "github.com/mickael-kerjean/nuage/server/model" "github.com/mickael-kerjean/net/webdav" "github.com/mickael-kerjean/mux" "time" + "fmt" ) var start time.Time = time.Now() @@ -40,7 +40,8 @@ func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) { } // webdav is WIP - return http.NotFound(res, req) + http.NotFound(res, req) + return h := &webdav.Handler{ Prefix: "/s/" + share_id, @@ -53,12 +54,7 @@ func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) { } return "OK" }(err) - log.Printf("INFO %s WEBDAV %s %s %s", share_id, req.Method, req.URL.Path, e) - elapsed := time.Now().Sub(start) - if elapsed > 1000*time.Millisecond { - log.Println("\n\n\n") - } - start = time.Now() + Log.Info(fmt.Sprintf("INFO %s WEBDAV %s %s %s", share_id, req.Method, req.URL.Path, e)) }, } h.ServeHTTP(res, req) diff --git a/server/main.go b/server/main.go index fadf3246..adcd5e50 100644 --- a/server/main.go +++ b/server/main.go @@ -1,11 +1,12 @@ package main import ( + "fmt" "github.com/mickael-kerjean/mux" . "github.com/mickael-kerjean/nuage/server/common" . "github.com/mickael-kerjean/nuage/server/ctrl" . "github.com/mickael-kerjean/nuage/server/middleware" - "log" + _ "github.com/mickael-kerjean/nuage/server/plugin" "net/http" "strconv" ) @@ -13,7 +14,6 @@ import ( func main() { app := App{} app.Config = NewConfig() - app.Helpers = NewHelpers(app.Config) Init(&app) select {} } @@ -53,15 +53,15 @@ func Init(a *App) *http.Server { r.PathPrefix("/").Handler(DefaultHandler(FILE_INDEX, *a)).Methods("GET") srv := &http.Server{ - Addr: ":" + strconv.Itoa(a.Config.General.Port), + Addr: ":" + strconv.Itoa(a.Config.Get("general.port").Int()), Handler: r, } go func() { if err := srv.ListenAndServe(); err != nil { - log.Fatal("SERVER START ERROR ", err) + Log.Error(fmt.Sprintf("server start: %v", err)) return } - log.Println("SERVER START OK") + Log.Info("Server start") }() return srv } diff --git a/server/middleware/http.go b/server/middleware/http.go index 04f348f5..1ab10202 100644 --- a/server/middleware/http.go +++ b/server/middleware/http.go @@ -26,12 +26,14 @@ func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http. fn(ctx, &resw, req) req.Body.Close() - if ctx.Config.Log.Telemetry { - go telemetry(req, &resw, start, ctx.Backend.Info()) - } - if ctx.Config.Log.Enable { - go logger(req, &resw, start) - } + go func() { + if ctx.Config.Get("log.telemetry").Bool() { + go telemetry(req, &resw, start, ctx.Backend.Info()) + } + if ctx.Config.Get("log.enable").Bool() { + go logger(req, &resw, start) + } + }() } } @@ -99,7 +101,7 @@ func ExtractSession(req *http.Request, ctx *App) (map[string]string, error) { str = cookie.Value } - str, _ = DecryptString(ctx.Config.General.SecretKey, str) + str, _ = DecryptString(ctx.Config.Get("general.secret_key").String(), str) err := json.Unmarshal([]byte(str), &res) return res, err } diff --git a/server/model/backend/custombackend.go b/server/model/backend/custombackend.go deleted file mode 100644 index 965f1878..00000000 --- a/server/model/backend/custombackend.go +++ /dev/null @@ -1,42 +0,0 @@ -package backend - -import ( - . "github.com/mickael-kerjean/nuage/server/common" - "io" - "os" - "strings" -) - -type CustomBackend struct { -} - -func NewCustomBackend(params map[string]string, app *App) (*CustomBackend, error) { - return &CustomBackend{}, nil -} - -func (b CustomBackend) Info() string { - return "N/A" -} - -func (b CustomBackend) Ls(path string) ([]os.FileInfo, error) { - return nil, NewError("", 401) -} - -func (b CustomBackend) Cat(path string) (io.Reader, error) { - return strings.NewReader(""), NewError("", 401) -} -func (b CustomBackend) Mkdir(path string) error { - return NewError("", 401) -} -func (b CustomBackend) Rm(path string) error { - return NewError("", 401) -} -func (b CustomBackend) Mv(from string, to string) error { - return NewError("", 401) -} -func (b CustomBackend) Touch(path string) error { - return NewError("", 401) -} -func (b CustomBackend) Save(path string, file io.Reader) error { - return NewError("", 401) -} diff --git a/server/model/backend/dropbox.go b/server/model/backend/dropbox.go index 4b7aeced..4c25cd7a 100644 --- a/server/model/backend/dropbox.go +++ b/server/model/backend/dropbox.go @@ -19,10 +19,18 @@ type Dropbox struct { Bearer string } -func NewDropbox(params map[string]string, app *App) (IBackend, error) { - backend := Dropbox{} - backend.ClientId = app.Config.OAuthProvider.Dropbox.ClientID - backend.Hostname = app.Config.General.Host +func init() { + Backend.Register("dropbox", Dropbox{}) +} + +func (d Dropbox) Init(params map[string]string, app *App) (IBackend, error) { + backend := &Dropbox{} + if env := os.Getenv("DROPBOX_CLIENT_ID"); env != "" { + backend.ClientId = env + } else { + backend.ClientId = app.Config.Get("oauth.dropbox.client_id").Default("").String() + } + backend.Hostname = app.Config.Get("general.host").String() backend.Bearer = params["bearer"] if backend.ClientId == "" { diff --git a/server/model/backend/ftp.go b/server/model/backend/ftp.go index 55659de9..91c785fe 100644 --- a/server/model/backend/ftp.go +++ b/server/model/backend/ftp.go @@ -14,6 +14,8 @@ import ( var FtpCache AppCache func init() { + Backend.Register("ftp", Ftp{}) + FtpCache = NewAppCache(2, 1) FtpCache.OnEvict(func(key string, value interface{}) { c := value.(*Ftp) @@ -25,7 +27,7 @@ type Ftp struct { client *goftp.Client } -func NewFtp(params map[string]string, app *App) (IBackend, error) { +func (f Ftp) Init(params map[string]string, app *App) (IBackend, error) { c := FtpCache.Get(params) if c != nil { d := c.(*Ftp) diff --git a/server/model/backend/gdrive.go b/server/model/backend/gdrive.go index 0be0f826..e41ee7c8 100644 --- a/server/model/backend/gdrive.go +++ b/server/model/backend/gdrive.go @@ -22,22 +22,29 @@ type GDrive struct { Config *oauth2.Config } -func NewGDrive(params map[string]string, app *App) (IBackend, error) { +func init() { + Backend.Register("gdrive", GDrive{}) +} + + +func (g GDrive) Init(params map[string]string, app *App) (IBackend, error) { backend := GDrive{} - if app.Config.OAuthProvider.GoogleDrive.ClientID == "" { - return backend, NewError("Missing Client ID: Contact your admin", 502) - } else if app.Config.OAuthProvider.GoogleDrive.ClientSecret == "" { - return backend, NewError("Missing Client Secret: Contact your admin", 502) - } else if app.Config.General.Host == "" { - return backend, NewError("Missing Hostname: Contact your admin", 502) - } + config := &oauth2.Config{ Endpoint: google.Endpoint, - ClientID: app.Config.OAuthProvider.GoogleDrive.ClientID, - ClientSecret: app.Config.OAuthProvider.GoogleDrive.ClientSecret, - RedirectURL: app.Config.General.Host + "/login", + ClientID: app.Config.Get("oauth.gdrive.client_id").Default(os.Getenv("GDRIVE_CLIENT_ID")).String(), + ClientSecret: app.Config.Get("oauth.gdrive.client_secret").Default(os.Getenv("GDRIVE_CLIENT_SECRET")).String(), + RedirectURL: app.Config.Get("general.host").String() + "/login", Scopes: []string{"https://www.googleapis.com/auth/drive"}, } + if config.ClientID == "" { + return backend, NewError("Missing Client ID: Contact your admin", 502) + } else if config.ClientSecret == "" { + return backend, NewError("Missing Client Secret: Contact your admin", 502) + } else if config.RedirectURL == "/login" { + return backend, NewError("Missing Hostname: Contact your admin", 502) + } + token := &oauth2.Token{ AccessToken: params["token"], RefreshToken: params["refresh"], diff --git a/server/model/backend/git.go b/server/model/backend/git.go index 2b3061a5..2bf1ef92 100644 --- a/server/model/backend/git.go +++ b/server/model/backend/git.go @@ -26,6 +26,8 @@ type Git struct { } func init() { + Backend.Register("git", Git{}) + GitCache = NewAppCache() cachePath := filepath.Join(GetCurrentDir(), GitCachePath) os.RemoveAll(cachePath) @@ -50,7 +52,7 @@ type GitParams struct { basePath string } -func NewGit(params map[string]string, app *App) (*Git, error) { +func (git Git) Init(params map[string]string, app *App) (IBackend, error) { if obj := GitCache.Get(params); obj != nil { return obj.(*Git), nil } @@ -95,7 +97,7 @@ func NewGit(params map[string]string, app *App) (*Git, error) { } hash := GenerateID(params) - p.basePath = app.Helpers.AbsolutePath(GitCachePath + "repo_" + fmt.Sprint(hash) + "/") + p.basePath = GetAbsolutePath(GitCachePath + "repo_" + fmt.Sprint(hash) + "/") repo, err := g.git.open(p, p.basePath) g.git.repo = repo diff --git a/server/model/backend/s3.go b/server/model/backend/s3.go index a1578490..55ef856d 100644 --- a/server/model/backend/s3.go +++ b/server/model/backend/s3.go @@ -22,10 +22,12 @@ type S3Backend struct { } func init() { + Backend.Register("s3", S3Backend{}) S3Cache = NewAppCache(2, 1) } -func NewS3(params map[string]string, app *App) (IBackend, error) { + +func (s S3Backend) Init(params map[string]string, app *App) (IBackend, error) { if params["region"] == "" { params["region"] = "us-east-2" } diff --git a/server/model/backend/sftp.go b/server/model/backend/sftp.go index 8e76b546..0416d4e1 100644 --- a/server/model/backend/sftp.go +++ b/server/model/backend/sftp.go @@ -18,16 +18,16 @@ type Sftp struct { } func init() { - SftpCache = NewAppCache() + Backend.Register("sftp", Sftp{}) + SftpCache = NewAppCache() SftpCache.OnEvict(func(key string, value interface{}) { c := value.(*Sftp) c.Close() }) } -func NewSftp(params map[string]string, app *App) (*Sftp, error) { - var s Sftp = Sftp{} +func (s Sftp) Init(params map[string]string, app *App) (IBackend, error) { p := struct { hostname string port string diff --git a/server/model/backend/webdav.go b/server/model/backend/webdav.go index 287794df..e19654f3 100644 --- a/server/model/backend/webdav.go +++ b/server/model/backend/webdav.go @@ -24,7 +24,11 @@ type WebDavParams struct { path string } -func NewWebDav(params map[string]string, app *App) (IBackend, error) { +func init() { + Backend.Register("webdav", WebDav{}) +} + +func (w WebDav) Init(params map[string]string, app *App) (IBackend, error) { params["url"] = regexp.MustCompile(`\/$`).ReplaceAllString(params["url"], "") backend := WebDav{ params: &WebDavParams{ diff --git a/server/model/files.go b/server/model/files.go index 884ae788..29bebc71 100644 --- a/server/model/files.go +++ b/server/model/files.go @@ -3,48 +3,37 @@ package model import ( "fmt" . "github.com/mickael-kerjean/nuage/server/common" - "github.com/mickael-kerjean/nuage/server/model/backend" + _ "github.com/mickael-kerjean/nuage/server/model/backend" ) func NewBackend(ctx *App, conn map[string]string) (IBackend, error) { - isAllowed := false - for i := range ctx.Config.Connections { - if ctx.Config.Connections[i].Type == conn["type"] { - if ctx.Config.Connections[i].Hostname == nil { - isAllowed = true - break; - }else if *ctx.Config.Connections[i].Hostname == conn["hostname"] { - isAllowed = true - break; + isAllowed := func() bool { + ret := false + var conns [] struct { + Type string `json:"type"` + Hostname string `json:"hostname"` + Path string `json:"path"` + } + ctx.Config.Get("connections").Scan(&conns) + for i := range conns { + if conns[i].Type == conn["type"] { + if conns[i].Hostname != "" && conns[i].Hostname != conn["hostname"] { + continue + } else if conns[i].Path != "" && conns[i].Path != conn["path"] { + continue + } else { + ret = true + break + } } } - } + return ret + }() if isAllowed == false { - return backend.NewNothing(conn, ctx) + return Backend.Get(BACKEND_NIL).Init(conn, ctx) } - - switch conn["type"] { - case "webdav": - return backend.NewWebDav(conn, ctx) - case "ftp": - return backend.NewFtp(conn, ctx) - case "sftp": - return backend.NewSftp(conn, ctx) - case "git": - return backend.NewGit(conn, ctx) - case "s3": - return backend.NewS3(conn, ctx) - case "dropbox": - return backend.NewDropbox(conn, ctx) - case "gdrive": - return backend.NewGDrive(conn, ctx) - case "custombackend": - return backend.NewCustomBackend(conn, ctx) - default: - return backend.NewNothing(conn, ctx) - } - return nil, NewError("Invalid backend type", 501) + return Backend.Get(conn["type"]).Init(conn, ctx) } func GetHome(b IBackend) (string, error) { @@ -56,6 +45,7 @@ func GetHome(b IBackend) (string, error) { return "", err } + func MapStringInterfaceToMapStringString(m map[string]interface{}) map[string]string { res := make(map[string]string) for key, value := range m { diff --git a/server/model/share.go b/server/model/share.go index 1f04083c..912b7683 100644 --- a/server/model/share.go +++ b/server/model/share.go @@ -261,12 +261,22 @@ func ShareProofVerifier(ctx *App, s Share, proof Proof) (Proof, error) { p.Message = NewString("We've sent you a message with a verification code") // Send email - addr := fmt.Sprintf("%s:%d", ctx.Config.Email.Server, ctx.Config.Email.Port) + addr := fmt.Sprintf( + "%s:%d", + ctx.Config.Get("email.server").String(), + ctx.Config.Get("email.port").Int(), + ) mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" subject := "Subject: Your verification code\n" msg := []byte(subject + mime + "\n" + b.String()) - auth := smtp.PlainAuth("", ctx.Config.Email.Username, ctx.Config.Email.Password, ctx.Config.Email.Server) - if err := smtp.SendMail(addr, auth, ctx.Config.Email.From, []string{"mickael@kerjean.me"}, msg); err != nil { + auth := smtp.PlainAuth( + "", + ctx.Config.Get("email.username").String(), + ctx.Config.Get("email.password").String(), + ctx.Config.Get("email.server").String(), + ) + + if err := smtp.SendMail(addr, auth, ctx.Config.Get("email.from").String(), []string{proof.Value}, msg); err != nil { log.Println("ERROR: ", err) log.Println("Verification code: " + code) return p, NewError("Couldn't send email", 500) @@ -317,7 +327,7 @@ func ShareProofGetAlreadyVerified(req *http.Request, ctx *App) []Proof { if len(cookieValue) > 500 { return p } - j, err := DecryptString(ctx.Config.General.SecretKey, cookieValue) + j, err := DecryptString(ctx.Config.Get("general.secret_key").String(), cookieValue) if err != nil { return p } diff --git a/server/plugin/example/build.sh b/server/plugin/example/build.sh new file mode 100755 index 00000000..4b75ceb5 --- /dev/null +++ b/server/plugin/example/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +go build -buildmode=plugin -o ../../../dist/data/plugin/example.so index.go diff --git a/server/plugin/example/index.go b/server/plugin/example/index.go new file mode 100644 index 00000000..45ab95dd --- /dev/null +++ b/server/plugin/example/index.go @@ -0,0 +1,24 @@ +package main + +import ( + . "github.com/mickael-kerjean/nuage/server/common" + "io" + "net/http" +) + +func Register(config *Config) []Plugin { + config.Get("plugins.example.foo").Default("bar") + + return []Plugin{ + { + Type: PROCESS_FILE_CONTENT_BEFORE_SEND, // where to hook our plugin in the request lifecycle + Call: hook, // actual function we trigger + Priority: 5, // determine execution order whilst multiple plugin type + }, + } +} + +func hook(file io.Reader, ctx *App, res *http.ResponseWriter, req *http.Request) (io.Reader, error){ + Log.Info("example plugin with config: '" + ctx.Config.Get("plugins.example.foo").String() + "'") + return file, nil +} diff --git a/server/plugin/image/build.sh b/server/plugin/image/build.sh new file mode 100755 index 00000000..0fe78304 --- /dev/null +++ b/server/plugin/image/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +go build -buildmode=plugin -o ../../../dist/data/plugin/image.so index.go diff --git a/server/plugin/image/index.go b/server/plugin/image/index.go new file mode 100644 index 00000000..b7676156 --- /dev/null +++ b/server/plugin/image/index.go @@ -0,0 +1,109 @@ +package main + +import ( + . "github.com/mickael-kerjean/nuage/server/common" + "github.com/mickael-kerjean/nuage/server/plugin/image/lib" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "strconv" +) + +const ( + ImageCachePath = "data/cache/image/" +) + +func Register(conf *Config) []Plugin { + conf.Get("plugins.transcoder.image.enable").Default(true) + + cachePath := filepath.Join(GetCurrentDir(), ImageCachePath) + os.RemoveAll(cachePath) + os.MkdirAll(cachePath, os.ModePerm) + + return []Plugin{ + { + Type: PROCESS_FILE_CONTENT_BEFORE_SEND, // where to hook our plugin in the request lifecycle + Call: hook, // actual function we trigger + Priority: -1, // last plugin to execute + }, + } +} + +func hook(reader io.Reader, ctx *App, res *http.ResponseWriter, req *http.Request) (io.Reader, error){ + if ctx.Config.Get("plugins.transcoder.image.enable").Bool() == false { + return reader, nil + } + Log.Debug("Image plugin") + + query := req.URL.Query() + mType := GetMimeType(query.Get("path")) + + if strings.HasPrefix(mType, "image/") == false { + return reader, nil + } else if mType == "image/svg" { + return reader, nil + } else if mType == "image/x-icon" { + return reader, nil + } else if query.Get("thumbnail") != "true" && query.Get("size") == "" { + return reader, nil + } + + ///////////////////////// + // Specify transformation + transform := &lib.Transform{ + Temporary: GetAbsolutePath(ImageCachePath + "image_" + QuickString(10)), + Size: 300, + Crop: true, + Quality: 50, + Exif: false, + } + if query.Get("thumbnail") == "true" { + (*res).Header().Set("Cache-Control", "max-age=259200") + } else if query.Get("size") != "" { + (*res).Header().Set("Cache-Control", "max-age=600") + size, err := strconv.ParseInt(query.Get("size"), 10, 64) + if err != nil { + return reader, nil + } + transform.Size = int(size) + transform.Crop = false + transform.Quality = 90 + transform.Exif = true + } + + ///////////////////////////// + // Insert file in the fs + // => lower RAM usage while processing + file, err := os.OpenFile(transform.Temporary, os.O_WRONLY|os.O_CREATE, os.ModePerm) + if err != nil { + return reader, NewError("Can't use filesystem", 500) + } + io.Copy(file, reader) + file.Close() + if obj, ok := reader.(interface{ Close() error }); ok { + obj.Close() + } + defer func() { + os.Remove(transform.Temporary) + }() + + ///////////////////////// + // Transcode RAW image + if lib.IsRaw(mType) { + if lib.ExtractPreview(transform) == nil { + mType = "image/jpeg" + (*res).Header().Set("Content-Type", mType) + } else { + return reader, nil + } + } + + ///////////////////////// + // Final stage: resizing + if mType != "image/jpeg" && mType != "image/png" && mType != "image/gif" && mType != "image/tiff" { + return reader, nil + } + return lib.CreateThumbnail(transform) +} diff --git a/server/services/images/raw.c b/server/plugin/image/lib/raw.c similarity index 100% rename from server/services/images/raw.c rename to server/plugin/image/lib/raw.c diff --git a/server/services/images/raw.go b/server/plugin/image/lib/raw.go similarity index 98% rename from server/services/images/raw.go rename to server/plugin/image/lib/raw.go index 54c42879..e485dd80 100644 --- a/server/services/images/raw.go +++ b/server/plugin/image/lib/raw.go @@ -1,4 +1,4 @@ -package images +package lib // #cgo pkg-config: libraw // #include diff --git a/server/services/images/raw.h b/server/plugin/image/lib/raw.h similarity index 100% rename from server/services/images/raw.h rename to server/plugin/image/lib/raw.h diff --git a/server/services/images/resizer.c b/server/plugin/image/lib/resizer.c similarity index 100% rename from server/services/images/resizer.c rename to server/plugin/image/lib/resizer.c diff --git a/server/services/images/resizer.go b/server/plugin/image/lib/resizer.go similarity index 98% rename from server/services/images/resizer.go rename to server/plugin/image/lib/resizer.go index 1885b9db..c8229d49 100644 --- a/server/services/images/resizer.go +++ b/server/plugin/image/lib/resizer.go @@ -1,4 +1,4 @@ -package images +package lib // #cgo pkg-config: vips // #include diff --git a/server/services/images/resizer.h b/server/plugin/image/lib/resizer.h similarity index 100% rename from server/services/images/resizer.h rename to server/plugin/image/lib/resizer.h diff --git a/server/plugin/index.go b/server/plugin/index.go new file mode 100644 index 00000000..fa54eb45 --- /dev/null +++ b/server/plugin/index.go @@ -0,0 +1,68 @@ +package plugin + +import ( + "os" + "path/filepath" + plg "plugin" + . "github.com/mickael-kerjean/nuage/server/common" + "sort" + "strings" + "fmt" + "net/http" + "io" +) + +const PluginPath = "data/plugin/" + +var plugins = make(map[string][]Plugin) + +func init() { + ex, _ := os.Executable() + pPath := filepath.Join(filepath.Dir(ex), PluginPath) + + file, err := os.Open(pPath) + if err != nil { + return + } + files, err := file.Readdir(0) + + c := NewConfig() + for i:=0; i < len(files); i++ { + name := files[i].Name() + if strings.HasPrefix(name, ".") == true { + continue + } + p, err := plg.Open(pPath + "/" + name) + if err != nil { + Log.Warning(fmt.Sprintf("Can't load plugin: %s => %v", name, err)) + continue + } + + f, err := p.Lookup("Register") + if err != nil { + Log.Warning(fmt.Sprintf("Can't register plugin: %s => %v", name, err)) + continue + } + if obj, ok := f.(func(config *Config) []Plugin); ok { + for _, plg := range obj(c) { + plugins[plg.Type] = append(plugins[plg.Type], plg) + sort.SliceStable(plugins[plg.Type], func(i, j int) bool { + return plugins[plg.Type][i].Priority > plugins[plg.Type][j].Priority + }) + } + } + } +} + + +func ProcessFileContentBeforeSend() []func(io.Reader, *App, *http.ResponseWriter, *http.Request) (io.Reader, error) { + fs := plugins[PROCESS_FILE_CONTENT_BEFORE_SEND] + ret := make([]func(io.Reader, *App, *http.ResponseWriter, *http.Request) (io.Reader, error), len(fs)) + for _, p := range fs { + if f, ok := p.Call.(func(io.Reader, *App, *http.ResponseWriter, *http.Request) (io.Reader, error)); ok { + ret = append(ret, f) + } + } + return ret +} + diff --git a/server/services/pipeline.go b/server/services/pipeline.go deleted file mode 100644 index 3c8eeb1e..00000000 --- a/server/services/pipeline.go +++ /dev/null @@ -1,99 +0,0 @@ -package services - -import ( - . "github.com/mickael-kerjean/nuage/server/common" - "github.com/mickael-kerjean/nuage/server/services/images" - "io" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" -) - -const ( - ImageCachePath = "data/cache/image/" -) - -func init() { - cachePath := filepath.Join(GetCurrentDir(), ImageCachePath) - os.RemoveAll(cachePath) - os.MkdirAll(cachePath, os.ModePerm) -} - -func ProcessFileBeforeSend(reader io.Reader, ctx *App, req *http.Request, res *http.ResponseWriter) (io.Reader, error) { - query := req.URL.Query() - mType := ctx.Helpers.MimeType(query.Get("path")) - (*res).Header().Set("Content-Type", mType) - - if strings.HasPrefix(mType, "image/") { - if mType == "image/svg" { - (*res).Header().Set("Content-Type", "image/svg+xml") - return reader, nil - } else if mType == "image/x-icon" { - return reader, nil - } - - if query.Get("thumbnail") != "true" && query.Get("size") == "" { - return reader, nil - } - - ///////////////////////// - // Specify transformation - transform := &images.Transform{ - Temporary: ctx.Helpers.AbsolutePath(ImageCachePath + "image_" + QuickString(10)), - Size: 300, - Crop: true, - Quality: 50, - Exif: false, - } - if query.Get("thumbnail") == "true" { - (*res).Header().Set("Cache-Control", "max-age=259200") - } else if query.Get("size") != "" { - (*res).Header().Set("Cache-Control", "max-age=600") - size, err := strconv.ParseInt(query.Get("size"), 10, 64) - if err != nil { - return reader, nil - } - transform.Size = int(size) - transform.Crop = false - transform.Quality = 90 - transform.Exif = true - } - - ///////////////////////////// - // Insert file in the fs - // => lower RAM usage while processing - file, err := os.OpenFile(transform.Temporary, os.O_WRONLY|os.O_CREATE, os.ModePerm) - if err != nil { - return reader, NewError("Can't use filesystem", 500) - } - io.Copy(file, reader) - file.Close() - if obj, ok := reader.(interface{ Close() error }); ok { - obj.Close() - } - defer func() { - os.Remove(transform.Temporary) - }() - - ///////////////////////// - // Transcode RAW image - if images.IsRaw(mType) { - if images.ExtractPreview(transform) == nil { - mType = "image/jpeg" - (*res).Header().Set("Content-Type", mType) - } else { - return reader, nil - } - } - - ///////////////////////// - // Final stage: resizing - if mType != "image/jpeg" && mType != "image/png" && mType != "image/gif" && mType != "image/tiff" { - return reader, nil - } - return images.CreateThumbnail(transform) - } - return reader, nil -} diff --git a/server/services/webdav.go b/server/services/webdav.go deleted file mode 100644 index 176752e6..00000000 --- a/server/services/webdav.go +++ /dev/null @@ -1,28 +0,0 @@ -package services - -// type Webdav struct { -// } - -// func NewWebdav() Webdav { -// return Webdav{} -// } - -// func (w Webdav) Mkdir(ctx context.Context, name string, perm os.FileMode) error { -// return nil -// } - -// func (w Webdav) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) { -// return File{}, nil -// } - -// func (w Webdav) RemoveAll(ctx context.Context, name string) error { -// return nil -// } - -// func (w Webdav) Rename(ctx context.Context, oldName, newName string) error { -// return nil -// } - -// func (w Webdav) Stat(ctx context.Context, name string) (os.FileInfo, error) { -// return nil, nil -// }