feat: use `Newtonsoft.Json` streaming methods (#596)
This commit is contained in:
Родитель
e89a151051
Коммит
cb6204f542
|
@ -1,20 +1,32 @@
|
|||
namespace Microsoft.ComponentDetection.Common;
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.ComponentDetection.Common.Exceptions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class FileWritingService : IFileWritingService
|
||||
{
|
||||
/// <summary>
|
||||
/// The format string used to generate the timestamp for the manifest file.
|
||||
/// </summary>
|
||||
public const string TimestampFormatString = "yyyyMMddHHmmssfff";
|
||||
|
||||
private readonly object lockObject = new object();
|
||||
private readonly string timestamp = DateTime.Now.ToString(TimestampFormatString);
|
||||
private readonly ConcurrentDictionary<string, StreamWriter> bufferedStreams = new();
|
||||
|
||||
private readonly object lockObject = new();
|
||||
private readonly string timestamp = DateTime.Now.ToString(TimestampFormatString, CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// The base path to write files to.
|
||||
/// If null or empty, the temp path will be used.
|
||||
/// </summary>
|
||||
public string BasePath { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Init(string basePath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(basePath) && !Directory.Exists(basePath))
|
||||
|
@ -25,19 +37,25 @@ public sealed class FileWritingService : IFileWritingService
|
|||
this.BasePath = string.IsNullOrEmpty(basePath) ? Path.GetTempPath() : basePath;
|
||||
}
|
||||
|
||||
public void AppendToFile(string relativeFilePath, string text)
|
||||
/// <inheritdoc />
|
||||
public void AppendToFile<T>(string relativeFilePath, T obj)
|
||||
{
|
||||
relativeFilePath = this.ResolveFilePath(relativeFilePath);
|
||||
|
||||
if (!this.bufferedStreams.TryGetValue(relativeFilePath, out var streamWriter))
|
||||
{
|
||||
streamWriter = new StreamWriter(relativeFilePath, true);
|
||||
this.bufferedStreams.TryAdd(relativeFilePath, streamWriter);
|
||||
_ = this.bufferedStreams.TryAdd(relativeFilePath, streamWriter);
|
||||
}
|
||||
|
||||
streamWriter.Write(text);
|
||||
var serializer = new JsonSerializer
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
};
|
||||
serializer.Serialize(streamWriter, obj);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteFile(string relativeFilePath, string text)
|
||||
{
|
||||
relativeFilePath = this.ResolveFilePath(relativeFilePath);
|
||||
|
@ -48,6 +66,7 @@ public sealed class FileWritingService : IFileWritingService
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task WriteFileAsync(string relativeFilePath, string text)
|
||||
{
|
||||
relativeFilePath = this.ResolveFilePath(relativeFilePath);
|
||||
|
@ -55,23 +74,44 @@ public sealed class FileWritingService : IFileWritingService
|
|||
await File.WriteAllTextAsync(relativeFilePath, text);
|
||||
}
|
||||
|
||||
public void WriteFile(FileInfo relativeFilePath, string text)
|
||||
/// <inheritdoc />
|
||||
public void WriteFile<T>(FileInfo relativeFilePath, T obj)
|
||||
{
|
||||
File.WriteAllText(relativeFilePath.FullName, text);
|
||||
using var streamWriter = new StreamWriter(relativeFilePath.FullName);
|
||||
using var jsonWriter = new JsonTextWriter(streamWriter);
|
||||
var serializer = new JsonSerializer
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
};
|
||||
serializer.Serialize(jsonWriter, obj);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ResolveFilePath(string relativeFilePath)
|
||||
{
|
||||
this.EnsureInit();
|
||||
if (relativeFilePath.Contains("{timestamp}"))
|
||||
if (relativeFilePath.Contains("{timestamp}", StringComparison.Ordinal))
|
||||
{
|
||||
relativeFilePath = relativeFilePath.Replace("{timestamp}", this.timestamp);
|
||||
relativeFilePath = relativeFilePath.Replace("{timestamp}", this.timestamp, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
relativeFilePath = Path.Combine(this.BasePath, relativeFilePath);
|
||||
return relativeFilePath;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => this.Dispose(true);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
foreach (var (filename, streamWriter) in this.bufferedStreams)
|
||||
{
|
||||
await streamWriter.DisposeAsync();
|
||||
_ = this.bufferedStreams.TryRemove(filename, out _);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureInit()
|
||||
{
|
||||
if (string.IsNullOrEmpty(this.BasePath))
|
||||
|
@ -90,22 +130,7 @@ public sealed class FileWritingService : IFileWritingService
|
|||
foreach (var (filename, streamWriter) in this.bufferedStreams)
|
||||
{
|
||||
streamWriter.Dispose();
|
||||
this.bufferedStreams.TryRemove(filename, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
foreach (var (filename, streamWriter) in this.bufferedStreams)
|
||||
{
|
||||
await streamWriter.DisposeAsync();
|
||||
this.bufferedStreams.TryRemove(filename, out _);
|
||||
_ = this.bufferedStreams.TryRemove(filename, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,18 +4,53 @@ using System;
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
// All file paths are relative and will replace occurrences of {timestamp} with the shared file timestamp.
|
||||
/// <summary>
|
||||
/// Provides methods for writing files.
|
||||
/// </summary>
|
||||
public interface IFileWritingService : IDisposable, IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the file writing service with the given base path.
|
||||
/// </summary>
|
||||
/// <param name="basePath">The base path to use for all file operations.</param>
|
||||
void Init(string basePath);
|
||||
|
||||
void AppendToFile(string relativeFilePath, string text);
|
||||
/// <summary>
|
||||
/// Appends the object to the file as JSON.
|
||||
/// </summary>
|
||||
/// <param name="relativeFilePath">The relative path to the file.</param>
|
||||
/// <param name="obj">The object to append.</param>
|
||||
/// <typeparam name="T">The type of the object to append.</typeparam>
|
||||
void AppendToFile<T>(string relativeFilePath, T obj);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the text to the file.
|
||||
/// </summary>
|
||||
/// <param name="relativeFilePath">The relative path to the file.</param>
|
||||
/// <param name="text">The text to write.</param>
|
||||
void WriteFile(string relativeFilePath, string text);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the text to the file.
|
||||
/// </summary>
|
||||
/// <param name="relativeFilePath">The relative path to the file.</param>
|
||||
/// <param name="text">The text to write.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task WriteFileAsync(string relativeFilePath, string text);
|
||||
|
||||
void WriteFile(FileInfo relativeFilePath, string text);
|
||||
/// <summary>
|
||||
/// Writes the object to the file as JSON.
|
||||
/// </summary>
|
||||
/// <param name="relativeFilePath">The relative path to the file.</param>
|
||||
/// <param name="obj">The object to write.</param>
|
||||
/// <typeparam name="T">The type of the object to write.</typeparam>
|
||||
void WriteFile<T>(FileInfo relativeFilePath, T obj);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the complete file path from the given relative file path.
|
||||
/// Replaces occurrences of {timestamp} with the shared file timestamp.
|
||||
/// </summary>
|
||||
/// <param name="relativeFilePath">The relative path to the file.</param>
|
||||
/// <returns>The complete file path.</returns>
|
||||
string ResolveFilePath(string relativeFilePath);
|
||||
}
|
||||
|
|
|
@ -54,20 +54,23 @@ public class BcdeScanCommandService : IArgumentHandlingService
|
|||
this.logger.LogInformation("Scan Manifest file: {ManifestFile}", this.fileWritingService.ResolveFilePath(ManifestRelativePath));
|
||||
}
|
||||
|
||||
var manifestJson = JsonConvert.SerializeObject(scanResult, Formatting.Indented);
|
||||
|
||||
if (userRequestedManifestPath == null)
|
||||
{
|
||||
this.fileWritingService.AppendToFile(ManifestRelativePath, manifestJson);
|
||||
this.fileWritingService.AppendToFile(ManifestRelativePath, scanResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.fileWritingService.WriteFile(userRequestedManifestPath, manifestJson);
|
||||
this.fileWritingService.WriteFile(userRequestedManifestPath, scanResult);
|
||||
}
|
||||
|
||||
if (detectionArguments.PrintManifest)
|
||||
{
|
||||
Console.WriteLine(manifestJson);
|
||||
using var jsonWriter = new JsonTextWriter(Console.Out);
|
||||
var serializer = new JsonSerializer
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
};
|
||||
serializer.Serialize(jsonWriter, scanResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
namespace Microsoft.ComponentDetection.Common.Tests;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using FluentAssertions;
|
||||
|
@ -11,6 +13,17 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|||
[TestCategory("Governance/ComponentDetection")]
|
||||
public class FileWritingServiceTests
|
||||
{
|
||||
private const string SampleObjectJson = @"{
|
||||
""key1"": ""value1"",
|
||||
""key2"": ""value2""
|
||||
}";
|
||||
|
||||
private static readonly IDictionary<string, string> SampleObject = new Dictionary<string, string>
|
||||
{
|
||||
{ "key1", "value1" },
|
||||
{ "key2", "value2" },
|
||||
};
|
||||
|
||||
private FileWritingService serviceUnderTest;
|
||||
private string tempFolder;
|
||||
|
||||
|
@ -29,22 +42,20 @@ public class FileWritingServiceTests
|
|||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void TestCleanup()
|
||||
{
|
||||
Directory.Delete(this.tempFolder, true);
|
||||
}
|
||||
public void TestCleanup() => Directory.Delete(this.tempFolder, true);
|
||||
|
||||
[TestMethod]
|
||||
public void AppendToFile_AppendsToFiles()
|
||||
{
|
||||
var relativeDir = "someOtherFileName.txt";
|
||||
var relativeDir = "someOtherFileName.json";
|
||||
var fileLocation = Path.Combine(this.tempFolder, relativeDir);
|
||||
File.Create(fileLocation).Dispose();
|
||||
this.serviceUnderTest.AppendToFile(relativeDir, "someSampleText");
|
||||
|
||||
this.serviceUnderTest.AppendToFile(relativeDir, SampleObject);
|
||||
this.serviceUnderTest.Dispose();
|
||||
|
||||
var text = File.ReadAllText(Path.Combine(this.tempFolder, relativeDir));
|
||||
text
|
||||
.Should().Be("someSampleText");
|
||||
text.Should().Be(SampleObjectJson);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
@ -53,22 +64,33 @@ public class FileWritingServiceTests
|
|||
var relativeDir = "someFileName.txt";
|
||||
this.serviceUnderTest.WriteFile(relativeDir, "sampleText");
|
||||
var text = File.ReadAllText(Path.Combine(this.tempFolder, relativeDir));
|
||||
text
|
||||
.Should().Be("sampleText");
|
||||
text.Should().Be("sampleText");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteFile_WritesJson()
|
||||
{
|
||||
var relativeDir = "someFileName.txt";
|
||||
var fileInfo = new FileInfo(Path.Combine(this.tempFolder, relativeDir));
|
||||
|
||||
this.serviceUnderTest.WriteFile(fileInfo, SampleObject);
|
||||
|
||||
var text = File.ReadAllText(Path.Combine(this.tempFolder, relativeDir));
|
||||
text.Should().Be(SampleObjectJson);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteFile_AppendToFile_WorkWithTemplatizedPaths()
|
||||
{
|
||||
var relativeDir = "somefile_{timestamp}.txt";
|
||||
|
||||
this.serviceUnderTest.WriteFile(relativeDir, "sampleText");
|
||||
this.serviceUnderTest.AppendToFile(relativeDir, "sampleText2");
|
||||
this.serviceUnderTest.AppendToFile(relativeDir, SampleObject);
|
||||
this.serviceUnderTest.Dispose();
|
||||
|
||||
var files = Directory.GetFiles(this.tempFolder);
|
||||
files
|
||||
.Should().NotBeEmpty();
|
||||
File.ReadAllText(files[0])
|
||||
.Should().Contain($"sampleTextsampleText2");
|
||||
files.Should().NotBeEmpty();
|
||||
File.ReadAllText(files[0]).Should().Contain($"sampleText{SampleObjectJson}");
|
||||
this.VerifyTimestamp(files[0], "somefile_", ".txt");
|
||||
}
|
||||
|
||||
|
@ -76,7 +98,9 @@ public class FileWritingServiceTests
|
|||
public void ResolveFilePath_ResolvedTemplatizedPaths()
|
||||
{
|
||||
var relativeDir = "someOtherFile_{timestamp}.txt";
|
||||
|
||||
this.serviceUnderTest.WriteFile(relativeDir, string.Empty);
|
||||
|
||||
var fullPath = this.serviceUnderTest.ResolveFilePath(relativeDir);
|
||||
this.VerifyTimestamp(fullPath, "someOtherFile_", ".txt");
|
||||
}
|
||||
|
@ -86,7 +110,8 @@ public class FileWritingServiceTests
|
|||
{
|
||||
var relativeDir = Guid.NewGuid();
|
||||
var actualServiceUnderTest = new FileWritingService();
|
||||
Action action = () => actualServiceUnderTest.Init(Path.Combine(this.serviceUnderTest.BasePath, relativeDir.ToString()));
|
||||
|
||||
var action = () => actualServiceUnderTest.Init(Path.Combine(this.serviceUnderTest.BasePath, relativeDir.ToString()));
|
||||
|
||||
action.Should().Throw<InvalidUserInputException>();
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче