Self-Hosted Git

This commit is contained in:
Simon Martens
2025-09-30 17:04:30 +02:00
parent 2a2a292cc9
commit 8c9dc19d5b
12 changed files with 573 additions and 110 deletions

168
HaWeb/CLAUDE.md Normal file
View 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.

View 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)
);
}
}

View 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();
}
}

View 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);
}

View File

@@ -18,4 +18,5 @@ public interface IXMLFileProvider {
public bool HasChanged();
public void DeleteHamannFile(string filename);
public void Scan();
public void Reload();
}

View File

@@ -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;

View File

@@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LibGit2Sharp" Version="0.31.0" />
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="2.5.1" />
</ItemGroup>

View File

@@ -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>();

View File

@@ -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,

View File

@@ -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,

View File

@@ -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": ""
}

View File

@@ -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",