diff --git a/.editorconfig b/.editorconfig index f6d5add..d67494b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -42,3 +42,9 @@ indent_size = 4 [*.{md,txt}] trim_trailing_whitespace = true insert_final_newline = false + +# Indent style for other files + +[*.{csproj,resx,xml,json,jsonc}] +indent_style = space +indent_size = 2 diff --git a/.github/pr_template_branch_to_dev.md b/.github/pr_template_branch_to_dev.md new file mode 100644 index 0000000..8467c80 --- /dev/null +++ b/.github/pr_template_branch_to_dev.md @@ -0,0 +1,44 @@ +## Description + + + +[Brief description of changes and purpose] + +## Changes + + + +### New Features +- [Feature 1] +- [Feature 2] + +### Bug Fixes +- [Fix 1 - describe issue] +- [Fix 2 - describe issue] + +### Improvements +- [Improvement 1] +- [Improvement 2] + +### Other +- [Any other changes] + +## Additional Information + + + +### Issues + +- Fixes #num +- Closes #num +- Related to #num + +### Checklist +- [ ] Code follows project standards +- [ ] Documentation updated if required +- [ ] No breaking changes introduced +- [ ] Tests pass (if applicable) + +--- + +**Note**: Keep changes focused and minimal. Don't add unrelated "improvements". \ No newline at end of file diff --git a/.github/pr_template_dev_to_main.md b/.github/pr_template_dev_to_main.md new file mode 100644 index 0000000..fcbed28 --- /dev/null +++ b/.github/pr_template_dev_to_main.md @@ -0,0 +1,57 @@ +# Merge dev into main + +## Summary + +[brief summary of major changes]. + +## Changes + + + + + +### Features +* `PullRequestUrl` by @`username` + * `PullRequestTitle` + * Key detail 1 + * Key detail 2 + +### Fixes +* `PullRequestUrl` by @`username` + * `PullRequestTitle` + * What was fixed + * Impact of fix + + + + \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a287bc7..a400bc5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,36 +1,4 @@ -## Description +Please go to the `Preview` tab and select the appropriate sub-template: - - -Description... - -## Changes - - - -### New Features -- - -### Bug Fixes -- - -### Improvements -- - -### Other -- - -## Additional Information - - - -### Issues - -- fixes #num -- closes #num -- resolves #num - -### Checklist -- [ ] Code follows project standards -- [ ] Documentation updated if required -- [ ] Program compiles with no errors \ No newline at end of file +* [Template for branch => dev](?expand=1&template=pr_template_branch_to_dev.md) +* [Template for dev => main](?expand=1&template=pr_template_dev_to_main.md) \ No newline at end of file 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/.vscode/settings.json b/.vscode/settings.json index f4919cd..b142e68 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,10 +4,12 @@ // cSpell "cSpell.words": [ "cssync", + "initializers", "isatty", "konsole", "libc", - "rclone" + "rclone", + "resx" ], "cSpell.caseSensitive": true, "cSpell.autoFormatConfigFile": true, diff --git a/README.md b/README.md index 0550c7b..3245bfa 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,32 @@ # cssync -| c | s | sync | -|:-----:|:-------:|:----:| -| Cloud | Storage | Sync | -
- - - -
+
+ +| c | s | sync | +|:-----:|:-------:|:----:| +| Cloud | Storage | Sync | > [!WARNING] -> This project is WIP +> 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. + +## Structure + +This project is separated in to parts. -Cloud Storage Sync (cssync).
-Written in C# for performance and safety,
-aiming for cross platform compatibility for various cloud storage services using rclone. +| cssync.Backend | cssync.Gui | +|:---------------------:|:---------------------------:| +| Handles all the logic | Handle user input | +| Allow cli access | Provide graphical interface | -| cssync.Backend | cssync.Cli / cssync.Gui | -|:--------------:|:-----------------------:| -| Handles logic | Handle user input | +> [!NOTE] +> The backend is currently in development and the GUI will come in the future. 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.Backend/Cssync.cs b/src/cssync.Backend/Cssync.cs index 9733385..934d200 100644 --- a/src/cssync.Backend/Cssync.cs +++ b/src/cssync.Backend/Cssync.cs @@ -1,32 +1,44 @@ // 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; +/// +/// 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() + { + if (!await Json.GetConfigStatus()) + { + Log.Info("cssync is currently disabled by config"); + } + + 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) { - string warningMsg = "Cssync has not been implemented yet! Please check the main branch out for the latest updates."; - Log.Warn(warningMsg); - return warningMsg; + // TODO } } diff --git a/src/cssync.Backend/Program.cs b/src/cssync.Backend/Program.cs index 6fd6337..9830e00 100644 --- a/src/cssync.Backend/Program.cs +++ b/src/cssync.Backend/Program.cs @@ -3,15 +3,63 @@ 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() + { + 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) { - // 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... + if (!await HasTerminal()) + { + 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; + } + // await Cssync.RunCssync(); } } 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 4dc95f7..c1f32f3 100644 --- a/src/cssync.Backend/helper/Json.cs +++ b/src/cssync.Backend/helper/Json.cs @@ -1,44 +1,25 @@ // 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 Dictionary> Variables { get; set; } - public required Dictionary> Timer { 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()); + internal static async Task Serialize(Config config) + => JsonConvert.SerializeObject(config, Formatting.Indented); - /// - /// 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; @@ -49,27 +30,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)) @@ -77,33 +69,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 { - Variables = [], - Timer = [], - }; - await WriteConfig(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 b60c7ed..ed6b22a 100644 --- a/src/cssync.Backend/helper/ModifyConfig.cs +++ b/src/cssync.Backend/helper/ModifyConfig.cs @@ -1,368 +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; -/// -/// Provides methods to modify the cssync configuration file -/// -public class ModifyConfig +public static class ModifyConfig { - /// - /// Edits a single value inside a configuration section at a specific index. - /// - /// - /// 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. - /// - public static async Task EditValue(string section, string key, int location, object value) + private static async Task<(bool success, JToken? section)> GetSection(Config config, string section) + { + if (!config.Sections.TryGetValue(section, out JToken? jSection)) + { + Log.Error("Section '{section}' not found", section); + return (false, null); + } + return (true, jSection); + } + + public static async Task EditValue(string section, string key, int index, object value) { - 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(); + 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; + } + + if (jSection[key] is JArray arr) + { + if (index < 0 || index >= arr.Count) { - Log.Error("Entered value is null"); + Log.Error("Index out of range: {index}", index); return; } - else if (section == "Variables") - { - if (!config.Variables.TryGetValue(key, out List? values)) - { - Log.Error("Selected key does not exist"); - return; - } - else if (location < 0 || location >= values.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) - { - 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; - } - } - else if (section == "Timer") - { - if (!config.Timer.TryGetValue(key, out List? values)) - { - Log.Error("Selected key does not exist"); - return; - } - else if (location < 0 || location >= values.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) - { - 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; - } - } - Log.Error("Section not found"); + 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"); } - /// - /// 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) { - Log.Info("Appending value in config at: {section}, {key}", section, key); - Log.Debug("value: {value}", value); var config = await Json.Deserialize(); + 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; - } - else if (section == "Variables") - { - 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; - } - else - { - Log.Error("New value is an incorrect data type: {value}", value.GetType()); - return; - } - } - else if (section == "Timer") - { + Log.Error("Entered value is null"); + return; + } + + if (jSection[key] == null) + { + jSection[key] = new JArray(); + } - 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; - } - else - { - Log.Error("New value is an incorrect data type: {value}", value.GetType()); - return; - } + if (jSection[key] is JArray arr) + { + switch (value) + { + case IEnumerable strList: + foreach (var s in strList) arr.Add(s); + break; + case IEnumerable intList: + foreach (var i in intList) arr.Add(i); + break; + default: + arr.Add(JToken.FromObject(value)); + break; } - Log.Error("Section not found"); } - 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"); } - /// - /// 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) + public static async Task RemoveValue(string section, string key, int index) { - Log.Info("Removing value in config at section: {section}, key: {key}, index: {location}", section, key, location); var config = await Json.Deserialize(); + var (success, jSection) = await GetSection(config, section); + if (!success || jSection is null) return; - try + if (jSection[key] is JArray arr) { - if (section == "Variables") - { - if (!config.Variables.TryGetValue(key, out List? values)) - { - Log.Error("Selected key does not exist"); - return; - } - else if (location < 0 || location >= values.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; - } - } - else if (section == "Timer") + if (index < 0 || index >= arr.Count) { - if (!config.Timer.TryGetValue(key, out List? values)) - { - Log.Error("Selected key does not exist"); - return; - } - else if (location < 0 || location >= values.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; - } + Log.Error("Index out of range: {index}", index); + return; } - Log.Error("Section not found"); + 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"); } - /// - /// 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(); + var (success, jSection) = await GetSection(config, section); + if (!success || jSection is null) return; - try - { - if (section == "Variables") - { - 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"); - return; - } - } - else if (section == "Timer") - { - 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; - } - } - Log.Error("Section not found"); - } - 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"); } - /// - /// Removes an entire key-value pair from the configuration. - /// - /// - /// 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) { - Log.Info("Removing key in config at section: {section}, key: {key}", section, key); var config = await Json.Deserialize(); + var (success, jSection) = await GetSection(config, section); + if (!success || jSection is null) return; - try - { - if (section == "Variables") - { - if (!config.Variables.Remove(key)) - { - Log.Error("Selected key does not exist"); - return; - } - else - { - await Json.WriteConfig(config); - Log.Info("Successfully removed key"); - return; - } - } - else if (section == "Timer") - { - 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.Error("Section not found"); - } - catch (Exception ex) + if (!((JObject)jSection).Remove(key)) { - Log.Error("Something went wrong. {ex}: {ex.Message}", ex, ex.Message); + 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"); } } diff --git a/src/cssync.Backend/helper/ParseInput.cs b/src/cssync.Backend/helper/ParseInput.cs new file mode 100644 index 0000000..c99618a --- /dev/null +++ b/src/cssync.Backend/helper/ParseInput.cs @@ -0,0 +1,72 @@ +// 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 async Task SingleArgument(string arg) + { + switch (arg) + { + case "--help" or "-h": + Console.WriteLine(Resources.Help); + break; + + case "--status" or "-s": + if (await Json.GetConfigStatus()) + Console.WriteLine("cssync is currently enabled"); + else + Console.WriteLine("cssync is currently disabled"); + break; + + case "--init" or "-i": + Console.WriteLine("Generating config"); + await Json.GenConfig(); + Console.WriteLine("Finished generating config"); + break; + + default: + Console.WriteLine($"{arg} is unknown. Use --help for available options."); + break; + } + } + + internal static async Task TwoArguments(string flag, string option) + { + if (flag is "--help" or "-h") + { + 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(Resources.HelpMore); + break; + + default: + Console.WriteLine($"{option} is unknown. Use --help for available options."); + break; + } + return; + } + + // Unknown flag with option + 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..35720e0 --- /dev/null +++ b/src/cssync.Backend/helper/Resources.cs @@ -0,0 +1,110 @@ +// 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 = """ + 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."; + 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."; +} 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 - - - - - - -