From 33fb353314ac79ce4fca90d9672f11aeb21b1709 Mon Sep 17 00:00:00 2001 From: Leopold Strobl Date: Thu, 22 Jan 2026 09:54:32 +0100 Subject: [PATCH] =?UTF-8?q?M=C3=B6glichkeit=20eingebaut,=20zwischen=20Zusa?= =?UTF-8?q?tzfeldern=20und=20den=20normalen=20Attachments=20zu=20wechseln?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EPI2CrewbrainFile/Program.cs | 427 +++++++++++++---------------- EPI2CrewbrainFile/README.md | 9 + EPI2CrewbrainFile/appsettings.json | 4 + 3 files changed, 201 insertions(+), 239 deletions(-) diff --git a/EPI2CrewbrainFile/Program.cs b/EPI2CrewbrainFile/Program.cs index 2450ef5..3efdc17 100644 --- a/EPI2CrewbrainFile/Program.cs +++ b/EPI2CrewbrainFile/Program.cs @@ -4,9 +4,7 @@ using Microsoft.Extensions.Configuration; using RestSharp; using RestSharp.Authenticators; using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; @@ -32,29 +30,30 @@ internal class Program var appSettingsPath = Path.Combine(baseDir, "appsettings.json"); var config = LoadConfig(baseDir); - var apiBase = (config["Crewbrain:ApiBase"] ?? "").TrimEnd('/'); - if (string.IsNullOrWhiteSpace(apiBase)) - throw new Exception("Missing config 'Crewbrain:ApiBase' (e.g. https://vt-media.crewbrain.com/api)"); - - // Paths var angebotPath = config["Paths:Angebot"] ?? ""; var auftragPath = config["Paths:Auftrag"] ?? ""; var lieferscheinPath = config["Paths:Lieferschein"] ?? ""; - // Upload settings (3 unterschiedliche TypeIDs / Flags) - var settingsAngebot = UploadSettings.FromConfig(config, "Crewbrain:Upload:Angebot"); - var settingsAuftrag = UploadSettings.FromConfig(config, "Crewbrain:Upload:Auftrag"); - var settingsLieferschein = UploadSettings.FromConfig(config, "Crewbrain:Upload:Lieferschein"); + var apiBase = (config["Crewbrain:ApiBase"] ?? "").TrimEnd('/'); + if (string.IsNullOrWhiteSpace(apiBase)) + throw new Exception("Missing config 'Crewbrain:ApiBase' (e.g. https://vt-media.crewbrain.com/api)"); + + var target = UploadTargetExtensions.Parse(config["Crewbrain:Upload:Target"]); + var attachmentsVisibility = (config["Crewbrain:Upload:AttachmentsVisibility"] ?? "ALL").Trim(); + + // per Dokumentart: TypeId + overwrite + var angebotCfg = UploadCategoryConfig.FromConfig(config, "Crewbrain:Upload:Angebot"); + var auftragCfg = UploadCategoryConfig.FromConfig(config, "Crewbrain:Upload:Auftrag"); + var lieferscheinCfg = UploadCategoryConfig.FromConfig(config, "Crewbrain:Upload:Lieferschein"); Log.Info("=== EPI2CrewbrainFile.Console started ==="); Log.Info($"Config path: {appSettingsPath}"); Log.Info($"Crewbrain ApiBase: {apiBase}"); + Log.Info($"Upload Target: {target}"); + Log.Info($"Attachments Visibility: {attachmentsVisibility}"); + Log.Info($"Paths: Angebot='{angebotPath}' Auftrag='{auftragPath}' Lieferschein='{lieferscheinPath}'"); + Log.Info($"TypeIds (only for AdditionalData): Angebot={angebotCfg.TypeId} Auftrag={auftragCfg.TypeId} Lieferschein={lieferscheinCfg.TypeId}"); - Log.Info($"AngebotPath: {angebotPath} | TypeId={settingsAngebot.TypeId} | OverwriteSingleDocument={settingsAngebot.OverwriteSingleDocument}"); - Log.Info($"AuftragPath: {auftragPath} | TypeId={settingsAuftrag.TypeId} | OverwriteSingleDocument={settingsAuftrag.OverwriteSingleDocument}"); - Log.Info($"LieferscheinPath: {lieferscheinPath} | TypeId={settingsLieferschein.TypeId} | OverwriteSingleDocument={settingsLieferschein.OverwriteSingleDocument}"); - - // Access token (prompt once, store) var accessToken = TokenAuthHelper.EnsureAccessToken( config: config, appSettingsPath: appSettingsPath, @@ -64,13 +63,34 @@ internal class Program Log.Info($"TokenAuth: AccessToken loaded (len={accessToken.Length})"); - // Uploader service - var service = new UploaderService(apiBase, accessToken, appSettingsPath, Log); + var service = new UploaderService(apiBase, accessToken, target, attachmentsVisibility); - // Process - ProcessFolder(folder: angebotPath, uploader: service, tag: "Angebot", settings: settingsAngebot); - ProcessFolder(folder: auftragPath, uploader: service, tag: "Auftrag", settings: settingsAuftrag); - ProcessFolder(folder: lieferscheinPath, uploader: service, tag: "Lieferschein", settings: settingsLieferschein); + // Angebot + ProcessFolder( + folder: angebotPath, + uploader: service, + tag: "Angebot", + typeId: angebotCfg.TypeId, + overwriteSingleDocument: angebotCfg.OverwriteSingleDocument + ); + + // Auftrag + ProcessFolder( + folder: auftragPath, + uploader: service, + tag: "Auftrag", + typeId: auftragCfg.TypeId, + overwriteSingleDocument: auftragCfg.OverwriteSingleDocument + ); + + // Lieferschein + ProcessFolder( + folder: lieferscheinPath, + uploader: service, + tag: "Lieferschein", + typeId: lieferscheinCfg.TypeId, + overwriteSingleDocument: lieferscheinCfg.OverwriteSingleDocument + ); Log.Info("=== EPI2CrewbrainFile.Console finished successfully ==="); return 0; @@ -99,7 +119,7 @@ internal class Program XmlConfigurator.Configure(LogManager.GetRepository(Assembly.GetEntryAssembly()!), new FileInfo(configFile)); } - private static void ProcessFolder(string folder, UploaderService uploader, string tag, UploadSettings settings) + private static void ProcessFolder(string folder, UploaderService uploader, string tag, int typeId, bool overwriteSingleDocument) { if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder)) { @@ -133,10 +153,10 @@ internal class Program Log.Info($"[{tag}] Resolved EventIDManual={eventIdManual} -> CrewBrain ID={eventId}"); var filenameWithoutExt = Path.GetFileNameWithoutExtension(file.Name); - var ok = uploader.UploadDocumentToEvent( + var ok = uploader.UploadPdf( eventId: eventId.Value, - typeId: settings.TypeId, - overwriteSingleDocument: settings.OverwriteSingleDocument, + typeId: typeId, + overwriteSingleDocument: overwriteSingleDocument, fullPath: file.FullName, filename: filenameWithoutExt, filetype: file.Extension.TrimStart('.') @@ -159,19 +179,22 @@ internal class Program } /// - /// Robust: findet 3- bis 5-stellige IDs im Format NNN-NN / NNNN-NN / NNNNN-NN. - /// Wenn mehrere Treffer existieren, nimmt er den längsten (also bevorzugt 5-stellig vor 4-stellig vor 3-stellig). + /// Extrahiert EventIDManual generisch robust (3- bis 5-stellig + "-"+2-stellig). + /// Beispiele: 905-01, 4843-01, 12345-01 + /// Wenn mehrere Matches: nimmt den längsten (meist der richtige). /// private static string ExtractEventIdManual(string fileName) { - // Match: 3..5 digits - 2 digits, nicht Teil einer größeren Zahl var matches = Regex.Matches(fileName, @"(? m.Groups[1].Value) - .OrderByDescending(s => s.Length) // bevorzugt 12345-01 - .First(); + string best = ""; + foreach (Match m in matches) + { + var v = m.Groups[1].Value; + if (v.Length > best.Length) best = v; + } + return best; } private static void MoveToUploaded(string baseFolder, string fileName, string tag) @@ -186,34 +209,54 @@ internal class Program return; } - var nameNoExt = Path.GetFileNameWithoutExtension(fileName); - var ext = Path.GetExtension(fileName); // inkl ".pdf" - if (string.IsNullOrWhiteSpace(ext)) ext = ""; - var dest = Path.Combine(uploadedDir, fileName); + + // Duplikate: Timestamp VOR Extension if (File.Exists(dest)) { - var stamp = DateTime.Now.ToString("yyyyMMddHHmmssffff"); - var newName = $"{nameNoExt}_{stamp}{ext}"; + var name = Path.GetFileNameWithoutExtension(fileName); + var ext = Path.GetExtension(fileName); // inkl. ".pdf" + var ts = DateTime.Now.ToString("yyyyMMddHHmmssffff"); + var newName = $"{name}_{ts}{ext}"; dest = Path.Combine(uploadedDir, newName); } File.Move(source, dest); Log.Info($"[{tag}] Moved to '{dest}'"); } +} - private sealed record UploadSettings(int TypeId, bool OverwriteSingleDocument) +internal enum UploadTarget +{ + AdditionalData, + JobAttachments +} + +internal static class UploadTargetExtensions +{ + public static UploadTarget Parse(string? raw) { - public static UploadSettings FromConfig(IConfiguration cfg, string prefix) - { - int typeId = 0; - bool overwrite = false; + raw = (raw ?? "").Trim(); + if (raw.Equals("JobAttachments", StringComparison.OrdinalIgnoreCase)) return UploadTarget.JobAttachments; + if (raw.Equals("AdditionalData", StringComparison.OrdinalIgnoreCase)) return UploadTarget.AdditionalData; + return UploadTarget.AdditionalData; // default + } +} - int.TryParse(cfg[$"{prefix}:TypeId"], out typeId); - bool.TryParse(cfg[$"{prefix}:OverwriteSingleDocument"], out overwrite); +internal sealed class UploadCategoryConfig +{ + public int TypeId { get; init; } = 0; + public bool OverwriteSingleDocument { get; init; } = false; - return new UploadSettings(typeId, overwrite); - } + public static UploadCategoryConfig FromConfig(IConfiguration cfg, string prefix) + { + int typeId = 0; + int.TryParse(cfg[$"{prefix}:TypeId"], out typeId); + + bool overwrite = false; + bool.TryParse(cfg[$"{prefix}:OverwriteSingleDocument"], out overwrite); + + return new UploadCategoryConfig { TypeId = typeId, OverwriteSingleDocument = overwrite }; } } @@ -234,7 +277,6 @@ internal static class TokenAuthHelper Console.Write("CrewBrain Username: "); var user = (Console.ReadLine() ?? "").Trim(); - var pass = ReadPassword("CrewBrain Password: "); if (string.IsNullOrWhiteSpace(user) || string.IsNullOrWhiteSpace(pass)) @@ -247,29 +289,6 @@ internal static class TokenAuthHelper return fetched; } - public static void ClearStoredToken(string appSettingsPath, ILog log) - { - try - { - var json = File.Exists(appSettingsPath) ? File.ReadAllText(appSettingsPath) : ""; - var root = (JsonNode.Parse(json) as JsonObject) ?? new JsonObject(); - - var crew = root["Crewbrain"] as JsonObject; - var tokenAuth = crew?["TokenAuth"] as JsonObject; - if (tokenAuth == null) return; - - tokenAuth["AccessToken"] = ""; - tokenAuth["ClearedAtUtc"] = DateTime.UtcNow.ToString("o"); - - File.WriteAllText(appSettingsPath, root.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); - log.Warn("TokenAuth: Stored token cleared in appsettings.json (will re-prompt next time)."); - } - catch (Exception ex) - { - log.Warn("TokenAuth: Failed to clear token from appsettings.json", ex); - } - } - private static string FetchAccessToken(string apiBase, string user, string pass, ILog log) { var options = new RestClientOptions(apiBase.TrimEnd('/') + "/") @@ -292,6 +311,7 @@ internal static class TokenAuthHelper var node = JsonNode.Parse(body) as JsonObject ?? throw new Exception($"AccessToken response not a JSON object: {body}"); + // bei dir: {"Accesstoken":"..."} var token = (node["Accesstoken"]?.GetValue() ?? "").Trim(); if (string.IsNullOrWhiteSpace(token)) throw new Exception($"TokenAuth: 'Accesstoken' missing in response: {body}"); @@ -371,23 +391,20 @@ internal class UploaderService { private static readonly ILog Log = LogManager.GetLogger(typeof(UploaderService)); - private readonly string _baseUrl; // https://.../api/ - private string _accessToken; // X-API-KEY - private readonly string _appSettingsPath; // for clearing token on 401/403 - private readonly ILog _rootLog; + private readonly string _baseUrl; // https://.../api/ + private readonly string _accessToken; // X-API-KEY + private readonly UploadTarget _target; + private readonly string _attachmentsVisibility; - public UploaderService(string apiBase, string accessToken, string appSettingsPath, ILog rootLog) + public UploaderService(string apiBase, string accessToken, UploadTarget target, string attachmentsVisibility) { _baseUrl = (apiBase ?? "").TrimEnd('/') + "/"; _accessToken = (accessToken ?? "").Trim(); - _appSettingsPath = appSettingsPath; - _rootLog = rootLog; + _target = target; + _attachmentsVisibility = string.IsNullOrWhiteSpace(attachmentsVisibility) ? "ALL" : attachmentsVisibility.Trim(); } public int? ResolveCrewbrainEventIdByManualId(string eventIdManual) - => ResolveCrewbrainEventIdByManualIdInternal(eventIdManual, allowTokenRetry: true); - - private int? ResolveCrewbrainEventIdByManualIdInternal(string eventIdManual, bool allowTokenRetry) { try { @@ -398,21 +415,12 @@ internal class UploaderService req.AddHeader("X-API-KEY", _accessToken); req.AddQueryParameter("EventIDManual", eventIdManual); - DebugLogRawRequest(client, req, bodyPreview: null); + DebugLogRequest(client, req, bodyForLog: null); var resp = client.Execute(req); + Log.Info($"GET v2/events?EventIDManual={eventIdManual} -> {(int)resp.StatusCode} {resp.StatusCode}"); - if ((int)resp.StatusCode == 401 || (int)resp.StatusCode == 403) - { - Log.Error("Resolve: unauthorized/forbidden. Token might be invalid/expired."); - - if (allowTokenRetry && TryReAuthToken()) - return ResolveCrewbrainEventIdByManualIdInternal(eventIdManual, allowTokenRetry: false); - - return null; - } - if (!resp.IsSuccessful) { var b = resp.Content ?? ""; @@ -451,79 +459,88 @@ internal class UploaderService } } - public bool UploadDocumentToEvent(int eventId, int typeId, bool overwriteSingleDocument, string fullPath, string filename, string filetype) - => UploadDocumentToEventInternal(eventId, typeId, overwriteSingleDocument, fullPath, filename, filetype, allowTokenRetry: true); - - private bool UploadDocumentToEventInternal(int eventId, int typeId, bool overwriteSingleDocument, string fullPath, string filename, string filetype, bool allowTokenRetry) + public bool UploadPdf(int eventId, int typeId, bool overwriteSingleDocument, string fullPath, string filename, string filetype) { try { filename = (filename ?? "").Trim(); filetype = (filetype ?? "").Trim().TrimStart('.'); - if (string.IsNullOrWhiteSpace(filename)) filename = Path.GetFileNameWithoutExtension(fullPath) ?? ""; if (string.IsNullOrWhiteSpace(filename)) - throw new Exception("UploadDocumentToEvent: filename is empty."); - + throw new Exception("Upload: filename is empty."); if (string.IsNullOrWhiteSpace(filetype)) filetype = "pdf"; - var bytes = File.ReadAllBytes(fullPath); - var base64 = Convert.ToBase64String(bytes); + var base64 = Convert.ToBase64String(File.ReadAllBytes(fullPath)); - // API fordert "Filename" mandatory → wir geben hier den kompletten Dateinamen inkl. Extension mit, - // aber lassen dein "filename" (ohne Ext) drin lesbar. - var filenameWithExt = filename.EndsWith("." + filetype, StringComparison.OrdinalIgnoreCase) - ? filename - : $"{filename}.{filetype}"; - - var payload = new + // Log payload-Preview OHNE Base64 (nur Länge) + var payloadForLog = new { - Base64String = base64, - Filename = filenameWithExt, - Filetype = filetype + Base64String = $"", + Filename = filename, + Filetype = filetype, + Target = _target.ToString(), + TypeIdUsedOnlyForAdditionalData = typeId, + OverwriteSingleDocument = overwriteSingleDocument, + Visibility = _attachmentsVisibility }; + Log.Debug("Upload payload preview: " + JsonSerializer.Serialize(payloadForLog)); var client = new RestClient(new RestClientOptions(_baseUrl) { FollowRedirects = true }); - var req = new RestRequest($"v2/events/{eventId}/additionalDatas/{typeId}/addDocument", Method.Put); + RestRequest req; + string bodyToSend; + + if (_target == UploadTarget.AdditionalData) + { + // PUT /v2/events/{eventId}/additionalDatas/{typeId}/addDocument + req = new RestRequest($"v2/events/{eventId}/additionalDatas/{typeId}/addDocument", Method.Put); + + if (overwriteSingleDocument) + req.AddQueryParameter("overwriteSingleDocument", "true"); + + var payload = new + { + Base64String = base64, + Filename = filename, + Filetype = filetype + }; + bodyToSend = JsonSerializer.Serialize(payload); + } + else + { + // POST /v2/events/{eventId}/attachments (EventAttachment) + // Falls euer Pfad anders heißt, hier anpassen. + req = new RestRequest($"v2/events/{eventId}/attachments", Method.Post); + + var payload = new + { + Base64String = base64, + Filename = filename, + Filetype = filetype, + Visibility = _attachmentsVisibility, + VisibilityUsergroups = new int[] { 0 } + }; + bodyToSend = JsonSerializer.Serialize(payload); + } + req.AddHeader("Accept", "application/json"); req.AddHeader("Content-Type", "application/json"); req.AddHeader("X-API-KEY", _accessToken); + req.AddStringBody(bodyToSend, DataFormat.Json); - if (overwriteSingleDocument) - req.AddQueryParameter("overwriteSingleDocument", "true"); - - var jsonBody = JsonSerializer.Serialize(payload); - req.AddStringBody(jsonBody, DataFormat.Json); - - // DEBUG Raw Request (ohne Base64) - var bodyPreview = JsonSerializer.Serialize(new - { - Base64String = $"", - Filename = filenameWithExt, - Filetype = filetype - }); - DebugLogRawRequest(client, req, bodyPreview); + DebugLogRequest(client, req, bodyForLog: bodyToSend); var resp = client.Execute(req); - Log.Info($"PUT v2/events/{eventId}/additionalDatas/{typeId}/addDocument -> {(int)resp.StatusCode} {resp.StatusCode}"); - if ((int)resp.StatusCode == 401 || (int)resp.StatusCode == 403) - { - Log.Error("Upload: unauthorized/forbidden. Token might be invalid/expired."); + Log.Info($"{req.Method} {req.Resource} -> {(int)resp.StatusCode} {resp.StatusCode}"); - // NOTE: Bei euch kommt 403 auch für Validierungsfehler (z.B. Filename fehlt) - // Darum: ReAuth nur dann, wenn Response "wirklich" nach Auth aussieht. - // Wir machen es pragmatisch: wenn allowTokenRetry -> einmal neu Token holen und retry, - // ABER nur wenn message NICHT nach Feldvalidierung klingt. - var content = resp.Content ?? ""; - if (allowTokenRetry && LooksLikeAuthFailure(content) && TryReAuthToken()) - return UploadDocumentToEventInternal(eventId, typeId, overwriteSingleDocument, fullPath, filename, filetype, allowTokenRetry: false); - } - - if (!resp.IsSuccessful) + // Success-Codes: + // - AdditionalData: meist 200/201/202 möglich (je nach Implementierung) + // - Attachments: laut Doku 201 Created + var ok = (int)resp.StatusCode is >= 200 and < 300; + if (!ok) { var body = resp.Content ?? ""; if (body.Length > 2000) body = body.Substring(0, 2000) + "...(truncated)"; @@ -535,125 +552,57 @@ internal class UploaderService } catch (Exception ex) { - Log.Error($"UploadDocumentToEvent failed (EventID={eventId}, File={fullPath})", ex); + Log.Error($"Upload failed (EventID={eventId}, File={fullPath})", ex); return false; } } - private bool TryReAuthToken() - { - try - { - // Token leeren, dann per EnsureAccessToken neu holen (Prompt) - TokenAuthHelper.ClearStoredToken(_appSettingsPath, _rootLog); - - // Config neu laden, damit EnsureAccessToken die aktuelle Datei sieht - var baseDir = AppContext.BaseDirectory; - var cfg = new ConfigurationBuilder() - .SetBasePath(baseDir) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) - .Build(); - - var apiBase = (cfg["Crewbrain:ApiBase"] ?? "").TrimEnd('/'); - var newToken = TokenAuthHelper.EnsureAccessToken(cfg, _appSettingsPath, _rootLog, apiBase); - - if (string.IsNullOrWhiteSpace(newToken)) - return false; - - _accessToken = newToken.Trim(); - _rootLog.Warn("TokenAuth: Re-auth succeeded, token replaced in memory."); - return true; - } - catch (Exception ex) - { - _rootLog.Warn("TokenAuth: Re-auth failed.", ex); - return false; - } - } - - private static bool LooksLikeAuthFailure(string body) - { - if (string.IsNullOrWhiteSpace(body)) return true; - - // Wenn es klar nach Validierungsfehler aussieht (Missing mandatory field etc.), NICHT reauthen - var b = body.ToLowerInvariant(); - if (b.Contains("missing mandatory field")) return false; - if (b.Contains("\"field\"")) return false; - - // Auth-typische Signale - if (b.Contains("unauthorized")) return true; - if (b.Contains("forbidden")) return true; - if (b.Contains("invalid token")) return true; - if (b.Contains("not authorized")) return true; - - // default: lieber einmal reauthen als endlos scheitern - return true; - } - - private static void DebugLogRawRequest(RestClient client, RestRequest req, string? bodyPreview) + private static void DebugLogRequest(RestClient client, RestRequest req, string? bodyForLog) { if (!Log.IsDebugEnabled) return; + Uri? uri; try { - var baseUrl = client.Options.BaseUrl?.ToString() ?? "(null)"; - var resource = req.Resource ?? ""; - var method = req.Method.ToString(); + uri = client.BuildUri(req); + } + catch + { + uri = null; + } - var fullUrl = CombineUrl(baseUrl, resource); - var headers = new List(); - var query = new List(); + var sb = new StringBuilder(); + sb.AppendLine("=== RAW REQUEST (debug) ==="); + sb.AppendLine($"Method: {req.Method}"); + sb.AppendLine($"URL: {(uri?.ToString() ?? "(unable to build url)")}"); + if (req.Parameters != null) + { + sb.AppendLine("Params/Headers:"); foreach (var p in req.Parameters) { - if (p.Type == ParameterType.HttpHeader) + // Header masking + if (p.Type == ParameterType.HttpHeader && p.Name.Equals("X-API-KEY", StringComparison.OrdinalIgnoreCase)) { - var val = p.Value?.ToString() ?? ""; - if (p.Name != null && p.Name.Equals("X-API-KEY", StringComparison.OrdinalIgnoreCase)) - { - val = Mask(val); - } - headers.Add($"{p.Name}: {val}"); + sb.AppendLine($" Header {p.Name}: ***masked***"); } - else if (p.Type == ParameterType.QueryString) + else { - query.Add($"{p.Name}={p.Value}"); + sb.AppendLine($" {p.Type} {p.Name}: {p.Value}"); } } - - Log.Debug("----- RAW REQUEST -----"); - Log.Debug($"{method} {fullUrl}"); - if (query.Count > 0) Log.Debug("Query: " + string.Join("&", query)); - if (headers.Count > 0) - { - Log.Debug("Headers:"); - foreach (var h in headers) Log.Debug(" " + h); - } - - if (!string.IsNullOrWhiteSpace(bodyPreview)) - { - Log.Debug("Body (preview):"); - Log.Debug(bodyPreview); - } - Log.Debug("-----------------------"); } - catch (Exception ex) + + if (!string.IsNullOrWhiteSpace(bodyForLog)) { - Log.Debug("Failed to build raw request debug log.", ex); + // Base64 aus Body entfernen (nur für Log), sehr simple: + var redacted = Regex.Replace(bodyForLog, @"""Base64String"":\s*""[^""]*""", @"""Base64String"":"""""); + if (redacted.Length > 4000) redacted = redacted.Substring(0, 4000) + "...(truncated)"; + sb.AppendLine("Body (redacted):"); + sb.AppendLine(redacted); } - } - private static string CombineUrl(string baseUrl, string resource) - { - baseUrl = (baseUrl ?? "").TrimEnd('/'); - resource = (resource ?? "").TrimStart('/'); - return baseUrl + "/" + resource; - } - - private static string Mask(string s) - { - s = s ?? ""; - if (s.Length <= 6) return new string('*', s.Length); - return s.Substring(0, 3) + "***" + s.Substring(s.Length - 3) + $" (len={s.Length})"; + sb.AppendLine("==========================="); + Log.Debug(sb.ToString()); } } diff --git a/EPI2CrewbrainFile/README.md b/EPI2CrewbrainFile/README.md index f65f30e..965a5b3 100644 --- a/EPI2CrewbrainFile/README.md +++ b/EPI2CrewbrainFile/README.md @@ -55,6 +55,8 @@ Diese TypeID wird später in der Anwendung konfiguriert. "ApiBase": "https://vt-media.crewbrain.com/api", "Upload": { + "Target": "AdditionalData", + "AttachmentsVisibility": "ALL", "Angebot": { "TypeId": 1, "OverwriteSingleDocument": false @@ -76,6 +78,13 @@ Diese TypeID wird später in der Anwendung konfiguriert. Erklärung der Felder Feld Bedeutung ApiBase Basis-URL der CrewBrain API + +Crewbrain:Upload:Target: + +"AdditionalData" = AddOnField-Dokument (TypeId je Dokumentart) + +"JobAttachments" = normales Job Attachment (TypeId wird ignoriert) + Upload..TypeId TypeID der Zusatzinformation (siehe oben) Upload..OverwriteSingleDocument Ersetzt vorhandenes Dokument (nur erlaubt bei DOCUMENT_SINGLE) TokenAuth.AccessToken Wird automatisch vom Tool gesetzt diff --git a/EPI2CrewbrainFile/appsettings.json b/EPI2CrewbrainFile/appsettings.json index ad057e9..fcec2ad 100644 --- a/EPI2CrewbrainFile/appsettings.json +++ b/EPI2CrewbrainFile/appsettings.json @@ -7,6 +7,10 @@ }, "Crewbrain": { "Upload": { + "Target": "JobAttachments", + "AttachmentsVisibility": "ALL", + + "Angebot": { "TypeId": 2, "OverwriteSingleDocument": false }, "Auftrag": { "TypeId": 3, "OverwriteSingleDocument": false }, "Lieferschein": { "TypeId": 4, "OverwriteSingleDocument": false }