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