From 0c6b4aaf3ab5d29400fd727bc740161cbe300434 Mon Sep 17 00:00:00 2001 From: Akeoot Date: Mon, 29 Dec 2025 01:37:53 +0100 Subject: [PATCH 01/11] remove(cli): delete obsolete cli project and implementation - Remove CLI project file entirely as backend now handles command parsing - Remove cssync.Cli from .sln following this change --- cssync.sln | 15 ---- src/cssync.Cli/Program.cs | 147 ------------------------------- src/cssync.Cli/cssync.Cli.csproj | 14 --- 3 files changed, 176 deletions(-) delete mode 100644 src/cssync.Cli/Program.cs delete mode 100644 src/cssync.Cli/cssync.Cli.csproj diff --git a/cssync.sln b/cssync.sln index 86bd7d2..f0852e2 100644 --- a/cssync.sln +++ b/cssync.sln @@ -7,8 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cssync.Backend", "src\cssync.Backend\cssync.Backend.csproj", "{7B696CFB-B168-4EF0-9152-9F12A7728080}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cssync.Cli", "src\cssync.Cli\cssync.Cli.csproj", "{FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,24 +29,11 @@ Global {7B696CFB-B168-4EF0-9152-9F12A7728080}.Release|x64.Build.0 = Release|Any CPU {7B696CFB-B168-4EF0-9152-9F12A7728080}.Release|x86.ActiveCfg = Release|Any CPU {7B696CFB-B168-4EF0-9152-9F12A7728080}.Release|x86.Build.0 = Release|Any CPU - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}.Debug|x64.ActiveCfg = Debug|Any CPU - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}.Debug|x64.Build.0 = Debug|Any CPU - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}.Debug|x86.ActiveCfg = Debug|Any CPU - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}.Debug|x86.Build.0 = Debug|Any CPU - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}.Release|Any CPU.Build.0 = Release|Any CPU - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}.Release|x64.ActiveCfg = Release|Any CPU - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}.Release|x64.Build.0 = Release|Any CPU - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}.Release|x86.ActiveCfg = Release|Any CPU - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {7B696CFB-B168-4EF0-9152-9F12A7728080} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {FAB0E13E-20E0-4AD6-8F44-F79D952DFD1A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection EndGlobal diff --git a/src/cssync.Cli/Program.cs b/src/cssync.Cli/Program.cs deleted file mode 100644 index 5029f47..0000000 --- a/src/cssync.Cli/Program.cs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) Ame (Akeoott) . Licensed under the GPLv3 License. -// See the LICENSE file in the repository root for full license text. - -using cssync.Backend; - -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace cssync.Cli; - -internal class MainCli -{ - [DllImport("libc")] - private static extern int isatty(int fd); - public static bool HasTerminal() - { - try - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return !Console.IsOutputRedirected && !Console.IsInputRedirected; - } - else - { - return isatty(0) == 1; // stdin - } - } - catch - { - return false; - } - } - - internal static async Task Main() - { - if (!HasTerminal()) - { - Console.WriteLine("This program must be run from a terminal."); - return; - } - Console.WriteLine(Process.GetCurrentProcess()); - Console.WriteLine("Initiated CLI application. Make sure rclone is configured. Use `rclone config` to configure rclone."); - - await RunCLI(); - } - - internal const string mainOptions = """ - Usage: - > [options] - Options: - > help | See all options - > exit | Exit program - > rclone | Interact directly with rclone - > cssync | Interact with cssync - """; - - internal static async Task RunCLI() - { - string input; - - Console.WriteLine(mainOptions); - Console.WriteLine("\nWARNING: Its highly recommended to use the UI version of cssync (If available)"); - - while (true) - { - input = GetString("\n~\n> "); - - switch (input) - { - case "exit": - return; - - case "rclone": - await InitRclone(); - break; - - case "cssync": - await InitCssync(); - break; - - default: - Console.WriteLine(mainOptions); - break; - } - } - } - - internal static async Task InitRclone() - { - string input; - string response; - - while (true) - { - Console.WriteLine("Enter 'return' to go back"); - input = GetString("\n~\n> rclone "); - - if (input == "return") - { - return; - } - else - { - response = await Rclone.RunRclone(input); - Console.WriteLine(response); - } - } - } - - internal static async Task InitCssync() - { - string input; - string response; - - while (true) - { - Console.WriteLine("Enter 'return' to go back"); - input = GetString("\n~\n> cssync "); - - if (input == "return") - { - return; - } - else - { - response = await Cssync.RunCssync(input); - Console.WriteLine(response); - } - } - } - - internal static string GetString(string prompt) - { - string? value; - do - { - Console.Write(prompt); - value = Console.ReadLine()?.Trim(); - - if (string.IsNullOrWhiteSpace(value)) - { - Console.WriteLine(mainOptions); - } - } while (string.IsNullOrWhiteSpace(value)); - return value; - } -} diff --git a/src/cssync.Cli/cssync.Cli.csproj b/src/cssync.Cli/cssync.Cli.csproj deleted file mode 100644 index f2dfb03..0000000 --- a/src/cssync.Cli/cssync.Cli.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - Exe - net10.0 - enable - enable - - - - - - - From fd7dfcc74e9241f7ebf49b0843199831f43aa905 Mon Sep 17 00:00:00 2001 From: Akeoot Date: Mon, 29 Dec 2025 01:52:00 +0100 Subject: [PATCH 02/11] feat(backend): add argument parsing and terminal detection logic - Implement `HasTerminal()` with cross-platform TTY checks - Add args support for `--help`, `--enable`, `--disable`, and `--force` - Introduce `ParseInput` helper for handling CLI arguments - Validate terminal requirements before running backend - Provide help text and improved usage feedback --- src/cssync.Backend/Program.cs | 61 +++++++++++++- src/cssync.Backend/helper/ParseInput.cs | 102 ++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 src/cssync.Backend/helper/ParseInput.cs diff --git a/src/cssync.Backend/Program.cs b/src/cssync.Backend/Program.cs index 6fd6337..414184d 100644 --- a/src/cssync.Backend/Program.cs +++ b/src/cssync.Backend/Program.cs @@ -3,15 +3,68 @@ using cssync.Backend.helper; +using System.Diagnostics; +using System.Runtime.InteropServices; + namespace cssync.Backend; internal class MainBackend { - internal static async Task Main() + [DllImport("libc")] + private static extern int isatty(int fd); + public static async Task HasTerminal() { - // NOTE: Backend may or may not become completely independent for automated use. - // As long as thats no the case, this may stay unchanged. - // TODO: Rewrite pending once appropriate... + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return !Console.IsOutputRedirected && !Console.IsInputRedirected; + } + else + { + return isatty(0) == 1; // stdin + } + } + catch + { + return false; + } + } + + internal static async Task Main(string[] args) + { + bool needsTerminal = true; + if (args.Length >= 1) + { + (args, needsTerminal) = ParseInput.ForceArgument(args); + } + + if (!await HasTerminal() && !needsTerminal) + { + Log.Critical("This program must be run from a terminal."); + Thread.Sleep(1000); + return; + } + + Log.Debug("Current process: {ProcessName}", Process.GetCurrentProcess()); + + switch (args.Length) + { + case 0: + ParseInput.NoArguments(); + break; + + case 1: + await ParseInput.SingleArgument(args[0]); + break; + + case 2: + await ParseInput.TwoArguments(args[0], args[1]); + break; + default: + Console.WriteLine($"Too many arguments. Use --help for usage."); + break; + } } } diff --git a/src/cssync.Backend/helper/ParseInput.cs b/src/cssync.Backend/helper/ParseInput.cs new file mode 100644 index 0000000..650f635 --- /dev/null +++ b/src/cssync.Backend/helper/ParseInput.cs @@ -0,0 +1,102 @@ +// Copyright (c) Ame (Akeoott) . Licensed under the GPLv3 License. +// See the LICENSE file in the repository root for full license text. + +namespace cssync.Backend.helper; + +internal class ParseInput +{ + internal static void NoArguments() + { + Console.WriteLine("Use --help for available options."); + } + + internal static (string[], bool) ForceArgument(string[] args) + { + bool forced = false; + if (args.Contains("--force") || args.Contains("-f")) + { + Array.Clear(args); + forced = true; + } + return (args, forced); + } + + internal static async Task SingleArgument(string arg) + { + switch (arg) + { + case "--help" or "-h": + Console.WriteLine + ( + """ + Usage: + cssync [flag] [option] | Only one flag and option at a time. + Flags taking options: + --help | Learn how to use the available options + Flags not taking options: + --enable | Enable cssync to start performing rclone operations + --disable | Disable cssync to stop performing rclone operations + --force | Force running this application without a terminal (Can cause issues) + Options: + config | Learn to configure cssync and rclone + rclone | Learn how to use rclone + cssync | Learn how to use cssync + learn-more | Learn the in depth details about cssync + + Please read through the options before using cssync! + """ + ); + break; + + case "--enable" or "-e": + // TODO: Add enabling value to config + Console.WriteLine("This option will come soon..."); + break; + + case "--disable" or "-d": + // TODO: Add disabling value to config + Console.WriteLine("This option will come soon..."); + break; + + default: + Console.WriteLine($"{arg} is unknown. Use --help for available options."); + break; + } + } + + internal static async Task TwoArguments(string flag, string option) + { + // These flags don't accept options + if (flag is "--enable" or "-e" or "--disable" or "-d") + { + Console.WriteLine($"{flag} does not accept options."); + return; + } + + // Only help flag accepts options + if (flag is "--help" or "-h") + { + switch (option) + { + case "config": + case "rclone": + case "cssync": + case "learn-more": + Console.WriteLine( + $""" + This option is not available at the moment. + Check back later for {option} help. + """); + break; + + default: + Console.WriteLine($"{option} is unknown. Use --help for available options."); + break; + } + return; + } + + // Unknown flag with option + Console.WriteLine($"{flag} is unknown. Use --help for available options."); + } +} From f9fe020c8ea2fb0a7e7f2fda8f8d5950cadf13f6 Mon Sep 17 00:00:00 2001 From: Akeoot Date: Mon, 29 Dec 2025 02:10:16 +0100 Subject: [PATCH 03/11] docs(readme): update badges, project description, and structure section - reorganized shields for GitHub last commit, stars, and repo size - clarified project description removing rclone-specific mention - added a Structure section explaining separation of backend and GUI - updated table to reflect backend logic handling and planned GUI interface - simplified note about backend independence and GUI plans --- README.md | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a9ee352..bc7c145 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,29 @@ # cssync + + + + + + | c | s | sync | |:-----:|:-------:|:----:| | Cloud | Storage | Sync | - - > [!WARNING] > This project is WIP and POC Written in C# for simplicity, performance and safety.
-Aiming for cross platform compatibility and support for various cloud storage services using rclone. +Aiming for cross platform compatibility and support for various cloud storage services. + +## Structure + +This project is separated in to parts. -| cssync.Backend | cssync.Cli / cssync.Gui | -|:--------------:|:-----------------------:| -| Handles logic | Handle user input | +| cssync.Backend | cssync.Gui | +|:---------------------:|:---------------------------:| +| Handles all the logic | Handle user input | +| Allow cli access | Provide graphical interface | -> [!NOTE] -> Planing to make backend fully independent from CLI and GUI, -> by just accepting args when running in a terminal. -> Gui is planned and will happen once the backend is finished. +> [!INFO] +> The backend is currently in development and the GUI will come in the future. From ab111cd3776464694eb170125558104cf6db16a8 Mon Sep 17 00:00:00 2001 From: Akeoot Date: Mon, 29 Dec 2025 04:15:20 +0100 Subject: [PATCH 04/11] refactor(backend/config): unify config section handling and add cssync enable/disable - refactored ModifyConfig to use a helper for section/key validation - consolidated type-specific logic for editing, appending, and removing values - updated AppendKey and RemoveKey to support both Variables and Timers sections - added EnableDisableCssync and GetCssyncStatus methods to manage cssync Run state - updated ParseInput to support --status flag and properly enable/disable cssync - removed unused ForceArgument logic and simplified terminal check in Main --- src/cssync.Backend/Program.cs | 9 +- src/cssync.Backend/helper/Json.cs | 7 +- src/cssync.Backend/helper/ModifyConfig.cs | 382 +++++++++------------- src/cssync.Backend/helper/ParseInput.cs | 65 ++-- 4 files changed, 197 insertions(+), 266 deletions(-) diff --git a/src/cssync.Backend/Program.cs b/src/cssync.Backend/Program.cs index 414184d..4ff782d 100644 --- a/src/cssync.Backend/Program.cs +++ b/src/cssync.Backend/Program.cs @@ -33,13 +33,7 @@ public static async Task HasTerminal() internal static async Task Main(string[] args) { - bool needsTerminal = true; - if (args.Length >= 1) - { - (args, needsTerminal) = ParseInput.ForceArgument(args); - } - - if (!await HasTerminal() && !needsTerminal) + if (!await HasTerminal()) { Log.Critical("This program must be run from a terminal."); Thread.Sleep(1000); @@ -47,7 +41,6 @@ internal static async Task Main(string[] args) } Log.Debug("Current process: {ProcessName}", Process.GetCurrentProcess()); - switch (args.Length) { case 0: diff --git a/src/cssync.Backend/helper/Json.cs b/src/cssync.Backend/helper/Json.cs index 4dc95f7..b972afc 100644 --- a/src/cssync.Backend/helper/Json.cs +++ b/src/cssync.Backend/helper/Json.cs @@ -7,8 +7,9 @@ namespace cssync.Backend.helper; internal class Config { + public required bool Run { get; set; } public required Dictionary> Variables { get; set; } - public required Dictionary> Timer { get; set; } + public required Dictionary> Timers { get; set; } } internal class Json @@ -82,10 +83,12 @@ internal static async Task GenConfig() Log.Warn("Config doesn't exist. Generating config"); Config config = new() { + Run = false, Variables = [], - Timer = [], + Timers = [], }; await WriteConfig(config); + Log.Info("Successfully generated config"); } } diff --git a/src/cssync.Backend/helper/ModifyConfig.cs b/src/cssync.Backend/helper/ModifyConfig.cs index b60c7ed..6c85050 100644 --- a/src/cssync.Backend/helper/ModifyConfig.cs +++ b/src/cssync.Backend/helper/ModifyConfig.cs @@ -3,31 +3,42 @@ namespace cssync.Backend.helper; -/// -/// Provides methods to modify the cssync configuration file -/// public class ModifyConfig { /// - /// Edits a single value inside a configuration section at a specific index. + /// Helper to validate section and key, returning the list and expected type /// - /// - /// The configuration section to edit (e.g. "Variables" or "Timer"). - /// - /// - /// The key within the section containing the value list. - /// - /// - /// Zero-based index of the value to edit within the key. - /// - /// - /// The new value to assign. Must be a string for Variables or an int for Timer. - /// + private static async Task<(bool success, Type expectedType, object? list)> GetSectionList(Config config, string section, string key) + { + switch (section) + { + case "Variables": + if (!config.Variables.TryGetValue(key, out List? varList)) + { + Log.Error("Selected key does not exist in Variables"); + return (false, typeof(string), null); + } + return (true, typeof(string), varList); + + case "Timers": + if (!config.Timers.TryGetValue(key, out List? timerList)) + { + Log.Error("Selected key does not exist in Timers"); + return (false, typeof(int), null); + } + return (true, typeof(int), timerList); + + default: + Log.Error("Section '{section}' not found", section); + return (false, null!, null); + } + } + public static async Task EditValue(string section, string key, int location, object value) { + var config = await Json.Deserialize(); Log.Info("Editing value in config at section: {section}, key: {key}, index: {location}", section, key, location); Log.Debug("value: {value}", value); - var config = await Json.Deserialize(); try { @@ -36,67 +47,53 @@ public static async Task EditValue(string section, string key, int location, obj Log.Error("Entered value is null"); return; } - else if (section == "Variables") + + var (success, expectedType, listObj) = await GetSectionList(config, section, key); + if (!success || listObj is null) return; + + if (expectedType == typeof(string)) { - if (!config.Variables.TryGetValue(key, out List? values)) + if (value is not string newValueStr) { - Log.Error("Selected key does not exist"); + Log.Error("New value is an incorrect data type: {value}", value.GetType()); return; } - else if (location < 0 || location >= values.Count) + var list = (List)listObj; + if (location < 0 || location >= list.Count) { Log.Error("Selected value does not exist at location: {location}", location); return; } - else if (value is not string newString) - { - Log.Error("New value is an incorrect data type: {value}", value.GetType()); - return; - } - else if (values[location] == newString) + if (list[location] == newValueStr) { Log.Error("New value is the same as existing one"); return; } - else - { - values[location] = newString; - await Json.WriteConfig(config); - Log.Info("Successfully edited value"); - return; - } + list[location] = newValueStr; } - else if (section == "Timer") + else if (expectedType == typeof(int)) { - if (!config.Timer.TryGetValue(key, out List? values)) + if (value is not int newValueInt) { - Log.Error("Selected key does not exist"); + Log.Error("New value is an incorrect data type: {value}", value.GetType()); return; } - else if (location < 0 || location >= values.Count) + var list = (List)listObj; + if (location < 0 || location >= list.Count) { Log.Error("Selected value does not exist at location: {location}", location); return; } - else if (value is not int newInt) - { - Log.Error("New value is an incorrect data type: {value}", value.GetType()); - return; - } - else if (values[location] == newInt) + if (list[location] == newValueInt) { Log.Error("New value is the same as existing one"); return; } - else - { - values[location] = newInt; - await Json.WriteConfig(config); - Log.Info("Successfully edited value"); - return; - } + list[location] = newValueInt; } - Log.Error("Section not found"); + + await Json.WriteConfig(config); + Log.Info("Successfully edited value"); } catch (Exception ex) { @@ -104,24 +101,11 @@ public static async Task EditValue(string section, string key, int location, obj } } - /// - /// Appends one or more values to an existing list in the configuration. - /// - /// - /// The configuration section to modify (e.g. "Variables" or "Timer"). - /// - /// - /// The key within the section containing the value list. - /// - /// - /// The value(s) to append. Can be a single value or a list of values. - /// Must be a string (or List<string>) for Variables or an int (or List<int>) for Timer. - /// public static async Task AppendValue(string section, string key, object value) { + var config = await Json.Deserialize(); Log.Info("Appending value in config at: {section}, {key}", section, key); Log.Debug("value: {value}", value); - var config = await Json.Deserialize(); try { @@ -130,62 +114,35 @@ public static async Task AppendValue(string section, string key, object value) Log.Error("Entered value is null"); return; } - else if (section == "Variables") + + var (success, expectedType, listObj) = await GetSectionList(config, section, key); + if (!success || listObj is null) return; + + if (expectedType == typeof(string)) { - if (!config.Variables.TryGetValue(key, out List? values)) - { - Log.Error("Selected key does not exist"); - return; - } - else if (value is string newString) - { - values.Add(newString); - await Json.WriteConfig(config); - Log.Info("Successfully appended value"); - return; - } - else if (value is List newStringList) - { - values.AddRange(newStringList); - await Json.WriteConfig(config); - Log.Info("Successfully appended value"); - return; - } + var list = (List)listObj; + if (value is string strVal) list.Add(strVal); + else if (value is List strList) list.AddRange(strList); else { Log.Error("New value is an incorrect data type: {value}", value.GetType()); return; } } - else if (section == "Timer") + else if (expectedType == typeof(int)) { - - if (!config.Timer.TryGetValue(key, out List? values)) - { - Log.Error("Selected key does not exist"); - return; - } - else if (value is int newInt) - { - values.Add(newInt); - await Json.WriteConfig(config); - Log.Info("Successfully appended value"); - return; - } - else if (value is List newIntList) - { - values.AddRange(newIntList); - await Json.WriteConfig(config); - Log.Info("Successfully appended value"); - return; - } + var list = (List)listObj; + if (value is int intVal) list.Add(intVal); + else if (value is List intList) list.AddRange(intList); else { Log.Error("New value is an incorrect data type: {value}", value.GetType()); return; } } - Log.Error("Section not found"); + + await Json.WriteConfig(config); + Log.Info("Successfully appended value"); } catch (Exception ex) { @@ -193,66 +150,39 @@ public static async Task AppendValue(string section, string key, object value) } } - /// - /// Removes a specific value from a list in the configuration at a given index. - /// - /// - /// The configuration section to modify (e.g. "Variables" or "Timer"). - /// - /// - /// The key within the section containing the value list. - /// - /// - /// Zero-based index of the value to remove within the key. - /// public static async Task RemoveValue(string section, string key, int location) { - Log.Info("Removing value in config at section: {section}, key: {key}, index: {location}", section, key, location); var config = await Json.Deserialize(); + Log.Info("Removing value in config at section: {section}, key: {key}, index: {location}", section, key, location); try { - if (section == "Variables") + var (success, expectedType, listObj) = await GetSectionList(config, section, key); + if (!success || listObj is null) return; + + if (expectedType == typeof(string)) { - if (!config.Variables.TryGetValue(key, out List? values)) - { - Log.Error("Selected key does not exist"); - return; - } - else if (location < 0 || location >= values.Count) + var list = (List)listObj; + if (location < 0 || location >= list.Count) { Log.Error("Selected value does not exist at location: {location}", location); return; } - else - { - values.RemoveAt(location); - await Json.WriteConfig(config); - Log.Info("Successfully removed value"); - return; - } + list.RemoveAt(location); } - else if (section == "Timer") + else if (expectedType == typeof(int)) { - if (!config.Timer.TryGetValue(key, out List? values)) - { - Log.Error("Selected key does not exist"); - return; - } - else if (location < 0 || location >= values.Count) + var list = (List)listObj; + if (location < 0 || location >= list.Count) { Log.Error("Selected value does not exist at location: {location}", location); return; } - else - { - values.RemoveAt(location); - await Json.WriteConfig(config); - Log.Info("Successfully removed value"); - return; - } + list.RemoveAt(location); } - Log.Error("Section not found"); + + await Json.WriteConfig(config); + Log.Info("Successfully removed value"); } catch (Exception ex) { @@ -260,53 +190,69 @@ public static async Task RemoveValue(string section, string key, int location) } } - /// - /// Appends a new key to the configuration, initializing it with an empty list. - /// - /// - /// The configuration section where the key should be added (e.g. "Variables" or "Timer"). - /// - /// - /// The new key to add to the section. - /// public static async Task AppendKey(string section, string key) { - Log.Info("Appending key in config at section: {section}, key: {key}", section, key); var config = await Json.Deserialize(); + Log.Info("Appending key in config at section: {section}, key: {key}", section, key); try { - if (section == "Variables") + switch (section) { - if (config.Variables.ContainsKey(key)) - { - Log.Error("Selected key already exists"); - return; - } - else - { - config.Variables[key] = []; - await Json.WriteConfig(config); - Log.Info("Successfully appended key"); + case "Variables": + if (config.Variables.ContainsKey(key)) + { + Log.Error("Selected key already exists"); + return; + } + config.Variables[key] = new List(); + break; + + case "Timer": + if (config.Timers.ContainsKey(key)) + { + Log.Error("Selected key already exists"); + return; + } + config.Timers[key] = new List(); + break; + + default: + Log.Error("Section not found"); return; - } } - else if (section == "Timer") + + await Json.WriteConfig(config); + Log.Info("Successfully appended key"); + } + catch (Exception ex) + { + Log.Error("Something went wrong. {ex}: {ex.Message}", ex, ex.Message); + } + } + + public static async Task RemoveKey(string section, string key) + { + var config = await Json.Deserialize(); + Log.Info("Removing key in config at section: {section}, key: {key}", section, key); + + try + { + bool removed = section switch { - if (config.Timer.ContainsKey(key)) - { - Log.Error("Selected key already exists"); - return; - } - else - { - config.Timer[key] = []; - await Json.WriteConfig(config); - Log.Info("Successfully appended key"); - return; - } + "Variables" => config.Variables.Remove(key), + "Timer" => config.Timers.Remove(key), + _ => throw new InvalidOperationException($"Section '{section}' not found") + }; + + if (!removed) + { + Log.Error("Selected key does not exist"); + return; } - Log.Error("Section not found"); + + await Json.WriteConfig(config); + Log.Info("Successfully removed key"); } catch (Exception ex) { @@ -315,54 +261,50 @@ public static async Task AppendKey(string section, string key) } /// - /// Removes an entire key-value pair from the configuration. + /// Enable or disable cssync from performing rclone operations automatically /// - /// - /// The configuration section containing the key (e.g. "Variables" or "Timer"). - /// - /// - /// The key to remove from the section. - /// - public static async Task RemoveKey(string section, string key) + /// true to enable cssync and false to disable cssync + /// + public static async Task EnableDisableCssync(bool shouldEnable) { - Log.Info("Removing key in config at section: {section}, key: {key}", section, key); var config = await Json.Deserialize(); try { - if (section == "Variables") + if (shouldEnable) { - if (!config.Variables.Remove(key)) - { - Log.Error("Selected key does not exist"); - return; - } - else - { - await Json.WriteConfig(config); - Log.Info("Successfully removed key"); - return; - } + Log.Debug("Enabling cssync"); + config.Run = true; + await Json.WriteConfig(config); + Log.Info("Successfully enabled cssync to perform rclone operations"); } - else if (section == "Timer") + else { - if (!config.Timer.Remove(key)) - { - Log.Error("Selected key does not exist"); - return; - } - else - { - await Json.WriteConfig(config); - Log.Info("Successfully removed key"); - return; - } + Log.Debug("Disabling cssync"); + config.Run = false; + await Json.WriteConfig(config); + Log.Info("Successfully disabled cssync from performing rclone operations"); } - Log.Error("Section not found"); } catch (Exception ex) { Log.Error("Something went wrong. {ex}: {ex.Message}", ex, ex.Message); } } + + public static async Task GetCssyncStatus() + { + var config = await Json.Deserialize(); + Log.Debug("Getting cssync status"); + + try + { + return config.Run; + } + catch (Exception ex) + { + Log.Error("Something went wrong. {ex}: {ex.Message}", ex, ex.Message); + return false; + } + } } diff --git a/src/cssync.Backend/helper/ParseInput.cs b/src/cssync.Backend/helper/ParseInput.cs index 650f635..2303a46 100644 --- a/src/cssync.Backend/helper/ParseInput.cs +++ b/src/cssync.Backend/helper/ParseInput.cs @@ -10,52 +10,45 @@ internal static void NoArguments() Console.WriteLine("Use --help for available options."); } - internal static (string[], bool) ForceArgument(string[] args) - { - bool forced = false; - if (args.Contains("--force") || args.Contains("-f")) - { - Array.Clear(args); - forced = true; - } - return (args, forced); - } - internal static async Task SingleArgument(string arg) { switch (arg) { case "--help" or "-h": - Console.WriteLine - ( + Console.WriteLine( """ Usage: - cssync [flag] [option] | Only one flag and option at a time. - Flags taking options: - --help | Learn how to use the available options - Flags not taking options: - --enable | Enable cssync to start performing rclone operations - --disable | Disable cssync to stop performing rclone operations - --force | Force running this application without a terminal (Can cause issues) - Options: - config | Learn to configure cssync and rclone - rclone | Learn how to use rclone - cssync | Learn how to use cssync - learn-more | Learn the in depth details about cssync + cssync [flag] [help-scope] + Flags: + --help | Learn how to use the available scope + --status | Display if cssync is enabled or disabled + --enable | Enable cssync to start performing rclone operations + --disable | Disable cssync to stop performing rclone operations + Help-Scopes (only usable with the --help flag): + config | Learn to configure cssync and rclone + rclone | Learn how to use rclone + cssync | Learn how to use cssync + more | Learn the in depth details about cssync Please read through the options before using cssync! - """ - ); + """); + break; + + case "--status" or "-s": + if (await ModifyConfig.GetCssyncStatus()) + Console.WriteLine("cssync is currently enabled"); + else + Console.WriteLine("cssync is currently disabled"); break; case "--enable" or "-e": - // TODO: Add enabling value to config - Console.WriteLine("This option will come soon..."); + await ModifyConfig.EnableDisableCssync(true); + Console.WriteLine("Successfully enabled cssync to perform rclone operations"); break; case "--disable" or "-d": - // TODO: Add disabling value to config - Console.WriteLine("This option will come soon..."); + await ModifyConfig.EnableDisableCssync(false); + Console.WriteLine("Successfully disabled cssync from performing rclone operations"); break; default: @@ -81,12 +74,12 @@ internal static async Task TwoArguments(string flag, string option) case "config": case "rclone": case "cssync": - case "learn-more": + case "more": Console.WriteLine( - $""" - This option is not available at the moment. - Check back later for {option} help. - """); + $""" + This option is not available at the moment. + Check back later for {option} help. + """); break; default: From 598425971018305667f1b4e4839b85b6bff6794d Mon Sep 17 00:00:00 2001 From: Akeoot Date: Tue, 30 Dec 2025 20:00:08 +0100 Subject: [PATCH 05/11] refactor(backend): centralize help messages and update status handling - replace inline help text in ParseInput with Resources static strings - introduce Resources.cs to store help, config, rclone and cssync messages - update ParseInput to use central help resources and add --init flag - rename ModifyConfig.GetCssyncStatus() to GetStatus() for consistency - improve unknown flag handling and compatibility error messaging - extend .editorconfig to format json and jsonc files with 2-space indents --- .editorconfig | 2 +- src/cssync.Backend/helper/ModifyConfig.cs | 2 +- src/cssync.Backend/helper/ParseInput.cs | 49 +++----- src/cssync.Backend/helper/Resources.cs | 130 ++++++++++++++++++++++ 4 files changed, 149 insertions(+), 34 deletions(-) create mode 100644 src/cssync.Backend/helper/Resources.cs diff --git a/.editorconfig b/.editorconfig index 8e6f778..d67494b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -45,6 +45,6 @@ insert_final_newline = false # Indent style for other files -[*.{csproj,resx,xml}] +[*.{csproj,resx,xml,json,jsonc}] indent_style = space indent_size = 2 diff --git a/src/cssync.Backend/helper/ModifyConfig.cs b/src/cssync.Backend/helper/ModifyConfig.cs index 6c85050..e7518fb 100644 --- a/src/cssync.Backend/helper/ModifyConfig.cs +++ b/src/cssync.Backend/helper/ModifyConfig.cs @@ -292,7 +292,7 @@ public static async Task EnableDisableCssync(bool shouldEnable) } } - public static async Task GetCssyncStatus() + public static async Task GetStatus() { var config = await Json.Deserialize(); Log.Debug("Getting cssync status"); diff --git a/src/cssync.Backend/helper/ParseInput.cs b/src/cssync.Backend/helper/ParseInput.cs index 2303a46..5dbad62 100644 --- a/src/cssync.Backend/helper/ParseInput.cs +++ b/src/cssync.Backend/helper/ParseInput.cs @@ -15,40 +15,20 @@ internal static async Task SingleArgument(string arg) switch (arg) { case "--help" or "-h": - Console.WriteLine( - """ - Usage: - cssync [flag] [help-scope] - Flags: - --help | Learn how to use the available scope - --status | Display if cssync is enabled or disabled - --enable | Enable cssync to start performing rclone operations - --disable | Disable cssync to stop performing rclone operations - Help-Scopes (only usable with the --help flag): - config | Learn to configure cssync and rclone - rclone | Learn how to use rclone - cssync | Learn how to use cssync - more | Learn the in depth details about cssync - - Please read through the options before using cssync! - """); + Console.WriteLine(Resources.Help); break; case "--status" or "-s": - if (await ModifyConfig.GetCssyncStatus()) + if (await ModifyConfig.GetStatus()) Console.WriteLine("cssync is currently enabled"); else Console.WriteLine("cssync is currently disabled"); break; - case "--enable" or "-e": - await ModifyConfig.EnableDisableCssync(true); - Console.WriteLine("Successfully enabled cssync to perform rclone operations"); - break; - - case "--disable" or "-d": - await ModifyConfig.EnableDisableCssync(false); - Console.WriteLine("Successfully disabled cssync from performing rclone operations"); + case "--init" or "-i": + Console.WriteLine("Generating config"); + await Json.GenConfig(); + Console.WriteLine("Finished generating config"); break; default: @@ -72,14 +52,19 @@ internal static async Task TwoArguments(string flag, string option) switch (option) { case "config": + Console.WriteLine(Resources.HelpConfig); + break; + case "rclone": + Console.WriteLine(Resources.HelpRclone); + break; + case "cssync": + Console.WriteLine(Resources.HelpCssync); + break; + case "more": - Console.WriteLine( - $""" - This option is not available at the moment. - Check back later for {option} help. - """); + Console.WriteLine(Resources.HelpMore); break; default: @@ -90,6 +75,6 @@ This option is not available at the moment. } // Unknown flag with option - Console.WriteLine($"{flag} is unknown. Use --help for available options."); + Console.WriteLine($"{flag} is unknown or not compatible with {option}. Use --help for available options."); } } diff --git a/src/cssync.Backend/helper/Resources.cs b/src/cssync.Backend/helper/Resources.cs new file mode 100644 index 0000000..4b066b3 --- /dev/null +++ b/src/cssync.Backend/helper/Resources.cs @@ -0,0 +1,130 @@ +// Copyright (c) Ame (Akeoott) . Licensed under the GPLv3 License. +// See the LICENSE file in the repository root for full license text. + +namespace cssync.Backend.helper; + +internal static class Resources +{ + internal const string Help = """ + cssync is work in progress. If you find any bugs or have suggestions, + please open an issue on github Akeoott/cssync, any help is appreciated! + + Usage: + cssync [flag] [help-scope] + Flags: + --help | Learn how to use the available scope + --status | Display if cssync is enabled or disabled + --init | Generate the config + Help-Scopes (only usable with the --help flag): + config | Learn to configure cssync and rclone + rclone | Learn how to use rclone + cssync | Learn how to use cssync + more | Learn the in-depth details about cssync + + Please read through the options before using cssync! + """; + + internal const string HelpConfig = """ + Note (ignore till the GUI is available): + When using this application without the GUI, + configuration must be done manually. + + How to configure cssync: + cssync currently stores config.json next to the application binary in all cases. + If the config does not exist, run this application with --init to generate one. + Once you found the config, open it and manually edit its values. + The default config is going to look like this: + { + "Run": false, + "Variables": {}, + "Timers": {} + } + + "Run": + It's basically an on and off switch: + If "Run" = true, it will run cssync. + If "Run" = false, it will not run cssync. + + "Variables": + Stores all your rclone presets. + Each key in "Variables" is an ordered list of rclone commands that run sequentially. + Here is an example on how "Variables" can look like: + "Variables": { + "key1": [ + "sync ~/Pictures remote:Pictures/", + "delete remote:Pictures/" + ], + "key2": [ + "sync ~/Downloads remote:Backup/", + "delete remote:Backup/" + ] + }, + + IMPORTANT: + A timer is required to execute a Variable. + For a timer to work, it must have the exact same name as the Variable it affects. + + "Variables" may contain multiple keys. + Each key is a user-chosen name that represents a group of rclone commands. + + For more information about rclone, run the application with --help rclone + + "Timers": + Stores a delay time in seconds before executing the matching Variable. + 1200 in "Timers" equals a 20 minute delay. + 3600 in "Timers" equals a 1 hour delay. + Here is an example on how "Timers" can look like: + "Timers": { + "key1": [ + 1200 + ], + "key2": [ + 3600 + ] + }, + + For a timer to work, it must have the exact same name as the Variable it affects. + Otherwise, it will not work. A timer is required to execute a variable. + + Note: + Although timers are arrays internally (for future expansion), + each timer must contain exactly ONE integer value today. + + Full config example: + { + "Run": true, + "Variables": { + "key1": [ + "sync ~/Pictures remote:Pictures/", + "sync ~/Videos remote:Videos/" + ], + "key2": [ + "sync ~/Documents remote:Backup/" + ] + }, + "Timers": { + "key1": [ + 3600 + ], + "key2": [ + 1200 + ] + }, + } + + How to configure rclone: + Open your terminal and enter rclone config. + Follow the instructions provided by rclone. + If you're unsure how to correctly configure rclone, + please read the official rclone documentation for your remote, + aka cloud storage provider or watch a YouTube video explaining it. + + For more information about rclone, run the application with --help rclone + + https://rclone.org/docs/ + """; + + internal const string HelpRclone = "This option is currently under development! Please try again later."; + internal const string HelpCssync = "This option is currently under development! Please try again later."; + internal const string HelpMore = "This option is currently under development! Please try again later."; +} From 1f3d294f371440549e109e7a6b1c00ddc15859a8 Mon Sep 17 00:00:00 2001 From: Akeoot Date: Tue, 30 Dec 2025 20:09:22 +0100 Subject: [PATCH 06/11] refactor(backend): simplify cssync entry and prepare for execution flow - remove legacy cssyncOptions string and unused command scaffolding - change RunCssync signature to no-arg Task to align with upcoming flow - add status check to prevent running when disabled in config - update Program.cs to reference new Cssync.RunCssync usage (currently commented) - leave TODO for future Cssync execution logic --- src/cssync.Backend/Cssync.cs | 29 +++++++++-------------------- src/cssync.Backend/Program.cs | 2 ++ 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/cssync.Backend/Cssync.cs b/src/cssync.Backend/Cssync.cs index 9733385..1c9c277 100644 --- a/src/cssync.Backend/Cssync.cs +++ b/src/cssync.Backend/Cssync.cs @@ -5,28 +5,17 @@ namespace cssync.Backend; +/// +/// Reads config and acts on the data it contains +/// public static class Cssync { - internal const string cssyncOptions = """ - Usage: - > cssync [option] [scope] [args] - Options: - > cssync help | See all options - > cssync list | Display entire json config - > cssync run | Run a variable - > cssync edit | Edit a value in config - > cssync append | Append a value to config - > cssync remove | Remove a value from config - Scopes: - help | See usage and expanded info for each option - variables | Configure your Variables (rclone command presets) - timer | Configure the Timer (when commands periodically execute) - """; - - public static async Task RunCssync(string option = "help", string? scope = null, params string[] args) + public static async Task RunCssync() { - string warningMsg = "Cssync has not been implemented yet! Please check the main branch out for the latest updates."; - Log.Warn(warningMsg); - return warningMsg; + if (!await ModifyConfig.GetStatus()) + { + Log.Info("cssync is currently disabled by config"); + } + // TODO: Add Cssync logic } } diff --git a/src/cssync.Backend/Program.cs b/src/cssync.Backend/Program.cs index 4ff782d..9830e00 100644 --- a/src/cssync.Backend/Program.cs +++ b/src/cssync.Backend/Program.cs @@ -59,5 +59,7 @@ internal static async Task Main(string[] args) Console.WriteLine($"Too many arguments. Use --help for usage."); break; } + + // await Cssync.RunCssync(); } } From 544555a7d44c82be20d71cd405727691fd98f753 Mon Sep 17 00:00:00 2001 From: Akeoot Date: Wed, 31 Dec 2025 03:17:48 +0100 Subject: [PATCH 07/11] refactor(backend): migrate config handling to Newtonsoft.Json and simplify ModifyConfig - Added Newtonsoft.Json dependency for flexible JSON handling - Replaced System.Text.Json with Newtonsoft.Json for serialization/deserialization - Refactored Config class to use a single JObject "Sections" instead of separate Variables/Timers dictionaries - Updated Json helper to handle reading, writing, and generating config using JObject - Updated ModifyConfig to work with JObject for all CRUD operations on config keys and arrays - Simplified Enable/Disable cssync logic and value editing logic - Improved logging for array manipulations and key existence checks - Removed redundant try/catch blocks where validation handles errors - Updated GetStatus/GetConfigStatus and other methods to align with new JObject structure --- src/cssync.Backend/cssync.Backend.csproj | 1 + src/cssync.Backend/helper/Json.cs | 106 +++---- src/cssync.Backend/helper/ModifyConfig.cs | 328 ++++++---------------- 3 files changed, 133 insertions(+), 302 deletions(-) diff --git a/src/cssync.Backend/cssync.Backend.csproj b/src/cssync.Backend/cssync.Backend.csproj index 9e7a117..0a559f7 100644 --- a/src/cssync.Backend/cssync.Backend.csproj +++ b/src/cssync.Backend/cssync.Backend.csproj @@ -10,6 +10,7 @@ + diff --git a/src/cssync.Backend/helper/Json.cs b/src/cssync.Backend/helper/Json.cs index b972afc..ee90ca3 100644 --- a/src/cssync.Backend/helper/Json.cs +++ b/src/cssync.Backend/helper/Json.cs @@ -1,45 +1,22 @@ // Copyright (c) Ame (Akeoott) . Licensed under the GPLv3 License. // See the LICENSE file in the repository root for full license text. -using System.Text.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace cssync.Backend.helper; internal class Config { - public required bool Run { get; set; } - public required Dictionary> Variables { get; set; } - public required Dictionary> Timers { get; set; } + public bool Run { get; set; } = false; + public JObject Sections { get; set; } = new JObject(); } -internal class Json +internal static class Json { - internal static readonly string configPath = AppDomain.CurrentDomain.BaseDirectory + "config.json"; - internal static readonly JsonSerializerOptions options = new() { WriteIndented = true }; + private static readonly string configPath = AppDomain.CurrentDomain.BaseDirectory + "config.json"; + private static DateTime _lastConfigWriteTime = DateTime.MinValue; - /// - /// Get current serialized configuration of cssync. - /// - /// Serialized config - public static async Task GetSerializedConfig() - => Serialize(await Deserialize()); - - /// - /// Serializes input from the Config class - /// - /// json config - /// string containing serialized config - internal static string Serialize(Config input) - { - Log.Debug("Serializing config"); - return JsonSerializer.Serialize(input, options); - } - - /// - /// Deserializes config located next to application. - /// Generates config if not found. - /// - /// Config of application internal static async Task Deserialize() { int attempts = 0; @@ -50,27 +27,38 @@ internal static async Task Deserialize() { await GenConfig(); } + try { Log.Debug("Deserializing config"); - string json = await File.ReadAllTextAsync(configPath); - return JsonSerializer.Deserialize(json) - ?? throw new InvalidDataException("Output is null or empty"); + return JsonConvert.DeserializeObject(json) ?? new Config(); } - catch (Exception ex) when (ex is FileNotFoundException || ex is JsonException || ex is InvalidDataException) + catch (Exception ex) when (ex is FileNotFoundException || ex is JsonException) { attempts++; Log.Critical("Loading config failed (attempt {attempts}/3). {ex}: {ex.Message}", attempts, ex, ex.Message); await Task.Delay(100); } } - throw new InvalidOperationException($"Something went wrong. Failed to get config after {attempts} attempts"); + + throw new InvalidOperationException($"Failed to load config after {attempts} attempts"); + } + + internal static async Task WriteConfig(Config config) + { + try + { + string json = JsonConvert.SerializeObject(config, Formatting.Indented); + await File.WriteAllTextAsync(configPath, json); + Log.Debug("Wrote config to disk"); + } + catch (Exception ex) + { + Log.Critical("Failed to write config. {ex}: {ex.Message}", ex, ex.Message); + } } - /// - /// Generates a json file in application directory if no file was found. - /// internal static async Task GenConfig() { if (File.Exists(configPath)) @@ -78,35 +66,27 @@ internal static async Task GenConfig() Log.Info("Config exists"); return; } - else + + Log.Warn("Config doesn't exist. Generating config"); + var config = new Config { - Log.Warn("Config doesn't exist. Generating config"); - Config config = new() + Sections = new JObject { - Run = false, - Variables = [], - Timers = [], - }; - await WriteConfig(config); - Log.Info("Successfully generated config"); - } + ["Variables"] = new JObject(), + ["Timers"] = new JObject() + } + }; + await WriteConfig(config); + Log.Info("Successfully generated config"); } - /// - /// Writes config to file asynchronously - /// - /// Config to write - internal static async Task WriteConfig(Config config) + internal static async Task GetConfigStatus() { - try - { - string jsonString = Serialize(config); - await File.WriteAllTextAsync(configPath, jsonString); - Log.Debug("Wrote to config"); - } - catch (Exception ex) - { - Log.Critical("Failed to write config. {ex}: {ex.Message}", ex, ex.Message); - } + DateTime currentWriteTime = File.GetLastWriteTimeUtc(configPath); + if (currentWriteTime == _lastConfigWriteTime) return false; + _lastConfigWriteTime = currentWriteTime; + + var config = await Deserialize(); + return config.Run; } } diff --git a/src/cssync.Backend/helper/ModifyConfig.cs b/src/cssync.Backend/helper/ModifyConfig.cs index e7518fb..ed6b22a 100644 --- a/src/cssync.Backend/helper/ModifyConfig.cs +++ b/src/cssync.Backend/helper/ModifyConfig.cs @@ -1,310 +1,160 @@ // Copyright (c) Ame (Akeoott) . Licensed under the GPLv3 License. // See the LICENSE file in the repository root for full license text. +using Newtonsoft.Json.Linq; + namespace cssync.Backend.helper; -public class ModifyConfig +public static class ModifyConfig { - /// - /// Helper to validate section and key, returning the list and expected type - /// - private static async Task<(bool success, Type expectedType, object? list)> GetSectionList(Config config, string section, string key) + private static async Task<(bool success, JToken? section)> GetSection(Config config, string section) { - switch (section) + if (!config.Sections.TryGetValue(section, out JToken? jSection)) { - case "Variables": - if (!config.Variables.TryGetValue(key, out List? varList)) - { - Log.Error("Selected key does not exist in Variables"); - return (false, typeof(string), null); - } - return (true, typeof(string), varList); - - case "Timers": - if (!config.Timers.TryGetValue(key, out List? timerList)) - { - Log.Error("Selected key does not exist in Timers"); - return (false, typeof(int), null); - } - return (true, typeof(int), timerList); - - default: - Log.Error("Section '{section}' not found", section); - return (false, null!, null); + Log.Error("Section '{section}' not found", section); + return (false, null); } + return (true, jSection); } - public static async Task EditValue(string section, string key, int location, object value) + public static async Task EditValue(string section, string key, int index, object value) { var config = await Json.Deserialize(); - Log.Info("Editing value in config at section: {section}, key: {key}, index: {location}", section, key, location); - Log.Debug("value: {value}", value); + var (success, jSection) = await GetSection(config, section); + if (!success || jSection is null) return; - try + if (value is null) { - if (value is null) - { - Log.Error("Entered value is null"); - return; - } - - var (success, expectedType, listObj) = await GetSectionList(config, section, key); - if (!success || listObj is null) return; - - if (expectedType == typeof(string)) - { - if (value is not string newValueStr) - { - Log.Error("New value is an incorrect data type: {value}", value.GetType()); - return; - } - var list = (List)listObj; - if (location < 0 || location >= list.Count) - { - Log.Error("Selected value does not exist at location: {location}", location); - return; - } - if (list[location] == newValueStr) - { - Log.Error("New value is the same as existing one"); - return; - } - list[location] = newValueStr; - } - else if (expectedType == typeof(int)) - { - if (value is not int newValueInt) - { - Log.Error("New value is an incorrect data type: {value}", value.GetType()); - return; - } - var list = (List)listObj; - if (location < 0 || location >= list.Count) - { - Log.Error("Selected value does not exist at location: {location}", location); - return; - } - if (list[location] == newValueInt) - { - Log.Error("New value is the same as existing one"); - return; - } - list[location] = newValueInt; - } - - await Json.WriteConfig(config); - Log.Info("Successfully edited value"); + Log.Error("Entered value is null"); + return; } - catch (Exception ex) - { - Log.Error("Something went wrong. {ex}: {ex.Message}", ex, ex.Message); - } - } - - public static async Task AppendValue(string section, string key, object value) - { - var config = await Json.Deserialize(); - Log.Info("Appending value in config at: {section}, {key}", section, key); - Log.Debug("value: {value}", value); - try + if (jSection[key] is JArray arr) { - if (value is null) + if (index < 0 || index >= arr.Count) { - Log.Error("Entered value is null"); + Log.Error("Index out of range: {index}", index); return; } - - var (success, expectedType, listObj) = await GetSectionList(config, section, key); - if (!success || listObj is null) return; - - if (expectedType == typeof(string)) - { - var list = (List)listObj; - if (value is string strVal) list.Add(strVal); - else if (value is List strList) list.AddRange(strList); - else - { - Log.Error("New value is an incorrect data type: {value}", value.GetType()); - return; - } - } - else if (expectedType == typeof(int)) - { - var list = (List)listObj; - if (value is int intVal) list.Add(intVal); - else if (value is List intList) list.AddRange(intList); - else - { - Log.Error("New value is an incorrect data type: {value}", value.GetType()); - return; - } - } - - await Json.WriteConfig(config); - Log.Info("Successfully appended value"); + arr[index] = JToken.FromObject(value); } - catch (Exception ex) + else { - Log.Error("Something went wrong. {ex}: {ex.Message}", ex, ex.Message); + Log.Error("Key '{key}' does not exist or is not an array", key); + return; } + + await Json.WriteConfig(config); + Log.Info("Edited value successfully"); } - public static async Task RemoveValue(string section, string key, int location) + public static async Task AppendValue(string section, string key, object value) { var config = await Json.Deserialize(); - Log.Info("Removing value in config at section: {section}, key: {key}, index: {location}", section, key, location); + var (success, jSection) = await GetSection(config, section); + if (!success || jSection is null) return; - try + if (value is null) { - var (success, expectedType, listObj) = await GetSectionList(config, section, key); - if (!success || listObj is null) return; - - if (expectedType == typeof(string)) - { - var list = (List)listObj; - if (location < 0 || location >= list.Count) - { - Log.Error("Selected value does not exist at location: {location}", location); - return; - } - list.RemoveAt(location); - } - else if (expectedType == typeof(int)) - { - var list = (List)listObj; - if (location < 0 || location >= list.Count) - { - Log.Error("Selected value does not exist at location: {location}", location); - return; - } - list.RemoveAt(location); - } - - await Json.WriteConfig(config); - Log.Info("Successfully removed value"); + Log.Error("Entered value is null"); + return; } - catch (Exception ex) + + if (jSection[key] == null) { - Log.Error("Something went wrong. {ex}: {ex.Message}", ex, ex.Message); + jSection[key] = new JArray(); } - } - public static async Task AppendKey(string section, string key) - { - var config = await Json.Deserialize(); - Log.Info("Appending key in config at section: {section}, key: {key}", section, key); - - try + if (jSection[key] is JArray arr) { - switch (section) + switch (value) { - case "Variables": - if (config.Variables.ContainsKey(key)) - { - Log.Error("Selected key already exists"); - return; - } - config.Variables[key] = new List(); + case IEnumerable strList: + foreach (var s in strList) arr.Add(s); break; - - case "Timer": - if (config.Timers.ContainsKey(key)) - { - Log.Error("Selected key already exists"); - return; - } - config.Timers[key] = new List(); + case IEnumerable intList: + foreach (var i in intList) arr.Add(i); break; - default: - Log.Error("Section not found"); - return; + arr.Add(JToken.FromObject(value)); + break; } - - await Json.WriteConfig(config); - Log.Info("Successfully appended key"); } - catch (Exception ex) + else { - Log.Error("Something went wrong. {ex}: {ex.Message}", ex, ex.Message); + Log.Error("Key '{key}' is not an array", key); + return; } + + await Json.WriteConfig(config); + Log.Info("Appended value successfully"); } - public static async Task RemoveKey(string section, string key) + public static async Task RemoveValue(string section, string key, int index) { var config = await Json.Deserialize(); - Log.Info("Removing key in config at section: {section}, key: {key}", section, key); + var (success, jSection) = await GetSection(config, section); + if (!success || jSection is null) return; - try + if (jSection[key] is JArray arr) { - bool removed = section switch + if (index < 0 || index >= arr.Count) { - "Variables" => config.Variables.Remove(key), - "Timer" => config.Timers.Remove(key), - _ => throw new InvalidOperationException($"Section '{section}' not found") - }; - - if (!removed) - { - Log.Error("Selected key does not exist"); + Log.Error("Index out of range: {index}", index); return; } - - await Json.WriteConfig(config); - Log.Info("Successfully removed key"); + arr.RemoveAt(index); } - catch (Exception ex) + else { - Log.Error("Something went wrong. {ex}: {ex.Message}", ex, ex.Message); + Log.Error("Key '{key}' does not exist or is not an array", key); + return; } + + await Json.WriteConfig(config); + Log.Info("Removed value successfully"); } - /// - /// Enable or disable cssync from performing rclone operations automatically - /// - /// true to enable cssync and false to disable cssync - /// - public static async Task EnableDisableCssync(bool shouldEnable) + public static async Task AppendKey(string section, string key) { var config = await Json.Deserialize(); + var (success, jSection) = await GetSection(config, section); + if (!success || jSection is null) return; - try - { - if (shouldEnable) - { - Log.Debug("Enabling cssync"); - config.Run = true; - await Json.WriteConfig(config); - Log.Info("Successfully enabled cssync to perform rclone operations"); - } - else - { - Log.Debug("Disabling cssync"); - config.Run = false; - await Json.WriteConfig(config); - Log.Info("Successfully disabled cssync from performing rclone operations"); - } - } - catch (Exception ex) + if (jSection[key] != null) { - Log.Error("Something went wrong. {ex}: {ex.Message}", ex, ex.Message); + Log.Error("Key '{key}' already exists", key); + return; } + + jSection[key] = new JArray(); + await Json.WriteConfig(config); + Log.Info("Appended key successfully"); } - public static async Task GetStatus() + public static async Task RemoveKey(string section, string key) { var config = await Json.Deserialize(); - Log.Debug("Getting cssync status"); + var (success, jSection) = await GetSection(config, section); + if (!success || jSection is null) return; - try - { - return config.Run; - } - catch (Exception ex) + if (!((JObject)jSection).Remove(key)) { - Log.Error("Something went wrong. {ex}: {ex.Message}", ex, ex.Message); - return false; + Log.Error("Key '{key}' does not exist", key); + return; } + + await Json.WriteConfig(config); + Log.Info("Removed key successfully"); + } + + public static async Task EnableDisableCssync(bool enable) + { + var config = await Json.Deserialize(); + config.Run = enable; + await Json.WriteConfig(config); + Log.Info(enable + ? "Enabled cssync" + : "Disabled cssync"); } } From 3986bb2b0380a1888a2cc2d22239b32785d9ba36 Mon Sep 17 00:00:00 2001 From: Akeoot Date: Wed, 31 Dec 2025 04:00:19 +0100 Subject: [PATCH 08/11] docs: update HelpConfig instructions and add builds to .gitignore - Refined HelpConfig text for clarity and conciseness - Simplified explanations of "Run", "Variables", and "Timers" - Updated examples and formatting for easier reading - Included guidance for rclone setup with links - Added 'builds' folder to .gitignore to ignore local build artifacts --- .gitignore | 1 + src/cssync.Backend/helper/Resources.cs | 174 +++++++++++-------------- 2 files changed, 78 insertions(+), 97 deletions(-) diff --git a/.gitignore b/.gitignore index d3f0629..4e2a877 100644 --- a/.gitignore +++ b/.gitignore @@ -105,6 +105,7 @@ StyleCopReport.xml *.vspscc *.vssscc .builds +builds *.pidb *.svclog *.scc diff --git a/src/cssync.Backend/helper/Resources.cs b/src/cssync.Backend/helper/Resources.cs index 4b066b3..35720e0 100644 --- a/src/cssync.Backend/helper/Resources.cs +++ b/src/cssync.Backend/helper/Resources.cs @@ -25,103 +25,83 @@ cssync [flag] [help-scope] """; internal const string HelpConfig = """ - Note (ignore till the GUI is available): - When using this application without the GUI, - configuration must be done manually. - - How to configure cssync: - cssync currently stores config.json next to the application binary in all cases. - If the config does not exist, run this application with --init to generate one. - Once you found the config, open it and manually edit its values. - The default config is going to look like this: - { - "Run": false, - "Variables": {}, - "Timers": {} - } - - "Run": - It's basically an on and off switch: - If "Run" = true, it will run cssync. - If "Run" = false, it will not run cssync. - - "Variables": - Stores all your rclone presets. - Each key in "Variables" is an ordered list of rclone commands that run sequentially. - Here is an example on how "Variables" can look like: - "Variables": { - "key1": [ - "sync ~/Pictures remote:Pictures/", - "delete remote:Pictures/" - ], - "key2": [ - "sync ~/Downloads remote:Backup/", - "delete remote:Backup/" - ] - }, - - IMPORTANT: - A timer is required to execute a Variable. - For a timer to work, it must have the exact same name as the Variable it affects. - - "Variables" may contain multiple keys. - Each key is a user-chosen name that represents a group of rclone commands. - - For more information about rclone, run the application with --help rclone - - "Timers": - Stores a delay time in seconds before executing the matching Variable. - 1200 in "Timers" equals a 20 minute delay. - 3600 in "Timers" equals a 1 hour delay. - Here is an example on how "Timers" can look like: - "Timers": { - "key1": [ - 1200 - ], - "key2": [ - 3600 - ] - }, - - For a timer to work, it must have the exact same name as the Variable it affects. - Otherwise, it will not work. A timer is required to execute a variable. - - Note: - Although timers are arrays internally (for future expansion), - each timer must contain exactly ONE integer value today. - - Full config example: - { - "Run": true, - "Variables": { - "key1": [ - "sync ~/Pictures remote:Pictures/", - "sync ~/Videos remote:Videos/" - ], - "key2": [ - "sync ~/Documents remote:Backup/" - ] - }, - "Timers": { - "key1": [ - 3600 - ], - "key2": [ - 1200 - ] - }, - } - - How to configure rclone: - Open your terminal and enter rclone config. - Follow the instructions provided by rclone. - If you're unsure how to correctly configure rclone, - please read the official rclone documentation for your remote, - aka cloud storage provider or watch a YouTube video explaining it. - - For more information about rclone, run the application with --help rclone - - https://rclone.org/docs/ + Configuring cssync (manual editing required until GUI is available): + + Overview: + This application is primarily a GUI for managing rclone operations. Until the GUI is available, + you must configure cssync manually by editing the config.json file located next to the application binary. + + Initializing Config: + Run the application with --init to generate a default config if one does not exist. + Default config structure: + { + "Run": false, + "Variables": {}, + "Timers": {} + } + + Config Fields: + + 1. "Run": + Controls whether cssync is active. + - true => cssync will execute rclone commands + - false => cssync will not execute any commands + + 2. "Variables": + Stores named groups of rclone commands. + - Each key represents a user-defined preset (e.g., "key1"). + - The value is an ordered list of rclone commands that run sequentially. + Example: + "Variables": { + "key1": [ + "sync ~/Pictures remote:Pictures/", + "delete remote:Pictures/" + ], + "key2": [ + "sync ~/Downloads remote:Backup/" + ] + } + + Important: + - Each Variable requires a corresponding Timer with the exact same key name to execute. + - Variables can contain multiple keys, each representing a distinct command group. + - For rclone help, run the application with --help rclone or visit: https://rclone.org/docs/ + + 3. "Timers": + Stores delay times (in seconds) before executing a Variable. + Example: + "Timers": { + "key1": [3600], // 1 hour delay + "key2": [1200] // 20 minute delay + } + + Notes: + - Each Timer key must match a Variable key. + - Currently, only a single integer value per Timer is supported (array reserved for future expansion). + + Full Config Example: + { + "Run": true, + "Variables": { + "key1": [ + "sync ~/Pictures remote:Pictures/", + "sync ~/Videos remote:Videos/" + ], + "key2": [ + "sync ~/Documents remote:Backup/" + ] + }, + "Timers": { + "key1": [3600], + "key2": [1200] + } + } + + Configuring rclone: + 1. Open a terminal and run `rclone config`. + 2. Follow the prompts to set up your remotes. + 3. For detailed guidance, refer to official rclone docs or tutorials: + https://rclone.org/docs/ """; internal const string HelpRclone = "This option is currently under development! Please try again later."; From d7afbf27ab9d553e1f231313a1eb3501a8289ca3 Mon Sep 17 00:00:00 2001 From: Akeoot Date: Wed, 31 Dec 2025 04:01:30 +0100 Subject: [PATCH 09/11] fix(backend): update status check to use Json.GetConfigStatus and remove unused flags --- src/cssync.Backend/helper/ParseInput.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/cssync.Backend/helper/ParseInput.cs b/src/cssync.Backend/helper/ParseInput.cs index 5dbad62..c99618a 100644 --- a/src/cssync.Backend/helper/ParseInput.cs +++ b/src/cssync.Backend/helper/ParseInput.cs @@ -19,7 +19,7 @@ internal static async Task SingleArgument(string arg) break; case "--status" or "-s": - if (await ModifyConfig.GetStatus()) + if (await Json.GetConfigStatus()) Console.WriteLine("cssync is currently enabled"); else Console.WriteLine("cssync is currently disabled"); @@ -39,14 +39,6 @@ internal static async Task SingleArgument(string arg) internal static async Task TwoArguments(string flag, string option) { - // These flags don't accept options - if (flag is "--enable" or "-e" or "--disable" or "-d") - { - Console.WriteLine($"{flag} does not accept options."); - return; - } - - // Only help flag accepts options if (flag is "--help" or "-h") { switch (option) From 6b91a67d8d2158b4f1989133b6c047979f763e5c Mon Sep 17 00:00:00 2001 From: Akeoot Date: Wed, 31 Dec 2025 04:02:58 +0100 Subject: [PATCH 10/11] feat(json, cssync) start implementing continuous Cssync loop based on config - Cssync.RunCssync now monitors config and stops automatically if "Run" is false - Introduced CancellationToken and background task loop (CssyncLoop) - Added Json.Serialize helper for future use --- src/cssync.Backend/Cssync.cs | 27 +++++++++++++++++++++++++-- src/cssync.Backend/helper/Json.cs | 3 +++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/cssync.Backend/Cssync.cs b/src/cssync.Backend/Cssync.cs index 1c9c277..934d200 100644 --- a/src/cssync.Backend/Cssync.cs +++ b/src/cssync.Backend/Cssync.cs @@ -1,6 +1,8 @@ // Copyright (c) Ame (Akeoott) . Licensed under the GPLv3 License. // See the LICENSE file in the repository root for full license text. +using Newtonsoft.Json.Linq; + using cssync.Backend.helper; namespace cssync.Backend; @@ -12,10 +14,31 @@ public static class Cssync { public static async Task RunCssync() { - if (!await ModifyConfig.GetStatus()) + if (!await Json.GetConfigStatus()) { Log.Info("cssync is currently disabled by config"); } - // TODO: Add Cssync logic + + var cts = new CancellationTokenSource(); + var token = cts.Token; + + Task task = Task.Run(() => CssyncLoop(token), token); + + while (true) + { + if (!await Json.GetConfigStatus()) + { + cts.Cancel(); + await Task.WhenAll(task); + Log.Info("cssync was stopped by config"); + return; + } + await Task.Delay(5000); + } + } + + private static async Task CssyncLoop(CancellationToken token) + { + // TODO } } diff --git a/src/cssync.Backend/helper/Json.cs b/src/cssync.Backend/helper/Json.cs index ee90ca3..c1f32f3 100644 --- a/src/cssync.Backend/helper/Json.cs +++ b/src/cssync.Backend/helper/Json.cs @@ -17,6 +17,9 @@ internal static class Json private static readonly string configPath = AppDomain.CurrentDomain.BaseDirectory + "config.json"; private static DateTime _lastConfigWriteTime = DateTime.MinValue; + internal static async Task Serialize(Config config) + => JsonConvert.SerializeObject(config, Formatting.Indented); + internal static async Task Deserialize() { int attempts = 0; From 70c4edcb114a5d8b96610d501d7962f6c371bb0e Mon Sep 17 00:00:00 2001 From: Akeoot Date: Fri, 2 Jan 2026 01:54:31 +0100 Subject: [PATCH 11/11] fix(docs): update README structure by wrapping badges in a div for better formatting --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bc7c145..1147565 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ # cssync - - - - - + +
| c | s | sync | |:-----:|:-------:|:----:|