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  }