feature (plugin): API to develop plugin

This commit is contained in:
Mickael KERJEAN
2018-10-24 11:52:20 +11:00
parent ff319c2fd7
commit ddd1b83b27
46 changed files with 782 additions and 555 deletions

View File

@ -42,6 +42,10 @@ RUN mkdir -p /tmp/go/src/github.com/mickael-kerjean/ && \
cd ../ && \ cd ../ && \
GOPATH=/tmp/go go build -o ./dist/nuage ./server/main.go && \ 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 # Finalise the build
apk --no-cache add ca-certificates && \ apk --no-cache add ca-certificates && \
mv dist /app && \ mv dist /app && \

View File

@ -1,48 +1,8 @@
package common package common
import (
"net"
"net/http"
"os"
"path/filepath"
"time"
)
type App struct { type App struct {
Config *Config Config *Config
Helpers *Helpers
Backend IBackend Backend IBackend
Body map[string]interface{} Body map[string]interface{}
Session map[string]string 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,
},
}

View File

@ -1,27 +1,52 @@
package backend package common
import ( import (
. "github.com/mickael-kerjean/nuage/server/common"
"io" "io"
"os" "os"
"strings" "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 return &Nothing{}, nil
} }
func (b Nothing) Info() string { func (b Nothing) Info() string {
return "N/A" return "N/A"
} }
func (b Nothing) Ls(path string) ([]os.FileInfo, error) { func (b Nothing) Ls(path string) ([]os.FileInfo, error) {
return nil, NewError("", 401) return nil, NewError("", 401)
} }
func (b Nothing) Cat(path string) (io.Reader, error) { func (b Nothing) Cat(path string) (io.Reader, error) {
return strings.NewReader(""), NewError("", 401) return strings.NewReader(""), NewError("", 401)
} }

View File

@ -1,198 +1,188 @@
package common package common
import ( import (
"bytes"
"encoding/json" "encoding/json"
"github.com/fsnotify/fsnotify" "io"
"log" "io/ioutil"
"os" "github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"path/filepath" "path/filepath"
"sync"
"os"
) )
const ( var configPath string = filepath.Join(GetCurrentDir(), CONFIG_PATH + "config.json")
CONFIG_PATH = "data/config/"
APP_VERSION = "v0.3" 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 { func NewConfig() *Config {
c := Config{} a := Config{}
c.Initialise() return a.load()
return &c
} }
type Config struct { type Config struct {
General struct { mu sync.Mutex
Name string `json:"name"` path *string
Port int `json:"port"` json string
Host string `json:"host"` reader gjson.Result
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:"-"`
} }
func (c *Config) Initialise() { func (this *Config) load() *Config {
c.Runtime.Dirname = GetCurrentDir() if f, err := os.OpenFile(configPath, os.O_RDONLY, os.ModePerm); err == nil {
c.Runtime.ConfigPath = filepath.Join(c.Runtime.Dirname, CONFIG_PATH) j, _ := ioutil.ReadAll(f)
os.MkdirAll(c.Runtime.ConfigPath, os.ModePerm) this.json = string(j)
if err := c.loadConfig(filepath.Join(c.Runtime.ConfigPath, "config.json")); err != nil { f.Close()
log.Println("> Can't load configuration file: ", err) } else {
this.json = `{}`
} }
if err := c.loadMimeType(filepath.Join(c.Runtime.ConfigPath, "mime.json")); err != nil { if gjson.Valid(this.json) == true {
log.Println("> Can't load mimetype config") this.reader = gjson.Parse(this.json)
} }
go c.ChangeListener() return this
} }
func (c *Config) loadConfig(path string) error { func (this *Config) Get(path string) *Config {
file, err := os.Open(path) this.path = &path
defer file.Close() return this
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 (c *Config) ChangeListener() { func (this *Config) Default(value interface{}) *Config {
watcher, err := fsnotify.NewWatcher() if this.path == nil {
if err != nil { return this
log.Fatal(err)
} }
defer watcher.Close()
done := make(chan bool) if val := this.reader.Get(*this.path).Value(); val == nil {
go func() { this.mu.Lock()
for { this.json, _ = sjson.Set(this.json, *this.path, value)
select { this.reader = gjson.Parse(this.json)
case event := <-watcher.Events: this.save()
if event.Op&fsnotify.Write == fsnotify.Write { this.mu.Unlock()
config_path := filepath.Join(c.Runtime.ConfigPath, "config.json") }
if err = c.loadConfig(config_path); err != nil { return this
log.Println("can't load config file")
} else {
c.populateDefault(config_path)
}
}
}
}
}()
_ = watcher.Add(c.Runtime.ConfigPath)
<-done
} }
func (c *Config) populateDefault(path string) { func (this *Config) Set(value interface{}) *Config {
if c.General.Port == 0 { if this.path == nil {
c.General.Port = 8334 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 == "" { return 0
c.General.SecretKey = RandomString(16) }
j, err := json.MarshalIndent(c, "", " ")
if err == nil { func (this Config) Bool() bool {
f, err := os.OpenFile(path, os.O_WRONLY, os.ModePerm) val := this.reader.Get(*this.path).Value()
if err == nil { switch val.(type) {
f.Write(j) case bool: return val.(bool)
f.Close()
}
}
} }
if c.OAuthProvider.Dropbox.ClientID == "" { return false
c.OAuthProvider.Dropbox.ClientID = os.Getenv("DROPBOX_CLIENT_ID") }
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 == "" { if f, err := os.OpenFile(configPath, os.O_WRONLY|os.O_CREATE, os.ModePerm); err == nil {
c.OAuthProvider.GoogleDrive.ClientID = os.Getenv("GDRIVE_CLIENT_ID") buf := bytes.NewBuffer(PrettyPrint([]byte(this.json)))
} io.Copy(f, buf)
if c.OAuthProvider.GoogleDrive.ClientSecret == "" { f.Close()
c.OAuthProvider.GoogleDrive.ClientSecret = os.Getenv("GDRIVE_CLIENT_SECRET")
}
if c.General.Host == "" {
c.General.Host = os.Getenv("APPLICATION_URL")
} }
} }
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 { publicConf := struct {
Editor string `json:"editor"` Editor string `json:"editor"`
ForkButton bool `json:"fork_button"` ForkButton bool `json:"fork_button"`
DisplayHidden bool `json:"display_hidden"` DisplayHidden bool `json:"display_hidden"`
AutoConnect bool `json:"auto_connect"` AutoConnect bool `json:"auto_connect"`
Name string `json:"name"` Name string `json:"name"`
RememberMe *bool `json:"remember_me"` RememberMe bool `json:"remember_me"`
Connections interface{} `json:"connections"` Connections interface{} `json:"connections"`
MimeTypes map[string]string `json:"mime"` MimeTypes map[string]string `json:"mime"`
}{ }{
Editor: c.General.Editor, Editor: this.Get("general.editor").String(),
ForkButton: c.General.ForkButton, ForkButton: this.Get("general.fork_button").Bool(),
DisplayHidden: c.General.DisplayHidden, DisplayHidden: this.Get("general.display_hidden").Bool(),
AutoConnect: c.General.AutoConnect, AutoConnect: this.Get("general.auto_connect").Bool(),
Connections: c.Connections, Name: this.Get("general.name").String(),
MimeTypes: c.MimeTypes, RememberMe: this.Get("general.remember_me").Bool(),
Name: c.General.Name, Connections: this.Get("connections").Interface(),
RememberMe: c.General.RememberMe, MimeTypes: AllMimeTypes(),
} }
j, err := json.Marshal(publicConf) j, err := json.Marshal(publicConf)
if err != nil { if err != nil {
@ -200,13 +190,3 @@ func (c *Config) Export() (string, error) {
} }
return string(j), nil 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)
}

View File

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

View File

@ -1,6 +1,8 @@
package common package common
const ( const (
APP_VERSION = "v0.3"
CONFIG_PATH = "data/config/"
COOKIE_NAME_AUTH = "auth" COOKIE_NAME_AUTH = "auth"
COOKIE_NAME_PROOF = "proof" COOKIE_NAME_PROOF = "proof"
COOKIE_PATH = "/api/" COOKIE_PATH = "/api/"

View File

@ -138,6 +138,7 @@ func verify(something []byte) ([]byte, error) {
return something, nil return something, nil
} }
// Create a unique ID that can be use to identify different session
func GenerateID(params map[string]string) string { func GenerateID(params map[string]string) string {
p := "type =>" + params["type"] p := "type =>" + params["type"]
p += "host =>" + params["host"] p += "host =>" + params["host"]

View File

@ -35,8 +35,9 @@ func TestIDGeneration(t *testing.T) {
func TestStringGeneration(t *testing.T) { func TestStringGeneration(t *testing.T) {
str := QuickString(10) str := QuickString(10)
str1 := QuickString(10) str1 := QuickString(16)
str2 := QuickString(10) str2 := QuickString(24)
assert.Equal(t, len(str), 10) assert.Equal(t, len(str), 10)
t.Log(str, str1, str2) assert.Equal(t, len(str1), 16)
assert.Equal(t, len(str2), 24)
} }

33
server/common/default.go Normal file
View File

@ -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,
},
}

View File

@ -1,5 +1,19 @@
package common 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 { func IsDirectory(path string) bool {
if string(path[len(path)-1]) != "/" { if string(path[len(path)-1]) != "/" {
return false return false

View File

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

View File

@ -2,13 +2,7 @@ package common
import ( import (
"time" "time"
"log" slog "log"
)
const (
LOG_INFO = "INFO"
LOG_WARNING = "WARNING"
LOG_ERROR = "ERROR"
) )
type LogEntry struct { type LogEntry struct {
@ -27,34 +21,76 @@ type LogEntry struct {
Backend string `json:"backend"` Backend string `json:"backend"`
} }
func Log(ctx *App, str string, level string){ type log struct{
if ctx.Config.Log.Enable == false { enable bool
return
}
shouldDisplay := func(r string, l string) bool { debug bool
levels := []string{"DEBUG", "INFO", "WARNING", "ERROR"} info bool
warn bool
error bool
}
configLevel := -1 func (l *log) Info(str string) {
currentLevel := 0 if l.info && l.enable {
slog.Printf("INFO %s\n", str)
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) 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
}()

37
server/common/mime.go Normal file
View File

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

11
server/common/plugin.go Normal file
View File

@ -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"
)

View File

@ -7,6 +7,7 @@ import (
) )
type IBackend interface { type IBackend interface {
Init(params map[string]string, app *App) (IBackend, error)
Ls(path string) ([]os.FileInfo, error) Ls(path string) ([]os.FileInfo, error)
Cat(path string) (io.Reader, error) Cat(path string) (io.Reader, error)
Mkdir(path string) error Mkdir(path string) error

View File

@ -1,5 +1,10 @@
package common package common
import (
"bytes"
"encoding/json"
)
func NewBool(t bool) *bool { func NewBool(t bool) *bool {
return &t return &t
} }
@ -21,6 +26,7 @@ func NewBoolFromInterface(val interface{}) bool {
default: return false default: return false
} }
} }
func NewInt64pFromInterface(val interface{}) *int64 { func NewInt64pFromInterface(val interface{}) *int64 {
switch val.(type) { switch val.(type) {
case int64: case int64:
@ -32,6 +38,7 @@ func NewInt64pFromInterface(val interface{}) *int64 {
default: return nil default: return nil
} }
} }
func NewStringpFromInterface(val interface{}) *string { func NewStringpFromInterface(val interface{}) *string {
switch val.(type) { switch val.(type) {
case string: case string:
@ -49,3 +56,13 @@ func NewStringFromInterface(val interface{}) string {
default: return "" 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()
}

View File

@ -2,8 +2,8 @@ package ctrl
import ( import (
. "github.com/mickael-kerjean/nuage/server/common" . "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/model"
"github.com/mickael-kerjean/nuage/server/plugin"
"io" "io"
"net/http" "net/http"
"path/filepath" "path/filepath"
@ -110,10 +110,17 @@ func FileCat(ctx App, res http.ResponseWriter, req *http.Request) {
return return
} }
file, err = services.ProcessFileBeforeSend(file, &ctx, req, &res) mType := GetMimeType(req.URL.Query().Get("path"))
if err != nil { res.Header().Set("Content-Type", mType)
SendErrorResult(res, err)
return 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) io.Copy(res, file)
} }

View File

@ -69,8 +69,7 @@ func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
SendErrorResult(res, NewError(err.Error(), 500)) SendErrorResult(res, NewError(err.Error(), 500))
return 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 { if err != nil {
SendErrorResult(res, NewError(err.Error(), 500)) SendErrorResult(res, NewError(err.Error(), 500))
return return

View File

@ -49,7 +49,7 @@ func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) {
return "" return ""
} }
var data map[string]string 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 { if err != nil {
return "" return ""
} }
@ -78,7 +78,7 @@ func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) {
if err != nil { if err != nil {
return "" 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 { if err != nil {
return "" return ""
} }
@ -156,7 +156,7 @@ func ShareVerifyProof(ctx App, res http.ResponseWriter, req *http.Request) {
Name: COOKIE_NAME_PROOF, Name: COOKIE_NAME_PROOF,
Value: func(p []model.Proof) string { Value: func(p []model.Proof) string {
j, _ := json.Marshal(p) 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 return str
}(verifiedProof), }(verifiedProof),
Path: COOKIE_PATH, Path: COOKIE_PATH,

View File

@ -22,7 +22,7 @@ func StaticHandler(_path string, ctx App) http.Handler {
return return
} }
absPath := ctx.Helpers.AbsolutePath(_path) absPath := GetAbsolutePath(_path)
fsrv := http.FileServer(http.Dir(absPath)) fsrv := http.FileServer(http.Dir(absPath))
_, err := os.Open(path.Join(absPath, req.URL.Path+".gz")) _, err := os.Open(path.Join(absPath, req.URL.Path+".gz"))
if err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") { 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) SecureHeader(&header)
p := _path 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") res.Header().Set("Content-Encoding", "gzip")
p += ".gz" p += ".gz"
} }
http.ServeFile(res, req, ctx.Helpers.AbsolutePath(p)) http.ServeFile(res, req, GetAbsolutePath(p))
}) })
} }

View File

@ -2,13 +2,13 @@ package ctrl
import ( import (
"net/http" "net/http"
"log"
. "github.com/mickael-kerjean/nuage/server/common" . "github.com/mickael-kerjean/nuage/server/common"
. "github.com/mickael-kerjean/nuage/server/middleware" . "github.com/mickael-kerjean/nuage/server/middleware"
"github.com/mickael-kerjean/nuage/server/model" "github.com/mickael-kerjean/nuage/server/model"
"github.com/mickael-kerjean/net/webdav" "github.com/mickael-kerjean/net/webdav"
"github.com/mickael-kerjean/mux" "github.com/mickael-kerjean/mux"
"time" "time"
"fmt"
) )
var start time.Time = time.Now() var start time.Time = time.Now()
@ -40,7 +40,8 @@ func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) {
} }
// webdav is WIP // webdav is WIP
return http.NotFound(res, req) http.NotFound(res, req)
return
h := &webdav.Handler{ h := &webdav.Handler{
Prefix: "/s/" + share_id, Prefix: "/s/" + share_id,
@ -53,12 +54,7 @@ func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) {
} }
return "OK" return "OK"
}(err) }(err)
log.Printf("INFO %s WEBDAV %s %s %s", share_id, req.Method, req.URL.Path, e) Log.Info(fmt.Sprintf("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()
}, },
} }
h.ServeHTTP(res, req) h.ServeHTTP(res, req)

View File

@ -1,11 +1,12 @@
package main package main
import ( import (
"fmt"
"github.com/mickael-kerjean/mux" "github.com/mickael-kerjean/mux"
. "github.com/mickael-kerjean/nuage/server/common" . "github.com/mickael-kerjean/nuage/server/common"
. "github.com/mickael-kerjean/nuage/server/ctrl" . "github.com/mickael-kerjean/nuage/server/ctrl"
. "github.com/mickael-kerjean/nuage/server/middleware" . "github.com/mickael-kerjean/nuage/server/middleware"
"log" _ "github.com/mickael-kerjean/nuage/server/plugin"
"net/http" "net/http"
"strconv" "strconv"
) )
@ -13,7 +14,6 @@ import (
func main() { func main() {
app := App{} app := App{}
app.Config = NewConfig() app.Config = NewConfig()
app.Helpers = NewHelpers(app.Config)
Init(&app) Init(&app)
select {} select {}
} }
@ -53,15 +53,15 @@ func Init(a *App) *http.Server {
r.PathPrefix("/").Handler(DefaultHandler(FILE_INDEX, *a)).Methods("GET") r.PathPrefix("/").Handler(DefaultHandler(FILE_INDEX, *a)).Methods("GET")
srv := &http.Server{ srv := &http.Server{
Addr: ":" + strconv.Itoa(a.Config.General.Port), Addr: ":" + strconv.Itoa(a.Config.Get("general.port").Int()),
Handler: r, Handler: r,
} }
go func() { go func() {
if err := srv.ListenAndServe(); err != nil { if err := srv.ListenAndServe(); err != nil {
log.Fatal("SERVER START ERROR ", err) Log.Error(fmt.Sprintf("server start: %v", err))
return return
} }
log.Println("SERVER START OK") Log.Info("Server start")
}() }()
return srv return srv
} }

View File

@ -26,12 +26,14 @@ func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.
fn(ctx, &resw, req) fn(ctx, &resw, req)
req.Body.Close() req.Body.Close()
if ctx.Config.Log.Telemetry { go func() {
go telemetry(req, &resw, start, ctx.Backend.Info()) if ctx.Config.Get("log.telemetry").Bool() {
} go telemetry(req, &resw, start, ctx.Backend.Info())
if ctx.Config.Log.Enable { }
go logger(req, &resw, start) 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 = 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) err := json.Unmarshal([]byte(str), &res)
return res, err return res, err
} }

View File

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

View File

@ -19,10 +19,18 @@ type Dropbox struct {
Bearer string Bearer string
} }
func NewDropbox(params map[string]string, app *App) (IBackend, error) { func init() {
backend := Dropbox{} Backend.Register("dropbox", Dropbox{})
backend.ClientId = app.Config.OAuthProvider.Dropbox.ClientID }
backend.Hostname = app.Config.General.Host
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"] backend.Bearer = params["bearer"]
if backend.ClientId == "" { if backend.ClientId == "" {

View File

@ -14,6 +14,8 @@ import (
var FtpCache AppCache var FtpCache AppCache
func init() { func init() {
Backend.Register("ftp", Ftp{})
FtpCache = NewAppCache(2, 1) FtpCache = NewAppCache(2, 1)
FtpCache.OnEvict(func(key string, value interface{}) { FtpCache.OnEvict(func(key string, value interface{}) {
c := value.(*Ftp) c := value.(*Ftp)
@ -25,7 +27,7 @@ type Ftp struct {
client *goftp.Client 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) c := FtpCache.Get(params)
if c != nil { if c != nil {
d := c.(*Ftp) d := c.(*Ftp)

View File

@ -22,22 +22,29 @@ type GDrive struct {
Config *oauth2.Config 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{} 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{ config := &oauth2.Config{
Endpoint: google.Endpoint, Endpoint: google.Endpoint,
ClientID: app.Config.OAuthProvider.GoogleDrive.ClientID, ClientID: app.Config.Get("oauth.gdrive.client_id").Default(os.Getenv("GDRIVE_CLIENT_ID")).String(),
ClientSecret: app.Config.OAuthProvider.GoogleDrive.ClientSecret, ClientSecret: app.Config.Get("oauth.gdrive.client_secret").Default(os.Getenv("GDRIVE_CLIENT_SECRET")).String(),
RedirectURL: app.Config.General.Host + "/login", RedirectURL: app.Config.Get("general.host").String() + "/login",
Scopes: []string{"https://www.googleapis.com/auth/drive"}, 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{ token := &oauth2.Token{
AccessToken: params["token"], AccessToken: params["token"],
RefreshToken: params["refresh"], RefreshToken: params["refresh"],

View File

@ -26,6 +26,8 @@ type Git struct {
} }
func init() { func init() {
Backend.Register("git", Git{})
GitCache = NewAppCache() GitCache = NewAppCache()
cachePath := filepath.Join(GetCurrentDir(), GitCachePath) cachePath := filepath.Join(GetCurrentDir(), GitCachePath)
os.RemoveAll(cachePath) os.RemoveAll(cachePath)
@ -50,7 +52,7 @@ type GitParams struct {
basePath string 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 { if obj := GitCache.Get(params); obj != nil {
return obj.(*Git), nil return obj.(*Git), nil
} }
@ -95,7 +97,7 @@ func NewGit(params map[string]string, app *App) (*Git, error) {
} }
hash := GenerateID(params) 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) repo, err := g.git.open(p, p.basePath)
g.git.repo = repo g.git.repo = repo

View File

@ -22,10 +22,12 @@ type S3Backend struct {
} }
func init() { func init() {
Backend.Register("s3", S3Backend{})
S3Cache = NewAppCache(2, 1) 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"] == "" { if params["region"] == "" {
params["region"] = "us-east-2" params["region"] = "us-east-2"
} }

View File

@ -18,16 +18,16 @@ type Sftp struct {
} }
func init() { func init() {
SftpCache = NewAppCache() Backend.Register("sftp", Sftp{})
SftpCache = NewAppCache()
SftpCache.OnEvict(func(key string, value interface{}) { SftpCache.OnEvict(func(key string, value interface{}) {
c := value.(*Sftp) c := value.(*Sftp)
c.Close() c.Close()
}) })
} }
func NewSftp(params map[string]string, app *App) (*Sftp, error) { func (s Sftp) Init(params map[string]string, app *App) (IBackend, error) {
var s Sftp = Sftp{}
p := struct { p := struct {
hostname string hostname string
port string port string

View File

@ -24,7 +24,11 @@ type WebDavParams struct {
path string 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"], "") params["url"] = regexp.MustCompile(`\/$`).ReplaceAllString(params["url"], "")
backend := WebDav{ backend := WebDav{
params: &WebDavParams{ params: &WebDavParams{

View File

@ -3,48 +3,37 @@ package model
import ( import (
"fmt" "fmt"
. "github.com/mickael-kerjean/nuage/server/common" . "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) { func NewBackend(ctx *App, conn map[string]string) (IBackend, error) {
isAllowed := false isAllowed := func() bool {
for i := range ctx.Config.Connections { ret := false
if ctx.Config.Connections[i].Type == conn["type"] { var conns [] struct {
if ctx.Config.Connections[i].Hostname == nil { Type string `json:"type"`
isAllowed = true Hostname string `json:"hostname"`
break; Path string `json:"path"`
}else if *ctx.Config.Connections[i].Hostname == conn["hostname"] { }
isAllowed = true ctx.Config.Get("connections").Scan(&conns)
break; 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 { if isAllowed == false {
return backend.NewNothing(conn, ctx) return Backend.Get(BACKEND_NIL).Init(conn, ctx)
} }
return Backend.Get(conn["type"]).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)
} }
func GetHome(b IBackend) (string, error) { func GetHome(b IBackend) (string, error) {
@ -56,6 +45,7 @@ func GetHome(b IBackend) (string, error) {
return "", err return "", err
} }
func MapStringInterfaceToMapStringString(m map[string]interface{}) map[string]string { func MapStringInterfaceToMapStringString(m map[string]interface{}) map[string]string {
res := make(map[string]string) res := make(map[string]string)
for key, value := range m { for key, value := range m {

View File

@ -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") p.Message = NewString("We've sent you a message with a verification code")
// Send email // 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" mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
subject := "Subject: Your verification code\n" subject := "Subject: Your verification code\n"
msg := []byte(subject + mime + "\n" + b.String()) msg := []byte(subject + mime + "\n" + b.String())
auth := smtp.PlainAuth("", ctx.Config.Email.Username, ctx.Config.Email.Password, ctx.Config.Email.Server) auth := smtp.PlainAuth(
if err := smtp.SendMail(addr, auth, ctx.Config.Email.From, []string{"mickael@kerjean.me"}, msg); err != nil { "",
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("ERROR: ", err)
log.Println("Verification code: " + code) log.Println("Verification code: " + code)
return p, NewError("Couldn't send email", 500) return p, NewError("Couldn't send email", 500)
@ -317,7 +327,7 @@ func ShareProofGetAlreadyVerified(req *http.Request, ctx *App) []Proof {
if len(cookieValue) > 500 { if len(cookieValue) > 500 {
return p return p
} }
j, err := DecryptString(ctx.Config.General.SecretKey, cookieValue) j, err := DecryptString(ctx.Config.Get("general.secret_key").String(), cookieValue)
if err != nil { if err != nil {
return p return p
} }

3
server/plugin/example/build.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
go build -buildmode=plugin -o ../../../dist/data/plugin/example.so index.go

View File

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

3
server/plugin/image/build.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
go build -buildmode=plugin -o ../../../dist/data/plugin/image.so index.go

View File

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

View File

@ -1,4 +1,4 @@
package images package lib
// #cgo pkg-config: libraw // #cgo pkg-config: libraw
// #include <raw.h> // #include <raw.h>

View File

@ -1,4 +1,4 @@
package images package lib
// #cgo pkg-config: vips // #cgo pkg-config: vips
// #include <resizer.h> // #include <resizer.h>

68
server/plugin/index.go Normal file
View File

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

View File

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

View File

@ -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
// }