From 0b8cf27ce9693d79d0c474c0d366b6c1a1511406 Mon Sep 17 00:00:00 2001 From: genteure Date: Tue, 28 Jun 2022 16:10:06 +0800 Subject: [PATCH] Core: Refactor file name templateing --- BililiveRecorder.Core/Config/V3/ConfigV3.cs | 4 +- .../Config/V3/IFileNameConfig.cs | 9 ++ .../DependencyInjectionExtensions.cs | 2 - .../Recording/RawDataRecordTask.cs | 3 - .../Recording/RecordTaskBase.cs | 30 +++-- .../Recording/StandardRecordTask.cs | 3 - .../Templating/FileNameGenerator.cs | 112 +++++++++--------- .../Templating/FileNameTemplateOutput.cs | 23 ++++ .../Templating/FileNameTemplateStatus.cs | 10 ++ BililiveRecorder.WPF/Pages/SettingsPage.xaml | 10 +- .../Pages/SettingsPage.xaml.cs | 31 +++-- BililiveRecorder.Web/Api/MiscController.cs | 8 +- .../PublicApi.HasNoChangesAsync.verified.txt | 33 ++++-- .../Recording/CheckIsWithinPathTests.cs | 86 +++++++++----- 14 files changed, 235 insertions(+), 129 deletions(-) create mode 100644 BililiveRecorder.Core/Config/V3/IFileNameConfig.cs create mode 100644 BililiveRecorder.Core/Templating/FileNameTemplateOutput.cs create mode 100644 BililiveRecorder.Core/Templating/FileNameTemplateStatus.cs diff --git a/BililiveRecorder.Core/Config/V3/ConfigV3.cs b/BililiveRecorder.Core/Config/V3/ConfigV3.cs index d6834cf..5db61f1 100644 --- a/BililiveRecorder.Core/Config/V3/ConfigV3.cs +++ b/BililiveRecorder.Core/Config/V3/ConfigV3.cs @@ -18,7 +18,7 @@ namespace BililiveRecorder.Core.Config.V3 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 })) { } @@ -28,7 +28,7 @@ namespace BililiveRecorder.Core.Config.V3 public string? WorkDirectory => this.GetPropertyValue(); } - public partial class GlobalConfig + public partial class GlobalConfig : IFileNameConfig { public GlobalConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name })) { diff --git a/BililiveRecorder.Core/Config/V3/IFileNameConfig.cs b/BililiveRecorder.Core/Config/V3/IFileNameConfig.cs new file mode 100644 index 0000000..f810235 --- /dev/null +++ b/BililiveRecorder.Core/Config/V3/IFileNameConfig.cs @@ -0,0 +1,9 @@ +namespace BililiveRecorder.Core.Config.V3 +{ + public interface IFileNameConfig + { + public string? FileNameRecordTemplate { get; } + + public string? WorkDirectory { get; } + } +} diff --git a/BililiveRecorder.Core/DependencyInjectionExtensions.cs b/BililiveRecorder.Core/DependencyInjectionExtensions.cs index 4c3c7c8..52a1ca9 100644 --- a/BililiveRecorder.Core/DependencyInjectionExtensions.cs +++ b/BililiveRecorder.Core/DependencyInjectionExtensions.cs @@ -6,7 +6,6 @@ using BililiveRecorder.Core.Config.V3; using BililiveRecorder.Core.Danmaku; using BililiveRecorder.Core.Recording; using BililiveRecorder.Core.Scripting; -using BililiveRecorder.Core.Templating; using BililiveRecorder.Flv; using Microsoft.Extensions.DependencyInjection; using Polly.Registry; @@ -46,7 +45,6 @@ namespace BililiveRecorder.DependencyInjection ; public static IServiceCollection AddRecorderRecording(this IServiceCollection services) => services - .AddSingleton() .AddScoped() .AddScoped() .AddScoped() diff --git a/BililiveRecorder.Core/Recording/RawDataRecordTask.cs b/BililiveRecorder.Core/Recording/RawDataRecordTask.cs index 8d31bcc..c005114 100644 --- a/BililiveRecorder.Core/Recording/RawDataRecordTask.cs +++ b/BililiveRecorder.Core/Recording/RawDataRecordTask.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using BililiveRecorder.Core.Api; using BililiveRecorder.Core.Event; using BililiveRecorder.Core.Scripting; -using BililiveRecorder.Core.Templating; using Serilog; namespace BililiveRecorder.Core.Recording @@ -17,12 +16,10 @@ namespace BililiveRecorder.Core.Recording public RawDataRecordTask(IRoom room, ILogger logger, IApiClient apiClient, - FileNameGenerator fileNameGenerator, UserScriptRunner userScriptRunner) : base(room: room, logger: logger?.ForContext().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!, apiClient: apiClient, - fileNameGenerator: fileNameGenerator, userScriptRunner: userScriptRunner) { } diff --git a/BililiveRecorder.Core/Recording/RecordTaskBase.cs b/BililiveRecorder.Core/Recording/RecordTaskBase.cs index 19c168c..16b444e 100644 --- a/BililiveRecorder.Core/Recording/RecordTaskBase.cs +++ b/BililiveRecorder.Core/Recording/RecordTaskBase.cs @@ -52,13 +52,14 @@ namespace BililiveRecorder.Core.Recording private DateTimeOffset ioStatsLastTrigger; 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.logger = logger ?? throw new ArgumentNullException(nameof(logger)); 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.fileNameGenerator = new FileNameGenerator(room.RoomConfig, logger); this.ct = this.cts.Token; 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), - Title = FileNameGenerator.RemoveInvalidFileName(this.room.Title, ignore_slash: false), - RoomId = this.room.RoomConfig.RoomId, - ShortId = this.room.ShortId, - AreaParent = FileNameGenerator.RemoveInvalidFileName(this.room.AreaNameParent, ignore_slash: false), - AreaChild = FileNameGenerator.RemoveInvalidFileName(this.room.AreaNameChild, ignore_slash: false), - Qn = this.qn, - Json = this.room.RawBilibiliApiJsonData, - }); + var output = this.fileNameGenerator.CreateFilePath(new FileNameTemplateContext + { + Name = FileNameGenerator.RemoveInvalidFileName(this.room.Name, ignore_slash: false), + Title = FileNameGenerator.RemoveInvalidFileName(this.room.Title, ignore_slash: false), + RoomId = this.room.RoomConfig.RoomId, + ShortId = this.room.ShortId, + AreaParent = FileNameGenerator.RemoveInvalidFileName(this.room.AreaNameParent, ignore_slash: false), + AreaChild = FileNameGenerator.RemoveInvalidFileName(this.room.AreaNameChild, ignore_slash: false), + Qn = this.qn, + Json = this.room.RawBilibiliApiJsonData, + }); + + return (output.FullPath!, output.RelativePath); + } #region Api Requests diff --git a/BililiveRecorder.Core/Recording/StandardRecordTask.cs b/BililiveRecorder.Core/Recording/StandardRecordTask.cs index caef975..e725be8 100644 --- a/BililiveRecorder.Core/Recording/StandardRecordTask.cs +++ b/BililiveRecorder.Core/Recording/StandardRecordTask.cs @@ -10,7 +10,6 @@ using BililiveRecorder.Core.Config; using BililiveRecorder.Core.Event; using BililiveRecorder.Core.ProcessingRules; using BililiveRecorder.Core.Scripting; -using BililiveRecorder.Core.Templating; using BililiveRecorder.Flv; using BililiveRecorder.Flv.Amf; using BililiveRecorder.Flv.Pipeline; @@ -44,12 +43,10 @@ namespace BililiveRecorder.Core.Recording IFlvTagReaderFactory flvTagReaderFactory, ITagGroupReaderFactory tagGroupReaderFactory, IFlvProcessingContextWriterFactory writerFactory, - FileNameGenerator fileNameGenerator, UserScriptRunner userScriptRunner) : base(room: room, logger: logger?.ForContext().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!, apiClient: apiClient, - fileNameGenerator: fileNameGenerator, userScriptRunner: userScriptRunner) { this.flvTagReaderFactory = flvTagReaderFactory ?? throw new ArgumentNullException(nameof(flvTagReaderFactory)); diff --git a/BililiveRecorder.Core/Templating/FileNameGenerator.cs b/BililiveRecorder.Core/Templating/FileNameGenerator.cs index d26e7d7..8047a5e 100644 --- a/BililiveRecorder.Core/Templating/FileNameGenerator.cs +++ b/BililiveRecorder.Core/Templating/FileNameGenerator.cs @@ -7,6 +7,7 @@ using Fluid.Ast; using Fluid.Values; using Newtonsoft.Json.Linq; using Serilog; +using Serilog.Core; namespace BililiveRecorder.Core.Templating { @@ -15,8 +16,6 @@ namespace BililiveRecorder.Core.Templating // TODO: 需要改得更通用一些 // 日志不应该一定绑定到一个直播间上 - private static readonly ILogger logger = Log.Logger.ForContext(); - private static readonly FluidParser parser; private static readonly IFluidTemplate defaultTemplate; @@ -39,8 +38,8 @@ namespace BililiveRecorder.Core.Templating } } - private readonly GlobalConfig config; - private IFluidTemplate? template; + private readonly IFileNameConfig config; + private readonly ILogger logger; static FileNameGenerator() { @@ -72,32 +71,22 @@ namespace BililiveRecorder.Core.Templating defaultTemplate = parser.Parse(DefaultConfig.Instance.FileNameRecordTemplate); } - public FileNameGenerator(GlobalConfig config) + public FileNameGenerator(IFileNameConfig config, ILogger? logger) { this.config = config ?? throw new ArgumentNullException(nameof(config)); - - config.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(config.FileNameRecordTemplate)) - { - this.UpdateTemplate(); - } - }; - - this.UpdateTemplate(); + this.logger = logger?.ForContext() ?? Logger.None; } - private void UpdateTemplate() + public FileNameTemplateOutput CreateFilePath(FileNameTemplateContext data) { - if (!parser.TryParse(this.config.FileNameRecordTemplate, out var template, out var error)) - { - logger.Warning("文件名模板格式不正确,请修改: {ParserError}", error); - } - this.template = template; - } + var status = FileNameTemplateStatus.Success; + string? errorMessage = null; + string relativePath; + string? fullPath; + + var workDirectory = this.config.WorkDirectory; + var skipFullPath = workDirectory is null; - public (string fullPath, string relativePath) CreateFilePath(FileNameTemplateContext data) - { var now = DateTimeOffset.Now; var templateOptions = new TemplateOptions { @@ -105,49 +94,57 @@ namespace BililiveRecorder.Core.Templating }; templateOptions.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.CamelCase; templateOptions.ValueConverters.Add(o => o is JContainer j ? new JContainerValue(j) : null); - templateOptions.Filters.AddFilter("format_qn", static (FluidValue input, FilterArguments arguments, TemplateContext context) - => new StringValue(StreamQualityNumber.MapToString((int)input.ToNumberValue())) - ); + templateOptions.Filters.AddFilter("format_qn", + static (FluidValue input, FilterArguments arguments, TemplateContext context) => new StringValue(StreamQualityNumber.MapToString((int)input.ToNumberValue()))); var context = new TemplateContext(data, templateOptions); - var workDirectory = this.config.WorkDirectory!; - - if (this.template is not { } t) + if (!parser.TryParse(this.config.FileNameRecordTemplate, out var template, out var error)) { - logger.ForContext(LoggingContext.RoomId, data.RoomId).Warning("文件名模板格式不正确,请检查设置。将写入到默认路径。"); + this.logger.Warning("文件名模板格式不正确,请修改: {ParserError}", error); + errorMessage = "文件名模板格式不正确,请修改: " + error; + status = FileNameTemplateStatus.TemplateError; goto returnDefaultPath; } - var relativePath = t.Render(context); + relativePath = template.Render(context); 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; } - var ext = Path.GetExtension(fullPath); + var ext = Path.GetExtension(relativePath); 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"; - 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; } - return (fullPath, relativePath); + return new FileNameTemplateOutput(status, errorMessage, relativePath, fullPath); returnDefaultPath: 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 @@ -238,22 +235,29 @@ namespace BililiveRecorder.Core.Templating return input; } + private static readonly char[] separator = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; + 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; - parent = parent.Replace('/', '\\'); - 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); + return childSegments.Slice(0, parentSegments.Length).SequenceEqual(parentSegments); } } } diff --git a/BililiveRecorder.Core/Templating/FileNameTemplateOutput.cs b/BililiveRecorder.Core/Templating/FileNameTemplateOutput.cs new file mode 100644 index 0000000..5192c82 --- /dev/null +++ b/BililiveRecorder.Core/Templating/FileNameTemplateOutput.cs @@ -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; } + } +} diff --git a/BililiveRecorder.Core/Templating/FileNameTemplateStatus.cs b/BililiveRecorder.Core/Templating/FileNameTemplateStatus.cs new file mode 100644 index 0000000..1cfde69 --- /dev/null +++ b/BililiveRecorder.Core/Templating/FileNameTemplateStatus.cs @@ -0,0 +1,10 @@ +namespace BililiveRecorder.Core.Templating +{ + public enum FileNameTemplateStatus + { + Success = 0, + TemplateError, + OutOfRange, + FileConflict, + } +} diff --git a/BililiveRecorder.WPF/Pages/SettingsPage.xaml b/BililiveRecorder.WPF/Pages/SettingsPage.xaml index 594feb2..9358cf8 100644 --- a/BililiveRecorder.WPF/Pages/SettingsPage.xaml +++ b/BililiveRecorder.WPF/Pages/SettingsPage.xaml @@ -11,6 +11,7 @@ l:ResxLocalizationProvider.DefaultDictionary="Strings" xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls" 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:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core" xmlns:configv3="clr-namespace:BililiveRecorder.Core.Config.V3;assembly=BililiveRecorder.Core" @@ -96,7 +97,14 @@