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 }