Use go module as dependency tool instead of glide
[src/xds/xds-server.git] / lib / xdsserver / folders.go
1 /*
2  * Copyright (C) 2017-2018 "IoT.bzh"
3  * Author Sebastien Douheret <sebastien@iot.bzh>
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *   http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17
18 package xdsserver
19
20 import (
21         "encoding/xml"
22         "fmt"
23         "log"
24         "os"
25         "path/filepath"
26         "strings"
27         "time"
28
29         common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git"
30         "gerrit.automotivelinux.org/gerrit/src/xds/xds-server.git/lib/xdsconfig"
31         "gerrit.automotivelinux.org/gerrit/src/xds/xds-server.git/lib/xsapiv1"
32         "github.com/franciscocpg/reflectme"
33         "github.com/syncthing/syncthing/lib/sync"
34 )
35
36 // Folders Represent a an XDS folders
37 type Folders struct {
38         *Context
39         fileOnDisk string
40         folders    map[string]*IFOLDER
41         registerCB []RegisteredCB
42 }
43
44 // RegisteredCB Hold registered callbacks
45 type RegisteredCB struct {
46         cb   *FolderEventCB
47         data *FolderEventCBData
48 }
49
50 // Mutex to make add/delete atomic
51 var fcMutex = sync.NewMutex()
52 var ffMutex = sync.NewMutex()
53
54 // FoldersConstructor Create a new instance of Model Folders
55 func FoldersConstructor(ctx *Context) *Folders {
56         file, _ := xdsconfig.FoldersConfigFilenameGet()
57         return &Folders{
58                 Context:    ctx,
59                 fileOnDisk: file,
60                 folders:    make(map[string]*IFOLDER),
61                 registerCB: []RegisteredCB{},
62         }
63 }
64
65 // LoadConfig Load folders configuration from disk
66 func (f *Folders) LoadConfig() error {
67         var flds []xsapiv1.FolderConfig
68         var stFlds []xsapiv1.FolderConfig
69
70         // load from disk
71         if f.Config.Options.NoFolderConfig {
72                 f.Log.Infof("Don't read folder config file (-no-folderconfig option is set)")
73         } else if f.fileOnDisk != "" {
74                 f.Log.Infof("Use folder config file: %s", f.fileOnDisk)
75                 err := foldersConfigRead(f.fileOnDisk, &flds)
76                 if err != nil {
77                         if strings.HasPrefix(err.Error(), "EOF") {
78                                 f.Log.Warnf("Empty folder config file")
79                         } else if strings.HasPrefix(err.Error(), "No folder config") {
80                                 f.Log.Warnf(err.Error())
81                         } else {
82                                 return err
83                         }
84                 }
85         } else {
86                 f.Log.Warnf("Folders config filename not set")
87         }
88
89         // Retrieve initial Syncthing config (just append don't overwrite existing ones)
90         if f.SThg != nil {
91                 f.Log.Infof("Retrieve syncthing folder config")
92                 if err := f.SThg.FolderLoadFromStConfig(&stFlds); err != nil {
93                         // Don't exit on such error, just log it
94                         f.Log.Errorf(err.Error())
95                 }
96         } else {
97                 f.Log.Infof("Syncthing support is disabled.")
98         }
99
100         // Merge syncthing folders into XDS folders
101         for _, stf := range stFlds {
102                 found := false
103                 for i, xf := range flds {
104                         if xf.ID == stf.ID {
105                                 found = true
106                                 // sanity check
107                                 if xf.Type != xsapiv1.TypeCloudSync {
108                                         flds[i].Status = xsapiv1.StatusErrorConfig
109                                 }
110                                 break
111                         }
112                 }
113                 // add it
114                 if !found {
115                         flds = append(flds, stf)
116                 }
117         }
118
119         // Detect ghost project
120         // (IOW existing in xds file config and not in syncthing database)
121         if f.SThg != nil {
122                 for i, xf := range flds {
123                         // only for syncthing project
124                         if xf.Type != xsapiv1.TypeCloudSync {
125                                 continue
126                         }
127                         found := false
128                         for _, stf := range stFlds {
129                                 if stf.ID == xf.ID {
130                                         found = true
131                                         break
132                                 }
133                         }
134                         if !found {
135                                 flds[i].Status = xsapiv1.StatusErrorConfig
136                         }
137                 }
138         }
139
140         // Update folders
141         f.Log.Infof("Loading initial folders config: %d folders found", len(flds))
142         for _, fc := range flds {
143                 if _, err := f.createUpdate(fc, false, true); err != nil {
144                         return err
145                 }
146         }
147
148         // Save config on disk
149         err := f.SaveConfig()
150
151         return err
152 }
153
154 // SaveConfig Save folders configuration to disk
155 func (f *Folders) SaveConfig() error {
156         if f.fileOnDisk == "" {
157                 return fmt.Errorf("Folders config filename not set")
158         }
159
160         // FIXME: buffered save or avoid to write on disk each time
161         return foldersConfigWrite(f.fileOnDisk, f.getConfigArrUnsafe())
162 }
163
164 // ResolveID Complete a Folder ID (helper for user that can use partial ID value)
165 func (f *Folders) ResolveID(id string) (string, error) {
166         if id == "" {
167                 return "", nil
168         }
169
170         match := []string{}
171         for iid := range f.folders {
172                 if strings.HasPrefix(iid, id) {
173                         match = append(match, iid)
174                 }
175         }
176
177         if len(match) == 1 {
178                 return match[0], nil
179         } else if len(match) == 0 {
180                 return id, fmt.Errorf("Unknown id")
181         }
182         return id, fmt.Errorf("Multiple IDs found %v", match)
183 }
184
185 // Get returns the folder config or nil if not existing
186 func (f *Folders) Get(id string) *IFOLDER {
187         if id == "" {
188                 return nil
189         }
190         fc, exist := f.folders[id]
191         if !exist {
192                 return nil
193         }
194         return fc
195 }
196
197 // GetConfigArr returns the config of all folders as an array
198 func (f *Folders) GetConfigArr() []xsapiv1.FolderConfig {
199         fcMutex.Lock()
200         defer fcMutex.Unlock()
201
202         return f.getConfigArrUnsafe()
203 }
204
205 // getConfigArrUnsafe Same as GetConfigArr without mutex protection
206 func (f *Folders) getConfigArrUnsafe() []xsapiv1.FolderConfig {
207         conf := []xsapiv1.FolderConfig{}
208         for _, v := range f.folders {
209                 conf = append(conf, (*v).GetConfig())
210         }
211         return conf
212 }
213
214 // Add adds a new folder
215 func (f *Folders) Add(newF xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
216         return f.createUpdate(newF, true, false)
217 }
218
219 // CreateUpdate creates or update a folder
220 func (f *Folders) createUpdate(newF xsapiv1.FolderConfig, create bool, initial bool) (*xsapiv1.FolderConfig, error) {
221         var err error
222
223         fcMutex.Lock()
224         defer fcMutex.Unlock()
225
226         // Sanity check
227         if _, exist := f.folders[newF.ID]; create && exist {
228                 return nil, fmt.Errorf("ID already exists")
229         }
230         if newF.ClientPath == "" {
231                 return nil, fmt.Errorf("ClientPath must be set")
232         }
233
234         // Create a new folder object
235         var fld IFOLDER
236         switch newF.Type {
237         // SYNCTHING
238         case xsapiv1.TypeCloudSync:
239                 if f.SThg != nil {
240                         fld = NewFolderST(f.Context, f.SThg)
241                 } else {
242                         f.Log.Debugf("Disable project %v (syncthing not initialized)", newF.ID)
243                         fld = NewFolderSTDisable(f.Context)
244                 }
245
246         // PATH MAP
247         case xsapiv1.TypePathMap:
248                 fld = NewFolderPathMap(f.Context)
249         default:
250                 return nil, fmt.Errorf("Unsupported folder type")
251         }
252
253         // Allocate a new UUID
254         if create {
255                 newF.ID = fld.NewUID("")
256         }
257         if !create && newF.ID == "" {
258                 return nil, fmt.Errorf("Cannot update folder with null ID")
259         }
260
261         // Set default value if needed
262         if newF.Status == "" {
263                 newF.Status = xsapiv1.StatusDisable
264         }
265         if newF.Label == "" {
266                 newF.Label = filepath.Base(newF.ClientPath)
267                 if len(newF.ID) > 8 {
268                         newF.Label += "_" + newF.ID[0:8]
269                 }
270         }
271
272         // Normalize path (needed for Windows path including bashlashes)
273         newF.ClientPath = common.PathNormalize(newF.ClientPath)
274
275         var newFolder *xsapiv1.FolderConfig
276         if create {
277                 // Add folder
278                 if newFolder, err = fld.Add(newF); err != nil {
279                         newF.Status = xsapiv1.StatusErrorConfig
280                         log.Printf("ERROR Adding folder: %v\n", err)
281                         return newFolder, err
282                 }
283         } else {
284                 // Just update project config
285                 if newFolder, err = fld.Setup(newF); err != nil {
286                         newF.Status = xsapiv1.StatusErrorConfig
287                         log.Printf("ERROR Updating folder: %v\n", err)
288                         return newFolder, err
289                 }
290         }
291
292         // Add to folders list
293         f.folders[newF.ID] = &fld
294
295         // Save config on disk
296         if !initial {
297                 if err := f.SaveConfig(); err != nil {
298                         return newFolder, err
299                 }
300         }
301
302         // Force sync after creation
303         // (need to defer to be sure that WS events will arrive after HTTP creation reply)
304         go func() {
305                 time.Sleep(time.Millisecond * 500)
306                 fld.Sync()
307         }()
308
309         return newFolder, nil
310 }
311
312 // Delete deletes a specific folder
313 func (f *Folders) Delete(id string) (xsapiv1.FolderConfig, error) {
314         var err error
315
316         fcMutex.Lock()
317         defer fcMutex.Unlock()
318
319         fld := xsapiv1.FolderConfig{}
320         fc, exist := f.folders[id]
321         if !exist {
322                 return fld, fmt.Errorf("unknown id")
323         }
324
325         fld = (*fc).GetConfig()
326
327         if err = (*fc).Remove(); err != nil {
328                 return fld, err
329         }
330
331         delete(f.folders, id)
332
333         // Save config on disk
334         err = f.SaveConfig()
335
336         return fld, err
337 }
338
339 // Update Update a specific folder
340 func (f *Folders) Update(id string, cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
341         fcMutex.Lock()
342         defer fcMutex.Unlock()
343
344         fc, exist := f.folders[id]
345         if !exist {
346                 return nil, fmt.Errorf("unknown id")
347         }
348
349         // Copy current in a new object to change nothing in case of an error rises
350         newCfg := xsapiv1.FolderConfig{}
351         reflectme.Copy((*fc).GetConfig(), &newCfg)
352
353         // Only update some fields
354         dirty := false
355         for _, fieldName := range xsapiv1.FolderConfigUpdatableFields {
356                 valNew, err := reflectme.GetField(cfg, fieldName)
357                 if err == nil {
358                         valCur, err := reflectme.GetField(newCfg, fieldName)
359                         if err == nil && valNew != valCur {
360                                 err = reflectme.SetField(&newCfg, fieldName, valNew)
361                                 if err != nil {
362                                         return nil, err
363                                 }
364                                 dirty = true
365                         }
366                 }
367         }
368
369         if !dirty {
370                 return &newCfg, nil
371         }
372
373         fld, err := (*fc).Update(newCfg)
374         if err != nil {
375                 return fld, err
376         }
377
378         // Save config on disk
379         err = f.SaveConfig()
380
381         // Send event to notified changes
382         // TODO emit folder change event
383
384         return fld, err
385 }
386
387 // ForceSync Force the synchronization of a folder
388 func (f *Folders) ForceSync(id string) error {
389         fc := f.Get(id)
390         if fc == nil {
391                 return fmt.Errorf("Unknown id")
392         }
393         return (*fc).Sync()
394 }
395
396 // IsFolderInSync Returns true when folder is in sync
397 func (f *Folders) IsFolderInSync(id string) (bool, error) {
398         fc := f.Get(id)
399         if fc == nil {
400                 return false, fmt.Errorf("Unknown id")
401         }
402         return (*fc).IsInSync()
403 }
404
405 //*** Private functions ***
406
407 // Use XML format and not json to be able to save/load all fields including
408 // ones that are masked in json (IOW defined with `json:"-"`)
409 type xmlFolders struct {
410         XMLName xml.Name               `xml:"folders"`
411         Version string                 `xml:"version,attr"`
412         Folders []xsapiv1.FolderConfig `xml:"folders"`
413 }
414
415 // foldersConfigRead reads folders config from disk
416 func foldersConfigRead(file string, folders *[]xsapiv1.FolderConfig) error {
417         if !common.Exists(file) {
418                 return fmt.Errorf("No folder config file found (%s)", file)
419         }
420
421         ffMutex.Lock()
422         defer ffMutex.Unlock()
423
424         fd, err := os.Open(file)
425         defer fd.Close()
426         if err != nil {
427                 return err
428         }
429
430         data := xmlFolders{}
431         err = xml.NewDecoder(fd).Decode(&data)
432         if err == nil {
433                 // Decode old type encoding (number) for backward compatibility
434                 for i, d := range data.Folders {
435                         switch d.Type {
436                         case "1":
437                                 data.Folders[i].Type = xsapiv1.TypePathMap
438                         case "2":
439                                 data.Folders[i].Type = xsapiv1.TypeCloudSync
440                         case "3":
441                                 data.Folders[i].Type = xsapiv1.TypeCifsSmb
442                         }
443                 }
444
445                 *folders = data.Folders
446         }
447         return err
448 }
449
450 // foldersConfigWrite writes folders config on disk
451 func foldersConfigWrite(file string, folders []xsapiv1.FolderConfig) error {
452         ffMutex.Lock()
453         defer ffMutex.Unlock()
454
455         fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
456         defer fd.Close()
457         if err != nil {
458                 return err
459         }
460
461         data := &xmlFolders{
462                 Version: "1",
463                 Folders: folders,
464         }
465
466         enc := xml.NewEncoder(fd)
467         enc.Indent("", "  ")
468         return enc.Encode(data)
469 }