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