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.
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.9 KiB |
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