├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── Makefile ├── README.md ├── backup ├── abstract.go ├── common.go ├── config.go ├── deduplication.go ├── heuristic.go ├── logfiles.go ├── messagekeys.go ├── process.go ├── progress.go └── utils.go ├── core ├── abstract.go ├── common.go ├── dir.go ├── format.go ├── info.go ├── messagekeys.go ├── path.go ├── proxylog.go └── utils.go ├── data ├── assets │ ├── ajax-loader-gears_32x32.gif │ ├── base.css │ ├── emblem-important-red.gif │ ├── emblem-synchronizing-cyan.gif │ ├── emblem-synchronizing-yellow.gif │ ├── loading_20181001_64x64.gif │ ├── loading_animated_64x64.gif │ ├── translate.en.toml │ └── translate.ru.toml ├── data.go ├── generate │ └── generate.go └── graphics │ ├── emblem-important-red.xcf │ ├── emblem-synchronizing-cyan.xcf │ ├── emblem-synchronizing-yellow.xcf │ └── tape-icon.xcf ├── docs ├── gorsync_about_dialog.png ├── gorsync_about_dialog_v0.3.1.png ├── gorsync_about_dialog_v0.3.2.png ├── gorsync_main_form.png ├── gorsync_main_form_v0.3.1.png ├── gorsync_main_form_v0.3.2.png ├── gorsync_preference_dialog.png ├── gorsync_preference_dialog_v0.3.1.png ├── gorsync_preference_dialog_v0.3.2.png └── gorsync_preference_dialog_v0.3.3.png ├── gorsync.go ├── gorsync_build.sh ├── gorsync_run.sh ├── locale ├── common.go ├── localization.go └── messagekeys.go ├── packaging ├── archlinux_pkgbuild │ ├── gorsync-git │ │ ├── .SRCINFO │ │ ├── .gitignore │ │ ├── PKGBUILD │ │ └── gorsync-git.install │ └── prepare_aur_pkgbuild.sh └── build_packages │ ├── create_archive_source_package.sh │ └── create_distrib_packages_with_fpm.sh ├── rsync ├── abstract.go ├── common.go ├── errors.go ├── messagekeys.go ├── rsync.go └── utils.go ├── sandbox ├── dev_scripts │ ├── create_distrib_via_fpm_with_docker.sh │ └── mount_10G_quota_dir.sh └── sandbox.tar.gz └── ui └── gtkui ├── aboutdlg.go ├── app.go ├── common.go ├── context.go ├── dialogs.go ├── gs_schema_install.sh ├── gs_schema_uninstall.sh ├── gsettings └── org.d2r2.gorsync.gschema.xml ├── gtk_before_3_22.go ├── gtk_since_3_22.go ├── markup.go ├── messagekeys.go ├── notifier.go ├── prefdlg.go ├── settings.go ├── settingstools.go ├── uitools.go ├── utils.go └── validator.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pprof 3 | gorsync 4 | go-rsync 5 | __debug_bin 6 | packaging/build_packages/packages/* 7 | data/assets_vfsdata.go 8 | data/generate/generate 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | # - "1.6" 5 | # - "1.7" 6 | # - "1.11" 7 | - "1.15" 8 | - "1.17" 9 | # - "tip" 10 | 11 | env: 12 | - GOARCH=amd64 13 | 14 | sudo: false 15 | 16 | dist: 17 | - xenial 18 | - focal 19 | 20 | #services: 21 | # - docker 22 | 23 | before_install: 24 | - sudo apt-get update -qq 25 | - sudo apt-get install -qq -y gtk+3.0 libgtk-3-dev libnotify-dev 26 | - sudo apt-get install -qq -y xvfb 27 | - "export DISPLAY=:99.0" 28 | - sudo /usr/bin/Xvfb $DISPLAY 2>1 > /dev/null & 29 | - "export GTK_VERSION=$(pkg-config --modversion gtk+-3.0 | tr . _| cut -d '_' -f 1-2)" 30 | - "export GLib_VERSION=$(pkg-config --modversion glib-2.0 | tr . _| cut -d '_' -f 1-2)" 31 | - "export Cairo_VERSION=$(pkg-config --modversion cairo)" 32 | - "export Pango_VERSION=$(pkg-config --modversion pango)" 33 | - echo "GTK ${GTK_VERSION}, GLib ${GLib_VERSION} (Cairo ${Cairo_VERSION}, Pango ${Pango_VERSION})" 34 | 35 | install: 36 | #- go get -t -tags "gtk_${GTK_VERSION} glib_${GLib_VERSION}" github.com/d2r2/gotk3/... 37 | - go get -t -tags "gtk_${GTK_VERSION} glib_${GLib_VERSION}" github.com/d2r2/go-rsync 38 | 39 | script: 40 | - go test -tags "gtk_${GTK_VERSION} glib_${GLib_VERSION}" github.com/d2r2/go-rsync 41 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "Launch program (dev)", 10 | "type": "go", 11 | "request": "launch", 12 | "mode": "exec", 13 | "program": "${workspaceFolder}/gorsync", 14 | "preLaunchTask": "Build program DEV", 15 | }, 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build program DEV", 8 | "type": "shell", 9 | "command": "${workspaceFolder}/gorsync_build.sh", 10 | "options": { 11 | "env": { 12 | "GOPATH": "${config:go.gopath}" 13 | }, 14 | "cwd": "${workspaceFolder}" 15 | } 16 | }, 17 | { 18 | "label": "Build program PROD", 19 | "type": "shell", 20 | "command": "${workspaceFolder}/gorsync_build.sh --buildtype Release", 21 | "options": { 22 | "env": { 23 | "GOPATH": "${config:go.gopath}" 24 | }, 25 | "cwd": "${workspaceFolder}" 26 | } 27 | }, 28 | { 29 | "label": "Create archive package (with sources)", 30 | "type": "shell", 31 | "command": "${workspaceFolder}/packaging/build_packages/create_archive_source_package.sh", 32 | "options": { 33 | "env": { 34 | "GOPATH": "${config:go.gopath}" 35 | }, 36 | "cwd": "${workspaceFolder}/packaging/build_packages" 37 | } 38 | }, 39 | { 40 | "label": "Create installation packages (archlinux, redhat, debian, freebsd)", 41 | "type": "shell", 42 | "command": "${workspaceFolder}/packaging/build_packages/create_distrib_packages_with_fpm.sh", 43 | "options": { 44 | "env": { 45 | "GOPATH": "${config:go.gopath}" 46 | }, 47 | "cwd": "${workspaceFolder}/packaging/build_packages" 48 | } 49 | } 50 | ], 51 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Find absolute path where Makedef resides with no difference how 2 | # make utility is started (by current path, either with -C option). 3 | mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) 4 | # Take directories part path from absolute path to Makedef file. 5 | mkfile_dir := $(dir $(mkfile_path)) 6 | current_dir := $(notdir $(patsubst %/,%,$(dir $(mkfile_path)))) 7 | GOCMD=go 8 | GOCLEAN=$(GOCMD) clean 9 | GOTEST=$(GOCMD) test 10 | BINARY_NAME=gorsync 11 | # Always reassign GOPATH here, because Makedef is used not only here, but in source archive file 12 | # compile mode too. Move up by 4 folders from current dir to setup GOPATH as a point 13 | # to root folder where src, bin, pkg folders are resided. 14 | GOBUILD=eval 'GOPATH=$(mkfile_dir)../../../.. ./gorsync_build.sh --buildtype Release --output $(PWD)/$(BINARY_NAME)' 15 | # GOBUILD=eval 'GOPATH=$(PWD) ./gorsync_build.sh --buildtype Release' 16 | 17 | all: build 18 | 19 | # Used for some debugging only. 20 | print: 21 | $(eval GOPATH=$(shell bash -c 'echo ${PWD%/*/*/*}')) 22 | echo $(GOPATH) 23 | $(eval GOPATH=$(shell bash -c "echo ${PWD}")) 24 | echo $(GOPATH) 25 | echo ../..$(GOPATH) 26 | # echo '${PWD%/*/*/*}' 27 | 28 | # Main build entry 29 | build: 30 | $(GOBUILD) 31 | 32 | # Delete application binary from current folder and from $GOPATH/bin (if present). 33 | clean: 34 | $(GOCLEAN) 35 | rm -f $(BINARY_NAME) 36 | 37 | -------------------------------------------------------------------------------- /backup/abstract.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package backup 13 | 14 | import ( 15 | "time" 16 | 17 | "github.com/d2r2/go-rsync/core" 18 | ) 19 | 20 | // Notifier interface is used as a contract to provide 21 | // event-driven mechanism, to map backup process steps with, 22 | // for instance, user interface. 23 | type Notifier interface { 24 | 25 | // Pair of calls to report about 1st pass start and completion. 26 | NotifyPlanStage_NodeStructureStartInquiry(sourceID int, 27 | sourceRsync string) error 28 | NotifyPlanStage_NodeStructureDoneInquiry(sourceID int, 29 | sourceRsync string, dir *core.Dir) error 30 | 31 | // Pair of calls to report about 2nd pass start and completion. 32 | NotifyBackupStage_FolderStartBackup(rootDest string, 33 | paths core.SrcDstPath, backupType core.FolderBackupType, 34 | leftToBackup core.FolderSize, 35 | timePassed time.Duration, eta *time.Duration, 36 | ) error 37 | NotifyBackupStage_FolderDoneBackup(rootDest string, 38 | paths core.SrcDstPath, backupType core.FolderBackupType, 39 | leftToBackup core.FolderSize, sizeDone core.SizeProgress, 40 | timePassed time.Duration, eta *time.Duration, 41 | sessionErr error) error 42 | } 43 | -------------------------------------------------------------------------------- /backup/common.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package backup 13 | 14 | import ( 15 | "fmt" 16 | 17 | "github.com/d2r2/go-logger" 18 | ) 19 | 20 | // You can manage verbosity of log output 21 | // in the package by changing last parameter value 22 | // (comment/uncomment corresponding lines). 23 | var LocalLog = logger.NewPackageLogger("backup", 24 | // logger.DebugLevel, 25 | logger.InfoLevel, 26 | ) 27 | 28 | var f = fmt.Sprintf 29 | -------------------------------------------------------------------------------- /backup/config.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package backup 13 | 14 | import ( 15 | "fmt" 16 | 17 | "github.com/d2r2/go-rsync/core" 18 | "github.com/d2r2/go-rsync/rsync" 19 | ) 20 | 21 | /* 22 | type IRsyncConfigurable interface { 23 | GetRsyncParams(addExtraParams []string) []string 24 | } 25 | */ 26 | 27 | // Node contain information about single RSYNC source backup. 28 | type Node struct { 29 | Module Module 30 | RootDir *core.Dir 31 | } 32 | 33 | // Plan keep all necessary information obtained from 34 | // preferences and 1st backup pass to start backup process. 35 | type Plan struct { 36 | Config *Config 37 | Nodes []Node 38 | BackupSize core.FolderSize 39 | } 40 | 41 | // GetModules returns all RSYNC source/destination blocks 42 | // defined in single (specific) backup profile. 43 | func (v *Plan) GetModules() []Module { 44 | modules := []Module{} 45 | for _, item := range v.Nodes { 46 | modules = append(modules, item.Module) 47 | } 48 | return modules 49 | } 50 | 51 | // Config keeps backup session configuration. 52 | // Config instance is initialized mainly from 53 | // GLIB GSettings in ui/gtkui package. 54 | type Config struct { 55 | SigFileIgnoreBackup string `toml:"sig_file_ignore_backup"` 56 | RsyncRetryCount *int `toml:"retry_count"` 57 | AutoManageBackupBlockSize *bool `toml:"auto_manage_backup_block_size"` 58 | MaxBackupBlockSizeMb *int `toml:"max_backup_block_size_mb"` 59 | UsePreviousBackup *bool `toml:"use_previous_backup"` 60 | NumberOfPreviousBackupToUse *int `toml:"number_of_previous_backup_to_use"` 61 | EnableLowLevelLogForRsync *bool `toml:"enable_low_level_log_rsync"` 62 | EnableIntensiveLowLevelLogForRsync *bool `toml:"enable_intensive_low_level_log_rsync"` 63 | 64 | RsyncTransferSourceOwner *bool `toml:"rsync_transfer_source_owner"` // rsync --owner 65 | RsyncTransferSourceGroup *bool `toml:"rsync_transfer_source_group"` // rsync --group 66 | RsyncTransferSourcePermissions *bool `toml:"rsync_transfer_source_permissions"` // rsync --perms 67 | RsyncRecreateSymlinks *bool `toml:"rsync_recreate_symlinks"` // rsync --links 68 | RsyncTransferDeviceFiles *bool `toml:"rsync_transfer_device_files"` // rsync --devices 69 | RsyncTransferSpecialFiles *bool `toml:"rsync_transfer_special_files"` // rsync --specials 70 | RsyncCompressFileTransfer *bool `toml:"rsync_compress_file_transfer"` // rsync --compress 71 | 72 | // BackupNode list contain all RSYNC sources to backup in one session. 73 | //Modules []Module `toml:"backup_module"` 74 | } 75 | 76 | /* 77 | func NewConfig(filePath string) (*Config, error) { 78 | var config Config 79 | if _, err := toml.DecodeFile(filePath, &config); err != nil { 80 | return nil, err 81 | } 82 | LocalLog.Debug(f("%+v", config)) 83 | return &config, nil 84 | } 85 | */ 86 | 87 | func (conf *Config) usePreviousBackupEnabled() bool { 88 | var usePreviousBackup = true 89 | if conf.UsePreviousBackup != nil { 90 | usePreviousBackup = *conf.UsePreviousBackup 91 | } 92 | return usePreviousBackup 93 | } 94 | 95 | func (conf *Config) numberOfPreviousBackupToUse() int { 96 | var numberOfPreviousBackupToUse = 1 97 | if conf.NumberOfPreviousBackupToUse != nil { 98 | numberOfPreviousBackupToUse = *conf.NumberOfPreviousBackupToUse 99 | } 100 | return numberOfPreviousBackupToUse 101 | } 102 | 103 | func (conf *Config) getRsyncLoggingSettings() *rsync.Logging { 104 | logging := &rsync.Logging{} 105 | if conf.EnableLowLevelLogForRsync != nil { 106 | logging.EnableLog = *conf.EnableLowLevelLogForRsync 107 | } 108 | if conf.EnableIntensiveLowLevelLogForRsync != nil { 109 | logging.EnableIntensiveLog = *conf.EnableIntensiveLowLevelLogForRsync 110 | } 111 | return logging 112 | } 113 | 114 | func (conf *Config) getBackupBlockSizeSettings() *backupBlockSizeSettings { 115 | blockSize := &backupBlockSizeSettings{AutoManageBackupBlockSize: true, BackupBlockSize: 500} 116 | if conf.AutoManageBackupBlockSize != nil { 117 | blockSize.AutoManageBackupBlockSize = *conf.AutoManageBackupBlockSize 118 | } 119 | if conf.MaxBackupBlockSizeMb != nil { 120 | blockSize.BackupBlockSize = uint64(*conf.MaxBackupBlockSizeMb * 1024 * 1024) 121 | } 122 | return blockSize 123 | } 124 | 125 | // Module signify RSYNC source/destination block, with 126 | // source/destination URLs and other auxiliary options. 127 | // Used as configuration data in the backup session code. 128 | type Module struct { 129 | SourceRsync string `toml:"src_rsync"` 130 | DestSubPath string `toml:"dst_subpath"` 131 | 132 | ChangeFilePermission string `toml:"rsync_change_file_permission"` 133 | AuthPassword *string `toml:"module_auth_password"` 134 | 135 | RsyncTransferSourceOwner *bool `toml:"rsync_transfer_source_owner"` // rsync --owner 136 | RsyncTransferSourceGroup *bool `toml:"rsync_transfer_source_group"` // rsync --group 137 | RsyncTransferSourcePermissions *bool `toml:"rsync_transfer_source_permissions"` // rsync --perms 138 | RsyncRecreateSymlinks *bool `toml:"rsync_recreate_symlinks"` // rsync --links 139 | RsyncTransferDeviceFiles *bool `toml:"rsync_transfer_device_files"` // rsync --devices 140 | RsyncTransferSpecialFiles *bool `toml:"rsync_transfer_special_files"` // rsync --specials 141 | } 142 | 143 | // GetRsyncParams prepare RSYNC CLI parameters to run console RSYNC process. 144 | func GetRsyncParams(conf *Config, module *Module, addExtraParams []string) []string { 145 | var params []string 146 | if module.RsyncTransferSourceOwner != nil && *module.RsyncTransferSourceOwner || 147 | module.RsyncTransferSourceOwner == nil && conf.RsyncTransferSourceOwner != nil && 148 | *conf.RsyncTransferSourceOwner { 149 | params = append(params, "--owner") 150 | } 151 | if module.RsyncTransferSourceGroup != nil && *module.RsyncTransferSourceGroup || 152 | module.RsyncTransferSourceGroup == nil && conf.RsyncTransferSourceGroup != nil && 153 | *conf.RsyncTransferSourceGroup { 154 | params = append(params, "--group") 155 | } 156 | if module.RsyncTransferSourcePermissions != nil && *module.RsyncTransferSourcePermissions || 157 | module.RsyncTransferSourcePermissions == nil && conf.RsyncTransferSourcePermissions != nil && 158 | *conf.RsyncTransferSourcePermissions { 159 | params = append(params, "--perms") 160 | } 161 | if module.RsyncRecreateSymlinks != nil && *module.RsyncRecreateSymlinks || 162 | module.RsyncRecreateSymlinks == nil && conf.RsyncRecreateSymlinks != nil && 163 | *conf.RsyncRecreateSymlinks { 164 | params = append(params, "--links") 165 | } 166 | if module.RsyncTransferDeviceFiles != nil && *module.RsyncTransferDeviceFiles || 167 | module.RsyncTransferDeviceFiles == nil && conf.RsyncTransferDeviceFiles != nil && 168 | *conf.RsyncTransferDeviceFiles { 169 | params = append(params, "--devices") 170 | } 171 | if module.RsyncTransferSpecialFiles != nil && *module.RsyncTransferSpecialFiles || 172 | module.RsyncTransferSpecialFiles == nil && conf.RsyncTransferSpecialFiles != nil && 173 | *conf.RsyncTransferSpecialFiles { 174 | params = append(params, "--specials") 175 | } 176 | if conf.RsyncCompressFileTransfer != nil && *conf.RsyncCompressFileTransfer { 177 | params = append(params, "--compress") 178 | } 179 | if module.ChangeFilePermission != "" { 180 | params = append(params, fmt.Sprintf("--chmod=%s", module.ChangeFilePermission)) 181 | } 182 | 183 | params = append(params, addExtraParams...) 184 | return params 185 | } 186 | -------------------------------------------------------------------------------- /backup/deduplication.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package backup 13 | 14 | import ( 15 | "bufio" 16 | "bytes" 17 | "crypto/sha256" 18 | "encoding/base64" 19 | "encoding/gob" 20 | "io/ioutil" 21 | "os" 22 | "path" 23 | "path/filepath" 24 | "sort" 25 | "time" 26 | 27 | logger "github.com/d2r2/go-logger" 28 | "github.com/d2r2/go-rsync/locale" 29 | "github.com/d2r2/go-rsync/rsync" 30 | ) 31 | 32 | // NodeSignature keep RSYNC source path 33 | // crypted with hash function and destination subpath. 34 | // RSYNC source path crypted with hash function 35 | // is used as "source identifier" to search repeated 36 | // backup sessions to use for deduplication. 37 | // Content of this object is serialized to the file 38 | // stored in backup session root folder. 39 | type NodeSignature struct { 40 | SourceRsyncCipher string 41 | DestSubPath string 42 | } 43 | 44 | // GetSignature builds NodeSignature object on the basis of BackupNodePath data. 45 | func GetSignature(module Module) NodeSignature { 46 | signature := NodeSignature{SourceRsyncCipher: GenerateSourceID(module.SourceRsync), 47 | DestSubPath: module.DestSubPath} 48 | return signature 49 | } 50 | 51 | // GenerateSourceID convert RSYNC source URL to unique identifier. 52 | func GenerateSourceID(rsyncSource string) string { 53 | return chipherStr(rsync.NormalizeRsyncURL(rsyncSource)) 54 | } 55 | 56 | // chipherStr encode str with SHA256 hash function. 57 | // Used to encode RSYNC source path before file serialization. 58 | func chipherStr(str string) string { 59 | hasher := sha256.New() 60 | var b bytes.Buffer 61 | b.WriteString(str) 62 | _, err := hasher.Write(b.Bytes()) 63 | if err != nil { 64 | LocalLog.Fatal(err) 65 | } 66 | sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) 67 | return sha 68 | } 69 | 70 | // NodeSignatures keeps list of RSYNC source to backup in one session. 71 | type NodeSignatures struct { 72 | Signatures []NodeSignature 73 | } 74 | 75 | // GetNodeSignatures convert RSYNC module source URLs to 76 | // unique identifiers built with use chiper function. 77 | // These signatures used lately to find previous backup session 78 | // for deduplication purpose. 79 | func GetNodeSignatures(modules []Module) NodeSignatures { 80 | signatures := make([]NodeSignature, len(modules)) 81 | for i, item := range modules { 82 | signatures[i] = GetSignature(item) 83 | } 84 | s := NodeSignatures{Signatures: signatures} 85 | return s 86 | } 87 | 88 | // FindFirstSignature find first item which match signature parameter. 89 | func (v NodeSignatures) FindFirstSignature(signature string) *NodeSignature { 90 | for _, item := range v.Signatures { 91 | if item.SourceRsyncCipher == signature { 92 | return &item 93 | } 94 | } 95 | return nil 96 | } 97 | 98 | // PrevBackup describe previous backup found, which contain same RSYNC source. 99 | // Such previous backups used for RSYNC utility deduplication, which 100 | // significantly decrease size and time for new backup session. 101 | type PrevBackup struct { 102 | // Full path to signature file name 103 | SignatureFileName string 104 | Signature NodeSignature 105 | } 106 | 107 | // GetDirPath returns full path to data copied in previous successful backup session. 108 | func (v PrevBackup) GetDirPath() string { 109 | backupPath := path.Join(path.Dir(v.SignatureFileName), v.Signature.DestSubPath) 110 | return backupPath 111 | } 112 | 113 | // PreviousBackups keeps list of previous backup found. See description of PrevBackup. 114 | type PreviousBackups struct { 115 | Backups []PrevBackup 116 | } 117 | 118 | // GetDirPaths provide file system paths to previous backup sessions found. 119 | func (v *PreviousBackups) GetDirPaths() []string { 120 | paths := make([]string, len(v.Backups)) 121 | for i, b := range v.Backups { 122 | paths[i] = b.GetDirPath() 123 | } 124 | return paths 125 | } 126 | 127 | // FilterBySourceID choose backup sessions which contains same source 128 | // as specified by sourceID. 129 | func (v *PreviousBackups) FilterBySourceID(sourceID string) *PreviousBackups { 130 | var newPreviousBackups []PrevBackup 131 | for _, v := range v.Backups { 132 | if sourceID == v.Signature.SourceRsyncCipher { 133 | newPreviousBackups = append(newPreviousBackups, v) 134 | } 135 | } 136 | return &PreviousBackups{Backups: newPreviousBackups} 137 | } 138 | 139 | type prevBackupEntry struct { 140 | time time.Time 141 | backup PrevBackup 142 | } 143 | 144 | // FindPrevBackupPathsByNodeSignatures search for previous backup sessions which 145 | // might significantly decrease backup size and speed up process. 146 | // In the end it should return list of previous backup sessions sorted by date/time 147 | // in descending order (recent go first). 148 | func FindPrevBackupPathsByNodeSignatures(lg logger.PackageLog, destPath string, 149 | signs NodeSignatures, lastN int) (*PreviousBackups, error) { 150 | 151 | // select all child items from root backup destination path 152 | items, err := ioutil.ReadDir(destPath) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | candidates := make(map[string][]prevBackupEntry) 158 | 159 | // loop through child folders to identify them as a previous backup sessions 160 | for _, item := range items { 161 | if item.IsDir() { 162 | fileName := filepath.Join(destPath, item.Name(), GetMetadataSignatureFileName()) 163 | stat, err := os.Stat(fileName) 164 | if err != nil { 165 | if !os.IsNotExist(err) { 166 | if os.IsPermission(err) { 167 | lg.Notify(locale.T(MsgLogBackupStagePreviousBackupDiscoveryPermissionError, 168 | struct{ Path string }{Path: item.Name()})) 169 | } else { 170 | lg.Notify(locale.T(MsgLogBackupStagePreviousBackupDiscoveryOtherError, 171 | struct { 172 | Path string 173 | Error error 174 | }{Path: item.Name(), Error: err})) 175 | } 176 | } 177 | continue 178 | } 179 | 180 | file, err := os.Open(fileName) 181 | if err != nil { 182 | return nil, err 183 | } 184 | defer file.Close() 185 | 186 | scanner := bufio.NewScanner(file) 187 | for scanner.Scan() { 188 | signs2, err := DecodeSignatures(scanner.Text()) 189 | if err != nil { 190 | break 191 | } 192 | for _, item1 := range signs.Signatures { 193 | if candidate := signs2.FindFirstSignature(item1.SourceRsyncCipher); candidate != nil { 194 | backup := PrevBackup{SignatureFileName: fileName, Signature: *candidate} 195 | candidates[item1.SourceRsyncCipher] = append(candidates[item1.SourceRsyncCipher], 196 | prevBackupEntry{time: stat.ModTime(), backup: backup}) 197 | } 198 | } 199 | } 200 | 201 | if err := scanner.Err(); err != nil { 202 | return nil, err 203 | } 204 | } 205 | } 206 | 207 | // sort all candidates found by creation/modification time, to select the most resent previous backup sessions 208 | candidates2 := make(map[string][]prevBackupEntry) 209 | for k, v := range candidates { 210 | // sort previous backup sessions in descending order (the most recent come first) 211 | sorted := filesSortedByDate{Files: v} 212 | sort.Sort(sorted) 213 | maxPrevSessions := lastN 214 | // extra protection: according to limitation which exist in RSYNC, 215 | // no more than 20 --link-dest options could be provided with CLI, otherwise 216 | // RSYNC call failed (syntax or usage error, code 1) thrown; 217 | // maximum number of --link-dest option in single RSYNC call (detected experimentally) 218 | const maxLinkDest = 20 219 | // if still exceed, cut down 220 | if maxPrevSessions > maxLinkDest { 221 | maxPrevSessions = maxLinkDest 222 | } 223 | if len(sorted.Files) > maxPrevSessions { 224 | // cut to maxPrevSessions maximum 225 | sorted.Files = sorted.Files[:maxPrevSessions] 226 | } 227 | candidates2[k] = sorted.Files 228 | } 229 | 230 | var backups []PrevBackup 231 | for _, v := range candidates2 { 232 | for _, v2 := range v { 233 | backups = append(backups, v2.backup) 234 | } 235 | } 236 | 237 | backups2 := &PreviousBackups{Backups: backups} 238 | return backups2, nil 239 | } 240 | 241 | // Temporary object used to sort found previous backup sessions by creation/modification date 242 | // in descending order (the most recent come first). 243 | type filesSortedByDate struct { 244 | Files []prevBackupEntry 245 | } 246 | 247 | func (s filesSortedByDate) Len() int { 248 | return len(s.Files) 249 | } 250 | 251 | func (s filesSortedByDate) Less(i, j int) bool { 252 | return s.Files[i].time.After(s.Files[j].time) 253 | } 254 | 255 | func (s filesSortedByDate) Swap(i, j int) { 256 | node := s.Files[i] 257 | s.Files[i] = s.Files[j] 258 | s.Files[j] = node 259 | } 260 | 261 | // CreateMetadataSignatureFile serialize RSYNC sources plus destination subpaths 262 | // to the special "backup session signature" file. 263 | func CreateMetadataSignatureFile(modules []Module, destPath string) error { 264 | signs := GetNodeSignatures(modules) 265 | err := createDirAll(destPath) 266 | if err != nil { 267 | return err 268 | } 269 | destPath = filepath.Join(destPath, GetMetadataSignatureFileName()) 270 | file, err := os.Create(destPath) 271 | if err != nil { 272 | return err 273 | } 274 | defer file.Close() 275 | v, err := EncodeSignatures(signs) 276 | if err != nil { 277 | return err 278 | } 279 | _, err = file.WriteString(v) 280 | if err != nil { 281 | return err 282 | } 283 | return nil 284 | } 285 | 286 | // EncodeSignatures encode NodeSignatures object to self-describing binary format. 287 | func EncodeSignatures(signs NodeSignatures) (string, error) { 288 | var b bytes.Buffer 289 | e := gob.NewEncoder(&b) 290 | err := e.Encode(signs) 291 | if err != nil { 292 | return "", err 293 | } 294 | return base64.StdEncoding.EncodeToString(b.Bytes()), nil 295 | } 296 | 297 | // DecodeSignatures decode NodeSignatures object from self-describing binary format. 298 | func DecodeSignatures(str string) (*NodeSignatures, error) { 299 | m := &NodeSignatures{} 300 | by, err := base64.StdEncoding.DecodeString(str) 301 | if err != nil { 302 | return nil, err 303 | } 304 | b := bytes.Buffer{} 305 | b.Write(by) 306 | d := gob.NewDecoder(&b) 307 | err = d.Decode(m) 308 | if err != nil { 309 | return nil, err 310 | } 311 | return m, nil 312 | } 313 | -------------------------------------------------------------------------------- /backup/logfiles.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package backup 13 | 14 | import ( 15 | "io/ioutil" 16 | "os" 17 | "path" 18 | 19 | shell "github.com/d2r2/go-shell" 20 | ) 21 | 22 | // LogFiles track log files during backup session. 23 | // It has functionality to relocate log files from 24 | // one storage to another: used when log files moved 25 | // from /tmp partition to permanent destination location. 26 | type LogFiles struct { 27 | rootPath string 28 | logs map[string]*os.File 29 | } 30 | 31 | // NewLogFiles create new LogFiles instance. 32 | func NewLogFiles() *LogFiles { 33 | v := &LogFiles{logs: make(map[string]*os.File)} 34 | return v 35 | } 36 | 37 | // CreateOrGetLogFile return os.File by file name identifier. 38 | // This allow to control and operate multiple log files in one place. 39 | func (v *LogFiles) CreateOrGetLogFile(suffixPath string) (*os.File, error) { 40 | err := v.assignRootPathByDefault() 41 | if err != nil { 42 | return nil, err 43 | } 44 | file := v.logs[suffixPath] 45 | if file == nil { 46 | file, err = os.OpenFile(v.getFullPath(suffixPath), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) 47 | if err != nil { 48 | return nil, err 49 | } 50 | v.logs[suffixPath] = file 51 | } 52 | return file, nil 53 | } 54 | 55 | func (v *LogFiles) getFullPath(suffixPath string) string { 56 | return path.Join(v.rootPath, suffixPath) 57 | } 58 | 59 | // Close will close all os.File instances found in the object. 60 | func (v *LogFiles) Close() error { 61 | for suffixPath, val := range v.logs { 62 | if val != nil { 63 | err := val.Close() 64 | if err != nil { 65 | return err 66 | } 67 | v.logs[suffixPath] = nil 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | // ChangeRootPath relocate log files from one storage to another. 74 | // Used to move from 1st backup stage (plan stage) to 2nd (backup stage). 75 | // In 1st backup stage we keep log files in /tmp partition, in 2nd stage 76 | // we relocate and save them in destination location. 77 | func (v *LogFiles) ChangeRootPath(newRootPath string) error { 78 | err := v.Close() 79 | if err != nil { 80 | return err 81 | } 82 | if _, err = os.Stat(v.rootPath); !os.IsNotExist(err) { 83 | for suffixPath := range v.logs { 84 | oldpath := v.getFullPath(suffixPath) 85 | newpath := path.Join(newRootPath, suffixPath) 86 | _, err = shell.CopyFile(oldpath, newpath) 87 | if err != nil { 88 | return err 89 | } 90 | } 91 | } 92 | v.rootPath = newRootPath 93 | return nil 94 | } 95 | 96 | func (v *LogFiles) assignRootPathByDefault() error { 97 | if v.rootPath == "" { 98 | dir, err := ioutil.TempDir("", "gorsync_logs_") 99 | if err != nil { 100 | return err 101 | } 102 | v.rootPath = dir 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /backup/messagekeys.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package backup 13 | 14 | // ------------------------------------------------------------ 15 | // File contains message identifiers for localization purpose. 16 | // Message identifier names is self-descriptive, so ordinary 17 | // it's easy to understand what message is made for. 18 | // Message ID is used to call translation functions from 19 | // "locale" package. 20 | // ------------------------------------------------------------ 21 | 22 | const ( 23 | MsgRsyncInfo = "RsyncInfo" 24 | MsgGolangInfo = "GolangInfo" 25 | 26 | MsgFolderBackupTypeSkipDescription = "FolderBackupTypeSkipDescription" 27 | MsgFolderBackupTypeRecursiveDescription = "FolderBackupTypeRecursiveDescription" 28 | MsgFolderBackupTypeContentDescription = "FolderBackupTypeContentDescription" 29 | 30 | MsgLogPlanStageStarting = "LogPlanStageStarting" 31 | MsgLogPlanStageStartTime = "LogPlanStageStartTime" 32 | MsgLogPlanStageEndTime = "LogPlanStageEndTime" 33 | MsgLogPlanStartIterateViaNSources = "LogPlanStartIterateViaNSources" 34 | MsgLogPlanStageInquirySource = "LogPlanStageInquirySource" 35 | MsgLogPlanStageSourceFolderCountInfo = "LogPlanStageSourceFolderCountInfo" 36 | MsgLogPlanStageSourceSkipFolderCountInfo = "LogPlanStageSourceSkipFolderCountInfo" 37 | MsgLogPlanStageSourceTotalSizeInfo = "LogPlanStageSourceTotalSizeInfo" 38 | MsgLogPlanStageUseTemporaryFolder = "LogPlanStageUseTemporaryFolder" 39 | MsgLogPlanStageBuildFolderError = "LogPlanStageBuildFolderError" 40 | 41 | MsgLogBackupStageStarting = "LogBackupStageStarting" 42 | MsgLogBackupStageStartTime = "LogBackupStageStartTime" 43 | MsgLogBackupStageEndTime = "LogBackupStageEndTime" 44 | MsgLogBackupStageBackupToDestination = "LogBackupStageBackupToDestination" 45 | MsgLogBackupStagePreviousBackupDiscoveryPermissionError = "LogBackupStagePreviousBackupDiscoveryPermissionError" 46 | MsgLogBackupStagePreviousBackupDiscoveryOtherError = "LogBackupStagePreviousBackupDiscoveryOtherError" 47 | MsgLogBackupStagePreviousBackupFoundAndWillBeUsed = "LogBackupStagePreviousBackupFoundAndWillBeUsed" 48 | MsgLogBackupStagePreviousBackupFoundButDisabled = "LogBackupStagePreviousBackupFoundButDisabled" 49 | MsgLogBackupStagePreviousBackupNotFound = "LogBackupStagePreviousBackupNotFound" 50 | MsgLogBackupStageStartToBackupFromSource = "LogBackupStageStartToBackupFromSource" 51 | MsgLogBackupStageRenameDestination = "LogBackupStageRenameDestination" 52 | MsgLogBackupStageFailedToCreateFolder = "LogBackupStageFailedToCreateFolder" 53 | MsgLogBackupDetectedTotalBackupSizeGetChanged = "LogBackupDetectedTotalBackupSizeGetChanged" 54 | MsgLogBackupStageProgressBackupSuccess = "LogBackupStageProgressBackupSuccess" 55 | MsgLogBackupStageProgressBackupError = "LogBackupStageProgressBackupError" 56 | MsgLogBackupStageProgressSkipBackupError = "LogBackupStageProgressSkipBackupError" 57 | MsgLogBackupStageCriticalError = "LogBackupStageCriticalError" 58 | MsgLogBackupStageDiscoveringPreviousBackups = "LogBackupStageDiscoveringPreviousBackups" 59 | MsgLogBackupStageRecoveredFromError = "LogBackupStageRecoveredFromError" 60 | MsgLogBackupStageSaveRsyncExtraLogTo = "LogBackupStageSaveRsyncExtraLogTo" 61 | MsgLogBackupStageSaveLogTo = "LogBackupStageSaveLogTo" 62 | MsgLogBackupStageExitMessage = "LogBackupStageExitMessage" 63 | 64 | MsgLogStatisticsSummaryCaption = "LogStatisticsSummaryCaption" 65 | MsgLogStatisticsEnvironmentCaption = "LogStatisticsEnvironmentCaption" 66 | MsgLogStatisticsResultsCaption = "LogStatisticsResultsCaption" 67 | MsgLogStatisticsStatusCaption = "LogStatisticsStatusCaption" 68 | MsgLogStatisticsStatusSuccessfullyCompleted = "LogStatisticsStatusSuccessfullyCompleted" 69 | MsgLogStatisticsStatusCompletedWithErrors = "LogStatisticsStatusCompletedWithErrors" 70 | MsgLogStatisticsPlanStageCaption = "LogStatisticsPlanStageCaption" 71 | MsgLogStatisticsPlanStageSourceToBackup = "LogStatisticsPlanStageSourceToBackup" 72 | MsgLogStatisticsPlanStageTotalSize = "LogStatisticsPlanStageTotalSize" 73 | MsgLogStatisticsPlanStageFolderCount = "LogStatisticsPlanStageFolderCount" 74 | MsgLogStatisticsPlanStageFolderSkipCount = "LogStatisticsPlanStageFolderSkipCount" 75 | MsgLogStatisticsPlanStageTimeTaken = "LogStatisticsPlanStageTimeTaken" 76 | MsgLogStatisticsBackupStageCaption = "LogStatisticsBackupStageCaption" 77 | MsgLogStatisticsBackupStageDestinationPath = "LogStatisticsBackupStageDestinationPath" 78 | MsgLogStatisticsBackupStagePreviousBackupFound = "LogStatisticsBackupStagePreviousBackupFound" 79 | MsgLogStatisticsBackupStagePreviousBackupFoundButDisabled = "LogStatisticsBackupStagePreviousBackupFoundButDisabled" 80 | MsgLogStatisticsBackupStageNoValidPreviousBackupFound = "LogStatisticsBackupStageNoValidPreviousBackupFound" 81 | MsgLogStatisticsBackupStageTotalSize = "LogStatisticsBackupStageTotalSize" 82 | MsgLogStatisticsBackupStageSkippedSize = "LogStatisticsBackupStageSkippedSize" 83 | MsgLogStatisticsBackupStageFailedToBackupSize = "LogStatisticsBackupStageFailedToBackupSize" 84 | MsgLogStatisticsBackupStageTimeTaken = "LogStatisticsBackupStageTimeTaken" 85 | ) 86 | -------------------------------------------------------------------------------- /backup/utils.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package backup 13 | 14 | import ( 15 | "bufio" 16 | "bytes" 17 | "errors" 18 | "fmt" 19 | "os" 20 | "time" 21 | 22 | "github.com/d2r2/go-rsync/core" 23 | "github.com/d2r2/go-rsync/locale" 24 | ) 25 | 26 | // TAB_RUNE keep tab character. 27 | const TAB_RUNE = '\t' 28 | 29 | func createDirAll(path string) error { 30 | err := os.MkdirAll(path, 0777) 31 | return err 32 | } 33 | 34 | func createDirInBackupStage(path string) error { 35 | err := createDirAll(path) 36 | if err != nil { 37 | err = errors.New(locale.T(MsgLogBackupStageFailedToCreateFolder, 38 | struct { 39 | Path string 40 | Error error 41 | }{Path: path, Error: err})) 42 | return err 43 | } 44 | return nil 45 | } 46 | 47 | func splitToLines(buf *bytes.Buffer) ([]string, error) { 48 | var lines []string 49 | scanner := bufio.NewScanner(buf) 50 | for scanner.Scan() { 51 | lines = append(lines, scanner.Text()) 52 | } 53 | if err := scanner.Err(); err != nil { 54 | return nil, err 55 | } 56 | return lines, nil 57 | } 58 | 59 | func writeLineIndent(buf *bytes.Buffer, tabNumber int, text string) { 60 | for i := 0; i < tabNumber; i++ { 61 | buf.WriteRune(TAB_RUNE) 62 | } 63 | buf.WriteString(fmt.Sprintln(text)) 64 | } 65 | 66 | // GetBackupTypeDescription return localized description of how 67 | // application will backup specific directory described by core.Dir object. 68 | // It could be 3 options: 69 | // 1) full backup: backup full content include all nested folders; 70 | // 2) flat backup: backup only direct files in folder, ignore nested folders; 71 | // 3) skip backup: skip folder backup (this happens when specific signature file found). 72 | func GetBackupTypeDescription(backupType core.FolderBackupType) string { 73 | var backupStr string 74 | switch backupType { 75 | case core.FBT_SKIP: 76 | backupStr = locale.T(MsgFolderBackupTypeSkipDescription, nil) 77 | case core.FBT_RECURSIVE: 78 | backupStr = locale.T(MsgFolderBackupTypeRecursiveDescription, nil) 79 | case core.FBT_CONTENT: 80 | backupStr = locale.T(MsgFolderBackupTypeContentDescription, nil) 81 | } 82 | return backupStr 83 | } 84 | 85 | // GetBackupFolderName return new folder name for ongoing backup process. 86 | func GetBackupFolderName(incomplete bool, date *time.Time) string { 87 | prefixPath := "~rsync_backup" 88 | if incomplete { 89 | prefixPath += "_(incomplete)" 90 | } 91 | var dt time.Time = time.Now() 92 | if date != nil { 93 | dt = *date 94 | } 95 | prefixPath += dt.Format("~20060102-150405~") 96 | return prefixPath 97 | } 98 | 99 | // GetMetadataSignatureFileName return the name of specific file 100 | // which describe all sources used in backup process. 101 | func GetMetadataSignatureFileName() string { 102 | return "~backup_nodes~.signatures" 103 | } 104 | 105 | // GetLogFileName return the name of general backup process log. 106 | func GetLogFileName() string { 107 | return "~backup_log~.log" 108 | } 109 | 110 | // GetRsyncLogFileName return the name of specific low-level RSYNC utility log. 111 | func GetRsyncLogFileName() string { 112 | return "~rsync_log~.log" 113 | } 114 | -------------------------------------------------------------------------------- /core/abstract.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package core 13 | 14 | // FolderBackupType define how 15 | // to backup folder content. 16 | type FolderBackupType int 17 | 18 | const ( 19 | // FBT_UNKNOWN denotes undefined backup approach. 20 | FBT_UNKNOWN FolderBackupType = iota 21 | // FBT_SKIP denotes skip to backup folder content (including subfolders). 22 | FBT_SKIP 23 | // FBT_RECURSIVE denotes backup full folder content including all subfolders. 24 | FBT_RECURSIVE 25 | // FBT_CONTENT denotes backup only files located directly in the folder. Do not backup subfolders. 26 | FBT_CONTENT 27 | ) 28 | 29 | // String implement Stringer interface. 30 | func (v FolderBackupType) String() string { 31 | var backupStr string 32 | switch v { 33 | case FBT_SKIP: 34 | backupStr = "skip" 35 | case FBT_RECURSIVE: 36 | backupStr = "full folder content" 37 | case FBT_CONTENT: 38 | backupStr = "folder files" 39 | case FBT_UNKNOWN: 40 | backupStr = "" 41 | } 42 | return backupStr 43 | } 44 | 45 | // FolderSize used to signify size of backup objects. 46 | type FolderSize int64 47 | 48 | // NewFolderSize create new FolderSize instance. 49 | func NewFolderSize(size int64) FolderSize { 50 | v := FolderSize(size) 51 | return v 52 | } 53 | 54 | // GetByteCount returns size of FolderSize in bytes. 55 | func (v FolderSize) GetByteCount() uint64 { 56 | return uint64(v) 57 | } 58 | 59 | // Add combines sizes of two FolderSize objects. 60 | func (v FolderSize) Add(value FolderSize) FolderSize { 61 | a := v + value 62 | return a 63 | } 64 | 65 | // AddSizeProgress accumulate all sizes from SizeProgress with instance size. 66 | func (v FolderSize) AddSizeProgress(value SizeProgress) FolderSize { 67 | var totalDone FolderSize 68 | if value.Completed != nil { 69 | totalDone += *value.Completed 70 | } 71 | if value.Failed != nil { 72 | totalDone += *value.Failed 73 | } 74 | if value.Skipped != nil { 75 | totalDone += *value.Skipped 76 | } 77 | a := v + totalDone 78 | return a 79 | } 80 | 81 | // SizeProgress keeps all sizes which may arise during backup process. 82 | type SizeProgress struct { 83 | // Completed signify successfully backed up size. 84 | Completed *FolderSize 85 | // Skipped signify size that was skipped during backup process. 86 | Skipped *FolderSize 87 | // Failed signify size that was not backed up due to some issues. 88 | Failed *FolderSize 89 | } 90 | 91 | // NewProgressCompleted creates the SizeProgress object 92 | // with the size that was successfully backed up. 93 | func NewProgressCompleted(size FolderSize) SizeProgress { 94 | this := SizeProgress{Completed: &size} 95 | return this 96 | } 97 | 98 | // NewProgressSkipped creates the SizeProgress object 99 | // with the size that was skipped. 100 | func NewProgressSkipped(size FolderSize) SizeProgress { 101 | this := SizeProgress{Skipped: &size} 102 | return this 103 | } 104 | 105 | // NewProgressFailed creates the SizeProgress object 106 | // with the size that was not backed up due to some issues. 107 | func NewProgressFailed(size FolderSize) SizeProgress { 108 | this := SizeProgress{Failed: &size} 109 | return this 110 | } 111 | 112 | // Add combines sizes of two SizeProgress objects. 113 | func (v *SizeProgress) Add(size SizeProgress) { 114 | if size.Completed != nil { 115 | if v.Completed == nil { 116 | v.Completed = size.Completed 117 | } else { 118 | done := *v.Completed + *size.Completed 119 | v.Completed = &done 120 | } 121 | } 122 | if size.Skipped != nil { 123 | if v.Skipped == nil { 124 | v.Skipped = size.Skipped 125 | } else { 126 | done := *v.Skipped + *size.Skipped 127 | v.Skipped = &done 128 | } 129 | } 130 | if size.Failed != nil { 131 | if v.Failed == nil { 132 | v.Failed = size.Failed 133 | } else { 134 | done := *v.Failed + *size.Failed 135 | v.Failed = &done 136 | } 137 | } 138 | } 139 | 140 | // GetTotal gets total SizeProgress size. 141 | func (v *SizeProgress) GetTotal() FolderSize { 142 | var total FolderSize 143 | return total.AddSizeProgress(*v) 144 | } 145 | -------------------------------------------------------------------------------- /core/common.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package core 13 | 14 | import ( 15 | "fmt" 16 | 17 | logger "github.com/d2r2/go-logger" 18 | ) 19 | 20 | // You can manage verbosity of log output 21 | // in the package by changing last parameter value 22 | // (comment/uncomment corresponding lines). 23 | var lg = logger.NewPackageLogger("core", 24 | // logger.DebugLevel, 25 | logger.InfoLevel, 26 | ) 27 | 28 | var f = fmt.Sprintf 29 | -------------------------------------------------------------------------------- /core/dir.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package core 13 | 14 | import ( 15 | "fmt" 16 | "io/ioutil" 17 | "os" 18 | ) 19 | 20 | // DirMetrics keeps metrics defined in 1st pass of folders tree. 21 | // Metrics used lately in heuristic algorithm to find optimal folder tree traverse. 22 | type DirMetrics struct { 23 | // Define depth from root folder. Root folder has Depth = 0. 24 | Depth int 25 | // Total count of all child folders. 26 | ChildrenCount int 27 | // "Size" metric defines summary size of all local files, 28 | // do not include any child folders. 29 | Size *FolderSize 30 | // "Full size" metric, which include all files and 31 | // child folders with their content. 32 | FullSize *FolderSize 33 | // Flag which means, that folder contain special file 34 | // which serves as signal to skip backup this folder. 35 | IgnoreToBackup bool 36 | // Flag which means, that this folder already marked 37 | // as "measured" in traverse path search. 38 | Measured bool 39 | // Type of backup for current folder defined 40 | // as a result of traverse path search. 41 | BackupType FolderBackupType 42 | } 43 | 44 | // Dir is a "tree data structure" to describe folder's tree 45 | // received from the source in 1st pass of backup process to measure 46 | // counts/sizes and to predict time necessary for backup process (ETA). 47 | // https://en.wikipedia.org/wiki/Tree_%28data_structure%29 48 | type Dir struct { 49 | Paths SrcDstPath 50 | Name string 51 | Parent *Dir 52 | Childs []*Dir 53 | Metrics DirMetrics 54 | } 55 | 56 | // BuildDirTree scans and creates Dir object which reflects 57 | // real recursive directory structure defined by file system path 58 | // in paths argument. 59 | func BuildDirTree(paths SrcDstPath, ignoreBackupFileSigName string) (*Dir, error) { 60 | info, err := os.Stat(paths.DestPath) 61 | if err != nil { 62 | return nil, err 63 | } 64 | if !info.IsDir() { 65 | // does not translate this message, since it is very unlikely 66 | return nil, fmt.Errorf("path %q should be a folder", paths.DestPath) 67 | } 68 | root := &Dir{Name: info.Name(), Paths: paths, Metrics: DirMetrics{Depth: 0}} 69 | _, err = createOffsprings(root, paths, ignoreBackupFileSigName, 1) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return root, nil 74 | } 75 | 76 | // GetTotalSize calculates total size of data 77 | // to backup, including all subfolders. 78 | func (v *Dir) GetTotalSize() FolderSize { 79 | // use nested call to make recursive calculations 80 | return getTotalSize(v) 81 | } 82 | 83 | // GetIgnoreSize calculates total size of data 84 | // which marked as "skip to backup" including all subfolders. 85 | func (v *Dir) GetIgnoreSize() FolderSize { 86 | // use nested call to make recursive calculations 87 | return getIgnoreSize(v) 88 | } 89 | 90 | // GetFullBackupSize calculates total size of data 91 | // which marked to backup "full content". 92 | func (v *Dir) GetFullBackupSize() FolderSize { 93 | // use nested call to make recursive calculations 94 | return getFullBackupSize(v) 95 | } 96 | 97 | // GetContentBackupSize calculates total size of data 98 | // which marked to backup "local folder content". 99 | func (v *Dir) GetContentBackupSize() FolderSize { 100 | // use nested call to make recursive calculations 101 | return getContentBackupSize(v) 102 | } 103 | 104 | // GetFoldersCount return total folder count 105 | // in this directory tree. 106 | func (v *Dir) GetFoldersCount() int { 107 | // use nested call to make recursive calculations 108 | return getFoldersCount(v) 109 | } 110 | 111 | // GetFoldersIgnoreCount return total folder count 112 | // to "skip to backup" in this directory tree. 113 | func (v *Dir) GetFoldersIgnoreCount() int { 114 | // use nested call to make recursive calculations 115 | return getFoldersIgnoreCount(v) 116 | } 117 | 118 | /* 119 | func containsMeasuredDir(dir *Dir) bool { 120 | if dir.Metrics.Measured { 121 | return true 122 | } 123 | for _, item := range dir.Childs { 124 | if containsMeasuredDir(item) { 125 | return true 126 | } 127 | } 128 | return false 129 | } 130 | 131 | func containsNonMeasuredDir(dir *Dir) bool { 132 | if !dir.Metrics.Measured { 133 | return true 134 | } 135 | for _, item := range dir.Childs { 136 | if containsNonMeasuredDir(item) { 137 | return true 138 | } 139 | } 140 | return false 141 | } 142 | */ 143 | 144 | func getTotalSize(dir *Dir) FolderSize { 145 | var size FolderSize 146 | if dir.Metrics.BackupType == FBT_CONTENT { 147 | size = *dir.Metrics.Size 148 | } else if dir.Metrics.BackupType == FBT_RECURSIVE { 149 | size = *dir.Metrics.FullSize 150 | } else if dir.Metrics.BackupType == FBT_SKIP { 151 | size = *dir.Metrics.FullSize 152 | } 153 | for _, item := range dir.Childs { 154 | size += getTotalSize(item) 155 | } 156 | return size 157 | } 158 | 159 | func getFullBackupSize(dir *Dir) FolderSize { 160 | var size FolderSize 161 | if dir.Metrics.BackupType == FBT_RECURSIVE { 162 | size = *dir.Metrics.FullSize 163 | } 164 | for _, item := range dir.Childs { 165 | size += getFullBackupSize(item) 166 | } 167 | return size 168 | } 169 | 170 | func getContentBackupSize(dir *Dir) FolderSize { 171 | var size FolderSize 172 | if dir.Metrics.BackupType == FBT_CONTENT { 173 | size = *dir.Metrics.Size 174 | } 175 | for _, item := range dir.Childs { 176 | size += getContentBackupSize(item) 177 | } 178 | return size 179 | } 180 | 181 | func getIgnoreSize(dir *Dir) FolderSize { 182 | var size FolderSize 183 | if dir.Metrics.BackupType == FBT_SKIP { 184 | size = *dir.Metrics.FullSize 185 | } 186 | for _, item := range dir.Childs { 187 | size += getIgnoreSize(item) 188 | } 189 | return size 190 | } 191 | 192 | func getFoldersIgnoreCount(dir *Dir) int { 193 | count := 0 194 | if dir.Metrics.BackupType == FBT_SKIP { 195 | count++ 196 | } 197 | for _, item := range dir.Childs { 198 | count += getFoldersIgnoreCount(item) 199 | } 200 | return count 201 | } 202 | 203 | func getFoldersCount(dir *Dir) int { 204 | count := len(dir.Childs) 205 | for _, item := range dir.Childs { 206 | count += getFoldersCount(item) 207 | } 208 | return count 209 | } 210 | 211 | func createOffsprings(parent *Dir, paths SrcDstPath, 212 | sigFileIgnoreBackup string, depth int) (int, error) { 213 | 214 | // lg.Debug(f("Iterate path: %q", path)) 215 | items, err := ioutil.ReadDir(paths.DestPath) 216 | if err != nil { 217 | return 0, err 218 | } 219 | if sigFileIgnoreBackupFound(items, sigFileIgnoreBackup) { 220 | parent.Metrics.IgnoreToBackup = true 221 | parent.Metrics.ChildrenCount = 1 222 | return 1, nil 223 | } 224 | totalCount := 1 225 | for _, item := range items { 226 | if item.IsDir() { 227 | name := item.Name() 228 | paths2 := paths.Join(name) 229 | dir := &Dir{Parent: parent, Name: name, Paths: paths2, 230 | Metrics: DirMetrics{Depth: depth}} 231 | count, err := createOffsprings(dir, paths2, 232 | sigFileIgnoreBackup, depth+1) 233 | if err != nil { 234 | return 0, err 235 | } 236 | parent.Childs = append(parent.Childs, dir) 237 | totalCount += count 238 | } 239 | } 240 | parent.Metrics.ChildrenCount = totalCount 241 | return totalCount, nil 242 | } 243 | 244 | func sigFileIgnoreBackupFound(items []os.FileInfo, sigFileIgnoreBackup string) bool { 245 | for _, item := range items { 246 | if !item.IsDir() && item.Name() == sigFileIgnoreBackup { 247 | return true 248 | } 249 | } 250 | return false 251 | } 252 | -------------------------------------------------------------------------------- /core/format.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package core 13 | 14 | import ( 15 | "bytes" 16 | "math" 17 | "time" 18 | 19 | "github.com/d2r2/go-rsync/locale" 20 | ) 21 | 22 | // FormatDurationToDaysHoursMinsSecs print time span 23 | // in the format "x1 day(s) x2 hour(s) x3 minute(s) x4 second(s)". 24 | // Understand plural cases for right spellings. Might be limited 25 | // to number of sections to print. 26 | func FormatDurationToDaysHoursMinsSecs(dur time.Duration, short bool, sections *int) string { 27 | var buf bytes.Buffer 28 | var totalHrs float64 = dur.Hours() 29 | days := totalHrs / 24 30 | count := 0 31 | if days >= 1 { 32 | count++ 33 | var a int 34 | if sections == nil || *sections > count { 35 | a = int(days) 36 | } else { 37 | a = int(Round(days)) 38 | } 39 | if short { 40 | buf.WriteString(f("%d %s", a, locale.TP(MsgDaysShort, nil, a))) 41 | } else { 42 | buf.WriteString(f("%d %s", a, locale.TP(MsgDaysLong, nil, a))) 43 | } 44 | } 45 | hours := totalHrs - float64(int(days)*24) 46 | if (hours >= 1 || count > 0) && (sections == nil || *sections > count) { 47 | if count > 0 { 48 | buf.WriteString(" ") 49 | } 50 | count++ 51 | var a int 52 | if sections == nil || *sections > count { 53 | a = int(hours) 54 | } else { 55 | a = int(Round(hours)) 56 | } 57 | if short { 58 | buf.WriteString(f("%d %s", a, locale.TP(MsgHoursShort, nil, a))) 59 | } else { 60 | buf.WriteString(f("%d %s", a, locale.TP(MsgHoursLong, nil, a))) 61 | } 62 | } 63 | var totalSecsLeft float64 = (dur - time.Duration(days)*24*time.Hour - 64 | time.Duration(hours)*time.Hour).Seconds() 65 | minutes := totalSecsLeft / 60 66 | if (minutes > 1 || count > 0) && (sections == nil || *sections > count) { 67 | if count > 0 { 68 | buf.WriteString(" ") 69 | } 70 | count++ 71 | var a int 72 | if sections == nil || *sections > count { 73 | a = int(minutes) 74 | } else { 75 | a = int(Round(minutes)) 76 | } 77 | if short { 78 | buf.WriteString(f("%d %s", a, locale.TP(MsgMinutesShort, nil, a))) 79 | } else { 80 | buf.WriteString(f("%d %s", a, locale.TP(MsgMinutesLong, nil, a))) 81 | } 82 | } 83 | seconds := int(totalSecsLeft - float64(int(minutes)*60)) 84 | if (seconds > 0 || count == 0) && (sections == nil || *sections > count) { 85 | if count > 0 { 86 | buf.WriteString(" ") 87 | } 88 | if short { 89 | buf.WriteString(f("%d %s", seconds, locale.TP(MsgSecondsShort, nil, seconds))) 90 | } else { 91 | buf.WriteString(f("%d %s", seconds, locale.TP(MsgSecondsLong, nil, seconds))) 92 | } 93 | } 94 | return buf.String() 95 | } 96 | 97 | // pluralFloatToInt is doing some workaround how to interpret 98 | // float amounts in context of plural forms. 99 | func pluralFloatToInt(val float64) int { 100 | if val == 1 { 101 | return 1 102 | } else if val < 1 { 103 | return 0 104 | } else if val < 2 { 105 | return 2 106 | } else { 107 | return int(Round(math.Floor(val))) 108 | } 109 | } 110 | 111 | // byte count in corresponding data measurements 112 | const ( 113 | KB = 1000 114 | MB = 1000 * KB 115 | GB = 1000 * MB 116 | TB = 1000 * GB 117 | PB = 1000 * TB 118 | EB = 1000 * PB 119 | ) 120 | 121 | // FormatSize convert byte count amount to human-readable (short) string representation. 122 | func FormatSize(byteCount uint64, short bool) string { 123 | if byteCount > EB { 124 | a := float64(byteCount) / EB 125 | if short { 126 | return f("%v %s", a, 127 | locale.TP(MsgExaBytesShort, nil, pluralFloatToInt(a))) 128 | } else { 129 | return f("%v %s", a, 130 | locale.TP(MsgExaBytesLong, nil, pluralFloatToInt(a))) 131 | } 132 | } else if byteCount > PB { 133 | a := float64(byteCount) / PB 134 | if short { 135 | return f("%v %s", a, 136 | locale.TP(MsgPetaBytesShort, nil, pluralFloatToInt(a))) 137 | } else { 138 | return f("%v %s", a, 139 | locale.TP(MsgPetaBytesLong, nil, pluralFloatToInt(a))) 140 | } 141 | } else if byteCount > TB { 142 | a := float64(byteCount) / TB 143 | if short { 144 | return f("%v %s", a, 145 | locale.TP(MsgTeraBytesShort, nil, pluralFloatToInt(a))) 146 | } else { 147 | return f("%v %s", a, 148 | locale.TP(MsgTeraBytesLong, nil, pluralFloatToInt(a))) 149 | } 150 | } else if byteCount > GB { 151 | a := float64(byteCount) / GB 152 | if short { 153 | return f("%.1f %s", a, 154 | locale.TP(MsgGigaBytesShort, nil, pluralFloatToInt(a))) 155 | } else { 156 | return f("%.1f %s", a, 157 | locale.TP(MsgGigaBytesLong, nil, pluralFloatToInt(a))) 158 | } 159 | } else if byteCount > MB { 160 | a := int(Round(float64(byteCount) / MB)) 161 | if short { 162 | return f("%v %s", a, 163 | locale.TP(MsgMegaBytesShort, nil, a)) 164 | } else { 165 | return f("%v %s", a, 166 | locale.TP(MsgMegaBytesLong, nil, a)) 167 | } 168 | } else if byteCount > KB { 169 | a := int(Round(float64(byteCount) / KB)) 170 | if short { 171 | return f("%v %s", a, 172 | locale.TP(MsgKiloBytesShort, nil, a)) 173 | } else { 174 | return f("%v %s", a, 175 | locale.TP(MsgKiloBytesLong, nil, a)) 176 | } 177 | } else { 178 | a := int(byteCount) 179 | if short { 180 | return f("%v %s", a, 181 | locale.TP(MsgBytesShort, nil, a)) 182 | } else { 183 | return f("%v %s", a, 184 | locale.TP(MsgBytesLong, nil, a)) 185 | } 186 | } 187 | } 188 | 189 | // GetReadableSize convert FolderSize to human readable string representation. 190 | func GetReadableSize(size FolderSize) string { 191 | return FormatSize(size.GetByteCount(), true) 192 | } 193 | -------------------------------------------------------------------------------- /core/info.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package core 13 | 14 | import ( 15 | "fmt" 16 | "runtime" 17 | "strconv" 18 | "time" 19 | 20 | "github.com/d2r2/go-rsync/locale" 21 | "github.com/davecgh/go-spew/spew" 22 | ) 23 | 24 | // AppRunMode signify what happens, when app 25 | // will be closed. With this type standard 26 | // behavior might be changed and app will be 27 | // started again. 28 | type AppRunMode int 29 | 30 | const ( 31 | // AppRegularRun - regular run, app will be closed on exit. 32 | AppRegularRun AppRunMode = iota 33 | // AppRunReload - app will be reinitialized and restarted again. 34 | // This behavior allow to automatically restart app when some 35 | // settings change require app to reload. 36 | AppRunReload 37 | ) 38 | 39 | // contain version+buildnum 40 | // initialized with option: 41 | // -ldflags "-X main.version `head -1 version` -X main.buildnum `date -u +%Y%m%d%H%M%S`" 42 | var ( 43 | _buildnum string 44 | _version string 45 | ) 46 | 47 | // Keep global application run mode. 48 | var _appRunMode AppRunMode 49 | 50 | // SetVersion save application version provided with compile via -ldflags CLI parameter. 51 | func SetVersion(version string) { 52 | _version = version 53 | } 54 | 55 | // SetBuildNum save application build number provided with compile via -ldflags CLI parameter. 56 | func SetBuildNum(buildnum string) { 57 | _buildnum = buildnum 58 | } 59 | 60 | func SetAppRunMode(appRunMode AppRunMode) { 61 | _appRunMode = appRunMode 62 | } 63 | 64 | // Pass in parameter datetime 65 | // from bash expression `date -u +%y%m%d%H%M%S`. 66 | func generateBuildNum() string { 67 | if _, err := strconv.Atoi(_buildnum); err == nil && len(_buildnum) == 14 { 68 | year, _ := strconv.Atoi(_buildnum[0:4]) 69 | month, _ := strconv.Atoi(_buildnum[4:6]) 70 | day, _ := strconv.Atoi(_buildnum[6:8]) 71 | hour, _ := strconv.Atoi(_buildnum[8:10]) 72 | min, _ := strconv.Atoi(_buildnum[10:12]) 73 | sec, _ := strconv.Atoi(_buildnum[12:]) 74 | tm := time.Date(year, time.Month(month), day, hour, min, sec, 0, time.Local) 75 | tm2 := time.Date(2010, time.January, 1, 0, 0, 0, 0, time.Local) 76 | return fmt.Sprintf("%d", (tm.Unix()-tm2.Unix())/30) 77 | } 78 | return _buildnum 79 | } 80 | 81 | func GetAppRunMode() AppRunMode { 82 | return _appRunMode 83 | } 84 | 85 | // GetAppVersion returns string representation of application version. 86 | func GetAppVersion() string { 87 | return spew.Sprintf("v%s", _version) 88 | } 89 | 90 | // GetAppArchitecture1 returns application architecture. 91 | func GetAppArchitecture() string { 92 | return runtime.GOARCH 93 | } 94 | 95 | // GetGolangVersion returns golang version used to compile application. 96 | func GetGolangVersion() string { 97 | return runtime.Version() 98 | } 99 | 100 | // GetAppTitle returns application non-translatable title. 101 | func GetAppTitle() string { 102 | return "Gorsync Backup" 103 | } 104 | 105 | // GetAppExtraTitle returns application translatable extra title. 106 | func GetAppExtraTitle() string { 107 | return locale.T(MsgAppTitleExtra, nil) 108 | } 109 | 110 | // GetAppFullTitle returns application full title. 111 | func GetAppFullTitle() string { 112 | appTitle := GetAppTitle() 113 | appTitleExtra := GetAppExtraTitle() 114 | if appTitleExtra != "" { 115 | appTitle += " " + appTitleExtra 116 | } 117 | return appTitle 118 | } 119 | -------------------------------------------------------------------------------- /core/messagekeys.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package core 13 | 14 | // ------------------------------------------------------------ 15 | // File contains message identifiers for localization purpose. 16 | // Message identifier names is self-descriptive, so ordinary 17 | // it's easy to understand what message is made for. 18 | // Message ID is used to call translation functions from 19 | // "locale" package. 20 | // ------------------------------------------------------------ 21 | 22 | const ( 23 | MsgAppTitleExtra = "AppTitleExtra" 24 | 25 | MsgDaysLong = "DaysLong" 26 | MsgDaysShort = "DaysShort" 27 | MsgHoursLong = "HoursLong" 28 | MsgHoursShort = "HoursShort" 29 | MsgMinutesLong = "MinutesLong" 30 | MsgMinutesShort = "MinutesShort" 31 | MsgSecondsLong = "SecondsLong" 32 | MsgSecondsShort = "SecondsShort" 33 | 34 | MsgBytesLong = "BytesLong" 35 | MsgBytesShort = "BytesShort" 36 | MsgKiloBytesLong = "KiloBytesLong" 37 | MsgKiloBytesShort = "KiloBytesShort" 38 | MsgMegaBytesLong = "MegaBytesLong" 39 | MsgMegaBytesShort = "MegaBytesShort" 40 | MsgGigaBytesLong = "GigaBytesLong" 41 | MsgGigaBytesShort = "GigaBytesShort" 42 | MsgTeraBytesLong = "TeraBytesLong" 43 | MsgTeraBytesShort = "TeraBytesShort" 44 | MsgPetaBytesLong = "PetaBytesLong" 45 | MsgPetaBytesShort = "PetaBytesShort" 46 | MsgExaBytesLong = "ExaBytesLong" 47 | MsgExaBytesShort = "ExaBytesShort" 48 | ) 49 | -------------------------------------------------------------------------------- /core/path.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package core 13 | 14 | import ( 15 | "bytes" 16 | "path" 17 | "path/filepath" 18 | "strings" 19 | ) 20 | 21 | // SrcDstPath link to each other RSYNC source URL 22 | // with destination extra path added to backup folder. 23 | type SrcDstPath struct { 24 | RsyncSourcePath string 25 | DestPath string 26 | } 27 | 28 | // Join fork SrcDstPath with new variant, where 29 | // new "folder" appended to the end of the path. 30 | func (v SrcDstPath) Join(item string) SrcDstPath { 31 | newPathTwin := SrcDstPath{ 32 | RsyncSourcePath: RsyncPathJoin(v.RsyncSourcePath, item), 33 | DestPath: filepath.Join(v.DestPath, item)} 34 | return newPathTwin 35 | } 36 | 37 | // RsyncPathJoin used to join path elements in RSYNC url. 38 | func RsyncPathJoin(elements ...string) string { 39 | // use standard URL separator 40 | const separator = '/' 41 | var buf bytes.Buffer 42 | for _, item := range elements { 43 | buf.WriteString(item) 44 | if buf.Len() > 0 && buf.Bytes()[buf.Len()-1] != separator { 45 | buf.WriteByte(separator) 46 | } 47 | } 48 | return buf.String() 49 | } 50 | 51 | // GetRelativePath cut off root prefix from destPath (if found). 52 | func GetRelativePath(rootDest, destPath string) (string, error) { 53 | rel, err := filepath.Rel(rootDest, destPath) 54 | if err != nil { 55 | return "", err 56 | } 57 | rel = "." + strings.Trim((path.Join(" ", rel, " ")), " ") 58 | return rel, nil 59 | } 60 | 61 | // GetRelativePaths cut off root prefix from multiple paths (if found). 62 | func GetRelativePaths(rootDest string, paths []string) ([]string, error) { 63 | var newPaths []string 64 | for _, p := range paths { 65 | np, err := GetRelativePath(rootDest, p) 66 | if err != nil { 67 | return nil, err 68 | } 69 | newPaths = append(newPaths, np) 70 | } 71 | return newPaths, nil 72 | } 73 | -------------------------------------------------------------------------------- /core/proxylog.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package core 13 | 14 | import ( 15 | "fmt" 16 | 17 | logger "github.com/d2r2/go-logger" 18 | "github.com/davecgh/go-spew/spew" 19 | ) 20 | 21 | // WriteLine is a delegate to describe log output call. 22 | type WriteLine func(line string) error 23 | 24 | // ProxyLog is used to substitute regular log console output 25 | // with output to the file, either to the GUI window. 26 | // ProxyLog implements logger.PackageLog interface which 27 | // provide regular log methods. 28 | type ProxyLog struct { 29 | parent logger.PackageLog 30 | packageName string 31 | packageLen int 32 | timeFormat string 33 | 34 | customWriteLine WriteLine 35 | customLogLevel logger.LogLevel 36 | } 37 | 38 | // Static cast to verify that type implement specific interface 39 | var _ logger.PackageLog = &ProxyLog{} 40 | 41 | func NewProxyLog(parent logger.PackageLog, packageName string, packageLen int, 42 | timeFormat string, writeLine WriteLine, customLogLevel logger.LogLevel) *ProxyLog { 43 | 44 | v := &ProxyLog{parent: parent, packageName: packageName, packageLen: packageLen, 45 | timeFormat: timeFormat, customLogLevel: customLogLevel, 46 | customWriteLine: writeLine} 47 | return v 48 | } 49 | 50 | func (v *ProxyLog) getFormat() logger.FormatOptions { 51 | options := logger.FormatOptions{TimeFormat: v.timeFormat, 52 | LevelLength: logger.LevelShort, PackageLength: v.packageLen} 53 | return options 54 | } 55 | 56 | // Printf implement logger.PackageLog.Printf method. 57 | func (v *ProxyLog) Printf(level logger.LogLevel, format string, args ...interface{}) { 58 | if v.parent != nil { 59 | v.parent.Printf(level, format, args...) 60 | } 61 | if v.customWriteLine != nil && level <= v.customLogLevel { 62 | msg := spew.Sprintf(format, args...) 63 | packageName := v.packageName 64 | out := logger.FormatMessage(v.getFormat(), level, packageName, msg, false) 65 | err := v.customWriteLine(out + fmt.Sprintln()) 66 | if err != nil { 67 | v.parent.Fatal(err) 68 | } 69 | } 70 | } 71 | 72 | // Print implement logger.PackageLog.Print method. 73 | func (v *ProxyLog) Print(level logger.LogLevel, args ...interface{}) { 74 | if v.parent != nil { 75 | v.parent.Print(level, args...) 76 | } 77 | if v.customWriteLine != nil && level <= v.customLogLevel { 78 | msg := fmt.Sprint(args...) 79 | packageName := v.packageName 80 | out := logger.FormatMessage(v.getFormat(), level, packageName, msg, false) 81 | err := v.customWriteLine(out + fmt.Sprintln()) 82 | if err != nil { 83 | v.parent.Fatal(err) 84 | } 85 | } 86 | } 87 | 88 | // Debugf implement logger.PackageLog.Debugf method. 89 | func (v *ProxyLog) Debugf(format string, args ...interface{}) { 90 | v.Printf(logger.DebugLevel, format, args...) 91 | } 92 | 93 | // Debug implement logger.PackageLog.Debug method. 94 | func (v *ProxyLog) Debug(args ...interface{}) { 95 | v.Print(logger.DebugLevel, args...) 96 | } 97 | 98 | // Infof implement logger.PackageLog.Infof method. 99 | func (v *ProxyLog) Infof(format string, args ...interface{}) { 100 | v.Printf(logger.InfoLevel, format, args...) 101 | } 102 | 103 | // Info implement logger.PackageLog.Info method. 104 | func (v *ProxyLog) Info(args ...interface{}) { 105 | v.Print(logger.InfoLevel, args...) 106 | } 107 | 108 | // Notifyf implement logger.PackageLog.Notifyf method. 109 | func (v *ProxyLog) Notifyf(format string, args ...interface{}) { 110 | v.Printf(logger.NotifyLevel, format, args...) 111 | } 112 | 113 | // Notify implement logger.PackageLog.Notify method. 114 | func (v *ProxyLog) Notify(args ...interface{}) { 115 | v.Print(logger.NotifyLevel, args...) 116 | } 117 | 118 | // Warningf implement logger.PackageLog.Warningf method. 119 | func (v *ProxyLog) Warningf(format string, args ...interface{}) { 120 | v.Printf(logger.WarnLevel, format, args...) 121 | } 122 | 123 | // Warnf implement logger.PackageLog.Warnf method. 124 | func (v *ProxyLog) Warnf(format string, args ...interface{}) { 125 | v.Printf(logger.WarnLevel, format, args...) 126 | } 127 | 128 | // Warning implement logger.PackageLog.Warning method. 129 | func (v *ProxyLog) Warning(args ...interface{}) { 130 | v.Print(logger.WarnLevel, args...) 131 | } 132 | 133 | // Warn implement logger.PackageLog.Warn method. 134 | func (v *ProxyLog) Warn(args ...interface{}) { 135 | v.Print(logger.WarnLevel, args...) 136 | } 137 | 138 | // Errorf implement logger.PackageLog.Errorf method. 139 | func (v *ProxyLog) Errorf(format string, args ...interface{}) { 140 | v.Printf(logger.ErrorLevel, format, args...) 141 | } 142 | 143 | // Error implement logger.PackageLog.Error method. 144 | func (v *ProxyLog) Error(args ...interface{}) { 145 | v.Print(logger.ErrorLevel, args...) 146 | } 147 | 148 | // Panicf implement logger.PackageLog.Panicf method. 149 | func (v *ProxyLog) Panicf(format string, args ...interface{}) { 150 | v.Printf(logger.PanicLevel, format, args...) 151 | } 152 | 153 | // Panic implement logger.PackageLog.Panic method. 154 | func (v *ProxyLog) Panic(args ...interface{}) { 155 | v.Print(logger.PanicLevel, args...) 156 | } 157 | 158 | // Fatalf implement logger.PackageLog.Fatalf method. 159 | func (v *ProxyLog) Fatalf(format string, args ...interface{}) { 160 | v.Printf(logger.FatalLevel, format, args...) 161 | } 162 | 163 | // Fatal implement logger.PackageLog.Fatal method. 164 | func (v *ProxyLog) Fatal(args ...interface{}) { 165 | v.Print(logger.FatalLevel, args...) 166 | } 167 | -------------------------------------------------------------------------------- /core/utils.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package core 13 | 14 | import ( 15 | "math" 16 | "regexp" 17 | "strings" 18 | 19 | shell "github.com/d2r2/go-shell" 20 | ) 21 | 22 | // Round returns the nearest integer, rounding ties away from zero. 23 | // This functionality is for "before Go 1.10" period, because 24 | // math.Round() was added only since Go 1.10. 25 | func Round(x float64) float64 { 26 | t := math.Trunc(x) 27 | if math.Abs(x-t) >= 0.5 { 28 | return t + math.Copysign(1, x) 29 | } 30 | return t 31 | } 32 | 33 | // DivMod return integer devision characteristics: quotient and remainder. 34 | func DivMod(numerator, denominator int64) (quotient, remainder int64) { 35 | quotient = numerator / denominator // integer division, decimals are truncated 36 | remainder = numerator % denominator 37 | return 38 | } 39 | 40 | // SplitByEOL normalize "end-of-line" identifiers. 41 | func SplitByEOL(text string) []string { 42 | return strings.Split(strings.Replace(text, "\r\n", "\n", -1), "\n") 43 | } 44 | 45 | // RunExecutableWithExtraVars execute external process returning exit code either any 46 | // error which might happens during start up or execution phases. 47 | func RunExecutableWithExtraVars(pathToApp string, env []string, args ...string) (int, error) { 48 | app := shell.NewApp(pathToApp, args...) 49 | app.AddEnvironments(env) 50 | ec := app.Run(nil, nil) 51 | return ec.ExitCode, ec.Error 52 | } 53 | 54 | // FindStringSubmatchIndexes simplify named Regexp subexpressions extraction via map interface. 55 | // Each entry return 2-byte array with start/end indexes of occurrence. 56 | func FindStringSubmatchIndexes(re *regexp.Regexp, s string) map[string][2]int { 57 | captures := make(map[string][2]int) 58 | ind := re.FindStringSubmatchIndex(s) 59 | names := re.SubexpNames() 60 | for i, name := range names { 61 | if name != "" && i < len(ind)/2 { 62 | if ind[i*2] != -1 && ind[i*2+1] != -1 { 63 | captures[name] = [2]int{ind[i*2], ind[i*2+1]} 64 | } 65 | } 66 | } 67 | return captures 68 | } 69 | -------------------------------------------------------------------------------- /data/assets/ajax-loader-gears_32x32.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/data/assets/ajax-loader-gears_32x32.gif -------------------------------------------------------------------------------- /data/assets/base.css: -------------------------------------------------------------------------------- 1 | /* Standard icon rotation animation. 2 | */ 3 | @keyframes spin { 4 | to { -gtk-icon-transform: rotate(1turn); } 5 | } 6 | 7 | /* Visual Bell Animation, note each frame defined 8 | separately because GTK doesn't support grouping them 9 | together like normal CSS. 10 | Taken with some modifications from open source Tilix project. 11 | */ 12 | @keyframes shake { 13 | 0% {-gtk-icon-transform: translateX(0);} 14 | 10% {-gtk-icon-transform: translateX(0);} 15 | 1% {-gtk-icon-transform: translateX(-2px);} 16 | 3% {-gtk-icon-transform: translateX(-1.5px);} 17 | 5% {-gtk-icon-transform: translateX(-0.9px);} 18 | 7% {-gtk-icon-transform: translateX(-0.6px);} 19 | 9% {-gtk-icon-transform: translateX(-0.3px);} 20 | 2% {-gtk-icon-transform: translateX(2px);} 21 | 4% {-gtk-icon-transform: translateX(1.5px);} 22 | 6% {-gtk-icon-transform: translateX(0.9px);} 23 | 8% {-gtk-icon-transform: translateX(0.6px);} 24 | } 25 | 26 | 27 | /**************** 28 | * Entry * 29 | ****************/ 30 | 31 | entry.entry-image-right-spin image.right { 32 | animation: spin 2s linear infinite; 33 | } 34 | 35 | entry.entry-image-right-error image.right { 36 | color: @error_color; 37 | } 38 | 39 | entry.entry-image-right-warning image.right { 40 | color: @warning_color; 41 | } 42 | 43 | entry.entry-image-right-shake image.right { 44 | animation: shake 8s linear infinite; 45 | } 46 | 47 | 48 | /**************** 49 | * Image * 50 | ****************/ 51 | image.image-spin { 52 | animation: spin 2s linear infinite; 53 | } 54 | 55 | image.image-error { 56 | color: @error_color; 57 | } 58 | 59 | image.image-warning { 60 | color: @warning_color; 61 | } 62 | 63 | image.image-information { 64 | color: @theme_selected_bg_color; 65 | } 66 | 67 | image.image-shake { 68 | animation: shake 10s linear infinite; 69 | } 70 | 71 | 72 | /**************** 73 | * Box * 74 | ****************/ 75 | box.info-panel { 76 | background-color: shade(@theme_bg_color, 0.8); 77 | padding: 10px; 78 | } 79 | 80 | /**************** 81 | * Label * 82 | ****************/ 83 | label.label-index-caption { 84 | /* color: shade(@theme_fg_color, 0.3); */ 85 | color: alpha(@theme_fg_color, 0.2); 86 | } 87 | 88 | /**************** 89 | * Header bar * 90 | ****************/ 91 | headerbar.themed > separator { 92 | margin-right: 8px; 93 | margin-left: 8px; 94 | } 95 | 96 | 97 | /**************** 98 | * Progress bar * 99 | ****************/ 100 | progressbar progress, trough { 101 | min-height: 20px; 102 | } 103 | 104 | @keyframes blink { 105 | 106 | from { background-image: linear-gradient(to top, @theme_bg_color, @progressbar_bg_color); } 107 | 108 | 50% { background-image: linear-gradient(to top, alpha(@theme_bg_color, 0.9), alpha(@theme_bg_color, 0.5)); } 109 | 110 | to { background-image: linear-gradient(to bottom, @progressbar_bg_color, @theme_bg_color); } 111 | 112 | /* 113 | from { background-image: linear-gradient(to top, alpha(@theme_bg_color, 1), alpha(@progressbar_bg_color, 1)); } 114 | 50% { background-image: linear-gradient(to top, alpha(@theme_bg_color, 0.9), alpha(@progressbar_bg_color, 0.1)); } 115 | to { background-image: linear-gradient(to top, alpha(@theme_bg_color, 1), alpha(@progressbar_bg_color, 1)); } 116 | */ 117 | } 118 | 119 | progressbar progress { 120 | 121 | 122 | background-image: linear-gradient(to top, alpha(@theme_bg_color, 0.7), @progressbar_bg_color); 123 | 124 | 125 | 126 | border-radius: 3px; 127 | border-style: solid; 128 | 129 | border-color: @progressbar_border; 130 | 131 | } 132 | 133 | progressbar.run-animation progress { 134 | 135 | animation-name: blink; 136 | 137 | animation-duration: 5s; 138 | animation-timing-function: linear; 139 | animation-iteration-count: infinite; 140 | 141 | } 142 | 143 | /* 144 | progressbar progress { 145 | background-image: linear-gradient(to top, @theme_bg_color, @theme_fg_color); 146 | 147 | border-radius: 3px; 148 | border-style: solid; 149 | border-color: alpha(@progressbar_border, 0.01); 150 | } 151 | */ 152 | 153 | 154 | /* 155 | progressbar trough { 156 | background-color: rgba(255, 255, 255, 255); 157 | } 158 | */ 159 | -------------------------------------------------------------------------------- /data/assets/emblem-important-red.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/data/assets/emblem-important-red.gif -------------------------------------------------------------------------------- /data/assets/emblem-synchronizing-cyan.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/data/assets/emblem-synchronizing-cyan.gif -------------------------------------------------------------------------------- /data/assets/emblem-synchronizing-yellow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/data/assets/emblem-synchronizing-yellow.gif -------------------------------------------------------------------------------- /data/assets/loading_20181001_64x64.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/data/assets/loading_20181001_64x64.gif -------------------------------------------------------------------------------- /data/assets/loading_animated_64x64.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/data/assets/loading_animated_64x64.gif -------------------------------------------------------------------------------- /data/data.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | // +build !gorsync_rel 13 | 14 | package data 15 | 16 | import ( 17 | "net/http" 18 | ) 19 | 20 | // Assets contains project assets. 21 | // Depending on "gorsync_rel" build constraint tag take data 22 | // from file system folder, either from embedded code. 23 | var Assets http.FileSystem = http.Dir("data/assets") 24 | -------------------------------------------------------------------------------- /data/generate/generate.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package main 13 | 14 | import ( 15 | "log" 16 | 17 | "github.com/d2r2/go-rsync/data" 18 | "github.com/shurcooL/vfsgen" 19 | ) 20 | 21 | // This application is used to generate "assets_vfsdata.go" file for application Release compilation. 22 | // In Release mode all data files found in "assets" folder is encapsulated to "assets_vfsdata.go". 23 | 24 | func main() { 25 | err := vfsgen.Generate(data.Assets, vfsgen.Options{ 26 | PackageName: "data", 27 | BuildTags: "gorsync_rel", 28 | VariableName: "Assets", 29 | }) 30 | if err != nil { 31 | log.Fatalln(err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /data/graphics/emblem-important-red.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/data/graphics/emblem-important-red.xcf -------------------------------------------------------------------------------- /data/graphics/emblem-synchronizing-cyan.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/data/graphics/emblem-synchronizing-cyan.xcf -------------------------------------------------------------------------------- /data/graphics/emblem-synchronizing-yellow.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/data/graphics/emblem-synchronizing-yellow.xcf -------------------------------------------------------------------------------- /data/graphics/tape-icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/data/graphics/tape-icon.xcf -------------------------------------------------------------------------------- /docs/gorsync_about_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/docs/gorsync_about_dialog.png -------------------------------------------------------------------------------- /docs/gorsync_about_dialog_v0.3.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/docs/gorsync_about_dialog_v0.3.1.png -------------------------------------------------------------------------------- /docs/gorsync_about_dialog_v0.3.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/docs/gorsync_about_dialog_v0.3.2.png -------------------------------------------------------------------------------- /docs/gorsync_main_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/docs/gorsync_main_form.png -------------------------------------------------------------------------------- /docs/gorsync_main_form_v0.3.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/docs/gorsync_main_form_v0.3.1.png -------------------------------------------------------------------------------- /docs/gorsync_main_form_v0.3.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/docs/gorsync_main_form_v0.3.2.png -------------------------------------------------------------------------------- /docs/gorsync_preference_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/docs/gorsync_preference_dialog.png -------------------------------------------------------------------------------- /docs/gorsync_preference_dialog_v0.3.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/docs/gorsync_preference_dialog_v0.3.1.png -------------------------------------------------------------------------------- /docs/gorsync_preference_dialog_v0.3.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/docs/gorsync_preference_dialog_v0.3.2.png -------------------------------------------------------------------------------- /docs/gorsync_preference_dialog_v0.3.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/docs/gorsync_preference_dialog_v0.3.3.png -------------------------------------------------------------------------------- /gorsync.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package main 13 | 14 | import ( 15 | "bytes" 16 | "flag" 17 | "fmt" 18 | "os" 19 | "runtime" 20 | "runtime/pprof" 21 | 22 | logger "github.com/d2r2/go-logger" 23 | "github.com/d2r2/go-rsync/core" 24 | "github.com/d2r2/go-rsync/locale" 25 | "github.com/d2r2/go-rsync/rsync" 26 | "github.com/d2r2/go-rsync/ui/gtkui" 27 | "github.com/d2r2/gotk3/libnotify" 28 | ) 29 | 30 | const ( 31 | MsgMainAppSubsystemInitialized = "MainAppSubsystemInitialized" 32 | MsgMainAppExitedNormally = "MainAppExitedNormally" 33 | ) 34 | 35 | // You can manage verbosity of log output 36 | // in the package by changing last parameter value 37 | // (comment/uncomment corresponding lines). 38 | var lg = logger.NewPackageLogger("main", 39 | // logger.DebugLevel, 40 | logger.InfoLevel, 41 | ) 42 | 43 | // Contain version+buildnum initialized with option: 44 | // -ldflags "-X main.version `head -1 version` -X main.buildnum `date -u +%Y%m%d%H%M%S`" 45 | var ( 46 | buildnum string 47 | version string 48 | ) 49 | 50 | // main entry 51 | func main() { 52 | lg.Debugf("Version=%v", version) 53 | lg.Debugf("Build number=%v", buildnum) 54 | // Save application version provided in compilation time. 55 | core.SetVersion(version) 56 | core.SetBuildNum(buildnum) 57 | 58 | var cpuprofile string 59 | flag.StringVar(&cpuprofile, "cpuprofile", "", `Write cpu profile to "file" for debugging purpose. 60 | Generate CPU profile for debugging. Use command "go tool pprof --pdf ./cpu.pprof > ./profile.pdf" 61 | to create execution graph in pdf document.`) 62 | var memprofile string 63 | flag.StringVar(&memprofile, "memprofile", "", `Write memory profile to "file" for debugging purpose. 64 | Generate memory profile for debugging. Use command "go tool pprof --pdf ./mem.pprof > ./profile.pdf" 65 | to create memory usage graph in pdf document.`) 66 | var versionFlag bool 67 | flag.BoolVar(&versionFlag, "version", false, `Print environment and version information.`) 68 | 69 | flag.Parse() 70 | 71 | // Activate cpu profiling to trace cpu consumption for debugging purpose. 72 | if cpuprofile != "" { 73 | f, err := os.Create(cpuprofile) 74 | if err != nil { 75 | lg.Fatal("could not create CPU profile: ", err) 76 | } 77 | defer f.Close() 78 | if err := pprof.StartCPUProfile(f); err != nil { 79 | lg.Fatal("could not start CPU profile: ", err) 80 | } 81 | defer pprof.StopCPUProfile() 82 | } 83 | 84 | // Print application version information. 85 | if versionFlag { 86 | localizer := locale.CreateLocalizer("EN") 87 | var b bytes.Buffer 88 | b.WriteString(fmt.Sprintf("\t%s %s\n", core.GetAppFullTitle(), core.GetAppVersion())) 89 | version, protocol, err := rsync.GetRsyncVersion() 90 | if err != nil { 91 | if rsync.IsExtractVersionAndProtocolError(err) { 92 | version = "?" 93 | protocol = version 94 | lg.Warn(err) 95 | } else { 96 | lg.Fatal(err) 97 | } 98 | } 99 | b.WriteString("\t" + localizer.Translate(gtkui.MsgRsyncInfo, struct{ RSYNCDetectedVer, RSYNCDetectedProtocol string }{ 100 | RSYNCDetectedVer: version, RSYNCDetectedProtocol: protocol}) + "\n") 101 | b.WriteString("\t" + localizer.Translate(gtkui.MsgGolangInfo, struct{ GolangVersion, AppArchitecture string }{ 102 | GolangVersion: core.GetGolangVersion(), 103 | AppArchitecture: core.GetAppArchitecture()}) + "\n") 104 | print(b.String()) 105 | os.Exit(0) 106 | } 107 | 108 | // Initialize language by default; later it 109 | // might be reinitialized from application preferences. 110 | locale.SetLanguage("") 111 | 112 | // Initialize libnotify subsystem. 113 | err := libnotify.Init(core.GetAppTitle()) 114 | if err != nil { 115 | lg.Fatal(err) 116 | } 117 | lg.Info(locale.T(MsgMainAppSubsystemInitialized, 118 | struct{ Subsystem string }{Subsystem: "Libnotify"})) 119 | 120 | for { 121 | // Create application. 122 | app, err := gtkui.CreateApp() 123 | if err != nil { 124 | lg.Fatal(err) 125 | } 126 | 127 | // Run application. 128 | app.Run([]string{}) 129 | 130 | // If request was made to reload app, then we re-run app 131 | // without exiting (can be used for changing app UI language). 132 | if core.GetAppRunMode() == core.AppRegularRun { 133 | break 134 | } else if core.GetAppRunMode() == core.AppRunReload { 135 | core.SetAppRunMode(core.AppRegularRun) 136 | } 137 | } 138 | 139 | // Uninitialize libnotify subsystem on application exit. 140 | libnotify.Uninit() 141 | 142 | // Save memory profile to investigate leaked memory. 143 | if memprofile != "" { 144 | f, err := os.Create(memprofile) 145 | if err != nil { 146 | lg.Fatal("could not create memory profile: ", err) 147 | } 148 | defer f.Close() 149 | runtime.GC() // get up-to-date statistics 150 | if err := pprof.WriteHeapProfile(f); err != nil { 151 | lg.Fatal("could not write memory profile: ", err) 152 | } 153 | } 154 | 155 | // Say goodbye. 156 | lg.Info(locale.T(MsgMainAppExitedNormally, nil)) 157 | } 158 | -------------------------------------------------------------------------------- /gorsync_build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Example showing use of getopt detection and use of GNU enhanced getopt 4 | # to handle arguments containing whitespace. 5 | # 6 | # Written in 2004 by Hoylen Sue 7 | # Modified in 2018-2022 by Denis Dyakov 8 | # 9 | # To the extent possible under law, the author(s) have dedicated all copyright and 10 | # related and neighboring rights to this software to the public domain worldwide. 11 | # This software is distributed without any warranty. 12 | # 13 | # You should have received a copy of the CC0 Public Domain Dedication along with this software. 14 | # If not, see . 15 | 16 | PROG=$(basename $0) 17 | VERSION=v0.4 18 | 19 | # Define default values, if parameters not specified 20 | RELEASE_TYPE="Release" 21 | DEV_TYPE="Development" 22 | 23 | # Remove this trap if you are doing your own error detection or don't care about errors 24 | trap "echo $PROG: error encountered: aborted; exit 3" ERR 25 | 26 | #---------------------------------------------------------------- 27 | # Process command line arguments 28 | 29 | ## Define options: trailing colon means has an argument (customize this: 1 of 3) 30 | 31 | SHORT_OPTS=b:t:o:h 32 | LONG_OPTS=buildtype:,tags:,output:,version,race,help 33 | 34 | SHORT_HELP="Usage: ${PROG} [options] arguments 35 | Options: 36 | -b Build type. Release type = ${RELEASE_TYPE}. 37 | -t Build tags. 38 | -o Specify output path to write app binary. 39 | -h Show this help message." 40 | 41 | LONG_HELP="Usage: ${PROG} [options] arguments 42 | Options: 43 | -b | --buildtype Build type. Release type = ${RELEASE_TYPE}. 44 | -t | --tags Build tags. 45 | -o | --output Specify output path to write app binary. 46 | -h | --help Show this help message. 47 | -r | --race Investigate application race conditions. 48 | --version Show version information." 49 | 50 | # Detect if GNU Enhanced getopt is available 51 | 52 | HAS_GNU_ENHANCED_GETOPT= 53 | if getopt -T >/dev/null; then : 54 | else 55 | if [ $? -eq 4 ]; then 56 | HAS_GNU_ENHANCED_GETOPT=yes 57 | fi 58 | fi 59 | 60 | # Run getopt (runs getopt first in `if` so `trap ERR` does not interfere) 61 | 62 | if [ -n "$HAS_GNU_ENHANCED_GETOPT" ]; then 63 | # Use GNU enhanced getopt 64 | if ! getopt --name "$PROG" --long $LONG_OPTS --options $SHORT_OPTS -- "$@" >/dev/null; then 65 | echo "$PROG: usage error (use -h or --help for help)" >&2 66 | exit 2 67 | fi 68 | ARGS=$(getopt --name "$PROG" --long $LONG_OPTS --options $SHORT_OPTS -- "$@") 69 | else 70 | # Use original getopt (no long option names, no whitespace, no sorting) 71 | if ! getopt $SHORT_OPTS "$@" >/dev/null; then 72 | echo "$PROG: usage error (use -h for help)" >&2 73 | exit 2 74 | fi 75 | ARGS=$(getopt $SHORT_OPTS "$@") 76 | fi 77 | eval set -- $ARGS 78 | 79 | ## Process parsed options (customize this: 2 of 3) 80 | 81 | while [ $# -gt 0 ]; do 82 | case "$1" in 83 | -b | --buildtype) BUILD_TYPE="$2"; shift;; 84 | -t | --tags) BUILD_TAGS="$2"; shift;; 85 | -o | --output) OUTPUT="-o $2"; shift;; 86 | -v | --verbose) VERBOSE=true;; 87 | -r | --race) RACE="-race";; 88 | -h | --help) if [ -n "$HAS_GNU_ENHANCED_GETOPT" ] 89 | then echo "$LONG_HELP"; 90 | else echo "$SHORT_HELP"; 91 | fi; exit 1;; 92 | --version) echo "$PROG $VERSION"; exit 1;; 93 | --) shift; break;; # end of options 94 | esac 95 | shift 96 | done 97 | 98 | # Form application version from latest GIT tag/release. 99 | # Extract latest GIT tag. 100 | GIT_TAG=$(git describe --tags --abbrev=0) 101 | # Extract number of commits passed from last GIT release. 102 | COMMITS_AFTER=$(git rev-list ${GIT_TAG}..HEAD --count) 103 | COMMIT_ID=$(git rev-parse --short=7 HEAD) 104 | # Remove 'v' char from tag, if present 105 | [[ ${GIT_TAG:0:1} == "v" ]] && GIT_TAG=${GIT_TAG:1} 106 | # Combine last GIT tag and number of commits since, if applicable, to build application version. 107 | APP_VERSION=$GIT_TAG 108 | [[ "$COMMITS_AFTER" != "0" ]] && APP_VERSION="$GIT_TAG+$(($COMMITS_AFTER))~g$COMMIT_ID" 109 | 110 | shopt -s nocasematch 111 | if [[ "$BUILD_TYPE" == "$RELEASE_TYPE" ]]; then 112 | echo "RELEASE type build in progress..." 113 | go run data/generate/generate.go && mv ./assets_vfsdata.go ./data 114 | # Add extra options here (-s -w), to decrease release binary size, read here https://golang.org/cmd/link/ 115 | go build -v $RACE -ldflags="-X main.version=$APP_VERSION -X main.buildnum=$(date -u +%Y%m%d%H%M%S) -s -w" -tags "gorsync_rel $BUILD_TAGS" $OUTPUT gorsync.go 116 | else 117 | [[ -z "$BUILD_TYPE" ]] || [[ "$BUILD_TYPE" == "$DEV_TYPE" ]] || echo "WARNING: unknown build type provided: $BUILD_TYPE" 118 | echo "DEVELOPMENT type build in progress..." 119 | go build -v $RACE -ldflags="-X main.version=$APP_VERSION -X main.buildnum=$(date -u +%Y%m%d%H%M%S)" -tags "$BUILD_TAGS" $OUTPUT gorsync.go 120 | fi 121 | shopt -u nocasematch 122 | 123 | -------------------------------------------------------------------------------- /gorsync_run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ./gorsync_build.sh $@ 4 | [ $? -eq 0 ] && ./gorsync || exit $? 5 | -------------------------------------------------------------------------------- /locale/common.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package locale 13 | 14 | import ( 15 | logger "github.com/d2r2/go-logger" 16 | ) 17 | 18 | // You can manage verbosity of log output 19 | // in the package by changing last parameter value 20 | // (comment/uncomment corresponding lines). 21 | var lg = logger.NewPackageLogger("locale", 22 | // logger.DebugLevel, 23 | logger.InfoLevel, 24 | ) 25 | -------------------------------------------------------------------------------- /locale/localization.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package locale 13 | 14 | import ( 15 | "io/ioutil" 16 | "os" 17 | "strings" 18 | "sync" 19 | 20 | "github.com/BurntSushi/toml" 21 | "github.com/d2r2/go-rsync/data" 22 | "github.com/nicksnyder/go-i18n/v2/i18n" 23 | "golang.org/x/text/language" 24 | ) 25 | 26 | // Localizer is a stub to get access to *i18n.Localizer. 27 | // As it found, *i18n.Localizer is not a thread safe, 28 | // so sync.Mutex must be used to synchronize calls to object internals. 29 | type Localizer struct { 30 | sync.Mutex 31 | syncCalls bool 32 | localizer *i18n.Localizer 33 | Lang string 34 | } 35 | 36 | // substituteLang change empty language "" with system defined. 37 | func substituteLang(lang string) string { 38 | if lang == "" { 39 | lang = os.Getenv("LANG") 40 | // remove ".UTF-8" suffix from language if found, as "en-US.UTF-8" 41 | if i := strings.Index(lang, ".UTF-8"); i != -1 { 42 | lang = lang[:i] 43 | } 44 | } 45 | return lang 46 | } 47 | 48 | // CreateLocalizer create localizer object to generate text messages. 49 | func CreateLocalizer(lang string) *Localizer { 50 | bundle := i18n.NewBundle(language.English) 51 | bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) 52 | mustParseMessageFile(bundle, "translate.en.toml") 53 | mustParseMessageFile(bundle, "translate.ru.toml") 54 | 55 | //Localizer = i18n.NewLocalizer(bundle, "en-US") 56 | localizer := i18n.NewLocalizer(bundle, lang) 57 | // Test translation 58 | // fmt.Println(Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "HelloWorld"})) 59 | v := &Localizer{localizer: localizer, Lang: lang, syncCalls: false} 60 | return v 61 | } 62 | 63 | // Translate form and output a message based on messageID and template configuration. 64 | func (v *Localizer) Translate(messageID string, template interface{}) string { 65 | if v.syncCalls { 66 | v.Lock() 67 | defer v.Unlock() 68 | } 69 | 70 | // get localized message 71 | msg := v.localizer.MustLocalize(&i18n.LocalizeConfig{ 72 | MessageID: messageID, 73 | TemplateData: template}) 74 | return msg 75 | } 76 | 77 | // TranslatePlural form and output a message based on messageID, template and pluralCount configuration. 78 | func (v *Localizer) TranslatePlural(messageID string, template interface{}, 79 | pluralCount interface{}) string { 80 | 81 | if v.syncCalls { 82 | v.Lock() 83 | defer v.Unlock() 84 | } 85 | 86 | // get localized message 87 | msg := v.localizer.MustLocalize(&i18n.LocalizeConfig{ 88 | MessageID: messageID, 89 | TemplateData: template, 90 | PluralCount: pluralCount}) 91 | return msg 92 | } 93 | 94 | // GlobalLocalizer is a global variable to translate everything in application 95 | var GlobalLocalizer *Localizer 96 | 97 | // One of 2 main methods to translate message ID text, using format 98 | // functionality based on template interface. 99 | var T = func(messageID string, template interface{}) string { 100 | // if Localizer isn't initialized, set up with system language 101 | if GlobalLocalizer == nil { 102 | SetLanguage("") 103 | } 104 | // get localized message 105 | return GlobalLocalizer.Translate(messageID, template) 106 | } 107 | 108 | // One of 2 main methods to translate message ID text, using format 109 | // functionality based on template interface. Extra functionality 110 | // allow to control plural form behavior. 111 | var TP = func(messageID string, template interface{}, pluralCount interface{}) string { 112 | // if Localizer isn't initialized, set up with system language 113 | if GlobalLocalizer == nil { 114 | SetLanguage("") 115 | } 116 | // get localized message 117 | return GlobalLocalizer.TranslatePlural(messageID, template, pluralCount) 118 | } 119 | 120 | func mustParseMessageFile(bundle *i18n.Bundle, assetIconName string) { 121 | file, err := data.Assets.Open(assetIconName) 122 | if err != nil { 123 | lg.Fatal(err) 124 | } 125 | defer file.Close() 126 | 127 | buf, err := ioutil.ReadAll(file) 128 | if err != nil { 129 | lg.Fatal(err) 130 | } 131 | 132 | bundle.MustParseMessageFileBytes(buf, assetIconName) 133 | } 134 | 135 | // SetLanguage set up language globally for application localization. 136 | func SetLanguage(lang string) { 137 | lang = substituteLang(lang) 138 | if GlobalLocalizer == nil || GlobalLocalizer.Lang != lang { 139 | GlobalLocalizer = CreateLocalizer(lang) 140 | lg.Info(GlobalLocalizer.Translate(MsgLocaleSetAppLangugeInterface, 141 | struct{ Language string }{Language: GlobalLocalizer.Lang})) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /locale/messagekeys.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package locale 13 | 14 | // ------------------------------------------------------------ 15 | // File contains message identifiers for localization purpose. 16 | // Message identifier names is self-descriptive, so ordinary 17 | // it's easy to understand what message is made for. 18 | // Message ID is used to call translation functions from 19 | // "locale" package. 20 | // ------------------------------------------------------------ 21 | 22 | const ( 23 | MsgLocaleSetAppLangugeInterface = "LocaleSetAppLangugeInterface" 24 | ) 25 | -------------------------------------------------------------------------------- /packaging/archlinux_pkgbuild/gorsync-git/.SRCINFO: -------------------------------------------------------------------------------- 1 | pkgbase = gorsync-git 2 | pkgdesc = GTK+ frontend (backup application) for RSYNC utility. 3 | pkgver = 0.3.3 4 | pkgrel = 1 5 | url = https://github.com/d2r2/go-rsync 6 | install = gorsync-git.install 7 | arch = x86_64 8 | arch = i686 9 | license = GPL3 10 | makedepends = git 11 | makedepends = go 12 | depends = rsync 13 | depends = glib2 14 | depends = gtk3 15 | depends = libnotify 16 | provides = gorsync 17 | source = go-rsync::git+https://github.com/d2r2/go-rsync.git 18 | md5sums = SKIP 19 | 20 | pkgname = gorsync-git 21 | -------------------------------------------------------------------------------- /packaging/archlinux_pkgbuild/gorsync-git/.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/fa441f903154d8159ef71db23d4816d802450fef/ArchLinuxPackages.gitignore 2 | 3 | *.tar 4 | *.tar.* 5 | *.jar 6 | *.exe 7 | *.msi 8 | *.zip 9 | *.tgz 10 | *.log 11 | *.log.* 12 | *.sig 13 | 14 | pkg/ 15 | src/ 16 | go-rsync/ 17 | 18 | -------------------------------------------------------------------------------- /packaging/archlinux_pkgbuild/gorsync-git/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Denis Dyakov 2 | 3 | pkgname=gorsync-git 4 | _pkgname=go-rsync 5 | # It's not a problem that we have legacy version in pkgver, 6 | # it will be updated in the building process to the actual. 7 | # But better keep this value actual, since pkgver or pkgrel 8 | # variables are used to notify all users that an upgrade is needed: 9 | # https://wiki.archlinux.org/index.php/AUR_submission_guidelines 10 | pkgver=0.4.1 11 | epoch= 12 | pkgrel=1 13 | pkgdesc="GTK+ frontend (backup application) for RSYNC utility." 14 | arch=('x86_64' 'i686') 15 | url="https://github.com/d2r2/go-rsync" 16 | license=('GPL3') 17 | makedepends=('git' 'go') 18 | depends=('rsync' 'glib2' 'gtk3' 'libnotify') 19 | provides=('gorsync') 20 | install="${pkgname}.install" 21 | source=("${_pkgname}::git+https://github.com/d2r2/go-rsync.git") 22 | md5sums=('SKIP') 23 | 24 | # Get the tag of the commit to use 25 | # Separated out to allow for `makepkg -e` not running prepare() 26 | _get_tag() { 27 | _tag=$(git tag --list | grep '^v' | grep -v alpha | tail -n1) 28 | # echo "Selected git tag: $_tag" >&2 # To STDERR as called from pkgver() 29 | } 30 | 31 | # Get desktop file content to register application in 32 | # Linux GUI environment. 33 | _get_desktop_entry_file() 34 | { 35 | local EMBEDDED 36 | EMBEDDED=$(cat << EndOfMsg 37 | [Desktop Entry] 38 | Name=Gorsync Backup 39 | Comment=Easy-to-use backup app based on Rsync console utility 40 | Exec=gorsync 41 | Icon=media-tape-symbolic 42 | Type=Application 43 | Encoding=UTF-8 44 | Terminal=false 45 | Categories=GNOME;GTK;Utility;System; 46 | Keywords=backup;rsync; 47 | EndOfMsg 48 | ) 49 | echo "${EMBEDDED}" 50 | } 51 | 52 | # prepare() { 53 | # cd "${srcdir}/${_pkgname}" 54 | # _get_tag 55 | # git reset --hard "${_tag}" 56 | # } 57 | 58 | pkgver() { 59 | cd "${srcdir}/${_pkgname}" 60 | _get_tag 61 | # Example: v2.1.0-beta-3 -> 2.1.0.beta.r3 62 | # Version specification: https://github.com/robert7/nixnote2/issues/28 63 | # echo "$_tag" | sed -E 's/^v//;s/-?([0-9]+)$/.r\1/;s/-/./' 64 | echo "${_tag}" | sed 's/^v//;s/-/./g' 65 | } 66 | 67 | build() { 68 | rm -rf "${srcdir}/.go/src" 69 | mkdir -p "${srcdir}/.go/src/github.com/d2r2" 70 | # export GOPATH="${srcdir}/.go" 71 | mv "${srcdir}/${_pkgname}" "${srcdir}/.go/src/github.com/d2r2/" 72 | cd "${srcdir}/.go/src/github.com/d2r2/${_pkgname}/" 73 | _get_tag 74 | # echo "Tag ${_tag}" 75 | GOPATH="${srcdir}/.go" go get -v -u ./... && \ 76 | git reset --hard "${_tag}" && \ 77 | GOPATH="${srcdir}/.go" ./gorsync_build.sh --buildtype Release 78 | } 79 | 80 | package() { 81 | _binname="gorsync" 82 | # echo "Working dir $(pwd)" 83 | # echo "Srcdir dir ${srcdir}" 84 | cd "${srcdir}/.go/src/github.com/d2r2/${_pkgname}" 85 | install -Dm755 "${_binname}" "${pkgdir}/usr/bin/${_binname}" 86 | _appsharedir="${pkgdir}/usr/share/applications" 87 | mkdir -p "${_appsharedir}" && \ 88 | echo "$(_get_desktop_entry_file)" > "${_appsharedir}/gorsync.desktop" && \ 89 | chmod 644 "${_appsharedir}/gorsync.desktop" 90 | install -Dm644 "ui/gtkui/gsettings/org.d2r2.gorsync.gschema.xml" "$pkgdir/gsettings/org.d2r2.gorsync.gschema.xml" 91 | # install -Dm644 "LICENSE" "$pkgdir/usr/share/licenses/${_pkgname}/LICENSE" 92 | } 93 | 94 | -------------------------------------------------------------------------------- /packaging/archlinux_pkgbuild/gorsync-git/gorsync-git.install: -------------------------------------------------------------------------------- 1 | post_install() { 2 | : 3 | #!/usr/bin/env sh 4 | 5 | # !!! This script is a part of distribution packaging system !!! 6 | # !!! Each line of this script was tested and debugged on Linux 32bit, Linux 64bit, FreeBSD !!! 7 | # !!! Change with great care, do not break it !!! 8 | 9 | SCHEMA_FILE="org.d2r2.gorsync.gschema.xml" 10 | 11 | get_gsettings_schema_file() 12 | { 13 | local EMBEDDED 14 | # ***** !!!!! DO NOT REMOVE THIS COMMENT BLOCK - HEREDOC WILL BE POSTED HERE !!!!! ***** 15 | # AUTOMATICALLY_REPLACED_WITH_EMBEDDED_XML_FILE_DECLARATION 16 | # ***** !!!!! DO NOT REMOVE THIS COMMENT BLOCK - HEREDOC WILL BE POSTED HERE !!!!! ***** 17 | if [ ${#EMBEDDED} -le 0 ]; then 18 | cat "gsettings/${SCHEMA_FILE}" 19 | else 20 | echo "${EMBEDDED}" 21 | fi 22 | } 23 | 24 | 25 | # if [ -z "$1" ]; then 26 | PREFIX=/usr 27 | OS_LOWERCASE=$(echo "$OSTYPE" | tr "[:upper:]" "[:lower:]") 28 | # FreeBSD 29 | if [ "$OS_LOWERCASE" = "freebsd" ]; then 30 | PREFIX="${PREFIX}/local" 31 | # Linux OS 32 | # elif [[ "$OSTYPE" == "linux-gnu" ]]; then 33 | # Mac OSX 34 | # elif [[ "$OSTYPE" == "darwin"* ]]; then 35 | # POSIX compatibility layer and Linux environment emulation for Windows 36 | # elif [[ "$OSTYPE" == "cygwin" ]]; then 37 | # Lightweight shell and GNU utilities compiled for Windows (part of MinGW) 38 | # elif [[ "$OSTYPE" == "msys" ]]; then 39 | # Windows 40 | # elif [[ "$OSTYPE" == "win32" ]]; then 41 | # else 42 | # Unknown. 43 | fi 44 | # else 45 | # export PREFIX=$1 46 | # fi 47 | 48 | if [ "$(id -u)" != "0" ]; then 49 | # Make sure only root can run our script 50 | echo "This script must be run as root" 1>&2 51 | exit 1 52 | fi 53 | 54 | # Check availability of required commands 55 | # COMMANDS="install glib-compile-schemas glib-compile-resources msgfmt desktop-file-validate gtk-update-icon-cache" 56 | COMMANDS="install glib-compile-schemas glib-compile-resources msgfmt desktop-file-validate gtk-update-icon-cache" 57 | # if [ "$PREFIX" = '/usr' ] || [ "$PREFIX" = "/usr/local" ]; then 58 | # COMMANDS="$COMMANDS xdg-desktop-menu" 59 | # fi 60 | # PACKAGES="coreutils glib2 glib2 gettext desktop-file-utils gtk-update-icon-cache xdg-utils" 61 | PACKAGES="coreutils glib2 glib2 gettext desktop-file-utils gtk-update-icon-cache xdg-utils" 62 | i=0 63 | for COMMAND in $COMMANDS; do 64 | type $COMMAND >/dev/null 2>&1 || { 65 | j=0 66 | for PACKAGE in $PACKAGES; do 67 | if [ $i = $j ]; then 68 | break 69 | fi 70 | j=$(( $j + 1 )) 71 | done 72 | echo "Your system is missing command $COMMAND, please install $PACKAGE" 73 | exit 1 74 | } 75 | i=$(( $i + 1 )) 76 | done 77 | 78 | SCHEMA_PATH=${PREFIX}/share/glib-2.0/schemas 79 | echo "Installing gsettings schema to ${SCHEMA_PATH}" 80 | 81 | # Copy and compile schema 82 | echo "Copying and compiling schema..." 83 | install -d ${SCHEMA_PATH} 84 | # install -m 644 gsettings/${SCHEMA_FILE} ${SCHEMA_PATH}/ 85 | echo "$(get_gsettings_schema_file)" > ${SCHEMA_PATH}/${SCHEMA_FILE} 86 | chmod 0644 ${SCHEMA_PATH}/${SCHEMA_FILE} 87 | # Redirect output to /dev/null help on some linux distributions (redhat), which produce 88 | # lot of warnings about "Schema ... are depricated." not related to application. 89 | glib-compile-schemas ${SCHEMA_PATH}/ 2>/dev/null 90 | 91 | } 92 | 93 | 94 | pre_remove() { 95 | : 96 | #!/usr/bin/env sh 97 | 98 | # !!! This script is a part of distribution packaging system !!! 99 | # !!! Each line of this script was tested and debugged on Linux 32bit, Linux 64bit, FreeBSD !!! 100 | # !!! Change with great care, do not break it !!! 101 | 102 | # if [ -z "$1" ]; then 103 | PREFIX=/usr 104 | 105 | # FreeBSD 106 | if [ $(echo "$OSTYPE" | tr "[:upper:]" "[:lower:]") == "freebsd" ]; then 107 | PREFIX="${PREFIX}/local" 108 | # Linux OS 109 | # elif [[ "$OSTYPE" == "linux-gnu" ]]; then 110 | # Mac OSX 111 | # elif [[ "$OSTYPE" == "darwin"* ]]; then 112 | # POSIX compatibility layer and Linux environment emulation for Windows 113 | # elif [[ "$OSTYPE" == "cygwin" ]]; then 114 | # Lightweight shell and GNU utilities compiled for Windows (part of MinGW) 115 | # elif [[ "$OSTYPE" == "msys" ]]; then 116 | # Windows 117 | # elif [[ "$OSTYPE" == "win32" ]]; then 118 | # else 119 | # Unknown. 120 | fi 121 | # else 122 | # export PREFIX=$1 123 | # fi 124 | 125 | if [ "$(id -u)" != "0" ]; then 126 | # Make sure only root can run our script 127 | echo "This script must be run as root" 1>&2 128 | exit 1 129 | fi 130 | 131 | SCHEMA_PATH=${PREFIX}/share/glib-2.0/schemas 132 | echo "Uninstalling gsettings schema from ${SCHEMA_PATH}" 133 | 134 | rm ${SCHEMA_PATH}/org.d2r2.gorsync.gschema.xml 135 | # Redirect output to /dev/null help on some linux distributions (redhat), which produce 136 | # lot of warnings about "Schema ... are depricated." not related to application. 137 | glib-compile-schemas ${SCHEMA_PATH}/ 2>/dev/null 138 | 139 | } 140 | -------------------------------------------------------------------------------- /packaging/archlinux_pkgbuild/prepare_aur_pkgbuild.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Read manuals to understand how to build aur package in Arch Linux: 4 | # https://wiki.archlinux.org/index.php/Arch_User_Repository 5 | # https://wiki.archlinux.org/index.php/Arch_package_guidelines 6 | # https://wiki.archlinux.org/index.php/Creating_packages 7 | # https://wiki.archlinux.org/index.php/PKGBUILD 8 | # https://wiki.archlinux.org/index.php/Makepkg 9 | # https://wiki.archlinux.org/index.php/AUR_submission_guidelines 10 | # Examples aur packages with go sources: 11 | # https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=gometalinter-git 12 | # https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=vim-go 13 | # https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=gotags-git 14 | 15 | TEMP_DIR=$(mktemp -d) 16 | SAVE_DIR="${PWD}" 17 | 18 | git clone ssh://aur@aur.archlinux.org/gorsync-git.git "${TEMP_DIR}" 19 | #cp -R ./gorsync-git $TEMP_DIR 20 | cp ./gorsync-git/PKGBUILD "${TEMP_DIR}/" 21 | cp ./gorsync-git/gorsync-git.install "${TEMP_DIR}/" 22 | cd "${TEMP_DIR}" 23 | makepkg --printsrcinfo > .SRCINFO 24 | echo "go to ${TEMP_DIR} and run makepkg..." 25 | -------------------------------------------------------------------------------- /packaging/build_packages/create_archive_source_package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # !!! This script is a part of distribution packaging system !!! 4 | # !!! Change with great care, do not break it !!! 5 | 6 | 7 | get_make_shell_script() 8 | { 9 | local EMBEDDED 10 | EMBEDDED=$(cat << EndOfMsg 11 | #!/usr/bin/env sh 12 | make -C ./src/github.com/d2r2/go-rsync 13 | EndOfMsg 14 | ) 15 | echo "${EMBEDDED}" 16 | } 17 | 18 | 19 | echo "****************************************************************************************************" 20 | echo "This script will convert project sources to source archive file, embedding all external dependencies" 21 | echo "into the single file. This will preserve application sources release consistency and allow to compile" 22 | echo "application binary at any time later, without necessity to download dependencies." 23 | echo "****************************************************************************************************" 24 | 25 | APP_NAME='gorsync' 26 | # Form application version from latest GIT tag/release. 27 | # Extract latest GIT tag. 28 | GIT_TAG=$(git describe --tags --abbrev=0) 29 | # Extract number of commits passed from last GIT release. 30 | COMMITS_AFTER=$(git rev-list ${GIT_TAG}..HEAD --count) 31 | # Remove 'v' char from tag, if present 32 | [[ ${GIT_TAG:0:1} == "v" ]] && GIT_TAG=${GIT_TAG:1} 33 | # Combine last GIT tag and number of commits since, if applicable, to build application version. 34 | APP_VERSION=$GIT_TAG 35 | # Add extra 1 to increment build number (to start index from 1). 36 | [[ "$COMMITS_AFTER" != "0" ]] && APP_VERSION="$GIT_TAG-$(($COMMITS_AFTER+1))" 37 | 38 | 39 | TEMP_DIR=$(mktemp -d) 40 | 41 | CURDIR=$PWD 42 | # echo ${CURDIR} 43 | PROJECT_PATH="${CURDIR%/*/*}" 44 | # echo $PROJECT_PATH 45 | GOCODE_PATH="${PROJECT_PATH%/*/*/*/*}" 46 | 47 | PROJECT_SUBPATH="${PROJECT_PATH##$GOCODE_PATH}" 48 | # echo $PROJECT_SUBPATH 49 | GOCODE_SRC_SUBPATH="${PROJECT_SUBPATH%/*}" 50 | # echo $GOCODE_SRC_SUBPATH 51 | 52 | echo "Copying project sources to: ${TEMP_DIR}${GOCODE_SRC_SUBPATH}..." 53 | mkdir -p $TEMP_DIR$GOCODE_SRC_SUBPATH 54 | rsync -avrq --exclude packaging/build_packages/packages $PROJECT_PATH $TEMP_DIR$GOCODE_SRC_SUBPATH 55 | # cp ./Makefile $TEMP_DIR$PROJECT_SUBPATH 56 | SCRIPT_NAME=make_app_from_archive_source.sh 57 | echo "$(get_make_shell_script)" > ${TEMP_DIR}/${SCRIPT_NAME} 58 | chmod +x ${TEMP_DIR}/${SCRIPT_NAME} 59 | rm -R $TEMP_DIR$PROJECT_SUBPATH/packaging/build_packages/packages 2>/dev/null 60 | rm $TEMP_DIR$PROJECT_SUBPATH/gorsync 2>/dev/null 61 | # echo $TEMP_DIR$GOCODE_SRC_SUBPATH 62 | 63 | GOPATH=$TEMP_DIR 64 | GOBIN=$TEMP_DIR/bin 65 | echo "Installing govendor tool..." 66 | GOVENDORURL=github.com/kardianos/govendor 67 | go get -u $GOVENDORURL 68 | go install $GOVENDORURL 69 | # echo $TEMP_DIR$PROJECT_SUBPATH 70 | cd $TEMP_DIR$PROJECT_SUBPATH 71 | $GOBIN/govendor init 72 | # $GOBIN/govendor list # uncomment for debugging purpose 73 | echo "Converting project missing or external packages to embedded vendor packages, using govendor tool..." 74 | $GOBIN/govendor fetch -v +outside 75 | cd $CURDIR 76 | echo "Removing govendor tool..." 77 | go clean -i $GOVENDORURL 78 | rm -R --force ${TEMP_DIR}/src/$(dirname $GOVENDORURL) 79 | 80 | for dir in $TEMP_DIR/*/ $TEMP_DIR/.[^.]*/ ; do 81 | # echo "$dir" 82 | last_dir=$(basename $dir) 83 | if [ "$last_dir" != "src" ]; then 84 | rm -R --force $dir 85 | fi 86 | done 87 | 88 | 89 | ARCHIVE_PATH="./packages" 90 | ARCHIVE_SOURCE=${APP_NAME}_${APP_VERSION}.tar.gz 91 | echo "Building archive ${ARCHIVE_PATH}/${ARCHIVE_SOURCE}..." 92 | mkdir -p ./packages 93 | rm ./packages/${ARCHIVE_SOURCE} 2>/dev/null 94 | tar cfz ./packages/${ARCHIVE_SOURCE} -C ${TEMP_DIR} ./ 95 | 96 | echo "...done." 97 | -------------------------------------------------------------------------------- /packaging/build_packages/create_distrib_packages_with_fpm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Example showing use of getopt detection and use of GNU enhanced getopt 4 | # to handle arguments containing whitespace. 5 | # 6 | # Written in 2004 by Hoylen Sue 7 | # Modified in 2018 by Denis Dyakov 8 | # 9 | # To the extent possible under law, the author(s) have dedicated all copyright and 10 | # related and neighboring rights to this software to the public domain worldwide. 11 | # This software is distributed without any warranty. 12 | # 13 | # You should have received a copy of the CC0 Public Domain Dedication along with this software. 14 | # If not, see . 15 | 16 | # Install in advance 'fmp' application according to: https://fpm.readthedocs.io/en/latest/installing.html 17 | 18 | # !!! This script is a part of distribution packaging system !!! 19 | # !!! Script work together with gs_schema_install.sh/..._uninstall.sh to [de]install app GLIB schema file !!! 20 | # !!! Change with great care, do not break it !!! 21 | 22 | # Get desktop file content to register application in 23 | # Linux GUI environment. 24 | get_desktop_entry_file() 25 | { 26 | local EMBEDDED 27 | EMBEDDED=$(cat << EndOfMsg 28 | [Desktop Entry] 29 | Name=Gorsync Backup 30 | Comment=Easy-to-use backup app based on Rsync console utility 31 | Exec=gorsync 32 | Icon=media-tape-symbolic 33 | Type=Application 34 | Encoding=UTF-8 35 | Terminal=false 36 | Categories=GNOME;GTK;Utility;System; 37 | Keywords=backup;rsync; 38 | EndOfMsg 39 | ) 40 | echo "${EMBEDDED}" 41 | } 42 | 43 | PROG=$(basename $0) 44 | VERSION=v0.3 45 | 46 | # Remove this trap if you are doing your own error detection or don't care about errors 47 | trap "echo $PROG: error encountered: aborted; exit 3" ERR 48 | 49 | #---------------------------------------------------------------- 50 | # Process command line arguments 51 | 52 | ## Define options: trailing colon means has an argument (customize this: 1 of 3) 53 | 54 | SHORT_OPTS=h 55 | LONG_OPTS=version,skip-app-build,help,version 56 | 57 | SHORT_HELP="Usage: ${PROG} [options] arguments 58 | Options: 59 | -h Show this help message." 60 | 61 | LONG_HELP="Usage: ${PROG} [options] arguments 62 | Options: 63 | --help Show this help message. 64 | --skip-app-build Skip compiling of application itself. 65 | --version Show version information." 66 | 67 | # Detect if GNU Enhanced getopt is available 68 | 69 | HAS_GNU_ENHANCED_GETOPT= 70 | if getopt -T >/dev/null; then : 71 | else 72 | if [ $? -eq 4 ]; then 73 | HAS_GNU_ENHANCED_GETOPT=yes 74 | fi 75 | fi 76 | 77 | # Run getopt (runs getopt first in `if` so `trap ERR` does not interfere) 78 | 79 | if [ -n "$HAS_GNU_ENHANCED_GETOPT" ]; then 80 | # Use GNU enhanced getopt 81 | if ! getopt --name "$PROG" --long $LONG_OPTS --options $SHORT_OPTS -- "$@" >/dev/null; then 82 | echo "$PROG: usage error (use -h or --help for help)" >&2 83 | exit 2 84 | fi 85 | ARGS=$(getopt --name "$PROG" --long $LONG_OPTS --options $SHORT_OPTS -- "$@") 86 | else 87 | # Use original getopt (no long option names, no whitespace, no sorting) 88 | if ! getopt $SHORT_OPTS "$@" >/dev/null; then 89 | echo "$PROG: usage error (use -h for help)" >&2 90 | exit 2 91 | fi 92 | ARGS=$(getopt $SHORT_OPTS "$@") 93 | fi 94 | eval set -- $ARGS 95 | 96 | ## Process parsed options (customize this: 2 of 3) 97 | 98 | while [ $# -gt 0 ]; do 99 | case "$1" in 100 | --skip-app-build) SKIP_APP_BUILD=true;; 101 | -h | --help) if [ -n "$HAS_GNU_ENHANCED_GETOPT" ] 102 | then echo "$LONG_HELP"; 103 | else echo "$SHORT_HELP"; 104 | fi; exit 1;; 105 | --version) echo "$PROG $VERSION"; exit 1;; 106 | --) shift; break;; # end of options 107 | esac 108 | shift 109 | done 110 | 111 | echo "************************************************************************" 112 | echo "This script will generate linux distribution packages using fpm utility." 113 | echo "You must install fpm in advance to use this script:" 114 | echo "https://fpm.readthedocs.io/en/latest/installing.html" 115 | echo "************************************************************************" 116 | 117 | 118 | TEMPDIR=/tmp/gorsync_build_app 119 | DISTRIB=distrib 120 | SCRIPTS=scripts 121 | APP_NAME='gorsync' 122 | APP_URL="https://gorsync.github.io" 123 | AUTHOR="Denis Dyakov " 124 | LICENSE="GPL3" 125 | 126 | echo -e "Use path to keep data for package generation: ${TEMPDIR}\n" 127 | 128 | systems=( \ 129 | # for Archlinux 130 | "ARCHLINUX" \ 131 | # for Debian, Ubuntu 132 | "DEBIAN" \ 133 | # for Redhat, Centos 134 | "REDHAT" \ 135 | # for FreeBSD 136 | "FREEBSD") 137 | prefixes=( \ 138 | # for Archlinux 139 | "usr" \ 140 | # for Debian, Ubuntu 141 | "usr" \ 142 | # for Redhat, Centos 143 | "usr" \ 144 | # for FreeBSD 145 | "usr/local") 146 | fpm_packages=( \ 147 | # for Archlinux 148 | "pacman" \ 149 | # for Debian, Ubuntu 150 | "deb" \ 151 | # for Redhat, Centos 152 | "rpm" \ 153 | # for FreeBSD 154 | "freebsd") 155 | warnings=( \ 156 | # for Archlinux 157 | "" \ 158 | # for Debian, Ubuntu 159 | "" \ 160 | # for Redhat, Centos 161 | "" \ 162 | # for FreeBSD 163 | "Application binary '${APP_NAME}' generated on Linux is not valid for FreeBSD.\n\tBuild it natively on FreeBSD, substitute and re-run with --skip-app-build option.") 164 | fpm_dependencies=( \ 165 | # for Archlinux 166 | "--depends rsync --depends glib2 --depends gtk3 --depends libnotify" \ 167 | # for Debian, Ubuntu 168 | "--depends rsync --depends libglib2.0-dev --depends libgtk-3-dev --depends libnotify-dev" \ 169 | # for Redhat, Centos 170 | "--depends rsync --depends glib2-devel --depends gtk3 --depends libnotify-devel" \ 171 | # for FreeBSD 172 | "--depends rsync --depends glib --depends gtk3 --depends libnotify") 173 | 174 | # rm -R $TEMPDIR >/dev/null 2>&1 175 | 176 | for ((i=0; i<${#systems[@]};++i)) 177 | do 178 | echo "Start packaging ${systems[i]}..." 179 | 180 | mkdir -p $TEMPDIR/${systems[i]}/$DISTRIB/${prefixes[i]}/bin 181 | mkdir -p $TEMPDIR/${systems[i]}/$DISTRIB/${prefixes[i]}/share/applications 182 | mkdir -p $TEMPDIR/${systems[i]}/$SCRIPTS 183 | 184 | SAVE_DIR="${PWD}" 185 | cd ../.. 186 | PARENT_DIR="${PWD}" 187 | 188 | cp "$PARENT_DIR/ui/gtkui/gs_schema_install.sh" "$TEMPDIR/${systems[i]}/$SCRIPTS" 189 | # Prepare and embed xml file as HEREDOC into the gs_schema_install.sh 190 | XML_SCHEMA=$(cat $PARENT_DIR/ui/gtkui/gsettings/org.d2r2.gorsync.gschema.xml) 191 | XML_SCHEMA="${XML_SCHEMA//\\/\\\\}" 192 | XML_SCHEMA="${XML_SCHEMA//\//\\/}" 193 | XML_SCHEMA="${XML_SCHEMA//&/\\&}" 194 | XML_SCHEMA="${XML_SCHEMA//$'\n'/\\n}" 195 | sed -i "s/# AUTOMATICALLY_REPLACED_WITH_EMBEDDED_XML_FILE_DECLARATION/EMBEDDED=$\(cat << EndOfMsg\n${XML_SCHEMA}\nEndOfMsg\n)/" \ 196 | "$TEMPDIR/${systems[i]}/$SCRIPTS/gs_schema_install.sh" 197 | cp "$PARENT_DIR/ui/gtkui/gs_schema_uninstall.sh" "$TEMPDIR/${systems[i]}/$SCRIPTS" 198 | 199 | # Form application version from latest GIT tag/release. 200 | # Extract latest GIT tag. 201 | GIT_TAG=$(git describe --tags --abbrev=0) 202 | # Extract number of commits passed from last GIT release. 203 | COMMITS_AFTER=$(git rev-list ${GIT_TAG}..HEAD --count) 204 | # Remove 'v' char from tag, if present 205 | [[ ${GIT_TAG:0:1} == "v" ]] && GIT_TAG=${GIT_TAG:1} 206 | # Combine last GIT tag and number of commits since, if applicable, to build application version. 207 | APP_VERSION=$GIT_TAG 208 | # Add extra 1 to increment build number (to start index from 1). 209 | ITERATION=$(($COMMITS_AFTER+1)) 210 | 211 | APP_BUILD_SUCCESSFULL=true 212 | if [ -z $SKIP_APP_BUILD ]; then 213 | ./gorsync_build.sh --buildtype Release 214 | if [ $? -eq 0 ]; then 215 | echo "App successfully compiled." 216 | cd "$SAVE_DIR" 217 | cp "$PARENT_DIR/$APP_NAME" "$TEMPDIR/${systems[i]}/$DISTRIB/${prefixes[i]}/bin" 218 | else 219 | APP_BUILD_SUCCESSFULL=false 220 | fi 221 | fi 222 | cd "$SAVE_DIR" 223 | echo "$(get_desktop_entry_file)" > "$TEMPDIR/${systems[i]}/$DISTRIB/${prefixes[i]}/share/applications/gorsync.desktop" 224 | 225 | if [ $APP_BUILD_SUCCESSFULL = true ]; then 226 | 227 | mkdir -p ./packages && cd ./packages 228 | 229 | fpm -s dir -f \ 230 | -t ${fpm_packages[i]} \ 231 | -C "$TEMPDIR/${systems[i]}/$DISTRIB" \ 232 | --name $APP_NAME \ 233 | --version $APP_VERSION \ 234 | --iteration $ITERATION \ 235 | --description "GTK+ frontend (backup application) for RSYNC utility" \ 236 | ${fpm_dependencies[i]} \ 237 | --after-install "$TEMPDIR/${systems[i]}/$SCRIPTS/gs_schema_install.sh" \ 238 | --before-remove "$TEMPDIR/${systems[i]}/$SCRIPTS/gs_schema_uninstall.sh" \ 239 | --conflicts "gorsync-git" \ 240 | --maintainer "$AUTHOR" \ 241 | --url "$APP_URL" \ 242 | --license "$LICENSE" 243 | # --config-files /etc 244 | 245 | # Replacing single-spaces with empty via contruction ${MYSTRING//in/by} (or) ${MYSTRING//in} 246 | if [ -z ${warnings[i]// } ]; then 247 | : 248 | else 249 | echo -e "WARNING: ${warnings[i]}" 250 | fi 251 | 252 | echo -e "...${systems[i]} done.\n" 253 | else 254 | echo -e "...${systems[i]} FAIL.\n" 255 | fi 256 | cd "$SAVE_DIR" 257 | 258 | done 259 | 260 | -------------------------------------------------------------------------------- /rsync/abstract.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package rsync 13 | 14 | import ( 15 | logger "github.com/d2r2/go-logger" 16 | "github.com/d2r2/go-rsync/core" 17 | ) 18 | 19 | // Logging keep settings whether we need to log RSYNC utility functioning. 20 | // We can log only RSYNC calls, but also STDOUT output for intensive log. 21 | type Logging struct { 22 | EnableLog bool 23 | EnableIntensiveLog bool 24 | Log logger.PackageLog 25 | } 26 | 27 | // ErrorHookCall is a delegate used to work around RSYNC issues 28 | // caused by out of disk space case. 29 | type ErrorHookCall func(err error, paths core.SrcDstPath, predictedSize *core.FolderSize, 30 | repeated int, retryLeft int) (newRetryLeft int, criticalError error) 31 | 32 | // ErrorHook contains call and predicted size to work around RSYNC issues 33 | // caused by out of disk space case. 34 | type ErrorHook struct { 35 | Call ErrorHookCall 36 | PredictedSize *core.FolderSize 37 | } 38 | 39 | func NewErrorHook(call ErrorHookCall, predictedSize core.FolderSize) *ErrorHook { 40 | v := &ErrorHook{Call: call, PredictedSize: &predictedSize} 41 | return v 42 | } 43 | 44 | // Options keep settings for RSYNC call. 45 | // Settings include: retry count, parameters, ErrorHook object 46 | // for recover attempt if issue thrown. 47 | type Options struct { 48 | RetryCount int 49 | Params []string 50 | ErrorHook *ErrorHook 51 | Password *string 52 | } 53 | 54 | func NewOptions(params []string) *Options { 55 | options := &Options{Params: params} 56 | return options 57 | } 58 | 59 | // AddParams add RSYNC command line options. 60 | func (v *Options) AddParams(params ...string) *Options { 61 | v.Params = append(v.Params, params...) 62 | return v 63 | } 64 | 65 | // SetRetryCount set retry count for repeated call in case 66 | // of error return results (exit code <> 0). 67 | func (v *Options) SetRetryCount(retryCount *int) *Options { 68 | if retryCount != nil { 69 | if *retryCount >= 0 { 70 | // limit number of retry count to 5 maximum 71 | if *retryCount < 6 { 72 | v.RetryCount = *retryCount 73 | } else { 74 | v.RetryCount = 5 75 | } 76 | } 77 | } 78 | return v 79 | } 80 | 81 | // SetAuthPassword set password to use in RSYNC call to 82 | // get data from authenticated (password protected) RSYNC module. 83 | // Read option "secrets file" at https://linux.die.net/man/5/rsyncd.conf, 84 | // which describe how to protect RSYNC data source with password. 85 | func (v *Options) SetAuthPassword(password *string) *Options { 86 | v.Password = password 87 | return v 88 | } 89 | 90 | // SetErrorHook define callback function to run, if RESYNC 91 | // utility exited with error code <> 0. 92 | // Such callback might suggest issue source and make recommendation 93 | // to user via UI to resolve the issue before following retry. 94 | func (v *Options) SetErrorHook(errorHook *ErrorHook) *Options { 95 | v.ErrorHook = errorHook 96 | return v 97 | } 98 | 99 | // WithDefaultParams return list of obligatory options 100 | // for each run of RSYNC utility. 101 | func WithDefaultParams(params []string) []string { 102 | defParams := []string{"--progress", "--verbose"} 103 | params2 := append(defParams, params...) 104 | return params2 105 | } 106 | -------------------------------------------------------------------------------- /rsync/common.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package rsync 13 | 14 | import ( 15 | "fmt" 16 | 17 | "github.com/d2r2/go-logger" 18 | ) 19 | 20 | // You can manage verbosity of log output 21 | // in the package by changing last parameter value 22 | // (comment/uncomment corresponding lines). 23 | var lg = logger.NewPackageLogger("rsync", 24 | // logger.DebugLevel, 25 | logger.InfoLevel, 26 | ) 27 | 28 | var f = fmt.Sprintf 29 | -------------------------------------------------------------------------------- /rsync/errors.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package rsync 13 | 14 | import ( 15 | "bytes" 16 | "regexp" 17 | "strings" 18 | 19 | "github.com/d2r2/go-rsync/core" 20 | "github.com/d2r2/go-rsync/locale" 21 | ) 22 | 23 | // ProcessTerminatedError denote a situation with termination pending. 24 | type ProcessTerminatedError struct { 25 | } 26 | 27 | func (v *ProcessTerminatedError) Error() string { 28 | return locale.T(MsgRsyncProcessTerminatedError, nil) 29 | } 30 | 31 | // IsProcessTerminatedError check that error able to cast 32 | // to ProcessTerminatedError. 33 | func IsProcessTerminatedError(err error) bool { 34 | if err != nil { 35 | _, ok := err.(*ProcessTerminatedError) 36 | return ok 37 | } 38 | return false 39 | } 40 | 41 | // CallFailedError denote a situation when RSYNC execution 42 | // completed with non-zero exit code. 43 | type CallFailedError struct { 44 | ExitCode int 45 | Description string 46 | } 47 | 48 | // extractError used to extract textual description of error 49 | // which improve understanding of error root cause. 50 | func extractError(stdErr *bytes.Buffer) string { 51 | var descr string 52 | buf := stdErr.String() 53 | re := regexp.MustCompile(`(?m:^@ERROR:(?P.*)$)`) 54 | m := core.FindStringSubmatchIndexes(re, buf) 55 | if len(m) > 0 { 56 | grErr := "error" 57 | if _, ok := m[grErr]; ok { 58 | start := m[grErr][0] 59 | end := m[grErr][1] 60 | descr = strings.TrimSpace(buf[start:end]) 61 | } 62 | } 63 | return descr 64 | } 65 | 66 | // NewCallFailedError creates error object based on ExitCode from RSYNC. 67 | // Use STDERR variable to extract more human readable error description. 68 | func NewCallFailedError(exitCode int, stdErr *bytes.Buffer) *CallFailedError { 69 | descr := extractError(stdErr) 70 | if descr != "" { 71 | descr += ", " + getRsyncExitCodeDesc(exitCode) 72 | } else { 73 | descr = getRsyncExitCodeDesc(exitCode) 74 | } 75 | 76 | v := &CallFailedError{ 77 | ExitCode: exitCode, 78 | Description: descr, 79 | } 80 | return v 81 | } 82 | 83 | func (v *CallFailedError) Error() string { 84 | return locale.T(MsgRsyncCallFailedError, 85 | struct { 86 | Description string 87 | ExitCode int 88 | }{Description: v.Description, ExitCode: v.ExitCode}) 89 | } 90 | 91 | // IsCallFailedError check that error able to cast 92 | // to CallFailedError. 93 | func IsCallFailedError(err error) bool { 94 | if err != nil { 95 | _, ok := err.(*CallFailedError) 96 | return ok 97 | } 98 | return false 99 | } 100 | 101 | // GetRsyncExitCodeDesc return RSYNC exit code descriptions 102 | // taken from here: http://wpkg.org/Rsync_exit_codes 103 | func getRsyncExitCodeDesc(exitCode int) string { 104 | codes := map[int]string{ 105 | 0: "success", 106 | 1: "syntax or usage error", 107 | 2: "protocol incompatibility", 108 | 3: "errors selecting input/output files, dirs", 109 | 4: "requested action not supported: an attempt was made to manipulate " + 110 | "64-bit files on a platform that cannot support them; or an option was " + 111 | "specified that is supported by the client and not by the server", 112 | 5: "error starting client-server protocol", 113 | 6: "daemon unable to append to log-file", 114 | 10: "error in socket I/O", 115 | 11: "error in file I/O", 116 | 12: "error in rsync protocol data stream", 117 | 13: "errors with program diagnostics", 118 | 14: "error in IPC code", 119 | 20: "received SIGUSR1 or SIGINT", 120 | 21: "some error returned by waitpid()", 121 | 22: "error allocating core memory buffers", 122 | 23: "partial transfer due to error", 123 | 24: "partial transfer due to vanished source files", 124 | 25: "the --max-delete limit stopped deletions", 125 | 30: "timeout in data send/receive", 126 | 35: "timeout waiting for daemon connection", 127 | 255: "unexplained error", 128 | } 129 | if v, ok := codes[exitCode]; ok { 130 | return v 131 | } else { 132 | return f("Undefined rsync exit code: %d", exitCode) 133 | } 134 | } 135 | 136 | // ExtractVersionAndProtocolError denote a situation when attempt 137 | // to extract rsync version/protocol has failed, and 138 | // version and protocol are undefined. 139 | // This error is not critical (in the main) and should not lead to app failure. 140 | type ExtractVersionAndProtocolError struct { 141 | } 142 | 143 | func (v *ExtractVersionAndProtocolError) Error() string { 144 | return locale.T(MsgRsyncExtractVersionAndProtocolError, nil) 145 | } 146 | 147 | // IsExtractVersionAndProtocolError check that error able to cast 148 | // to ExtractVersionAndProtocolError. 149 | func IsExtractVersionAndProtocolError(err error) bool { 150 | if err != nil { 151 | _, ok := err.(*ExtractVersionAndProtocolError) 152 | return ok 153 | } 154 | return false 155 | } 156 | -------------------------------------------------------------------------------- /rsync/messagekeys.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package rsync 13 | 14 | // ------------------------------------------------------------ 15 | // File contains message identifiers for localization purpose. 16 | // Message identifier names is self-descriptive, so ordinary 17 | // it's easy to understand what message is made for. 18 | // Message ID is used to call translation functions from 19 | // "locale" package. 20 | // ------------------------------------------------------------ 21 | 22 | const ( 23 | MsgRsyncCallFailedError = "RsyncCallFailedError" 24 | MsgRsyncProcessTerminatedError = "RsyncProcessTerminatedError" 25 | MsgRsyncCannotFindFolderSizeOutputError = "RsyncCannotFindFolderSizeOutputError" 26 | MsgRsyncCannotParseFolderSizeOutputError = "RsyncCannotParseFolderSizeOutputError" 27 | MsgRsyncExtractVersionAndProtocolError = "RsyncExtractVersionAndProtocolError" 28 | ) 29 | -------------------------------------------------------------------------------- /rsync/rsync.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package rsync 13 | 14 | import ( 15 | "bufio" 16 | "bytes" 17 | "context" 18 | "fmt" 19 | "regexp" 20 | "strings" 21 | 22 | "github.com/d2r2/go-rsync/core" 23 | shell "github.com/d2r2/go-shell" 24 | ) 25 | 26 | // RSYNC_APP_CMD contains RSYNC console utility system name to run. 27 | const RSYNC_APP_CMD = "rsync" 28 | 29 | // RunRsyncWithRetry run RSYNC utility with retry attempts. 30 | func RunRsyncWithRetry(ctx context.Context, options *Options, log *Logging, stdOut *bytes.Buffer, 31 | paths core.SrcDstPath) (sessionErr, retryErr, criticalErr error) { 32 | 33 | retryCount := 0 34 | if options != nil { 35 | retryCount = options.RetryCount 36 | } 37 | index := 0 38 | for { 39 | err := runSystemRsync(ctx, options.Password, 40 | options.Params, log, stdOut, 41 | paths.RsyncSourcePath, paths.DestPath) 42 | 43 | if err == nil { 44 | return 45 | } else if IsProcessTerminatedError(err) { 46 | sessionErr = err 47 | criticalErr = err 48 | return 49 | } 50 | 51 | if err != nil { 52 | retryErr = err 53 | } 54 | 55 | // in case of error we are trying to recover from 56 | // fail state via call to ErrorHook call-back function 57 | if options != nil && options.ErrorHook != nil { 58 | var newRetryLeft int 59 | newRetryLeft, criticalErr = options.ErrorHook.Call(err, paths, 60 | options.ErrorHook.PredictedSize, index, retryCount) 61 | if criticalErr != nil { 62 | break 63 | } 64 | retryCount = newRetryLeft 65 | } 66 | 67 | retryCount-- 68 | if retryCount < 0 { 69 | break 70 | } 71 | index++ 72 | } 73 | if criticalErr == nil && retryErr != nil { 74 | sessionErr = retryErr 75 | retryErr = nil 76 | } 77 | return 78 | } 79 | 80 | // IsInstalled do verify that RSYNC application present in the system. 81 | func IsInstalled() error { 82 | app := shell.NewApp(RSYNC_APP_CMD) 83 | return app.CheckIsInstalled() 84 | } 85 | 86 | // GetRsyncVersion run RSYNC to get version and protocol. 87 | func GetRsyncVersion() (version string, protocol string, err error) { 88 | app := shell.NewApp(RSYNC_APP_CMD, "--version") 89 | var stdOut, stdErr bytes.Buffer 90 | exitCode := app.Run(&stdOut, &stdErr) 91 | if exitCode.Error != nil { 92 | return "", "", exitCode.Error 93 | } 94 | scanner := bufio.NewScanner(&stdOut) 95 | scanner.Split(bufio.ScanLines) 96 | 97 | // Expression should parse a line variant: 98 | // rsync version 3.1.3 protocol version 31 99 | // rsync version v3.2.3 protocol version 31 100 | re := regexp.MustCompile(`version\s+v?(?P\d+\.\d+(\.\d+)?)(\s+protocol\s+version\s+(?P\d+))?`) 101 | for scanner.Scan() { 102 | line := scanner.Text() 103 | m := core.FindStringSubmatchIndexes(re, line) 104 | if len(m) > 0 { 105 | grName := "version" 106 | if _, ok := m[grName]; ok { 107 | start := m[grName][0] 108 | end := m[grName][1] 109 | version = line[start:end] 110 | } 111 | grName = "protocol" 112 | if _, ok := m[grName]; ok { 113 | start := m[grName][0] 114 | end := m[grName][1] 115 | protocol = line[start:end] 116 | } 117 | break 118 | } 119 | } 120 | // Extracted RSYNC version cannot be empty. 121 | if version == "" { 122 | // Return error which should be treated as a warning in the main, 123 | // when RSYNC version (and protocol) is undetected for some reason. 124 | return "", "", &ExtractVersionAndProtocolError{} 125 | } 126 | return version, protocol, nil 127 | } 128 | 129 | // runSystemRsync run RSYNC utility. 130 | // Parameters: 131 | // - Save console output to stdOut variable. 132 | func runSystemRsync(ctx context.Context, password *string, 133 | params []string, log *Logging, stdOut *bytes.Buffer, 134 | source, dest string) error { 135 | 136 | var args []string 137 | if params != nil { 138 | args = params 139 | } 140 | args = append(args, source, dest) 141 | stdOut2 := stdOut 142 | stdErr := bytes.NewBuffer(nil) 143 | 144 | var logBuf bytes.Buffer 145 | logEnabled := false 146 | if log != nil && log.EnableLog && log.Log != nil { 147 | logEnabled = true 148 | if stdOut2 == nil { 149 | stdOut2 = bytes.NewBuffer(nil) 150 | } 151 | } 152 | 153 | app := shell.NewApp(RSYNC_APP_CMD, args...) 154 | var passwd string 155 | if password != nil { 156 | passwd = *password 157 | } 158 | // Always add password variable RSYNC_PASSWORD, even when password not specified 159 | // by configuration, for protection from console password stdin input request 160 | // for RSYNC module with authentication. 161 | app.AddEnvironments([]string{fmt.Sprintf("RSYNC_PASSWORD=%s", passwd)}) 162 | if passwd != "" { 163 | lg.Debugf("PASSWD: %v", passwd) 164 | } 165 | lg.Debugf("Args: %v", args) 166 | waitCh, err := app.Start(stdOut2, stdErr) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | select { 172 | case <-ctx.Done(): 173 | lg.Debugf("Killing rsync: %v", args) 174 | err := app.Kill() 175 | if err != nil { 176 | return err 177 | } 178 | return &ProcessTerminatedError{} 179 | case st := <-waitCh: 180 | // Enable RSYNC log output 181 | if logEnabled { 182 | logBuf.WriteString(RSYNC_APP_CMD) 183 | if len(args) > 0 { 184 | logBuf.WriteString(" ") 185 | logBuf.WriteString(strings.Join(args, " ")) 186 | } 187 | // Enable intensive RSYNC log output, when we save 188 | // whole stdout print. 189 | if log.EnableIntensiveLog { 190 | logBuf.WriteString(fmt.Sprintln()) 191 | logBuf.WriteString(fmt.Sprintln(">>>>>>>>>>>>>>>> Stdout start >>>>>>>>>>>>>>>>")) 192 | logBuf.WriteString(fmt.Sprintln(strings.TrimRight(stdOut2.String(), "\n"))) 193 | logBuf.WriteString(fmt.Sprint("<<<<<<<<<<<<<<<< Stdout end <<<<<<<<<<<<<<<<")) 194 | } 195 | log.Log.Info(logBuf.String()) 196 | } 197 | if st.Error != nil { 198 | return st.Error 199 | } else if st.ExitCode != 0 { 200 | lg.Debugf("STDERR: %v", stdErr.String()) 201 | return NewCallFailedError(st.ExitCode, stdErr) 202 | } 203 | return nil 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /rsync/utils.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package rsync 13 | 14 | import ( 15 | "bytes" 16 | "context" 17 | "errors" 18 | "fmt" 19 | "io/ioutil" 20 | "os" 21 | "regexp" 22 | "strconv" 23 | "strings" 24 | 25 | "github.com/d2r2/go-rsync/core" 26 | "github.com/d2r2/go-rsync/locale" 27 | ) 28 | 29 | // ObtainDirLocalSize parse STDOUT from RSYNC dry-run execution to extract local size of directory without nested folders. 30 | func ObtainDirLocalSize(ctx context.Context, password *string, dir *core.Dir, 31 | retryCount *int, rsyncProtocol string, log *Logging) (*core.FolderSize, error) { 32 | 33 | // RSYNC "dry run" to get total size of backup 34 | var stdOut bytes.Buffer 35 | options := NewOptions(WithDefaultParams([]string{"--dry-run", "--compress"})). 36 | AddParams("--dirs"). 37 | SetRetryCount(retryCount). 38 | SetAuthPassword(password) 39 | sessionErr, _, _ := RunRsyncWithRetry(ctx, options, log, &stdOut, dir.Paths) 40 | if sessionErr != nil { 41 | return nil, sessionErr 42 | } 43 | backupSize, err := extractBackupSize(&stdOut, rsyncProtocol) 44 | if err != nil { 45 | return nil, err 46 | } 47 | if backupSize != nil { 48 | lg.Debugf("Get rsync %q size: %v", dir.Paths.RsyncSourcePath, 49 | core.GetReadableSize(*backupSize)) 50 | } 51 | return backupSize, nil 52 | } 53 | 54 | // ObtainDirLocalSize parse STDOUT from RSYNC dry-run execution to extract full size of directory. 55 | func ObtainDirFullSize(ctx context.Context, password *string, dir *core.Dir, 56 | retryCount *int, rsyncProtocol string, log *Logging) (*core.FolderSize, error) { 57 | 58 | // RSYNC "dry run" to get total size of backup 59 | var stdOut bytes.Buffer 60 | options := NewOptions(WithDefaultParams([]string{"--dry-run", "--compress"})). 61 | AddParams("--recursive", "--include=*/"). 62 | SetRetryCount(retryCount). 63 | SetAuthPassword(password) 64 | sessionErr, _, _ := RunRsyncWithRetry(ctx, options, log, &stdOut, dir.Paths) 65 | if sessionErr != nil { 66 | return nil, sessionErr 67 | } 68 | backupSize, err := extractBackupSize(&stdOut, rsyncProtocol) 69 | if err != nil { 70 | return nil, err 71 | } 72 | return backupSize, nil 73 | } 74 | 75 | // extractBackupSize parse and decode RSYNC STDOUT output to obtain folder content size. 76 | func extractBackupSize(stdOut *bytes.Buffer, rsyncProtocol string) (*core.FolderSize, error) { 77 | // Parse the line: "total size is 2,227,810,354 speedup is 507,127.33 (DRY RUN)" 78 | // to extract "total size" value. 79 | re := regexp.MustCompile(`total\s+size\s+is\s+(?P((\d+)\,?)+)`) 80 | str := stdOut.String() 81 | m := core.FindStringSubmatchIndexes(re, str) 82 | if a, ok := m["Number"]; ok { 83 | str2 := strings.Replace(str[a[0]:a[1]], ",", "", -1) 84 | // lg.Debugf("%v", str2) 85 | i, err := strconv.ParseInt(str2, 10, 64) 86 | if err != nil { 87 | return nil, errors.New(locale.T(MsgRsyncCannotParseFolderSizeOutputError, 88 | struct{ Text string }{Text: str2})) 89 | } 90 | i2 := core.FolderSize(i) 91 | return &i2, nil 92 | } else { 93 | return nil, errors.New(locale.T(MsgRsyncCannotFindFolderSizeOutputError, nil)) 94 | } 95 | } 96 | 97 | // GetPathStatus verify that RSYNC source path is valid. 98 | // For this RSYNC is launched, than exit status is evaluated. 99 | func GetPathStatus(ctx context.Context, password *string, 100 | sourceRSync string, recursive bool) error { 101 | 102 | tempDir, err := ioutil.TempDir("", "backup_dir_status_") 103 | if err != nil { 104 | return err 105 | } 106 | defer os.RemoveAll(tempDir) 107 | 108 | paths := core.SrcDstPath{ 109 | RsyncSourcePath: core.RsyncPathJoin(sourceRSync, ""), 110 | DestPath: tempDir, 111 | } 112 | options := NewOptions(WithDefaultParams([]string{"--include=*/", "--dry-run"})). 113 | SetAuthPassword(password) 114 | if recursive { 115 | options.AddParams("--recursive") 116 | } 117 | sessionErr, _, _ := RunRsyncWithRetry(ctx, options, nil, nil, paths) 118 | if sessionErr != nil { 119 | return sessionErr 120 | } 121 | return nil 122 | } 123 | 124 | // NormalizeRsyncURL normalize RSYNC URL by: 125 | // 1) remove user specification (if found). 126 | // 2) remove excess '/' chars in path following host. 127 | func NormalizeRsyncURL(rsyncURL string) string { 128 | _, host, path := parseRsyncURL(strings.TrimSpace(rsyncURL)) 129 | path = removeExcessSlashChars(path) 130 | // assemble RSYNC URL path back, but without user specification 131 | newRsyncURL := fmt.Sprintf("rsync://%s%s", host, path) 132 | // lg.Debugf("Original RSYNC URL: %s", rsyncURL) 133 | // lg.Debugf("Modified RSYNC URL: %s", newRsyncURL) 134 | return newRsyncURL 135 | } 136 | 137 | // parseRsyncURL disassemble RSYNC URL to the parts. 138 | // This parts include: rsync prefix, user (if specified), host and path. 139 | func parseRsyncURL(rsyncURL string) (user, host, path string) { 140 | re := regexp.MustCompile(`(?i:^rsync://(?P[^@]*@)?(?P[^/]*)(?P.*)$)`) 141 | m := core.FindStringSubmatchIndexes(re, rsyncURL) 142 | if len(m) > 0 { 143 | grUser := "user" 144 | if _, ok := m[grUser]; ok { 145 | start := m[grUser][0] 146 | end := m[grUser][1] 147 | user = rsyncURL[start:end] 148 | } 149 | grHost := "host" 150 | if _, ok := m[grHost]; ok { 151 | start := m[grHost][0] 152 | end := m[grHost][1] 153 | host = rsyncURL[start:end] 154 | } 155 | grPath := "path" 156 | if _, ok := m[grPath]; ok { 157 | start := m[grPath][0] 158 | end := m[grPath][1] 159 | path = rsyncURL[start:end] 160 | } 161 | } 162 | return 163 | } 164 | 165 | // removeExcessSlashChars remove excess path divider in RSYNC path. 166 | func removeExcessSlashChars(path string) string { 167 | var buf bytes.Buffer 168 | lastCharIsSlash := false 169 | for _, ch := range path { 170 | if ch == '/' { 171 | if lastCharIsSlash { 172 | continue 173 | } 174 | lastCharIsSlash = true 175 | } else { 176 | lastCharIsSlash = false 177 | } 178 | buf.WriteRune(ch) 179 | } 180 | 181 | path = buf.String() 182 | if path[len(path)-1] == '/' { 183 | path = path[:len(path)-1] 184 | } 185 | 186 | return path 187 | } 188 | -------------------------------------------------------------------------------- /sandbox/dev_scripts/create_distrib_via_fpm_with_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This script is experimental and might not work correctly! 3 | # Was created as an option to build 32bit packages on 64bit OS. 4 | # Not sure about results. 5 | 6 | DKR="sudo docker" 7 | LINUX_64BIT=debian 8 | LINUX_32BIT=i386/debian 9 | CNR_64BIT=gorsync_build_64bit 10 | CNR_32BIT=gorsync_build_32bit 11 | GOARCH_64BIT=amd64 12 | GOARCH_32BIT=386 13 | 14 | 15 | deploy_docker_container() { 16 | local CNR_NAME=$1 17 | local DISTR_NAME=$2 18 | 19 | $DKR ps | grep $CNR_NAME && $DKR stop $CNR_NAME 20 | $DKR ps -a | grep $CNR_NAME && $DKR rm $CNR_NAME 21 | $DKR create --name=$CNR_NAME -it $DISTR_NAME && $DKR start $CNR_NAME 22 | $DKR exec $CNR_NAME apt update && $DKR exec $CNR_NAME apt upgrade -y 23 | $DKR exec $CNR_NAME apt install -y curl wget 24 | } 25 | 26 | install_dependencies() { 27 | local CNR_NAME=$1 28 | 29 | $DKR exec $CNR_NAME apt install -y rsync libglib2.0-dev libgtk-3-dev libnotify-dev git ruby ruby-dev rubygems build-essential bsdtar rpm 30 | $DKR exec $CNR_NAME gem install --no-ri --no-rdoc fpm 31 | } 32 | 33 | install_golang() { 34 | local CNR_NAME=$1 35 | local GOARCH=$2 36 | 37 | # $DKR exec $CNR_NAME curl -sL -o /usr/local/sbin/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme 38 | # $DKR exec $CNR_NAME chmod +x /usr/local/sbin/gimme 39 | # $DKR exec $CNR_NAME sh -c 'GIMME_ARCH=$GOARCH gimme 1.13 | grep "GOROOT"' 40 | $DKR exec $CNR_NAME apt install -y golang 41 | } 42 | 43 | build_gorsync_packages() { 44 | local CNR_NAME=$1 45 | 46 | GOCODEPATH=/root/Downloads/gocode 47 | GORSYNCSUBPATH=github.com/d2r2/go-rsync 48 | PACKAGINGSUBPATH=packaging/fpm_packages 49 | # GOROOTPATH=$($DKR exec $CNR_NAME gimme 1.13 | grep "GOROOT" | sed "s/^.*'\(.*\)'.*$/\1/") 50 | echo $GOROOTPATH 51 | $DKR exec $CNR_NAME sh -c "GOROOT=$GOROOTPATH PATH=\$GOROOT/bin:\$PATH GOPATH=$GOCODEPATH go version" 52 | $DKR exec $CNR_NAME mkdir -p $GOCODEPATH 53 | $DKR exec $CNR_NAME sh -c "GOROOT=$GOROOTPATH PATH=\$GOROOT/bin:\$PATH GOPATH=$GOCODEPATH go get -u -v github.com/d2r2/go-rsync" 54 | $DKR exec $CNR_NAME sh -c "GOROOT=$GOROOTPATH PATH=\$GOROOT/bin:\$PATH GOPATH=$GOCODEPATH go get -u -v all" 55 | $DKR exec $CNR_NAME sh -c "cd $GOCODEPATH/src/$GORSYNCSUBPATH/$PACKAGINGSUBPATH && GOROOT=$GOROOTPATH PATH=\$GOROOT/bin:\$PATH GOPATH=$GOCODEPATH ./create_distrib_packages_with_fpm.sh" 56 | } 57 | 58 | # deploy and build 64 bit Gorsync packages 59 | # deploy_docker_container $CNR_64BIT $LINUX_64BIT 60 | # install_dependencies $CNR_64BIT 61 | # build_gorsync_packages $CNR_64BIT 62 | 63 | # deploy and build 32 bit Gorsync packages 64 | deploy_docker_container $CNR_32BIT $LINUX_32BIT 65 | install_dependencies $CNR_32BIT 66 | install_golang $CNR_32BIT $GOARCH_32BIT 67 | build_gorsync_packages $CNR_32BIT 68 | 69 | 70 | -------------------------------------------------------------------------------- /sandbox/dev_scripts/mount_10G_quota_dir.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Read this to create quota directory: 3 | # https://www.linuxquestions.org/questions/linux-server-73/directory-quota-601140/ 4 | # 5 | # This script help to mount destination folder to test application for "out of disk space" cases 6 | # to imporeve recovery and error management. 7 | # 8 | sudo mount -o loop,rw,usrquota,grpquota /run/media/ddyakov/sda9/tmp2/limit_size.ext4 /home/ddyakov/Downloads/7777 9 | 10 | -------------------------------------------------------------------------------- /sandbox/sandbox.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2r2/go-rsync/23e692101acc00f6e7ce302c588ebc8eac271a2a/sandbox/sandbox.tar.gz -------------------------------------------------------------------------------- /ui/gtkui/common.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package gtkui 13 | 14 | import ( 15 | "github.com/d2r2/go-logger" 16 | ) 17 | 18 | // You can manage verbosity of log output 19 | // in the package by changing last parameter value 20 | // (comment/uncomment corresponding lines). 21 | var lg = logger.NewPackageLogger("gtkui", 22 | // logger.DebugLevel, 23 | logger.InfoLevel, 24 | ) 25 | -------------------------------------------------------------------------------- /ui/gtkui/context.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package gtkui 13 | 14 | import ( 15 | "context" 16 | "sync" 17 | ) 18 | 19 | // ContextPack keeps cancellable context with its cancel function. 20 | type ContextPack struct { 21 | Context context.Context 22 | Cancel func() 23 | } 24 | 25 | // ForkContext create child context from the parent. 26 | func ForkContext(parent context.Context) *ContextPack { 27 | child, cancel := context.WithCancel(parent) 28 | v := &ContextPack{Context: child, Cancel: cancel} 29 | return v 30 | } 31 | 32 | // RunningContexts keeps all contexts of currently started services, 33 | // preliminary added to the list, which we would like to control, 34 | // tracking and managing their states. 35 | // All methods of RunningContexts type are thread-safe. 36 | type RunningContexts struct { 37 | sync.RWMutex 38 | running []*ContextPack 39 | } 40 | 41 | // AddContext add new service to track. 42 | func (v *RunningContexts) AddContext(pack *ContextPack) { 43 | v.Lock() 44 | defer v.Unlock() 45 | v.running = append(v.running, pack) 46 | } 47 | 48 | func (v *RunningContexts) findIndex(ctx context.Context) int { 49 | index := -1 50 | for i, item := range v.running { 51 | if item.Context == ctx { 52 | index = i 53 | break 54 | } 55 | } 56 | return index 57 | } 58 | 59 | // RemoveContext remove service from the list. 60 | func (v *RunningContexts) RemoveContext(ctx context.Context) { 61 | v.Lock() 62 | defer v.Unlock() 63 | index := v.findIndex(ctx) 64 | if index != -1 { 65 | v.running = append(v.running[:index], v.running[index+1:]...) 66 | } 67 | } 68 | 69 | // CancelContext cancel service from the list. 70 | func (v *RunningContexts) CancelContext(ctx context.Context) { 71 | v.Lock() 72 | defer v.Unlock() 73 | index := v.findIndex(ctx) 74 | if index != -1 { 75 | v.running[index].Cancel() 76 | v.running = append(v.running[:index], v.running[:index+1]...) 77 | } 78 | } 79 | 80 | // CancelAll cancel all services in the list. 81 | func (v *RunningContexts) CancelAll() { 82 | v.Lock() 83 | defer v.Unlock() 84 | for _, item := range v.running { 85 | item.Cancel() 86 | } 87 | v.running = []*ContextPack{} 88 | } 89 | 90 | // FindContext finds service by context object. 91 | func (v *RunningContexts) FindContext(ctx context.Context) *ContextPack { 92 | v.RLock() 93 | defer v.RUnlock() 94 | index := v.findIndex(ctx) 95 | if index != -1 { 96 | return v.running[index] 97 | } 98 | return nil 99 | } 100 | 101 | // GetCount returns number of services in the list to control. 102 | func (v *RunningContexts) GetCount() int { 103 | v.RLock() 104 | defer v.RUnlock() 105 | return len(v.running) 106 | } 107 | 108 | // BackupSessionStatus keeps contexts - live multi-thread processes, 109 | // which life cycle should be controlled. 110 | type BackupSessionStatus struct { 111 | parent context.Context 112 | running RunningContexts 113 | } 114 | 115 | func NewBackupSessionStatus(parent context.Context) *BackupSessionStatus { 116 | v := &BackupSessionStatus{parent: parent} 117 | return v 118 | } 119 | 120 | // Start forks new context for parent thread. 121 | func (v *BackupSessionStatus) Start() *ContextPack { 122 | pack := ForkContext(v.parent) 123 | v.running.AddContext(pack) 124 | return pack 125 | } 126 | 127 | // IsRunning checks if any children threads are alive. 128 | func (v *BackupSessionStatus) IsRunning() bool { 129 | return v.running.GetCount() > 0 130 | } 131 | 132 | // Stop terminates all live thread's contexts. 133 | func (v *BackupSessionStatus) Stop() { 134 | v.running.CancelAll() 135 | } 136 | 137 | // Done removes context from the pool of controlled threads. 138 | func (v *BackupSessionStatus) Done(ctx context.Context) { 139 | v.running.RemoveContext(ctx) 140 | } 141 | -------------------------------------------------------------------------------- /ui/gtkui/dialogs.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package gtkui 13 | 14 | import ( 15 | "bytes" 16 | 17 | "github.com/d2r2/go-rsync/core" 18 | "github.com/d2r2/go-rsync/locale" 19 | "github.com/d2r2/gotk3/gtk" 20 | "github.com/d2r2/gotk3/pango" 21 | ) 22 | 23 | // schemaSettingsErrorDialog display error related to GLIB GSettings application configuration. 24 | func schemaSettingsErrorDialog(parent *gtk.Window, text string, extraMsg *string) error { 25 | //title := "Schema settings configuration error" 26 | titleMarkup := NewMarkup(MARKUP_SIZE_LARGER, 0, 0, nil, nil, 27 | NewMarkup(MARKUP_SIZE_LARGER, 0, 0, nil, nil, 28 | NewMarkup(MARKUP_SIZE_LARGER, 0, 0, locale.T(MsgSchemaConfigDlgTitle, nil), nil))) 29 | paragraphs := []*DialogParagraph{NewDialogParagraph(text). 30 | SetJustify(gtk.JUSTIFY_CENTER).SetHorizAlign(gtk.ALIGN_CENTER)} 31 | if extraMsg != nil { 32 | paragraphs = append(paragraphs, NewDialogParagraph(*extraMsg). 33 | SetJustify(gtk.JUSTIFY_CENTER).SetHorizAlign(gtk.ALIGN_CENTER)) 34 | } 35 | 36 | err := ErrorMessage(parent, titleMarkup.String(), paragraphs) 37 | if err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | func removeUndescore(buttonCaption string) string { 44 | var buf bytes.Buffer 45 | for _, ch := range buttonCaption { 46 | if ch == '_' { 47 | continue 48 | } 49 | buf.WriteRune(ch) 50 | } 51 | 52 | buttonCaption = buf.String() 53 | return buttonCaption 54 | } 55 | 56 | func createInterruptBackupDialog(parent *gtk.Window) (*MessageDialog, error) { 57 | title := locale.T(MsgAppWindowTerminateBackupDlgTitle, nil) 58 | titleMarkup := NewMarkup(MARKUP_SIZE_LARGER, 0, 0, nil, nil, 59 | NewMarkup(MARKUP_SIZE_LARGER, 0, 0, title, nil)) 60 | terminateButtonCaption1 := locale.T(MsgDialogYesButton, nil) 61 | terminateButtonMarkup1 := NewMarkup(MARKUP_SIZE_LARGER, 0, 0, 62 | removeUndescore(terminateButtonCaption1), nil) 63 | continueButtonCaption1 := locale.T(MsgDialogNoButton, nil) 64 | continueButtonMarkup1 := NewMarkup(MARKUP_SIZE_LARGER, 0, 0, 65 | removeUndescore(continueButtonCaption1), nil) 66 | escapeKeyMarkup := NewMarkup(MARKUP_SIZE_LARGER, 0, 0, nil, nil, 67 | NewMarkup(MARKUP_SIZE_LARGER, 0, 0, "esc", nil)) 68 | text := locale.T(MsgAppWindowTerminateBackupDlgText, 69 | struct{ TerminateButton, ContinueButton, EscapeKey string }{ 70 | TerminateButton: terminateButtonMarkup1.String(), 71 | ContinueButton: continueButtonMarkup1.String(), 72 | EscapeKey: escapeKeyMarkup.String()}) 73 | // textMarkup := NewMarkup(0, 0, 0, text, nil) 74 | 75 | buttons := []DialogButton{ 76 | {terminateButtonCaption1, gtk.RESPONSE_YES, false, func(btn *gtk.Button) error { 77 | style, err2 := btn.GetStyleContext() 78 | if err2 != nil { 79 | return err2 80 | } 81 | // style.AddClass("suggested-action") 82 | style.AddClass("destructive-action") 83 | return nil 84 | }}, 85 | {continueButtonCaption1, gtk.RESPONSE_NO, true, func(btn *gtk.Button) error { 86 | style, err2 := btn.GetStyleContext() 87 | if err2 != nil { 88 | return err2 89 | } 90 | style.AddClass("suggested-action") 91 | // style.AddClass("destructive-action") 92 | return nil 93 | }}, 94 | } 95 | dialog, err := SetupMessageDialog(parent, titleMarkup.String(), "", 96 | []*DialogParagraph{NewDialogParagraph(text).SetMarkup(true)}, buttons, nil) 97 | return dialog, err 98 | } 99 | 100 | // interruptBackupDialog shows dialog and query for active process termination. 101 | func interruptBackupDialog(parent *gtk.Window) (bool, error) { 102 | dialog, err := createInterruptBackupDialog(parent) 103 | if err != nil { 104 | return false, err 105 | } 106 | 107 | response := dialog.Run(false) 108 | PrintDialogResponse(response) 109 | return IsResponseYes(response), nil 110 | } 111 | 112 | // OutOfSpaceResponse denote response from RSYNC out of space dialog query. 113 | type OutOfSpaceResponse int 114 | 115 | // 3 response type from RSYNC out of space dialog query: 116 | // 1) retry RSYNC failed call; 117 | // 2) ignore RSYNC filed call, but continue backup process; 118 | // 3) immediately terminate backup process. 119 | const ( 120 | OutOfSpaceRetry OutOfSpaceResponse = iota 121 | OutOfSpaceIgnore 122 | OutOfSpaceTerminate 123 | ) 124 | 125 | // outOfSpaceDialogAsync show dialog once RSYNC out of space issue happens. 126 | func outOfSpaceDialogAsync(parent *gtk.Window, paths core.SrcDstPath, freeSpace uint64) (OutOfSpaceResponse, error) { 127 | title := locale.T(MsgAppWindowOutOfSpaceDlgTitle, nil) 128 | titleMarkup := NewMarkup(MARKUP_SIZE_LARGER, 0, 0, nil, nil, 129 | NewMarkup(MARKUP_SIZE_LARGER, 0, 0, title, nil)) 130 | terminateButtonCaption := locale.T(MsgAppWindowOutOfSpaceDlgTerminateButton, nil) 131 | terminateButtonMarkup := NewMarkup(MARKUP_SIZE_LARGER, 0, 0, 132 | removeUndescore(terminateButtonCaption), nil) 133 | ignoreButtonCaption := locale.T(MsgAppWindowOutOfSpaceDlgIgnoreButton, nil) 134 | ignoreButtonMarkup := NewMarkup(MARKUP_SIZE_LARGER, 0, 0, 135 | removeUndescore(ignoreButtonCaption), nil) 136 | retryButtonCaption := locale.T(MsgAppWindowOutOfSpaceDlgRetryButton, nil) 137 | retryButtonMarkup := NewMarkup(MARKUP_SIZE_LARGER, 0, 0, 138 | removeUndescore(retryButtonCaption), nil) 139 | escapeKeyMarkup := NewMarkup(MARKUP_SIZE_LARGER, 0, 0, nil, nil, 140 | NewMarkup(MARKUP_SIZE_LARGER, 0, 0, "esc", nil)) 141 | buttons := []DialogButton{ 142 | {retryButtonCaption, gtk.RESPONSE_YES, true, func(btn *gtk.Button) error { 143 | style, err2 := btn.GetStyleContext() 144 | if err2 != nil { 145 | return err2 146 | } 147 | style.AddClass("suggested-action") 148 | // style.AddClass("destructive-action") 149 | return nil 150 | }}, 151 | {ignoreButtonCaption, gtk.RESPONSE_CANCEL, !true, nil}, 152 | {terminateButtonCaption, gtk.RESPONSE_NO, !true, func(btn *gtk.Button) error { 153 | style, err2 := btn.GetStyleContext() 154 | if err2 != nil { 155 | return err2 156 | } 157 | //style.AddClass("suggested-action") 158 | style.AddClass("destructive-action") 159 | return nil 160 | }}, 161 | } 162 | text := locale.T(MsgAppWindowOutOfSpaceDlgText1, 163 | struct{ Path, FreeSpace string }{Path: paths.DestPath, 164 | FreeSpace: core.FormatSize(freeSpace, true)}) 165 | paragraphs := []*DialogParagraph{NewDialogParagraph(text).SetEllipsize(pango.ELLIPSIZE_MIDDLE).SetMaxWidthChars(10)} 166 | text = locale.T(MsgAppWindowOutOfSpaceDlgText2, 167 | struct{ EscapeKey, RetryButton, IgnoreButton, TerminateButton string }{EscapeKey: escapeKeyMarkup.String(), 168 | RetryButton: retryButtonMarkup.String(), IgnoreButton: ignoreButtonMarkup.String(), 169 | TerminateButton: terminateButtonMarkup.String()}) 170 | paragraphs = append(paragraphs, NewDialogParagraph(text).SetMarkup(true).SetHorizAlign(gtk.ALIGN_CENTER)) 171 | 172 | ch := make(chan gtk.ResponseType) 173 | defer close(ch) 174 | 175 | MustIdleAdd(func() { 176 | dialog, err2 := SetupMessageDialog(parent, titleMarkup.String(), "", paragraphs, buttons, nil) 177 | if err2 != nil { 178 | lg.Fatal(err2) 179 | } 180 | ch <- dialog.Run(false) 181 | }) 182 | 183 | response, _ := <-ch 184 | PrintDialogResponse(response) 185 | 186 | if IsResponseYes(response) { 187 | return OutOfSpaceRetry, nil 188 | } else if IsResponseNo(response) { 189 | return OutOfSpaceTerminate, nil 190 | } else { 191 | return OutOfSpaceIgnore, nil 192 | } 193 | } 194 | 195 | // questionDialog shows standard question dialog with localizable YES/NO selection. 196 | func questionDialog(parent *gtk.Window, titleMarkup string, textMarkup string, 197 | defaultNo bool, yesDestructive bool, noSuggested bool) (bool, error) { 198 | yesButtonCaption := locale.T(MsgDialogYesButton, nil) 199 | noButtonCaption := locale.T(MsgDialogNoButton, nil) 200 | // escapeKeyMarkup := NewMarkup(MARKUP_SIZE_LARGER, 0, 0, nil, nil, 201 | // NewMarkup(MARKUP_SIZE_LARGER, 0, 0, "esc", nil)) 202 | buttons := []DialogButton{ 203 | {yesButtonCaption, gtk.RESPONSE_YES, false, func(btn *gtk.Button) error { 204 | if yesDestructive { 205 | style, err2 := btn.GetStyleContext() 206 | if err2 != nil { 207 | return err2 208 | } 209 | // style.AddClass("suggested-action") 210 | style.AddClass("destructive-action") 211 | } 212 | return nil 213 | }}, 214 | {noButtonCaption, gtk.RESPONSE_NO, defaultNo, func(btn *gtk.Button) error { 215 | if noSuggested { 216 | style, err2 := btn.GetStyleContext() 217 | if err2 != nil { 218 | return err2 219 | } 220 | style.AddClass("suggested-action") 221 | // style.AddClass("destructive-action") 222 | } 223 | return nil 224 | }}, 225 | } 226 | dialog, err := SetupMessageDialog(parent, titleMarkup, "", 227 | []*DialogParagraph{NewDialogParagraph(textMarkup).SetMarkup(true)}, buttons, nil) 228 | for { 229 | response := dialog.Run(false) 230 | if err != nil { 231 | return false, err 232 | } 233 | if IsResponseDeleteEvent(response) { 234 | if defaultNo { 235 | return false, nil 236 | } 237 | } else if IsResponseYes(response) { 238 | return true, nil 239 | } else if IsResponseNo(response) { 240 | return false, nil 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /ui/gtkui/gs_schema_install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # !!! This script is a part of distribution packaging system !!! 4 | # !!! Each line of this script was tested and debugged on Linux 32bit, Linux 64bit, FreeBSD !!! 5 | # !!! Change with great care, do not break it !!! 6 | 7 | SCHEMA_FILE="org.d2r2.gorsync.gschema.xml" 8 | 9 | get_gsettings_schema_file() 10 | { 11 | local EMBEDDED 12 | # ***** !!!!! DO NOT REMOVE THIS COMMENT BLOCK - HEREDOC WILL BE POSTED HERE !!!!! ***** 13 | # AUTOMATICALLY_REPLACED_WITH_EMBEDDED_XML_FILE_DECLARATION 14 | # ***** !!!!! DO NOT REMOVE THIS COMMENT BLOCK - HEREDOC WILL BE POSTED HERE !!!!! ***** 15 | if [ ${#EMBEDDED} -le 0 ]; then 16 | cat "gsettings/${SCHEMA_FILE}" 17 | else 18 | echo "${EMBEDDED}" 19 | fi 20 | } 21 | 22 | 23 | # if [ -z "$1" ]; then 24 | PREFIX=/usr 25 | OS_LOWERCASE=$(echo "$OSTYPE" | tr "[:upper:]" "[:lower:]") 26 | # FreeBSD 27 | if [ "$OS_LOWERCASE" = "freebsd" ]; then 28 | PREFIX="${PREFIX}/local" 29 | # Linux OS 30 | # elif [[ "$OSTYPE" == "linux-gnu" ]]; then 31 | # Mac OSX 32 | # elif [[ "$OSTYPE" == "darwin"* ]]; then 33 | # POSIX compatibility layer and Linux environment emulation for Windows 34 | # elif [[ "$OSTYPE" == "cygwin" ]]; then 35 | # Lightweight shell and GNU utilities compiled for Windows (part of MinGW) 36 | # elif [[ "$OSTYPE" == "msys" ]]; then 37 | # Windows 38 | # elif [[ "$OSTYPE" == "win32" ]]; then 39 | # else 40 | # Unknown. 41 | fi 42 | # else 43 | # export PREFIX=$1 44 | # fi 45 | 46 | if [ "$(id -u)" != "0" ]; then 47 | # Make sure only root can run our script 48 | echo "This script must be run as root" 1>&2 49 | exit 1 50 | fi 51 | 52 | # Check availability of required commands 53 | # COMMANDS="install glib-compile-schemas glib-compile-resources msgfmt desktop-file-validate gtk-update-icon-cache" 54 | COMMANDS="install glib-compile-schemas glib-compile-resources msgfmt desktop-file-validate gtk-update-icon-cache" 55 | # if [ "$PREFIX" = '/usr' ] || [ "$PREFIX" = "/usr/local" ]; then 56 | # COMMANDS="$COMMANDS xdg-desktop-menu" 57 | # fi 58 | # PACKAGES="coreutils glib2 glib2 gettext desktop-file-utils gtk-update-icon-cache xdg-utils" 59 | PACKAGES="coreutils glib2 glib2 gettext desktop-file-utils gtk-update-icon-cache xdg-utils" 60 | i=0 61 | for COMMAND in $COMMANDS; do 62 | type $COMMAND >/dev/null 2>&1 || { 63 | j=0 64 | for PACKAGE in $PACKAGES; do 65 | if [ $i = $j ]; then 66 | break 67 | fi 68 | j=$(( $j + 1 )) 69 | done 70 | echo "Your system is missing command $COMMAND, please install $PACKAGE" 71 | exit 1 72 | } 73 | i=$(( $i + 1 )) 74 | done 75 | 76 | SCHEMA_PATH=${PREFIX}/share/glib-2.0/schemas 77 | echo "Installing gsettings schema to ${SCHEMA_PATH}" 78 | 79 | # Copy and compile schema 80 | echo "Copying and compiling schema..." 81 | install -d ${SCHEMA_PATH} 82 | # install -m 644 gsettings/${SCHEMA_FILE} ${SCHEMA_PATH}/ 83 | echo "$(get_gsettings_schema_file)" > ${SCHEMA_PATH}/${SCHEMA_FILE} 84 | chmod 0644 ${SCHEMA_PATH}/${SCHEMA_FILE} 85 | # Redirect output to /dev/null help on some linux distributions (redhat), which produce 86 | # lot of warnings about "Schema ... are depricated." not related to application. 87 | glib-compile-schemas ${SCHEMA_PATH}/ 2>/dev/null 88 | 89 | -------------------------------------------------------------------------------- /ui/gtkui/gs_schema_uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # !!! This script is a part of distribution packaging system !!! 4 | # !!! Each line of this script was tested and debugged on Linux 32bit, Linux 64bit, FreeBSD !!! 5 | # !!! Change with great care, do not break it !!! 6 | 7 | # if [ -z "$1" ]; then 8 | PREFIX=/usr 9 | OS_LOWERCASE=$(echo "$OSTYPE" | tr "[:upper:]" "[:lower:]") 10 | # FreeBSD 11 | if [ "$OS_LOWERCASE" = "freebsd" ]; then 12 | PREFIX="${PREFIX}/local" 13 | # Linux OS 14 | # elif [[ "$OSTYPE" == "linux-gnu" ]]; then 15 | # Mac OSX 16 | # elif [[ "$OSTYPE" == "darwin"* ]]; then 17 | # POSIX compatibility layer and Linux environment emulation for Windows 18 | # elif [[ "$OSTYPE" == "cygwin" ]]; then 19 | # Lightweight shell and GNU utilities compiled for Windows (part of MinGW) 20 | # elif [[ "$OSTYPE" == "msys" ]]; then 21 | # Windows 22 | # elif [[ "$OSTYPE" == "win32" ]]; then 23 | # else 24 | # Unknown. 25 | fi 26 | # else 27 | # export PREFIX=$1 28 | # fi 29 | 30 | if [ "$(id -u)" != "0" ]; then 31 | # Make sure only root can run our script 32 | echo "This script must be run as root" 1>&2 33 | exit 1 34 | fi 35 | 36 | SCHEMA_PATH=${PREFIX}/share/glib-2.0/schemas 37 | echo "Uninstalling gsettings schema from ${SCHEMA_PATH}" 38 | 39 | rm ${SCHEMA_PATH}/org.d2r2.gorsync.gschema.xml 40 | # Redirect output to /dev/null help on some linux distributions (redhat), which produce 41 | # lot of warnings about "Schema ... are depricated." not related to application. 42 | glib-compile-schemas ${SCHEMA_PATH}/ 2>/dev/null 43 | 44 | -------------------------------------------------------------------------------- /ui/gtkui/gsettings/org.d2r2.gorsync.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | '!!!__SKIPBACKUP__!!!' 37 | 38 | 39 | 40 | true 41 | Show desktop notification about backup procedure completion 42 | 43 | 44 | 45 | false 46 | Run special script located in /etc/gorsync/ to notify about backup completion 47 | 48 | 49 | 50 | 2 51 | 52 | 53 | 54 | false 55 | Do not shows about dialog on application startup 56 | 57 | 58 | 59 | '14px' 60 | Session log window font size (in pixels) 61 | 62 | 63 | 64 | '' 65 | User interface language 66 | 67 | 68 | 69 | true 70 | Determine automatically default backup block size 71 | 72 | 73 | 74 | 300 75 | Maximum batch size to backup at once 76 | 77 | 78 | 79 | true 80 | Activate attempts for search and use of previous backups 81 | 82 | 83 | 84 | 3 85 | Specify number of previous backups used for deduplication 86 | 87 | 88 | 89 | false 90 | Enable RSYNC log level log 91 | 92 | 93 | 94 | false 95 | Enable RSYNC intensive log level log (include stdout output) 96 | 97 | 98 | 99 | true 100 | RSYNC --links option. Look for RSYNC help for details 101 | 102 | 103 | 104 | true 105 | RSYNC --perms option. Look for RSYNC help for details 106 | 107 | 108 | 109 | true 110 | RSYNC --group option. Look for RSYNC help for details 111 | 112 | 113 | 114 | true 115 | RSYNC --owner option. Look for RSYNC help for details 116 | 117 | 118 | 119 | false 120 | RSYNC --devices option. Look for RSYNC help for details 121 | 122 | 123 | 124 | false 125 | RSYNC --specials option. Look for RSYNC help for details 126 | 127 | 128 | 129 | false 130 | RSYNC --compress option. Look for RSYNC help for details 131 | 132 | 133 | 134 | [] 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | '' 144 | 145 | 146 | 147 | '' 148 | 149 | 150 | 151 | [] 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | '' 160 | 161 | 162 | 163 | '' 164 | 165 | 166 | 167 | 168 | '' 169 | 170 | 171 | 172 | '' 173 | 174 | 175 | 176 | 177 | true 178 | RSYNC --links option. Look for RSYNC help for details 179 | 180 | 181 | 182 | false 183 | RSYNC --links option. Look for RSYNC help for details 184 | 185 | 186 | 187 | true 188 | RSYNC --perms option. Look for RSYNC help for details 189 | 190 | 191 | 192 | false 193 | RSYNC --perms option. Look for RSYNC help for details 194 | 195 | 196 | 197 | true 198 | RSYNC --group option. Look for RSYNC help for details 199 | 200 | 201 | 202 | false 203 | RSYNC --group option. Look for RSYNC help for details 204 | 205 | 206 | 207 | true 208 | RSYNC --owner option. Look for RSYNC help for details 209 | 210 | 211 | 212 | false 213 | RSYNC --owner option. Look for RSYNC help for details 214 | 215 | 216 | 217 | true 218 | RSYNC --devices option. Look for RSYNC help for details 219 | 220 | 221 | 222 | false 223 | RSYNC --devices option. Look for RSYNC help for details 224 | 225 | 226 | 227 | true 228 | RSYNC --specials option. Look for RSYNC help for details 229 | 230 | 231 | 232 | false 233 | RSYNC --specials option. Look for RSYNC help for details 234 | 235 | 236 | 237 | 238 | true 239 | 240 | 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /ui/gtkui/gtk_before_3_22.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | //+build gtk_3_6 gtk_3_8 gtk_3_10 gtk_3_12 gtk_3_14 gtk_3_16 gtk_3_18 gtk_3_20 13 | 14 | package gtkui 15 | 16 | import "github.com/d2r2/gotk3/gtk" 17 | 18 | // SetScrolledWindowPropogatedHeight compiled for GTK+ before 3.22 does nothing. 19 | func SetScrolledWindowPropogatedHeight(sw *gtk.ScrolledWindow, propagate bool) { 20 | // No call 21 | } 22 | 23 | func ShowUri(window *gtk.Window, uri string) error { 24 | screen, err := window.GetScreen() 25 | if err != nil { 26 | return err 27 | } 28 | return gtk.ShowUri(screen, uri) 29 | } 30 | -------------------------------------------------------------------------------- /ui/gtkui/gtk_since_3_22.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | // +build !gtk_3_6,!gtk_3_8,!gtk_3_10,!gtk_3_12,!gtk_3_14,!gtk_3_16,!gtk_3_18,!gtk_3_20 13 | 14 | package gtkui 15 | 16 | import "github.com/d2r2/gotk3/gtk" 17 | 18 | // SetScrolledWindowPropogatedHeight compiled for GTK+ since 3.22 call corresponding GtkScrolledWindow method. 19 | func SetScrolledWindowPropogatedHeight(sw *gtk.ScrolledWindow, propagate bool) { 20 | sw.SetPropagateNaturalHeight(propagate) 21 | } 22 | 23 | func ShowUri(window *gtk.Window, uri string) error { 24 | return gtk.ShowUriOnWindow(window, uri) 25 | } 26 | -------------------------------------------------------------------------------- /ui/gtkui/settings.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package gtkui 13 | 14 | const ( 15 | APP_SCHEMA_ID = "org.d2r2.gorsync" 16 | SETTINGS_SCHEMA_ID = APP_SCHEMA_ID + "." + "Settings" 17 | SETTINGS_SCHEMA_PATH = "/org/d2r2/gorsync/" 18 | PROFILE_SCHEMA_SUFFIX_ID = "Profile" 19 | PROFILE_SCHEMA_SUFFIX_PATH = "profiles/%s" 20 | SOURCE_SCHEMA_SUFFIX_ID = "Source" 21 | SOURCE_SCHEMA_SUFFIX_PATH = "sources/%s" 22 | ) 23 | 24 | const ( 25 | CFG_IGNORE_FILE_SIGNATURE = "ignore-file-signature" 26 | CFG_RSYNC_RETRY_COUNT = "rsync-retry-count" 27 | CFG_MANAGE_AUTO_BACKUP_BLOCK_SIZE = "manage-automatically-backup-block-size" 28 | CFG_MAX_BACKUP_BLOCK_SIZE_MB = "max-backup-block-size-mb" 29 | CFG_ENABLE_USE_OF_PREVIOUS_BACKUP = "enable-use-of-previous-backup" 30 | CFG_NUMBER_OF_PREVIOUS_BACKUP_TO_USE = "number-of-previous-backup-to-use" 31 | CFG_ENABLE_LOW_LEVEL_LOG_OF_RSYNC = "enable-low-level-log-for-rsync" 32 | CFG_ENABLE_INTENSIVE_LOW_LEVEL_LOG_OF_RSYNC = "enable-intensive-low-level-log-for-rsync" 33 | CFG_RSYNC_TRANSFER_SOURCE_GROUP_INCONSISTENT = "rsync-transfer-source-group-inconsistent" 34 | CFG_RSYNC_TRANSFER_SOURCE_GROUP = "rsync-transfer-source-group" 35 | CFG_RSYNC_TRANSFER_SOURCE_OWNER_INCONSISTENT = "rsync-transfer-source-owner-inconsistent" 36 | CFG_RSYNC_TRANSFER_SOURCE_OWNER = "rsync-transfer-source-owner" 37 | CFG_RSYNC_TRANSFER_SOURCE_PERMISSIONS_INCONSISTENT = "rsync-transfer-source-permissions-inconsistent" 38 | CFG_RSYNC_TRANSFER_SOURCE_PERMISSIONS = "rsync-transfer-source-permissions" 39 | CFG_RSYNC_RECREATE_SYMLINKS_INCONSISTENT = "rsync-recreate-symlinks-inconsistent" 40 | CFG_RSYNC_RECREATE_SYMLINKS = "rsync-recreate-symlinks" 41 | CFG_RSYNC_TRANSFER_DEVICE_FILES_INCONSISTENT = "rsync-transfer-device-files-inconsistent" 42 | CFG_RSYNC_TRANSFER_DEVICE_FILES = "rsync-transfer-device-files" 43 | CFG_RSYNC_TRANSFER_SPECIAL_FILES_INCONSISTENT = "rsync-transfer-special-files-inconsistent" 44 | CFG_RSYNC_TRANSFER_SPECIAL_FILES = "rsync-transfer-special-files" 45 | CFG_RSYNC_COMPRESS_FILE_TRANSFER = "rsync-compress-file-transfer" 46 | CFG_BACKUP_LIST = "profile-list" 47 | CFG_SOURCE_LIST = "source-list" 48 | CFG_DONT_SHOW_ABOUT_ON_STARTUP = "dont-show-about-dialog-on-startup" 49 | CFG_UI_LANGUAGE = "ui-language" 50 | CFG_SESSION_LOG_WIDGET_FONT_SIZE = "session-log-widget-font-size" 51 | CFG_PROFILE_NAME = "profile-name" 52 | CFG_PROFILE_DEST_ROOT_PATH = "destination-root-path" 53 | CFG_MODULE_RSYNC_SOURCE_PATH = "rsync-source-path" 54 | CFG_MODULE_DEST_SUBPATH = "dest-subpath" 55 | CFG_MODULE_CHANGE_FILE_PERMISSION = "change-file-permission" 56 | CFG_MODULE_AUTH_PASSWORD = "auth-password" 57 | CFG_MODULE_ENABLED = "source-dest-block-enabled" 58 | CFG_PERFORM_DESKTOP_NOTIFICATION = "perform-backup-completion-desktop-notification" 59 | CFG_RUN_NOTIFICATION_SCRIPT = "run-backup-completion-notification-script" 60 | ) 61 | -------------------------------------------------------------------------------- /ui/gtkui/settingstools.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package gtkui 13 | 14 | import ( 15 | "bytes" 16 | "errors" 17 | "strconv" 18 | 19 | "github.com/d2r2/gotk3/glib" 20 | ) 21 | 22 | // ========================================================================================== 23 | // ************************* GLIB SETTINGS UTILITIES SECTION START ************************** 24 | // ========================================================================================== 25 | // In real application use this code section as utilities to simplify creation 26 | // of GLIB/GTK+ components and widgets, including menus, dialog boxes, messages, 27 | // application settings and so on... 28 | 29 | // SettingsStore simplify work with glib.Settings. 30 | type SettingsStore struct { 31 | settings *glib.Settings 32 | schemaID string 33 | path string 34 | } 35 | 36 | // removeExcessSlashChars normalize path and remove excess path divider in glib.Settings schema path. 37 | func removeExcessSlashChars(path string) string { 38 | var buf bytes.Buffer 39 | lastCharIsSlash := false 40 | for _, ch := range path { 41 | if ch == '/' { 42 | if lastCharIsSlash { 43 | continue 44 | } 45 | lastCharIsSlash = true 46 | } else { 47 | lastCharIsSlash = false 48 | } 49 | buf.WriteRune(ch) 50 | } 51 | 52 | path = buf.String() 53 | 54 | return path 55 | } 56 | 57 | // NewSettingsStore create new SettingsStore object - wrapper on glib.Settings. 58 | func NewSettingsStore(schemaID string, path string, changed func()) (*SettingsStore, error) { 59 | path = removeExcessSlashChars(path) 60 | lg.Debugf("glib.GSettings path: %s", path) 61 | gs, err := glib.SettingsNewWithPath(schemaID, path) 62 | if err != nil { 63 | return nil, err 64 | } 65 | _, err = gs.Connect("changed", func() { 66 | if changed != nil { 67 | changed() 68 | } 69 | }) 70 | if err != nil { 71 | return nil, err 72 | } 73 | v := &SettingsStore{settings: gs, schemaID: schemaID, path: path} 74 | return v, nil 75 | } 76 | 77 | // GetChildSettingsStore generate child glib.Settings object to manipulate with nested scheme. 78 | func (v *SettingsStore) GetChildSettingsStore(suffixSchemaID string, suffixPath string, 79 | changed func()) (*SettingsStore, error) { 80 | 81 | newSchemaID := v.schemaID + "." + suffixSchemaID 82 | newPath := v.path + "/" + suffixPath + "/" 83 | settings, err := NewSettingsStore(newSchemaID, newPath, changed) 84 | return settings, err 85 | } 86 | 87 | // GetSchema obtains glib.SettingsSchema from glib.Settings. 88 | func (v *SettingsStore) GetSchema() (*glib.SettingsSchema, error) { 89 | val, err := v.settings.GetProperty("settings-schema") 90 | if err != nil { 91 | return nil, err 92 | } 93 | if schema, ok := val.(*glib.SettingsSchema); ok { 94 | return schema, nil 95 | } else { 96 | return nil, errors.New("GLib settings-schema property is not convertible to SettingsSchema") 97 | } 98 | } 99 | 100 | // SettingsArray is a way how to create multiple (indexed) GLib setting's group 101 | // based on single schema. For instance, multiple backup profiles with identical 102 | // settings inside of each profile. 103 | type SettingsArray struct { 104 | store *SettingsStore 105 | arrayID string 106 | } 107 | 108 | // NewSettingsArray creates new SettingsArray, to keep/add/delete new 109 | // indexed glib.Settings object based on single schema. 110 | func (v *SettingsStore) NewSettingsArray(arrayID string) *SettingsArray { 111 | sa := &SettingsArray{store: v, arrayID: arrayID} 112 | return sa 113 | } 114 | 115 | // DeleteNode delete specific indexed glib.Settings defined by nodeID. 116 | func (v *SettingsArray) DeleteNode(childStore *SettingsStore, nodeID string) error { 117 | // Delete/reset whole child settings object. 118 | schema, err := childStore.GetSchema() 119 | if err != nil { 120 | return err 121 | } 122 | keys := schema.ListKeys() 123 | for _, key := range keys { 124 | childStore.settings.Reset(key) 125 | } 126 | 127 | // Delete index from the array, which identify 128 | // child object settings. 129 | original := v.store.settings.GetStrv(v.arrayID) 130 | var updated []string 131 | for _, id := range original { 132 | if id != nodeID { 133 | updated = append(updated, id) 134 | } 135 | } 136 | v.store.settings.SetStrv(v.arrayID, updated) 137 | return nil 138 | } 139 | 140 | // AddNode add specific indexed glib.Settings identified by returned nodeID. 141 | func (v *SettingsArray) AddNode() (nodeID string, err error) { 142 | list := v.store.settings.GetStrv(v.arrayID) 143 | // Append index to the end of array, which reference to the list 144 | // of child settings based on single settings schema. 145 | var ni int 146 | if len(list) > 0 { 147 | ni, err = strconv.Atoi(list[len(list)-1]) 148 | if err != nil { 149 | return "", err 150 | } 151 | ni++ 152 | } 153 | list = append(list, strconv.Itoa(ni)) 154 | v.store.settings.SetStrv(v.arrayID, list) 155 | return list[len(list)-1], nil 156 | } 157 | 158 | // GetArrayIDs return identifiers of glib.Settings with common schema, 159 | // which can be accessed using id from the list. 160 | func (v *SettingsArray) GetArrayIDs() []string { 161 | list := v.store.settings.GetStrv(v.arrayID) 162 | return list 163 | } 164 | 165 | // Binding cache link between Key string identifier and GLIB object property. 166 | // Code partially taken from https://github.com/gnunn1/tilix project. 167 | type Binding struct { 168 | Key string 169 | Object glib.IObject 170 | Property string 171 | Flags glib.SettingsBindFlags 172 | } 173 | 174 | // BindingHelper is a bookkeeping class that keeps track of objects which are 175 | // binded to a GSettings object so they can be unbinded later. it 176 | // also supports the concept of deferred bindings where a binding 177 | // can be added but is not actually attached to a Settings object 178 | // until one is set. 179 | type BindingHelper struct { 180 | bindings []Binding 181 | settings *SettingsStore 182 | } 183 | 184 | // NewBindingHelper creates new BindingHelper object. 185 | func (v *SettingsStore) NewBindingHelper() *BindingHelper { 186 | bh := &BindingHelper{settings: v} 187 | return bh 188 | } 189 | 190 | // SetSettings will replace underlying GLIB Settings object to unbind 191 | // previously set bindings and re-bind to the new settings automatically. 192 | func (v *BindingHelper) SetSettings(value *SettingsStore) { 193 | if value != v.settings { 194 | if v.settings != nil { 195 | v.Unbind() 196 | } 197 | v.settings = value 198 | if v.settings != nil { 199 | v.bindAll() 200 | } 201 | } 202 | } 203 | 204 | func (v *BindingHelper) bindAll() { 205 | if v.settings != nil { 206 | for _, b := range v.bindings { 207 | v.settings.settings.Bind(b.Key, b.Object, b.Property, b.Flags) 208 | } 209 | } 210 | } 211 | 212 | // addBind add a binding to the list 213 | func (v *BindingHelper) addBind(key string, object glib.IObject, property string, flags glib.SettingsBindFlags) { 214 | v.bindings = append(v.bindings, Binding{key, object, property, flags}) 215 | } 216 | 217 | // Bind add a binding to list and binds to Settings if it is set. 218 | func (v *BindingHelper) Bind(key string, object glib.IObject, property string, flags glib.SettingsBindFlags) { 219 | v.addBind(key, object, property, flags) 220 | if v.settings != nil { 221 | v.settings.settings.Bind(key, object, property, flags) 222 | } 223 | } 224 | 225 | // Unbind all added binds from settings object. 226 | func (v *BindingHelper) Unbind() { 227 | for _, b := range v.bindings { 228 | v.settings.settings.Unbind(b.Object, b.Property) 229 | } 230 | } 231 | 232 | // Clear unbind all bindings and clears list of bindings. 233 | func (v *BindingHelper) Clear() { 234 | v.Unbind() 235 | v.bindings = nil 236 | } 237 | 238 | // ========================================================================================== 239 | // ************************* GLIB SETTINGS UTILITIES SECTION END **************************** 240 | // ========================================================================================== 241 | -------------------------------------------------------------------------------- /ui/gtkui/validator.go: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------------------------------- 2 | // This file is a part of Gorsync Backup project (backup RSYNC frontend). 3 | // Copyright (c) 2017-2022 Denis Dyakov 4 | // 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 6 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 7 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 8 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | //-------------------------------------------------------------------------------------------------- 11 | 12 | package gtkui 13 | 14 | import ( 15 | "context" 16 | "fmt" 17 | "sync" 18 | ) 19 | 20 | // ValidatorData is an array of arbitrary data 21 | // used to pass to the validation process. 22 | type ValidatorData struct { 23 | Items []interface{} 24 | } 25 | 26 | // ValidatorInit initialize validation process with next attributes: 27 | // - Synchronous call. 28 | // - Should take a limited time to execute. 29 | // - Allowed to updated GTK+ widgets. 30 | type ValidatorInit func(data *ValidatorData, group []*ValidatorData) error 31 | 32 | // ValidatorRun run validation process with next characteristics: 33 | // - Asynchronous call. 34 | // - Can take long time to run. 35 | // - GTK+ widgets should not be updated here (read only allowed). 36 | type ValidatorRun func(groupLock *sync.Mutex, ctx context.Context, data *ValidatorData, 37 | group []*ValidatorData) ([]interface{}, error) 38 | 39 | // ValidatorEnd finalize validation process with next characteristics: 40 | // - Asynchronous call. 41 | // - Should take a limited time to execute. 42 | // - GTK+ widgets might be updated here, if you wrap calls to glib.IdleAdd method. 43 | type ValidatorEnd func(groupLock *sync.Mutex, data *ValidatorData, results []interface{}) error 44 | 45 | // ValidatorEntry stores validation data all together, 46 | // including 3-step validation process (initialize, run, finalize). 47 | type ValidatorEntry struct { 48 | group string 49 | index string 50 | init ValidatorInit 51 | run ValidatorRun 52 | end ValidatorEnd 53 | Data *ValidatorData 54 | } 55 | 56 | // GroupMap gives thread-safe indexed dictionary, 57 | // which allow manipulations in asynchronous mode. 58 | // Store validator groups uniquely identified 59 | // by group and index identifiers. 60 | type GroupMap struct { 61 | sync.RWMutex 62 | m map[string]*ContextPack // keep Context object here indexed by group+index identifiers 63 | lock map[string]*sync.Mutex // keep lock object here indexed by group identifier 64 | } 65 | 66 | func GroupMapNew() *GroupMap { 67 | v := &GroupMap{m: make(map[string]*ContextPack), 68 | lock: make(map[string]*sync.Mutex)} 69 | return v 70 | } 71 | 72 | // getFullIndex return complex index from concatenation 73 | // of group and index identifiers. 74 | func getFullIndex(group, index string) string { 75 | return fmt.Sprintf("%s_%s", group, index) 76 | } 77 | 78 | // Add create new group identified by group+index identifiers. 79 | // if not exists create, either return it. 80 | func (v *GroupMap) Add(group, index string, ctxPack *ContextPack) { 81 | v.Lock() 82 | defer v.Unlock() 83 | 84 | v.m[getFullIndex(group, index)] = ctxPack 85 | } 86 | 87 | // Get return group context identified by group+index identifiers if exists. 88 | func (v *GroupMap) Get(group, index string) (*ContextPack, bool) { 89 | v.RLock() 90 | defer v.RUnlock() 91 | 92 | ctxPack, ok := v.m[getFullIndex(group, index)] 93 | return ctxPack, ok 94 | } 95 | 96 | // GetLock return lock object identified by group identifier. 97 | func (v *GroupMap) GetLock(group string) *sync.Mutex { 98 | v.Lock() 99 | defer v.Unlock() 100 | 101 | if _, ok := v.lock[group]; !ok { 102 | v.lock[group] = &sync.Mutex{} 103 | } 104 | return v.lock[group] 105 | } 106 | 107 | // Remove delete group object identified by group+index identifiers. 108 | func (v *GroupMap) Remove(group, index string) { 109 | v.Lock() 110 | defer v.Unlock() 111 | 112 | delete(v.m, getFullIndex(group, index)) 113 | } 114 | 115 | // UIValidator simplify GTK UI validation process 116 | // mixing synchronized and asynchronous calls, 117 | // which all together does not freeze GTK UI, 118 | // providing beautiful GTK UI response. 119 | // UIValidator is a thread-safe (except cases 120 | // when you need update GtkWidget components - 121 | // you must be careful in such circumstances). 122 | type UIValidator struct { 123 | sync.RWMutex 124 | entries map[int]*ValidatorEntry 125 | sorted []int 126 | key int 127 | parent context.Context 128 | runningContexts RunningContexts 129 | groupRunning *GroupMap 130 | } 131 | 132 | func UIValidatorNew(parent context.Context) *UIValidator { 133 | entries := make(map[int]*ValidatorEntry) 134 | groupRunning := GroupMapNew() 135 | v := &UIValidator{entries: entries, parent: parent, groupRunning: groupRunning} 136 | return v 137 | } 138 | 139 | // AddEntry creates new validating process with specific groupID and subGroupID identifiers. 140 | // Provide additionally 3 callback methods: to initialize, to run and to finalize validation. 141 | func (v *UIValidator) AddEntry(group, index string, init ValidatorInit, run ValidatorRun, 142 | end ValidatorEnd, data ...interface{}) int { 143 | 144 | v.Lock() 145 | defer v.Unlock() 146 | 147 | vEntry := &ValidatorEntry{group: group, index: index, 148 | init: init, run: run, end: end, Data: &ValidatorData{data}} 149 | key := v.key 150 | v.entries[key] = vEntry 151 | v.sorted = append(v.sorted, key) 152 | v.key++ 153 | return key 154 | } 155 | 156 | // RemoveEntry remove validating process via index key. 157 | func (v *UIValidator) RemoveEntry(key int) { 158 | v.Lock() 159 | defer v.Unlock() 160 | 161 | if val, ok := v.entries[key]; ok { 162 | v.cancelValidatesIfRunning(val.group, val.index) 163 | 164 | lg.Debugf("Delete item %q with index %v", getFullIndex(val.group, val.index), key) 165 | delete(v.entries, key) 166 | } 167 | for ind, k := range v.sorted { 168 | if key == k { 169 | v.sorted = append(v.sorted[:ind], v.sorted[ind+1:]...) 170 | break 171 | } 172 | } 173 | } 174 | 175 | // GetCount return number of validating processes. 176 | func (v *UIValidator) GetCount() int { 177 | v.RLock() 178 | defer v.RUnlock() 179 | 180 | return len(v.entries) 181 | } 182 | 183 | // getGroupEntries return list of ValidatorEntry objects, 184 | // identified by group + index identifiers. 185 | func (v *UIValidator) getGroupEntries(group, index string) []*ValidatorEntry { 186 | var list []*ValidatorEntry 187 | for _, key := range v.sorted { 188 | if v.entries[key].group == group && v.entries[key].index == index { 189 | list = append(list, v.entries[key]) 190 | } 191 | } 192 | return list 193 | } 194 | 195 | // getGroupData return list of ValidatorData objects, 196 | // identified by group + index identifiers. 197 | func (v *UIValidator) getGroupData(group, index string) []*ValidatorData { 198 | var list []*ValidatorData 199 | for _, key := range v.sorted { 200 | if v.entries[key].group == group && v.entries[key].index == index { 201 | list = append(list, v.entries[key].Data) 202 | } 203 | } 204 | return list 205 | } 206 | 207 | // resultsOrError used to get results from 208 | // validator asynchronous context execution. 209 | type resultsOrError struct { 210 | Entry *ValidatorEntry 211 | Results []interface{} 212 | Error error 213 | } 214 | 215 | // callInit run 1st validation step synchronously. 216 | func (v *UIValidator) callInit(entry *ValidatorEntry, dataList []*ValidatorData) error { 217 | 218 | return entry.init(entry.Data, dataList) 219 | } 220 | 221 | // callRun run 2nd validation step asynchronously. 222 | func (v *UIValidator) callRun(groupLock *sync.Mutex, ctx context.Context, 223 | entry *ValidatorEntry, dataList []*ValidatorData) ([]interface{}, error) { 224 | 225 | return entry.run(groupLock, ctx, entry.Data, dataList) 226 | } 227 | 228 | // callEnd run 3rd validation step asynchronously, but can be 229 | // synchronized with GTK+ context via glib.IdleAdd function. 230 | func (v *UIValidator) callEnd(groupLock *sync.Mutex, r resultsOrError) { 231 | err := r.Entry.end(groupLock, r.Entry.Data, r.Results) 232 | if err != nil { 233 | lg.Fatal(err) 234 | } 235 | } 236 | 237 | // runAsync run 2nd and 3rd validation process steps. 238 | func (v *UIValidator) runAsync(group, index string, entryList []*ValidatorEntry, 239 | dataList []*ValidatorData) { 240 | 241 | resultCh := make(chan resultsOrError) 242 | 243 | ctxPack := ForkContext(v.parent) 244 | v.runningContexts.AddContext(ctxPack) 245 | v.groupRunning.Add(group, index, ctxPack) 246 | groupLock := v.groupRunning.GetLock(group) 247 | var wait sync.WaitGroup 248 | wait.Add(1) 249 | 250 | // Run 3rd validation step in advance, to wait for results 251 | // from 2nd validation steps. 252 | go func() { 253 | defer wait.Done() 254 | 255 | terminated := false 256 | for { 257 | select { 258 | case r, ok := <-resultCh: 259 | if ok { 260 | err := r.Error 261 | if err == nil { 262 | lg.Debugf("Read Validator results %v", r.Results) 263 | lg.Debugf("Call Validator End") 264 | v.callEnd(groupLock, r) 265 | } else { 266 | lg.Fatal(err) 267 | } 268 | } else { 269 | lg.Debugf("Complete group %q validation 2", getFullIndex(group, index)) 270 | terminated = true 271 | } 272 | case <-ctxPack.Context.Done(): 273 | terminated = true 274 | } 275 | if terminated { 276 | break 277 | } 278 | } 279 | }() 280 | 281 | // Run 2nd validation step. 282 | go func() { 283 | terminated := false 284 | for _, item := range entryList { 285 | r := resultsOrError{Entry: item} 286 | results, err := v.callRun(groupLock, ctxPack.Context, item, dataList) 287 | if err != nil { 288 | r.Error = err 289 | } else { 290 | r.Results = results 291 | } 292 | select { 293 | case resultCh <- r: 294 | case <-ctxPack.Context.Done(): 295 | terminated = true 296 | } 297 | if terminated { 298 | break 299 | } 300 | } 301 | lg.Debugf("Complete group %q validation 1", getFullIndex(group, index)) 302 | close(resultCh) 303 | // Wait for completion of 3rd validation step (finalizer), before exit. 304 | wait.Wait() 305 | v.runningContexts.RemoveContext(ctxPack.Context) 306 | v.groupRunning.Remove(group, index) 307 | }() 308 | } 309 | 310 | // cancelValidatesIfRunning checks if validation process 311 | // in progress and thus cancel it. 312 | func (v *UIValidator) cancelValidatesIfRunning(group, index string) { 313 | if ctxPack, ok := v.groupRunning.Get(group, index); ok { 314 | lg.Debugf("Cancel group %q validation", getFullIndex(group, index)) 315 | ctxPack.Cancel() 316 | } 317 | } 318 | 319 | // Validate is a main entry point to start validation process 320 | // for specific group. 321 | // Validate process trigger next strictly sequential steps: 322 | // 1) Call "init validation" custom function in synchronous context. 323 | // So, it's safe to update GTK+ widgets here. 324 | // 2) Call "run validation" custom function in asynchronous context. 325 | // You should never update GTK+ widgets here (you can read widgets), but might run 326 | // long-term operations here (for instance run some external application). 327 | // 3) Call "finalize validation" custom function in asynchronous context. 328 | // You can update GTK+ widgets here, if you wrap code there with glib.IdleAdd() 329 | // function from GOTK+ library to synchronize with GTK+ context. 330 | func (v *UIValidator) Validate(group, index string) error { 331 | v.Lock() 332 | defer v.Unlock() 333 | 334 | entryList := v.getGroupEntries(group, index) 335 | dataList := v.getGroupData(group, index) 336 | if len(entryList) > 0 { 337 | // If found that previous validation processes in progress 338 | // with specific id still, then cancel it. 339 | v.cancelValidatesIfRunning(group, index) 340 | 341 | for _, item := range entryList { 342 | // 1st step of validation process 343 | err := v.callInit(item, dataList) 344 | if err != nil { 345 | return err 346 | } 347 | // 2nd and 3rd steps of validation process 348 | v.runAsync(group, index, entryList, dataList) 349 | } 350 | } 351 | return nil 352 | } 353 | 354 | // CancelValidates cancel processes identified 355 | // by group + index identifiers, if running. 356 | func (v *UIValidator) CancelValidates(group, index string) { 357 | v.Lock() 358 | defer v.Unlock() 359 | 360 | v.cancelValidatesIfRunning(group, index) 361 | } 362 | 363 | // CancelAll cancel all pending processes if running. 364 | func (v *UIValidator) CancelAll() { 365 | v.runningContexts.CancelAll() 366 | } 367 | --------------------------------------------------------------------------------