#[Copyright 2023 ITwrx. This file is part of EZ-Bkup. EZ-Bkup is released under the General Public License 3.0. See COPYING or for details.]# 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 #selected by default. selByDef*: bool sources*: seq[string] destinations*: seq[string] proc stringToSeq(commaSepString: string):seq[string] = var newSeq: seq[string] if hasCommas(commaSepString) == true: newSeq = toSeq(commaSepString.split(',')) else: newSeq = @[commaSepString] return newSeq proc seqToString(strSeq: seq[string]):string = var newStr: string if strSeq.len >= 1: newStr = strSeq.join(",") else: newStr = "" return newStr proc locationsExist*(routine: Routine): bool = var logMsg: string var exists: seq[int] for source in routine.sources: if dirExists(source) or fileExists(source): exists.add(1) else: logMsg = "(" & nowDT & ")" & " Source not found: " & source writeErrorToLog(logMsg) exists.add(0) for dest in routine.destinations: if dirExists(dest) or fileExists(dest): exists.add(1) else: logMsg = "(" & nowDT & ")" & " Destination not found: " & dest writeErrorToLog(logMsg) exists.add(0) if 0 in exists: return false else: return true 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) #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) 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) 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 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) #preload the selected seq with routines that have selByDef == true. proc selectedPreload*(): seq[RoutineId] = var selected: seq[RoutineId] let model = newRoutineModel(databasePath) for routine in model.routineSeq(): if routine.selByDef == true: selected.add(routine.id) return selected