github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/plugins/webdav/scripts.go (about) 1 // This file is part of the Smart Home 2 // Program complex distribution https://github.com/e154/smart-home 3 // Copyright (C) 2024, Filippov Alex 4 // 5 // This library is free software: you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 3 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Library General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library. If not, see 17 // <https://www.gnu.org/licenses/>. 18 19 package webdav 20 21 import ( 22 "context" 23 "fmt" 24 "os" 25 "path/filepath" 26 "sync" 27 "time" 28 29 "github.com/pkg/errors" 30 "github.com/spf13/afero" 31 "go.uber.org/atomic" 32 33 "github.com/e154/smart-home/adaptors" 34 "github.com/e154/smart-home/common/apperr" 35 "github.com/e154/smart-home/common/events" 36 m "github.com/e154/smart-home/models" 37 "github.com/e154/smart-home/system/bus" 38 "github.com/e154/smart-home/system/scripts" 39 ) 40 41 type Scripts struct { 42 *FS 43 adaptors *adaptors.Adaptors 44 scriptService scripts.ScriptService 45 eventBus bus.Bus 46 rootDir string 47 done chan struct{} 48 isStarted *atomic.Bool 49 isSyncFiles *atomic.Bool 50 sync.Mutex 51 fileInfos map[string]*FileInfo 52 } 53 54 func NewScripts(fs *FS) *Scripts { 55 return &Scripts{ 56 FS: fs, 57 rootDir: "scripts", 58 isStarted: atomic.NewBool(false), 59 isSyncFiles: atomic.NewBool(false), 60 } 61 } 62 63 func (s *Scripts) Start(adaptors *adaptors.Adaptors, scriptService scripts.ScriptService, eventBus bus.Bus) { 64 if !s.isStarted.CompareAndSwap(false, true) { 65 return 66 } 67 68 s.adaptors = adaptors 69 s.eventBus = eventBus 70 s.scriptService = scriptService 71 s.fileInfos = make(map[string]*FileInfo) 72 73 s.preload() 74 75 s.done = make(chan struct{}) 76 go func() { 77 ticker := time.NewTicker(time.Second * 5) 78 defer ticker.Stop() 79 80 for { 81 select { 82 case <-ticker.C: 83 s.syncFiles() 84 case <-s.done: 85 return 86 } 87 } 88 }() 89 90 _ = eventBus.Subscribe("system/models/scripts/+", s.eventHandler) 91 } 92 93 func (s *Scripts) Shutdown() { 94 if !s.isStarted.CompareAndSwap(true, false) { 95 return 96 } 97 98 s.fileInfos = nil 99 close(s.done) 100 _ = s.eventBus.Unsubscribe("system/models/scripts/+", s.eventHandler) 101 } 102 103 // eventHandler ... 104 func (s *Scripts) eventHandler(_ string, message interface{}) { 105 106 switch msg := message.(type) { 107 case events.EventUpdatedScriptModel: 108 go s.eventUpdateScript(msg) 109 case events.EventRemovedScriptModel: 110 go s.eventRemoveScript(msg) 111 case events.EventCreatedScriptModel: 112 go s.eventAddScript(msg) 113 } 114 } 115 116 func (s *Scripts) eventAddScript(msg events.EventCreatedScriptModel) { 117 if msg.Owner == events.OwnerSystem { 118 return 119 } 120 filePath := s.getFilePath(msg.Script) 121 if err := afero.WriteFile(s.Fs, filePath, []byte(msg.Script.Source), 0644); err != nil { 122 log.Error(err.Error()) 123 return 124 } 125 _ = s.Fs.Chtimes(filePath, msg.Script.CreatedAt, msg.Script.CreatedAt) 126 info, err := s.Fs.Stat(filePath) 127 if err != nil { 128 log.Error(err.Error()) 129 return 130 } 131 s.Lock() 132 s.fileInfos[filePath] = &FileInfo{ 133 Size: info.Size(), 134 ModTime: info.ModTime(), 135 LastCheck: time.Now(), 136 } 137 s.Unlock() 138 } 139 140 func (s *Scripts) onRemoveHandler(ctx context.Context, filePath string) (err error) { 141 s.Lock() 142 defer s.Unlock() 143 if err = s.removeScript(ctx, filePath); err != nil { 144 return 145 } 146 _ = s.Fs.RemoveAll(filePath) 147 delete(s.fileInfos, filePath) 148 return 149 } 150 151 func (s *Scripts) eventRemoveScript(msg events.EventRemovedScriptModel) { 152 if msg.Owner == events.OwnerSystem { 153 return 154 } 155 filePath := s.getFilePath(msg.Script) 156 _ = s.Fs.RemoveAll(filePath) 157 s.Lock() 158 delete(s.fileInfos, filePath) 159 s.Unlock() 160 } 161 162 func (s *Scripts) eventUpdateScript(msg events.EventUpdatedScriptModel) { 163 if msg.Owner == events.OwnerSystem { 164 return 165 } 166 filePath := s.getFilePath(msg.Script) 167 _ = afero.WriteFile(s.Fs, filePath, []byte(msg.Script.Source), 0644) 168 _ = s.Fs.Chtimes(filePath, msg.Script.UpdatedAt, msg.Script.UpdatedAt) 169 info, err := s.Fs.Stat(filePath) 170 if err != nil { 171 log.Error(err.Error()) 172 return 173 } 174 s.Lock() 175 s.fileInfos[filePath] = &FileInfo{ 176 Size: info.Size(), 177 ModTime: msg.Script.UpdatedAt, 178 LastCheck: time.Now(), 179 } 180 if msg.OldScript != nil && msg.OldScript.Name != msg.Script.Name { 181 filePath = s.getFilePath(msg.OldScript) 182 _ = s.Fs.RemoveAll(filePath) 183 delete(s.fileInfos, filePath) 184 } 185 s.Unlock() 186 187 } 188 189 func (s *Scripts) preload() { 190 log.Info("Preload script list") 191 192 var recordDir = filepath.Join(rootDir, s.rootDir) 193 194 _ = s.Fs.MkdirAll(recordDir, 0755) 195 196 var page int64 197 var scripts []*m.Script 198 const perPage = 500 199 var err error 200 201 LOOP: 202 203 if scripts, _, err = s.adaptors.Script.List(context.Background(), perPage, perPage*page, "desc", "id", nil, nil); err != nil { 204 log.Error(err.Error()) 205 return 206 } 207 208 for _, script := range scripts { 209 filePath := s.getFilePath(script) 210 if err = afero.WriteFile(s.Fs, filePath, []byte(script.Source), 0644); err != nil { 211 log.Error(err.Error()) 212 } 213 if err = s.Fs.Chtimes(filePath, script.CreatedAt, script.UpdatedAt); err != nil { 214 log.Error(err.Error()) 215 } 216 } 217 218 if len(scripts) != 0 { 219 page++ 220 goto LOOP 221 } 222 223 s.Lock() 224 defer s.Unlock() 225 err = afero.Walk(s.Fs, filepath.Join(rootDir, s.rootDir), func(path string, info os.FileInfo, err error) error { 226 if err != nil { 227 return err 228 } 229 if info.IsDir() { 230 return nil 231 } 232 233 s.fileInfos[path] = &FileInfo{ 234 Size: info.Size(), 235 ModTime: info.ModTime(), 236 LastCheck: time.Now(), 237 } 238 239 return nil 240 }) 241 } 242 243 func (s *Scripts) getFilePath(script *m.Script) string { 244 return filepath.Join(rootDir, s.rootDir, getFileName(script)) 245 } 246 247 func (s *Scripts) removeScript(ctx context.Context, path string) (err error) { 248 scriptName := extractScriptName(path) 249 var script *m.Script 250 script, err = s.adaptors.Script.GetByName(ctx, scriptName) 251 if err == nil { 252 log.Infof("remove script: %s, (id: %d)", script.Name, script.Id) 253 if err = s.adaptors.Script.Delete(ctx, script.Id); err != nil { 254 return 255 } 256 s.eventBus.Publish(fmt.Sprintf("system/models/scripts/%d", script.Id), events.EventRemovedScriptModel{ 257 Common: events.Common{ 258 Owner: events.OwnerSystem, 259 }, 260 ScriptId: script.Id, 261 Script: script, 262 }) 263 } 264 return 265 } 266 267 func (s *Scripts) createScript(ctx context.Context, name string, fileInfo os.FileInfo) (err error) { 268 scriptName := extractScriptName(fileInfo.Name()) 269 lang := getScriptLang(fileInfo.Name()) 270 271 if lang == "" { 272 err = errors.Errorf("bad file name %s", scriptName) 273 return 274 } 275 276 var source []byte 277 source, err = afero.ReadFile(s.Fs, name) 278 if err != nil { 279 return 280 } 281 282 if _, err = s.adaptors.Script.GetByName(ctx, scriptName); err == nil { 283 return 284 } 285 286 log.Infof("create script: %s", scriptName) 287 288 script := &m.Script{ 289 Name: scriptName, 290 Lang: lang, 291 Source: string(source), 292 } 293 engine, err := s.scriptService.NewEngine(script) 294 if err != nil { 295 return 296 } 297 if err = engine.Compile(); err != nil { 298 err = errors.Wrap(apperr.ErrScriptCompile, err.Error()) 299 return 300 } 301 302 if _, err = s.adaptors.Script.Add(ctx, script); err != nil { 303 return err 304 } 305 306 s.eventBus.Publish(fmt.Sprintf("system/models/scripts/%d", script.Id), events.EventCreatedScriptModel{ 307 Common: events.Common{ 308 Owner: events.OwnerSystem, 309 }, 310 ScriptId: script.Id, 311 Script: script, 312 }) 313 314 return 315 } 316 317 func (s *Scripts) updateScript(ctx context.Context, name string, fileInfo os.FileInfo) (err error) { 318 scriptName := extractScriptName(fileInfo.Name()) 319 lang := getScriptLang(fileInfo.Name()) 320 321 if lang == "" { 322 err = errors.New("bad extension") 323 return 324 } 325 326 var source []byte 327 source, err = afero.ReadFile(s.Fs, name) 328 if err != nil { 329 return 330 } 331 332 log.Infof("update script: %s", scriptName) 333 334 var script *m.Script 335 script, err = s.adaptors.Script.GetByName(ctx, scriptName) 336 if err == nil { 337 script.Source = string(source) 338 script.Lang = lang 339 340 var engine *scripts.Engine 341 engine, err = s.scriptService.NewEngine(script) 342 if err != nil { 343 return 344 } 345 if err = engine.Compile(); err != nil { 346 err = errors.Wrap(apperr.ErrScriptCompile, err.Error()) 347 return 348 } 349 if err = s.adaptors.Script.Update(ctx, script); err != nil { 350 return err 351 } 352 s.eventBus.Publish(fmt.Sprintf("system/models/scripts/%d", script.Id), events.EventUpdatedScriptModel{ 353 Common: events.Common{ 354 Owner: events.OwnerSystem, 355 }, 356 ScriptId: script.Id, 357 Script: script, 358 }) 359 return 360 } 361 return 362 } 363 364 func (s *Scripts) syncFiles() { 365 if !s.isSyncFiles.CompareAndSwap(false, true) { 366 return 367 } 368 defer s.isSyncFiles.Store(false) 369 370 s.Lock() 371 defer s.Unlock() 372 373 for _, fileInfo := range s.fileInfos { 374 fileInfo.IsInitialized = false 375 } 376 377 _ = afero.Walk(s.Fs, "/webdav/scripts", func(path string, info os.FileInfo, err error) error { 378 if !s.isStarted.Load() { 379 return errors.New("not started") 380 } 381 if info.IsDir() { 382 return nil 383 } 384 fileInfo, ok := s.fileInfos[path] 385 if ok { 386 fileInfo.IsInitialized = true 387 if info.ModTime().After(fileInfo.ModTime) || fileInfo.Size != info.Size() { 388 log.Infof("File %s has changed.", path) 389 fileInfo.Size = info.Size() 390 fileInfo.ModTime = info.ModTime() 391 fileInfo.LastCheck = time.Now() 392 393 if _err := s.updateScript(context.Background(), path, info); _err != nil { 394 if errors.Is(_err, apperr.ErrScriptNotFound) { 395 fileInfo.IsInitialized = false 396 } 397 log.Error(_err.Error()) 398 } 399 } 400 } else { 401 if _err := s.createScript(context.Background(), path, info); _err != nil { 402 log.Error(_err.Error()) 403 } 404 s.fileInfos[path] = &FileInfo{ 405 Size: info.Size(), 406 ModTime: info.ModTime(), 407 LastCheck: time.Now(), 408 IsInitialized: true, 409 } 410 } 411 return nil 412 }) 413 414 if !s.isStarted.Load() { 415 return 416 } 417 418 for path, fileInfo := range s.fileInfos { 419 if !s.isStarted.Load() { 420 return 421 } 422 if !fileInfo.IsInitialized { 423 if err := s.removeScript(context.Background(), path); err != nil { 424 //log.Error(err.Error()) 425 } 426 delete(s.fileInfos, path) 427 } 428 } 429 }