ed0078ea255fb6c722053cfb76c13530d521de5f
[src/xds/xds-server.git] / lib / model / folders.go
1 package model
2
3 import (
4         "encoding/xml"
5         "fmt"
6         "log"
7         "os"
8         "path/filepath"
9         "strings"
10         "time"
11
12         "github.com/Sirupsen/logrus"
13         common "github.com/iotbzh/xds-common/golib"
14         "github.com/iotbzh/xds-server/lib/folder"
15         "github.com/iotbzh/xds-server/lib/syncthing"
16         "github.com/iotbzh/xds-server/lib/xdsconfig"
17         "github.com/syncthing/syncthing/lib/sync"
18 )
19
20 // Folders Represent a an XDS folders
21 type Folders struct {
22         fileOnDisk string
23         Conf       *xdsconfig.Config
24         Log        *logrus.Logger
25         SThg       *st.SyncThing
26         folders    map[string]*folder.IFOLDER
27         registerCB []RegisteredCB
28 }
29
30 type RegisteredCB struct {
31         cb   *folder.EventCB
32         data *folder.EventCBData
33 }
34
35 // Mutex to make add/delete atomic
36 var fcMutex = sync.NewMutex()
37 var ffMutex = sync.NewMutex()
38
39 // FoldersNew Create a new instance of Model Folders
40 func FoldersNew(cfg *xdsconfig.Config, st *st.SyncThing) *Folders {
41         file, _ := xdsconfig.FoldersConfigFilenameGet()
42         return &Folders{
43                 fileOnDisk: file,
44                 Conf:       cfg,
45                 Log:        cfg.Log,
46                 SThg:       st,
47                 folders:    make(map[string]*folder.IFOLDER),
48                 registerCB: []RegisteredCB{},
49         }
50 }
51
52 // LoadConfig Load folders configuration from disk
53 func (f *Folders) LoadConfig() error {
54         var flds []folder.FolderConfig
55         var stFlds []folder.FolderConfig
56
57         // load from disk
58         if f.Conf.Options.NoFolderConfig {
59                 f.Log.Infof("Don't read folder config file (-no-folderconfig option is set)")
60         } else if f.fileOnDisk != "" {
61                 f.Log.Infof("Use folder config file: %s", f.fileOnDisk)
62                 err := foldersConfigRead(f.fileOnDisk, &flds)
63                 if err != nil {
64                         if strings.HasPrefix(err.Error(), "No folder config") {
65                                 f.Log.Warnf(err.Error())
66                         } else {
67                                 return err
68                         }
69                 }
70         } else {
71                 f.Log.Warnf("Folders config filename not set")
72         }
73
74         // Retrieve initial Syncthing config (just append don't overwrite existing ones)
75         if f.SThg != nil {
76                 f.Log.Infof("Retrieve syncthing folder config")
77                 if err := f.SThg.FolderLoadFromStConfig(&stFlds); err != nil {
78                         // Don't exit on such error, just log it
79                         f.Log.Errorf(err.Error())
80                 }
81         }
82
83         // Merge syncthing folders into XDS folders
84         for _, stf := range stFlds {
85                 found := false
86                 for i, xf := range flds {
87                         if xf.ID == stf.ID {
88                                 found = true
89                                 // sanity check
90                                 if xf.Type != folder.TypeCloudSync {
91                                         flds[i].Status = folder.StatusErrorConfig
92                                 }
93                                 break
94                         }
95                 }
96                 // add it
97                 if !found {
98                         flds = append(flds, stf)
99                 }
100         }
101
102         // Detect ghost project
103         // (IOW existing in xds file config and not in syncthing database)
104         for i, xf := range flds {
105                 // only for syncthing project
106                 if xf.Type != folder.TypeCloudSync {
107                         continue
108                 }
109                 found := false
110                 for _, stf := range stFlds {
111                         if stf.ID == xf.ID {
112                                 found = true
113                                 break
114                         }
115                 }
116                 if !found {
117                         flds[i].Status = folder.StatusErrorConfig
118                 }
119         }
120
121         // Update folders
122         f.Log.Infof("Loading initial folders config: %d folders found", len(flds))
123         for _, fc := range flds {
124                 if _, err := f.createUpdate(fc, false, true); err != nil {
125                         return err
126                 }
127         }
128
129         // Save config on disk
130         err := f.SaveConfig()
131
132         return err
133 }
134
135 // SaveConfig Save folders configuration to disk
136 func (f *Folders) SaveConfig() error {
137         if f.fileOnDisk == "" {
138                 return fmt.Errorf("Folders config filename not set")
139         }
140
141         // FIXME: buffered save or avoid to write on disk each time
142         return foldersConfigWrite(f.fileOnDisk, f.getConfigArrUnsafe())
143 }
144
145 // Get returns the folder config or nil if not existing
146 func (f *Folders) Get(id string) *folder.IFOLDER {
147         if id == "" {
148                 return nil
149         }
150         fc, exist := f.folders[id]
151         if !exist {
152                 return nil
153         }
154         return fc
155 }
156
157 // GetConfigArr returns the config of all folders as an array
158 func (f *Folders) GetConfigArr() []folder.FolderConfig {
159         fcMutex.Lock()
160         defer fcMutex.Unlock()
161
162         return f.getConfigArrUnsafe()
163 }
164
165 // getConfigArrUnsafe Same as GetConfigArr without mutex protection
166 func (f *Folders) getConfigArrUnsafe() []folder.FolderConfig {
167         var conf []folder.FolderConfig
168
169         for _, v := range f.folders {
170                 conf = append(conf, (*v).GetConfig())
171         }
172         return conf
173 }
174
175 // Add adds a new folder
176 func (f *Folders) Add(newF folder.FolderConfig) (*folder.FolderConfig, error) {
177         return f.createUpdate(newF, true, false)
178 }
179
180 // CreateUpdate creates or update a folder
181 func (f *Folders) createUpdate(newF folder.FolderConfig, create bool, initial bool) (*folder.FolderConfig, error) {
182
183         fcMutex.Lock()
184         defer fcMutex.Unlock()
185
186         // Sanity check
187         if _, exist := f.folders[newF.ID]; create && exist {
188                 return nil, fmt.Errorf("ID already exists")
189         }
190         if newF.ClientPath == "" {
191                 return nil, fmt.Errorf("ClientPath must be set")
192         }
193
194         // Create a new folder object
195         var fld folder.IFOLDER
196         switch newF.Type {
197         // SYNCTHING
198         case folder.TypeCloudSync:
199                 if f.SThg == nil {
200                         return nil, fmt.Errorf("ClownSync type not supported (syncthing not initialized)")
201                 }
202                 fld = f.SThg.NewFolderST(f.Conf)
203         // PATH MAP
204         case folder.TypePathMap:
205                 fld = folder.NewFolderPathMap(f.Conf)
206         default:
207                 return nil, fmt.Errorf("Unsupported folder type")
208         }
209
210         // Set default value if needed
211         if newF.Status == "" {
212                 newF.Status = folder.StatusDisable
213         }
214         if newF.Label == "" {
215                 newF.Label = filepath.Base(newF.ClientPath) + "_" + newF.ID[0:8]
216         }
217
218         // Allocate a new UUID
219         if create {
220                 i := len(newF.Label)
221                 if i > 20 {
222                         i = 20
223                 }
224                 newF.ID = fld.NewUID(newF.Label[:i])
225         }
226         if !create && newF.ID == "" {
227                 return nil, fmt.Errorf("Cannot update folder with null ID")
228         }
229
230         // Normalize path (needed for Windows path including bashlashes)
231         newF.ClientPath = common.PathNormalize(newF.ClientPath)
232
233         // Add new folder
234         newFolder, err := fld.Add(newF)
235         if err != nil {
236                 newF.Status = folder.StatusErrorConfig
237                 log.Printf("ERROR Adding folder: %v\n", err)
238                 return newFolder, err
239         }
240
241         // Add to folders list
242         f.folders[newF.ID] = &fld
243
244         // Save config on disk
245         if !initial {
246                 if err := f.SaveConfig(); err != nil {
247                         return newFolder, err
248                 }
249         }
250
251         // Register event change callback
252         for _, rcb := range f.registerCB {
253                 if err := fld.RegisterEventChange(rcb.cb, rcb.data); err != nil {
254                         return newFolder, err
255                 }
256         }
257
258         // Force sync after creation
259         // (need to defer to be sure that WS events will arrive after HTTP creation reply)
260         go func() {
261                 time.Sleep(time.Millisecond * 500)
262                 fld.Sync()
263         }()
264
265         return newFolder, nil
266 }
267
268 // Delete deletes a specific folder
269 func (f *Folders) Delete(id string) (folder.FolderConfig, error) {
270         var err error
271
272         fcMutex.Lock()
273         defer fcMutex.Unlock()
274
275         fld := folder.FolderConfig{}
276         fc, exist := f.folders[id]
277         if !exist {
278                 return fld, fmt.Errorf("unknown id")
279         }
280
281         fld = (*fc).GetConfig()
282
283         if err = (*fc).Remove(); err != nil {
284                 return fld, err
285         }
286
287         delete(f.folders, id)
288
289         // Save config on disk
290         err = f.SaveConfig()
291
292         return fld, err
293 }
294
295 // RegisterEventChange requests registration for folder event change
296 func (f *Folders) RegisterEventChange(id string, cb *folder.EventCB, data *folder.EventCBData) error {
297
298         flds := make(map[string]*folder.IFOLDER)
299         if id != "" {
300                 // Register to a specific folder
301                 flds[id] = f.Get(id)
302         } else {
303                 // Register to all folders
304                 flds = f.folders
305                 f.registerCB = append(f.registerCB, RegisteredCB{cb: cb, data: data})
306         }
307
308         for _, fld := range flds {
309                 err := (*fld).RegisterEventChange(cb, data)
310                 if err != nil {
311                         return err
312                 }
313         }
314
315         return nil
316 }
317
318 // ForceSync Force the synchronization of a folder
319 func (f *Folders) ForceSync(id string) error {
320         fc := f.Get(id)
321         if fc == nil {
322                 return fmt.Errorf("Unknown id")
323         }
324         return (*fc).Sync()
325 }
326
327 // IsFolderInSync Returns true when folder is in sync
328 func (f *Folders) IsFolderInSync(id string) (bool, error) {
329         fc := f.Get(id)
330         if fc == nil {
331                 return false, fmt.Errorf("Unknown id")
332         }
333         return (*fc).IsInSync()
334 }
335
336 //*** Private functions ***
337
338 // Use XML format and not json to be able to save/load all fields including
339 // ones that are masked in json (IOW defined with `json:"-"`)
340 type xmlFolders struct {
341         XMLName xml.Name              `xml:"folders"`
342         Version string                `xml:"version,attr"`
343         Folders []folder.FolderConfig `xml:"folders"`
344 }
345
346 // foldersConfigRead reads folders config from disk
347 func foldersConfigRead(file string, folders *[]folder.FolderConfig) error {
348         if !common.Exists(file) {
349                 return fmt.Errorf("No folder config file found (%s)", file)
350         }
351
352         ffMutex.Lock()
353         defer ffMutex.Unlock()
354
355         fd, err := os.Open(file)
356         defer fd.Close()
357         if err != nil {
358                 return err
359         }
360
361         data := xmlFolders{}
362         err = xml.NewDecoder(fd).Decode(&data)
363         if err == nil {
364                 *folders = data.Folders
365         }
366         return err
367 }
368
369 // foldersConfigWrite writes folders config on disk
370 func foldersConfigWrite(file string, folders []folder.FolderConfig) error {
371         ffMutex.Lock()
372         defer ffMutex.Unlock()
373
374         fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
375         defer fd.Close()
376         if err != nil {
377                 return err
378         }
379
380         data := &xmlFolders{
381                 Version: "1",
382                 Folders: folders,
383         }
384
385         enc := xml.NewEncoder(fd)
386         enc.Indent("", "  ")
387         return enc.Encode(data)
388 }