summaryrefslogtreecommitdiffstats
path: root/lib/model/folders.go
diff options
context:
space:
mode:
Diffstat (limited to 'lib/model/folders.go')
-rw-r--r--lib/model/folders.go388
1 files changed, 388 insertions, 0 deletions
diff --git a/lib/model/folders.go b/lib/model/folders.go
new file mode 100644
index 0000000..ed0078e
--- /dev/null
+++ b/lib/model/folders.go
@@ -0,0 +1,388 @@
+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)
+}