mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-11-02 03:54:59 +08:00
feature (plugin): API to develop plugin
This commit is contained in:
@ -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 && \
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
77
server/common/config_test.go
Normal file
77
server/common/config_test.go
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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/"
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
33
server/common/default.go
Normal file
33
server/common/default.go
Normal 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,
|
||||
},
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}()
|
||||
|
||||
37
server/common/mime.go
Normal file
37
server/common/mime.go
Normal 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
11
server/common/plugin.go
Normal 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"
|
||||
)
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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 == "" {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"],
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
3
server/plugin/example/build.sh
Executable file
3
server/plugin/example/build.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
go build -buildmode=plugin -o ../../../dist/data/plugin/example.so index.go
|
||||
24
server/plugin/example/index.go
Normal file
24
server/plugin/example/index.go
Normal 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
3
server/plugin/image/build.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
go build -buildmode=plugin -o ../../../dist/data/plugin/image.so index.go
|
||||
109
server/plugin/image/index.go
Normal file
109
server/plugin/image/index.go
Normal 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)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package images
|
||||
package lib
|
||||
|
||||
// #cgo pkg-config: libraw
|
||||
// #include <raw.h>
|
||||
@ -1,4 +1,4 @@
|
||||
package images
|
||||
package lib
|
||||
|
||||
// #cgo pkg-config: vips
|
||||
// #include <resizer.h>
|
||||
68
server/plugin/index.go
Normal file
68
server/plugin/index.go
Normal 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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
// }
|
||||
Reference in New Issue
Block a user