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