diff --git a/HaWeb/CLAUDE.md b/HaWeb/CLAUDE.md new file mode 100644 index 0000000..a9d2987 --- /dev/null +++ b/HaWeb/CLAUDE.md @@ -0,0 +1,168 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +HaWeb is a digital edition website for the Hamann correspondence (Hamann-Ausgabe), built with ASP.NET Core 6 and modern frontend tooling. It renders scholarly editions of historical letters and commentary from XML sources, with features for search, indexing, and editorial workflow management. + +The project depends on two sibling projects: +- **HaDocumentV6**: Parses XML files into convenient C# models +- **HaXMLReaderV6**: Forward-parses XML elements (letters, comments, traditions, marginals) into HTML + +## Build and Development Commands + +### Initial Setup +```bash +npm install # Install frontend dependencies +dotnet restore # Restore .NET packages +``` + +### Development (run both in separate terminals) +```bash +npm run dev # Watch and rebuild CSS/JS (uses Vite) +dotnet watch run # Watch and rebuild ASP.NET app +``` + +Set `DOTNET_ENVIRONMENT=Development` for development features. On Windows PowerShell: `$Env:ASPNETCORE_ENVIRONMENT = "Development"` + +### Production Build +```bash +npm run build # Build CSS/JS first (required!) +dotnet build HaWeb.csproj # Build the web application +``` + +### Production Deployment (Linux) +```bash +npm run build +dotnet publish --runtime linux-x64 -c Release +``` + +## Architecture + +### Frontend Build System +- **Vite**: Bundles JavaScript modules from `wwwroot/js/main.js` → `wwwroot/dist/scripts.js` +- **PostCSS + Tailwind**: Processes CSS from `wwwroot/css/*.css` → `wwwroot/dist/styles.css` +- **Build order matters**: Always run `npm run build` before `dotnet build` for production + +PostCSS plugin order in `postcss.config.cjs` is critical: `tailwindcss` → `postcss-import` → `autoprefixer` + +### Backend Architecture (ASP.NET Core MVC) + +**Core Services** (registered as singletons in Program.cs): +- `XMLTestService`: XML validation and testing +- `XMLInteractionService`: Central XML interaction layer +- `HaDocumentWrapper`: Wraps HaDocumentV6 models +- `GitService`: Git repository management with LibGit2Sharp +- `XMLFileProvider`: File system abstraction for XML files +- `WebSocketMiddleware`: Real-time notifications for XML changes + +**Controllers** (main routes): +- `BriefeContoller`: Letter display and navigation +- `RegisterController`: Index/register views (persons, places, etc.) +- `SucheController`: Search functionality +- `EditionController`: Edition-specific views +- `AdminController`: Administrative functions (feature-gated) +- `XMLStateController`: XML state management for editors +- `WebhookController`: Git webhook endpoint for automated pulls +- `CMIF`: CMIF (Correspondence Metadata Interchange Format) export + +**Key Architectural Components**: +- `HTMLParser/`: Custom XML→HTML transformation rules and state machines +- `FileHelpers/`: XML file reading and management +- `SearchHelpers/`: Search implementation with state pattern +- `Settings/`: Configuration for XML node parsing, collections, and rules + +### Feature Flags +Managed via `Microsoft.FeatureManagement.AspNetCore` in `appsettings.json`: +- `AdminService`: Enables admin routes +- `LocalPublishService`: Local publishing features +- `SyntaxCheck`: XML syntax validation +- `Notifications`: WebSocket notifications + +### Configuration +Environment-specific settings in `appsettings.{Environment}.json`. Key settings: +- `FileStoragePath`: Base directory for all data storage (absolute path) + - Compiled Hamann.xml files stored in `[FileStoragePath]/HAMANN/` + - Git repository content in `[FileStoragePath]/GIT/` +- `RepositoryBranch`: Git branch to track (e.g., "main") +- `RepositoryURL`: Git repository URL (e.g., "https://github.com/user/repo") +- `WebhookSecret`: Optional HMAC-SHA256 secret for webhook validation (GitHub format) + +An external `settings.json` can be loaded from `[FileStoragePath]/GIT/settings.json` for runtime configuration overrides. + +### Data Flow +1. Root XML file (`Hamann.xml`) must exist at build output root +2. `HaDocumentV6` parses XML into document models +3. `HaXMLReaderV6` transforms XML elements to HTML +4. Controllers fetch models via services and render Razor views +5. Frontend JavaScript adds interactivity (marginals, search highlighting, themes) + +### Frontend JavaScript Modules +Modular ES6 structure in `wwwroot/js/`: +- `main.js`: Entry point, exports startup functions +- `marginals.mjs`: Marginal notes display +- `search.mjs`: Search highlighting with mark.js +- `theme.mjs`: Theme switching (light/dark) +- `websocket.mjs`: WebSocket connection for live updates +- `filelistform.mjs`: XML state management forms +- `htmx.min.js`: HTMX for dynamic interactions + +Each module exports a `startup_*` function called during initialization. + +## Important Development Notes + +### XML Parsing System +The project uses a custom forward-parsing system for XML transformation defined in: +- `Settings/NodeRules/`: Rules for specific XML node types (letters, comments, marginals) +- `Settings/ParsingRules/`: Parsing rules for text, links, edits, comments +- `Settings/ParsingState/`: State machines for parsing contexts +- `HTMLParser/`: Core XML parsing helpers + +When modifying XML handling, update both the node rules and corresponding parsing state. + +### Working with Views +Views follow standard Razor conventions. Shared layouts in `Views/Shared/`. The project uses tag helpers defined in `HTMLHelpers/TagHelpers.cs` for custom rendering. + +### CSS Architecture +- `tailwind.css`: Main Tailwind input +- Component-specific CSS files (`letter.css`, `register.css`, `search.css`, etc.) +- `site.css`: Imported by `main.js`, serves as CSS entry point +- Production builds include autoprefixer and cssnano minification + +### WebSocket Notifications +Real-time updates for XML changes via WebSocket. Configured with: +- Keep-alive: 30 minutes +- Connection filtering via `AllowedWebSocketConnections` in appsettings +- Middleware registered before static files in Program.cs + +### Git Integration +The application uses **LibGit2Sharp** for Git operations on XML source files: + +**GitService** (`FileHelpers/GitService.cs`): +- Manages Git repository at `[FileStoragePath]/GIT/` +- Pulls latest changes from remote on webhook trigger +- Retrieves current commit SHA, branch, and timestamp +- Supports default credentials (SSH agent, credential manager) +- Auto-initializes or clones repository if needed + +**Webhook Endpoint** (`/api/webhook/git`): +- POST endpoint for Git webhooks (GitHub, GitLab, etc.) +- Optional signature validation using `WebhookSecret` (HMAC-SHA256) +- Triggers `git pull` operation +- Automatically scans and parses XML files after pull +- Returns status with commit info + +**Status Endpoint** (`/api/webhook/status`): +- GET endpoint to check current Git state +- Returns commit SHA, branch, timestamp + +**Workflow**: +1. Webhook triggered by push to repository +2. `GitService.Pull()` fetches and merges latest changes +3. `XMLFileProvider.Scan()` detects changed files +4. XML files are collected, validated, and parsed +5. New compiled `Hamann.xml` is generated +6. Website reflects updated content + +**Configuration**: Set `WebhookSecret` in appsettings to enable signature validation for webhooks. \ No newline at end of file diff --git a/HaWeb/Controllers/WebhookController.cs b/HaWeb/Controllers/WebhookController.cs new file mode 100644 index 0000000..da8f5eb --- /dev/null +++ b/HaWeb/Controllers/WebhookController.cs @@ -0,0 +1,149 @@ +namespace HaWeb.Controllers; +using Microsoft.AspNetCore.Mvc; +using HaWeb.FileHelpers; +using System.Security.Cryptography; +using System.Text; + +[ApiController] +[Route("api/webhook")] +public class WebhookController : Controller { + private readonly IGitService _gitService; + private readonly IXMLFileProvider _xmlProvider; + private readonly IConfiguration _config; + private readonly ILogger _logger; + + public WebhookController( + IGitService gitService, + IXMLFileProvider xmlProvider, + IConfiguration config, + ILogger logger) { + _gitService = gitService; + _xmlProvider = xmlProvider; + _config = config; + _logger = logger; + } + + [HttpPost("git")] + public async Task GitWebhook([FromHeader(Name = "X-Hub-Signature-256")] string? signature) { + try { + // Validate webhook secret if configured + var webhookSecret = _config.GetValue("WebhookSecret"); + if (!string.IsNullOrEmpty(webhookSecret)) { + Request.EnableBuffering(); + + using var reader = new StreamReader(Request.Body, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + Request.Body.Position = 0; + + _logger.LogInformation("Webhook received - Content-Type: {ContentType}, Body length: {Length}, Body SHA256: {BodyHash}", + Request.ContentType, body.Length, + Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(body))).ToLower()); + + if (!ValidateSignature(body, signature, webhookSecret)) { + _logger.LogWarning("Webhook signature validation failed - check ValidateSignature logs above"); + return Unauthorized(new { error = "Invalid signature" }); + } + } + + _logger.LogInformation("Git webhook triggered, initiating pull..."); + + // Pull latest changes + var hasChanges = _gitService.Pull(); + + if (!hasChanges) { + _logger.LogInformation("No changes detected after pull"); + return Ok(new { + success = true, + message = "Pull completed, no changes detected", + hasChanges = false + }); + } + + _logger.LogInformation("Changes detected, triggering repository scan and parse..."); + + // Trigger full reload: scan files, parse XML, generate and load Hamann.xml + _xmlProvider.Reload(); + + var gitState = _gitService.GetGitState(); + + _logger.LogInformation("Repository updated and library reloaded successfully to commit {Commit}", gitState?.Commit?[..7]); + + return Ok(new { + success = true, + message = "Repository pulled and parsed successfully", + hasChanges = true, + commit = gitState?.Commit, + branch = gitState?.Branch, + timestamp = gitState?.PullTime + }); + } + catch (Exception ex) { + _logger.LogError(ex, "Error processing git webhook"); + return StatusCode(500, new { + success = false, + error = ex.Message + }); + } + } + + [HttpGet("status")] + public IActionResult GetStatus() { + try { + var gitState = _gitService.GetGitState(); + + if (gitState == null) { + return NotFound(new { error = "Could not retrieve git state" }); + } + + return Ok(new { + commit = gitState.Commit, + commitShort = gitState.Commit?[..7], + branch = gitState.Branch, + url = gitState.URL, + timestamp = gitState.PullTime + }); + } + catch (Exception ex) { + _logger.LogError(ex, "Error getting git status"); + return StatusCode(500, new { error = ex.Message }); + } + } + + private bool ValidateSignature(string payload, string? signatureHeader, string secret) { + if (string.IsNullOrEmpty(signatureHeader)) { + _logger.LogWarning("Signature validation failed: No signature header provided"); + return false; + } + + // GitHub uses HMAC-SHA256 with format "sha256=" + var prefix = "sha256="; + if (!signatureHeader.StartsWith(prefix)) { + _logger.LogWarning("Signature validation failed: Header doesn't start with '{Prefix}', got: {Header}", prefix, signatureHeader); + return false; + } + + var expectedHash = signatureHeader.Substring(prefix.Length); + + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); + var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)); + var computedHash = Convert.ToHexString(hash).ToLower(); + + // Test with GitHub's example values + var testPayload = "Hello, World!"; + var testSecret = "It's a Secret to Everybody"; + var testExpected = "757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17"; + using var testHmac = new HMACSHA256(Encoding.UTF8.GetBytes(testSecret)); + var testHash = testHmac.ComputeHash(Encoding.UTF8.GetBytes(testPayload)); + var testComputed = Convert.ToHexString(testHash).ToLower(); + _logger.LogWarning("GitHub test case - Expected: {Expected}, Computed: {Computed}, Match: {Match}", + testExpected, testComputed, testExpected == testComputed); + + _logger.LogWarning("Signature validation - Expected: {Expected}, Computed: {Computed}, Match: {Match}", + expectedHash, computedHash, expectedHash == computedHash); + + return CryptographicOperations.FixedTimeEquals( + Encoding.UTF8.GetBytes(computedHash), + Encoding.UTF8.GetBytes(expectedHash) + ); + } +} \ No newline at end of file diff --git a/HaWeb/FileHelpers/GitService.cs b/HaWeb/FileHelpers/GitService.cs new file mode 100644 index 0000000..16d35bc --- /dev/null +++ b/HaWeb/FileHelpers/GitService.cs @@ -0,0 +1,147 @@ +namespace HaWeb.FileHelpers; +using LibGit2Sharp; +using HaWeb.Models; + +public class GitService : IGitService { + private readonly string _repositoryPath; + private readonly string _remoteName; + private readonly string _branch; + private readonly string _url; + private readonly ILogger? _logger; + + public GitService(IConfiguration config, ILogger? logger = null) { + _logger = logger; + _remoteName = "origin"; + _branch = config.GetValue("RepositoryBranch") ?? "main"; + _url = config.GetValue("RepositoryURL") ?? string.Empty; + + var fileStoragePath = config.GetValue("FileStoragePath") ?? throw new ArgumentException("FileStoragePath not configured"); + _repositoryPath = Path.Combine(fileStoragePath, "GIT"); + + // Ensure repository exists + if (!Repository.IsValid(_repositoryPath)) { + _logger?.LogWarning("Repository not found at {Path}, attempting to initialize/clone", _repositoryPath); + InitializeRepository(); + } + } + + public GitState? GetGitState() { + try { + using var repo = new Repository(_repositoryPath); + var headCommit = repo.Head.Tip; + + if (headCommit == null) { + _logger?.LogWarning("No commits found in repository"); + return null; + } + + return new GitState { + Commit = headCommit.Sha, + Branch = repo.Head.FriendlyName, + URL = _url, + PullTime = headCommit.Author.When.ToLocalTime().DateTime + }; + } + catch (Exception ex) { + _logger?.LogError(ex, "Failed to get Git state"); + return null; + } + } + + public bool Pull() { + try { + using var repo = new Repository(_repositoryPath); + var oldCommitSha = repo.Head.Tip?.Sha; + + // Configure pull options + var options = new PullOptions { + FetchOptions = new FetchOptions { + CredentialsProvider = (_url, _user, _cred) => GetCredentials() + } + }; + + // Create signature for merge commit (if needed) + var signature = new Signature( + new Identity("HaWeb Service", "hawebservice@localhost"), + DateTimeOffset.Now + ); + + // Perform pull + var result = Commands.Pull(repo, signature, options); + + var newCommitSha = repo.Head.Tip?.Sha; + var hasChanges = oldCommitSha != newCommitSha; + + if (hasChanges) { + _logger?.LogInformation("Pull successful: {OldCommit} -> {NewCommit}", + oldCommitSha?[..7], newCommitSha?[..7]); + } else { + _logger?.LogInformation("Pull completed but no new changes"); + } + + return hasChanges; + } + catch (Exception ex) { + _logger?.LogError(ex, "Failed to pull from remote repository"); + return false; + } + } + + public bool HasChanged(string? previousCommitSha) { + if (string.IsNullOrEmpty(previousCommitSha)) { + return true; + } + + var currentState = GetGitState(); + return currentState != null && currentState.Commit != previousCommitSha; + } + + private void InitializeRepository() { + try { + if (!Directory.Exists(_repositoryPath)) { + Directory.CreateDirectory(_repositoryPath); + } + + // If there's a .git directory but it's invalid, try to reinitialize + var gitDir = Path.Combine(_repositoryPath, ".git"); + if (Directory.Exists(gitDir)) { + _logger?.LogWarning("Invalid .git directory found, attempting to use existing repository"); + return; + } + + // If URL is provided, clone; otherwise just initialize + if (!string.IsNullOrEmpty(_url)) { + _logger?.LogInformation("Cloning repository from {Url} to {Path}", _url, _repositoryPath); + var cloneOptions = new CloneOptions(); + cloneOptions.FetchOptions.CredentialsProvider = (_url, _user, _cred) => GetCredentials(); + + // Clone with default branch, then checkout the specified branch if different + Repository.Clone(_url, _repositoryPath, cloneOptions); + _logger?.LogInformation("Repository cloned successfully"); + + // Checkout the specified branch if it's not the default + using var repo = new Repository(_repositoryPath); + var branch = repo.Branches[_branch] ?? repo.Branches[$"origin/{_branch}"]; + if (branch != null && branch.FriendlyName != repo.Head.FriendlyName) { + Commands.Checkout(repo, branch); + _logger?.LogInformation("Checked out branch {Branch}", _branch); + } + } + else { + _logger?.LogInformation("Initializing empty repository at {Path}", _repositoryPath); + Repository.Init(_repositoryPath); + } + } + catch (Exception ex) { + _logger?.LogError(ex, "Failed to initialize repository"); + throw; + } + } + + private Credentials? GetCredentials() { + // For now, use default credentials (SSH agent, credential manager, etc.) + // Can be extended to support username/password or personal access tokens + // from configuration if needed + return new DefaultCredentials(); + } +} \ No newline at end of file diff --git a/HaWeb/FileHelpers/IGitService.cs b/HaWeb/FileHelpers/IGitService.cs new file mode 100644 index 0000000..8420293 --- /dev/null +++ b/HaWeb/FileHelpers/IGitService.cs @@ -0,0 +1,19 @@ +namespace HaWeb.FileHelpers; + +public interface IGitService { + /// + /// Gets the current Git state (commit SHA, branch, timestamp) + /// + GitState? GetGitState(); + + /// + /// Pulls latest changes from the remote repository + /// + /// True if pull was successful and changes were detected, false otherwise + bool Pull(); + + /// + /// Checks if the repository has a different commit than the provided SHA + /// + bool HasChanged(string? previousCommitSha); +} \ No newline at end of file diff --git a/HaWeb/FileHelpers/IXMLFileProvider.cs b/HaWeb/FileHelpers/IXMLFileProvider.cs index 7a0a337..8d56ebe 100644 --- a/HaWeb/FileHelpers/IXMLFileProvider.cs +++ b/HaWeb/FileHelpers/IXMLFileProvider.cs @@ -18,4 +18,5 @@ public interface IXMLFileProvider { public bool HasChanged(); public void DeleteHamannFile(string filename); public void Scan(); + public void Reload(); } \ No newline at end of file diff --git a/HaWeb/FileHelpers/XMLFileProvider.cs b/HaWeb/FileHelpers/XMLFileProvider.cs index 543440c..be77a0a 100644 --- a/HaWeb/FileHelpers/XMLFileProvider.cs +++ b/HaWeb/FileHelpers/XMLFileProvider.cs @@ -4,16 +4,15 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using HaWeb.Models; using HaWeb.XMLParser; using System.Xml.Linq; -using System.Runtime.InteropServices; using Microsoft.Extensions.Primitives; // XMLProvider provides a wrapper around the available XML data on a FILE basis public class XMLFileProvider : IXMLFileProvider { private readonly IHaDocumentWrappper _Lib; private readonly IXMLInteractionService _XMLService; + private readonly IGitService _GitService; private IFileProvider _hamannFileProvider; - private IFileProvider _bareRepositoryFileProvider; private IFileProvider _workingTreeFileProvider; public event EventHandler FileChange; @@ -21,9 +20,6 @@ public class XMLFileProvider : IXMLFileProvider { public event EventHandler NewState; public event EventHandler NewData; - private string _Branch; - private string _URL; - private List? _WorkingTreeFiles; private List? _HamannFiles; @@ -31,24 +27,28 @@ public class XMLFileProvider : IXMLFileProvider { private System.Timers.Timer? _changeTokenTimer; // Startup (LAST) - public XMLFileProvider(IXMLInteractionService xmlservice, IHaDocumentWrappper _lib, IConfiguration config) { + public XMLFileProvider(IXMLInteractionService xmlservice, IHaDocumentWrappper _lib, IGitService gitService, IConfiguration config) { // TODO: Test Read / Write Access _Lib = _lib; _XMLService = xmlservice; + _GitService = gitService; - _Branch = config.GetValue("RepositoryBranch"); - _URL = config.GetValue("RepositoryURL"); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - _hamannFileProvider = new PhysicalFileProvider(config.GetValue("HamannFileStoreWindows")); - _bareRepositoryFileProvider = new PhysicalFileProvider(config.GetValue("BareRepositoryPathWindows")); - _workingTreeFileProvider = new PhysicalFileProvider(config.GetValue("WorkingTreePathWindows")); + var fileStoragePath = config.GetValue("FileStoragePath") ?? throw new ArgumentException("FileStoragePath not configured"); + + // Ensure directories exist + var hamannPath = Path.Combine(fileStoragePath, "HAMANN"); + var gitPath = Path.Combine(fileStoragePath, "GIT"); + + if (!Directory.Exists(hamannPath)) { + Directory.CreateDirectory(hamannPath); } - else { - _hamannFileProvider = new PhysicalFileProvider(config.GetValue("HamannFileStoreLinux")); - _bareRepositoryFileProvider = new PhysicalFileProvider(config.GetValue("BareRepositoryPathLinux")); - _workingTreeFileProvider = new PhysicalFileProvider(config.GetValue("WorkingTreePathLinux")); + if (!Directory.Exists(gitPath)) { + Directory.CreateDirectory(gitPath); } + _hamannFileProvider = new PhysicalFileProvider(hamannPath); + _workingTreeFileProvider = new PhysicalFileProvider(gitPath); + // Create File Lists; Here and in xmlservice, which does preliminary checking Scan(); if (_WorkingTreeFiles != null && _WorkingTreeFiles.Any()) { @@ -57,7 +57,6 @@ public class XMLFileProvider : IXMLFileProvider { } _HamannFiles = _ScanHamannFiles(); - _RegisterChangeToken(); // Check if hamann file already is current working tree status // -> YES: Load up the file via _lib.SetLibrary(); if (_IsAlreadyParsed()) { @@ -91,8 +90,6 @@ public class XMLFileProvider : IXMLFileProvider { } public void ParseConfiguration(IConfiguration config) { - _Branch = config.GetValue("RepositoryBranch"); - Scan(); // Reset XMLInteractionService if (_WorkingTreeFiles != null && _WorkingTreeFiles.Any()) { @@ -158,7 +155,59 @@ public class XMLFileProvider : IXMLFileProvider { public void Scan() { _WorkingTreeFiles = _ScanWorkingTreeFiles(); - _GitState = _ScanGitData(); + _GitState = _GitService.GetGitState(); + } + + public void Reload() { + Scan(); + + // Reset XMLInteractionService + if (_WorkingTreeFiles != null && _WorkingTreeFiles.Any()) { + var state = _XMLService.Collect(_WorkingTreeFiles, _XMLService.GetRootDefs()); + _XMLService.SetState(state); + OnNewState(state); + } + + _HamannFiles = _ScanHamannFiles(); + _XMLService.SetSCCache(null); + + // Check if hamann file already is current working tree status + if (_IsAlreadyParsed()) { + _Lib.SetLibrary(_HamannFiles!.First(), null, null); + if (_Lib.GetLibrary() != null) { + OnNewData(); + return; + } + } + + // Try to create a new file + var created = _XMLService.TryCreate(_XMLService.GetState()); + if (created != null) { + var file = SaveHamannFile(created, _hamannFileProvider.GetFileInfo("./").PhysicalPath, null); + if (file != null) { + _Lib.SetLibrary(file, created.Document, null); + if (_Lib.GetLibrary() != null) { + OnNewData(); + return; + } + } + } + + // It failed, so use the last best File: + if (_HamannFiles != null && _HamannFiles.Any()) { + _Lib.SetLibrary(_HamannFiles.First(), null, null); + if (_Lib.GetLibrary() != null) { + OnNewData(); + return; + } + } + + // Use Fallback: + var options = new HaWeb.Settings.HaDocumentOptions(); + if (_Lib.SetLibrary(null, null, null) == null) { + throw new Exception("Die Fallback Hamann.xml unter " + options.HamannXMLFilePath + " kann nicht geparst werden."); + } + OnNewData(); } public IFileInfo? SaveHamannFile(XElement element, string basefilepath, ModelStateDictionary? ModelState) { @@ -191,7 +240,7 @@ public class XMLFileProvider : IXMLFileProvider { public bool HasChanged() { if (_GitState == null) return true; - var current = _ScanGitData(); + var current = _GitService.GetGitState(); if (current != null && !String.Equals(current.Commit, _GitState.Commit)) { _GitState = current; return true; @@ -199,22 +248,6 @@ public class XMLFileProvider : IXMLFileProvider { return false; } - private GitState? _ScanGitData() { - var head = _bareRepositoryFileProvider.GetFileInfo("refs/heads/" + _Branch); - // TODO: Failsave reading from FIle - try { - return new GitState { - URL = _URL, - Branch = _Branch, - PullTime = head.LastModified.ToLocalTime().DateTime, - Commit = File.ReadAllText(head.PhysicalPath).Trim() - }; - } - catch { - return null; - } - } - // Gets all XML Files private List? _ScanWorkingTreeFiles() { var files = _workingTreeFileProvider.GetDirectoryContents(string.Empty)!.Where(x => !x.IsDirectory && x.Name.EndsWith(".xml"))!.ToList(); @@ -240,46 +273,6 @@ public class XMLFileProvider : IXMLFileProvider { return fhash == ghash; } - private void _RegisterChangeToken() { - ChangeToken.OnChange( - () => _bareRepositoryFileProvider.Watch("refs/heads/" + _Branch), - async (state) => await this._InvokeChanged(state), - this._ScanGitData() - ); - } - - private async Task _InvokeChanged(GitState? gitdata) { - if (_changeTokenTimer != null) return; - Console.WriteLine("FILECHANGE DETECTED, RELOAD"); - Scan(); - - OnFileChange(_ScanGitData()); - // Reset XMLInteractionService - if (_WorkingTreeFiles != null && _WorkingTreeFiles.Any()) { - var state = _XMLService.Collect(_WorkingTreeFiles, _XMLService.GetRootDefs()); - _XMLService.SetState(state); - OnNewState(state); - } - - // -> Try to create a new file - var created = _XMLService.TryCreate(_XMLService.GetState()); - if (created != null) { - var file = SaveHamannFile(created, _hamannFileProvider.GetFileInfo("./").PhysicalPath, null); - if (file != null) { - var ret = _Lib.SetLibrary(file, created.Document, null); - if (ret != null) OnNewData(); - } - } - - _XMLService.SetSCCache(null); - _GitState = _ScanGitData(); - _changeTokenTimer = new(5000) { AutoReset = false, Enabled = true }; - _changeTokenTimer.Elapsed += this._OnElapsed; - } - - private void _OnElapsed(Object source, System.Timers.ElapsedEventArgs e) { - _changeTokenTimer = null; - } protected virtual void OnFileChange(GitState? state) { EventHandler eh = FileChange; diff --git a/HaWeb/HaWeb.csproj b/HaWeb/HaWeb.csproj index c173c91..db8f81e 100644 --- a/HaWeb/HaWeb.csproj +++ b/HaWeb/HaWeb.csproj @@ -8,6 +8,7 @@ + diff --git a/HaWeb/Program.cs b/HaWeb/Program.cs index b6823b1..9a10c31 100644 --- a/HaWeb/Program.cs +++ b/HaWeb/Program.cs @@ -4,30 +4,24 @@ using HaWeb.XMLParser; using HaWeb.XMLTests; using HaWeb.FileHelpers; using Microsoft.FeatureManagement; -using System.Runtime.InteropServices; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Primitives; var builder = WebApplication.CreateBuilder(args); -// Add additional configuration -List configpaths = new List(); -if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - var p = builder.Configuration.GetValue("WorkingTreePathWindows") + "settings.json"; - configpaths.Add(p); - builder.Configuration.AddJsonFile(p, optional: true, reloadOnChange: true); -} -else { - var p = builder.Configuration.GetValue("WorkingTreePathLinux") + "settings.json"; - configpaths.Add(p); - builder.Configuration.AddJsonFile(p, optional: true, reloadOnChange: true); +// Add additional configuration from Git repository +var fileStoragePath = builder.Configuration.GetValue("FileStoragePath"); +if (!string.IsNullOrEmpty(fileStoragePath)) { + var externalSettingsPath = Path.Combine(fileStoragePath, "GIT", "settings.json"); + builder.Configuration.AddJsonFile(externalSettingsPath, optional: true, reloadOnChange: true); } // Create initial Data var tS = new XMLTestService(); var XMLIS = new XMLInteractionService(builder.Configuration, tS); var hdW = new HaDocumentWrapper(XMLIS, builder.Configuration); -var XMLFP = new XMLFileProvider(XMLIS, hdW, builder.Configuration); +var gitService = new GitService(builder.Configuration, builder.Services.BuildServiceProvider().GetService>()); +var XMLFP = new XMLFileProvider(XMLIS, hdW, gitService, builder.Configuration); // Add services to the container. builder.Services.AddControllers().AddXmlSerializerFormatters(); @@ -36,6 +30,7 @@ builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton((_) => tS); builder.Services.AddSingleton((_) => XMLIS); builder.Services.AddSingleton((_) => hdW); +builder.Services.AddSingleton((_) => gitService); builder.Services.AddSingleton(_ => XMLFP); builder.Services.AddSingleton(); builder.Services.AddTransient(); diff --git a/HaWeb/appsettings.Development.json b/HaWeb/appsettings.Development.json index a81506a..a2ddc33 100644 --- a/HaWeb/appsettings.Development.json +++ b/HaWeb/appsettings.Development.json @@ -13,16 +13,10 @@ }, "AllowedWebSocketConnections": "*", "AllowedHosts": "*", - "HamannFileStoreLinux": "/home/simon/source/hamann-ausgabe-core/HaWeb/testdata/", - "HamannFileStoreWindows": "C:/Users/simon/Downloads/test/", - "BareRepositoryPathLinux": "/home/simon/source/hamann-xml/.git/", - "BareRepositoryPathWindows": "D:/Simon/source/hamann-xml/.git/", - "WorkingTreePathLinux": "/home/simon/source/hamann-xml/", - "WorkingTreePathWindows": "D:/Simon/source/hamann-xml/", + "FileStoragePath": "/home/simon/source/hamann-ausgabe-core/HaWeb/testdata/", "RepositoryBranch": "Main", "RepositoryURL": "https://github.com/Theodor-Springmann-Stiftung/hamann-xml", - "StoredPDFPathWindows": "", - "StoredPDFPathLinux": "", + "WebhookSecret": "secret", "FileSizeLimit": 52428800, "AvailableStartYear": 1700, "AvailableEndYear": 1800, diff --git a/HaWeb/appsettings.Staging.json b/HaWeb/appsettings.Staging.json index 589dca5..6025cb9 100644 --- a/HaWeb/appsettings.Staging.json +++ b/HaWeb/appsettings.Staging.json @@ -13,14 +13,10 @@ }, "AllowedWebSocketConnections": "*", "AllowedHosts": "*", - "HamannFileStoreLinux": "/var/www/vhosts/development.hamann-ausgabe.de/httpdocs/Hamann/", - "BareRepositoryPathLinux": "/var/www/vhosts/development.hamann-ausgabe.de/httpdocs/Bare/", - "BareRepositoryPathWindows": "C:/Users/simon/source/hamann-xml/.git/", - "WorkingTreePathLinux": "/var/www/vhosts/development.hamann-ausgabe.de/httpdocs/Repo/", + "FileStoragePath": "/var/www/vhosts/development.hamann-ausgabe.de/httpdocs/Storage/", "RepositoryBranch": "main", "RepositoryURL": "https://github.com/Theodor-Springmann-Stiftung/hamann-xml", - "StoredPDFPathWindows": "", - "StoredPDFPathLinux": "", + "WebhookSecret": "", "FileSizeLimit": 52428800, "AvailableStartYear": 1700, "AvailableEndYear": 1800, diff --git a/HaWeb/appsettings.json b/HaWeb/appsettings.json index 3ab346c..8667073 100644 --- a/HaWeb/appsettings.json +++ b/HaWeb/appsettings.json @@ -13,10 +13,8 @@ }, "AllowedWebSocketConnections": "*", "AllowedHosts": "*", - "HamannFileStoreLinux": "/var/www/vhosts/hamann-ausgabe.de/httpdocs/Hamann/", - "BareRepositoryPathLinux": "/var/www/vhosts/hamann-ausgabe.de/httpdocs/Bare/", - "BareRepositoryPathWindows": "D:/Simon/source/hamann-xml/.git/", - "WorkingTreePathLinux": "/var/www/vhosts/hamann-ausgabe.de/httpdocs/Repo/", - "WorkingTreePathWindows": "D:/Simon/source/hamann-xml/", - "RepositoryBranch": "main" + "FileStoragePath": "/var/www/vhosts/hamann-ausgabe.de/httpdocs/Storage/", + "RepositoryBranch": "main", + "RepositoryURL": "", + "WebhookSecret": "" } diff --git a/HaWeb/package-lock.json b/HaWeb/package-lock.json index a0522bb..8236dca 100644 --- a/HaWeb/package-lock.json +++ b/HaWeb/package-lock.json @@ -1657,6 +1657,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -3722,6 +3723,7 @@ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", "dev": true, + "peer": true, "requires": { "nanoid": "^3.3.7", "picocolors": "^1.0.0",