mirror of
https://github.com/Theodor-Springmann-Stiftung/hamann-ausgabe-core.git
synced 2025-10-29 01:05:32 +00:00
Self-Hosted Git
This commit is contained in:
168
HaWeb/CLAUDE.md
Normal file
168
HaWeb/CLAUDE.md
Normal file
@@ -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.
|
||||
149
HaWeb/Controllers/WebhookController.cs
Normal file
149
HaWeb/Controllers/WebhookController.cs
Normal file
@@ -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<WebhookController> _logger;
|
||||
|
||||
public WebhookController(
|
||||
IGitService gitService,
|
||||
IXMLFileProvider xmlProvider,
|
||||
IConfiguration config,
|
||||
ILogger<WebhookController> logger) {
|
||||
_gitService = gitService;
|
||||
_xmlProvider = xmlProvider;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("git")]
|
||||
public async Task<IActionResult> GitWebhook([FromHeader(Name = "X-Hub-Signature-256")] string? signature) {
|
||||
try {
|
||||
// Validate webhook secret if configured
|
||||
var webhookSecret = _config.GetValue<string>("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=<hash>"
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
147
HaWeb/FileHelpers/GitService.cs
Normal file
147
HaWeb/FileHelpers/GitService.cs
Normal file
@@ -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<GitService>? _logger;
|
||||
|
||||
public GitService(IConfiguration config, ILogger<GitService>? logger = null) {
|
||||
_logger = logger;
|
||||
_remoteName = "origin";
|
||||
_branch = config.GetValue<string>("RepositoryBranch") ?? "main";
|
||||
_url = config.GetValue<string>("RepositoryURL") ?? string.Empty;
|
||||
|
||||
var fileStoragePath = config.GetValue<string>("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();
|
||||
}
|
||||
}
|
||||
19
HaWeb/FileHelpers/IGitService.cs
Normal file
19
HaWeb/FileHelpers/IGitService.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace HaWeb.FileHelpers;
|
||||
|
||||
public interface IGitService {
|
||||
/// <summary>
|
||||
/// Gets the current Git state (commit SHA, branch, timestamp)
|
||||
/// </summary>
|
||||
GitState? GetGitState();
|
||||
|
||||
/// <summary>
|
||||
/// Pulls latest changes from the remote repository
|
||||
/// </summary>
|
||||
/// <returns>True if pull was successful and changes were detected, false otherwise</returns>
|
||||
bool Pull();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the repository has a different commit than the provided SHA
|
||||
/// </summary>
|
||||
bool HasChanged(string? previousCommitSha);
|
||||
}
|
||||
@@ -18,4 +18,5 @@ public interface IXMLFileProvider {
|
||||
public bool HasChanged();
|
||||
public void DeleteHamannFile(string filename);
|
||||
public void Scan();
|
||||
public void Reload();
|
||||
}
|
||||
@@ -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<GitState?> FileChange;
|
||||
@@ -21,9 +20,6 @@ public class XMLFileProvider : IXMLFileProvider {
|
||||
public event EventHandler<XMLParsingState?> NewState;
|
||||
public event EventHandler NewData;
|
||||
|
||||
private string _Branch;
|
||||
private string _URL;
|
||||
|
||||
private List<IFileInfo>? _WorkingTreeFiles;
|
||||
private List<IFileInfo>? _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<string>("RepositoryBranch");
|
||||
_URL = config.GetValue<string>("RepositoryURL");
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||
_hamannFileProvider = new PhysicalFileProvider(config.GetValue<string>("HamannFileStoreWindows"));
|
||||
_bareRepositoryFileProvider = new PhysicalFileProvider(config.GetValue<string>("BareRepositoryPathWindows"));
|
||||
_workingTreeFileProvider = new PhysicalFileProvider(config.GetValue<string>("WorkingTreePathWindows"));
|
||||
var fileStoragePath = config.GetValue<string>("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<string>("HamannFileStoreLinux"));
|
||||
_bareRepositoryFileProvider = new PhysicalFileProvider(config.GetValue<string>("BareRepositoryPathLinux"));
|
||||
_workingTreeFileProvider = new PhysicalFileProvider(config.GetValue<string>("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<string>("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<IFileInfo>? _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<GitState?> eh = FileChange;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LibGit2Sharp" Version="0.31.0" />
|
||||
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="2.5.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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<string> configpaths = new List<string>();
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||
var p = builder.Configuration.GetValue<string>("WorkingTreePathWindows") + "settings.json";
|
||||
configpaths.Add(p);
|
||||
builder.Configuration.AddJsonFile(p, optional: true, reloadOnChange: true);
|
||||
}
|
||||
else {
|
||||
var p = builder.Configuration.GetValue<string>("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<string>("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<ILogger<GitService>>());
|
||||
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<IXMLTestService, XMLTestService>((_) => tS);
|
||||
builder.Services.AddSingleton<IXMLInteractionService, XMLInteractionService>((_) => XMLIS);
|
||||
builder.Services.AddSingleton<IHaDocumentWrappper, HaDocumentWrapper>((_) => hdW);
|
||||
builder.Services.AddSingleton<IGitService, GitService>((_) => gitService);
|
||||
builder.Services.AddSingleton<IXMLFileProvider, XMLFileProvider>(_ => XMLFP);
|
||||
builder.Services.AddSingleton<WebSocketMiddleware>();
|
||||
builder.Services.AddTransient<IReaderService, ReaderService>();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
2
HaWeb/package-lock.json
generated
2
HaWeb/package-lock.json
generated
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user