< Summary

Information
Class: LifeChart.Infrastructure.Backup.NextcloudBackupProvider
Assembly: LifeChart.Infrastructure
File(s): /home/runner/work/lifechart/lifechart/src/LifeChart.Infrastructure/Backup/NextcloudBackupProvider.cs
Tag: 113_29159449707
Line coverage
0%
Covered lines: 0
Uncovered lines: 80
Coverable lines: 80
Total lines: 141
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 20
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
BackupAsync()100%210%
ListBackupsAsync()0%620%
RestoreAsync()100%210%
GetConfig()0%4260%
EnsureFolderExistsAsync()100%210%
ParsePropfindResponse(...)0%156120%

File(s)

/home/runner/work/lifechart/lifechart/src/LifeChart.Infrastructure/Backup/NextcloudBackupProvider.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Net.Http.Headers;
 3using System.Text;
 4using System.Xml.Linq;
 5using LifeChart.Application.Interfaces;
 6using LifeChart.Application.Settings;
 7
 8namespace LifeChart.Infrastructure.Backup;
 9
 10public class NextcloudBackupProvider : IBackupProvider
 11{
 12    private readonly HttpClient _httpClient;
 13    private readonly ISettingsService _settings;
 14
 015    public NextcloudBackupProvider(HttpClient httpClient, ISettingsService settings)
 16    {
 017        _httpClient = httpClient;
 018        _settings = settings;
 019    }
 20
 21    public async Task BackupAsync(string dbPath)
 22    {
 023        var (webDavUrl, auth) = GetConfig();
 024        await EnsureFolderExistsAsync(webDavUrl, auth);
 25
 026        var fileName = $"lifechart_backup_{DateTime.Today:yyyy-MM-dd}.sqlite";
 027        var fileBytes = await File.ReadAllBytesAsync(dbPath);
 28
 029        var request = new HttpRequestMessage(HttpMethod.Put, $"{webDavUrl}{fileName}")
 030        {
 031            Headers = { Authorization = auth },
 032            Content = new ByteArrayContent(fileBytes)
 033        };
 034        request.Content.Headers.ContentType =
 035            new MediaTypeHeaderValue("application/octet-stream");
 36
 037        var response = await _httpClient.SendAsync(request);
 038        response.EnsureSuccessStatusCode();
 039    }
 40
 41    public async Task<IEnumerable<BackupInfo>> ListBackupsAsync()
 42    {
 043        var (webDavUrl, auth) = GetConfig();
 44
 045        var request = new HttpRequestMessage(new HttpMethod("PROPFIND"), webDavUrl)
 046        {
 047            Content = new StringContent(PropfindBody, Encoding.UTF8, "application/xml")
 048        };
 049        request.Headers.Authorization = auth;
 050        request.Headers.Add("Depth", "1");
 51
 052        var response = await _httpClient.SendAsync(request);
 053        if (!response.IsSuccessStatusCode) return [];
 54
 055        var xml = await response.Content.ReadAsStringAsync();
 056        return ParsePropfindResponse(xml);
 057    }
 58
 59    public async Task RestoreAsync(BackupInfo backup, string targetPath)
 60    {
 061        var (webDavUrl, auth) = GetConfig();
 62
 063        var request = new HttpRequestMessage(HttpMethod.Get, $"{webDavUrl}{backup.FileName}")
 064        {
 065            Headers = { Authorization = auth }
 066        };
 67
 068        var response = await _httpClient.SendAsync(request);
 069        response.EnsureSuccessStatusCode();
 70
 071        var bytes = await response.Content.ReadAsByteArrayAsync();
 072        await File.WriteAllBytesAsync(targetPath, bytes);
 073    }
 74
 75    private (string webDavUrl, AuthenticationHeaderValue auth) GetConfig()
 76    {
 077        var settings = _settings.Load();
 78
 079        if (string.IsNullOrEmpty(settings.NextcloudUrl) ||
 080            string.IsNullOrEmpty(settings.NextcloudUsername) ||
 081            string.IsNullOrEmpty(settings.NextcloudPassword))
 082            throw new InvalidOperationException("Nextcloud ist nicht konfiguriert.");
 83
 084        var baseUrl = settings.NextcloudUrl.TrimEnd('/');
 085        var webDavUrl =
 086            $"{baseUrl}/remote.php/dav/files/{settings.NextcloudUsername}/LifeChart/";
 87
 088        var credentials = Convert.ToBase64String(
 089            Encoding.UTF8.GetBytes(
 090                $"{settings.NextcloudUsername}:{settings.NextcloudPassword}"));
 091        var auth = new AuthenticationHeaderValue("Basic", credentials);
 92
 093        return (webDavUrl, auth);
 94    }
 95
 96    private async Task EnsureFolderExistsAsync(string webDavUrl, AuthenticationHeaderValue auth)
 97    {
 098        var request = new HttpRequestMessage(new HttpMethod("MKCOL"), webDavUrl)
 099        {
 0100            Headers = { Authorization = auth }
 0101        };
 102        // 201 Created or 405 Method Not Allowed (folder exists) — beide OK
 0103        await _httpClient.SendAsync(request);
 0104    }
 105
 106    private static IEnumerable<BackupInfo> ParsePropfindResponse(string xml)
 107    {
 0108        var doc = XDocument.Parse(xml);
 0109        XNamespace dav = "DAV:";
 110
 0111        return doc.Descendants(dav + "response")
 0112            .Where(r =>
 0113            {
 0114                var href = r.Element(dav + "href")?.Value ?? string.Empty;
 0115                return href.EndsWith(".sqlite", StringComparison.OrdinalIgnoreCase);
 0116            })
 0117            .Select(r =>
 0118            {
 0119                var href = r.Element(dav + "href")!.Value;
 0120                var fileName = Uri.UnescapeDataString(href.Split('/').Last());
 0121                var lastModStr = r.Descendants(dav + "getlastmodified")
 0122                    .FirstOrDefault()?.Value ?? string.Empty;
 0123                DateTime.TryParse(lastModStr, CultureInfo.CurrentCulture, out var lastMod);
 0124                var sizeStr = r.Descendants(dav + "getcontentlength")
 0125                    .FirstOrDefault()?.Value ?? "0";
 0126                long.TryParse(sizeStr, out var size);
 0127                return new BackupInfo(fileName, lastMod, size);
 0128            })
 0129            .OrderByDescending(b => b.CreatedAt);
 130    }
 131
 132    private const string PropfindBody = """
 133        <?xml version="1.0" encoding="utf-8"?>
 134        <d:propfind xmlns:d="DAV:">
 135          <d:prop>
 136            <d:getlastmodified/>
 137            <d:getcontentlength/>
 138          </d:prop>
 139        </d:propfind>
 140        """;
 141}