Core: Refactor file name templateing

This commit is contained in:
genteure 2022-06-28 16:10:06 +08:00
parent bbada97219
commit 0b8cf27ce9
14 changed files with 235 additions and 129 deletions

View File

@ -18,7 +18,7 @@ namespace BililiveRecorder.Core.Config.V3
public bool DisableConfigSave { get; set; } = false; // for CLI public bool DisableConfigSave { get; set; } = false; // for CLI
} }
public partial class RoomConfig public partial class RoomConfig : IFileNameConfig
{ {
public RoomConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name })) public RoomConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name }))
{ } { }
@ -28,7 +28,7 @@ namespace BililiveRecorder.Core.Config.V3
public string? WorkDirectory => this.GetPropertyValue<string>(); public string? WorkDirectory => this.GetPropertyValue<string>();
} }
public partial class GlobalConfig public partial class GlobalConfig : IFileNameConfig
{ {
public GlobalConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name })) public GlobalConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name }))
{ {

View File

@ -0,0 +1,9 @@
namespace BililiveRecorder.Core.Config.V3
{
public interface IFileNameConfig
{
public string? FileNameRecordTemplate { get; }
public string? WorkDirectory { get; }
}
}

View File

@ -6,7 +6,6 @@ using BililiveRecorder.Core.Config.V3;
using BililiveRecorder.Core.Danmaku; using BililiveRecorder.Core.Danmaku;
using BililiveRecorder.Core.Recording; using BililiveRecorder.Core.Recording;
using BililiveRecorder.Core.Scripting; using BililiveRecorder.Core.Scripting;
using BililiveRecorder.Core.Templating;
using BililiveRecorder.Flv; using BililiveRecorder.Flv;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Polly.Registry; using Polly.Registry;
@ -46,7 +45,6 @@ namespace BililiveRecorder.DependencyInjection
; ;
public static IServiceCollection AddRecorderRecording(this IServiceCollection services) => services public static IServiceCollection AddRecorderRecording(this IServiceCollection services) => services
.AddSingleton<FileNameGenerator>()
.AddScoped<IRecordTaskFactory, RecordTaskFactory>() .AddScoped<IRecordTaskFactory, RecordTaskFactory>()
.AddScoped<IFlvProcessingContextWriterFactory, FlvProcessingContextWriterWithFileWriterFactory>() .AddScoped<IFlvProcessingContextWriterFactory, FlvProcessingContextWriterWithFileWriterFactory>()
.AddScoped<IFlvTagReaderFactory, FlvTagReaderFactory>() .AddScoped<IFlvTagReaderFactory, FlvTagReaderFactory>()

View File

@ -5,7 +5,6 @@ using System.Threading.Tasks;
using BililiveRecorder.Core.Api; using BililiveRecorder.Core.Api;
using BililiveRecorder.Core.Event; using BililiveRecorder.Core.Event;
using BililiveRecorder.Core.Scripting; using BililiveRecorder.Core.Scripting;
using BililiveRecorder.Core.Templating;
using Serilog; using Serilog;
namespace BililiveRecorder.Core.Recording namespace BililiveRecorder.Core.Recording
@ -17,12 +16,10 @@ namespace BililiveRecorder.Core.Recording
public RawDataRecordTask(IRoom room, public RawDataRecordTask(IRoom room,
ILogger logger, ILogger logger,
IApiClient apiClient, IApiClient apiClient,
FileNameGenerator fileNameGenerator,
UserScriptRunner userScriptRunner) UserScriptRunner userScriptRunner)
: base(room: room, : base(room: room,
logger: logger?.ForContext<RawDataRecordTask>().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!, logger: logger?.ForContext<RawDataRecordTask>().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!,
apiClient: apiClient, apiClient: apiClient,
fileNameGenerator: fileNameGenerator,
userScriptRunner: userScriptRunner) userScriptRunner: userScriptRunner)
{ {
} }

View File

@ -52,13 +52,14 @@ namespace BililiveRecorder.Core.Recording
private DateTimeOffset ioStatsLastTrigger; private DateTimeOffset ioStatsLastTrigger;
private TimeSpan durationSinceNoDataReceived; private TimeSpan durationSinceNoDataReceived;
protected RecordTaskBase(IRoom room, ILogger logger, IApiClient apiClient, FileNameGenerator fileNameGenerator, UserScriptRunner userScriptRunner) protected RecordTaskBase(IRoom room, ILogger logger, IApiClient apiClient, UserScriptRunner userScriptRunner)
{ {
this.room = room ?? throw new ArgumentNullException(nameof(room)); this.room = room ?? throw new ArgumentNullException(nameof(room));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.fileNameGenerator = fileNameGenerator ?? throw new ArgumentNullException(nameof(fileNameGenerator));
this.userScriptRunner = userScriptRunner ?? throw new ArgumentNullException(nameof(userScriptRunner)); this.userScriptRunner = userScriptRunner ?? throw new ArgumentNullException(nameof(userScriptRunner));
this.fileNameGenerator = new FileNameGenerator(room.RoomConfig, logger);
this.ct = this.cts.Token; this.ct = this.cts.Token;
this.timer.Elapsed += this.Timer_Elapsed_TriggerIOStats; this.timer.Elapsed += this.Timer_Elapsed_TriggerIOStats;
@ -174,17 +175,22 @@ namespace BililiveRecorder.Core.Recording
} }
} }
protected (string fullPath, string relativePath) CreateFileName() => this.fileNameGenerator.CreateFilePath(new FileNameTemplateContext protected (string fullPath, string relativePath) CreateFileName()
{ {
Name = FileNameGenerator.RemoveInvalidFileName(this.room.Name, ignore_slash: false), var output = this.fileNameGenerator.CreateFilePath(new FileNameTemplateContext
Title = FileNameGenerator.RemoveInvalidFileName(this.room.Title, ignore_slash: false), {
RoomId = this.room.RoomConfig.RoomId, Name = FileNameGenerator.RemoveInvalidFileName(this.room.Name, ignore_slash: false),
ShortId = this.room.ShortId, Title = FileNameGenerator.RemoveInvalidFileName(this.room.Title, ignore_slash: false),
AreaParent = FileNameGenerator.RemoveInvalidFileName(this.room.AreaNameParent, ignore_slash: false), RoomId = this.room.RoomConfig.RoomId,
AreaChild = FileNameGenerator.RemoveInvalidFileName(this.room.AreaNameChild, ignore_slash: false), ShortId = this.room.ShortId,
Qn = this.qn, AreaParent = FileNameGenerator.RemoveInvalidFileName(this.room.AreaNameParent, ignore_slash: false),
Json = this.room.RawBilibiliApiJsonData, AreaChild = FileNameGenerator.RemoveInvalidFileName(this.room.AreaNameChild, ignore_slash: false),
}); Qn = this.qn,
Json = this.room.RawBilibiliApiJsonData,
});
return (output.FullPath!, output.RelativePath);
}
#region Api Requests #region Api Requests

View File

@ -10,7 +10,6 @@ using BililiveRecorder.Core.Config;
using BililiveRecorder.Core.Event; using BililiveRecorder.Core.Event;
using BililiveRecorder.Core.ProcessingRules; using BililiveRecorder.Core.ProcessingRules;
using BililiveRecorder.Core.Scripting; using BililiveRecorder.Core.Scripting;
using BililiveRecorder.Core.Templating;
using BililiveRecorder.Flv; using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Amf; using BililiveRecorder.Flv.Amf;
using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.Flv.Pipeline;
@ -44,12 +43,10 @@ namespace BililiveRecorder.Core.Recording
IFlvTagReaderFactory flvTagReaderFactory, IFlvTagReaderFactory flvTagReaderFactory,
ITagGroupReaderFactory tagGroupReaderFactory, ITagGroupReaderFactory tagGroupReaderFactory,
IFlvProcessingContextWriterFactory writerFactory, IFlvProcessingContextWriterFactory writerFactory,
FileNameGenerator fileNameGenerator,
UserScriptRunner userScriptRunner) UserScriptRunner userScriptRunner)
: base(room: room, : base(room: room,
logger: logger?.ForContext<StandardRecordTask>().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!, logger: logger?.ForContext<StandardRecordTask>().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!,
apiClient: apiClient, apiClient: apiClient,
fileNameGenerator: fileNameGenerator,
userScriptRunner: userScriptRunner) userScriptRunner: userScriptRunner)
{ {
this.flvTagReaderFactory = flvTagReaderFactory ?? throw new ArgumentNullException(nameof(flvTagReaderFactory)); this.flvTagReaderFactory = flvTagReaderFactory ?? throw new ArgumentNullException(nameof(flvTagReaderFactory));

View File

@ -7,6 +7,7 @@ using Fluid.Ast;
using Fluid.Values; using Fluid.Values;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Serilog; using Serilog;
using Serilog.Core;
namespace BililiveRecorder.Core.Templating namespace BililiveRecorder.Core.Templating
{ {
@ -15,8 +16,6 @@ namespace BililiveRecorder.Core.Templating
// TODO: 需要改得更通用一些 // TODO: 需要改得更通用一些
// 日志不应该一定绑定到一个直播间上 // 日志不应该一定绑定到一个直播间上
private static readonly ILogger logger = Log.Logger.ForContext<FileNameGenerator>();
private static readonly FluidParser parser; private static readonly FluidParser parser;
private static readonly IFluidTemplate defaultTemplate; private static readonly IFluidTemplate defaultTemplate;
@ -39,8 +38,8 @@ namespace BililiveRecorder.Core.Templating
} }
} }
private readonly GlobalConfig config; private readonly IFileNameConfig config;
private IFluidTemplate? template; private readonly ILogger logger;
static FileNameGenerator() static FileNameGenerator()
{ {
@ -72,32 +71,22 @@ namespace BililiveRecorder.Core.Templating
defaultTemplate = parser.Parse(DefaultConfig.Instance.FileNameRecordTemplate); defaultTemplate = parser.Parse(DefaultConfig.Instance.FileNameRecordTemplate);
} }
public FileNameGenerator(GlobalConfig config) public FileNameGenerator(IFileNameConfig config, ILogger? logger)
{ {
this.config = config ?? throw new ArgumentNullException(nameof(config)); this.config = config ?? throw new ArgumentNullException(nameof(config));
this.logger = logger?.ForContext<FileNameGenerator>() ?? Logger.None;
config.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(config.FileNameRecordTemplate))
{
this.UpdateTemplate();
}
};
this.UpdateTemplate();
} }
private void UpdateTemplate() public FileNameTemplateOutput CreateFilePath(FileNameTemplateContext data)
{ {
if (!parser.TryParse(this.config.FileNameRecordTemplate, out var template, out var error)) var status = FileNameTemplateStatus.Success;
{ string? errorMessage = null;
logger.Warning("文件名模板格式不正确,请修改: {ParserError}", error); string relativePath;
} string? fullPath;
this.template = template;
} var workDirectory = this.config.WorkDirectory;
var skipFullPath = workDirectory is null;
public (string fullPath, string relativePath) CreateFilePath(FileNameTemplateContext data)
{
var now = DateTimeOffset.Now; var now = DateTimeOffset.Now;
var templateOptions = new TemplateOptions var templateOptions = new TemplateOptions
{ {
@ -105,49 +94,57 @@ namespace BililiveRecorder.Core.Templating
}; };
templateOptions.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.CamelCase; templateOptions.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.CamelCase;
templateOptions.ValueConverters.Add(o => o is JContainer j ? new JContainerValue(j) : null); templateOptions.ValueConverters.Add(o => o is JContainer j ? new JContainerValue(j) : null);
templateOptions.Filters.AddFilter("format_qn", static (FluidValue input, FilterArguments arguments, TemplateContext context) templateOptions.Filters.AddFilter("format_qn",
=> new StringValue(StreamQualityNumber.MapToString((int)input.ToNumberValue())) static (FluidValue input, FilterArguments arguments, TemplateContext context) => new StringValue(StreamQualityNumber.MapToString((int)input.ToNumberValue())));
);
var context = new TemplateContext(data, templateOptions); var context = new TemplateContext(data, templateOptions);
var workDirectory = this.config.WorkDirectory!; if (!parser.TryParse(this.config.FileNameRecordTemplate, out var template, out var error))
if (this.template is not { } t)
{ {
logger.ForContext(LoggingContext.RoomId, data.RoomId).Warning("文件名模板格式不正确,请检查设置。将写入到默认路径。"); this.logger.Warning("文件名模板格式不正确,请修改: {ParserError}", error);
errorMessage = "文件名模板格式不正确,请修改: " + error;
status = FileNameTemplateStatus.TemplateError;
goto returnDefaultPath; goto returnDefaultPath;
} }
var relativePath = t.Render(context); relativePath = template.Render(context);
relativePath = RemoveInvalidFileName(relativePath); relativePath = RemoveInvalidFileName(relativePath);
var fullPath = Path.GetFullPath(Path.Combine(workDirectory, relativePath));
if (!CheckIsWithinPath(workDirectory!, Path.GetDirectoryName(fullPath))) fullPath = skipFullPath ? null : Path.GetFullPath(Path.Combine(workDirectory, relativePath));
if (!skipFullPath && !CheckIsWithinPath(workDirectory!, Path.GetDirectoryName(fullPath)))
{ {
logger.ForContext(LoggingContext.RoomId, data.RoomId).Warning("录制文件位置超出允许范围,请检查设置。将写入到默认路径。"); this.logger.Warning("录制文件位置超出允许范围,请检查设置。将写入到默认路径。");
status = FileNameTemplateStatus.OutOfRange;
errorMessage = "录制文件位置超出允许范围";
goto returnDefaultPath; goto returnDefaultPath;
} }
var ext = Path.GetExtension(fullPath); var ext = Path.GetExtension(relativePath);
if (!ext.Equals(".flv", StringComparison.OrdinalIgnoreCase)) if (!ext.Equals(".flv", StringComparison.OrdinalIgnoreCase))
{ {
logger.ForContext(LoggingContext.RoomId, data.RoomId).Warning("录播姬只支持 FLV 文件格式,将在录制文件后缀名 {ExtensionName} 后添加 .flv。", ext); this.logger.Warning("录播姬只支持 FLV 文件格式,将在录制文件后缀名 {ExtensionName} 后添加 {DotFlv}。", ext, ".flv");
relativePath += ".flv"; relativePath += ".flv";
fullPath += ".flv";
if (!skipFullPath)
fullPath += ".flv";
} }
if (File.Exists(fullPath)) if (!skipFullPath && File.Exists(fullPath))
{ {
logger.ForContext(LoggingContext.RoomId, data.RoomId).Warning("录制文件名冲突,请检查设置。将写入到默认路径。"); this.logger.Warning("录制文件名冲突,将写入到默认路径。");
status = FileNameTemplateStatus.FileConflict;
errorMessage = "录制文件名冲突";
goto returnDefaultPath; goto returnDefaultPath;
} }
return (fullPath, relativePath); return new FileNameTemplateOutput(status, errorMessage, relativePath, fullPath);
returnDefaultPath: returnDefaultPath:
var defaultRelativePath = RemoveInvalidFileName(defaultTemplate.Render(context)); var defaultRelativePath = RemoveInvalidFileName(defaultTemplate.Render(context));
return (Path.GetFullPath(Path.Combine(this.config.WorkDirectory, defaultRelativePath)), defaultRelativePath); var defaultFullPath = skipFullPath ? null : Path.GetFullPath(Path.Combine(workDirectory, defaultRelativePath));
return new FileNameTemplateOutput(status, errorMessage, defaultRelativePath, defaultFullPath);
} }
private class JContainerValue : ObjectValueBase private class JContainerValue : ObjectValueBase
@ -238,22 +235,29 @@ namespace BililiveRecorder.Core.Templating
return input; return input;
} }
private static readonly char[] separator = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
internal static bool CheckIsWithinPath(string parent, string child) internal static bool CheckIsWithinPath(string parent, string child)
{ {
if (parent is null || child is null) var fullParent = Path.GetFullPath(parent);
var fullChild = Path.GetFullPath(child);
var parentSegments = fullParent.Split(separator, StringSplitOptions.None).AsSpan();
if (parentSegments[parentSegments.Length - 1] == "")
{
parentSegments = parentSegments.Slice(0, parentSegments.Length - 1);
}
var childSegments = fullChild.Split(separator, StringSplitOptions.None).AsSpan();
if (childSegments[childSegments.Length - 1] == "")
{
childSegments = childSegments.Slice(0, childSegments.Length - 1);
}
if (parentSegments.Length >= childSegments.Length)
return false; return false;
parent = parent.Replace('/', '\\'); return childSegments.Slice(0, parentSegments.Length).SequenceEqual(parentSegments);
if (!parent.EndsWith("\\"))
parent += "\\";
parent = Path.GetFullPath(parent);
child = child.Replace('/', '\\');
if (!child.EndsWith("\\"))
child += "\\";
child = Path.GetFullPath(child);
return child.StartsWith(parent, StringComparison.Ordinal);
} }
} }
} }

View File

@ -0,0 +1,23 @@
using System;
namespace BililiveRecorder.Core.Templating
{
public readonly struct FileNameTemplateOutput
{
public FileNameTemplateOutput(FileNameTemplateStatus status, string? errorMessage, string relativePath, string? fullPath)
{
this.Status = status;
this.ErrorMessage = errorMessage;
this.RelativePath = relativePath ?? throw new ArgumentNullException(nameof(relativePath));
this.FullPath = fullPath;
}
public FileNameTemplateStatus Status { get; }
public string? ErrorMessage { get; }
public string RelativePath { get; }
public string? FullPath { get; }
}
}

View File

@ -0,0 +1,10 @@
namespace BililiveRecorder.Core.Templating
{
public enum FileNameTemplateStatus
{
Success = 0,
TemplateError,
OutOfRange,
FileConflict,
}
}

View File

@ -11,6 +11,7 @@
l:ResxLocalizationProvider.DefaultDictionary="Strings" l:ResxLocalizationProvider.DefaultDictionary="Strings"
xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls" xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls"
xmlns:m="clr-namespace:BililiveRecorder.WPF.Models" xmlns:m="clr-namespace:BililiveRecorder.WPF.Models"
xmlns:t="clr-namespace:BililiveRecorder.Core.Templating;assembly=BililiveRecorder.Core"
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages" xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
xmlns:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core" xmlns:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core"
xmlns:configv3="clr-namespace:BililiveRecorder.Core.Config.V3;assembly=BililiveRecorder.Core" xmlns:configv3="clr-namespace:BililiveRecorder.Core.Config.V3;assembly=BililiveRecorder.Core"
@ -96,7 +97,14 @@
<TextBox Text="{Binding FileNameRecordTemplate,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/> <TextBox Text="{Binding FileNameRecordTemplate,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
</c:SettingWithDefault> </c:SettingWithDefault>
<Button Margin="0,10,0,5" Content="测试" Click="TestFileNameTemplate_Button_Click"/> <Button Margin="0,10,0,5" Content="测试" Click="TestFileNameTemplate_Button_Click"/>
<TextBox Visibility="Collapsed" IsReadOnly="True" x:Name="FileNameTestResult"/> <Border x:Name="FileNameTestResultArea" d:DataContext="{d:DesignInstance Type=t:FileNameTemplateOutput}" Visibility="Collapsed"
BorderBrush="{DynamicResource SystemControlBackgroundBaseMediumBrush}" CornerRadius="5" BorderThickness="1" Padding="5">
<StackPanel Orientation="Vertical" Margin="0">
<TextBlock Text="测试结果:"/>
<TextBlock Text="{Binding ErrorMessage,Mode=OneWay}" Margin="0,5"/>
<TextBox IsReadOnly="True" Text="{Binding RelativePath,Mode=OneWay}"/>
</StackPanel>
</Border>
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<GroupBox Header="录制画质"> <GroupBox Header="录制画质">

File diff suppressed because one or more lines are too long

View File

@ -23,19 +23,19 @@ namespace BililiveRecorder.Web.Api
/// <param name="input"></param> /// <param name="input"></param>
/// <returns></returns> /// <returns></returns>
[HttpPost("generateFileName")] [HttpPost("generateFileName")]
public ActionResult<string> GenerateFileName([FromBody] GenerateFileNameInput input) public ActionResult<FileNameTemplateOutput> GenerateFileName([FromBody] GenerateFileNameInput input)
{ {
var config = new GlobalConfig() var config = new GlobalConfig()
{ {
WorkDirectory = "/", WorkDirectory = "/",
FileNameRecordTemplate = input.Template FileNameRecordTemplate = input.Template
}; };
var generator = new FileNameGenerator(config); var generator = new FileNameGenerator(config, null);
var context = this.mapper.Map<FileNameTemplateContext>(input.Context); var context = this.mapper.Map<FileNameTemplateContext>(input.Context);
var (_, relativePath) = generator.CreateFilePath(context); var output = generator.CreateFilePath(context);
return relativePath; return output;
} }
} }
} }

View File

@ -92,7 +92,7 @@ namespace BililiveRecorder.Core.Config.V3
public bool WpfShowTitleAndArea { get; } public bool WpfShowTitleAndArea { get; }
} }
[Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)] [Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)]
public sealed class GlobalConfig : HierarchicalPropertyDefault.HierarchicalObject<BililiveRecorder.Core.Config.V3.DefaultConfig, BililiveRecorder.Core.Config.V3.GlobalConfig> public sealed class GlobalConfig : HierarchicalPropertyDefault.HierarchicalObject<BililiveRecorder.Core.Config.V3.DefaultConfig, BililiveRecorder.Core.Config.V3.GlobalConfig>, BililiveRecorder.Core.Config.V3.IFileNameConfig
{ {
public GlobalConfig() { } public GlobalConfig() { }
public string? Cookie { get; set; } public string? Cookie { get; set; }
@ -201,8 +201,13 @@ namespace BililiveRecorder.Core.Config.V3
public string? WorkDirectory { get; set; } public string? WorkDirectory { get; set; }
public bool WpfShowTitleAndArea { get; set; } public bool WpfShowTitleAndArea { get; set; }
} }
public interface IFileNameConfig
{
string? FileNameRecordTemplate { get; }
string? WorkDirectory { get; }
}
[Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)] [Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)]
public sealed class RoomConfig : HierarchicalPropertyDefault.HierarchicalObject<BililiveRecorder.Core.Config.V3.GlobalConfig, BililiveRecorder.Core.Config.V3.RoomConfig> public sealed class RoomConfig : HierarchicalPropertyDefault.HierarchicalObject<BililiveRecorder.Core.Config.V3.GlobalConfig, BililiveRecorder.Core.Config.V3.RoomConfig>, BililiveRecorder.Core.Config.V3.IFileNameConfig
{ {
public RoomConfig() { } public RoomConfig() { }
public bool AutoRecord { get; set; } public bool AutoRecord { get; set; }
@ -474,11 +479,8 @@ namespace BililiveRecorder.Core.Templating
{ {
public sealed class FileNameGenerator public sealed class FileNameGenerator
{ {
public FileNameGenerator(BililiveRecorder.Core.Config.V3.GlobalConfig config) { } public FileNameGenerator(BililiveRecorder.Core.Config.V3.IFileNameConfig config, Serilog.ILogger? logger) { }
[return: System.Runtime.CompilerServices.TupleElementNames(new string[] { public BililiveRecorder.Core.Templating.FileNameTemplateOutput CreateFilePath(BililiveRecorder.Core.Templating.FileNameTemplateContext data) { }
"fullPath",
"relativePath"})]
public System.ValueTuple<string, string> CreateFilePath(BililiveRecorder.Core.Templating.FileNameTemplateContext data) { }
} }
public class FileNameTemplateContext public class FileNameTemplateContext
{ {
@ -492,6 +494,21 @@ namespace BililiveRecorder.Core.Templating
public int ShortId { get; set; } public int ShortId { get; set; }
public string Title { get; set; } public string Title { get; set; }
} }
public readonly struct FileNameTemplateOutput
{
public FileNameTemplateOutput(BililiveRecorder.Core.Templating.FileNameTemplateStatus status, string? errorMessage, string relativePath, string? fullPath) { }
public string? ErrorMessage { get; }
public string? FullPath { get; }
public string RelativePath { get; }
public BililiveRecorder.Core.Templating.FileNameTemplateStatus Status { get; }
}
public enum FileNameTemplateStatus
{
Success = 0,
TemplateError = 1,
OutOfRange = 2,
FileConflict = 3,
}
} }
namespace BililiveRecorder.DependencyInjection namespace BililiveRecorder.DependencyInjection
{ {
@ -502,4 +519,4 @@ namespace BililiveRecorder.DependencyInjection
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddRecorderConfig(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, BililiveRecorder.Core.Config.V3.ConfigV3 config) { } public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddRecorderConfig(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, BililiveRecorder.Core.Config.V3.ConfigV3 config) { }
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddRecorderRecording(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { } public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddRecorderRecording(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { }
} }
} }

View File

@ -1,38 +1,70 @@
using System.Collections.Generic;
using System.IO; using System.IO;
using BililiveRecorder.Core.Templating;
using Xunit; using Xunit;
namespace BililiveRecorder.Core.UnitTests.Recording namespace BililiveRecorder.Core.UnitTests.Recording
{ {
public class CheckIsWithinPathTests public class CheckIsWithinPathTests
{ {
[Theory(Skip = "Path 差异")] [Theory, MemberData(nameof(GetTestData))]
[InlineData(@"C:\", @"C:\", false)] public void RunTest(bool expectation, string parent, string child)
[InlineData(@"C:", @"C:\foo", true)]
[InlineData(@"C:\", @"C:\foo", true)]
[InlineData(@"C:\foo", @"C:\foo", false)]
[InlineData(@"C:\foo\", @"C:\foo", false)]
[InlineData(@"C:\foo", @"C:\foo\", true)]
[InlineData(@"C:\foo\", @"C:\foo\bar\", true)]
[InlineData(@"C:\foo\", @"C:\foo\bar", true)]
[InlineData(@"C:\foo", @"C:\FOO\bar", false)]
[InlineData(@"C:\foo", @"C:/foo/bar", true)]
[InlineData(@"C:\foo", @"C:\foobar", false)]
[InlineData(@"C:\foo", @"C:\foobar\baz", false)]
[InlineData(@"C:\foo\", @"C:\foobar\baz", false)]
[InlineData(@"C:\foobar", @"C:\foo\bar", false)]
[InlineData(@"C:\foobar\", @"C:\foo\bar", false)]
[InlineData(@"C:\foo", @"C:\foo\..\bar\baz", false)]
[InlineData(@"C:\bar", @"C:\foo\..\bar\baz", true)]
[InlineData(@"C:\barr", @"C:\foo\..\bar\baz", false)]
[InlineData(@"C:\foo\", @"D:\foo\bar", false)]
[InlineData(@"\\server1\vol1\foo", @"\\server1\vol1\foo", false)]
[InlineData(@"\\server1\vol1\foo", @"\\server1\vol1\bar", false)]
[InlineData(@"\\server1\vol1\foo", @"\\server1\vol1\foo\bar", true)]
[InlineData(@"\\server1\vol1\foo", @"\\server1\vol1\foo\..\bar", false)]
public void Test(string parent, string child, bool result)
{ {
// TODO fix path tests Assert.Equal(expectation, FileNameGenerator.CheckIsWithinPath(parent, child));
Assert.Equal(result, Core.Templating.FileNameGenerator.CheckIsWithinPath(parent, Path.GetDirectoryName(child)!)); }
public static IEnumerable<object[]> GetTestData()
{
yield return new object[] { true, @"/path/a/", "/path/a/file.flv" };
yield return new object[] { true, @"/path/a", "/path/a/file.flv" };
yield return new object[] { true, @"/path/a/", "/path/a/b/file.flv" };
yield return new object[] { true, @"/path/a", "/path/a/b/file.flv" };
yield return new object[] { true, @"/path/a/", "/path/a/../a/file.flv" };
yield return new object[] { true, @"/path/a", "/path/a/../a/file.flv" };
yield return new object[] { false, @"/path/a/", "/path/a/../b/file.flv" };
yield return new object[] { false, @"/path/a", "/path/a/../b/file.flv" };
yield return new object[] { true, @"/", "/path/a/file.flv" };
yield return new object[] { true, @"/", "/file.flv" };
yield return new object[] { false, @"/path", "/path/a/../../../../file.flv" };
yield return new object[] { false, @"/path", "/path../../../../file.flv" };
yield return new object[] { true, @"/path/a/", "/path////a/file.flv" };
yield return new object[] { true, @"/path/a", "/path////a/file.flv" };
yield return new object[] { false, @"/path/", "/path/" };
yield return new object[] { false, @"/path", "/path" };
yield return new object[] { false, @"/path/", "/path" };
yield return new object[] { false, @"/path", "/path/" };
var isWindows = Path.DirectorySeparatorChar == '\\';
yield return new object[] { isWindows && false, @"C:\", @"C:\" };
yield return new object[] { isWindows && true, @"C:\", @"C:\foo" };
yield return new object[] { isWindows && false, @"C:\foo", @"C:\foo" };
yield return new object[] { isWindows && false, @"C:\foo\", @"C:\foo" };
yield return new object[] { isWindows && false, @"C:\foo", @"C:\foo\" };
yield return new object[] { isWindows && true, @"C:\foo\", @"C:\foo\bar\" };
yield return new object[] { isWindows && true, @"C:\foo\", @"C:\foo\bar" };
yield return new object[] { isWindows && false, @"C:\foo", @"C:\FOO\bar" };
yield return new object[] { isWindows && true, @"C:\foo", @"C:/foo/bar" };
yield return new object[] { isWindows && false, @"C:\foo", @"C:\foobar" };
yield return new object[] { isWindows && false, @"C:\foo", @"C:\foobar\baz" };
yield return new object[] { isWindows && false, @"C:\foo\", @"C:\foobar\baz" };
yield return new object[] { isWindows && false, @"C:\foobar", @"C:\foo\bar" };
yield return new object[] { isWindows && false, @"C:\foobar\", @"C:\foo\bar" };
yield return new object[] { isWindows && false, @"C:\foo", @"C:\foo\..\bar\baz" };
yield return new object[] { isWindows && true, @"C:\bar", @"C:\foo\..\bar\baz" };
yield return new object[] { isWindows && false, @"C:\barr", @"C:\foo\..\bar\baz" };
yield return new object[] { isWindows && false, @"C:\foo\", @"D:\foo\bar" };
yield return new object[] { isWindows && false, @"\\server1\vol1\foo", @"\\server1\vol1\foo" };
yield return new object[] { isWindows && false, @"\\server1\vol1\foo", @"\\server1\vol1\bar" };
yield return new object[] { isWindows && true, @"\\server1\vol1\foo", @"\\server1\vol1\foo\bar" };
yield return new object[] { isWindows && false, @"\\server1\vol1\foo", @"\\server1\vol1\foo\..\bar" };
} }
} }
} }