- ghost admin
This commit is contained in:
parent
00d2681a70
commit
a730639c2a
22 changed files with 1036 additions and 56 deletions
56
.gitignore
vendored
56
.gitignore
vendored
|
|
@ -6,3 +6,59 @@
|
||||||
/GhostImageManager/bin/*
|
/GhostImageManager/bin/*
|
||||||
/GhostImageManager/obj/*
|
/GhostImageManager/obj/*
|
||||||
/GhostImageManager/Properties/*
|
/GhostImageManager/Properties/*
|
||||||
|
/GhostImageManager/logs/ghost-image-manager-20260927.log
|
||||||
|
/GhostImageManager/logs/ghost-image-manager-20260928.log
|
||||||
|
/GhostImageManager/ghost-image-manager.db*
|
||||||
|
/images/2026/09/20222424_1_o.jpg
|
||||||
|
/images/2026/09/20222424_1.jpg
|
||||||
|
/images/2026/09/20222424_10_o.jpg
|
||||||
|
/images/2026/09/20222424_10.jpg
|
||||||
|
/images/2026/09/20222424_11_o.jpg
|
||||||
|
/images/2026/09/20222424_11.jpg
|
||||||
|
/images/2026/09/20222424_12_o.jpg
|
||||||
|
/images/2026/09/20222424_12.jpg
|
||||||
|
/images/2026/09/20222424_13_o.jpg
|
||||||
|
/images/2026/09/20222424_13.jpg
|
||||||
|
/images/2026/09/20222424_14_o.jpg
|
||||||
|
/images/2026/09/20222424_14.jpg
|
||||||
|
/images/2026/09/20222424_15_o.jpg
|
||||||
|
/images/2026/09/20222424_15.jpg
|
||||||
|
/images/2026/09/20222424_16_o.jpg
|
||||||
|
/images/2026/09/20222424_16.jpg
|
||||||
|
/images/2026/09/20222424_17_o.jpg
|
||||||
|
/images/2026/09/20222424_17.jpg
|
||||||
|
/images/2026/09/20222424_18_o.jpg
|
||||||
|
/images/2026/09/20222424_18.jpg
|
||||||
|
/images/2026/09/20222424_19_o.jpg
|
||||||
|
/images/2026/09/20222424_19.jpg
|
||||||
|
/images/2026/09/20222424_2_o.jpg
|
||||||
|
/images/2026/09/20222424_2.jpg
|
||||||
|
/images/2026/09/20222424_20_o.jpg
|
||||||
|
/images/2026/09/20222424_20.jpg
|
||||||
|
/images/2026/09/20222424_21_o.jpg
|
||||||
|
/images/2026/09/20222424_21.jpg
|
||||||
|
/images/2026/09/20222424_22_o.jpg
|
||||||
|
/images/2026/09/20222424_22.jpg
|
||||||
|
/images/2026/09/20222424_23_o.jpg
|
||||||
|
/images/2026/09/20222424_23.jpg
|
||||||
|
/images/2026/09/20222424_24_o.jpg
|
||||||
|
/images/2026/09/20222424_24.jpg
|
||||||
|
/images/2026/09/20222424_25_o.jpg
|
||||||
|
/images/2026/09/20222424_25.jpg
|
||||||
|
/images/2026/09/20222424_3_o.jpg
|
||||||
|
/images/2026/09/20222424_3.jpg
|
||||||
|
/images/2026/09/20222424_4_o.jpg
|
||||||
|
/images/2026/09/20222424_4.jpg
|
||||||
|
/images/2026/09/20222424_5_o.jpg
|
||||||
|
/images/2026/09/20222424_5.jpg
|
||||||
|
/images/2026/09/20222424_6_o.jpg
|
||||||
|
/images/2026/09/20222424_6.jpg
|
||||||
|
/images/2026/09/20222424_7_o.jpg
|
||||||
|
/images/2026/09/20222424_7.jpg
|
||||||
|
/images/2026/09/20222424_8_o.jpg
|
||||||
|
/images/2026/09/20222424_8.jpg
|
||||||
|
/images/2026/09/20222424_9_o.jpg
|
||||||
|
/images/2026/09/20222424_9.jpg
|
||||||
|
/images/size/w1000/2026/09/20222424_25.jpg
|
||||||
|
/images/size/w2000/2026/09/20222424_25.jpg
|
||||||
|
/images/size/w600/2026/09/20222424_25.jpg
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@
|
||||||
<span class="bi bi-images" aria-hidden="true"></span> Bilder
|
<span class="bi bi-images" aria-hidden="true"></span> Bilder
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="logs">
|
||||||
|
<span class="bi bi-journal-text" aria-hidden="true"></span> Logs
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="settings">
|
<NavLink class="nav-link" href="settings">
|
||||||
<span class="bi bi-gear-fill" aria-hidden="true"></span> Einstellungen
|
<span class="bi bi-gear-fill" aria-hidden="true"></span> Einstellungen
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
@inject GhostConfigService ConfigService
|
@inject GhostConfigService ConfigService
|
||||||
|
@inject GhostApiService GhostApi
|
||||||
@inject ImageAnalyzerService ImageAnalyzer
|
@inject ImageAnalyzerService ImageAnalyzer
|
||||||
@inject ImageReplacementService ReplacementService
|
@inject ImageReplacementService ReplacementService
|
||||||
@inject ImageDeletionService DeletionService
|
@inject ImageDeletionService DeletionService
|
||||||
@inject ImageExportService ExportService
|
@inject ImageExportService ExportService
|
||||||
|
@inject ImageDatabaseService DbService
|
||||||
@inject IJSRuntime JsRuntime
|
@inject IJSRuntime JsRuntime
|
||||||
@inject ILogger<Images> Logger
|
@inject ILogger<Images> Logger
|
||||||
|
|
||||||
|
|
@ -22,7 +24,7 @@
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
<label class="form-label">Ghost-Instanz</label>
|
<label class="form-label">Ghost-Instanz</label>
|
||||||
<select class="form-select" @bind="_selectedInstanceId" @bind:after="OnInstanceChanged">
|
<select class="form-select" @bind="_selectedInstanceId" @bind:after="OnInstanceChanged">
|
||||||
@foreach (var instance in _instances)
|
@foreach (var instance in _instances)
|
||||||
|
|
@ -31,14 +33,37 @@ else
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-2">
|
||||||
<label class="form-label">Suche nach Post</label>
|
<label class="form-label">Suche nach Post</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" @bind="_searchTerm" @bind:event="oninput" placeholder="Post-Titel..." />
|
<input type="text" class="form-control" @bind="_searchTerm" @bind:event="oninput" placeholder="Post-Titel..." />
|
||||||
<button class="btn btn-outline-secondary" @onclick="LoadImages" disabled="@_isLoading">Suchen</button>
|
<button class="btn btn-outline-secondary" @onclick="LoadImages" disabled="@_isLoading">Suchen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 d-flex align-items-end">
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Autor</label>
|
||||||
|
<select class="form-select" @bind="_authorFilter" @bind:after="ApplyFilters">
|
||||||
|
<option value="">Alle Autoren</option>
|
||||||
|
@foreach (var author in AvailableAuthors)
|
||||||
|
{
|
||||||
|
<option value="@author.Key">@author.Value (@GetAuthorCount(author.Key))</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Thema</label>
|
||||||
|
<select class="form-select" @bind="_themeFilter" @bind:after="ApplyFilters">
|
||||||
|
<option value="">Alle Themen</option>
|
||||||
|
@foreach (var theme in AvailableThemes)
|
||||||
|
{
|
||||||
|
<option value="@theme">@theme (@GetThemeCount(theme))</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 d-flex align-items-end">
|
||||||
|
<button class="btn btn-outline-secondary me-2" @onclick="ResetFilters" title="Filter zurücksetzen">
|
||||||
|
<i class="bi bi-x-circle"></i>
|
||||||
|
</button>
|
||||||
<button class="btn btn-primary me-2" @onclick="LoadImages" disabled="@_isLoading">
|
<button class="btn btn-primary me-2" @onclick="LoadImages" disabled="@_isLoading">
|
||||||
@if (_isLoading)
|
@if (_isLoading)
|
||||||
{
|
{
|
||||||
|
|
@ -95,6 +120,19 @@ else
|
||||||
<button class="btn btn-outline-danger @GetFilterClass(ImageFilter.Orphaned)" @onclick="() => SetFilter(ImageFilter.Orphaned)">Verwaist (@_orphanedCount)</button>
|
<button class="btn btn-outline-danger @GetFilterClass(ImageFilter.Orphaned)" @onclick="() => SetFilter(ImageFilter.Orphaned)">Verwaist (@_orphanedCount)</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="text-muted small">
|
||||||
|
Gefiltert: <strong>@_filteredImages.Count</strong> von @_allImages.Count
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_authorFilter) || !string.IsNullOrWhiteSpace(_themeFilter))
|
||||||
|
{
|
||||||
|
<span class="ms-2">
|
||||||
|
(@(!string.IsNullOrWhiteSpace(_authorFilter) ? "Autor" : "")
|
||||||
|
@(!string.IsNullOrWhiteSpace(_authorFilter) && !string.IsNullOrWhiteSpace(_themeFilter) ? " + " : "")
|
||||||
|
@(!string.IsNullOrWhiteSpace(_themeFilter) ? "Thema" : ""))
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="selectAllOrphaned" @onchange="ToggleSelectAllOrphaned" checked="@_selectAllOrphaned" />
|
<input class="form-check-input" type="checkbox" id="selectAllOrphaned" @onchange="ToggleSelectAllOrphaned" checked="@_selectAllOrphaned" />
|
||||||
|
|
@ -126,7 +164,24 @@ else
|
||||||
<strong>Pfad:</strong> <span class="text-truncate d-block" title="@image.RelativePath">@image.RelativePath</span>
|
<strong>Pfad:</strong> <span class="text-truncate d-block" title="@image.RelativePath">@image.RelativePath</span>
|
||||||
<strong>Größe:</strong> @image.FormattedFileSize<br />
|
<strong>Größe:</strong> @image.FormattedFileSize<br />
|
||||||
<strong>Dimensionen:</strong> @image.Dimensions<br />
|
<strong>Dimensionen:</strong> @image.Dimensions<br />
|
||||||
<strong>Datum:</strong> @image.LastModified.ToString("dd.MM.yyyy HH:mm")
|
<strong>Datei-Datum:</strong> @image.LastModified.ToString("dd.MM.yyyy HH:mm")<br />
|
||||||
|
@if (!string.IsNullOrEmpty(image.AuthorName))
|
||||||
|
{
|
||||||
|
<strong>Autor:</strong> @image.AuthorName<br />
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(image.Theme))
|
||||||
|
{
|
||||||
|
<strong>Thema:</strong> @image.Theme<br />
|
||||||
|
}
|
||||||
|
@if (image.CaptureDate.HasValue)
|
||||||
|
{
|
||||||
|
<strong>Aufnahmedatum:</strong> @image.CaptureDate.Value.ToString("dd.MM.yyyy HH:mm")<br />
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(image.CameraModel))
|
||||||
|
{
|
||||||
|
<strong>Kamera:</strong> @image.CameraDisplay<br />
|
||||||
|
<span class="d-block">@image.ExifSummary</span>
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if (image.IsOrphaned)
|
@if (image.IsOrphaned)
|
||||||
|
|
@ -157,10 +212,13 @@ else
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
<button class="btn btn-sm btn-outline-primary" @onclick="() => ShowReplaceDialog(image)">
|
<button class="btn btn-sm btn-outline-primary" @onclick="() => ShowReplaceDialog(image)">
|
||||||
Tauschen
|
Tauschen
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" @onclick="() => ShowEditDialog(image)">
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
@if (image.IsOrphaned)
|
@if (image.IsOrphaned)
|
||||||
{
|
{
|
||||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ShowDeleteDialog(image)">
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => ShowDeleteDialog(image)">
|
||||||
|
|
@ -299,11 +357,77 @@ else
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (_showEditDialog && _editingImage != null)
|
||||||
|
{
|
||||||
|
<div class="modal show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Bild bearbeiten: @_editingImage.FileName</h5>
|
||||||
|
<button type="button" class="btn-close" @onclick="CloseEditDialog"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<img src="@_editingImage.Url" class="img-fluid border" alt="@_editingImage.FileName" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Autor</label>
|
||||||
|
<select class="form-select" @bind="_editAuthorId">
|
||||||
|
<option value="">(keiner)</option>
|
||||||
|
@foreach (var user in _ghostUsers)
|
||||||
|
{
|
||||||
|
<option value="@user.Id">@user.Name</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Thema</label>
|
||||||
|
<input type="text" class="form-control" @bind="_editTheme" placeholder="z.B. Landschaft, Portrait..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Aufnahmedatum</label>
|
||||||
|
<InputDate class="form-control" @bind-Value="_editCaptureDate" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info small">
|
||||||
|
<strong>Kamera:</strong> @_editingImage.CameraDisplay<br />
|
||||||
|
<strong>EXIF:</strong> @_editingImage.ExifSummary<br />
|
||||||
|
<strong>Dateigröße:</strong> @_editingImage.FormattedFileSize | @_editingImage.Dimensions
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @onclick="CloseEditDialog">Abbrechen</button>
|
||||||
|
<button type="button" class="btn btn-primary" @onclick="SaveEdit" disabled="@_isSavingEdit">
|
||||||
|
@if (_isSavingEdit)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
<span>Speichere...</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Speichern</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<GhostInstance> _instances = new();
|
private List<GhostInstance> _instances = new();
|
||||||
private string _selectedInstanceId = "";
|
private string _selectedInstanceId = "";
|
||||||
private string _searchTerm = "";
|
private string _searchTerm = "";
|
||||||
|
private string _authorFilter = "";
|
||||||
|
private string _themeFilter = "";
|
||||||
private List<GhostImage> _allImages = new();
|
private List<GhostImage> _allImages = new();
|
||||||
|
private List<GhostImage> _filteredImages = new();
|
||||||
private bool _isLoading;
|
private bool _isLoading;
|
||||||
private bool _hasLoaded;
|
private bool _hasLoaded;
|
||||||
private string? _errorMessage;
|
private string? _errorMessage;
|
||||||
|
|
@ -327,6 +451,15 @@ else
|
||||||
private HashSet<string> _selectedImageUrls = new();
|
private HashSet<string> _selectedImageUrls = new();
|
||||||
private bool _selectAllOrphaned;
|
private bool _selectAllOrphaned;
|
||||||
|
|
||||||
|
private List<GhostUser> _ghostUsers = new();
|
||||||
|
|
||||||
|
private GhostImage? _editingImage;
|
||||||
|
private bool _showEditDialog;
|
||||||
|
private string? _editAuthorId;
|
||||||
|
private string? _editTheme;
|
||||||
|
private DateTime? _editCaptureDate;
|
||||||
|
private bool _isSavingEdit;
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
var config = ConfigService.Load();
|
var config = ConfigService.Load();
|
||||||
|
|
@ -337,8 +470,11 @@ else
|
||||||
private async Task OnInstanceChanged()
|
private async Task OnInstanceChanged()
|
||||||
{
|
{
|
||||||
_allImages.Clear();
|
_allImages.Clear();
|
||||||
|
_filteredImages.Clear();
|
||||||
_hasLoaded = false;
|
_hasLoaded = false;
|
||||||
_selectedImageUrls.Clear();
|
_selectedImageUrls.Clear();
|
||||||
|
_authorFilter = "";
|
||||||
|
_themeFilter = "";
|
||||||
await LoadImages();
|
await LoadImages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -355,9 +491,14 @@ else
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var instance = _instances.First(i => i.Id == _selectedInstanceId);
|
var instance = _instances.First(i => i.Id == _selectedInstanceId);
|
||||||
|
|
||||||
|
// Ghost-Benutzer laden
|
||||||
|
_ghostUsers = await GhostApi.GetUsersAsync(instance);
|
||||||
|
|
||||||
var filter = string.IsNullOrWhiteSpace(_searchTerm) ? "" : $"title:~'{EscapeFilter(_searchTerm)}'";
|
var filter = string.IsNullOrWhiteSpace(_searchTerm) ? "" : $"title:~'{EscapeFilter(_searchTerm)}'";
|
||||||
_allImages = await ImageAnalyzer.AnalyzeImagesAsync(instance, filter);
|
_allImages = await ImageAnalyzer.AnalyzeImagesAsync(instance, filter);
|
||||||
CalculateCounts();
|
CalculateCounts();
|
||||||
|
ApplyFilters();
|
||||||
_hasLoaded = true;
|
_hasLoaded = true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -379,19 +520,61 @@ else
|
||||||
_contentCount = _allImages.Count(i => i.UsedInPosts.Any(r => r.IsInContent));
|
_contentCount = _allImages.Count(i => i.UsedInPosts.Any(r => r.IsInContent));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<GhostImage> FilteredImages => _currentFilter switch
|
private List<GhostImage> FilteredImages => _filteredImages;
|
||||||
{
|
|
||||||
ImageFilter.Orphaned => _allImages.Where(i => i.IsOrphaned).ToList(),
|
|
||||||
ImageFilter.Feature => _allImages.Where(i => i.UsedInPosts.Any(r => r.IsFeatureImage)).ToList(),
|
|
||||||
ImageFilter.Content => _allImages.Where(i => i.UsedInPosts.Any(r => r.IsInContent)).ToList(),
|
|
||||||
_ => _allImages
|
|
||||||
};
|
|
||||||
|
|
||||||
private List<GhostImage> SelectedOrphanedImages => _allImages.Where(i => _selectedImageUrls.Contains(i.Url) && i.IsOrphaned).ToList();
|
private List<GhostImage> SelectedOrphanedImages => _filteredImages.Where(i => _selectedImageUrls.Contains(i.Url) && i.IsOrphaned).ToList();
|
||||||
|
|
||||||
|
private List<KeyValuePair<string, string>> AvailableAuthors => _allImages
|
||||||
|
.Where(i => !string.IsNullOrEmpty(i.AuthorId))
|
||||||
|
.GroupBy(i => i.AuthorId!)
|
||||||
|
.Select(g => new KeyValuePair<string, string>(g.Key, g.First().AuthorName ?? "Unbekannt"))
|
||||||
|
.OrderBy(kvp => kvp.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private List<string> AvailableThemes => _allImages
|
||||||
|
.Where(i => !string.IsNullOrEmpty(i.Theme))
|
||||||
|
.Select(i => i.Theme!)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(t => t)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private int GetAuthorCount(string authorId) => _allImages.Count(i => i.AuthorId == authorId);
|
||||||
|
private int GetThemeCount(string theme) => _allImages.Count(i => i.Theme == theme);
|
||||||
|
|
||||||
|
private void ApplyFilters()
|
||||||
|
{
|
||||||
|
var query = _allImages.AsEnumerable();
|
||||||
|
|
||||||
|
if (_currentFilter == ImageFilter.Orphaned)
|
||||||
|
query = query.Where(i => i.IsOrphaned);
|
||||||
|
else if (_currentFilter == ImageFilter.Feature)
|
||||||
|
query = query.Where(i => i.UsedInPosts.Any(r => r.IsFeatureImage));
|
||||||
|
else if (_currentFilter == ImageFilter.Content)
|
||||||
|
query = query.Where(i => i.UsedInPosts.Any(r => r.IsInContent));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_authorFilter))
|
||||||
|
query = query.Where(i => i.AuthorId == _authorFilter);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_themeFilter))
|
||||||
|
query = query.Where(i => i.Theme == _themeFilter);
|
||||||
|
|
||||||
|
_filteredImages = query.ToList();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetFilters()
|
||||||
|
{
|
||||||
|
_authorFilter = "";
|
||||||
|
_themeFilter = "";
|
||||||
|
_searchTerm = "";
|
||||||
|
_currentFilter = ImageFilter.All;
|
||||||
|
ApplyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
private void SetFilter(ImageFilter filter)
|
private void SetFilter(ImageFilter filter)
|
||||||
{
|
{
|
||||||
_currentFilter = filter;
|
_currentFilter = filter;
|
||||||
|
ApplyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetFilterClass(ImageFilter filter) => _currentFilter == filter ? "active" : "";
|
private string GetFilterClass(ImageFilter filter) => _currentFilter == filter ? "active" : "";
|
||||||
|
|
@ -507,6 +690,8 @@ else
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
_selectedImageUrls.Remove(_selectedImage.Url);
|
_selectedImageUrls.Remove(_selectedImage.Url);
|
||||||
|
if (_selectedImage.ManagedImageId.HasValue)
|
||||||
|
await DbService.DeleteAsync(_selectedImage.ManagedImageId.Value);
|
||||||
_successMessage = $"{_selectedImage.FileName} gelöscht.";
|
_successMessage = $"{_selectedImage.FileName} gelöscht.";
|
||||||
CloseDeleteDialog();
|
CloseDeleteDialog();
|
||||||
await LoadImages();
|
await LoadImages();
|
||||||
|
|
@ -522,6 +707,59 @@ else
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShowEditDialog(GhostImage image)
|
||||||
|
{
|
||||||
|
_editingImage = image;
|
||||||
|
_editAuthorId = image.AuthorId;
|
||||||
|
_editTheme = image.Theme;
|
||||||
|
_editCaptureDate = image.CaptureDate;
|
||||||
|
_showEditDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseEditDialog()
|
||||||
|
{
|
||||||
|
_showEditDialog = false;
|
||||||
|
_editingImage = null;
|
||||||
|
_editAuthorId = null;
|
||||||
|
_editTheme = null;
|
||||||
|
_editCaptureDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveEdit()
|
||||||
|
{
|
||||||
|
if (_editingImage?.ManagedImageId == null) return;
|
||||||
|
|
||||||
|
_isSavingEdit = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var selectedAuthor = _ghostUsers.FirstOrDefault(u => u.Id == _editAuthorId);
|
||||||
|
await DbService.UpdateAuthorAsync(_editingImage.ManagedImageId.Value, _editAuthorId, selectedAuthor?.Name);
|
||||||
|
await DbService.UpdateThemeAsync(_editingImage.ManagedImageId.Value, _editTheme);
|
||||||
|
await DbService.UpdateCaptureDateAsync(_editingImage.ManagedImageId.Value, _editCaptureDate);
|
||||||
|
|
||||||
|
// Lokale Anzeige aktualisieren
|
||||||
|
_editingImage.AuthorId = _editAuthorId;
|
||||||
|
_editingImage.AuthorName = selectedAuthor?.Name;
|
||||||
|
_editingImage.Theme = _editTheme;
|
||||||
|
_editingImage.CaptureDate = _editCaptureDate;
|
||||||
|
|
||||||
|
_successMessage = $"{_editingImage.FileName} aktualisiert.";
|
||||||
|
CloseEditDialog();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Fehler beim Speichern der Bild-Metadaten");
|
||||||
|
_errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isSavingEdit = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadNewImage(InputFileChangeEventArgs e)
|
private async Task LoadNewImage(InputFileChangeEventArgs e)
|
||||||
{
|
{
|
||||||
_newImageFile = e.File;
|
_newImageFile = e.File;
|
||||||
|
|
|
||||||
96
GhostImageManager/Components/Pages/Logs.razor
Normal file
96
GhostImageManager/Components/Pages/Logs.razor
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
@page "/logs"
|
||||||
|
@attribute [Authorize]
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@inject LogService LogService
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
|
||||||
|
<PageTitle>Logs</PageTitle>
|
||||||
|
|
||||||
|
<h1>System-Logs</h1>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Log-Datei</label>
|
||||||
|
<select class="form-select" @bind="_selectedFileName" @bind:after="LoadSelectedLog">
|
||||||
|
@if (_logFiles.Count == 0)
|
||||||
|
{
|
||||||
|
<option value="">Keine Logs vorhanden</option>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@foreach (var file in _logFiles)
|
||||||
|
{
|
||||||
|
<option value="@Path.GetFileName(file)">@Path.GetFileName(file)</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Zeilen</label>
|
||||||
|
<select class="form-select" @bind="_maxLines" @bind:after="LoadSelectedLog">
|
||||||
|
<option value="100">100</option>
|
||||||
|
<option value="500">500</option>
|
||||||
|
<option value="1000">1000</option>
|
||||||
|
<option value="5000">5000</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 d-flex align-items-end">
|
||||||
|
<button class="btn btn-outline-secondary me-2" @onclick="LoadSelectedLog">Aktualisieren</button>
|
||||||
|
<button class="btn btn-outline-primary" @onclick="DownloadLog">Herunterladen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span>Log-Ausgabe</span>
|
||||||
|
<span class="badge bg-secondary">@_logContent.Split('\n').Length Zeilen</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<pre class="m-0 p-3 bg-dark text-light" style="max-height: 70vh; overflow: auto; font-size: 0.85rem;"><code>@_logContent</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<string> _logFiles = new();
|
||||||
|
private string _selectedFileName = "";
|
||||||
|
private int _maxLines = 1000;
|
||||||
|
private string _logContent = "Keine Log-Datei ausgewählt.";
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
LoadLogFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadLogFiles()
|
||||||
|
{
|
||||||
|
_logFiles = LogService.GetLogFiles();
|
||||||
|
if (_logFiles.Any() && string.IsNullOrEmpty(_selectedFileName))
|
||||||
|
{
|
||||||
|
_selectedFileName = Path.GetFileName(_logFiles.First());
|
||||||
|
LoadSelectedLog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadSelectedLog()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_selectedFileName))
|
||||||
|
{
|
||||||
|
_logContent = "Keine Log-Datei ausgewählt.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logContent = LogService.ReadLogFile(_selectedFileName, _maxLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DownloadLog()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_selectedFileName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var content = LogService.ReadLogFile(_selectedFileName, int.MaxValue);
|
||||||
|
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
|
||||||
|
var base64 = Convert.ToBase64String(bytes);
|
||||||
|
|
||||||
|
await JSRuntime.InvokeVoidAsync("downloadFileFromBase64", _selectedFileName, base64, "text/plain");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -82,8 +82,8 @@
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Lokaler Image-Ordner</label>
|
<label class="form-label">Lokaler Image-Ordner</label>
|
||||||
<InputText @bind-Value="_editingInstance.ImageFolderPath" class="form-control" placeholder="C:\\Ghost\\content\\images" />
|
<InputText @bind-Value="_editingInstance.ImageFolderPath" class="form-control" placeholder="C:\\Ghost\\content\\images oder /var/www/ghost/content/images" />
|
||||||
<div class="form-text">Vollständiger Pfad zum Ghost content/images Ordner</div>
|
<div class="form-text">Vollständiger Pfad zum Ghost content/images Ordner (Windows oder Linux)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
|
|
|
||||||
41
GhostImageManager/Data/AppDbContext.cs
Normal file
41
GhostImageManager/Data/AppDbContext.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace GhostImageManager.Data;
|
||||||
|
|
||||||
|
public class AppDbContext : DbContext
|
||||||
|
{
|
||||||
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<ManagedImage> ManagedImages => Set<ManagedImage>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<ManagedImage>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.HasIndex(e => e.GhostImageUrl);
|
||||||
|
entity.HasIndex(e => e.GhostInstanceId);
|
||||||
|
entity.HasIndex(e => e.FileName);
|
||||||
|
entity.HasIndex(e => e.AuthorId);
|
||||||
|
entity.HasIndex(e => e.Theme);
|
||||||
|
entity.HasIndex(e => e.CaptureDate);
|
||||||
|
|
||||||
|
entity.Property(e => e.GhostImageUrl).HasMaxLength(2000);
|
||||||
|
entity.Property(e => e.RelativePath).HasMaxLength(1000);
|
||||||
|
entity.Property(e => e.FileName).HasMaxLength(500);
|
||||||
|
entity.Property(e => e.GhostInstanceId).HasMaxLength(100);
|
||||||
|
entity.Property(e => e.AuthorId).HasMaxLength(100);
|
||||||
|
entity.Property(e => e.AuthorName).HasMaxLength(200);
|
||||||
|
entity.Property(e => e.CameraMake).HasMaxLength(200);
|
||||||
|
entity.Property(e => e.CameraModel).HasMaxLength(200);
|
||||||
|
entity.Property(e => e.Lens).HasMaxLength(200);
|
||||||
|
entity.Property(e => e.Aperture).HasMaxLength(50);
|
||||||
|
entity.Property(e => e.ShutterSpeed).HasMaxLength(50);
|
||||||
|
entity.Property(e => e.Iso).HasMaxLength(50);
|
||||||
|
entity.Property(e => e.FocalLength).HasMaxLength(50);
|
||||||
|
entity.Property(e => e.Theme).HasMaxLength(500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
44
GhostImageManager/Data/ManagedImage.cs
Normal file
44
GhostImageManager/Data/ManagedImage.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
namespace GhostImageManager.Data;
|
||||||
|
|
||||||
|
public class ManagedImage
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
// Verknüpfung zum Ghost-Bild
|
||||||
|
public string GhostImageUrl { get; set; } = "";
|
||||||
|
public string RelativePath { get; set; } = "";
|
||||||
|
public string FileName { get; set; } = "";
|
||||||
|
|
||||||
|
// Instanz-Zuordnung
|
||||||
|
public string GhostInstanceId { get; set; } = "";
|
||||||
|
|
||||||
|
// Autor (Ghost-Benutzer)
|
||||||
|
public string? AuthorId { get; set; }
|
||||||
|
public string? AuthorName { get; set; }
|
||||||
|
|
||||||
|
// EXIF-Daten
|
||||||
|
public string? CameraMake { get; set; }
|
||||||
|
public string? CameraModel { get; set; }
|
||||||
|
public string? Lens { get; set; }
|
||||||
|
public string? Aperture { get; set; }
|
||||||
|
public string? ShutterSpeed { get; set; }
|
||||||
|
public string? Iso { get; set; }
|
||||||
|
public string? FocalLength { get; set; }
|
||||||
|
|
||||||
|
// Thema / Topic
|
||||||
|
public string? Theme { get; set; }
|
||||||
|
|
||||||
|
// Aufnahmedatum
|
||||||
|
public DateTime? CaptureDate { get; set; }
|
||||||
|
|
||||||
|
// Datei-Metadaten
|
||||||
|
public long FileSize { get; set; }
|
||||||
|
public int Width { get; set; }
|
||||||
|
public int Height { get; set; }
|
||||||
|
public DateTime FileModifiedDate { get; set; }
|
||||||
|
|
||||||
|
// Verwaltung
|
||||||
|
public DateTime FirstSeen { get; set; }
|
||||||
|
public DateTime LastUpdated { get; set; }
|
||||||
|
public bool IsOrphaned { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,10 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.66" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.66" />
|
||||||
|
<PackageReference Include="MetadataExtractor" Version="2.8.1" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
3
GhostImageManager/GhostImageManager.slnx
Normal file
3
GhostImageManager/GhostImageManager.slnx
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<Solution>
|
||||||
|
<Project Path="GhostImageManager.csproj" />
|
||||||
|
</Solution>
|
||||||
|
|
@ -2,6 +2,7 @@ namespace GhostImageManager.Models;
|
||||||
|
|
||||||
public class GhostImage
|
public class GhostImage
|
||||||
{
|
{
|
||||||
|
public int? ManagedImageId { get; set; }
|
||||||
public string Url { get; set; } = "";
|
public string Url { get; set; } = "";
|
||||||
public string FileName { get; set; } = "";
|
public string FileName { get; set; } = "";
|
||||||
public string RelativePath { get; set; } = "";
|
public string RelativePath { get; set; } = "";
|
||||||
|
|
@ -13,6 +14,21 @@ public class GhostImage
|
||||||
public bool IsOrphaned { get; set; } = true;
|
public bool IsOrphaned { get; set; } = true;
|
||||||
public List<PostReference> UsedInPosts { get; set; } = new();
|
public List<PostReference> UsedInPosts { get; set; } = new();
|
||||||
|
|
||||||
|
// Aus der Datenbank
|
||||||
|
public string? AuthorId { get; set; }
|
||||||
|
public string? AuthorName { get; set; }
|
||||||
|
public string? Theme { get; set; }
|
||||||
|
public DateTime? CaptureDate { get; set; }
|
||||||
|
|
||||||
|
// EXIF
|
||||||
|
public string? CameraMake { get; set; }
|
||||||
|
public string? CameraModel { get; set; }
|
||||||
|
public string? Lens { get; set; }
|
||||||
|
public string? Aperture { get; set; }
|
||||||
|
public string? ShutterSpeed { get; set; }
|
||||||
|
public string? Iso { get; set; }
|
||||||
|
public string? FocalLength { get; set; }
|
||||||
|
|
||||||
public string FormattedFileSize => FileSize switch
|
public string FormattedFileSize => FileSize switch
|
||||||
{
|
{
|
||||||
>= 1024 * 1024 => $"{FileSize / (1024.0 * 1024.0):F2} MB",
|
>= 1024 * 1024 => $"{FileSize / (1024.0 * 1024.0):F2} MB",
|
||||||
|
|
@ -21,6 +37,12 @@ public class GhostImage
|
||||||
};
|
};
|
||||||
|
|
||||||
public string Dimensions => Width > 0 && Height > 0 ? $"{Width}x{Height}" : "n/a";
|
public string Dimensions => Width > 0 && Height > 0 ? $"{Width}x{Height}" : "n/a";
|
||||||
|
|
||||||
|
public string CameraDisplay => string.IsNullOrEmpty(CameraModel)
|
||||||
|
? "n/a"
|
||||||
|
: $"{CameraMake} {CameraModel}".Trim();
|
||||||
|
|
||||||
|
public string ExifSummary => $"{Aperture ?? "-"} | {ShutterSpeed ?? "-"} | ISO {Iso ?? "-"} | {FocalLength ?? "-"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PostReference
|
public class PostReference
|
||||||
|
|
|
||||||
10
GhostImageManager/Models/GhostUser.cs
Normal file
10
GhostImageManager/Models/GhostUser.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace GhostImageManager.Models;
|
||||||
|
|
||||||
|
public class GhostUser
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Slug { get; set; } = "";
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? ProfileImage { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,42 @@
|
||||||
using GhostImageManager.Components;
|
using GhostImageManager.Components;
|
||||||
|
using GhostImageManager.Data;
|
||||||
using GhostImageManager.Models;
|
using GhostImageManager.Models;
|
||||||
using GhostImageManager.Services;
|
using GhostImageManager.Services;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Logging konfigurieren
|
||||||
|
var logsDir = Path.Combine(builder.Environment.ContentRootPath, "logs");
|
||||||
|
if (!Directory.Exists(logsDir))
|
||||||
|
Directory.CreateDirectory(logsDir);
|
||||||
|
|
||||||
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.MinimumLevel.Information()
|
||||||
|
.MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
|
||||||
|
.MinimumLevel.Override("Microsoft.AspNetCore", Serilog.Events.LogEventLevel.Warning)
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.WriteTo.Console()
|
||||||
|
.WriteTo.File(
|
||||||
|
Path.Combine(logsDir, "ghost-image-manager-.log"),
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
retainedFileCountLimit: 30,
|
||||||
|
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
builder.Host.UseSerilog();
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
|
|
||||||
|
// Datenbank
|
||||||
|
var dbPath = Path.Combine(builder.Environment.ContentRootPath, "ghost-image-manager.db");
|
||||||
|
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||||
|
options.UseSqlite($"Data Source={dbPath}"));
|
||||||
|
|
||||||
builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));
|
builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));
|
||||||
builder.Services.AddSingleton<GhostConfigService>();
|
builder.Services.AddSingleton<GhostConfigService>();
|
||||||
builder.Services.AddScoped<GhostApiService>();
|
builder.Services.AddScoped<GhostApiService>();
|
||||||
|
|
@ -16,7 +44,10 @@ builder.Services.AddScoped<ImageAnalyzerService>();
|
||||||
builder.Services.AddScoped<ImageReplacementService>();
|
builder.Services.AddScoped<ImageReplacementService>();
|
||||||
builder.Services.AddScoped<ImageDeletionService>();
|
builder.Services.AddScoped<ImageDeletionService>();
|
||||||
builder.Services.AddScoped<ImageExportService>();
|
builder.Services.AddScoped<ImageExportService>();
|
||||||
|
builder.Services.AddScoped<ImageDatabaseService>();
|
||||||
|
builder.Services.AddScoped<ExifService>();
|
||||||
builder.Services.AddScoped<AuthService>();
|
builder.Services.AddScoped<AuthService>();
|
||||||
|
builder.Services.AddSingleton<LogService>();
|
||||||
builder.Services.AddHttpClient<GhostApiService>();
|
builder.Services.AddHttpClient<GhostApiService>();
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
|
|
@ -41,6 +72,13 @@ builder.WebHost.ConfigureKestrel(options =>
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Datenbank sicherstellen
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
|
|
@ -73,7 +111,7 @@ app.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode();
|
.AddInteractiveServerRenderMode();
|
||||||
|
|
||||||
// Authentication API endpoints
|
// Authentication API endpoints
|
||||||
app.MapPost("/auth/login", async (HttpContext context, AuthService authService) =>
|
app.MapPost("/auth/login", async (HttpContext context, AuthService authService, ILogger<Program> logger) =>
|
||||||
{
|
{
|
||||||
var form = await context.Request.ReadFormAsync();
|
var form = await context.Request.ReadFormAsync();
|
||||||
var password = form["password"].ToString();
|
var password = form["password"].ToString();
|
||||||
|
|
@ -84,19 +122,25 @@ app.MapPost("/auth/login", async (HttpContext context, AuthService authService)
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Benutzer meldet sich an");
|
||||||
await authService.SignInAsync(context, password);
|
await authService.SignInAsync(context, password);
|
||||||
|
logger.LogInformation("Anmeldung erfolgreich");
|
||||||
return Results.Redirect(returnUrl);
|
return Results.Redirect(returnUrl);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
|
logger.LogWarning("Fehlgeschlagene Anmeldung");
|
||||||
return Results.Redirect($"/login?errorMessage={Uri.EscapeDataString("Ungültiges Passwort.")}&returnUrl={Uri.EscapeDataString(returnUrl)}");
|
return Results.Redirect($"/login?errorMessage={Uri.EscapeDataString("Ungültiges Passwort.")}&returnUrl={Uri.EscapeDataString(returnUrl)}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.MapPost("/auth/logout", async (HttpContext context, AuthService authService) =>
|
app.MapPost("/auth/logout", async (HttpContext context, AuthService authService, ILogger<Program> logger) =>
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Benutzer meldet sich ab");
|
||||||
await authService.SignOutAsync(context);
|
await authService.SignOutAsync(context);
|
||||||
return Results.Redirect("/login");
|
return Results.Redirect("/login");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
Log.CloseAndFlush();
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,25 @@ namespace GhostImageManager.Services;
|
||||||
public class AuthService
|
public class AuthService
|
||||||
{
|
{
|
||||||
private readonly AppSettings _appSettings;
|
private readonly AppSettings _appSettings;
|
||||||
|
private readonly ILogger<AuthService> _logger;
|
||||||
|
|
||||||
public AuthService(IConfiguration configuration)
|
public AuthService(IConfiguration configuration, ILogger<AuthService> logger)
|
||||||
{
|
{
|
||||||
_appSettings = configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();
|
_appSettings = configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ValidatePassword(string password)
|
public bool ValidatePassword(string password)
|
||||||
{
|
{
|
||||||
return !string.IsNullOrEmpty(password) &&
|
var isValid = !string.IsNullOrEmpty(password) &&
|
||||||
password == _appSettings.Password;
|
password == _appSettings.Password;
|
||||||
|
|
||||||
|
if (!isValid)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Passwort-Validierung fehlgeschlagen");
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SignInAsync(HttpContext context, string password)
|
public async Task SignInAsync(HttpContext context, string password)
|
||||||
|
|
|
||||||
92
GhostImageManager/Services/ExifService.cs
Normal file
92
GhostImageManager/Services/ExifService.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
using MetadataExtractor;
|
||||||
|
using MetadataExtractor.Formats.Exif;
|
||||||
|
|
||||||
|
namespace GhostImageManager.Services;
|
||||||
|
|
||||||
|
public class ExifData
|
||||||
|
{
|
||||||
|
public string? CameraMake { get; set; }
|
||||||
|
public string? CameraModel { get; set; }
|
||||||
|
public string? Lens { get; set; }
|
||||||
|
public string? Aperture { get; set; }
|
||||||
|
public string? ShutterSpeed { get; set; }
|
||||||
|
public string? Iso { get; set; }
|
||||||
|
public string? FocalLength { get; set; }
|
||||||
|
public DateTime? CaptureDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ExifService
|
||||||
|
{
|
||||||
|
private readonly ILogger<ExifService> _logger;
|
||||||
|
|
||||||
|
public ExifService(ILogger<ExifService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExifData ExtractExifData(string filePath)
|
||||||
|
{
|
||||||
|
var result = new ExifData();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("EXIF-Extraktion: Datei nicht gefunden {FilePath}", filePath);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var directories = ImageMetadataReader.ReadMetadata(filePath);
|
||||||
|
var ifd0Directory = directories.OfType<ExifIfd0Directory>().FirstOrDefault();
|
||||||
|
var subIfdDirectory = directories.OfType<ExifSubIfdDirectory>().FirstOrDefault();
|
||||||
|
var exifDirectory = directories.OfType<ExifDirectoryBase>().FirstOrDefault();
|
||||||
|
|
||||||
|
// Kamera
|
||||||
|
if (ifd0Directory != null)
|
||||||
|
{
|
||||||
|
result.CameraMake = ifd0Directory.GetDescription(ExifDirectoryBase.TagMake);
|
||||||
|
result.CameraModel = ifd0Directory.GetDescription(ExifDirectoryBase.TagModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aufnahmedatum
|
||||||
|
if (exifDirectory != null && exifDirectory.TryGetDateTime(ExifDirectoryBase.TagDateTimeOriginal, out var captureDate))
|
||||||
|
{
|
||||||
|
result.CaptureDate = captureDate;
|
||||||
|
}
|
||||||
|
else if (exifDirectory != null && exifDirectory.TryGetDateTime(ExifDirectoryBase.TagDateTime, out var dateTime))
|
||||||
|
{
|
||||||
|
result.CaptureDate = dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subIfdDirectory != null)
|
||||||
|
{
|
||||||
|
// Blende
|
||||||
|
result.Aperture = subIfdDirectory.GetDescription(ExifDirectoryBase.TagAperture);
|
||||||
|
if (string.IsNullOrEmpty(result.Aperture))
|
||||||
|
result.Aperture = subIfdDirectory.GetDescription(ExifDirectoryBase.TagFNumber);
|
||||||
|
|
||||||
|
// Verschlusszeit
|
||||||
|
result.ShutterSpeed = subIfdDirectory.GetDescription(ExifDirectoryBase.TagExposureTime);
|
||||||
|
|
||||||
|
// ISO
|
||||||
|
result.Iso = subIfdDirectory.GetDescription(ExifDirectoryBase.TagIsoEquivalent);
|
||||||
|
|
||||||
|
// Brennweite
|
||||||
|
result.FocalLength = subIfdDirectory.GetDescription(ExifDirectoryBase.TagFocalLength);
|
||||||
|
|
||||||
|
// Objektiv
|
||||||
|
result.Lens = subIfdDirectory.GetDescription(ExifDirectoryBase.TagLensModel);
|
||||||
|
if (string.IsNullOrEmpty(result.Lens))
|
||||||
|
result.Lens = subIfdDirectory.GetDescription(ExifDirectoryBase.TagLensSpecification);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("EXIF-Daten für {File} extrahiert", Path.GetFileName(filePath));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Fehler beim Extrahieren der EXIF-Daten für {FilePath}", filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,25 +1,34 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using GhostImageManager.Models;
|
using GhostImageManager.Models;
|
||||||
using HtmlAgilityPack;
|
|
||||||
|
|
||||||
namespace GhostImageManager.Services;
|
namespace GhostImageManager.Services;
|
||||||
|
|
||||||
public class GhostApiService
|
public class GhostApiService
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<GhostApiService> _logger;
|
||||||
|
|
||||||
public GhostApiService(HttpClient httpClient)
|
public GhostApiService(HttpClient httpClient, ILogger<GhostApiService> logger)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<GhostPost>> GetPostsAsync(GhostInstance instance, string filter = "", int limit = 100)
|
public async Task<List<GhostPost>> GetPostsAsync(GhostInstance instance, string filter = "", int limit = 100)
|
||||||
{
|
{
|
||||||
var request = CreateRequest(HttpMethod.Get, instance, $"posts/?limit={limit}&filter={Uri.EscapeDataString(filter)}&fields=id,title,slug,feature_image,html,created_at,updated_at,status");
|
var endpoint = $"posts/?limit={limit}&filter={Uri.EscapeDataString(filter)}&formats=html,plaintext&fields=id,title,slug,feature_image,html,created_at,updated_at,status";
|
||||||
|
_logger.LogInformation("Hole Posts von {Url} (Filter: {Filter}, Limit: {Limit})", instance.Url, filter, limit);
|
||||||
|
|
||||||
|
var request = CreateRequest(HttpMethod.Get, instance, endpoint);
|
||||||
var response = await _httpClient.SendAsync(request);
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Ghost API Fehler beim Laden der Posts: {StatusCode} - {Error}", response.StatusCode, errorContent);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
var doc = JsonDocument.Parse(json);
|
var doc = JsonDocument.Parse(json);
|
||||||
|
|
@ -43,14 +52,23 @@ public class GhostApiService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("{Count} Posts von Ghost geladen", posts.Count);
|
||||||
return posts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GhostPost?> GetPostAsync(GhostInstance instance, string postId)
|
public async Task<GhostPost?> GetPostAsync(GhostInstance instance, string postId)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Lade Post {PostId} von {Url}", postId, instance.Url);
|
||||||
|
|
||||||
var request = CreateRequest(HttpMethod.Get, instance, $"posts/{postId}/?fields=id,title,slug,feature_image,html,created_at,updated_at,status");
|
var request = CreateRequest(HttpMethod.Get, instance, $"posts/{postId}/?fields=id,title,slug,feature_image,html,created_at,updated_at,status");
|
||||||
var response = await _httpClient.SendAsync(request);
|
var response = await _httpClient.SendAsync(request);
|
||||||
if (!response.IsSuccessStatusCode) return null;
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Ghost API Fehler beim Laden von Post {PostId}: {StatusCode} - {Error}", postId, response.StatusCode, errorContent);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
var doc = JsonDocument.Parse(json);
|
var doc = JsonDocument.Parse(json);
|
||||||
|
|
@ -76,6 +94,8 @@ public class GhostApiService
|
||||||
|
|
||||||
public async Task<bool> UpdatePostAsync(GhostInstance instance, GhostPost post)
|
public async Task<bool> UpdatePostAsync(GhostInstance instance, GhostPost post)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Aktualisiere Post {PostId} ({Title}) in Ghost", post.Id, post.Title);
|
||||||
|
|
||||||
var request = CreateRequest(HttpMethod.Put, instance, $"posts/{post.Id}/?source=html");
|
var request = CreateRequest(HttpMethod.Put, instance, $"posts/{post.Id}/?source=html");
|
||||||
var body = new
|
var body = new
|
||||||
{
|
{
|
||||||
|
|
@ -95,11 +115,59 @@ public class GhostApiService
|
||||||
|
|
||||||
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
|
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
|
||||||
var response = await _httpClient.SendAsync(request);
|
var response = await _httpClient.SendAsync(request);
|
||||||
return response.IsSuccessStatusCode;
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Ghost API Fehler beim Aktualisieren von Post {PostId}: {StatusCode} - {Error}", post.Id, response.StatusCode, errorContent);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Post {PostId} erfolgreich aktualisiert", post.Id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<GhostUser>> GetUsersAsync(GhostInstance instance)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Hole Benutzer von {Url}", instance.Url);
|
||||||
|
|
||||||
|
var request = CreateRequest(HttpMethod.Get, instance, "users/?limit=100&fields=id,name,slug,email,profile_image");
|
||||||
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Ghost API Fehler beim Laden der Benutzer: {StatusCode} - {Error}", response.StatusCode, errorContent);
|
||||||
|
return new List<GhostUser>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
var doc = JsonDocument.Parse(json);
|
||||||
|
var users = new List<GhostUser>();
|
||||||
|
|
||||||
|
if (doc.RootElement.TryGetProperty("users", out var usersElement))
|
||||||
|
{
|
||||||
|
foreach (var user in usersElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
users.Add(new GhostUser
|
||||||
|
{
|
||||||
|
Id = GetString(user, "id"),
|
||||||
|
Name = GetString(user, "name"),
|
||||||
|
Slug = GetString(user, "slug"),
|
||||||
|
Email = GetString(user, "email"),
|
||||||
|
ProfileImage = GetString(user, "profile_image")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("{Count} Benutzer von Ghost geladen", users.Count);
|
||||||
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> UploadImageAsync(GhostInstance instance, Stream fileStream, string fileName, string? reference = null)
|
public async Task<string?> UploadImageAsync(GhostInstance instance, Stream fileStream, string fileName, string? reference = null)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Lade Bild {FileName} zu Ghost hoch", fileName);
|
||||||
|
|
||||||
var token = JwtTokenService.GenerateToken(instance.AdminApiKey);
|
var token = JwtTokenService.GenerateToken(instance.AdminApiKey);
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, $"{instance.Url.TrimEnd('/')}/ghost/api/admin/images/upload/");
|
var request = new HttpRequestMessage(HttpMethod.Post, $"{instance.Url.TrimEnd('/')}/ghost/api/admin/images/upload/");
|
||||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Ghost", token);
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Ghost", token);
|
||||||
|
|
@ -125,15 +193,24 @@ public class GhostApiService
|
||||||
|
|
||||||
request.Content = content;
|
request.Content = content;
|
||||||
var response = await _httpClient.SendAsync(request);
|
var response = await _httpClient.SendAsync(request);
|
||||||
if (!response.IsSuccessStatusCode) return null;
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Ghost API Fehler beim Hochladen von {FileName}: {StatusCode} - {Error}", fileName, response.StatusCode, errorContent);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
var doc = JsonDocument.Parse(json);
|
var doc = JsonDocument.Parse(json);
|
||||||
if (doc.RootElement.TryGetProperty("images", out var images) && images.GetArrayLength() > 0)
|
if (doc.RootElement.TryGetProperty("images", out var images) && images.GetArrayLength() > 0)
|
||||||
{
|
{
|
||||||
return GetString(images[0], "url");
|
var url = GetString(images[0], "url");
|
||||||
|
_logger.LogInformation("Bild {FileName} hochgeladen: {Url}", fileName, url);
|
||||||
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogWarning("Keine URL im Upload-Response für {FileName} gefunden", fileName);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,9 +238,3 @@ public class GhostApiService
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GhostImageReference
|
|
||||||
{
|
|
||||||
[JsonPropertyName("url")] public string Url { get; set; } = "";
|
|
||||||
[JsonPropertyName("ref")] public string? Ref { get; set; }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using GhostImageManager.Data;
|
||||||
using GhostImageManager.Models;
|
using GhostImageManager.Models;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
|
||||||
|
|
@ -7,25 +8,37 @@ namespace GhostImageManager.Services;
|
||||||
public class ImageAnalyzerService
|
public class ImageAnalyzerService
|
||||||
{
|
{
|
||||||
private readonly GhostApiService _ghostApiService;
|
private readonly GhostApiService _ghostApiService;
|
||||||
|
private readonly ExifService _exifService;
|
||||||
|
private readonly ImageDatabaseService _dbService;
|
||||||
private readonly ILogger<ImageAnalyzerService> _logger;
|
private readonly ILogger<ImageAnalyzerService> _logger;
|
||||||
|
|
||||||
public ImageAnalyzerService(GhostApiService ghostApiService, ILogger<ImageAnalyzerService> logger)
|
public ImageAnalyzerService(
|
||||||
|
GhostApiService ghostApiService,
|
||||||
|
ExifService exifService,
|
||||||
|
ImageDatabaseService dbService,
|
||||||
|
ILogger<ImageAnalyzerService> logger)
|
||||||
{
|
{
|
||||||
_ghostApiService = ghostApiService;
|
_ghostApiService = ghostApiService;
|
||||||
|
_exifService = exifService;
|
||||||
|
_dbService = dbService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<GhostImage>> AnalyzeImagesAsync(GhostInstance instance, string searchTerm = "")
|
public async Task<List<GhostImage>> AnalyzeImagesAsync(GhostInstance instance, string searchTerm = "")
|
||||||
{
|
{
|
||||||
var images = new List<GhostImage>();
|
var images = new List<GhostImage>();
|
||||||
|
_logger.LogInformation("Starte Bildanalyse für Instanz {InstanceName} ({Url}), Ordner: {Folder}", instance.Name, instance.Url, instance.ImageFolderPath);
|
||||||
|
|
||||||
if (!Directory.Exists(instance.ImageFolderPath))
|
if (!Directory.Exists(instance.ImageFolderPath))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Image folder not found: {Path}", instance.ImageFolderPath);
|
_logger.LogError("Image folder not found: {Path}", instance.ImageFolderPath);
|
||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Alle Posts abrufen
|
// 1. Alle Posts abrufen
|
||||||
|
_logger.LogInformation("Rufe Posts mit Filter '{Filter}' ab", searchTerm);
|
||||||
var posts = await _ghostApiService.GetPostsAsync(instance, searchTerm);
|
var posts = await _ghostApiService.GetPostsAsync(instance, searchTerm);
|
||||||
|
_logger.LogInformation("{Count} Posts geladen, extrahiere Bild-Referenzen", posts.Count);
|
||||||
|
|
||||||
// 2. Bild-Referenzen aus Posts sammeln
|
// 2. Bild-Referenzen aus Posts sammeln
|
||||||
var imageReferences = new Dictionary<string, List<PostReference>>(StringComparer.OrdinalIgnoreCase);
|
var imageReferences = new Dictionary<string, List<PostReference>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
@ -79,13 +92,15 @@ public class ImageAnalyzerService
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Lokale Dateien scannen
|
// 3. Lokale Dateien scannen
|
||||||
|
_logger.LogInformation("Scanne lokalen Image-Ordner: {Folder}", instance.ImageFolderPath);
|
||||||
var localFiles = ScanLocalImageFiles(instance.ImageFolderPath);
|
var localFiles = ScanLocalImageFiles(instance.ImageFolderPath);
|
||||||
|
_logger.LogInformation("{Count} lokale Bilddateien gefunden", localFiles.Count);
|
||||||
|
|
||||||
// 4. GhostImage-Objekte erstellen
|
// 4. GhostImage-Objekte erstellen und mit DB synchronisieren
|
||||||
foreach (var file in localFiles)
|
foreach (var file in localFiles)
|
||||||
{
|
{
|
||||||
var absoluteUrl = ConvertLocalPathToUrl(instance, file.FullName);
|
var absoluteUrl = ConvertLocalPathToUrl(instance, file.FullName);
|
||||||
var relativePath = GetRelativePath(instance.ImageFolderPath, file.FullName).Replace("\\", "/");
|
var relativePath = GetRelativePath(instance.ImageFolderPath, file.FullName);
|
||||||
|
|
||||||
var image = new GhostImage
|
var image = new GhostImage
|
||||||
{
|
{
|
||||||
|
|
@ -102,6 +117,18 @@ public class ImageAnalyzerService
|
||||||
// Dimensionen ermitteln
|
// Dimensionen ermitteln
|
||||||
GetImageDimensions(image);
|
GetImageDimensions(image);
|
||||||
|
|
||||||
|
// EXIF-Daten extrahieren
|
||||||
|
ExifData exifData = null!;
|
||||||
|
if (!file.Extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
exifData = _exifService.ExtractExifData(file.FullName);
|
||||||
|
ApplyExifData(image, exifData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mit Datenbank synchronisieren
|
||||||
|
var managedImage = await _dbService.SyncImageAsync(image, instance.Id, exifData);
|
||||||
|
ApplyDatabaseData(image, managedImage);
|
||||||
|
|
||||||
images.Add(image);
|
images.Add(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,21 +138,53 @@ public class ImageAnalyzerService
|
||||||
var localPath = ConvertUrlToLocalPath(instance, url);
|
var localPath = ConvertUrlToLocalPath(instance, url);
|
||||||
if (!File.Exists(localPath) && !images.Any(i => i.Url.Equals(url, StringComparison.OrdinalIgnoreCase)))
|
if (!File.Exists(localPath) && !images.Any(i => i.Url.Equals(url, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
images.Add(new GhostImage
|
var externalImage = new GhostImage
|
||||||
{
|
{
|
||||||
Url = url,
|
Url = url,
|
||||||
FileName = Path.GetFileName(url),
|
FileName = Path.GetFileName(url),
|
||||||
RelativePath = GetRelativePath(instance.ImageFolderPath, localPath).Replace("\\", "/"),
|
RelativePath = GetRelativePath(instance.ImageFolderPath, localPath),
|
||||||
FilePath = localPath,
|
FilePath = localPath,
|
||||||
UsedInPosts = imageReferences[url],
|
UsedInPosts = imageReferences[url],
|
||||||
IsOrphaned = false
|
IsOrphaned = false
|
||||||
});
|
};
|
||||||
|
|
||||||
|
var managedImage = await _dbService.SyncImageAsync(externalImage, instance.Id, null);
|
||||||
|
ApplyDatabaseData(externalImage, managedImage);
|
||||||
|
|
||||||
|
images.Add(externalImage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var orphanedCount = images.Count(i => i.IsOrphaned);
|
||||||
|
_logger.LogInformation("Bildanalyse abgeschlossen: {Total} Bilder, {Orphaned} verwaist, {Used} verwendet", images.Count, orphanedCount, images.Count - orphanedCount);
|
||||||
|
|
||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyExifData(GhostImage image, ExifData exifData)
|
||||||
|
{
|
||||||
|
image.CameraMake = exifData.CameraMake;
|
||||||
|
image.CameraModel = exifData.CameraModel;
|
||||||
|
image.Lens = exifData.Lens;
|
||||||
|
image.Aperture = exifData.Aperture;
|
||||||
|
image.ShutterSpeed = exifData.ShutterSpeed;
|
||||||
|
image.Iso = exifData.Iso;
|
||||||
|
image.FocalLength = exifData.FocalLength;
|
||||||
|
image.CaptureDate = exifData.CaptureDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyDatabaseData(GhostImage image, ManagedImage managedImage)
|
||||||
|
{
|
||||||
|
image.ManagedImageId = managedImage.Id;
|
||||||
|
image.AuthorId = managedImage.AuthorId;
|
||||||
|
image.AuthorName = managedImage.AuthorName;
|
||||||
|
image.Theme = managedImage.Theme;
|
||||||
|
|
||||||
|
// CaptureDate aus DB hat Priorität, falls manuell überschrieben
|
||||||
|
if (managedImage.CaptureDate.HasValue)
|
||||||
|
image.CaptureDate = managedImage.CaptureDate.Value;
|
||||||
|
}
|
||||||
|
|
||||||
private List<FileInfo> ScanLocalImageFiles(string folderPath)
|
private List<FileInfo> ScanLocalImageFiles(string folderPath)
|
||||||
{
|
{
|
||||||
var files = new List<FileInfo>();
|
var files = new List<FileInfo>();
|
||||||
|
|
@ -134,13 +193,16 @@ public class ImageAnalyzerService
|
||||||
if (!Directory.Exists(folderPath))
|
if (!Directory.Exists(folderPath))
|
||||||
return files;
|
return files;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
foreach (var ext in extensions)
|
foreach (var ext in extensions)
|
||||||
{
|
{
|
||||||
files.AddRange(new DirectoryInfo(folderPath)
|
files.AddRange(new DirectoryInfo(folderPath)
|
||||||
.EnumerateFiles($"*{ext}", SearchOption.AllDirectories));
|
.EnumerateFiles($"*{ext}", SearchOption.AllDirectories));
|
||||||
}
|
}
|
||||||
|
|
||||||
return files.OrderBy(f => f.FullName).ToList();
|
var excludedDirs = Path.Combine(folderPath, "size");
|
||||||
|
return files.Where(f => f.DirectoryName.StartsWith(excludedDirs) == false).OrderBy(f => f.FullName).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<string> ExtractImageUrls(string html)
|
private List<string> ExtractImageUrls(string html)
|
||||||
|
|
@ -164,7 +226,7 @@ public class ImageAnalyzerService
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Fehler beim Parsen von HTML");
|
_logger.LogError(ex, "Fehler beim Parsen von HTML nach Bild-URLs");
|
||||||
}
|
}
|
||||||
|
|
||||||
return urls.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
return urls.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
@ -172,7 +234,7 @@ public class ImageAnalyzerService
|
||||||
|
|
||||||
private string ConvertLocalPathToUrl(GhostInstance instance, string localPath)
|
private string ConvertLocalPathToUrl(GhostInstance instance, string localPath)
|
||||||
{
|
{
|
||||||
var relativePath = GetRelativePath(instance.ImageFolderPath, localPath).Replace("\\", "/");
|
var relativePath = GetRelativePath(instance.ImageFolderPath, localPath);
|
||||||
return $"{instance.Url.TrimEnd('/')}/content/images/{relativePath.TrimStart('/')}";
|
return $"{instance.Url.TrimEnd('/')}/content/images/{relativePath.TrimStart('/')}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,14 +244,17 @@ public class ImageAnalyzerService
|
||||||
if (url.StartsWith(contentImagesUrl, StringComparison.OrdinalIgnoreCase))
|
if (url.StartsWith(contentImagesUrl, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var relativePath = url[contentImagesUrl.Length..];
|
var relativePath = url[contentImagesUrl.Length..];
|
||||||
return Path.Combine(instance.ImageFolderPath, relativePath.Replace("/", "\\"));
|
return Path.Combine(instance.ImageFolderPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||||
}
|
}
|
||||||
return Path.Combine(instance.ImageFolderPath, Path.GetFileName(url));
|
return Path.Combine(instance.ImageFolderPath, Path.GetFileName(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetRelativePath(string basePath, string targetPath)
|
private string GetRelativePath(string basePath, string targetPath)
|
||||||
{
|
{
|
||||||
var baseUri = new Uri(basePath.EndsWith("\\") || basePath.EndsWith("/") ? basePath : basePath + "\\");
|
if (!basePath.EndsWith(Path.DirectorySeparatorChar) && !basePath.EndsWith(Path.AltDirectorySeparatorChar))
|
||||||
|
basePath += Path.DirectorySeparatorChar;
|
||||||
|
|
||||||
|
var baseUri = new Uri(basePath);
|
||||||
var targetUri = new Uri(targetPath);
|
var targetUri = new Uri(targetPath);
|
||||||
var relativeUri = baseUri.MakeRelativeUri(targetUri);
|
var relativeUri = baseUri.MakeRelativeUri(targetUri);
|
||||||
return Uri.UnescapeDataString(relativeUri.ToString());
|
return Uri.UnescapeDataString(relativeUri.ToString());
|
||||||
|
|
@ -209,7 +274,7 @@ public class ImageAnalyzerService
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogDebug(ex, "Konnte Bilddimensionen nicht ermitteln für {File}", image.FilePath);
|
_logger.LogWarning(ex, "Konnte Bilddimensionen nicht ermitteln für {File}", image.FilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
166
GhostImageManager/Services/ImageDatabaseService.cs
Normal file
166
GhostImageManager/Services/ImageDatabaseService.cs
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
using GhostImageManager.Data;
|
||||||
|
using GhostImageManager.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace GhostImageManager.Services;
|
||||||
|
|
||||||
|
public class ImageDatabaseService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _dbContext;
|
||||||
|
private readonly ILogger<ImageDatabaseService> _logger;
|
||||||
|
|
||||||
|
public ImageDatabaseService(AppDbContext dbContext, ILogger<ImageDatabaseService> logger)
|
||||||
|
{
|
||||||
|
_dbContext = dbContext;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ManagedImage?> GetByUrlAsync(string ghostImageUrl, string instanceId)
|
||||||
|
{
|
||||||
|
return await _dbContext.ManagedImages
|
||||||
|
.FirstOrDefaultAsync(m => m.GhostImageUrl == ghostImageUrl && m.GhostInstanceId == instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ManagedImage>> GetByInstanceAsync(string instanceId)
|
||||||
|
{
|
||||||
|
return await _dbContext.ManagedImages
|
||||||
|
.Where(m => m.GhostInstanceId == instanceId)
|
||||||
|
.OrderBy(m => m.FileName)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ManagedImage> SyncImageAsync(GhostImage ghostImage, string instanceId, ExifData? exifData = null)
|
||||||
|
{
|
||||||
|
var existing = await GetByUrlAsync(ghostImage.Url, instanceId);
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
existing = new ManagedImage
|
||||||
|
{
|
||||||
|
GhostImageUrl = ghostImage.Url,
|
||||||
|
RelativePath = ghostImage.RelativePath,
|
||||||
|
FileName = ghostImage.FileName,
|
||||||
|
GhostInstanceId = instanceId,
|
||||||
|
FirstSeen = now,
|
||||||
|
IsOrphaned = ghostImage.IsOrphaned
|
||||||
|
};
|
||||||
|
_dbContext.ManagedImages.Add(existing);
|
||||||
|
_logger.LogInformation("Neues ManagedImage angelegt: {FileName}", ghostImage.FileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Felder aktualisieren, die sich ändern können
|
||||||
|
existing.RelativePath = ghostImage.RelativePath;
|
||||||
|
existing.FileName = ghostImage.FileName;
|
||||||
|
existing.FileSize = ghostImage.FileSize;
|
||||||
|
existing.Width = ghostImage.Width;
|
||||||
|
existing.Height = ghostImage.Height;
|
||||||
|
existing.FileModifiedDate = ghostImage.LastModified;
|
||||||
|
existing.IsOrphaned = ghostImage.IsOrphaned;
|
||||||
|
existing.LastUpdated = now;
|
||||||
|
|
||||||
|
// EXIF-Daten nur überschreiben, wenn sie vorhanden sind
|
||||||
|
if (exifData != null)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(exifData.CameraMake))
|
||||||
|
existing.CameraMake = exifData.CameraMake;
|
||||||
|
if (!string.IsNullOrEmpty(exifData.CameraModel))
|
||||||
|
existing.CameraModel = exifData.CameraModel;
|
||||||
|
if (!string.IsNullOrEmpty(exifData.Lens))
|
||||||
|
existing.Lens = exifData.Lens;
|
||||||
|
if (!string.IsNullOrEmpty(exifData.Aperture))
|
||||||
|
existing.Aperture = exifData.Aperture;
|
||||||
|
if (!string.IsNullOrEmpty(exifData.ShutterSpeed))
|
||||||
|
existing.ShutterSpeed = exifData.ShutterSpeed;
|
||||||
|
if (!string.IsNullOrEmpty(exifData.Iso))
|
||||||
|
existing.Iso = exifData.Iso;
|
||||||
|
if (!string.IsNullOrEmpty(exifData.FocalLength))
|
||||||
|
existing.FocalLength = exifData.FocalLength;
|
||||||
|
if (exifData.CaptureDate.HasValue)
|
||||||
|
existing.CaptureDate = exifData.CaptureDate.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAuthorAsync(int id, string? authorId, string? authorName)
|
||||||
|
{
|
||||||
|
var image = await _dbContext.ManagedImages.FindAsync(id);
|
||||||
|
if (image == null) return;
|
||||||
|
|
||||||
|
image.AuthorId = authorId;
|
||||||
|
image.AuthorName = authorName;
|
||||||
|
image.LastUpdated = DateTime.UtcNow;
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
_logger.LogInformation("Autor für {FileName} aktualisiert: {AuthorName}", image.FileName, authorName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateThemeAsync(int id, string? theme)
|
||||||
|
{
|
||||||
|
var image = await _dbContext.ManagedImages.FindAsync(id);
|
||||||
|
if (image == null) return;
|
||||||
|
|
||||||
|
image.Theme = theme;
|
||||||
|
image.LastUpdated = DateTime.UtcNow;
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
_logger.LogInformation("Thema für {FileName} aktualisiert: {Theme}", image.FileName, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateCaptureDateAsync(int id, DateTime? captureDate)
|
||||||
|
{
|
||||||
|
var image = await _dbContext.ManagedImages.FindAsync(id);
|
||||||
|
if (image == null) return;
|
||||||
|
|
||||||
|
image.CaptureDate = captureDate;
|
||||||
|
image.LastUpdated = DateTime.UtcNow;
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
_logger.LogInformation("Aufnahmedatum für {FileName} aktualisiert: {CaptureDate}", image.FileName, captureDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(int id)
|
||||||
|
{
|
||||||
|
var image = await _dbContext.ManagedImages.FindAsync(id);
|
||||||
|
if (image == null) return;
|
||||||
|
|
||||||
|
_dbContext.ManagedImages.Remove(image);
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
_logger.LogInformation("ManagedImage gelöscht: {FileName}", image.FileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ManagedImage>> SearchAsync(string instanceId, string? searchTerm = null, string? theme = null, string? authorId = null)
|
||||||
|
{
|
||||||
|
var query = _dbContext.ManagedImages.Where(m => m.GhostInstanceId == instanceId);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||||
|
{
|
||||||
|
var term = searchTerm.ToLowerInvariant();
|
||||||
|
query = query.Where(m =>
|
||||||
|
(m.FileName != null && m.FileName.ToLower().Contains(term)) ||
|
||||||
|
(m.CameraModel != null && m.CameraModel.ToLower().Contains(term)) ||
|
||||||
|
(m.Theme != null && m.Theme.ToLower().Contains(term)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(theme))
|
||||||
|
{
|
||||||
|
query = query.Where(m => m.Theme == theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(authorId))
|
||||||
|
{
|
||||||
|
query = query.Where(m => m.AuthorId == authorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.OrderBy(m => m.FileName).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> GetThemesAsync(string instanceId)
|
||||||
|
{
|
||||||
|
return await _dbContext.ManagedImages
|
||||||
|
.Where(m => m.GhostInstanceId == instanceId && m.Theme != null && m.Theme != "")
|
||||||
|
.Select(m => m.Theme!)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(t => t)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ public class ImageDeletionService
|
||||||
|
|
||||||
public Task<bool> DeleteImageAsync(GhostImage image)
|
public Task<bool> DeleteImageAsync(GhostImage image)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Lösche Bild: {FileName} ({FilePath})", image.FileName, image.FilePath);
|
||||||
return Task.FromResult(DeleteFile(image.FilePath));
|
return Task.FromResult(DeleteFile(image.FilePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,16 @@ namespace GhostImageManager.Services;
|
||||||
|
|
||||||
public class ImageExportService
|
public class ImageExportService
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<ImageExportService> _logger;
|
||||||
|
|
||||||
|
public ImageExportService(ILogger<ImageExportService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] ExportToCsv(List<GhostImage> images)
|
public byte[] ExportToCsv(List<GhostImage> images)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Exportiere {Count} Bilder als CSV", images.Count);
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("Dateiname;Relativer Pfad;URL;Größe Bytes;Größe;Dimensionen;Letzte Änderung;Verwaist;Verwendet in Posts");
|
sb.AppendLine("Dateiname;Relativer Pfad;URL;Größe Bytes;Größe;Dimensionen;Letzte Änderung;Verwaist;Verwendet in Posts");
|
||||||
|
|
||||||
|
|
@ -22,6 +30,7 @@ public class ImageExportService
|
||||||
|
|
||||||
public byte[] ExportToTxt(List<GhostImage> images)
|
public byte[] ExportToTxt(List<GhostImage> images)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Exportiere {Count} Bilder als TXT", images.Count);
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("Ghost Image Export");
|
sb.AppendLine("Ghost Image Export");
|
||||||
sb.AppendLine($"Erstellt am: {DateTime.Now:yyyy-MM-dd HH:mm}");
|
sb.AppendLine($"Erstellt am: {DateTime.Now:yyyy-MM-dd HH:mm}");
|
||||||
|
|
@ -51,6 +60,7 @@ public class ImageExportService
|
||||||
|
|
||||||
public async Task<byte[]> ExportToZipAsync(List<GhostImage> images)
|
public async Task<byte[]> ExportToZipAsync(List<GhostImage> images)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Exportiere {Count} Bilder als ZIP", images.Count);
|
||||||
using var ms = new MemoryStream();
|
using var ms = new MemoryStream();
|
||||||
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, true))
|
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, true))
|
||||||
{
|
{
|
||||||
|
|
@ -59,7 +69,7 @@ public class ImageExportService
|
||||||
if (!File.Exists(image.FilePath))
|
if (!File.Exists(image.FilePath))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var entry = archive.CreateEntry(image.RelativePath.Replace("/", "\\"));
|
var entry = archive.CreateEntry(image.RelativePath.Replace("\\", "/"));
|
||||||
await using (var entryStream = entry.Open())
|
await using (var entryStream = entry.Open())
|
||||||
{
|
{
|
||||||
await using var fileStream = File.OpenRead(image.FilePath);
|
await using var fileStream = File.OpenRead(image.FilePath);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ public class ImageReplacementService
|
||||||
|
|
||||||
public async Task<bool> ReplaceImageAsync(GhostImage oldImage, Stream newImageStream, string newFileName)
|
public async Task<bool> ReplaceImageAsync(GhostImage oldImage, Stream newImageStream, string newFileName)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Ersetze Bild {OldFile} durch {NewFile}", oldImage.FileName, newFileName);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(oldImage.FilePath))
|
if (!File.Exists(oldImage.FilePath))
|
||||||
|
|
@ -24,6 +26,7 @@ public class ImageReplacementService
|
||||||
// Backup der alten Datei erstellen
|
// Backup der alten Datei erstellen
|
||||||
var backupPath = oldImage.FilePath + ".backup";
|
var backupPath = oldImage.FilePath + ".backup";
|
||||||
File.Copy(oldImage.FilePath, backupPath, overwrite: true);
|
File.Copy(oldImage.FilePath, backupPath, overwrite: true);
|
||||||
|
_logger.LogDebug("Backup erstellt: {BackupPath}", backupPath);
|
||||||
|
|
||||||
// Neue Datei schreiben
|
// Neue Datei schreiben
|
||||||
using (var fileStream = File.Create(oldImage.FilePath))
|
using (var fileStream = File.Create(oldImage.FilePath))
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
"id": "prod",
|
"id": "41512d16",
|
||||||
"name": "Produktion",
|
"name": "RBUBB",
|
||||||
"url": "https://www.brigitteundrolandunterwegs.com",
|
"url": "https://rbubb.rolandbrunner.me",
|
||||||
"adminApiKey": "HIER_ADMIN_API_KEY_EINGEBEN",
|
"adminApiKey": "6aa18e1e05dde700011bc18a:f32a3467ec3351ebe693be411f1cfc4be063ed85b467f157e67bdc750c1aa251",
|
||||||
"contentApiKey": "",
|
"contentApiKey": "467ee6c8bf369a09cc57446256",
|
||||||
"imageFolderPath": "C:\\Ghost\\content\\images"
|
"imageFolderPath": "E:\\Wp2Ghost\\images"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 720 KiB After Width: | Height: | Size: 78 KiB |
Loading…
Reference in a new issue