Initial Commit after switching from SVN to git
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- All application settings have been migrated to appsettings.json. -->
|
||||
<configuration>
|
||||
</configuration>
|
||||
@@ -0,0 +1,151 @@
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace fds;
|
||||
|
||||
internal static class FdsDebug
|
||||
{
|
||||
[DebuggerStepThrough]
|
||||
public static FileInfo LogFile(string fileName) =>
|
||||
new(AppBaseDirectory()!.FullName + fileName);
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public static DirectoryInfo? AppBaseDirectory()
|
||||
{
|
||||
string path = AppDomain.CurrentDomain.BaseDirectory + "tmp" + Path.DirectorySeparatorChar;
|
||||
var di = new DirectoryInfo(path);
|
||||
if (di.Exists) return di;
|
||||
if (Directory.Exists(AppDomain.CurrentDomain.BaseDirectory))
|
||||
{
|
||||
di.Create();
|
||||
return di;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public static void DebugLog_async(string codeReference, string sqlConnectionString,
|
||||
Exception? exc = null, string data = "", object? context = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(codeReference) || string.IsNullOrEmpty(sqlConnectionString)) return;
|
||||
try
|
||||
{
|
||||
Task.Run(() => DebugLog_sync(codeReference, sqlConnectionString, exc, data, context));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DebugLog_sync("fds_debug DebugLog_async", sqlConnectionString, ex);
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public static void DebugLog_sync(string codeReference, string sqlConnectionString,
|
||||
Exception? exc = null, string data = "", object? context = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(codeReference) || string.IsNullOrEmpty(sqlConnectionString)) return;
|
||||
using var con = new SqlConnection(sqlConnectionString);
|
||||
DebugLog(codeReference, con, exc, data, context);
|
||||
}
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public static void DebugLog(string codeReference, SqlConnection? sqlConnection,
|
||||
Exception? exc = null, string data = "", object? context = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(codeReference) || sqlConnection is null) return;
|
||||
string note = DateTime.Now.ToString("yyyy.MM.dd HH:mm:ss") + " - " + codeReference;
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var pl = new List<SqlParameter>
|
||||
{
|
||||
new("@CodeReference", codeReference),
|
||||
new("@ExceptionMessage", exc != null ? (object)exc.Message : DBNull.Value),
|
||||
new("@StackTrace", exc?.StackTrace != null ? (object)exc.StackTrace : DBNull.Value),
|
||||
new("@data", (object?)data ?? DBNull.Value)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
int w = 0;
|
||||
if (sqlConnection.State == ConnectionState.Broken) sqlConnection.Close();
|
||||
if (sqlConnection.State == ConnectionState.Connecting)
|
||||
{
|
||||
while (sqlConnection.State == ConnectionState.Connecting && w < 10)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
w++;
|
||||
}
|
||||
}
|
||||
else if (sqlConnection.State != ConnectionState.Open)
|
||||
{
|
||||
sqlConnection.Open();
|
||||
}
|
||||
w = 0;
|
||||
while (sqlConnection.State != ConnectionState.Open && w < 10)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
w++;
|
||||
}
|
||||
|
||||
using var cmd = new SqlCommand(
|
||||
"EXECUTE [dbo].[fds__admin_logdebug] @CodeReference,@ExceptionMessage,@StackTrace,@Data;",
|
||||
sqlConnection);
|
||||
cmd.Parameters.AddRange(pl.ToArray());
|
||||
cmd.ExecuteNonQuery();
|
||||
cmd.Parameters.Clear();
|
||||
}
|
||||
catch { /* swallow SQL errors */ }
|
||||
}
|
||||
catch { /* swallow DB errors */ }
|
||||
|
||||
if (exc != null)
|
||||
note += $"\r\nException:{exc.Message}\r\nStack:{exc.StackTrace}".Replace("\n", "\n ");
|
||||
if (!string.IsNullOrEmpty(data))
|
||||
note += $"\r\nData:{data}".Replace("\n", "\n ");
|
||||
note += "\r\n";
|
||||
|
||||
var debugLogfile = LogFile("DebugLog.txt");
|
||||
if (debugLogfile.Directory?.Exists == true)
|
||||
File.AppendAllText(debugLogfile.FullName, note);
|
||||
}
|
||||
catch { /* swallow logging errors */ }
|
||||
finally
|
||||
{
|
||||
Console.Write(note);
|
||||
Debug.WriteLine(note);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DebugToFile(string note, string filename = "DebugLog.txt")
|
||||
{
|
||||
try
|
||||
{
|
||||
var debugLogfile = LogFile(filename);
|
||||
if (debugLogfile.Directory?.Exists == true)
|
||||
File.AppendAllText(debugLogfile.FullName, DateTime.Now + ": " + note + "\r\n");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public static void DebugToFile(string codeReference, Exception? exc, string data, string filename = "DebugLog.txt")
|
||||
{
|
||||
string note = codeReference;
|
||||
if (exc != null)
|
||||
note += $"\r\nException:{exc.Message}\r\nStack:{exc.StackTrace}".Replace("\n", "\n ");
|
||||
if (!string.IsNullOrEmpty(data))
|
||||
note += $"\r\nData:{data}".Replace("\n", "\n ");
|
||||
DebugToFile(note, filename);
|
||||
}
|
||||
|
||||
// Convenience overload using default FDS connection string
|
||||
public static void DebugLog(string codeReference, Exception? exc = null, string data = "",
|
||||
object? context = null, bool executeAsync = true)
|
||||
{
|
||||
if (executeAsync)
|
||||
DebugLog_async(codeReference, FdsShared.FDSConnectionString(), exc, data, context);
|
||||
else
|
||||
DebugLog_sync(codeReference, FdsShared.FDSConnectionString(), exc, data, context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using fds.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Topshelf;
|
||||
|
||||
namespace fds;
|
||||
|
||||
public class FdsService : ServiceControl
|
||||
{
|
||||
private readonly PeriodicHostedService _hostedService;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
public FdsService()
|
||||
{
|
||||
var loggerFactory = LoggerFactory.Create(b =>
|
||||
b.SetMinimumLevel(LogLevel.Debug).AddFdsLogging());
|
||||
|
||||
var mfr = new FdsMfr(
|
||||
loggerFactory.CreateLogger<FdsMfr>(),
|
||||
loggerFactory);
|
||||
var interval = TimeSpan.FromMinutes(FdsConfig.ExecutionFrequency_Minutes);
|
||||
var jobs = new[]
|
||||
{
|
||||
new PeriodicJobDefinition("MfrSync", interval, async ct =>
|
||||
{
|
||||
bool debug = FdsConfig.DebugDetails;
|
||||
await mfr.UpdateIfNecessary_async(debug);
|
||||
await mfr.UpdateRequested_async(debug);
|
||||
await mfr.GetInvoiceFiles_async(debug);
|
||||
})
|
||||
};
|
||||
|
||||
var logger = loggerFactory.CreateLogger<PeriodicHostedService>();
|
||||
_hostedService = new PeriodicHostedService(jobs, logger);
|
||||
}
|
||||
|
||||
public bool Start(HostControl hostControl)
|
||||
{
|
||||
_ = _hostedService.StartAsync(_cts.Token);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Stop(HostControl hostControl)
|
||||
{
|
||||
_cts.Cancel();
|
||||
_hostedService.StopAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FdsMainModule
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
FdsConfig.Initialize();
|
||||
|
||||
string machineName = Environment.MachineName.ToLower();
|
||||
if (!new[] { "digital-pc", "digital-dpc" }.Contains(machineName))
|
||||
{
|
||||
HostFactory.Run(x =>
|
||||
{
|
||||
x.Service<FdsService>(s =>
|
||||
{
|
||||
s.ConstructUsing(name => new FdsService());
|
||||
s.WhenStarted((tc, host) => tc.Start(host));
|
||||
s.WhenStopped((tc, host) => tc.Stop(host));
|
||||
s.BeforeStoppingService(ctx =>
|
||||
{
|
||||
if (FdsConfig.DebugDetails) Task.Run(() => FdsDebug.DebugToFile("fds__data_service - beforestop", filename: "DebugDetail.txt"));
|
||||
});
|
||||
s.WhenPaused((tc, host) => tc.Stop(host));
|
||||
s.WhenContinued((tc, host) => tc.Start(host));
|
||||
});
|
||||
x.EnablePauseAndContinue();
|
||||
x.StartAutomatically();
|
||||
x.RunAsLocalSystem();
|
||||
x.SetDescription("MFR Data Sync");
|
||||
x.SetDisplayName("MFR Data Sync");
|
||||
x.SetServiceName("MFR Data Sync");
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var svc = new FdsService();
|
||||
svc.Start(null!);
|
||||
Console.WriteLine("Running locally — press any key to stop.");
|
||||
Console.ReadKey();
|
||||
svc.Stop(null!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
using System.Data;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MFR_RESTClient;
|
||||
using MFR_RESTClient.generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OCORE.SQL;
|
||||
using static OCORE.SQL.sql;
|
||||
using static OCORE.commons;
|
||||
|
||||
namespace fds;
|
||||
|
||||
public class FdsSqlOptions : sqloptions
|
||||
{
|
||||
private Dictionary<string, object>? _baseInfo;
|
||||
|
||||
public FdsSqlOptions(Dictionary<string, object>? dic = null)
|
||||
{
|
||||
_baseInfo = dic;
|
||||
OnError = (procedure, ex, data) =>
|
||||
FdsDebug.DebugLog(procedure, exc: ex, data: $"ctrl_nfo={dic}, sql={data}");
|
||||
}
|
||||
}
|
||||
|
||||
public class FdsMfr : IFdsMfr
|
||||
{
|
||||
private readonly ILogger<FdsMfr> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
public FdsMfr(ILogger<FdsMfr> logger, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
public enum UpdateNeed
|
||||
{
|
||||
Reset = 5,
|
||||
Full = 2,
|
||||
Short = 1,
|
||||
None = 0
|
||||
}
|
||||
|
||||
public async Task UpdateIfNecessary_async(bool debugDetails = false)
|
||||
{
|
||||
using var mfr = new FdsMfrClient(_loggerFactory);
|
||||
try
|
||||
{
|
||||
if (debugDetails) FdsDebug.DebugToFile("UpdateIfNecessary_async - unn - start awaited", filename: "DebugDetail.txt");
|
||||
await mfr.Update__Entitytables(debugDetails);
|
||||
if (debugDetails) FdsDebug.DebugToFile("UpdateIfNecessary_async - unn - completed", filename: "DebugDetail.txt");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FdsDebug.DebugLog("UpdateIfNecessary_async - main unn", exc: ex);
|
||||
if (debugDetails) FdsDebug.DebugToFile("UpdateIfNecessary_async - unn", exc: ex, data: "", filename: "DebugDetail.txt");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateRequested_async(bool debugDetails = false)
|
||||
{
|
||||
using var mfr = new FdsMfrClient(_loggerFactory);
|
||||
try
|
||||
{
|
||||
if (debugDetails) FdsDebug.DebugToFile("UpdateRequested_async - unn - start awaited", filename: "DebugDetail.txt");
|
||||
await mfr.Update__EntityRequests(debugDetails);
|
||||
if (debugDetails) FdsDebug.DebugToFile("UpdateRequested_async - unn - completed", filename: "DebugDetail.txt");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FdsDebug.DebugLog("UpdateRequested_async - main unn", exc: ex);
|
||||
if (debugDetails) FdsDebug.DebugToFile("UpdateRequested_async - unn", exc: ex, data: "", filename: "DebugDetail.txt");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task GetInvoiceFiles_async(bool debugDetails = false)
|
||||
{
|
||||
using var mfr = new FdsMfrClient(_loggerFactory);
|
||||
try
|
||||
{
|
||||
if (debugDetails) FdsDebug.DebugToFile("GetInvoiceFiles_async - start", filename: "DebugDetail.txt");
|
||||
var dtbl = await getSQLDatatable_async(
|
||||
"EXECUTE [dbo].[fds__fn_getMFRInvoicesWithoutfiles];",
|
||||
FdsShared.FDSConnectionString(), SqlParameterList: null, options: new FdsSqlOptions());
|
||||
if (dtbl.Count > 0)
|
||||
{
|
||||
foreach (DataRow ivrw in dtbl.DataTable.Rows)
|
||||
{
|
||||
string id = ivrw.nz("id"), docName = ivrw.nz("DocumentName"), fileurl = ivrw.nz("URI");
|
||||
if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(docName) && !string.IsNullOrEmpty(fileurl) && docName.EndsWith("pdf"))
|
||||
{
|
||||
var fl = mfr.GetFile(fileurl);
|
||||
if (fl != null && fl.Length > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await setSQLValue_async(
|
||||
"EXECUTE [dbo].[fds__setMFRInvoiceFile] @Id, @filename, @file;",
|
||||
FdsShared.FDSConnectionString(),
|
||||
SqlParameterList: new ParamList(
|
||||
SQL_VarChar("@Id", id),
|
||||
SQL_VarChar("@filename", docName),
|
||||
new SqlParameter("@file", fl) { SqlDbType = SqlDbType.VarBinary }),
|
||||
options: new FdsSqlOptions());
|
||||
}
|
||||
catch (Exception fsex)
|
||||
{
|
||||
FdsDebug.DebugLog("GetInvoiceFiles_async - mfr storefile", exc: fsex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (debugDetails) FdsDebug.DebugToFile("GetInvoiceFiles_async - completed", filename: "DebugDetail.txt");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FdsDebug.DebugLog("GetInvoiceFiles_async - main unn", exc: ex);
|
||||
if (debugDetails) FdsDebug.DebugToFile("GetInvoiceFiles_async", exc: ex, data: "", filename: "DebugDetail.txt");
|
||||
}
|
||||
}
|
||||
|
||||
public FileInfo? GetReportDoc(ref byte[]? file, string reportid, bool debugDetails = false)
|
||||
{
|
||||
var pl = new List<SqlParameter> { SQL_VarChar("@reportid", reportid) };
|
||||
var sqldt = Task.Run(async () => await getSQLDatatable_async(
|
||||
"EXECUTE [dbo].[fds__getReportDocument] @reportid;",
|
||||
FdsShared.FDSConnectionString(), SqlParameterList: pl, options: new FdsSqlOptions())).Result;
|
||||
|
||||
if (sqldt.Count > 0)
|
||||
{
|
||||
var irow = sqldt.FirstRow;
|
||||
string fln = irow.nz("DocumentName");
|
||||
byte[]? fl = null;
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(fln) && !irow.nbool("fds", false))
|
||||
{
|
||||
using var mfr = new FdsMfrClient(_loggerFactory);
|
||||
fl = mfr.GetFile(irow.nz("URI"));
|
||||
if (fl != null && fl.Length > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
Task.Run(async () => await setSQLValue_async(
|
||||
"EXECUTE [dbo].[fds__setMFRInvoiceFile] @Id, @filename, @file;",
|
||||
FdsShared.FDSConnectionString(),
|
||||
SqlParameterList: new ParamList(
|
||||
SQL_VarChar("@Id", reportid),
|
||||
SQL_VarChar("@filename", fln),
|
||||
new SqlParameter("@file", fl) { SqlDbType = SqlDbType.VarBinary }),
|
||||
options: new FdsSqlOptions())).Wait();
|
||||
}
|
||||
catch (Exception fsex) { FdsDebug.DebugLog("getReportDoc - mfr storefile", exc: fsex); }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fl = irow.no("file", null) as byte[];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FdsDebug.DebugLog("getReportDoc - mfr", exc: ex);
|
||||
if (debugDetails) FdsDebug.DebugToFile("getReportDoc - mfr", exc: ex, data: "", filename: "DebugDetail.txt");
|
||||
}
|
||||
if (fl != null)
|
||||
{
|
||||
file = fl;
|
||||
string safeName = fln.Replace("<p>", "").Replace("</p>", "").Replace("=", "_")
|
||||
.Replace(">", "_").Replace("|", "_").Replace("!", "_").Replace("<", "_").Replace("/", "_");
|
||||
return new FileInfo(Path.GetTempPath() + safeName);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public FileInfo? GetFdsDoc(ref byte[]? file, string reportid, string type)
|
||||
{
|
||||
var pl = new List<SqlParameter> { SQL_VarChar("@type", type), SQL_VarChar("@reportid", reportid) };
|
||||
var sqldt = Task.Run(async () => await getSQLDatatable_async(
|
||||
"EXECUTE [dbo].[fds__getFDSDocument] @type, @reportid;",
|
||||
FdsShared.FDSConnectionString(), SqlParameterList: pl, options: new FdsSqlOptions())).Result;
|
||||
|
||||
if (sqldt.Count > 0)
|
||||
{
|
||||
var irow = sqldt.FirstRow;
|
||||
byte[]? fl = irow.no("file", null) as byte[];
|
||||
string fln = irow.nz("DocumentName");
|
||||
if (fl != null)
|
||||
{
|
||||
file = fl;
|
||||
string safeName = fln.Replace("=", "_").Replace(">", "_").Replace("|", "_").Replace("!", "_").Replace("?", "_");
|
||||
return new FileInfo(Path.GetTempPath() + safeName);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public string DATEV(DatevHeader header, DataTable tbl) =>
|
||||
header.ToHeaderString() + "\r\n" +
|
||||
tbl.ToCsv(includeHeaders: true, quoteStrings: false, fieldDelimiter: ";",
|
||||
cultureinfo: new System.Globalization.CultureInfo("de-DE"), quoteHeader: false);
|
||||
|
||||
public static UpdateNeed UpdateNeedValue(string need) =>
|
||||
Enum.Parse<UpdateNeed>(need);
|
||||
|
||||
public FileInfo? GetDatevZip(ref Stream? stream, DateTime tgtdate, string mode, string authUser, bool includeFiles, bool debugDetails = false)
|
||||
{
|
||||
using var mfr = new FdsMfrClient(_loggerFactory);
|
||||
try
|
||||
{
|
||||
var dset = Task.Run(async () => await getSQLDataSet_async(
|
||||
"EXECUTE [dbo].[fds__getDatevExports] @tgtdate, @mode, @files, @authuser;",
|
||||
FdsShared.FDSConnectionString(),
|
||||
SqlParameterList: new List<SqlParameter>
|
||||
{
|
||||
SQL_Date("@tgtdate", tgtdate), SQL_VarChar("@mode", mode),
|
||||
SQL_Bit("@files", includeFiles), SQL_VarChar("@authuser", authUser)
|
||||
},
|
||||
tablenames: new[] { "admin", "inv", "buchungen", "debitoren" },
|
||||
options: new FdsSqlOptions())).Result;
|
||||
|
||||
var bediFiles = new List<DatevDocument>();
|
||||
|
||||
if (dset.Count >= 4)
|
||||
{
|
||||
var admin = dset.Tables("admin").Rows[0].toObjectDictionary();
|
||||
DateTime startdate = (DateTime)admin["startdate"], enddate = (DateTime)admin["enddate"];
|
||||
string register = tgtdate.ToString("yyyy\\/MM") + (mode.ToLower() == "w" ? "_w" + tgtdate.ToString("dd") : "");
|
||||
var fls = new Dictionary<string, byte[]>();
|
||||
|
||||
var header = new DatevHeader
|
||||
{
|
||||
Formatkategorie = DatevFormatkategorie.Buchungsstapel,
|
||||
Formatversion = DatevFormatversion.Buchungsstapel_9,
|
||||
Beraternummer = (int)admin["beraternummer"],
|
||||
Mandantennummer = (int)admin["mandantennummer"],
|
||||
WJBeginn = (DateTime)admin["WJ-Beginn"],
|
||||
Sachkontenlänge = (int)admin["Sachkontenlänge"],
|
||||
DatumVon = startdate,
|
||||
DatumBis = enddate,
|
||||
Bezeichnung = "fds_" + mode + tgtdate.ToString("yyMMdd")
|
||||
};
|
||||
|
||||
fls.Add($"EXTF_PCW_Buchungen_0_{startdate:yyMMdd HHmmss}.csv",
|
||||
DATEV(header, dset.Tables("buchungen")).ToByteArray(encoding: Encoding.GetEncoding("ISO-8859-1")));
|
||||
|
||||
header.Formatkategorie = DatevFormatkategorie.Debitoren__Kreditoren;
|
||||
header.Formatversion = DatevFormatversion.Debitoren__Kreditoren;
|
||||
fls.Add($"EXTF_PCW_Debitoren_0_{startdate:yyMMdd HHmmss}.csv",
|
||||
DATEV(header, dset.Tables("debitoren")).ToByteArray(encoding: Encoding.GetEncoding("ISO-8859-1")));
|
||||
|
||||
foreach (var fl in fls.Keys)
|
||||
bediFiles.Add(new DatevDocument(Guid.NewGuid().ToString().ToLower(), fl, "", "Processweb_Daten", register));
|
||||
|
||||
if (includeFiles)
|
||||
{
|
||||
foreach (DataRow irow in dset.Tables("inv").Rows)
|
||||
{
|
||||
byte[]? fl = null;
|
||||
string fln = irow.nz("DocumentName");
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(fln) && !irow.nbool("fds", true))
|
||||
fl = mfr.GetFile(irow.nz("URI"));
|
||||
else
|
||||
fl = irow.no("file", null) as byte[];
|
||||
}
|
||||
catch { }
|
||||
if (fl != null && !string.IsNullOrEmpty(fln))
|
||||
{
|
||||
fls.Add(fln, fl);
|
||||
bediFiles.Add(new DatevDocument((string)irow["file_guid"], fln,
|
||||
$"RgNr: {irow.nz("InvoiceId")} RgDatum: {irow.ndt_string("DateOfCreation", "dd.MM.yyyy")}",
|
||||
"ProcessWeb_Belege", register));
|
||||
}
|
||||
}
|
||||
fls.Add("document.xml", CreateDatevDocumentXml(bediFiles).ToByteArray());
|
||||
}
|
||||
|
||||
if (fls.Count > 0)
|
||||
{
|
||||
var archiveFile = new FileInfo(Path.GetTempPath() + $"DatevUpload_AR {startdate:yyyyMM}_{admin["mode"]}.zip");
|
||||
try
|
||||
{
|
||||
using var archive = new Archive(archiveFile, type: SevenZip.OutArchiveFormat.Zip,
|
||||
logger: _loggerFactory.CreateLogger<Archive>());
|
||||
if (archive.CompressToStream(fls, targetstream: stream!))
|
||||
{
|
||||
stream!.Position = 0;
|
||||
return archiveFile;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FdsDebug.DebugLog("getDatevZip - mfr", exc: ex);
|
||||
if (debugDetails) FdsDebug.DebugToFile("getDatevZip - mfr", exc: ex, data: "", filename: "DebugDetail.txt");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public string CreateDatevDocumentXml(List<DatevDocument> files)
|
||||
{
|
||||
var xmldoc = new XmlDocument();
|
||||
var xmlDecl = xmldoc.CreateXmlDeclaration("1.0", "UTF-8", null);
|
||||
xmldoc.InsertBefore(xmlDecl, xmldoc.DocumentElement);
|
||||
|
||||
var rootNode = xmldoc.CreateElement("archive", "http://xml.datev.de/bedi/tps/document/v04.0");
|
||||
xmldoc.AppendChild(rootNode);
|
||||
|
||||
var schemaLoc = xmldoc.CreateAttribute("xsi", "schemaLocation", "http://www.w3.org/2001/XMLSchema-instance");
|
||||
schemaLoc.Value = "http://xml.datev.de/bedi/tps/document/v04.0 document_v040.xsd";
|
||||
rootNode.Attributes.Append(schemaLoc);
|
||||
rootNode.SetAttribute("generatingSystem", "ProcessWeb");
|
||||
rootNode.SetAttribute("version", "4.0");
|
||||
|
||||
var headerElem = xmldoc.CreateElement("header");
|
||||
xmldoc.DocumentElement!.PrependChild(headerElem);
|
||||
var dateElem = xmldoc.CreateElement("date");
|
||||
dateElem.InnerText = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
|
||||
headerElem.AppendChild(dateElem);
|
||||
var descElem = xmldoc.CreateElement("description");
|
||||
descElem.InnerText = "DATEV Schnittstelle ProcessWeb";
|
||||
headerElem.AppendChild(descElem);
|
||||
|
||||
var content = xmldoc.CreateElement("content");
|
||||
xmldoc.DocumentElement.AppendChild(content);
|
||||
|
||||
foreach (var fl in files)
|
||||
{
|
||||
var docElem = xmldoc.CreateElement("document");
|
||||
docElem.SetAttribute("guid", fl.Guid);
|
||||
docElem.SetAttribute("type", fl.Type.ToString());
|
||||
docElem.SetAttribute("processID", fl.ProcessID.ToString());
|
||||
|
||||
if (!string.IsNullOrEmpty(fl.KeyWords))
|
||||
{
|
||||
var kw = xmldoc.CreateElement("keywords");
|
||||
kw.InnerText = fl.KeyWords;
|
||||
docElem.AppendChild(kw);
|
||||
}
|
||||
|
||||
var extension = xmldoc.CreateElement("extension");
|
||||
var extAttr = xmldoc.CreateAttribute("xsi", "type", "http://www.w3.org/2001/XMLSchema-instance");
|
||||
extAttr.Value = "File";
|
||||
extension.Attributes.Append(extAttr);
|
||||
extension.SetAttribute("name", fl.Filename);
|
||||
docElem.AppendChild(extension);
|
||||
|
||||
var repo = xmldoc.CreateElement("repository");
|
||||
void AddLevel(string id, string name)
|
||||
{
|
||||
var level = xmldoc.CreateElement("level");
|
||||
level.SetAttribute("id", id);
|
||||
level.SetAttribute("name", name);
|
||||
repo.AppendChild(level);
|
||||
}
|
||||
AddLevel("1", "ProcessWeb");
|
||||
AddLevel("2", fl.Repository);
|
||||
AddLevel("3", fl.Register);
|
||||
docElem.AppendChild(repo);
|
||||
|
||||
content.AppendChild(docElem);
|
||||
}
|
||||
|
||||
using var sw = new StringWriter();
|
||||
using (var tw = XmlWriter.Create(sw))
|
||||
{
|
||||
xmldoc.WriteTo(tw);
|
||||
tw.Flush();
|
||||
}
|
||||
return sw.GetStringBuilder().ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// DATEV data classes and enums
|
||||
|
||||
public class DatevHeader
|
||||
{
|
||||
public DatevKennzeichen Kennzeichen { get; set; } = DatevKennzeichen.EXTF;
|
||||
public int Versionsnummer { get; set; } = 700;
|
||||
public DatevFormatkategorie Formatkategorie { get; set; } = DatevFormatkategorie.Debitoren__Kreditoren;
|
||||
public DatevFormatversion Formatversion { get; set; } = DatevFormatversion.Debitoren__Kreditoren;
|
||||
public int Beraternummer { get; set; }
|
||||
public int Mandantennummer { get; set; }
|
||||
public DateTime WJBeginn { get; set; }
|
||||
public int Sachkontenlänge { get; set; }
|
||||
public DateTime DatumVon { get; set; }
|
||||
public DateTime DatumBis { get; set; }
|
||||
public string Bezeichnung { get; set; } = "";
|
||||
public DatevBuchungstyp Buchungstyp { get; set; } = DatevBuchungstyp.Finanzbuchführung;
|
||||
public DatevRechnungslegungszweck Rechnungslegungszweck { get; set; } = DatevRechnungslegungszweck.unabhängig;
|
||||
public bool Festschreibung { get; set; } = true;
|
||||
public string WKZ { get; set; } = "EUR";
|
||||
public DateTime Created { get; set; } = DateTime.Now;
|
||||
|
||||
public string Formatname => Enum.GetName(Formatkategorie)!.Replace("__", "/").Replace("_", " ");
|
||||
|
||||
public string ToHeaderString()
|
||||
{
|
||||
var sb = new List<string>
|
||||
{
|
||||
$"\"{Enum.GetName(Kennzeichen)}\"",
|
||||
Versionsnummer.ToString(),
|
||||
((int)Formatkategorie).ToString(),
|
||||
$"\"{Formatname}\"",
|
||||
((int)Formatversion).ToString(),
|
||||
Created.ToString("yyyyMMddHHmmssfff"),
|
||||
"", "\"\"", "\"\"", "\"\"",
|
||||
Beraternummer.ToString(),
|
||||
Mandantennummer.ToString(),
|
||||
WJBeginn.ToString("yyyyMMdd"),
|
||||
Sachkontenlänge.ToString(),
|
||||
DatumVon.ToString("yyyyMMdd"),
|
||||
DatumBis.ToString("yyyyMMdd"),
|
||||
$"\"{Bezeichnung}\"",
|
||||
"\"\"",
|
||||
((int)Buchungstyp).ToString(),
|
||||
((int)Rechnungslegungszweck).ToString(),
|
||||
Festschreibung ? "1" : "0",
|
||||
$"\"{WKZ}\"",
|
||||
"", "", "", "", "", "", "", ""
|
||||
};
|
||||
return string.Join(";", sb);
|
||||
}
|
||||
}
|
||||
|
||||
public enum DatevRechnungslegungszweck { unabhängig = 0, Steuerrecht = 30, Kalkulatorik = 40, Handelsrecht = 50, IFRS = 64 }
|
||||
public enum DatevBuchungstyp { Finanzbuchführung = 1, Jahresabschluss = 2 }
|
||||
public enum DatevKennzeichen { EXTF, DTVF }
|
||||
public enum DatevFormatkategorie { Debitoren__Kreditoren = 16, Sachkontenbeschriftungen = 20, Buchungsstapel = 21, Zahlungsbedingungen = 46, Diverse_Adressen = 48, Wiederkehrende_Buchungen = 65 }
|
||||
public enum DatevFormatversion { Debitoren__Kreditoren = 5, Sachkontenbeschriftungen = 3, Buchungsstapel_9 = 9, Buchungsstapel = 12, Zahlungsbedingungen = 2, Wiederkehrende_Buchungen = 4, Diverse_Adressen = 2 }
|
||||
|
||||
public class DatevDocument
|
||||
{
|
||||
public string Guid { get; set; }
|
||||
public int Type { get; set; } = 2;
|
||||
public int ProcessID { get; set; } = 2;
|
||||
public string Filename { get; set; }
|
||||
public string KeyWords { get; set; }
|
||||
public string Repository { get; set; }
|
||||
public string Register { get; set; }
|
||||
|
||||
public DatevDocument(string guid, string filename, string keywords, string repository, string register)
|
||||
{
|
||||
Guid = guid;
|
||||
Filename = filename;
|
||||
KeyWords = keywords;
|
||||
Repository = repository;
|
||||
Register = register;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
using System.Data;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MFR_RESTClient;
|
||||
using MFR_RESTClient.generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OCORE.SQL;
|
||||
using static OCORE.SQL.sql;
|
||||
using static OCORE.commons;
|
||||
|
||||
namespace fds;
|
||||
|
||||
public class FdsMfrClient : IDisposable
|
||||
{
|
||||
private readonly MFRClient _mfrClient;
|
||||
private readonly ILogger<FdsMfrClient> _logger;
|
||||
|
||||
private static MFRClientCredentials DefaultCredentials() =>
|
||||
new(FdsConfig.MFR_UserName, FdsConfig.MFR_Password);
|
||||
|
||||
public FdsMfrClient(ILoggerFactory? loggerFactory = null, MFRClientCredentials? credentials = null)
|
||||
{
|
||||
_logger = loggerFactory?.CreateLogger<FdsMfrClient>()
|
||||
?? Microsoft.Extensions.Logging.Abstractions.NullLogger<FdsMfrClient>.Instance;
|
||||
var config = new MFRClientConfig(FdsConfig.MFR_host);
|
||||
_mfrClient = new MFRClient(config, credentials ?? DefaultCredentials(),
|
||||
loggerFactory?.CreateLogger<MFRClient>());
|
||||
}
|
||||
|
||||
public bool IsReadonly
|
||||
{
|
||||
get => _mfrClient.IsReadonly;
|
||||
set => _mfrClient.IsReadonly = value;
|
||||
}
|
||||
|
||||
public MFRClientConfig ClientConfig => _mfrClient.ClientConfig;
|
||||
|
||||
public async Task<string> ReadAnything(string address, bool throwErrorIfNotOk = true) =>
|
||||
await _mfrClient.ReadAnything(address, throwErrorIfNotOk);
|
||||
|
||||
public byte[]? GetFile(string address, bool throwErrorIfNotOk = true) =>
|
||||
_mfrClient.GetFile(address, throwErrorIfNotOk);
|
||||
|
||||
public async Task<ODataEnvelope> ReadOData(string address, bool throwErrorIfNotOk = true) =>
|
||||
await _mfrClient.ReadOData(address, throwErrorIfNotOk);
|
||||
|
||||
public async Task<string> GetEntities(bool throwErrorIfNotOk = true) =>
|
||||
await _mfrClient.GetEntities(throwErrorIfNotOk);
|
||||
|
||||
// Database schema cache for MFR entity tables
|
||||
public class DatabaseSchema
|
||||
{
|
||||
private readonly EntityTypes _et;
|
||||
public bool IsValid { get; }
|
||||
public bool HasEntity { get; }
|
||||
public string ThisEntityName => EntityHelper.EntityName(_et);
|
||||
public Dictionary<string, string> EntityConfig { get; }
|
||||
public string EntityTableName { get; }
|
||||
public Dictionary<string, Dictionary<string, string>> ComplexTypes { get; } = new();
|
||||
public Dictionary<string, Dictionary<string, string>> NavProperties { get; } = new();
|
||||
private readonly DataSet _tableSet;
|
||||
|
||||
public DatabaseSchema(EntityTypes et)
|
||||
{
|
||||
_et = et;
|
||||
var dset = Task.Run(async () => await getSQLDataSet_async(
|
||||
"EXECUTE [dbo].[mfr__getSchema] 'table', @tgttype;",
|
||||
FdsShared.FDSConnectionString(),
|
||||
SqlParameterList: new List<SqlParameter> { new("@tgttype", ThisEntityName) },
|
||||
tablenames: new[] { "entity", "complex_types", "navigation_properties", "tables" },
|
||||
options: new FdsSqlOptions())).Result;
|
||||
|
||||
IsValid = dset.Count > 0;
|
||||
HasEntity = dset.Tables("entity").Rows.Count == 1;
|
||||
EntityConfig = dset.Tables("entity").Rows.Count > 0
|
||||
? dset.Tables("entity").Rows[0].toStringDictionary()
|
||||
: new Dictionary<string, string>();
|
||||
EntityTableName = EntityConfig.TryGetValue("tablename", out var tn) ? tn : "";
|
||||
|
||||
foreach (DataRow ctrw in dset.Tables("complex_types").Rows)
|
||||
ComplexTypes[ctrw.nz("name").ToLower()] = ctrw.toStringDictionary();
|
||||
foreach (DataRow nprw in dset.Tables("navigation_properties").Rows)
|
||||
NavProperties[nprw.nz("name").ToLower()] = nprw.toStringDictionary();
|
||||
|
||||
var sqlList = new List<string> { NewDatatableSql(EntityTableName) };
|
||||
var tableNames = new List<string> { EntityTableName };
|
||||
foreach (DataRow ttrw in dset.Tables("tables").Rows)
|
||||
{
|
||||
sqlList.Add(NewDatatableSql(ttrw.nz("tablename")));
|
||||
tableNames.Add(ttrw.nz("tablename"));
|
||||
}
|
||||
|
||||
_tableSet = Task.Run(async () => await getSQLDataSet_async(
|
||||
string.Join(Environment.NewLine, sqlList),
|
||||
FdsShared.FDSConnectionString(),
|
||||
tablenames: tableNames.ToArray(),
|
||||
options: new FdsSqlOptions())).Result.DataSet;
|
||||
}
|
||||
|
||||
public DataSet TgtDataset(string setId)
|
||||
{
|
||||
var dset = new DataSet();
|
||||
foreach (DataTable tbl in _tableSet.Tables)
|
||||
{
|
||||
var t = tbl.Clone();
|
||||
if (t.Columns.Contains("setid")) t.Columns["setid"]!.DefaultValue = setId;
|
||||
dset.Tables.Add(t);
|
||||
}
|
||||
return dset;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> Update__entitytable(EntityTypes et, FdsMfr.UpdateNeed updateNeed,
|
||||
long[]? entityId = null, bool debugDetails = false,
|
||||
Dictionary<string, DatabaseSchema>? schemaDic = null,
|
||||
string additionalCommandAfter = "")
|
||||
{
|
||||
if (et == EntityTypes.none) return false;
|
||||
entityId ??= Array.Empty<long>();
|
||||
if (updateNeed == FdsMfr.UpdateNeed.None && entityId.Length == 0) return true;
|
||||
|
||||
var startTime = DateTime.Now;
|
||||
_logger.LogDebug("Start before Schema {EntityType}", et);
|
||||
string setId = FdsShared.RandomString(5);
|
||||
string thisEntityName = EntityHelper.EntityName(et);
|
||||
|
||||
Action<string, string, string> dtf = (note, info, data) =>
|
||||
{
|
||||
if (debugDetails) FdsDebug.DebugToFile($"Update__entitytable - {note} For {thisEntityName}({setId}){(string.IsNullOrEmpty(info) ? "" : ": " + info)}", filename: "DebugDetail.txt");
|
||||
};
|
||||
Action<string, string, string> dlg = (note, info, data) =>
|
||||
{
|
||||
string str = $"Update__entitytable - {note} for {thisEntityName}({setId}){(string.IsNullOrEmpty(info) ? "" : ": " + info)}";
|
||||
_logger.LogError("{Message} | data={Data}", str, data);
|
||||
if (debugDetails) FdsDebug.DebugToFile(str, filename: "DebugDetail.txt");
|
||||
};
|
||||
|
||||
// Check lock (simplified — original lock logic was commented out)
|
||||
object lockstate = true;
|
||||
if (debugDetails && lockstate is bool) FdsDebug.DebugToFile($"Update__entitytable - lock received for {thisEntityName}({setId}) - {lockstate}", filename: "DebugDetail.txt");
|
||||
|
||||
if (lockstate is bool b && b)
|
||||
{
|
||||
try
|
||||
{
|
||||
var schema = schemaDic != null && schemaDic.TryGetValue(thisEntityName, out var s) ? s : new DatabaseSchema(et);
|
||||
if (!schema.IsValid)
|
||||
{
|
||||
dlg("Schema not found", "", "");
|
||||
}
|
||||
else if (schema.HasEntity)
|
||||
{
|
||||
var tgtDataset = schema.TgtDataset(setId);
|
||||
var filter = new List<string>();
|
||||
string entityIdFilter = "";
|
||||
bool isProcessed = false;
|
||||
|
||||
if (entityId.Length == 1)
|
||||
{
|
||||
entityIdFilter = $"({entityId[0]}L)";
|
||||
filter.Add("");
|
||||
updateNeed = FdsMfr.UpdateNeed.Reset;
|
||||
}
|
||||
else if (entityId.Length > 1)
|
||||
{
|
||||
for (int idx = 0; idx < entityId.Length; idx += 50)
|
||||
filter.Add("$filter=" + string.Join(" or ", entityId.Skip(idx).Take(50).Select(l => $"Id eq {l}L")));
|
||||
updateNeed = FdsMfr.UpdateNeed.Reset;
|
||||
}
|
||||
else if (updateNeed > FdsMfr.UpdateNeed.Short)
|
||||
{
|
||||
filter.Add("");
|
||||
}
|
||||
else if (schema.EntityConfig.ContainsKey("DateColumn") && !string.IsNullOrEmpty(schema.EntityConfig["DateColumn"]))
|
||||
{
|
||||
var lastDate = await getSQLValue_async(
|
||||
schema.EntityConfig.nz("DateSQL").ne($"SELECT MAX([{schema.EntityConfig["DateColumn"]}]) FROM [dbo].[{schema.EntityTableName}];"),
|
||||
FdsShared.FDSConnectionString(), options: new FdsSqlOptions());
|
||||
filter.Add(
|
||||
updateNeed == FdsMfr.UpdateNeed.Short && lastDate.Result is DateTime dt
|
||||
? $"$filter={schema.EntityConfig["DateColumn"]} gt DateTime'{dt:yyyy-MM-dd}T00:00:00'"
|
||||
: "");
|
||||
}
|
||||
|
||||
foreach (var iFilter in filter)
|
||||
{
|
||||
if (schema.EntityConfig.ContainsKey("url") && !string.IsNullOrEmpty(schema.EntityConfig["url"])
|
||||
&& ((updateNeed == FdsMfr.UpdateNeed.Short && !string.IsNullOrEmpty(iFilter)) || updateNeed > FdsMfr.UpdateNeed.Short))
|
||||
{
|
||||
isProcessed = true;
|
||||
_logger.LogDebug("Start MFR {Table}", schema.EntityTableName);
|
||||
string url = _mfrClient.ClientConfig.BaseUrl + schema.EntityConfig["url"].Replace("?", entityIdFilter + "?")
|
||||
+ (string.IsNullOrEmpty(iFilter) ? "" : (!schema.EntityConfig["url"].Contains('?') ? "?" : "&") + iFilter);
|
||||
var mfrResponse = await ReadOData(url);
|
||||
_logger.LogDebug("End MFR {Table}", schema.EntityTableName);
|
||||
|
||||
while (mfrResponse != null)
|
||||
{
|
||||
if (mfrResponse.Value is JObject) mfrResponse.ConvertToArray();
|
||||
try
|
||||
{
|
||||
if (mfrResponse.Value is JArray ja && ja.First is JContainer jc && jc.First is JProperty jp
|
||||
&& jp.Name == "odata.error" && ja.First.ToString().Contains("Sequence contains no elements", StringComparison.OrdinalIgnoreCase))
|
||||
mfrResponse.Value = new JArray();
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (mfrResponse.Value is JArray valueArray)
|
||||
{
|
||||
_logger.LogDebug("MFR rows: received={Received}, local={Local}", valueArray.Count, tgtDataset.Tables[schema.EntityTableName]!.Rows.Count);
|
||||
foreach (JObject vi in valueArray)
|
||||
{
|
||||
var vdic = vi.ToObject<Dictionary<string, object>>()!;
|
||||
var etrow = tgtDataset.Tables[schema.EntityTableName]!.NewRow();
|
||||
etrow["setid"] = setId;
|
||||
etrow["Id"] = vdic["Id"];
|
||||
foreach (var vkey in vdic.Keys)
|
||||
{
|
||||
if (vdic[vkey] == null) continue;
|
||||
if (schema.ComplexTypes.ContainsKey(vkey.ToLower()) && schema.ComplexTypes[vkey.ToLower()]["name"] != "CustomValues")
|
||||
StoreCP(tgtDataset, etrow, schema.ComplexTypes, vdic[vkey], $"{thisEntityName}:{vkey}", vkey, setId, vdic);
|
||||
else if (schema.ComplexTypes.ContainsKey(vkey.ToLower()) && schema.ComplexTypes[vkey.ToLower()]["name"] == "CustomValues")
|
||||
{
|
||||
if (etrow.Table.Columns.Contains(vkey)) etrow[vkey] = JsonConvert.SerializeObject(vdic[vkey]);
|
||||
}
|
||||
else if (schema.NavProperties.ContainsKey(vkey.ToLower()))
|
||||
StoreNP(tgtDataset, schema.ComplexTypes, schema.NavProperties[vkey.ToLower()], vdic[vkey], $"{thisEntityName}:{vkey}", setId, vdic);
|
||||
else
|
||||
SetRowValue(etrow, vkey, vdic[vkey]);
|
||||
}
|
||||
tgtDataset.Tables[schema.EntityTableName]!.Rows.Add(etrow);
|
||||
tgtDataset.Tables[schema.EntityTableName]!.AcceptChanges();
|
||||
}
|
||||
}
|
||||
|
||||
if (mfrResponse.NextLink != null && !string.IsNullOrEmpty(mfrResponse.NextLink.ToString()))
|
||||
mfrResponse = await ReadOData(mfrResponse.NextLink.ToString());
|
||||
else
|
||||
mfrResponse = null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("Start SQL {Table}", schema.EntityTableName);
|
||||
if (isProcessed)
|
||||
{
|
||||
foreach (DataTable tbl in tgtDataset.Tables)
|
||||
{
|
||||
int trc = tbl.Rows.Count;
|
||||
if (trc > 0 || entityId.Length > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
tbl.AcceptChanges();
|
||||
var dtwa = new DatatableWriterAsync(tbl, FdsShared.FDSConnectionString());
|
||||
dtwa.CommandAfterError = new SqlCommand($"SELECT * INTO [t_{setId.ToLower()}_{tbl.TableName}] FROM {dtwa.DestinationTableName};");
|
||||
dtwa.CommandAfter = new SqlCommand(
|
||||
$"EXECUTE [dbo].[{tbl.TableName.Replace("__", "__updt__")}] @tblname, @referencetable, @tgtid;" +
|
||||
"EXECUTE [dbo].[fds__setStatus] @table,@action,@setid,@info;" +
|
||||
(additionalCommandAfter ?? ""));
|
||||
dtwa.CommandAfter.Parameters.AddRange(new SqlParameter[]
|
||||
{
|
||||
new("@tblname", dtwa.DestinationTableName),
|
||||
new("@table", tbl.TableName),
|
||||
new("@action", (tbl.TableName.Equals(schema.EntityTableName, StringComparison.OrdinalIgnoreCase) ? "update_" : "imdu_") + updateNeed.ToString().ToLower()),
|
||||
new("@setid", setId.ToLower()),
|
||||
new("@entityname", schema.ThisEntityName),
|
||||
new("@info", JsonConvert.SerializeObject(new { count = trc, reference = schema.EntityTableName, id = entityId.Length > 0 ? string.Join(",", entityId) : (object?)null })),
|
||||
new("@referencetable", schema.EntityTableName),
|
||||
new("@tgtid", entityId.Length > 0 ? entityId[0].ToString() : (object)DBNull.Value)
|
||||
});
|
||||
dtwa.DoSubmit();
|
||||
}
|
||||
catch (Exception dsex)
|
||||
{
|
||||
dlg("Submission issue", dsex.Message, $"table: {tbl.TableName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
dlg(ex.Message, "", ex.StackTrace ?? "");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (debugDetails) FdsDebug.DebugToFile($"Update__entitytable - lock released for {thisEntityName}({setId})", filename: "DebugDetail.txt");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void SetRowValue(DataRow row, string key, object value)
|
||||
{
|
||||
if (!row.Table.Columns.Contains(key)) return;
|
||||
try
|
||||
{
|
||||
if (value is DateTime dt) { if (dt.Year > 1900) row[key] = value; }
|
||||
else if (value.GetType() == row.Table.Columns[key]!.DataType) row[key] = value;
|
||||
else if (value is string s && value.GetType() != row.Table.Columns[key]!.DataType)
|
||||
{
|
||||
try { row[key] = GenericExtensions.TryConvert(s, row.Table.Columns[key]!.DataType); }
|
||||
catch { try { row[key] = value; } catch { } }
|
||||
}
|
||||
else
|
||||
{
|
||||
try { row[key] = value; } catch { }
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void StoreCP(DataSet tgtDataset, DataRow etrow, Dictionary<string, Dictionary<string, string>> complexTypes,
|
||||
object cp, string entityProperty, string vkey, string setId, Dictionary<string, object> vdic)
|
||||
{
|
||||
if (cp is JArray arr)
|
||||
{
|
||||
foreach (JObject vitm in arr) StoreCP(tgtDataset, etrow, complexTypes, vitm, entityProperty, vkey, setId, vdic);
|
||||
}
|
||||
else if (cp is JObject jObj)
|
||||
{
|
||||
var cxType = complexTypes[vkey.ToLower()];
|
||||
var cto = jObj.ToObject<Dictionary<string, object>>()!;
|
||||
var cnr = tgtDataset.Tables[cxType["tablename"]]!.NewRow();
|
||||
cnr["setid"] = setId;
|
||||
foreach (var cky in cto.Keys)
|
||||
{
|
||||
if (cto[cky] != null && cnr.Table.Columns.Contains(cky))
|
||||
SetRowValue(cnr, cky, cto[cky]);
|
||||
}
|
||||
if (cnr.Table.Columns.Contains("EntityId")) cnr["EntityId"] = vdic["Id"];
|
||||
if (cnr.Table.Columns.Contains("Property")) cnr["Property"] = entityProperty;
|
||||
tgtDataset.Tables[cxType["tablename"]]!.Rows.Add(cnr);
|
||||
if (etrow.Table.Columns.Contains(vkey + "#ID") && cto.ContainsKey("id")) etrow[vkey + "#ID"] = vdic["Id"];
|
||||
}
|
||||
}
|
||||
|
||||
private void StoreNP(DataSet tgtDataset, Dictionary<string, Dictionary<string, string>> complexTypes,
|
||||
Dictionary<string, string> navProp, object np, string entityProperty, string setId, Dictionary<string, object> vdic)
|
||||
{
|
||||
if (np is JArray arr)
|
||||
{
|
||||
foreach (JObject vitm in arr) StoreNP(tgtDataset, complexTypes, navProp, vitm, entityProperty, setId, vdic);
|
||||
}
|
||||
else if (np is JObject jObj)
|
||||
{
|
||||
var nto = jObj.ToObject<Dictionary<string, object>>()!;
|
||||
if (nto.ContainsKey("Id"))
|
||||
{
|
||||
var nnr = tgtDataset.Tables[navProp["countertable"]]!.NewRow();
|
||||
var ncr = tgtDataset.Tables[navProp["tablename"]]!.NewRow();
|
||||
nnr["setid"] = setId;
|
||||
foreach (var cky in nto.Keys)
|
||||
{
|
||||
if (nto[cky] == null) continue;
|
||||
if (complexTypes.ContainsKey(cky.ToLower()) && complexTypes[cky.ToLower()]["name"] != "CustomValues")
|
||||
StoreCP(tgtDataset, nnr, complexTypes, nto[cky], $"{complexTypes[cky.ToLower()]["EntityType"]}:{cky}", cky, setId, vdic);
|
||||
else if (nnr.Table.Columns.Contains(cky))
|
||||
SetRowValue(nnr, cky, nto[cky]);
|
||||
}
|
||||
ncr["EntityId"] = vdic["Id"];
|
||||
ncr["PartnerType"] = navProp["countertype"];
|
||||
ncr["PartnerId"] = nto["Id"];
|
||||
ncr["Property"] = entityProperty;
|
||||
tgtDataset.Tables[navProp["tablename"]]!.Rows.Add(ncr);
|
||||
tgtDataset.Tables[navProp["countertable"]]!.Rows.Add(nnr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string NewDatatableSql(string tablename) =>
|
||||
$"Select TOP(0) [setid] = CAST('' as varchar(50)), * FROM [dbo].[{tablename}];";
|
||||
|
||||
public async Task Update__Entitytables(bool debugDetails = false, EntityTypes? tgtEntityType = null)
|
||||
{
|
||||
Action<string, string, string, Exception?> dtf = (note, info, data, ex) =>
|
||||
{
|
||||
if (debugDetails) FdsDebug.DebugToFile($"Update__AllEntitytables - {note}{(string.IsNullOrEmpty(info) ? "" : ": " + info)}", filename: "DebugDetail.txt", exc: ex, data: data);
|
||||
};
|
||||
Action<string, string, string, Exception?> dlg = (note, info, data, ex) =>
|
||||
{
|
||||
string str = $"Update__AllEntitytables - {note}{(string.IsNullOrEmpty(info) ? "" : ": " + info)}";
|
||||
_logger.LogError(ex, "{Message} | data={Data}", str, data);
|
||||
if (debugDetails) FdsDebug.DebugToFile(str, filename: "DebugDetail.txt", exc: ex, data: data ?? "");
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
string sql = "SELECT * FROM [dbo].[fds__getUpdateableTables]()"
|
||||
+ (tgtEntityType.HasValue ? $" WHERE [entity_name] = '{tgtEntityType.Value}'" : "") + ";";
|
||||
var updateableTables = await getSQLDatatable_async(sql, FdsShared.FDSConnectionString(), options: new FdsSqlOptions());
|
||||
dtf("UpdateableTables", updateableTables.Exception ?? "", $"({(updateableTables.Count > 0 ? updateableTables.DataTable.Rows.Count.ToString() : " no")} Rows)", null);
|
||||
|
||||
if (updateableTables.Count > 0 && updateableTables.DataTable.Columns.Contains("updateneed"))
|
||||
{
|
||||
foreach (DataRow rw in updateableTables.Select("updateneed > 0", "updateneed DESC"))
|
||||
{
|
||||
string etname = rw.nz("entity_name", "");
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(etname))
|
||||
dtf("no entity_name received", "", JsonConvert.SerializeObject(rw.ItemArray), null);
|
||||
else if (!rw.nbool("locked", true))
|
||||
{
|
||||
var tblUpdateNeed = (FdsMfr.UpdateNeed)rw.nint64("updateneed", 0);
|
||||
var et = EntityHelper.EntityValue(etname);
|
||||
if (et != EntityTypes.none)
|
||||
{
|
||||
dtf($"updating: {etname}", "", JsonConvert.SerializeObject(rw.ItemArray), null);
|
||||
await Update__entitytable(et, updateNeed: tblUpdateNeed, debugDetails: debugDetails);
|
||||
dtf("updating task completed", "", "", null);
|
||||
}
|
||||
else dtf("entitytype not known", "", $"etname={etname}", null);
|
||||
}
|
||||
else dtf($"Table update locked {etname}", "", JsonConvert.SerializeObject(rw.ItemArray), null);
|
||||
}
|
||||
catch (Exception tableUpdateEx) { dlg($"updatepart failed - {etname}", "", JsonConvert.SerializeObject(rw.ItemArray), tableUpdateEx); }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exa) { dlg("outer frame", "", "", exa); }
|
||||
}
|
||||
|
||||
public async Task Update__EntityRequests(bool debugDetails = false)
|
||||
{
|
||||
Action<string, string, string, Exception?> dtf = (note, info, data, ex) =>
|
||||
{
|
||||
if (debugDetails) FdsDebug.DebugToFile($"Update__EntityRequests - {note}{(string.IsNullOrEmpty(info) ? "" : ": " + info)}", filename: "DebugDetail.txt", exc: ex, data: data);
|
||||
};
|
||||
Action<string, string, string, Exception?> dlg = (note, info, data, ex) =>
|
||||
{
|
||||
string str = $"Update__EntityRequests - {note}{(string.IsNullOrEmpty(info) ? "" : ": " + info)}";
|
||||
_logger.LogError(ex, "{Message} | data={Data}", str, data);
|
||||
if (debugDetails) FdsDebug.DebugToFile(str, filename: "DebugDetail.txt", exc: ex, data: data ?? "");
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var updateableRequests = await getSQLDatatable_async(
|
||||
"SELECT * FROM [dbo].[fds__getUpdateableRequests]();",
|
||||
FdsShared.FDSConnectionString(), options: new FdsSqlOptions());
|
||||
dtf("UpdateableRequests", updateableRequests.Exception ?? "", $"({(updateableRequests.Count > 0 ? updateableRequests.DataTable.Rows.Count.ToString() : " no")} Rows)", null);
|
||||
|
||||
if (updateableRequests.Count > 0)
|
||||
{
|
||||
foreach (DataRow rw in updateableRequests.Select("", "order"))
|
||||
{
|
||||
string etname = rw.nz("entity_name", "");
|
||||
long tgtid = rw.nint64("Id", -1);
|
||||
if (tgtid > -1 && !string.IsNullOrWhiteSpace(etname))
|
||||
{
|
||||
try
|
||||
{
|
||||
var et = EntityHelper.EntityValue(etname);
|
||||
await Update__entitytable(et, updateNeed: FdsMfr.UpdateNeed.Reset,
|
||||
entityId: new[] { tgtid }, debugDetails: debugDetails,
|
||||
additionalCommandAfter: "DELETE FROM [dbo].[fds__mfr_updaterequests] WHERE LOWER([entity_name]) = LOWER(ISNULL(@entityname,'')) AND CAST(ISNULL([Id],'') as varchar(1000)) = @tgtid;");
|
||||
}
|
||||
catch (Exception tex) { dlg($"Update__EntityRequest single failed - {etname}", "", JsonConvert.SerializeObject(rw.ItemArray), tex); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exa) { dlg("outer frame", "", "", exa); }
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
(_mfrClient as IDisposable).Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace fds;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the application <see cref="IConfiguration"/> built from appsettings.json.
|
||||
/// Call <see cref="Initialize"/> once at startup before accessing <see cref="Current"/>.
|
||||
/// </summary>
|
||||
public static class FdsConfig
|
||||
{
|
||||
private static IConfiguration? _config;
|
||||
|
||||
internal static IConfiguration Current =>
|
||||
_config ?? throw new InvalidOperationException("FdsConfig has not been initialized. Call FdsConfig.Initialize() in Main().");
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
_config = new ConfigurationBuilder()
|
||||
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
|
||||
.Build();
|
||||
}
|
||||
|
||||
// -- Connection strings ---------------------------------------------------
|
||||
internal static string SQLConnectionString() =>
|
||||
Current.GetConnectionString("fuchs_ConnectionString")
|
||||
?? throw new InvalidOperationException("Missing connection string: fuchs_ConnectionString");
|
||||
|
||||
internal static string FDSConnectionString() =>
|
||||
Current.GetConnectionString("fuchs_fds_ConnectionString")
|
||||
?? throw new InvalidOperationException("Missing connection string: fuchs_fds_ConnectionString");
|
||||
|
||||
// -- App settings ---------------------------------------------------------
|
||||
internal static double ExecutionFrequency_Minutes =>
|
||||
Current.GetValue<double>("Fds:ExecutionFrequency_Minutes", 15);
|
||||
|
||||
internal static bool DebugDetails =>
|
||||
Current.GetValue<bool>("Fds:DebugDetails");
|
||||
|
||||
internal static string MFR_UserName =>
|
||||
Current["Fds:MFR_UserName"] ?? "";
|
||||
|
||||
internal static string MFR_Password =>
|
||||
Current["Fds:MFR_Password"] ?? "";
|
||||
|
||||
internal static string MFR_host =>
|
||||
Current["Fds:MFR_host"] ?? "";
|
||||
}
|
||||
|
||||
internal static class FdsShared
|
||||
{
|
||||
internal static string SQLConnectionString() => FdsConfig.SQLConnectionString();
|
||||
internal static string FDSConnectionString() => FdsConfig.FDSConnectionString();
|
||||
|
||||
internal static SqlConnection SqlCon() =>
|
||||
new(FdsConfig.SQLConnectionString());
|
||||
|
||||
public static string RandomString(byte length)
|
||||
{
|
||||
var r = new Random();
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
var sb = new StringBuilder();
|
||||
for (byte i = 1; i <= length; i++)
|
||||
{
|
||||
int idx = r.Next(0, chars.Length);
|
||||
sb.Append(chars[idx]);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string ToCsv(this DataRow source, bool quoteStrings, CultureInfo cultureinfo, string delimiter = ",")
|
||||
{
|
||||
var fieldValues = source.ItemArray;
|
||||
var rx = new Regex("(\")");
|
||||
|
||||
var converted = fieldValues!.Select(o =>
|
||||
{
|
||||
if (o is null or DBNull) return "";
|
||||
if (o is string s)
|
||||
return quoteStrings ? "\"" + rx.Replace(s, "\"\"") + "\"" : s;
|
||||
return o switch
|
||||
{
|
||||
decimal d => d.ToString(cultureinfo),
|
||||
float f => f.ToString(cultureinfo),
|
||||
double db => db.ToString(cultureinfo),
|
||||
bool b => b.ToString(cultureinfo),
|
||||
DateTime dt => dt.ToUniversalTime().ToString("U"),
|
||||
_ => o.ToString() ?? ""
|
||||
};
|
||||
}).ToArray();
|
||||
|
||||
return string.Join(delimiter, converted);
|
||||
}
|
||||
|
||||
public static string ToCsv(this DataTable source, bool includeHeaders, bool quoteStrings,
|
||||
string rowDelimiter = "\r\n", string fieldDelimiter = ",",
|
||||
CultureInfo? cultureinfo = null, bool? quoteHeader = null)
|
||||
{
|
||||
quoteHeader ??= quoteStrings;
|
||||
cultureinfo ??= CultureInfo.InvariantCulture;
|
||||
|
||||
var rows = source.Rows.Cast<DataRow>()
|
||||
.Select(row => row.ToCsv(quoteStrings, cultureinfo, delimiter: fieldDelimiter));
|
||||
|
||||
if (includeHeaders)
|
||||
{
|
||||
var rx = new Regex("(\")");
|
||||
var headers = string.Join(fieldDelimiter,
|
||||
source.Columns.Cast<DataColumn>()
|
||||
.Select(col => quoteHeader.Value
|
||||
? "\"" + rx.Replace(col.ColumnName, "\"\"") + "\""
|
||||
: col.ColumnName));
|
||||
rows = new[] { headers }.Concat(rows);
|
||||
}
|
||||
|
||||
return string.Join(rowDelimiter, rows);
|
||||
}
|
||||
|
||||
public static byte[] ToByteArray(this string input, Encoding? encoding = null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
using (var sw = new StreamWriter(ms, encoding: encoding ?? Encoding.UTF8, leaveOpen: true))
|
||||
{
|
||||
sw.Write(input);
|
||||
sw.Flush();
|
||||
}
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
public static bool WriteStreamToDisk(Stream streamToWrite, string filePath)
|
||||
{
|
||||
int cnt = 0;
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath)) File.Delete(filePath);
|
||||
using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Delete);
|
||||
ReadWriteStream(streamToWrite, fs, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FdsDebug.DebugLog("FdsShared.WriteStreamToDisk", exc: ex, data: $"filePath={filePath}");
|
||||
cnt++;
|
||||
if (cnt == 6) return false;
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ReadWriteStream(Stream readStream, Stream writeStream, bool closeWriteStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
const int length = 256;
|
||||
var buffer = new byte[length];
|
||||
readStream.Seek(0, SeekOrigin.Begin);
|
||||
int bytesRead = readStream.Read(buffer, 0, length);
|
||||
while (bytesRead > 0)
|
||||
{
|
||||
writeStream.Write(buffer, 0, bytesRead);
|
||||
bytesRead = readStream.Read(buffer, 0, length);
|
||||
}
|
||||
readStream.Close();
|
||||
if (closeWriteStream) writeStream.Close();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FdsDebug.DebugLog("FdsShared.ReadWriteStream", exc: ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static string NameBase(this FileInfo fi) =>
|
||||
fi.Name[..^fi.Extension.Length];
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SevenZip;
|
||||
|
||||
namespace fds;
|
||||
|
||||
public class Archive : IDisposable
|
||||
{
|
||||
public event Action? Saving;
|
||||
public event Action? FileSaved;
|
||||
public event Action? FileStreamCreated;
|
||||
|
||||
private FileInfo _archiveFile;
|
||||
private string _archivePassword;
|
||||
private OutArchiveFormat _archiveFormat;
|
||||
private readonly ILogger<Archive> _logger;
|
||||
public string TempPath { get; set; } = AppDomain.CurrentDomain.BaseDirectory;
|
||||
public Stream? ArchiveFileStream { get; set; }
|
||||
|
||||
private SevenZipExtractor? _zipOut;
|
||||
private SevenZipCompressor? _zipIn;
|
||||
public bool ZipAppend { get; set; } = true;
|
||||
public bool ExitOK { get; set; }
|
||||
public bool ZipInOK { get; set; }
|
||||
|
||||
public Archive(FileInfo archiveFile, string archivePassword = "", bool init = true,
|
||||
OutArchiveFormat type = OutArchiveFormat.SevenZip, ILogger<Archive>? logger = null)
|
||||
{
|
||||
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<Archive>.Instance;
|
||||
_archiveFormat = type;
|
||||
_archiveFile = new FileInfo(archiveFile.FullName.Replace(archiveFile.Extension,
|
||||
type == OutArchiveFormat.SevenZip ? ".7z" : archiveFile.Extension));
|
||||
_archivePassword = archivePassword;
|
||||
if (init) InitZipIn(type);
|
||||
}
|
||||
|
||||
private void InitZipIn(OutArchiveFormat type)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Zipping.SevenZipPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var assemblyDir = new DirectoryInfo(
|
||||
new Uri(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!).LocalPath);
|
||||
var zip = assemblyDir.GetFiles("7z.dll", SearchOption.AllDirectories).FirstOrDefault();
|
||||
Zipping.SevenZipPath = zip?.FullName ?? "";
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (string.IsNullOrEmpty(Zipping.SevenZipPath))
|
||||
{
|
||||
var assemblyDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
|
||||
var zip = assemblyDir.GetFiles("7z.dll", SearchOption.AllDirectories).FirstOrDefault();
|
||||
Zipping.SevenZipPath = zip?.FullName ?? "";
|
||||
}
|
||||
}
|
||||
if (string.IsNullOrEmpty(Zipping.SevenZipPath))
|
||||
_logger.LogError("SevenZipPath not found — 7z.dll is missing from the output directory.");
|
||||
}
|
||||
SevenZipCompressor.SetLibraryPath(Zipping.SevenZipPath);
|
||||
|
||||
_zipIn = new SevenZipCompressor
|
||||
{
|
||||
ArchiveFormat = (type == OutArchiveFormat.SevenZip && _archiveFile.Extension.Contains("7z", StringComparison.OrdinalIgnoreCase))
|
||||
? OutArchiveFormat.SevenZip : type,
|
||||
CompressionLevel = SevenZip.CompressionLevel.Ultra,
|
||||
CompressionMode = ZipAppend ? SevenZip.CompressionMode.Append : SevenZip.CompressionMode.Create,
|
||||
DirectoryStructure = false
|
||||
};
|
||||
|
||||
_zipIn.CompressionMethod = _zipIn.ArchiveFormat switch
|
||||
{
|
||||
OutArchiveFormat.SevenZip => CompressionMethod.Lzma2,
|
||||
OutArchiveFormat.Zip or OutArchiveFormat.GZip => CompressionMethod.Deflate,
|
||||
_ => CompressionMethod.Default
|
||||
};
|
||||
|
||||
ZipInOK = true;
|
||||
}
|
||||
|
||||
public void Extract(FileInfo dataArchiveFilePath, DirectoryInfo tgtDirectory, OutArchiveFormat type = default)
|
||||
{
|
||||
if (!dataArchiveFilePath.Exists) return;
|
||||
|
||||
if (type == default)
|
||||
type = dataArchiveFilePath.Extension.Contains("7z", StringComparison.OrdinalIgnoreCase)
|
||||
? OutArchiveFormat.SevenZip : OutArchiveFormat.Zip;
|
||||
|
||||
if (!ZipInOK) InitZipIn(type);
|
||||
|
||||
_zipOut = string.IsNullOrEmpty(_archivePassword)
|
||||
? new SevenZipExtractor(dataArchiveFilePath.FullName)
|
||||
: new SevenZipExtractor(dataArchiveFilePath.FullName, _archivePassword);
|
||||
|
||||
try
|
||||
{
|
||||
if (!_zipOut.ArchiveFileData[0].Encrypted && !string.IsNullOrEmpty(_archivePassword))
|
||||
_archivePassword = "";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Archive.Extract failed — path={Path}, target={Target}",
|
||||
dataArchiveFilePath.FullName, tgtDirectory.FullName);
|
||||
return;
|
||||
}
|
||||
|
||||
_zipOut.ExtractArchive(tgtDirectory.FullName);
|
||||
_zipOut.Dispose();
|
||||
}
|
||||
|
||||
public bool Compress(List<FileInfo> files, FileInfo? archiveFile = null, string? archivePass = null, OutArchiveFormat type = OutArchiveFormat.SevenZip)
|
||||
{
|
||||
if (files.Count == 0) return true;
|
||||
if (!ZipInOK) InitZipIn(type);
|
||||
|
||||
archiveFile ??= _archiveFile;
|
||||
archivePass = string.IsNullOrEmpty(archivePass) ? _archivePassword : archivePass;
|
||||
|
||||
if (archiveFile.Exists && ZipAppend)
|
||||
_zipIn!.CompressionMode = CompressionMode.Append;
|
||||
else
|
||||
{
|
||||
if (archiveFile.Exists) archiveFile.Delete();
|
||||
_zipIn!.CompressionMode = CompressionMode.Create;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var filesVerified = files.Where(f => f.Exists).ToArray();
|
||||
var filePaths = filesVerified.Select(f => f.FullName).ToArray();
|
||||
if (string.IsNullOrEmpty(archivePass))
|
||||
_zipIn.CompressFiles(archiveFile.FullName, filePaths);
|
||||
else
|
||||
{
|
||||
_zipIn.EncryptHeaders = true;
|
||||
_zipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256;
|
||||
_zipIn.CompressFilesEncrypted(archiveFile.FullName, archivePass, filePaths);
|
||||
}
|
||||
FileSaved?.Invoke();
|
||||
ExitOK = true;
|
||||
_zipIn = null;
|
||||
ZipInOK = false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ExitOK = false;
|
||||
}
|
||||
archiveFile.Refresh();
|
||||
return ExitOK && archiveFile.Exists;
|
||||
}
|
||||
|
||||
public bool CompressToStream(Dictionary<string, byte[]> files, Stream? targetstream = null)
|
||||
{
|
||||
if (files.Count == 0) return true;
|
||||
if (!ZipInOK) InitZipIn(_archiveFormat);
|
||||
|
||||
if (ArchiveFileStream == null)
|
||||
{
|
||||
_zipIn!.CompressionMode = CompressionMode.Create;
|
||||
ArchiveFileStream = new MemoryStream();
|
||||
}
|
||||
else
|
||||
_zipIn!.CompressionMode = CompressionMode.Append;
|
||||
|
||||
try
|
||||
{
|
||||
var filesStreams = files.ToDictionary(kv => kv.Key, kv => (Stream)new MemoryStream(kv.Value));
|
||||
var target = targetstream ?? ArchiveFileStream;
|
||||
if (string.IsNullOrEmpty(_archivePassword))
|
||||
_zipIn.CompressStreamDictionary(filesStreams, target);
|
||||
else
|
||||
{
|
||||
_zipIn.EncryptHeaders = true;
|
||||
_zipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256;
|
||||
_zipIn.CompressStreamDictionary(filesStreams, target, _archivePassword);
|
||||
}
|
||||
ArchiveFileStream.Seek(0, SeekOrigin.Begin);
|
||||
FileStreamCreated?.Invoke();
|
||||
ExitOK = true;
|
||||
_zipIn = null;
|
||||
ZipInOK = false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ExitOK = false;
|
||||
}
|
||||
return ExitOK;
|
||||
}
|
||||
|
||||
public bool WriteArchiveStreamToDisk(FileInfo? archiveFile = null)
|
||||
{
|
||||
try { if (_archiveFile.Exists) _archiveFile.Delete(); } catch { }
|
||||
FdsShared.WriteStreamToDisk(ArchiveFileStream!, (archiveFile ?? _archiveFile).FullName);
|
||||
_archiveFile.Refresh();
|
||||
return _archiveFile.Exists;
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
private bool _disposed;
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
try
|
||||
{
|
||||
ArchiveFileStream?.Dispose();
|
||||
_zipOut?.Dispose();
|
||||
_zipIn = null;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
public static class Zipping
|
||||
{
|
||||
public static string SevenZipPath = "";
|
||||
|
||||
public static void FastAppend(FileInfo fileToZip, FileInfo archiveFile)
|
||||
{
|
||||
if (fileToZip.Exists && archiveFile?.Exists == true)
|
||||
{
|
||||
using var zip = new Archive(archiveFile) { ZipAppend = true };
|
||||
zip.Compress(new List<FileInfo> { fileToZip });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>fds</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
|
||||
<NoWarn>1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
|
||||
<NoWarn>1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="install.bat" />
|
||||
<Content Include="un-install.bat" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MFR_RESTClient\MFR_RESTClient.csproj" />
|
||||
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE_web\OCORE_web.csproj" />
|
||||
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE\OCORE\OCORE.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="7z.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.2.24" />
|
||||
<PackageReference Include="Topshelf" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.6" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.6" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.6" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,76 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<StartupObject>Sub Main</StartupObject>
|
||||
<RootNamespace>fds</RootNamespace>
|
||||
<MyType>Empty</MyType>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<Configurations>db-dev.processweb.de;Debug;Release;server02.processweb.de</Configurations>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Import Include="System" />
|
||||
<Import Include="System.Data" />
|
||||
<Import Include="Microsoft.Data" />
|
||||
<Import Include="System.IO" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
|
||||
<NoWarn>42016,41999,42017,42018,42019,42032,42036,42020,42021,42022</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DefineDebug>false</DefineDebug>
|
||||
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
|
||||
<NoWarn>42016,41999,42017,42018,42019,42032,42036,42020,42021,42022</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="My Project\Application.Designer.vb">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Application.myapp</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
</Compile>
|
||||
<Compile Update="My Project\Resources.Designer.vb">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="My Project\Settings.Designer.vb">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Settings.settings</DependentUpon>
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="install.bat" />
|
||||
<None Update="My Project\Application.myapp">
|
||||
<Generator>MyApplicationCodeGenerator</Generator>
|
||||
<LastGenOutput>Application.Designer.vb</LastGenOutput>
|
||||
</None>
|
||||
<Content Include="un-install.bat" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MFR_RESTClient\MFR_RESTClient.csproj" />
|
||||
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE_web\OCORE_web.csproj" />
|
||||
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE\OCORE\OCORE.csproj" />
|
||||
<ProjectReference Include="..\..\..\WebProjectComponents\OCMSsharp\OCMS\OCMS.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="7z.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Fuchs_DataService.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- Updated packages -->
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<!-- Compatible packages (kept) -->
|
||||
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.1.23" />
|
||||
<PackageReference Include="Topshelf" Version="4.3.0" />
|
||||
<!-- New packages (needed for .NET 10) -->
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.5" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<ProjectView>ShowAllFiles</ProjectView>
|
||||
<PublishUrlHistory />
|
||||
<InstallUrlHistory />
|
||||
<SupportUrlHistory />
|
||||
<UpdateUrlHistory />
|
||||
<BootstrapperUrlHistory />
|
||||
<ErrorReportUrlHistory />
|
||||
<FallbackCulture>en-US</FallbackCulture>
|
||||
<VerifyUploadedFiles>false</VerifyUploadedFiles>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0"?>
|
||||
<doc>
|
||||
<assembly>
|
||||
<name>Fuchs_DataService</name>
|
||||
</assembly>
|
||||
<members>
|
||||
<member name="T:fds.FdsConfig">
|
||||
<summary>
|
||||
Holds the application <see cref="T:Microsoft.Extensions.Configuration.IConfiguration"/> built from appsettings.json.
|
||||
Call <see cref="M:fds.FdsConfig.Initialize"/> once at startup before accessing <see cref="P:fds.FdsConfig.Current"/>.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:fds.Logging.FdsLoggerProvider">
|
||||
<summary>
|
||||
Writes log entries to Debug output and a rolling file.
|
||||
Database logging is wired up but disabled — set <see cref="P:fds.Logging.FdsLoggerProvider.DatabaseLoggingEnabled"/> to true to activate.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:fds.Logging.FdsLoggerProvider.DatabaseLoggingEnabled">
|
||||
<summary>Set to true to activate database logging via fds__admin_logdebug.</summary>
|
||||
</member>
|
||||
<member name="M:fds.Logging.FdsLogger.WriteToDatabase(System.String,System.String,System.Exception)">
|
||||
<summary>
|
||||
Prepared DB logging via fds__admin_logdebug.
|
||||
Enable by setting <see cref="P:fds.Logging.FdsLoggerProvider.DatabaseLoggingEnabled"/> = true.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:fds.PeriodicJobDefinition">
|
||||
<summary>
|
||||
Defines a named job with its own execution schedule for use with <see cref="T:fds.PeriodicHostedService"/>.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:fds.PeriodicJobDefinition.#ctor(System.String,System.TimeSpan,System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task})">
|
||||
<summary>
|
||||
Defines a named job with its own execution schedule for use with <see cref="T:fds.PeriodicHostedService"/>.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:fds.PeriodicHostedService">
|
||||
<summary>
|
||||
A <see cref="T:Microsoft.Extensions.Hosting.BackgroundService"/> that runs multiple independent jobs, each on its own <see cref="T:System.Threading.PeriodicTimer"/>.
|
||||
</summary>
|
||||
</member>
|
||||
</members>
|
||||
</doc>
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Data;
|
||||
|
||||
namespace fds;
|
||||
|
||||
public interface IFdsMfr
|
||||
{
|
||||
Task UpdateIfNecessary_async(bool debugDetails = false);
|
||||
Task UpdateRequested_async(bool debugDetails = false);
|
||||
Task GetInvoiceFiles_async(bool debugDetails = false);
|
||||
FileInfo? GetReportDoc(ref byte[]? file, string reportid, bool debugDetails = false);
|
||||
FileInfo? GetFdsDoc(ref byte[]? file, string reportid, string type);
|
||||
FileInfo? GetDatevZip(ref Stream? stream, DateTime tgtdate, string mode, string authUser, bool includeFiles, bool debugDetails = false);
|
||||
string DATEV(DatevHeader header, DataTable tbl);
|
||||
string CreateDatevDocumentXml(List<DatevDocument> files);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace fds.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Writes log entries to Debug output and a rolling file.
|
||||
/// Database logging is wired up but disabled — set <see cref="DatabaseLoggingEnabled"/> to true to activate.
|
||||
/// </summary>
|
||||
public sealed class FdsLoggerProvider : ILoggerProvider
|
||||
{
|
||||
private readonly string _logDirectory;
|
||||
|
||||
/// <summary>Set to true to activate database logging via fds__admin_logdebug.</summary>
|
||||
public static bool DatabaseLoggingEnabled { get; set; } = false;
|
||||
|
||||
public FdsLoggerProvider(string? logDirectory = null)
|
||||
{
|
||||
_logDirectory = logDirectory
|
||||
?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tmp");
|
||||
Directory.CreateDirectory(_logDirectory);
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName) =>
|
||||
new FdsLogger(categoryName, _logDirectory);
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
internal sealed class FdsLogger : ILogger
|
||||
{
|
||||
private readonly string _categoryName;
|
||||
private readonly string _logDirectory;
|
||||
private static readonly Lock _fileLock = new();
|
||||
|
||||
internal FdsLogger(string categoryName, string logDirectory)
|
||||
{
|
||||
_categoryName = categoryName;
|
||||
_logDirectory = logDirectory;
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
|
||||
Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel)) return;
|
||||
|
||||
string message = formatter(state, exception);
|
||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
string levelTag = logLevel switch
|
||||
{
|
||||
LogLevel.Trace => "TRC",
|
||||
LogLevel.Debug => "DBG",
|
||||
LogLevel.Information => "INF",
|
||||
LogLevel.Warning => "WRN",
|
||||
LogLevel.Error => "ERR",
|
||||
LogLevel.Critical => "CRT",
|
||||
_ => "???"
|
||||
};
|
||||
|
||||
string line = $"{timestamp} [{levelTag}] {_categoryName}: {message}";
|
||||
if (exception != null)
|
||||
line += $"\r\n Exception: {exception.Message}\r\n Stack: {exception.StackTrace}";
|
||||
|
||||
// Always emit to Debug output
|
||||
Debug.WriteLine(line);
|
||||
|
||||
// Always write to file
|
||||
string filename = logLevel >= LogLevel.Error ? "ErrorLog.txt" : "DebugLog.txt";
|
||||
AppendToFile(filename, line);
|
||||
|
||||
// Database logging — prepared, not activated
|
||||
if (FdsLoggerProvider.DatabaseLoggingEnabled)
|
||||
WriteToDatabase(_categoryName, message, exception);
|
||||
}
|
||||
|
||||
private void AppendToFile(string filename, string line)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_fileLock)
|
||||
File.AppendAllText(Path.Combine(_logDirectory, filename), line + "\r\n");
|
||||
}
|
||||
catch { /* never throw from logger */ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepared DB logging via fds__admin_logdebug.
|
||||
/// Enable by setting <see cref="FdsLoggerProvider.DatabaseLoggingEnabled"/> = true.
|
||||
/// </summary>
|
||||
private static void WriteToDatabase(string codeReference, string message, Exception? exception)
|
||||
{
|
||||
// Activate by setting FdsLoggerProvider.DatabaseLoggingEnabled = true in appsettings / startup.
|
||||
//
|
||||
// using var con = new Microsoft.Data.SqlClient.SqlConnection(FdsConfig.FDSConnectionString());
|
||||
// using var cmd = new Microsoft.Data.SqlClient.SqlCommand(
|
||||
// "EXECUTE [dbo].[fds__admin_logdebug] @CodeReference, @ExceptionMessage, @StackTrace, @Data;", con);
|
||||
// cmd.Parameters.AddWithValue("@CodeReference", codeReference);
|
||||
// cmd.Parameters.AddWithValue("@ExceptionMessage", (object?)exception?.Message ?? DBNull.Value);
|
||||
// cmd.Parameters.AddWithValue("@StackTrace", (object?)exception?.StackTrace ?? DBNull.Value);
|
||||
// cmd.Parameters.AddWithValue("@Data", message);
|
||||
// con.Open();
|
||||
// cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public static class FdsLoggingExtensions
|
||||
{
|
||||
public static ILoggingBuilder AddFdsLogging(this ILoggingBuilder builder, string? logDirectory = null)
|
||||
{
|
||||
builder.AddProvider(new FdsLoggerProvider(logDirectory));
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace fds;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a named job with its own execution schedule for use with <see cref="PeriodicHostedService"/>.
|
||||
/// </summary>
|
||||
public sealed record PeriodicJobDefinition(
|
||||
string Name,
|
||||
TimeSpan Interval,
|
||||
Func<CancellationToken, Task> Execute);
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="BackgroundService"/> that runs multiple independent jobs, each on its own <see cref="PeriodicTimer"/>.
|
||||
/// </summary>
|
||||
public sealed class PeriodicHostedService : BackgroundService
|
||||
{
|
||||
private readonly IReadOnlyList<PeriodicJobDefinition> _jobs;
|
||||
private readonly ILogger<PeriodicHostedService> _logger;
|
||||
|
||||
public PeriodicHostedService(IEnumerable<PeriodicJobDefinition> jobs, ILogger<PeriodicHostedService> logger)
|
||||
{
|
||||
_jobs = jobs.ToList();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var jobTasks = _jobs.Select(job => RunJobAsync(job, stoppingToken)).ToList();
|
||||
await Task.WhenAll(jobTasks);
|
||||
}
|
||||
|
||||
private async Task RunJobAsync(PeriodicJobDefinition job, CancellationToken stoppingToken)
|
||||
{
|
||||
using var timer = new PeriodicTimer(job.Interval);
|
||||
_logger.LogInformation("Job '{Name}' scheduled with interval {Interval}.", job.Name, job.Interval);
|
||||
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken))
|
||||
{
|
||||
_logger.LogDebug("Job '{Name}' starting.", job.Name);
|
||||
try
|
||||
{
|
||||
await job.Execute(stoppingToken);
|
||||
_logger.LogDebug("Job '{Name}' completed.", job.Name);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogError(ex, "Job '{Name}' failed.", job.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"fuchs_ConnectionString": "Data Source=MSSQL4.NBG4.DOMAINXYZ.DE,10439;Initial Catalog=site_fuchs_dev;Persist Security Info=False;TrustServerCertificate=true;Encrypt=true;User ID=fuchs_web;password='Bt5pL/cJg9oxb5';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;",
|
||||
"fuchs_fds_ConnectionString": "Data Source=MSSQL4.NBG4.DOMAINXYZ.DE,10439;Initial Catalog=site_fuchs_dev;Persist Security Info=False;TrustServerCertificate=true;Encrypt=true;User ID=fuchs_fds;password='!Po@cGZ5bUn37khO';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;"
|
||||
},
|
||||
"Fds": {
|
||||
"ExecutionFrequency_Minutes": 15,
|
||||
"DebugDetails": true,
|
||||
"MFR_UserName": "system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com",
|
||||
"MFR_Password": "0oT4G3H2",
|
||||
"MFR_host": "portal.mobilefieldreport.com"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Fuchs_Dataservice.exe install --autostart
|
||||
@@ -0,0 +1 @@
|
||||
Fuchs_Dataservice.exe uninstall
|
||||
Reference in New Issue
Block a user