package model
import (
"encoding/xml"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/Sirupsen/logrus"
common "github.com/iotbzh/xds-common/golib"
"github.com/iotbzh/xds-server/lib/folder"
"github.com/iotbzh/xds-server/lib/syncthing"
"github.com/iotbzh/xds-server/lib/xdsconfig"
"github.com/syncthing/syncthing/lib/sync"
)
// Folders Represent a an XDS folders
type Folders struct {
fileOnDisk string
Conf *xdsconfig.Config
Log *logrus.Logger
SThg *st.SyncThing
folders map[string]*folder.IFOLDER
registerCB []RegisteredCB
}
type RegisteredCB struct {
cb *folder.EventCB
data *folder.EventCBData
}
// Mutex to make add/delete atomic
var fcMutex = sync.NewMutex()
var ffMutex = sync.NewMutex()
// FoldersNew Create a new instance of Model Folders
func FoldersNew(cfg *xdsconfig.Config, st *st.SyncThing) *Folders {
file, _ := xdsconfig.FoldersConfigFilenameGet()
return &Folders{
fileOnDisk: file,
Conf: cfg,
Log: cfg.Log,
SThg: st,
folders: make(map[string]*folder.IFOLDER),
registerCB: []RegisteredCB{},
}
}
// LoadConfig Load folders configuration from disk
func (f *Folders) LoadConfig() error {
var flds []folder.FolderConfig
var stFlds []folder.FolderConfig
// load from disk
if f.Conf.Options.NoFolderConfig {
f.Log.Infof("Don't read folder config file (-no-folderconfig option is set)")
} else if f.fileOnDisk != "" {
f.Log.Infof("Use folder config file: %s", f.fileOnDisk)
err := foldersConfigRead(f.fileOnDisk, &flds)
if err != nil {
if strings.HasPrefix(err.Error(), "No folder config") {
f.Log.Warnf(err.Error())
} else {
return err
}
}
} else {
f.Log.Warnf("Folders config filename not set")
}
// Retrieve initial Syncthing config (just append don't overwrite existing ones)
if f.SThg != nil {
f.Log.Infof("Retrieve syncthing folder config")
if err := f.SThg.FolderLoadFromStConfig(&stFlds); err != nil {
// Don't exit on such error, just log it
f.Log.Errorf(err.Error())
}
}
// Merge syncthing folders into XDS folders
for _, stf := range stFlds {
found := false
for i, xf := range flds {
if xf.ID == stf.ID {
found = true
// sanity check
if xf.Type != folder.TypeCloudSync {
flds[i].Status = folder.StatusErrorConfig
}
break
}
}
// add it
if !found {
flds = append(flds, stf)
}
}
// Detect ghost project
// (IOW existing in xds file config and not in syncthing database)
for i, xf := range flds {
// only for syncthing project
if xf.Type != folder.TypeCloudSync {
continue
}
found := false
for _, stf := range stFlds {
if stf.ID == xf.ID {
found = true
break
}
}
if !found {
flds[i].Status = folder.StatusErrorConfig
}
}
// Update folders
f.Log.Infof("Loading initial folders config: %d folders found", len(flds))
for _, fc := range flds {
if _, err := f.createUpdate(fc, false, true); err != nil {
return err
}
}
// Save config on disk
err := f.SaveConfig()
return err
}
// SaveConfig Save folders configuration to disk
func (f *Folders) SaveConfig() error {
if f.fileOnDisk == "" {
return fmt.Errorf("Folders config filename not set")
}
// FIXME: buffered save or avoid to write on disk each time
return foldersConfigWrite(f.fileOnDisk, f.getConfigArrUnsafe())
}
// Get returns the folder config or nil if not existing
func (f *Folders) Get(id string) *folder.IFOLDER {
if id == "" {
return nil
}
fc, exist := f.folders[id]
if !exist {
return nil
}
return fc
}
// GetConfigArr returns the config of all folders as an array
func (f *Folders) GetConfigArr() []folder.FolderConfig {
fcMutex.Lock()
defer fcMutex.Unlock()
return f.getConfigArrUnsafe()
}
// getConfigArrUnsafe Same as GetConfigArr without mutex protection
func (f *Folders) getConfigArrUnsafe() []folder.FolderConfig {
var conf []folder.FolderConfig
for _, v := range f.folders {
conf = append(conf, (*v).GetConfig())
}
return conf
}
// Add adds a new folder
func (f *Folders) Add(newF folder.FolderConfig) (*folder.FolderConfig, error) {
return f.createUpdate(newF, true, false)
}
// CreateUpdate creates or update a folder
func (f *Folders) createUpdate(newF folder.FolderConfig, create bool, initial bool) (*folder.FolderConfig, error) {
fcMutex.Lock()
defer fcMutex.Unlock()
// Sanity check
if _, exist := f.folders[newF.ID]; create && exist {
return nil, fmt.Errorf("ID already exists")
}
if newF.ClientPath == "" {
return nil, fmt.Errorf("ClientPath must be set")
}
// Create a new folder object
var fld folder.IFOLDER
switch newF.Type {
// SYNCTHING
case folder.TypeCloudSync:
if f.SThg == nil {
return nil, fmt.Errorf("ClownSync type not supported (syncthing not initialized)")
}
fld = f.SThg.NewFolderST(f.Conf)
// PATH MAP
case folder.TypePathMap:
fld = folder.NewFolderPathMap(f.Conf)
default:
return nil, fmt.Errorf("Unsupported folder type")
}
// Set default value if needed
if newF.Status == "" {
newF.Status = folder.StatusDisable
}
if newF.Label == "" {
newF.Label = filepath.Base(newF.ClientPath) + "_" + newF.ID[0:8]
}
// Allocate a new UUID
if create {
i := len(newF.Label)
if i > 20 {
i = 20
}
newF.ID = fld.NewUID(newF.Label[:i])
}
if !create && newF.ID == "" {
return nil, fmt.Errorf("Cannot update folder with null ID")
}
// Normalize path (needed for Windows path including bashlashes)
newF.ClientPath = common.PathNormalize(newF.ClientPath)
// Add new folder
newFolder, err := fld.Add(newF)
if err != nil {
newF.Status = folder.StatusErrorConfig
log.Printf("ERROR Adding folder: %v\n", err)
return newFolder, err
}
// Add to folders list
f.folders[newF.ID] = &fld
// Save config on disk
if !initial {
if err := f.SaveConfig(); err != nil {
return newFolder, err
}
}
// Register event change callback
for _, rcb := range f.registerCB {
if err := fld.RegisterEventChange(rcb.cb, rcb.data); err != nil {
return newFolder, err
}
}
// Force sync after creation
// (need to defer to be sure that WS events will arrive after HTTP creation reply)
go func() {
time.Sleep(time.Millisecond * 500)
fld.Sync()
}()
return newFolder, nil
}
// Delete deletes a specific folder
func (f *Folders) Delete(id string) (folder.FolderConfig, error) {
var err error
fcMutex.Lock()
defer fcMutex.Unlock()
fld := folder.FolderConfig{}
fc, exist := f.folders[id]
if !exist {
return fld, fmt.Errorf("unknown id")
}
fld = (*fc).GetConfig()
if err = (*fc).Remove(); err != nil {
return fld, err
}
delete(f.folders, id)
// Save config on disk
err = f.SaveConfig()
return fld, err
}
// RegisterEventChange requests registration for folder event change
func (f *Folders) RegisterEventChange(id string, cb *folder.EventCB, data *folder.EventCBData) error {
flds := make(map[string]*folder.IFOLDER)
if id != "" {
// Register to a specific folder
flds[id] = f.Get(id)
} else {
// Register to all folders
flds = f.folders
f.registerCB = append(f.registerCB, RegisteredCB{cb: cb, data: data})
}
for _, fld := range flds {
err := (*fld).RegisterEventChange(cb, data)
if err != nil {
return err
}
}
return nil
}
// ForceSync Force the synchronization of a folder
func (f *Folders) ForceSync(id string) error {
fc := f.Get(id)
if fc == nil {
return fmt.Errorf("Unknown id")
}
return (*fc).Sync()
}
// IsFolderInSync Returns true when folder is in sync
func (f *Folders) IsFolderInSync(id string) (bool, error) {
fc := f.Get(id)
if fc == nil {
return false, fmt.Errorf("Unknown id")
}
return (*fc).IsInSync()
}
//*** Private functions ***
// Use XML format and not json to be able to save/load all fields including
// ones that are masked in json (IOW defined with `json:"-"`)
type xmlFolders struct {
XMLName xml.Name `xml:"folders"`
Version string `xml:"version,attr"`
Folders []folder.FolderConfig `xml:"folders"`
}
// foldersConfigRead reads folders config from disk
func foldersConfigRead(file string, folders *[]folder.FolderConfig) error {
if !common.Exists(file) {
return fmt.Errorf("No folder config file found (%s)", file)
}
ffMutex.Lock()
defer ffMutex.Unlock()
fd, err := os.Open(file)
defer fd.Close()
if err != nil {
return err
}
data := xmlFolders{}
err = xml.NewDecoder(fd).Decode(&data)
if err == nil {
*folders = data.Folders
}
return err
}
// foldersConfigWrite writes folders config on disk
func foldersConfigWrite(file string, folders []folder.FolderConfig) error {
ffMutex.Lock()
defer ffMutex.Unlock()
fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
defer fd.Close()
if err != nil {
return err
}
data := &xmlFolders{
Version: "1",
Folders: folders,
}
enc := xml.NewEncoder(fd)
enc.Indent("", " ")
return enc.Encode(data)
}