Merge pull request #6955 from duplicati/feature/add-server-side-filters
Add server-side filter eval
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// Copyright (C) 2026, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
namespace Duplicati.WebserverCore.Dto.V2;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for testing filesystem filters
|
||||
/// </summary>
|
||||
public class TestFilterRequestDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The paths to evaluate against the filters
|
||||
/// </summary>
|
||||
public string[]? Paths { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The source paths (roots) which are unconditionally included
|
||||
/// </summary>
|
||||
public string[]? Sources { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The filter strings to evaluate
|
||||
/// </summary>
|
||||
public string[]? Filters { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (C) 2026, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
namespace Duplicati.WebserverCore.Dto.V2;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for the result of testing a single filesystem path against filters
|
||||
/// </summary>
|
||||
public record TestFilterResponseItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The path that was evaluated
|
||||
/// </summary>
|
||||
public string Path { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Whether the path is included by the filters
|
||||
/// </summary>
|
||||
public bool Included { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The string representation of the filter that matched, if any
|
||||
/// </summary>
|
||||
public string? MatchedFilter { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for the response of testing filesystem filters
|
||||
/// </summary>
|
||||
public record TestFilterResponseDto : ResponseEnvelope<TestFilterResponseItem[]>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a success response with the test results
|
||||
/// </summary>
|
||||
/// <param name="data">The data to include</param>
|
||||
/// <returns>The response DTO</returns>
|
||||
public static TestFilterResponseDto Create(TestFilterResponseItem[] data)
|
||||
=> new TestFilterResponseDto()
|
||||
{
|
||||
Success = true,
|
||||
Error = null,
|
||||
StatusCode = "OK",
|
||||
Data = data
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates an error response
|
||||
/// </summary>
|
||||
/// <param name="error">The error message</param>
|
||||
/// <param name="statusCode">The status code</param>
|
||||
/// <returns>The response DTO</returns>
|
||||
public static TestFilterResponseDto CreateError(string error, string statusCode)
|
||||
=> new TestFilterResponseDto()
|
||||
{
|
||||
Success = false,
|
||||
Error = error,
|
||||
StatusCode = statusCode,
|
||||
Data = null
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (C) 2026, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.WebserverCore.Dto.V2;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Duplicati.Library.Interface;
|
||||
|
||||
namespace Duplicati.WebserverCore.Endpoints.V2;
|
||||
|
||||
public class TestFilters : IEndpointV2
|
||||
{
|
||||
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<TestFilters>();
|
||||
|
||||
public static void Map(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapPost("/filesystem/test-filter", ([FromBody] TestFilterRequestDto request) => Execute(request))
|
||||
.RequireAuthorization();
|
||||
}
|
||||
|
||||
private static TestFilterResponseDto Execute(TestFilterRequestDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
return TestFilterResponseDto.Create(Evaluate(request).ToArray());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Library.Logging.Log.WriteErrorMessage(LOGTAG, "TestFilters", ex, "An error occurred while testing filters");
|
||||
return ex is UserInformationException uex
|
||||
? TestFilterResponseDto.CreateError(uex.Message, uex.HelpID)
|
||||
: TestFilterResponseDto.CreateError(ex.Message, "InternalError");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<TestFilterResponseItem> Evaluate(TestFilterRequestDto request)
|
||||
{
|
||||
var filter = FilterExpression.Deserialize(request.Filters ?? Array.Empty<string>());
|
||||
var enumeratefilter = filter;
|
||||
|
||||
FilterExpression.AnalyzeFilters(filter, out var includes, out var excludes);
|
||||
if (includes && !excludes)
|
||||
enumeratefilter = FilterExpression.Combine(filter, new FilterExpression("*" + System.IO.Path.DirectorySeparatorChar, true));
|
||||
|
||||
var sourceArray = request.Sources ?? Array.Empty<string>();
|
||||
var sources = new HashSet<string>(sourceArray, Utility.ClientFilenameStringComparer);
|
||||
|
||||
foreach (var path in request.Paths ?? Array.Empty<string>())
|
||||
{
|
||||
if (path == null) continue;
|
||||
|
||||
// If the path is exactly one of the sources, it's always included.
|
||||
if (sources.Contains(path))
|
||||
{
|
||||
yield return new TestFilterResponseItem
|
||||
{
|
||||
Path = path,
|
||||
Included = true,
|
||||
MatchedFilter = null
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the path is under any of the source folders
|
||||
var isUnderSource = sourceArray
|
||||
.Any(source => string.Equals(Util.AppendDirSeparator(path), Util.AppendDirSeparator(source), Utility.ClientFilenameStringComparison) ||
|
||||
Utility.IsPathBelowFolder(path, source));
|
||||
|
||||
// Do not return paths that are not part of the source selection
|
||||
if (!isUnderSource)
|
||||
continue;
|
||||
|
||||
var isIncluded = FilterExpression.Matches(enumeratefilter!, path, out var match);
|
||||
yield return new TestFilterResponseItem
|
||||
{
|
||||
Path = path,
|
||||
Included = isIncluded,
|
||||
MatchedFilter = match?.ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ public class SystemInfoProvider(IApplicationSettings applicationSettings, Connec
|
||||
"v2:system:temp-disk-space",
|
||||
"v1:subscribe:remotecontrol",
|
||||
"v1:ipc:controller",
|
||||
"v2:filesystem:test-filter"
|
||||
|
||||
// "v1:subscribe:scheduler",
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user