diff --git a/CLI/Program.cs b/CLI/Program.cs index 9543773..4bb7182 100644 --- a/CLI/Program.cs +++ b/CLI/Program.cs @@ -46,8 +46,29 @@ internal sealed class TrangaCli : Command string? logFilePath = settings.fileLoggerPath ?? ""; Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, logFilePath); + + TrangaSettings? trangaSettings = null; - TrangaSettings trangaSettings = new (settings.downloadLocation, settings.workingDirectory, settings.apiPort); + if (settings.downloadLocation is not null && settings.workingDirectory is not null) + { + trangaSettings = new TrangaSettings(settings.downloadLocation, settings.workingDirectory); + }else if (settings.downloadLocation is not null) + { + if (trangaSettings is null) + trangaSettings = new TrangaSettings(downloadLocation: settings.downloadLocation); + else + trangaSettings = new TrangaSettings(downloadLocation: settings.downloadLocation, settings.workingDirectory); + }else if (settings.workingDirectory is not null) + { + if (trangaSettings is null) + trangaSettings = new TrangaSettings(downloadLocation: settings.workingDirectory); + else + trangaSettings = new TrangaSettings(settings.downloadLocation, settings.workingDirectory); + } + else + { + trangaSettings = new TrangaSettings(); + } Directory.CreateDirectory(trangaSettings.downloadLocation); Directory.CreateDirectory(trangaSettings.workingDirectory); diff --git a/Dockerfile b/Dockerfile index b495a44..81b2260 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,4 +26,4 @@ WORKDIR /publish COPY --from=build-env /publish . USER 0 RUN chown 1000:1000 /publish -ENTRYPOINT ["dotnet", "/publish/Tranga.dll", "-c"] +ENTRYPOINT ["dotnet", "/publish/Tranga.dll", "-f -c"] diff --git a/Tranga/GlobalBase.cs b/Tranga/GlobalBase.cs index e9c68d0..ce01a79 100644 --- a/Tranga/GlobalBase.cs +++ b/Tranga/GlobalBase.cs @@ -9,7 +9,8 @@ namespace Tranga; public abstract class GlobalBase { - protected Logger? logger { get; init; } + [JsonIgnore] + public Logger? logger { get; init; } protected TrangaSettings settings { get; init; } protected HashSet notificationConnectors { get; init; } protected HashSet libraryConnectors { get; init; } @@ -88,7 +89,7 @@ public abstract class GlobalBase while(IsFileInUse(settings.libraryConnectorsFilePath)) Thread.Sleep(100); Log("Exporting libraryConnectors"); - File.WriteAllText(settings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors)); + File.WriteAllText(settings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors, Formatting.Indented)); } protected void DeleteLibraryConnector(LibraryConnector.LibraryType libraryType) @@ -98,10 +99,12 @@ public abstract class GlobalBase while(IsFileInUse(settings.libraryConnectorsFilePath)) Thread.Sleep(100); Log("Exporting libraryConnectors"); - File.WriteAllText(settings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors)); + File.WriteAllText(settings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors, Formatting.Indented)); } - protected bool IsFileInUse(string filePath) + protected bool IsFileInUse(string filePath) => IsFileInUse(filePath, this.logger); + + public static bool IsFileInUse(string filePath, Logger? logger) { if (!File.Exists(filePath)) return false; @@ -113,7 +116,7 @@ public abstract class GlobalBase } catch (IOException) { - Log($"File is in use {filePath}"); + logger?.WriteLine($"File is in use {filePath}"); return true; } } diff --git a/Tranga/LibraryConnectors/Kavita.cs b/Tranga/LibraryConnectors/Kavita.cs index 106c90e..3fb8983 100644 --- a/Tranga/LibraryConnectors/Kavita.cs +++ b/Tranga/LibraryConnectors/Kavita.cs @@ -1,4 +1,5 @@ using System.Text.Json.Nodes; +using Logging; using Newtonsoft.Json; using JsonSerializer = System.Text.Json.JsonSerializer; @@ -8,7 +9,7 @@ public class Kavita : LibraryConnector { public Kavita(GlobalBase clone, string baseUrl, string username, string password) : - base(clone, baseUrl, GetToken(baseUrl, username, password), LibraryType.Kavita) + base(clone, baseUrl, GetToken(baseUrl, username, password, clone.logger), LibraryType.Kavita) { } @@ -22,7 +23,7 @@ public class Kavita : LibraryConnector return $"Kavita {baseUrl}"; } - private static string GetToken(string baseUrl, string username, string password) + private static string GetToken(string baseUrl, string username, string password, Logger? logger = null) { HttpClient client = new() { @@ -40,16 +41,24 @@ public class Kavita : LibraryConnector try { HttpResponseMessage response = client.Send(requestMessage); - JsonObject? result = JsonSerializer.Deserialize(response.Content.ReadAsStream()); - if (result is not null) - return result["token"]!.GetValue(); + logger?.WriteLine($"Kavita | GetToken {requestMessage.RequestUri} -> {response.StatusCode}"); + if (response.IsSuccessStatusCode) + { + JsonObject? result = JsonSerializer.Deserialize(response.Content.ReadAsStream()); + if (result is not null) + return result["token"]!.GetValue(); + } + else + { + logger?.WriteLine($"Kavita | {response.Content}"); + } } catch (HttpRequestException e) { - Console.WriteLine($"Unable to retrieve token:\n\r{e}"); + logger?.WriteLine($"Kavita | Unable to retrieve token:\n\r{e}"); } - Console.WriteLine("Kavita: Did not receive token."); - throw new Exception("Kavita: Did not receive token."); + logger?.WriteLine("Kavita | Did not receive token."); + return ""; } public override void UpdateLibrary() diff --git a/Tranga/LibraryConnectors/LibraryConnector.cs b/Tranga/LibraryConnectors/LibraryConnector.cs index 5bd7097..0a66890 100644 --- a/Tranga/LibraryConnectors/LibraryConnector.cs +++ b/Tranga/LibraryConnectors/LibraryConnector.cs @@ -23,6 +23,8 @@ public abstract class LibraryConnector : GlobalBase Log($"Creating libraryConnector {Enum.GetName(libraryType)}"); if (!baseUrlRex.IsMatch(baseUrl)) throw new ArgumentException("Base url does not match pattern"); + if(auth == "") + throw new ArgumentNullException(nameof(auth), "Auth can not be empty"); this.baseUrl = baseUrlRex.Match(baseUrl).Value; this.auth = auth; this.libraryType = libraryType; diff --git a/Tranga/Server.cs b/Tranga/Server.cs index c12f1fa..cf2d15a 100644 --- a/Tranga/Server.cs +++ b/Tranga/Server.cs @@ -203,6 +203,9 @@ public class Server : GlobalBase case "Settings/userAgent": SendResponse(HttpStatusCode.OK, response, settings.userAgent); break; + case "Settings/customRequestLimit": + SendResponse(HttpStatusCode.OK, response, settings.requestLimits); + break; case "NotificationConnectors": SendResponse(HttpStatusCode.OK, response, notificationConnectors); break; @@ -220,6 +223,40 @@ public class Server : GlobalBase case "Ping": SendResponse(HttpStatusCode.OK, response, "Pong"); break; + case "LogMessages": + if (logger is null || !File.Exists(logger?.logFilePath)) + { + SendResponse(HttpStatusCode.NotFound, response); + break; + } + + if (requestVariables.TryGetValue("count", out string? count)) + { + try + { + uint messageCount = uint.Parse(count); + SendResponse(HttpStatusCode.OK, response, logger.Tail(messageCount)); + } + catch (FormatException f) + { + SendResponse(HttpStatusCode.InternalServerError, response, f); + } + }else + SendResponse(HttpStatusCode.OK, response, logger.GetLog()); + break; + case "LogFile": + if (logger is null || !File.Exists(logger?.logFilePath)) + { + SendResponse(HttpStatusCode.NotFound, response); + break; + } + + string logDir = new FileInfo(logger.logFilePath).DirectoryName!; + string tmpFilePath = Path.Join(logDir, "Tranga.log"); + File.Copy(logger.logFilePath, tmpFilePath); + SendResponse(HttpStatusCode.OK, response, new FileStream(tmpFilePath, FileMode.Open)); + File.Delete(tmpFilePath); + break; default: SendResponse(HttpStatusCode.BadRequest, response); break; @@ -229,11 +266,13 @@ public class Server : GlobalBase private void HandlePost(HttpListenerRequest request, HttpListenerResponse response) { Dictionary requestVariables = GetRequestVariables(request.Url!.Query); - string? connectorName, internalId, jobId, chapterNumStr, customFolderName, translatedLanguage; + string? connectorName, internalId, jobId, chapterNumStr, customFolderName, translatedLanguage, notificationConnectorStr, libraryConnectorStr; MangaConnector? connector; Manga? tmpManga; Manga manga; Job? job; + NotificationConnector.NotificationConnectorType notificationConnectorType; + LibraryConnector.LibraryType libraryConnectorType; string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; switch (path) { @@ -366,18 +405,6 @@ public class Server : GlobalBase settings.UpdateDownloadLocation(downloadLocation, moveFiles); SendResponse(HttpStatusCode.Accepted, response); break; - case "Settings/ChangeStyleSheet": - if (!requestVariables.TryGetValue("styleSheet", out string? styleSheet)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (settings.UpdateStyleSheet(styleSheet)) - SendResponse(HttpStatusCode.Accepted, response); - else - SendResponse(HttpStatusCode.BadRequest, response, "Invalid parameter for styleSheet"); - break; /*case "Settings/UpdateWorkingDirectory": if (!requestVariables.TryGetValue("workingDirectory", out string? workingDirectory)) { @@ -396,6 +423,10 @@ public class Server : GlobalBase settings.UpdateUserAgent(customUserAgent); SendResponse(HttpStatusCode.Accepted, response); break; + case "Settings/userAgent/Reset": + settings.UpdateUserAgent(null); + SendResponse(HttpStatusCode.Accepted, response); + break; case "Settings/customRequestLimit": if (!requestVariables.TryGetValue("requestType", out string? requestTypeStr) || !requestVariables.TryGetValue("requestsPerMinute", out string? requestsPerMinuteStr) || @@ -412,10 +443,15 @@ public class Server : GlobalBase SendResponse(HttpStatusCode.Accepted, response); }else SendResponse(HttpStatusCode.BadRequest, response); + settings.ExportSettings(); + break; + case "Settings/customRequestLimit/Reset": + settings.requestLimits = TrangaSettings.DefaultRequestLimits; + settings.ExportSettings(); break; case "NotificationConnectors/Update": - if (!requestVariables.TryGetValue("notificationConnector", out string? notificationConnectorStr) || - !Enum.TryParse(notificationConnectorStr, out NotificationConnector.NotificationConnectorType notificationConnectorType)) + if (!requestVariables.TryGetValue("notificationConnector", out notificationConnectorStr) || + !Enum.TryParse(notificationConnectorStr, out notificationConnectorType)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -456,9 +492,64 @@ public class Server : GlobalBase SendResponse(HttpStatusCode.BadRequest, response); } break; + case "NotificationConnectors/Test": + NotificationConnector notificationConnector; + if (!requestVariables.TryGetValue("notificationConnector", out notificationConnectorStr) || + !Enum.TryParse(notificationConnectorStr, out notificationConnectorType)) + { + SendResponse(HttpStatusCode.BadRequest, response); + break; + } + + if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify) + { + if (!requestVariables.TryGetValue("gotifyUrl", out string? gotifyUrl) || + !requestVariables.TryGetValue("gotifyAppToken", out string? gotifyAppToken)) + { + SendResponse(HttpStatusCode.BadRequest, response); + break; + } + notificationConnector = new Gotify(this, gotifyUrl, gotifyAppToken); + }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea) + { + if (!requestVariables.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook)) + { + SendResponse(HttpStatusCode.BadRequest, response); + break; + } + notificationConnector = new LunaSea(this, lunaseaWebhook); + }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy) + { + if (!requestVariables.TryGetValue("ntfyUrl", out string? ntfyUrl) || + !requestVariables.TryGetValue("ntfyAuth", out string? ntfyAuth)) + { + SendResponse(HttpStatusCode.BadRequest, response); + break; + } + notificationConnector = new Ntfy(this, ntfyUrl, ntfyAuth); + } + else + { + SendResponse(HttpStatusCode.BadRequest, response); + break; + } + + notificationConnector.SendNotification("Tranga Test", "This is Test-Notification."); + SendResponse(HttpStatusCode.Accepted, response); + break; + case "NotificationConnectors/Reset": + if (!requestVariables.TryGetValue("notificationConnector", out notificationConnectorStr) || + !Enum.TryParse(notificationConnectorStr, out notificationConnectorType)) + { + SendResponse(HttpStatusCode.BadRequest, response); + break; + } + DeleteNotificationConnector(notificationConnectorType); + SendResponse(HttpStatusCode.Accepted, response); + break; case "LibraryConnectors/Update": - if (!requestVariables.TryGetValue("libraryConnector", out string? libraryConnectorStr) || - !Enum.TryParse(libraryConnectorStr, out LibraryConnector.LibraryType libraryConnectorType)) + if (!requestVariables.TryGetValue("libraryConnector", out libraryConnectorStr) || + !Enum.TryParse(libraryConnectorStr, out libraryConnectorType)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -491,39 +582,52 @@ public class Server : GlobalBase SendResponse(HttpStatusCode.BadRequest, response); } break; - case "LogMessages": - if (logger is null || !File.Exists(logger?.logFilePath)) + case "LibraryConnectors/Test": + LibraryConnector libraryConnector; + if (!requestVariables.TryGetValue("libraryConnector", out libraryConnectorStr) || + !Enum.TryParse(libraryConnectorStr, out libraryConnectorType)) { - SendResponse(HttpStatusCode.NotFound, response); + SendResponse(HttpStatusCode.BadRequest, response); break; } - if (requestVariables.TryGetValue("count", out string? count)) + if (libraryConnectorType is LibraryConnector.LibraryType.Kavita) { - try + if (!requestVariables.TryGetValue("kavitaUrl", out string? kavitaUrl) || + !requestVariables.TryGetValue("kavitaUsername", out string? kavitaUsername) || + !requestVariables.TryGetValue("kavitaPassword", out string? kavitaPassword)) { - uint messageCount = uint.Parse(count); - SendResponse(HttpStatusCode.OK, response, logger.Tail(messageCount)); + SendResponse(HttpStatusCode.BadRequest, response); + break; } - catch (FormatException f) + libraryConnector = new Kavita(this, kavitaUrl, kavitaUsername, kavitaPassword); + }else if (libraryConnectorType is LibraryConnector.LibraryType.Komga) + { + if (!requestVariables.TryGetValue("komgaUrl", out string? komgaUrl) || + !requestVariables.TryGetValue("komgaAuth", out string? komgaAuth)) { - SendResponse(HttpStatusCode.InternalServerError, response, f); + SendResponse(HttpStatusCode.BadRequest, response); + break; } - }else - SendResponse(HttpStatusCode.OK, response, logger.GetLog()); + libraryConnector = new Komga(this, komgaUrl, komgaAuth); + } + else + { + SendResponse(HttpStatusCode.BadRequest, response); + break; + } + libraryConnector.UpdateLibrary(); + SendResponse(HttpStatusCode.Accepted, response); break; - case "LogFile": - if (logger is null || !File.Exists(logger?.logFilePath)) + case "LibraryConnectors/Reset": + if (!requestVariables.TryGetValue("libraryConnector", out libraryConnectorStr) || + !Enum.TryParse(libraryConnectorStr, out libraryConnectorType)) { - SendResponse(HttpStatusCode.NotFound, response); + SendResponse(HttpStatusCode.BadRequest, response); break; } - - string logDir = new FileInfo(logger.logFilePath).DirectoryName!; - string tmpFilePath = Path.Join(logDir, "Tranga.log"); - File.Copy(logger.logFilePath, tmpFilePath); - SendResponse(HttpStatusCode.OK, response, new FileStream(tmpFilePath, FileMode.Open)); - File.Delete(tmpFilePath); + DeleteLibraryConnector(libraryConnectorType); + SendResponse(HttpStatusCode.Accepted, response); break; default: SendResponse(HttpStatusCode.BadRequest, response); diff --git a/Tranga/TrangaArgs.cs b/Tranga/TrangaArgs.cs index 86d9028..72e4634 100644 --- a/Tranga/TrangaArgs.cs +++ b/Tranga/TrangaArgs.cs @@ -16,7 +16,9 @@ public partial class Tranga : GlobalBase string[]? consoleLogger = GetArg(args, ArgEnum.ConsoleLogger); string[]? fileLogger = GetArg(args, ArgEnum.FileLogger); - string? filePath = fileLogger?[0];//TODO validate path + string? filePath = GetArg(args, ArgEnum.FileLoggerPath)?[0]; + if (filePath is not null && !Directory.Exists(new FileInfo(filePath).DirectoryName)) + Directory.CreateDirectory(new FileInfo(filePath).DirectoryName!); List enabledLoggers = new(); if(consoleLogger is not null) @@ -105,7 +107,8 @@ public partial class Tranga : GlobalBase { ArgEnum.DownloadLocation, new(new []{"-d", "--downloadLocation"}, 1, "Directory to which downloaded Manga are saved") }, { ArgEnum.WorkingDirectory, new(new []{"-w", "--workingDirectory"}, 1, "Directory in which application-data is saved") }, { ArgEnum.ConsoleLogger, new(new []{"-c", "--consoleLogger"}, 0, "Enables the consoleLogger") }, - { ArgEnum.FileLogger, new(new []{"-f", "--fileLogger"}, 1, "Enables the fileLogger, Directory where logfiles are saved") }, + { ArgEnum.FileLogger, new(new []{"-f", "--fileLogger"}, 0, "Enables the fileLogger") }, + { ArgEnum.FileLoggerPath, new (new []{"-l", "--fPath"}, 1, "LogFilePath" ) }, { ArgEnum.Help, new(new []{"-h", "--help"}, 0, "Print this") } //{ ArgEnum., new(new []{""}, 1, "") } }; @@ -117,6 +120,7 @@ public partial class Tranga : GlobalBase WorkingDirectory, ConsoleLogger, FileLogger, + FileLoggerPath, Help } diff --git a/Tranga/TrangaSettings.cs b/Tranga/TrangaSettings.cs index 4058188..4fae2fb 100644 --- a/Tranga/TrangaSettings.cs +++ b/Tranga/TrangaSettings.cs @@ -13,7 +13,6 @@ public class TrangaSettings public string downloadLocation { get; private set; } public string workingDirectory { get; private set; } public int apiPortNumber { get; init; } - public string styleSheet { get; private set; } public string userAgent { get; set; } = DefaultUserAgent; [JsonIgnore] public string settingsFilePath => Path.Join(workingDirectory, "settings.json"); [JsonIgnore] public string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json"); @@ -37,34 +36,40 @@ public class TrangaSettings public TrangaSettings(string? downloadLocation = null, string? workingDirectory = null, int? apiPortNumber = null) { - string lockFilePath = $"{settingsFilePath}.lock"; - if (File.Exists(settingsFilePath) && !File.Exists(lockFilePath)) + string wd = workingDirectory ?? Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api"); + string sfp = Path.Join(wd, "settings.json"); + + string lockFilePath = $"{sfp}.lock"; + if (File.Exists(sfp) && !File.Exists(lockFilePath)) {//Load from settings file FileStream lockFile = File.Create(lockFilePath,0, FileOptions.DeleteOnClose); //lock settingsfile - string settingsStr = File.ReadAllText(settingsFilePath); + string settingsStr = File.ReadAllText(sfp); TrangaSettings settings = JsonConvert.DeserializeObject(settingsStr)!; + this.requestLimits = settings.requestLimits; + this.userAgent = settings.userAgent; this.downloadLocation = downloadLocation ?? settings.downloadLocation; this.workingDirectory = workingDirectory ?? settings.workingDirectory; this.apiPortNumber = apiPortNumber ?? settings.apiPortNumber; - this.styleSheet = "default" ?? settings.styleSheet; lockFile.Close(); //unlock settingsfile } - else if(!File.Exists(settingsFilePath)) + else if(!File.Exists(sfp)) {//No settings file exists if (downloadLocation?.Length < 1 || workingDirectory?.Length < 1) throw new ArgumentException("Download-location and working-directory paths can not be empty!"); + this.requestLimits = DefaultRequestLimits; + this.userAgent = DefaultUserAgent; this.apiPortNumber = apiPortNumber ?? 6531; this.downloadLocation = downloadLocation ?? (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Downloads")); this.workingDirectory = workingDirectory ?? Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api"); - this.styleSheet = "default"; ExportSettings(); } else {//Settingsfile is locked + this.requestLimits = DefaultRequestLimits; + this.userAgent = DefaultUserAgent; this.apiPortNumber = apiPortNumber!.Value; this.downloadLocation = downloadLocation!; this.workingDirectory = workingDirectory!; - this.styleSheet = "default"; } UpdateDownloadLocation(this.downloadLocation, false); } @@ -97,17 +102,6 @@ public class TrangaSettings })!; } - public bool UpdateStyleSheet(string newStyleSheet) - { - string[] validStyleSheets = { "default", "hover" }; - if (validStyleSheets.Contains(newStyleSheet)) - { - this.styleSheet = newStyleSheet; - return true; - } - return false; - } - public void UpdateDownloadLocation(string newPath, bool moveFiles = true) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) @@ -135,9 +129,9 @@ public class TrangaSettings ExportSettings(); } - public void UpdateUserAgent(string customUserAgent) + public void UpdateUserAgent(string? customUserAgent) { - this.userAgent = customUserAgent; + this.userAgent = customUserAgent ?? DefaultUserAgent; ExportSettings(); } @@ -145,24 +139,12 @@ public class TrangaSettings { if (File.Exists(settingsFilePath)) { - bool inUse = true; - while (inUse) - { - try - { - using FileStream stream = new(settingsFilePath, FileMode.Open, FileAccess.Read, FileShare.None); - stream.Close(); - inUse = false; - } - catch (IOException) - { - Thread.Sleep(100); - } - } + while(GlobalBase.IsFileInUse(settingsFilePath, null)) + Thread.Sleep(100); } else Directory.CreateDirectory(new FileInfo(settingsFilePath).DirectoryName!); - File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this)); + File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this, Formatting.Indented)); } public string GetFullCoverPath(Manga manga)