initial commit
This commit is contained in:
commit
188d6d1a3e
|
@ -0,0 +1,10 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.9.1 - 6-25-23
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- separate logging procs into Error and Info varieties. (ITwrx)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- ask-pass path resolution for various distros. (ITwrx)
|
||||||
|
- log sqlite3 access error/dependancy requirement on failed create/read. (ITwrx)
|
|
@ -0,0 +1,69 @@
|
||||||
|
#[Copyright 2023 ITwrx.
|
||||||
|
This file is part of EZ-Bkup.
|
||||||
|
EZ-Bkup is released under the General Public License 3.0.
|
||||||
|
See COPYING or <https://www.gnu.org/licenses/> for details.]#
|
||||||
|
|
||||||
|
import owlkettle
|
||||||
|
import "models/routine", "shared"
|
||||||
|
import views/[edit_routine_dialog, routine_list, app_menu_button]
|
||||||
|
|
||||||
|
const APP_NAME = "EZ-Bkup"
|
||||||
|
let databasePath = appPath & "/ez-bkup.sqlite"
|
||||||
|
|
||||||
|
viewable App:
|
||||||
|
## The main application
|
||||||
|
routineModel: RoutineModel ## The RoutineModel that stores all routines.
|
||||||
|
|
||||||
|
method view(app: AppState): Widget {.locks: "unknown".} =
|
||||||
|
result = gui:
|
||||||
|
Window:
|
||||||
|
title = APP_NAME
|
||||||
|
defaultSize = (1024, 768)
|
||||||
|
HeaderBar {.addTitlebar.}:
|
||||||
|
Icon {.addLeft.}:
|
||||||
|
name = "EZ-Bkup-icon"
|
||||||
|
pixelSize = 40
|
||||||
|
margin = 4
|
||||||
|
# Button to open the main menu
|
||||||
|
AppMenuButton {.addRight.}:
|
||||||
|
routineModel = app.routineModel
|
||||||
|
Box:
|
||||||
|
orient = OrientY
|
||||||
|
spacing = 6
|
||||||
|
#don't use available vertical space from parent. Only what's needed for children.
|
||||||
|
Box(orient = OrientX) {.expand: false.}:
|
||||||
|
Label:
|
||||||
|
text = "<span size=\"x-large\">Bkup Routines</span>"
|
||||||
|
xAlign = 0.05
|
||||||
|
useMarkup = true
|
||||||
|
#expand horizontally to push buttons to the right.
|
||||||
|
Box(orient = OrientX) {.expand: true.}
|
||||||
|
#expanding box containing buttons would fight the previous box for available horiz space placing buttons in the middle.
|
||||||
|
Box(orient = OrientX, spacing = 6, margin = 6) {.expand: false.}:
|
||||||
|
# Button to create a new routine
|
||||||
|
Button:
|
||||||
|
style = [ButtonSuggested]
|
||||||
|
text = "+ Routine"
|
||||||
|
proc clicked() =
|
||||||
|
## Opens the EditRoutineDialog for creating a new routine
|
||||||
|
let (res, state) = app.open: gui:
|
||||||
|
EditRoutineDialog(mode = EditRoutineCreate)
|
||||||
|
if res.kind == DialogAccept:
|
||||||
|
# The "Create" button was clicked
|
||||||
|
app.routineModel.add(EditRoutineDialogState(state).routine)
|
||||||
|
|
||||||
|
# Main content of the window: Contains the list of all routines.
|
||||||
|
Box:
|
||||||
|
orient = OrientY
|
||||||
|
margin = 6
|
||||||
|
spacing = 12
|
||||||
|
Frame:
|
||||||
|
RoutineList:
|
||||||
|
routineModel = app.routineModel
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
# Entry point for the application.
|
||||||
|
# Loads the model from the database and starts the application.
|
||||||
|
let model = newRoutineModel(databasePath)
|
||||||
|
brew("org.itwrx.EZ-Bkup", gui(App(routineModel = model)), icons=["icons/"], stylesheets=[loadStylesheet("styles.css")])
|
||||||
|
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="entity-edit.svg"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs8" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview6"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.6640625"
|
||||||
|
inkscape:cx="256"
|
||||||
|
inkscape:cy="256"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1012"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="44"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg4" />
|
||||||
|
<!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<path
|
||||||
|
d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"
|
||||||
|
id="path2" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;stroke:#ffffff;stroke-width:0.639399"
|
||||||
|
d="M 20.131455,511.30159 C 12.633018,510.11175 5.9356881,504.94557 2.5273571,497.72214 0.80908211,494.08053 0.61907627,493.12506 0.64628672,488.26291 0.6759534,482.96188 1.0418939,481.61881 19.071696,420.63829 29.189024,386.4194 38.315718,356.26729 39.35324,353.6336 c 2.635465,-6.68998 7.229742,-14.82621 11.729142,-20.77171 2.512161,-3.31956 44.396616,-45.61057 122.247588,-123.4342 L 291.75609,91.043174 356.35998,155.64706 420.96387,220.25095 300.92325,340.17902 C 171.1693,469.81131 178.1297,463.17622 165.19474,469.56391 c -2.93974,1.45173 -7.33514,3.35072 -9.76757,4.21997 -7.84741,2.80437 -126.489492,37.44519 -129.286325,37.74871 -1.487324,0.16141 -4.191549,0.0574 -6.00939,-0.231 z"
|
||||||
|
id="path183" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;stroke:#ffffff;stroke-width:0.639399"
|
||||||
|
d="M 379.49299,132.50707 314.89566,67.907648 341.03468,41.748365 c 17.00808,-17.021254 27.66441,-27.217667 30.50572,-29.189106 15.62214,-10.8394197 34.50594,-14.2580075 52.72251,-9.5445076 6.15681,1.5930596 16.91628,6.8355343 21.93427,10.6873156 2.14836,1.649065 14.6086,13.775928 27.68942,26.948586 25.38314,25.561343 27.63744,28.252973 31.83856,38.015094 4.0333,9.372178 5.06587,14.641025 5.01643,25.597163 -0.0377,8.36936 -0.24566,10.37066 -1.60466,15.44762 -1.90565,7.11912 -5.3524,14.6212 -9.39319,20.44494 -1.97605,2.84794 -12.19763,13.52857 -29.3429,30.66069 l -26.31052,26.29034 z"
|
||||||
|
id="path185" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/></svg>
|
After Width: | Height: | Size: 551 B |
|
@ -0,0 +1,187 @@
|
||||||
|
import std/[tables, strutils, sugar, hashes, algorithm, os]
|
||||||
|
from std/sequtils import toSeq
|
||||||
|
import tiny_sqlite
|
||||||
|
import "../shared"
|
||||||
|
|
||||||
|
# We create a custom type for representing the ID of a routine.
|
||||||
|
# This way Nim's type system automatically prevents us from using it as a regular integer and
|
||||||
|
# from confusing it with the ID of another entity.
|
||||||
|
|
||||||
|
type RoutineId* = distinct int64
|
||||||
|
|
||||||
|
proc `==`*(a, b: RoutineId): bool {.borrow.}
|
||||||
|
proc hash*(id: RoutineId): Hash {.borrow.}
|
||||||
|
proc `$`*(id: RoutineId): string =
|
||||||
|
result = "routine" & $int64(id)
|
||||||
|
|
||||||
|
type Routine* = object
|
||||||
|
id*: RoutineId
|
||||||
|
name*: string
|
||||||
|
selected*: bool
|
||||||
|
#selByDef == selected by default.
|
||||||
|
selByDef*: bool
|
||||||
|
sources*: seq[string]
|
||||||
|
destinations*: seq[string]
|
||||||
|
|
||||||
|
proc stringToSeq(commaSepString: string):seq[string] =
|
||||||
|
#return toSeq(commaSepString.split(','))
|
||||||
|
var newSeq: seq[string]
|
||||||
|
if hasCommas(commaSepString) == true:
|
||||||
|
newSeq = toSeq(commaSepString.split(','))
|
||||||
|
else:
|
||||||
|
newSeq = @[commaSepString]
|
||||||
|
|
||||||
|
return newSeq
|
||||||
|
|
||||||
|
proc seqToString(strSeq: seq[string]):string =
|
||||||
|
#return strSeq.join(",")
|
||||||
|
var newStr: string
|
||||||
|
if strSeq.len >= 1:
|
||||||
|
newStr = strSeq.join(",")
|
||||||
|
else:
|
||||||
|
newStr = ""
|
||||||
|
|
||||||
|
return newStr
|
||||||
|
|
||||||
|
#[proc matches*(routine: Routine, filter: string): bool =
|
||||||
|
## Checks if the routine matches the given filter.
|
||||||
|
## This function is used to search the list of routines.
|
||||||
|
filter.toLowerAscii() in toLowerAscii(routine.name)]#
|
||||||
|
|
||||||
|
type RoutineModel* = ref object
|
||||||
|
## Model for storing all routines. We model this as a ref object, so that changes
|
||||||
|
## made by any widget are also known to all other widgets that use the model.
|
||||||
|
|
||||||
|
db: DbConn
|
||||||
|
routines*: Table[RoutineId, Routine]
|
||||||
|
|
||||||
|
proc cmpRoutines(a, b: Routine): int =
|
||||||
|
cmp(a.name, b.name)
|
||||||
|
|
||||||
|
|
||||||
|
#this is our stand-in for the search proc. returns a seq so we can iterate in routine_list.
|
||||||
|
proc routineSeq*(model: RoutineModel): seq[Routine] =
|
||||||
|
var routineSeq: seq[Routine]
|
||||||
|
## Returns a seq of all routines. "id" is necessary to loop through table.
|
||||||
|
for id, routine in model.routines:
|
||||||
|
routineSeq.add(routine)
|
||||||
|
|
||||||
|
routineSeq.sort(cmpRoutines, Ascending)
|
||||||
|
|
||||||
|
#echo routineSeq
|
||||||
|
|
||||||
|
return routineSeq
|
||||||
|
|
||||||
|
let homePath = expandTilde("~")
|
||||||
|
|
||||||
|
proc newRoutineModel*(path: string = homePath & "/.ez-bkup/ez-bkup.sqlite"): RoutineModel =
|
||||||
|
## Load a RoutineModel from a database
|
||||||
|
|
||||||
|
var db: DbConn
|
||||||
|
try:
|
||||||
|
db = openDatabase(path)
|
||||||
|
except:
|
||||||
|
writeErrorToLog("Unable to open and/or create sqlite3 database. Please make sure sqlite3 is installed.")
|
||||||
|
db = openDatabase("")
|
||||||
|
|
||||||
|
# Create the Routine table
|
||||||
|
db.exec("""
|
||||||
|
CREATE TABLE IF NOT EXISTS Routine(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
selByDef INTEGER,
|
||||||
|
sources TEXT,
|
||||||
|
destinations TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Load all existing routines into RoutineModel.routines
|
||||||
|
var routines = initTable[RoutineId, Routine]()
|
||||||
|
for row in db.iterate("SELECT id, name, selByDef, sources, destinations FROM Routine"):
|
||||||
|
let (id, name, selByDef, sources, destinations) = row.unpack((RoutineId, string, bool, string, string))
|
||||||
|
|
||||||
|
var sourcesSeq: seq[string]
|
||||||
|
#no sources exist in DB.
|
||||||
|
if sources == "":
|
||||||
|
sourcesSeq = @[]
|
||||||
|
else:
|
||||||
|
#convert "sources" comma-separated string to seq[string].
|
||||||
|
sourcesSeq = stringToSeq(sources)
|
||||||
|
|
||||||
|
var destinationsSeq: seq[string]
|
||||||
|
#no destinations exist in DB.
|
||||||
|
if destinations == "":
|
||||||
|
destinationsSeq = @[]
|
||||||
|
else:
|
||||||
|
#convert "destinations" comma-separated string to seq[string].
|
||||||
|
destinationsSeq = stringToSeq(destinations)
|
||||||
|
|
||||||
|
routines[id] = Routine(id: id, name: name, selByDef: selByDef, sources: sourcesSeq, destinations: destinationsSeq)
|
||||||
|
|
||||||
|
#echo "newRoutineModel:"
|
||||||
|
#echo routines
|
||||||
|
|
||||||
|
result = RoutineModel(db: db, routines: routines)
|
||||||
|
|
||||||
|
proc add*(model: RoutineModel, routine: Routine) =
|
||||||
|
## Adds a new routine to the model
|
||||||
|
|
||||||
|
#convert routine.sources seq[string] to a comma-separated string to store as TEXT in sqlite.
|
||||||
|
let stringSources = seqToString(routine.sources)
|
||||||
|
|
||||||
|
#convert routine.destinations seq[string] to a comma-separated string to store as TEXT in sqlite.
|
||||||
|
let stringDestinations = seqToString(routine.destinations)
|
||||||
|
|
||||||
|
# Insert new routine into database
|
||||||
|
model.db.exec(
|
||||||
|
"INSERT INTO Routine (name, selByDef, sources, destinations) VALUES (?, ?, ?, ?)",
|
||||||
|
routine.name, routine.selByDef, stringSources, stringDestinations
|
||||||
|
)
|
||||||
|
|
||||||
|
# Insert new routine into RoutineModel.routines
|
||||||
|
let id = RoutineId(model.db.lastInsertRowId)
|
||||||
|
model.routines[id] = routine.dup(id = id)
|
||||||
|
|
||||||
|
proc update*(model: RoutineModel, routine: Routine) =
|
||||||
|
## Updates an existing routine. Routines are compared by their ID.
|
||||||
|
|
||||||
|
#convert routine.sources seq[string] to a comma-separated string to store as TEXT in sqlite.
|
||||||
|
let stringSources = seqToString(routine.sources)
|
||||||
|
|
||||||
|
#convert routine.destinations seq[string] to a comma-separated string to store as TEXT in sqlite.
|
||||||
|
let stringDestinations = seqToString(routine.destinations)
|
||||||
|
|
||||||
|
# Update routine in database
|
||||||
|
model.db.exec(
|
||||||
|
"UPDATE Routine SET name = ?, selByDef = ?, sources = ?, destinations = ? WHERE id = ?",
|
||||||
|
routine.name, routine.selByDef, stringSources, stringDestinations, routine.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update RoutineModel.routines
|
||||||
|
model.routines[routine.id] = routine
|
||||||
|
|
||||||
|
#[proc search*(model: UserModel, filter: string): seq[User] {.locks: 0.} =
|
||||||
|
## Returns a seq of all users that match the given filter.
|
||||||
|
for id, user in model.users:
|
||||||
|
if user.matches(filter):
|
||||||
|
result.add(user)
|
||||||
|
|
||||||
|
result.sort((a, b: User) => cmp(a.lastName, b.lastName))]#
|
||||||
|
|
||||||
|
proc delete*(model: RoutineModel, id: RoutineId) =
|
||||||
|
## Deletes the routine with the given ID
|
||||||
|
|
||||||
|
# Delete routine from database
|
||||||
|
model.db.exec("DELETE FROM Routine WHERE id = ?", id)
|
||||||
|
|
||||||
|
# Update RoutineModel.routines
|
||||||
|
model.routines.del(id)
|
||||||
|
|
||||||
|
proc clear*(model: RoutineModel) =
|
||||||
|
## Deletes all routines
|
||||||
|
|
||||||
|
# Delete all routines from database
|
||||||
|
model.db.exec("DELETE FROM Routine")
|
||||||
|
|
||||||
|
# Update RoutineModel.routines
|
||||||
|
model.routines.clear()
|
|
@ -0,0 +1,83 @@
|
||||||
|
#[Copyright 2023 ITwrx.
|
||||||
|
This file is part of EZ-Bkup.
|
||||||
|
EZ-Bkup is released under the General Public License 3.0.
|
||||||
|
See COPYING or <https://www.gnu.org/licenses/> for details.]#
|
||||||
|
|
||||||
|
import logging, times, os, distros
|
||||||
|
#import "models/routine"
|
||||||
|
|
||||||
|
#store the RoutineIds of ea. Routine selected for the current Bkup run.
|
||||||
|
#[type SelectedRoutine* = object
|
||||||
|
id*: RoutineId
|
||||||
|
name*: string
|
||||||
|
sources*: seq[string]
|
||||||
|
destinations*: seq[string]
|
||||||
|
|
||||||
|
type CurrentRun* = object
|
||||||
|
status*: string]#
|
||||||
|
|
||||||
|
let appPath* = getHomeDir() & ".ez-bkup"
|
||||||
|
|
||||||
|
if not dirExists(appPath):
|
||||||
|
createDir(appPath)
|
||||||
|
|
||||||
|
var logger = newFileLogger(appPath & "/errors.log")
|
||||||
|
let dt = now()
|
||||||
|
let nowDT = dt.format("M-d-YYYY h:mm:ss tt")
|
||||||
|
var logMsg: string
|
||||||
|
|
||||||
|
proc writeErrorToLog*(logMsg: string) =
|
||||||
|
logger.log(lvlError, logMsg)
|
||||||
|
|
||||||
|
proc writeInfoToLog*(logMsg: string) =
|
||||||
|
logger.log(lvlInfo, logMsg)
|
||||||
|
|
||||||
|
proc hasCommas*(filename: string):bool =
|
||||||
|
',' in filename
|
||||||
|
|
||||||
|
proc getAskPassPath*(): string =
|
||||||
|
var askPassPath: string
|
||||||
|
if detectOs(Fedora):
|
||||||
|
if fileExists("/usr/libexec/openssh/ssh-askpass"):
|
||||||
|
askPassPath = "/usr/libexec/openssh/ssh-askpass"
|
||||||
|
else:
|
||||||
|
writeErrorToLog("No ssh-askpass binary found. Please run 'dnf install openssh-askpass'")
|
||||||
|
askPassPath = ""
|
||||||
|
elif detectOs(Ubuntu):
|
||||||
|
if fileExists("/usr/lib/openssh/gnome-ssh-askpass"):
|
||||||
|
askPassPath = "/usr/lib/openssh/gnome-ssh-askpass"
|
||||||
|
else:
|
||||||
|
writeErrorToLog("No ssh-askpass binary found. Please run 'sudo apt install ssh-askpass-gnome'.")
|
||||||
|
askPassPath = ""
|
||||||
|
#save this for a musl build.
|
||||||
|
#[elif detectOs(Alpine):
|
||||||
|
if fileExists("/usr/lib/ssh/gtk-ssh-askpass"):
|
||||||
|
askPassPath = "/usr/lib/ssh/gtk-ssh-askpass"
|
||||||
|
else:
|
||||||
|
writeErrorToLog("No ssh-askpass binary found. Please run 'apk add gtk-ssh-askpass'.")
|
||||||
|
askPassPath = ""]#
|
||||||
|
elif detectOs(ArchLinux):
|
||||||
|
if fileExists("/usr/lib/ssh/ssh-askpass"):
|
||||||
|
askPassPath = "/usr/lib/ssh/ssh-askpass"
|
||||||
|
else:
|
||||||
|
writeErrorToLog("No ssh-askpass binary found. Please run 'pacman -S x11-ssh-askpass', or similar.")
|
||||||
|
askPassPath = ""
|
||||||
|
elif detectOs(Linux):
|
||||||
|
if fileExists("/usr/libexec/openssh/ssh-askpass"):
|
||||||
|
askPassPath = "/usr/libexec/openssh/ssh-askpass"
|
||||||
|
elif fileExists("/usr/lib/openssh/gnome-ssh-askpass"):
|
||||||
|
askPassPath = "/usr/lib/openssh/gnome-ssh-askpass"
|
||||||
|
elif fileExists("/usr/lib/ssh/gtk-ssh-askpass"):
|
||||||
|
askPassPath = "/usr/lib/ssh/gtk-ssh-askpass"
|
||||||
|
elif fileExists("/usr/lib/ssh/ssh-askpass"):
|
||||||
|
askPassPath = "/usr/lib/ssh/ssh-askpass"
|
||||||
|
else:
|
||||||
|
writeErrorToLog("No ssh-askpass binary found. Please install an ssh-askpass package for your distro, and let us know if EZ-Bkup still can't detect it's location.")
|
||||||
|
askPassPath = ""
|
||||||
|
else:
|
||||||
|
writeErrorToLog("Your OS does not appear to be supported at this time. If you are getting this error and you are using a x86_64 gnu libc-based linux distribution please report this issue. Please include the path to your ssh-askpass binary, as well.")
|
||||||
|
askPassPath = ""
|
||||||
|
|
||||||
|
return askPassPath
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
.aboutdialog box stack {font-size: 22px;}
|
||||||
|
.aboutdialog box label {font-size: 20px;}
|
||||||
|
.aboutdialog headerbar label {font-size: 16px;}
|
||||||
|
.aboutdialog stack scrolledwindow viewport grid {font-size: 22px;}
|
||||||
|
.error {color: #ff6b6b;}
|
|
@ -0,0 +1,42 @@
|
||||||
|
#[Copyright 2023 ITwrx.
|
||||||
|
This file is part of EZ-Bkup.
|
||||||
|
EZ-Bkup is released under the General Public License 3.0.
|
||||||
|
See COPYING or <https://www.gnu.org/licenses/> for details.]#
|
||||||
|
|
||||||
|
import owlkettle
|
||||||
|
import ../models/routine
|
||||||
|
|
||||||
|
viewable AppMenuButton:
|
||||||
|
## A button that opens the main menu of the application
|
||||||
|
|
||||||
|
routineModel: RoutineModel
|
||||||
|
|
||||||
|
method view(button: AppMenuButtonState): Widget {.locks: "unknown".} =
|
||||||
|
result = gui:
|
||||||
|
MenuButton:
|
||||||
|
icon = "open-menu-symbolic"
|
||||||
|
|
||||||
|
# A menu is created using the PopoverMenu widget.
|
||||||
|
# It allows us to create menus & submenus.
|
||||||
|
PopoverMenu:
|
||||||
|
Box:
|
||||||
|
orient = OrientY
|
||||||
|
Box(orient = OrientX, spacing=6, margin=6) {.expand: true.}:
|
||||||
|
Button:
|
||||||
|
icon = "help-about-symbolic"
|
||||||
|
style = [ButtonSuggested]
|
||||||
|
proc clicked() =
|
||||||
|
discard button.app.open: gui:
|
||||||
|
AboutDialog:
|
||||||
|
programName = "EZ-Bkup"
|
||||||
|
logo = "EZ-Bkup-icon"
|
||||||
|
style = [StyleClass("about-dialog")]
|
||||||
|
version = "0.9.0"
|
||||||
|
credits = @{
|
||||||
|
"Created By": @["https://ITwrx.org"],
|
||||||
|
"License": @["GPLv3"]
|
||||||
|
}
|
||||||
|
Label:
|
||||||
|
text = "About EZ-Bkup"
|
||||||
|
|
||||||
|
export AppMenuButton
|
|
@ -0,0 +1,41 @@
|
||||||
|
#[Copyright 2023 ITwrx.
|
||||||
|
This file is part of EZ-Bkup.
|
||||||
|
EZ-Bkup is released under the General Public License 3.0.
|
||||||
|
See COPYING or <https://www.gnu.org/licenses/> for details.]#
|
||||||
|
|
||||||
|
import owlkettle
|
||||||
|
import ../models/routine
|
||||||
|
import routine_editor
|
||||||
|
|
||||||
|
type EditRoutineDialogMode* = enum
|
||||||
|
EditRoutineCreate = "Create"
|
||||||
|
EditRoutineUpdate = "Update"
|
||||||
|
|
||||||
|
viewable EditRoutineDialog:
|
||||||
|
## A dialog for editing a routine. We use the same dialog for creating and updating
|
||||||
|
## a routine. Since we want to use different titles and labels for the buttons in each case,
|
||||||
|
## the EditRoutineDialog.mode field specifies the purpose of the dialog.
|
||||||
|
|
||||||
|
routine: Routine ## The routine being edited
|
||||||
|
mode: EditRoutineDialogMode ## Purpose of the dialog (create/update)
|
||||||
|
|
||||||
|
method view(dialog: EditRoutineDialogState): Widget {.locks: "unknown".} =
|
||||||
|
result = gui:
|
||||||
|
Dialog:
|
||||||
|
title = $dialog.mode & " Routine"
|
||||||
|
defaultSize = (800, 600)
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
# Create / Update Button
|
||||||
|
text = $dialog.mode
|
||||||
|
style = [ButtonSuggested]
|
||||||
|
res = DialogAccept
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Cancel"
|
||||||
|
res = DialogCancel
|
||||||
|
# Content
|
||||||
|
RoutineEditor:
|
||||||
|
routine = dialog.routine
|
||||||
|
proc changed(routine: Routine) =
|
||||||
|
dialog.routine = routine
|
||||||
|
|
||||||
|
export EditRoutineDialog, EditRoutineDialogState
|
|
@ -0,0 +1,221 @@
|
||||||
|
#[Copyright 2023 ITwrx.
|
||||||
|
This file is part of EZ-Bkup.
|
||||||
|
EZ-Bkup is released under the General Public License 3.0.
|
||||||
|
See COPYING or <https://www.gnu.org/licenses/> for details.]#
|
||||||
|
|
||||||
|
import owlkettle
|
||||||
|
import "../models/routine", "../shared"
|
||||||
|
|
||||||
|
viewable RoutineEditor:
|
||||||
|
## A form for editing a Routine
|
||||||
|
|
||||||
|
routine: Routine ## The routine that is being edited
|
||||||
|
|
||||||
|
# Since Routine is passed by value, we need to notify the parent widget of
|
||||||
|
# any changes to the routine. The changed callback
|
||||||
|
proc changed(routine: Routine)
|
||||||
|
|
||||||
|
method view(editor: RoutineEditorState): Widget {.locks: "unknown".} =
|
||||||
|
result = gui:
|
||||||
|
#must have top level container that everything else is inside of.
|
||||||
|
Box:
|
||||||
|
orient = OrientY
|
||||||
|
margin = 12
|
||||||
|
spacing = 12
|
||||||
|
Box {.expand: false.}:
|
||||||
|
orient = OrientX
|
||||||
|
spacing = 6
|
||||||
|
Label {.expand: false.}:
|
||||||
|
text = "<span weight=\"bold\">Name:</span>"
|
||||||
|
xAlign = 0
|
||||||
|
useMarkup = true
|
||||||
|
Entry {.expand: false.}:
|
||||||
|
text = editor.routine.name
|
||||||
|
xAlign = 0
|
||||||
|
proc changed(text: string) =
|
||||||
|
editor.routine.name = text
|
||||||
|
# Call the changed callback
|
||||||
|
if not editor.changed.isNil:
|
||||||
|
editor.changed.callback(editor.routine)
|
||||||
|
Box {.expand: false.}:
|
||||||
|
orient = OrientX
|
||||||
|
spacing = 6
|
||||||
|
Label {.expand: false.}:
|
||||||
|
#later, this may be "selected by default?", instead.
|
||||||
|
#"selected" checkboxes wiould be added to the list view.
|
||||||
|
#to allow the user to select which routines are used for a given run.
|
||||||
|
text = "<span weight=\"bold\">Enabled?</span>"
|
||||||
|
xAlign = 0
|
||||||
|
useMarkup = true
|
||||||
|
Switch {.expand: false.}:
|
||||||
|
state = editor.routine.selByDef
|
||||||
|
proc changed(state: bool) =
|
||||||
|
editor.routine.selByDef = state
|
||||||
|
if not editor.changed.isNil:
|
||||||
|
editor.changed.callback(editor.routine)
|
||||||
|
Label {.expand: false.}:
|
||||||
|
text = "<span weight=\"bold\">Sources:</span>"
|
||||||
|
xAlign = 0
|
||||||
|
useMarkup = true
|
||||||
|
Box {.expand: false.}:
|
||||||
|
spacing = 6
|
||||||
|
Button {.expand: false.}:
|
||||||
|
text = "+ File"
|
||||||
|
style = [ButtonSuggested]
|
||||||
|
proc clicked() =
|
||||||
|
let (res, state) = editor.app.open: gui:
|
||||||
|
FileChooserDialog:
|
||||||
|
title = "Add Source Files to Routine"
|
||||||
|
action = FileChooserOpen
|
||||||
|
selectMultiple = true
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Cancel"
|
||||||
|
res = DialogCancel
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Confirm"
|
||||||
|
res = DialogAccept
|
||||||
|
style = [ButtonSuggested]
|
||||||
|
if res.kind == DialogAccept:
|
||||||
|
for filename in FileChooserDialogState(state).filenames:
|
||||||
|
if hasCommas(filename):
|
||||||
|
discard editor.app.open: gui:
|
||||||
|
MessageDialog:
|
||||||
|
message = "Commas in Source filename/path are not supported."
|
||||||
|
style = [StyleClass("error")]
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Ok"
|
||||||
|
res = DialogAccept
|
||||||
|
elif filename in editor.routine.sources:
|
||||||
|
discard editor.app.open: gui:
|
||||||
|
MessageDialog:
|
||||||
|
message = "Duplicate Routine Source filenames (" & filename & ") are not supported."
|
||||||
|
style = [StyleClass("error")]
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Ok"
|
||||||
|
res = DialogAccept
|
||||||
|
else:
|
||||||
|
editor.routine.sources.add(filename)
|
||||||
|
if not editor.changed.isNil:
|
||||||
|
editor.changed.callback(editor.routine)
|
||||||
|
Button {.expand: false.}:
|
||||||
|
text = "+ Folder"
|
||||||
|
style = [ButtonSuggested]
|
||||||
|
proc clicked() =
|
||||||
|
let (res, state) = editor.app.open: gui:
|
||||||
|
FileChooserDialog:
|
||||||
|
title = "Add Source Folders to Routine"
|
||||||
|
action = FileChooserSelectFolder
|
||||||
|
selectMultiple = true
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Cancel"
|
||||||
|
res = DialogCancel
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Confirm"
|
||||||
|
res = DialogAccept
|
||||||
|
style = [ButtonSuggested]
|
||||||
|
if res.kind == DialogAccept:
|
||||||
|
for filename in FileChooserDialogState(state).filenames:
|
||||||
|
if hasCommas(filename):
|
||||||
|
discard editor.app.open: gui:
|
||||||
|
MessageDialog:
|
||||||
|
message = "Commas in Source filename/path are not supported."
|
||||||
|
style = [StyleClass("error")]
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Ok"
|
||||||
|
res = DialogAccept
|
||||||
|
elif filename in editor.routine.sources:
|
||||||
|
discard editor.app.open: gui:
|
||||||
|
MessageDialog:
|
||||||
|
message = "Duplicate Routine Source (" & filename & ") not supported."
|
||||||
|
style = [StyleClass("error")]
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Ok"
|
||||||
|
res = DialogAccept
|
||||||
|
else:
|
||||||
|
editor.routine.sources.add(filename)
|
||||||
|
if not editor.changed.isNil:
|
||||||
|
editor.changed.callback(editor.routine)
|
||||||
|
Box {.expand: false.}:
|
||||||
|
orient = OrientY
|
||||||
|
margin = 6
|
||||||
|
spacing = 6
|
||||||
|
#routineSeq returns an empty seq when no sources exist.
|
||||||
|
if editor.routine.sources.len != 0:
|
||||||
|
for it, routineSource in editor.routine.sources:
|
||||||
|
Box:
|
||||||
|
orient = OrientX
|
||||||
|
spacing = 6
|
||||||
|
Label:
|
||||||
|
text = routineSource
|
||||||
|
xAlign = 0
|
||||||
|
Button {.expand: false.}:
|
||||||
|
icon = "user-trash-symbolic"
|
||||||
|
proc clicked() =
|
||||||
|
editor.routine.sources.delete(it)
|
||||||
|
if not editor.changed.isNil:
|
||||||
|
editor.changed.callback(editor.routine)
|
||||||
|
Label {.expand: false.}:
|
||||||
|
text = "<span weight=\"bold\">Destinations:</span>"
|
||||||
|
xAlign = 0
|
||||||
|
useMarkup = true
|
||||||
|
Box {.expand: false.}:
|
||||||
|
spacing = 6
|
||||||
|
Button {.expand: false.}:
|
||||||
|
text = "+ Folder"
|
||||||
|
style = [ButtonSuggested]
|
||||||
|
proc clicked() =
|
||||||
|
let (res, state) = editor.app.open: gui:
|
||||||
|
FileChooserDialog:
|
||||||
|
title = "Add Destination Folders to Routine"
|
||||||
|
action = FileChooserSelectFolder
|
||||||
|
selectMultiple = true
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Cancel"
|
||||||
|
res = DialogCancel
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Confirm"
|
||||||
|
res = DialogAccept
|
||||||
|
style = [ButtonSuggested]
|
||||||
|
if res.kind == DialogAccept:
|
||||||
|
for filename in FileChooserDialogState(state).filenames:
|
||||||
|
if hasCommas(filename):
|
||||||
|
discard editor.app.open: gui:
|
||||||
|
MessageDialog:
|
||||||
|
message = "Commas in Destination filename/path are not supported."
|
||||||
|
style = [StyleClass("error")]
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Ok"
|
||||||
|
res = DialogAccept
|
||||||
|
elif filename in editor.routine.destinations:
|
||||||
|
discard editor.app.open: gui:
|
||||||
|
MessageDialog:
|
||||||
|
message = "Duplicate Routine Destination (" & filename & ") not supported."
|
||||||
|
style = [StyleClass("error")]
|
||||||
|
DialogButton {.addButton.}:
|
||||||
|
text = "Ok"
|
||||||
|
res = DialogAccept
|
||||||
|
else:
|
||||||
|
editor.routine.destinations.add(filename)
|
||||||
|
if not editor.changed.isNil:
|
||||||
|
editor.changed.callback(editor.routine)
|
||||||
|
Box {.expand: false.}:
|
||||||
|
orient = OrientY
|
||||||
|
margin = 6
|
||||||
|
spacing = 6
|
||||||
|
#routineSeq returns an empty seq when no destinations exist.
|
||||||
|
if editor.routine.destinations.len != 0:
|
||||||
|
for it, routineDestination in editor.routine.destinations:
|
||||||
|
Box:
|
||||||
|
orient = OrientX
|
||||||
|
spacing = 6
|
||||||
|
Label:
|
||||||
|
text = routineDestination
|
||||||
|
xAlign = 0
|
||||||
|
Button {.expand: false.}:
|
||||||
|
icon = "user-trash-symbolic"
|
||||||
|
proc clicked() =
|
||||||
|
editor.routine.destinations.delete(it)
|
||||||
|
if not editor.changed.isNil:
|
||||||
|
editor.changed.callback(editor.routine)
|
||||||
|
|
||||||
|
export RoutineEditor
|
|
@ -0,0 +1,219 @@
|
||||||
|
#[Copyright 2023 ITwrx.
|
||||||
|
This file is part of EZ-Bkup.
|
||||||
|
EZ-Bkup is released under the General Public License 3.0.
|
||||||
|
See COPYING or <https://www.gnu.org/licenses/> for details.]#
|
||||||
|
|
||||||
|
import owlkettle
|
||||||
|
import std/osproc
|
||||||
|
import edit_routine_dialog
|
||||||
|
import "../models/routine", "../shared"
|
||||||
|
|
||||||
|
##preload selectedRoutines using routines' selByDef field value from DB.
|
||||||
|
#var selectedRoutines: seq[SelectedRoutine]
|
||||||
|
|
||||||
|
viewable RoutineList:
|
||||||
|
## Displays a list of routines
|
||||||
|
|
||||||
|
#filter: string ## Filter used to search for routines
|
||||||
|
routineModel: RoutineModel ## Model of all routines
|
||||||
|
#selectedRoutines: selectedRoutines
|
||||||
|
#selectedRoutines: seq[SelectedRoutine]
|
||||||
|
runStatus: string
|
||||||
|
|
||||||
|
#proc changed(currentRun: CurrentRun)
|
||||||
|
proc changed(state: bool)
|
||||||
|
|
||||||
|
method view(list: RoutineListState): Widget {.locks: "unknown".} =
|
||||||
|
result = gui:
|
||||||
|
ScrolledWindow:
|
||||||
|
Box:
|
||||||
|
orient = OrientY
|
||||||
|
if list.routineModel.routineSeq().len() > 0:
|
||||||
|
ListBox:
|
||||||
|
#for routine in list.model.search(list.filter):
|
||||||
|
for it, routine in list.routineModel.routineSeq():
|
||||||
|
Box:
|
||||||
|
orient = OrientY
|
||||||
|
margin = 6
|
||||||
|
spacing = 6
|
||||||
|
Box:
|
||||||
|
orient = OrientX
|
||||||
|
margin = 6
|
||||||
|
spacing = 6
|
||||||
|
#[Switch {.expand: false.}:
|
||||||
|
state = routine.selByDef
|
||||||
|
proc changed(state: bool) =
|
||||||
|
#if state == true:
|
||||||
|
#list.selectedRoutines.ids.add(routine.id)
|
||||||
|
|
||||||
|
if not list.changed.isNil:
|
||||||
|
list.changed.callback(list.selectedRoutines) ]#
|
||||||
|
#[CheckButton {.expand: false.}:
|
||||||
|
state = routine.selected
|
||||||
|
#if routine.selected == true:
|
||||||
|
#echo list.currentRun.selected
|
||||||
|
#list.currentRun.selected[it] = true
|
||||||
|
#state = t
|
||||||
|
#state = list.currentRun.selectedRoutines[it]
|
||||||
|
proc changed(state: bool) =
|
||||||
|
list.currentRun.selectedRoutines.add(routine.id)
|
||||||
|
if routine.id in list.currentRun.selectedRoutines:
|
||||||
|
= state
|
||||||
|
#echo "hehe"
|
||||||
|
#list.currentRun.selectedRoutines[it] = state ]#
|
||||||
|
|
||||||
|
Label:
|
||||||
|
#text = "<span size=\"large\" weight=\"bold\">" & routine.name & "</span>"
|
||||||
|
text = "<span size=\"large\">" & routine.name & "</span>"
|
||||||
|
xAlign = 0 # Align left
|
||||||
|
useMarkup = true
|
||||||
|
# Edit Button
|
||||||
|
Button {.expand: false.}:
|
||||||
|
#icon = "entity-edit"
|
||||||
|
icon = "entity-edit-dark-theme"
|
||||||
|
proc clicked() =
|
||||||
|
## Opens the EditRoutineDialog for updating the existing routine
|
||||||
|
let (res, state) = list.app.open: gui:
|
||||||
|
EditRoutineDialog:
|
||||||
|
routine = routine
|
||||||
|
mode = EditRoutineUpdate
|
||||||
|
if res.kind == DialogAccept:
|
||||||
|
# The "Update" button was clicked
|
||||||
|
list.routineModel.update(EditRoutineDialogState(state).routine)
|
||||||
|
# Delete Button
|
||||||
|
Button {.expand: false.}:
|
||||||
|
icon = "user-trash-symbolic"
|
||||||
|
proc clicked() =
|
||||||
|
list.routineModel.delete(routine.id)
|
||||||
|
if routine.selByDef:
|
||||||
|
Box:
|
||||||
|
orient = OrientY
|
||||||
|
margin = 6
|
||||||
|
spacing = 6
|
||||||
|
Label:
|
||||||
|
text = "<span color=\"#6fffa3\">Sources:</span>"
|
||||||
|
xAlign = 0
|
||||||
|
useMarkup = true
|
||||||
|
#routineSeq returns an empty string when no sources exist.
|
||||||
|
if routine.sources.len != 0:
|
||||||
|
for it, routineSource in routine.sources:
|
||||||
|
Box:
|
||||||
|
orient = OrientX
|
||||||
|
spacing = 6
|
||||||
|
margin = 6
|
||||||
|
Label:
|
||||||
|
text = routineSource
|
||||||
|
xAlign = 0
|
||||||
|
else:
|
||||||
|
Box:
|
||||||
|
orient = OrientX
|
||||||
|
spacing = 6
|
||||||
|
margin = 6
|
||||||
|
Label:
|
||||||
|
text = "<span color=\"#ff6b6b\">Routine will be ignored. Source required.</span>"
|
||||||
|
xAlign = 0
|
||||||
|
useMarkup = true
|
||||||
|
Label:
|
||||||
|
text = "<span color=\"#6fffa3\">Destinations:</span>"
|
||||||
|
xAlign = 0
|
||||||
|
useMarkup = true
|
||||||
|
#routineSeq returns an empty string when no destinations exist.
|
||||||
|
if routine.destinations.len != 0:
|
||||||
|
for it, routineDestination in routine.destinations:
|
||||||
|
Box:
|
||||||
|
orient = OrientX
|
||||||
|
spacing = 6
|
||||||
|
margin = 6
|
||||||
|
Label:
|
||||||
|
text = routineDestination
|
||||||
|
xAlign = 0
|
||||||
|
else:
|
||||||
|
Box:
|
||||||
|
orient = OrientX
|
||||||
|
spacing = 6
|
||||||
|
margin = 6
|
||||||
|
Label:
|
||||||
|
text = "<span color=\"#ff6b6b\">Routine will be ignored. Destination required.</span>"
|
||||||
|
xAlign = 0
|
||||||
|
useMarkup = true
|
||||||
|
Box {.expand: false.}:
|
||||||
|
orient = OrientX
|
||||||
|
spacing = 6
|
||||||
|
margin = 6
|
||||||
|
Label {.expand: false.}:
|
||||||
|
text = "<span size=\"large\">Status: </span>"
|
||||||
|
xAlign = 0
|
||||||
|
useMarkup = true
|
||||||
|
Label:
|
||||||
|
if list.runStatus != "":
|
||||||
|
text = list.runStatus
|
||||||
|
else:
|
||||||
|
text = "<span color=\"#6AC9FF\" size=\"large\">Waiting patiently...</span>"
|
||||||
|
xAlign = 0
|
||||||
|
margin = 6
|
||||||
|
useMarkup = true
|
||||||
|
# Run Button
|
||||||
|
Button {.expand: false.}:
|
||||||
|
Box:
|
||||||
|
orient = OrientX
|
||||||
|
spacing = 6
|
||||||
|
margin = 6
|
||||||
|
Icon:
|
||||||
|
name = "media-floppy"
|
||||||
|
pixelSize = 40
|
||||||
|
margin = 4
|
||||||
|
Label {.expand: false.}:
|
||||||
|
text = "<span size=\"large\">Run Bkup!</span>"
|
||||||
|
xAlign = 0
|
||||||
|
useMarkup = true
|
||||||
|
proc clicked() =
|
||||||
|
list.runStatus = "<span color=\"#FFE97B\" size=\"large\">Running Bkup...</span>"
|
||||||
|
var rsyncRun: tuple[output: string, exitCode: int]
|
||||||
|
var rsyncErrors: seq[string]
|
||||||
|
var routineRunCount = 0
|
||||||
|
for routine in list.routineModel.routineSeq():
|
||||||
|
#using this as "selected" for now.
|
||||||
|
if routine.selByDef == true:
|
||||||
|
#skip routines that don't have at least one source and one destination.
|
||||||
|
#list.routineModel.routineSeq() returns an empty seq so len != 0 when empty...
|
||||||
|
if routine.sources.len != 0 and routine.destinations.len != 0:
|
||||||
|
routineRuncount += 1
|
||||||
|
for source in routine.sources:
|
||||||
|
for destination in routine.destinations:
|
||||||
|
list.runStatus = "<span color=\"#FFE97B\" size=\"large\">Bkup " & source & " to " & destination & "...</span>"
|
||||||
|
#try without requiring superuser privs by default.
|
||||||
|
rsyncRun = execCmdEx("rsync -aq " & source & " " & destination)
|
||||||
|
if rsyncRun.exitCode != 0:
|
||||||
|
#handle permission denied error.
|
||||||
|
if rsyncRun.exitCode == 23:
|
||||||
|
let rsyncRunCmd = "SUDO_ASKPASS=" & getAskPassPath() & " sudo -A rsync -aq " & source & " " & destination
|
||||||
|
rsyncRun = execCmdEx(rsyncRunCmd)
|
||||||
|
if rsyncRun.exitCode != 0:
|
||||||
|
rsyncErrors.add("EZ-Bkup's rsync process(es) returned error (" & $rsyncRun.output & ") while attempting to back up " & source & " to " & destination)
|
||||||
|
else:
|
||||||
|
rsyncErrors.add("EZ-Bkup's rsync process(es) returned error (" & $rsyncRun.output & ") while attempting to back up " & source & " to " & destination)
|
||||||
|
#makes the "Bkup Complete" msg below wait on the rsyncRun to finish.
|
||||||
|
if rsyncRun.exitCode == 1 or rsyncRun.exitCode == 0:
|
||||||
|
if rsyncErrors.len > 0:
|
||||||
|
list.runStatus = "<span color=\"#ff6b6b\" size=\"large\">Error! Please see the log at ~/.ez-bkup/errors.log</span>"
|
||||||
|
for err in rsyncErrors:
|
||||||
|
writeErrorToLog(err)
|
||||||
|
echo err
|
||||||
|
elif routineRunCount == 0:
|
||||||
|
list.runStatus = "<span color=\"#FFA651\" size=\"large\">Meh. No Bkup Routines were run.</span>"
|
||||||
|
else:
|
||||||
|
list.runStatus = "<span color=\"#6fffa3\" size=\"large\">Bkup Complete!</span>"
|
||||||
|
else:
|
||||||
|
Box {.expand: false.}:
|
||||||
|
orient = OrientY
|
||||||
|
margin = 6
|
||||||
|
spacing = 12
|
||||||
|
Label:
|
||||||
|
text = "<span size=\"large\">You don't have any Bkup Routines!!! 🙂</span>"
|
||||||
|
xAlign = 0
|
||||||
|
useMarkup = true
|
||||||
|
Label:
|
||||||
|
text = "<span size=\"large\">Please click on the + Routine button above to create your first Routine.</span>"
|
||||||
|
xAlign = 0
|
||||||
|
useMarkup = true
|
||||||
|
export RoutineList
|
Loading…
Reference in New Issue