2 * Copyright (C) 2017 "IoT.bzh"
3 * Author Sebastien Douheret <sebastien@iot.bzh>
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
29 "github.com/franciscocpg/reflectme"
30 common "github.com/iotbzh/xds-common/golib"
31 "github.com/iotbzh/xds-server/lib/xsapiv1"
32 "github.com/iotbzh/xds-server/lib/xdsconfig"
33 "github.com/syncthing/syncthing/lib/sync"
36 // Folders Represent a an XDS folders
40 folders map[string]*IFOLDER
41 registerCB []RegisteredCB
44 // RegisteredCB Hold registered callbacks
45 type RegisteredCB struct {
47 data *FolderEventCBData
50 // Mutex to make add/delete atomic
51 var fcMutex = sync.NewMutex()
52 var ffMutex = sync.NewMutex()
54 // FoldersNew Create a new instance of Model Folders
55 func FoldersNew(ctx *Context) *Folders {
56 file, _ := xdsconfig.FoldersConfigFilenameGet()
60 folders: make(map[string]*IFOLDER),
61 registerCB: []RegisteredCB{},
65 // LoadConfig Load folders configuration from disk
66 func (f *Folders) LoadConfig() error {
67 var flds []xsapiv1.FolderConfig
68 var stFlds []xsapiv1.FolderConfig
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)
77 if strings.HasPrefix(err.Error(), "No folder config") {
78 f.Log.Warnf(err.Error())
84 f.Log.Warnf("Folders config filename not set")
87 // Retrieve initial Syncthing config (just append don't overwrite existing ones)
89 f.Log.Infof("Retrieve syncthing folder config")
90 if err := f.SThg.FolderLoadFromStConfig(&stFlds); err != nil {
91 // Don't exit on such error, just log it
92 f.Log.Errorf(err.Error())
95 f.Log.Infof("Syncthing support is disabled.")
98 // Merge syncthing folders into XDS folders
99 for _, stf := range stFlds {
101 for i, xf := range flds {
105 if xf.Type != xsapiv1.TypeCloudSync {
106 flds[i].Status = xsapiv1.StatusErrorConfig
113 flds = append(flds, stf)
117 // Detect ghost project
118 // (IOW existing in xds file config and not in syncthing database)
120 for i, xf := range flds {
121 // only for syncthing project
122 if xf.Type != xsapiv1.TypeCloudSync {
126 for _, stf := range stFlds {
133 flds[i].Status = xsapiv1.StatusErrorConfig
139 f.Log.Infof("Loading initial folders config: %d folders found", len(flds))
140 for _, fc := range flds {
141 if _, err := f.createUpdate(fc, false, true); err != nil {
146 // Save config on disk
147 err := f.SaveConfig()
152 // SaveConfig Save folders configuration to disk
153 func (f *Folders) SaveConfig() error {
154 if f.fileOnDisk == "" {
155 return fmt.Errorf("Folders config filename not set")
158 // FIXME: buffered save or avoid to write on disk each time
159 return foldersConfigWrite(f.fileOnDisk, f.getConfigArrUnsafe())
162 // ResolveID Complete a Folder ID (helper for user that can use partial ID value)
163 func (f *Folders) ResolveID(id string) (string, error) {
169 for iid := range f.folders {
170 if strings.HasPrefix(iid, id) {
171 match = append(match, iid)
177 } else if len(match) == 0 {
178 return id, fmt.Errorf("Unknown id")
180 return id, fmt.Errorf("Multiple IDs found with provided prefix: " + id)
183 // Get returns the folder config or nil if not existing
184 func (f *Folders) Get(id string) *IFOLDER {
188 fc, exist := f.folders[id]
195 // GetConfigArr returns the config of all folders as an array
196 func (f *Folders) GetConfigArr() []xsapiv1.FolderConfig {
198 defer fcMutex.Unlock()
200 return f.getConfigArrUnsafe()
203 // getConfigArrUnsafe Same as GetConfigArr without mutex protection
204 func (f *Folders) getConfigArrUnsafe() []xsapiv1.FolderConfig {
205 conf := []xsapiv1.FolderConfig{}
206 for _, v := range f.folders {
207 conf = append(conf, (*v).GetConfig())
212 // Add adds a new folder
213 func (f *Folders) Add(newF xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
214 return f.createUpdate(newF, true, false)
217 // CreateUpdate creates or update a folder
218 func (f *Folders) createUpdate(newF xsapiv1.FolderConfig, create bool, initial bool) (*xsapiv1.FolderConfig, error) {
221 defer fcMutex.Unlock()
224 if _, exist := f.folders[newF.ID]; create && exist {
225 return nil, fmt.Errorf("ID already exists")
227 if newF.ClientPath == "" {
228 return nil, fmt.Errorf("ClientPath must be set")
231 // Create a new folder object
235 case xsapiv1.TypeCloudSync:
237 fld = NewFolderST(f.Context, f.SThg)
239 f.Log.Debugf("Disable project %v (syncthing not initialized)", newF.ID)
240 fld = NewFolderSTDisable(f.Context)
244 case xsapiv1.TypePathMap:
245 fld = NewFolderPathMap(f.Context)
247 return nil, fmt.Errorf("Unsupported folder type")
250 // Allocate a new UUID
252 newF.ID = fld.NewUID("")
254 if !create && newF.ID == "" {
255 return nil, fmt.Errorf("Cannot update folder with null ID")
258 // Set default value if needed
259 if newF.Status == "" {
260 newF.Status = xsapiv1.StatusDisable
262 if newF.Label == "" {
263 newF.Label = filepath.Base(newF.ClientPath)
264 if len(newF.ID) > 8 {
265 newF.Label += "_" + newF.ID[0:8]
269 // Normalize path (needed for Windows path including bashlashes)
270 newF.ClientPath = common.PathNormalize(newF.ClientPath)
273 newFolder, err := fld.Add(newF)
275 newF.Status = xsapiv1.StatusErrorConfig
276 log.Printf("ERROR Adding folder: %v\n", err)
277 return newFolder, err
280 // Add to folders list
281 f.folders[newF.ID] = &fld
283 // Save config on disk
285 if err := f.SaveConfig(); err != nil {
286 return newFolder, err
290 // Register event change callback
291 for _, rcb := range f.registerCB {
292 if err := fld.RegisterEventChange(rcb.cb, rcb.data); err != nil {
293 return newFolder, err
297 // Force sync after creation
298 // (need to defer to be sure that WS events will arrive after HTTP creation reply)
300 time.Sleep(time.Millisecond * 500)
304 return newFolder, nil
307 // Delete deletes a specific folder
308 func (f *Folders) Delete(id string) (xsapiv1.FolderConfig, error) {
312 defer fcMutex.Unlock()
314 fld := xsapiv1.FolderConfig{}
315 fc, exist := f.folders[id]
317 return fld, fmt.Errorf("unknown id")
320 fld = (*fc).GetConfig()
322 if err = (*fc).Remove(); err != nil {
326 delete(f.folders, id)
328 // Save config on disk
334 // Update Update a specific folder
335 func (f *Folders) Update(id string, cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
337 defer fcMutex.Unlock()
339 fc, exist := f.folders[id]
341 return nil, fmt.Errorf("unknown id")
344 // Copy current in a new object to change nothing in case of an error rises
345 newCfg := xsapiv1.FolderConfig{}
346 reflectme.Copy((*fc).GetConfig(), &newCfg)
348 // Only update some fields
350 for _, fieldName := range xsapiv1.FolderConfigUpdatableFields {
351 valNew, err := reflectme.GetField(cfg, fieldName)
353 valCur, err := reflectme.GetField(newCfg, fieldName)
354 if err == nil && valNew != valCur {
355 err = reflectme.SetField(&newCfg, fieldName, valNew)
368 fld, err := (*fc).Update(newCfg)
373 // Save config on disk
376 // Send event to notified changes
377 // TODO emit folder change event
382 // RegisterEventChange requests registration for folder event change
383 func (f *Folders) RegisterEventChange(id string, cb *FolderEventCB, data *FolderEventCBData) error {
385 flds := make(map[string]*IFOLDER)
387 // Register to a specific folder
390 // Register to all folders
392 f.registerCB = append(f.registerCB, RegisteredCB{cb: cb, data: data})
395 for _, fld := range flds {
396 err := (*fld).RegisterEventChange(cb, data)
405 // ForceSync Force the synchronization of a folder
406 func (f *Folders) ForceSync(id string) error {
409 return fmt.Errorf("Unknown id")
414 // IsFolderInSync Returns true when folder is in sync
415 func (f *Folders) IsFolderInSync(id string) (bool, error) {
418 return false, fmt.Errorf("Unknown id")
420 return (*fc).IsInSync()
423 //*** Private functions ***
425 // Use XML format and not json to be able to save/load all fields including
426 // ones that are masked in json (IOW defined with `json:"-"`)
427 type xmlFolders struct {
428 XMLName xml.Name `xml:"folders"`
429 Version string `xml:"version,attr"`
430 Folders []xsapiv1.FolderConfig `xml:"folders"`
433 // foldersConfigRead reads folders config from disk
434 func foldersConfigRead(file string, folders *[]xsapiv1.FolderConfig) error {
435 if !common.Exists(file) {
436 return fmt.Errorf("No folder config file found (%s)", file)
440 defer ffMutex.Unlock()
442 fd, err := os.Open(file)
449 err = xml.NewDecoder(fd).Decode(&data)
451 *folders = data.Folders
456 // foldersConfigWrite writes folders config on disk
457 func foldersConfigWrite(file string, folders []xsapiv1.FolderConfig) error {
459 defer ffMutex.Unlock()
461 fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
472 enc := xml.NewEncoder(fd)
474 return enc.Encode(data)