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
-
-
-
-
-
-
-