starting to write the manager library
This commit is contained in:
@@ -86,9 +86,9 @@ func decompressDelta(compressedData []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
func applyPatchToFile(originalbytes, delta []byte) ([]byte, error) {
|
||||
if patchedBytes, err := fdelta.Apply(originalbytes, delta); err != nil {
|
||||
patchedBytes, err := fdelta.Apply(originalbytes, delta)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
} else {
|
||||
return patchedBytes, nil
|
||||
}
|
||||
return patchedBytes, nil
|
||||
}
|
||||
|
140
common/engine/filesystem.go
Normal file
140
common/engine/filesystem.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
logger "github.com/apsdehal/go-logger"
|
||||
)
|
||||
|
||||
var log *logger.Logger
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
log, err = logger.New("utilities logger", 1, os.Stdout)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.SetFormat("[%{module}] [%{level}] %{message}")
|
||||
log.Info("Utilities logger Created")
|
||||
}
|
||||
|
||||
// CompressIntArray compresses an array of integers into a buffer
|
||||
func CompressIntArray(arry []int64, compressionBuffer *bytes.Buffer) (float64, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err := binary.Write(buf, binary.LittleEndian, arry)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
//now compress it
|
||||
compressor := gzip.NewWriter(compressionBuffer)
|
||||
// if err != nil {
|
||||
// fmt.Println("writer level failed to set compression level")
|
||||
// }
|
||||
if _, err := compressor.Write(buf.Bytes()); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := compressor.Close(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ratio := float64(len(compressionBuffer.Bytes())) / float64(len(buf.Bytes()))
|
||||
return ratio, nil
|
||||
}
|
||||
|
||||
// ExpandToIntArray firstly unzips the byte array, then it
|
||||
// converts the byte array back into an int array for use
|
||||
func ExpandToIntArray(length int64, arry []byte, intArray *[]int64) error {
|
||||
buf := bytes.NewBuffer(arry)
|
||||
if reader, err := gzip.NewReader(buf); err != nil {
|
||||
fmt.Println("gzip failed ", err)
|
||||
return err
|
||||
} else {
|
||||
*intArray = make([]int64, length) //you must know the length of the original data if you are to do it this way.
|
||||
err := binary.Read(reader, binary.LittleEndian, intArray)
|
||||
if err != nil {
|
||||
fmt.Println("read failed ", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// VerifySrcFile checks to see that the file is a regular file
|
||||
// that the OS has meta information about and that can be read by
|
||||
// the os.
|
||||
func VerifySrcFile(src string) (string, error) {
|
||||
_, fileName := filepath.Split(src) //dirPath
|
||||
sourceFileStat, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return fileName, errors.New("error on os.Stat " + err.Error())
|
||||
}
|
||||
|
||||
if !sourceFileStat.Mode().IsRegular() {
|
||||
return fileName, errors.New("%s is not a regular file" + src)
|
||||
}
|
||||
return fileName, nil
|
||||
}
|
||||
|
||||
func InitiateDirectory(directory string) {
|
||||
// For the keys-folder we need to check if the folder exists...
|
||||
checkDir, err := IsDirectory(directory)
|
||||
if err != nil {
|
||||
log.ErrorF("Error checking for "+directory+" directory: %s\r\n", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if checkDir == true {
|
||||
log.Warning(directory + " already exists")
|
||||
} else {
|
||||
// Create the directory.
|
||||
log.Info("Creating " + directory)
|
||||
err = CreateDirectory(directory)
|
||||
if err != nil {
|
||||
log.ErrorF("Error creating the folder %s\r\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func IsDirectory(path string) (bool, error) {
|
||||
|
||||
s, err := os.Stat(path) // returns an error if the path does not exist.
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err // Different error...?
|
||||
}
|
||||
|
||||
if s.IsDir() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil // Redundancy
|
||||
|
||||
}
|
||||
|
||||
func CreateDirectory(path string) error {
|
||||
// Assumes checks have been done on if the directory exists...
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil // Redundancy
|
||||
|
||||
}
|
||||
|
||||
func DeleteDirectory(path string) error {
|
||||
err := os.RemoveAll(path)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
//TODO: Most likely can be done with the filepath command so replace this everywhere
|
||||
func StripFilePathBase(pathToFile, base string) string {
|
||||
return strings.Replace(pathToFile, base, "", -1)
|
||||
}
|
@@ -3,7 +3,7 @@ package engine
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
logger "github.com/apsdehal/go-logger"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// The watcher is responsible for not only seeing when a file changes,
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// * copying any versions and keeping them safe (even if temporary)
|
||||
// * creating the diff of the file, in both directions if necessary
|
||||
// * storing the details in the database
|
||||
func NewPatcher(logger *logger.Logger, KEYFOLDER, DOWNLOADFOLDER, SYNCFOLDER, THUMBFOLDER, DIFFFOLDER string) (Patcher, error) {
|
||||
func NewPatcher(logger *zerolog.Logger, KEYFOLDER, DOWNLOADFOLDER, SYNCFOLDER, THUMBFOLDER, DIFFFOLDER string) (Patcher, error) {
|
||||
p := Patcher{
|
||||
logger,
|
||||
KEYFOLDER, DOWNLOADFOLDER, SYNCFOLDER, THUMBFOLDER, DIFFFOLDER,
|
||||
@@ -25,28 +25,26 @@ func NewPatcher(logger *logger.Logger, KEYFOLDER, DOWNLOADFOLDER, SYNCFOLDER, TH
|
||||
// last save is the file you want to get.
|
||||
func (p *Patcher) PatchFromFile(filePath, patchPath, restorePath string) error {
|
||||
if subject, err := openFile(filePath); err != nil {
|
||||
return fmt.Errorf("error on subject file: ", err)
|
||||
return fmt.Errorf("error on subject file: %s", err)
|
||||
} else if patch, err := openFile(patchPath); err != nil {
|
||||
return fmt.Errorf("error on patch file: ", err)
|
||||
return fmt.Errorf("error on patch file: %s", err)
|
||||
} else {
|
||||
return p.applyPatch(subject, patch, restorePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//applyPatch actively applies the patch to the subject. This could eventually
|
||||
// be upgraded for different patching algorithms
|
||||
func (p *Patcher) applyPatch(subject, patch []byte, restorePath string) error {
|
||||
if delta, err := decompressDelta(patch); err != nil {
|
||||
return fmt.Errorf("error decompressing delta", err)
|
||||
return fmt.Errorf("error decompressing delta %s", err)
|
||||
} else {
|
||||
if appliedBytes, err := applyPatchToFile(subject, delta); err != nil {
|
||||
return fmt.Errorf("error applying delta to original file", err)
|
||||
return fmt.Errorf("error applying delta to original file %s", err)
|
||||
} else if err := writeFile(restorePath, appliedBytes); err != nil {
|
||||
return fmt.Errorf("error writing patchedFile", err)
|
||||
} else {
|
||||
return nil
|
||||
return fmt.Errorf("error writing patchedFile %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
@@ -1,13 +1,13 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
logger "github.com/apsdehal/go-logger"
|
||||
watcher "github.com/radovskyb/watcher"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type FileWatcher struct {
|
||||
*watcher.Watcher
|
||||
*logger.Logger
|
||||
*zerolog.Logger
|
||||
Enabled bool
|
||||
KEYFOLDER string
|
||||
DOWNLOADFOLDER string
|
||||
@@ -16,7 +16,7 @@ type FileWatcher struct {
|
||||
DIFFFOLDER string
|
||||
}
|
||||
type Patcher struct {
|
||||
*logger.Logger
|
||||
*zerolog.Logger
|
||||
KEYFOLDER string
|
||||
DOWNLOADFOLDER string
|
||||
SYNCFOLDER string
|
||||
|
@@ -4,9 +4,9 @@ import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
logger "github.com/apsdehal/go-logger"
|
||||
"github.com/deranjer/gvc/common/database"
|
||||
watcher "github.com/radovskyb/watcher"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type key string
|
||||
@@ -22,7 +22,7 @@ type Event struct {
|
||||
// * copying any versions and keeping them safe (even if temporary)
|
||||
// * creating the diff of the file, in both directions if necessary
|
||||
// * storing the details in the database
|
||||
func NewWatcher(logger *logger.Logger, KEYFOLDER, DOWNLOADFOLDER, SYNCFOLDER, THUMBFOLDER, DIFFFOLDER string) (FileWatcher, error) {
|
||||
func NewWatcher(logger *zerolog.Logger, KEYFOLDER, DOWNLOADFOLDER, SYNCFOLDER, THUMBFOLDER, DIFFFOLDER string) (FileWatcher, error) {
|
||||
w := FileWatcher{
|
||||
watcher.New(),
|
||||
logger,
|
||||
@@ -59,11 +59,11 @@ func (fw *FileWatcher) BeginWatcherRoutine(ctx context.Context, wg *sync.WaitGro
|
||||
// we have filtered already on the [Op]erations we want to listen for so no need to check here
|
||||
case event := <-fw.Watcher.Event:
|
||||
if !fw.IsEnabled() {
|
||||
fw.Infof("ignoring event and reenabling the watcher %s\r\n", event)
|
||||
fw.Info().Msgf("ignoring event and reenabling the watcher %s\r\n", event)
|
||||
fw.Enable()
|
||||
continue
|
||||
}
|
||||
fw.Infof("event fired ", event)
|
||||
fw.Info().Msgf("event fired ", event)
|
||||
//this is currently slow as it does a db lookup on the path.
|
||||
//TODO: On load (or whenever a file is added to the watcher, the db information for files being watched, could be cached in memory. This would be much faster)
|
||||
fileInfo, err := onFileChanged(event.Path) //could return the 'Event' object here
|
||||
@@ -74,7 +74,7 @@ func (fw *FileWatcher) BeginWatcherRoutine(ctx context.Context, wg *sync.WaitGro
|
||||
//we need the hash of the current base, not the hash of the original file
|
||||
// fileHash := fileInfo.CurrentHash //hash needs to come from
|
||||
if err != nil {
|
||||
fw.ErrorF("path was not returned to sync path", err)
|
||||
fw.Err(err).Msg("path was not returned to sync path")
|
||||
continue
|
||||
}
|
||||
//cancel the event if it indeed is running...
|
||||
@@ -105,12 +105,12 @@ func (fw *FileWatcher) BeginWatcherRoutine(ctx context.Context, wg *sync.WaitGro
|
||||
eventContext := context.WithValue(cancelContext, key(event.Path), e)
|
||||
if err := manageFileDiffing(eventContext, event.Path, syncFilePath, fw.DIFFFOLDER, true, diffChannel, wg); err != nil {
|
||||
// I don't think this can be reached...
|
||||
fw.WarningF("Error managing the diffing process %s", err)
|
||||
fw.Warn().Msgf("Error managing the diffing process %s", err)
|
||||
}
|
||||
case err := <-fw.Watcher.Error:
|
||||
fw.Errorf("%s\r\n", err)
|
||||
fw.Err(err)
|
||||
case <-fw.Watcher.Closed:
|
||||
fw.Notice("radovskyb closed")
|
||||
//fw.Notice("radovskyb closed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
47
common/manager/initialize.go
Normal file
47
common/manager/initialize.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/atrox/homedir"
|
||||
engine "github.com/deranjer/gvc/common/engine"
|
||||
)
|
||||
|
||||
const (
|
||||
storageDirectory = "pickleit"
|
||||
)
|
||||
|
||||
var (
|
||||
PATH, KEYFOLDER, DOWNLOADFOLDER, SYNCFOLDER, DIFFFOLDER, THUMBFOLDER, LOGFOLDER, PLUGINFOLDER string
|
||||
)
|
||||
|
||||
func init() {
|
||||
homeDirectory, err := homedir.Dir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
PATH = filepath.Join(homeDirectory, storageDirectory)
|
||||
|
||||
//where private and public keys are kept
|
||||
KEYFOLDER = filepath.Join(PATH, "keys")
|
||||
//where downloaded files start
|
||||
DOWNLOADFOLDER = filepath.Join(PATH, "downloads")
|
||||
//where file originals live
|
||||
SYNCFOLDER = filepath.Join(PATH, "sync")
|
||||
//where patches and last versions live
|
||||
DIFFFOLDER = filepath.Join(PATH, "diff")
|
||||
//where the thumbnails are stored
|
||||
THUMBFOLDER = filepath.Join(PATH, "thumb")
|
||||
//where the logs are stored
|
||||
LOGFOLDER = filepath.Join(PATH, "logs")
|
||||
//where plugins are stored
|
||||
PLUGINFOLDER = filepath.Join(PATH, "plugins")
|
||||
|
||||
engine.InitiateDirectory(KEYFOLDER)
|
||||
engine.InitiateDirectory(DOWNLOADFOLDER)
|
||||
engine.InitiateDirectory(SYNCFOLDER)
|
||||
engine.InitiateDirectory(DIFFFOLDER)
|
||||
engine.InitiateDirectory(THUMBFOLDER)
|
||||
engine.InitiateDirectory(LOGFOLDER)
|
||||
engine.InitiateDirectory(PLUGINFOLDER)
|
||||
}
|
26
common/manager/manager.go
Normal file
26
common/manager/manager.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
engine "github.com/deranjer/gvc/common/engine"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// NewManager creates a new manager interface that contains all the needed information to make changes to the repo
|
||||
func NewManager(version string, informer chan OperatingMessage, log zerolog.Logger) (*Manager, error) {
|
||||
log.Info().Msg("Creating new Manager...")
|
||||
patcher, err := engine.NewPatcher(&log, KEYFOLDER, DOWNLOADFOLDER, SYNCFOLDER, THUMBFOLDER, DIFFFOLDER)
|
||||
if err != nil {
|
||||
log.Fatal().Msgf("Error creating a patcher %s", err)
|
||||
return &Manager{}, err
|
||||
}
|
||||
|
||||
m := Manager{
|
||||
version,
|
||||
//settings,
|
||||
&log,
|
||||
patcher,
|
||||
database,
|
||||
informer,
|
||||
}
|
||||
return &m, nil
|
||||
}
|
351
common/manager/manager.go.old
Normal file
351
common/manager/manager.go.old
Normal file
@@ -0,0 +1,351 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
logger "github.com/apsdehal/go-logger"
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
db "github.com/deranjer/gvc/common/database"
|
||||
engine "github.com/deranjer/gvc/common/engine"
|
||||
radovskyb "github.com/radovskyb/watcher"
|
||||
)
|
||||
|
||||
// Manager keeps track of all elements that are passed around the application.
|
||||
// It is responsible for the interactions that other parts of the application
|
||||
// may need with engine.
|
||||
//
|
||||
// Logging from inside here can be dangerous as the io.Writer may not be truly configured yet
|
||||
// so be careful of this as you can get null pointer exceptions
|
||||
func NewManager(version engine.Version, logLevel int, format string, informer chan OperatingMessage, logWriter io.WriteCloser) (*Manager, error) {
|
||||
//note re colors: https://stackoverflow.com/questions/1961209/making-some-text-in-printf-appear-in-green-and-red#1961222
|
||||
log, err := logger.New("client logger", 1, logWriter)
|
||||
if err != nil {
|
||||
panic(err) // Check for error, no easy way to recover without a logger.
|
||||
}
|
||||
log.SetFormat(format)
|
||||
log.SetLogLevel(logger.LogLevel(logLevel))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
watcher, err := engine.NewWatcher(log, KEYFOLDER, DOWNLOADFOLDER, SYNCFOLDER, THUMBFOLDER, DIFFFOLDER)
|
||||
if err != nil {
|
||||
log.CriticalF("Error creating a watcher %s", err)
|
||||
return &Manager{}, err
|
||||
}
|
||||
patcher, err := engine.NewPatcher(log, KEYFOLDER, DOWNLOADFOLDER, SYNCFOLDER, THUMBFOLDER, DIFFFOLDER)
|
||||
if err != nil {
|
||||
log.CriticalF("Error creating a patcher %s", err)
|
||||
return &Manager{}, err
|
||||
}
|
||||
fmt.Printf("manager version %+v\r\n", version)
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
log.CriticalF("Error retrieving the current user %s", err)
|
||||
return &Manager{}, err
|
||||
}
|
||||
database, err := db.NewDB(filepath.Join(PATH, version.DBName), format, logLevel, logWriter)
|
||||
if err != nil {
|
||||
log.CriticalF("There was an error initialising the data base [%s]\r\n", err)
|
||||
return &Manager{}, err
|
||||
}
|
||||
versionFormat := "%{bigVersion}.%{littleVersion}.%{microVersion}_%{timeUnix}_%{client}_%{job}_%{creator}_%{owner}_%{hash}_%{message}"
|
||||
machineID, err := machineid.ID()
|
||||
if err != nil {
|
||||
log.WarningF("there was an error identifying the machine %s\r\n", err)
|
||||
} else {
|
||||
log.Infof("Machine identified as %s\r\n", machineID)
|
||||
}
|
||||
settings := &UserSettings{Usr: *usr, versionFormat: versionFormat, darkMode: false, machineID: machineID}
|
||||
|
||||
m := Manager{
|
||||
version,
|
||||
settings,
|
||||
log,
|
||||
&wg,
|
||||
watcher,
|
||||
patcher,
|
||||
database,
|
||||
informer,
|
||||
logWriter,
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// TearDown should be called by the main loop to close everything that has been left open
|
||||
// TODO: check if the watcher/patcher/waitgroup or logger need tearing down.
|
||||
// Defer its running in main so that it happens last
|
||||
func (m *Manager) TearDown() {
|
||||
defer m.watcher.Close()
|
||||
defer m.dB.Close()
|
||||
m.Informer <- Op_WatchStopped.Retrieve()
|
||||
}
|
||||
|
||||
// StopWatching closes the current channels on the watcher, kills the file watcher and refreshes the instance
|
||||
// with a new watcher before responding that the watcher has been closed.
|
||||
func (m *Manager) StopWatching() {
|
||||
m.watcher.Close()
|
||||
m.watcher.Watcher = nil //confirm the old watcher is not referenced anymore
|
||||
m.watcher.Watcher = radovskyb.New() //due to closing the channels we need to create a new instance of the file wather
|
||||
m.Informer <- Op_WatchStopped.Retrieve()
|
||||
}
|
||||
|
||||
// DemoLogging demos the capabilities of the logger under the current settings
|
||||
// Nothing special here :)
|
||||
func (m *Manager) DemoLogging() {
|
||||
m.Critical("1. Critical level")
|
||||
m.Error("2. Error level")
|
||||
m.Warning("3. Warning level")
|
||||
m.Notice("4. Notice level")
|
||||
m.Info("5. Info level")
|
||||
m.Debug("6. debugging level")
|
||||
VersionHelp()
|
||||
}
|
||||
|
||||
// This adds a file for the watcher to keep an eye on
|
||||
// however the file will also need to be backedup
|
||||
// and added to the database.
|
||||
// This changes all paths to absolute paths rather than relative
|
||||
// when adding a file to monitor, this should check if the database
|
||||
// is already expecting to monitor this file. If it is this function should
|
||||
// do checks to make sure that it is successfully monitoring it, and that there
|
||||
// is a historical breadcrumb trail to recreate all the versions that the database
|
||||
// claims to have a copy of
|
||||
func (m *Manager) AddFileToMonitor(file string, hardCopy bool) (string, error) {
|
||||
var err error
|
||||
// the filepath should be absolute, but this should be done dynamically
|
||||
if file, err = filepath.Abs(file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
//TODO: what needs to happen is a channel for errors/progress is created
|
||||
//then pass that channel to a routine, and put all of the following in it
|
||||
// whenever an error returns, fire the string to the channel,
|
||||
// or send progress on the progress channel
|
||||
//however need to work out best way of returning the final result to the caller
|
||||
//- the way to do that is send the result on a third channel, for which is just the result
|
||||
//see commsManagment.go
|
||||
// f := NewFileManager()
|
||||
//DELAYED: this feature affects only large files and user experience. It can wait.
|
||||
|
||||
var tmpFile engine.File
|
||||
var filename string //we might aswell only verify the files validity once
|
||||
var hash [16]byte
|
||||
//check that the file actually exists
|
||||
if filename, err = engine.VerifySrcFile(file); err != nil {
|
||||
//there was no source file or it was not recognisable as a file
|
||||
return "", err
|
||||
}
|
||||
//generate a unique file name from the hash and the moment it was created
|
||||
//a sampled (and therefore) fast, hash of the file for 'uniqueness'
|
||||
if hash, err = engine.UniqueFileHash(file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if tmpFile, err = m.dB.CheckIfFileCurrentlyMonitored(file, hash); err != nil {
|
||||
if strings.Index(err.Error(), "not found") != -1 {
|
||||
//the file wasn't found, this is an ok error
|
||||
m.InfoF("The file was [%s], so continuing to create it in the database", err)
|
||||
} else {
|
||||
m.ErrorF("Error checking if file [%s] is monitored so will init file. Error: %s", tmpFile.Path, err)
|
||||
}
|
||||
tmpFile.CurrentHash = hash
|
||||
tmpFile.Name = filename
|
||||
tmpFile.Path = file
|
||||
tmpFile.CreatedAt = time.Now()
|
||||
tmpFile.Unique = base64.URLEncoding.EncodeToString([]byte(filename)) + "_" + base64.URLEncoding.EncodeToString((tmpFile.CurrentHash[:])) + "_" + strconv.FormatInt(tmpFile.CreatedAt.Unix(), 10) + "_" + filename
|
||||
tmpFile.BkpLocation = filepath.Join(SYNCFOLDER, tmpFile.Unique)
|
||||
tmpFile.CurrentBase = tmpFile.BkpLocation
|
||||
tmpFile.Ignore = false //we can have files in the database that are ignored. TODO: This was initially added so that 'All Files' would show up as a file (its a hack as it adds a dummy to the database)
|
||||
//we should now have a unique name for this file
|
||||
//if needs be, we can find out the real file name from the string
|
||||
//the hash will give us a reasononable indication of the similarity of the files
|
||||
//define filename of backup(s)
|
||||
if _, err := m.prepareDatabaseForFile(tmpFile); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
if err := m.copyFileToNewLocation(tmpFile.Path, tmpFile.BkpLocation, hardCopy); err != nil {
|
||||
m.ErrorF("There was an error copying the file to the backup location %s", err)
|
||||
return "", err
|
||||
}
|
||||
m.Informer <- Op_NewFile.Retrieve()
|
||||
}
|
||||
} else {
|
||||
m.DebugF("file [%s] is already in the database. Assuming sync file in place", tmpFile.Path)
|
||||
// we should check if the backup file exists, otherwise there is an issue
|
||||
if _, err := engine.VerifySrcFile(tmpFile.BkpLocation); err != nil {
|
||||
//if the backup doesn't exist, something has gone quite wrong....
|
||||
m.DebugF("The backup file doesn't seem to exist at the expected location, %s", err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return tmpFile.Path, m.watcher.Add(tmpFile.Path)
|
||||
}
|
||||
|
||||
// BeginWatching Ultimately this begins the routine keeping track of file changes
|
||||
// however eventually this will be responsible for the continued
|
||||
// running of the manager and will do more than just manage the watcher routine
|
||||
// TODO: REFACTOR THIS WHOLE THING. Passing functions around is weird behaviour.
|
||||
func (m *Manager) BeginWatching() error {
|
||||
m.Informer <- Op_WatchCommencing.Retrieve()
|
||||
//here we can create a channel that the diffs are passed on to
|
||||
//when a diff is pushed onto the channel, we can look for it and add it to the database
|
||||
diffChannel := make(chan engine.DiffObject)
|
||||
//this needs a redesign
|
||||
//parent context to all patching routines
|
||||
ctx := context.Background()
|
||||
m.WaitGroup.Add(3) // add wait groups; 1 for this. one for the watcherRoutine
|
||||
|
||||
//filter the watcher to Write and Create
|
||||
m.watcher.FilterOps(radovskyb.Write, radovskyb.Create)
|
||||
//stick this on a watcher otherwise the next routine will never be reached
|
||||
go m.watcher.BeginWatcherRoutine(ctx, m.WaitGroup, diffChannel, m.OnFileChange)
|
||||
|
||||
// TODO: This is causing it to go CPU crazy
|
||||
// it is perhaps too many routines and continuous loops and no way out...
|
||||
// manages responses from the diffing routine
|
||||
// TODO: could this be done by just waiting on the channel in a for loop?
|
||||
// perhaps that would help with memory intensivity?
|
||||
// Besides, still better to refactor the switch/case into one place if we can.
|
||||
// Although if we can just do in for loops and block the channel until the right time...?
|
||||
// https://stackoverflow.com/questions/55367231/golang-for-select-loop-consumes-100-of-cpu
|
||||
go func(diffChannel chan engine.DiffObject) {
|
||||
defer m.WaitGroup.Done()
|
||||
for {
|
||||
select {
|
||||
case diff := <-diffChannel:
|
||||
if diff.E != nil {
|
||||
m.WarningF("Creating the diff caused an error %s", diff.E)
|
||||
} else {
|
||||
// from here we know the errors etc, but mainly we just want to write the diff object to the database
|
||||
if err := m.dB.StoreDiff(diff); err != nil {
|
||||
m.ErrorF("Error storing the diff to the database %s", err)
|
||||
} else {
|
||||
m.NoticeF("Diff stored %s : %s", diff.StartTime, diff.Screenshot)
|
||||
m.Informer <- Op_NewDiff.Retrieve()
|
||||
}
|
||||
// at this point we can consider making a new commit (base file).
|
||||
// what this should do is check the size of the diff that just came back.
|
||||
// if its great than x% of the size of the original file, then lets create a new base
|
||||
// file.
|
||||
// 1. get size of diff
|
||||
diffSize := diff.DiffSize
|
||||
// 2. get size of base file
|
||||
fi, err := os.Stat(diff.Subject)
|
||||
if err != nil {
|
||||
m.ErrorF("error getting size of file %s", err)
|
||||
continue
|
||||
}
|
||||
m.NoticeF("time to create diff %s", diff.Message)
|
||||
// get the size
|
||||
size := fi.Size()
|
||||
deltaComparison := float64(diffSize) / float64(size)
|
||||
m.InfoF("size comparison size: %0.2f, diffSize: %0.2f, result: %0.2f", float64(size), float64(diffSize), deltaComparison)
|
||||
// 3. compare sizes
|
||||
if deltaComparison > float64(0.1) { //TODO: store the base file limit in an env var
|
||||
m.NoticeF("creating a new commit file as percent size is %.2f", deltaComparison)
|
||||
//the diff is greater than 60% of the base file, so lets create a new base file
|
||||
// 4. decide to make a new base file.
|
||||
// -------------
|
||||
// this involves patching the basefile with this patch, and creating a new base file
|
||||
// that other diffs will be based on from this point forward.
|
||||
|
||||
_, fileName := filepath.Split(diff.Object)
|
||||
unique := base64.URLEncoding.EncodeToString([]byte(fileName)) + "_" + base64.URLEncoding.EncodeToString((diff.ObjectHash[:])) + "_" + strconv.FormatInt(time.Now().Unix(), 10) + "_" + fileName
|
||||
restoreFile := filepath.Join(SYNCFOLDER, unique)
|
||||
//generate a unique file name from the hash and the moment it was created
|
||||
//a sampled (and therefore) fast, hash of the file for 'uniqueness'
|
||||
if err := m.BeginForwardPatch(diff.Object, diff.DiffPath, restoreFile); err != nil {
|
||||
m.ErrorF("There was an error patching the new base file, error: %s", err)
|
||||
} else if hash, err := engine.UniqueFileHash(restoreFile); err != nil {
|
||||
m.ErrorF("There was an error gettig the hash for the file, error: %s", err)
|
||||
} else {
|
||||
// 2. find out how the base file is selected - do you change the file in the database
|
||||
// so that it links to the new base file. This means that when a patch is applied it will also
|
||||
// need to update the location of the base file it refers to.
|
||||
if err := m.dB.UpdateFileData(diff.Object, restoreFile, hash); err != nil {
|
||||
m.ErrorF("There was an error resetting the file base %s", err)
|
||||
}
|
||||
m.Informer <- Op_NewBase.Retrieve()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}(diffChannel)
|
||||
|
||||
// Start the watching process - it'll check for changes every 100ms.
|
||||
go func() {
|
||||
if err := m.watcher.Start(time.Millisecond * 100); err != nil {
|
||||
m.ErrorF("error starting watcher %s\r\n", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
//TODO: refactor into a channel/routine
|
||||
// OnFileChange is a callback function that will be passed to the watcher so that the watcher knows file to act on. This digs out the details of the file and returns them to the watcher.
|
||||
// It returns the currently set base file as the subject that the diff will base itself off of
|
||||
func (m *Manager) OnFileChange(fileChanged string) (engine.File, error) {
|
||||
m.InfoF("on file changed %s", fileChanged)
|
||||
file, err := m.dB.FindFileByPath(fileChanged)
|
||||
return file, err
|
||||
}
|
||||
|
||||
// OnProgressChanged manages progress of the diff, TODO: Implement. Requires binarydist, so is this an outdated function?
|
||||
// func (m *Manager) OnProgressChanged(increment func(int) error, event *binarydist.Event) {
|
||||
// // fmt.Fprintln(w, "callback for ", event.Name, " progress ", event.Progress)
|
||||
// // increment(event.Progress)
|
||||
// m.Notice("on progress changed called with ...")
|
||||
// }
|
||||
|
||||
// prepareDatabaseForFile is responsible for keeping all references to the version of the file,
|
||||
// the diff and the metadata of the diffs. Before any file is copied and stored, it should be managed by the database
|
||||
//
|
||||
// TODO: This will need to initialise a diff object in the database, currently created by the diff package,
|
||||
// however going forward a diff maybe defined by the manager.
|
||||
func (m *Manager) prepareDatabaseForFile(tmpFile engine.File) (int, error) {
|
||||
if fileID, err := m.dB.InitialiseFileInDatabase(tmpFile); err != nil {
|
||||
m.ErrorF("Error checking if file [%s] is monitored. Error %s", tmpFile.Path, err)
|
||||
return 0, err
|
||||
} else {
|
||||
return fileID, nil
|
||||
}
|
||||
}
|
||||
|
||||
// copyFileToNewLocation will check the size of the file that needs to be copied
|
||||
// and decide whether it can manage it in memory. However it can be overridden if needs be
|
||||
// based on whether it is required to copy it to a new location. In such a case
|
||||
// the destination must be defined.
|
||||
//
|
||||
// TODO: Going forward this may use a totally custom naming convention to stop files
|
||||
// appearing in search (with the actual filename in the filename (if you get me), it will appear in search)
|
||||
//
|
||||
// TODO: Going forward the file name of the backup should be a reference to the hash'd data
|
||||
// incase two files being monitored have the same name.
|
||||
// This will only be implemented when we have a database managing these details
|
||||
func (m *Manager) copyFileToNewLocation(file, newLocation string, fsCopy bool) error {
|
||||
if fsCopy {
|
||||
//keep an original copy of the file available at all times
|
||||
if bytes, err := m.watcher.ForceFSCopy(file, newLocation); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.NoticeF("bytes %d ", bytes)
|
||||
}
|
||||
} else {
|
||||
if bytes, err := m.watcher.CleverCopy(file, newLocation); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.NoticeF("bytes %d ", bytes)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
120
common/manager/structures.go
Normal file
120
common/manager/structures.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package manager
|
||||
|
||||
//https://github.com/apsdehal/go-logger
|
||||
import (
|
||||
"os/user"
|
||||
"time"
|
||||
|
||||
database "github.com/deranjer/gvc/common/database"
|
||||
engine "github.com/deranjer/gvc/common/engine"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
Version string //What version of the client or server are we using
|
||||
//Settings *UserSettings
|
||||
*zerolog.Logger
|
||||
//*sync.WaitGroup
|
||||
//watcher engine.FileWatcher
|
||||
patcher engine.Patcher
|
||||
dB *database.DB
|
||||
Informer chan OperatingMessage
|
||||
//ProgressCommunicator io.WriteCloser
|
||||
}
|
||||
|
||||
type CustomPlugin interface {
|
||||
Init()
|
||||
Name() string
|
||||
Description() string
|
||||
}
|
||||
|
||||
// type PluginManager struct {
|
||||
// engine *qml.QQmlApplicationEngine
|
||||
// informer chan OperatingMessage
|
||||
// path string
|
||||
// plugins []string
|
||||
// }
|
||||
|
||||
type UserSettings struct {
|
||||
Usr user.User
|
||||
versionFormat string
|
||||
darkMode bool
|
||||
licenseKey string
|
||||
override bool
|
||||
machineID string
|
||||
//systemSettings engine.UXSettings
|
||||
}
|
||||
type VersioningFormat struct {
|
||||
bigVersion int64
|
||||
littleVersion int64
|
||||
microVersion int64
|
||||
currentTime time.Time
|
||||
client string
|
||||
job string
|
||||
userId string
|
||||
owner string
|
||||
hash string
|
||||
message string
|
||||
}
|
||||
|
||||
// this should enumerate certain message types that the front end can retrieve
|
||||
// over a channel. the manager will output certain message types at certain times.
|
||||
|
||||
// OpCode is a type that is used to describe what type
|
||||
// of event has occurred during the management process.
|
||||
type OpCode uint32
|
||||
|
||||
type OperatingMessage struct {
|
||||
Code OpCode
|
||||
data string
|
||||
CustomField string
|
||||
}
|
||||
|
||||
func (op *OperatingMessage) Custom() string {
|
||||
if op.CustomField != "" {
|
||||
return op.CustomField
|
||||
}
|
||||
return op.data
|
||||
}
|
||||
|
||||
// Ops
|
||||
const (
|
||||
OpNewDiff OpCode = iota
|
||||
OpNewFile
|
||||
OpNewBase
|
||||
OpWatchCommencing
|
||||
OpWatchStopped
|
||||
OpMessage
|
||||
OpEnablingPlugin
|
||||
OpPluginEnabled
|
||||
OpPluginError
|
||||
OpNone
|
||||
)
|
||||
|
||||
var ops = map[OpCode]OperatingMessage{
|
||||
OpNewDiff: {OpNewDiff, "New diff created", ""},
|
||||
OpNewFile: {OpNewFile, "New file created", ""},
|
||||
OpNewBase: {OpNewBase, "New base created", ""},
|
||||
//OpWatchCommencing: {Op_WatchCommencing, "File watching has started", ""},
|
||||
//OpWatchStopped: {Op_WatchStopped, "File watching has stopped", ""},
|
||||
OpMessage: {OpMessage, "Custom message attached - ", ""},
|
||||
OpEnablingPlugin: {OpEnablingPlugin, "Enabling Plugin - ", ""},
|
||||
OpPluginEnabled: {OpPluginEnabled, "Plugin Enabled", ""},
|
||||
OpPluginError: {OpPluginError, "Error enabling plugin", ""},
|
||||
OpNone: {OpNone, "No error code known", ""},
|
||||
}
|
||||
|
||||
// String prints the string version of the Op consts
|
||||
func (e OpCode) String() string {
|
||||
if op, found := ops[e]; found {
|
||||
return op.data
|
||||
}
|
||||
return "???"
|
||||
}
|
||||
|
||||
func (e OpCode) Retrieve() OperatingMessage {
|
||||
if op, found := ops[e]; found {
|
||||
return op
|
||||
}
|
||||
return ops[OpNone]
|
||||
}
|
Reference in New Issue
Block a user