Skip to content

Commit 1069a6d

Browse files
authored
Merge branch 'main' into main
2 parents 472bc04 + c68be04 commit 1069a6d

File tree

9 files changed

+177
-50
lines changed

9 files changed

+177
-50
lines changed

src/ImageSharp/IO/IFileSystem.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
namespace SixLabors.ImageSharp.IO;
@@ -9,16 +9,32 @@ namespace SixLabors.ImageSharp.IO;
99
internal interface IFileSystem
1010
{
1111
/// <summary>
12-
/// Returns a readable stream as defined by the path.
12+
/// Opens a file as defined by the path and returns it as a readable stream.
1313
/// </summary>
1414
/// <param name="path">Path to the file to open.</param>
15-
/// <returns>A stream representing the file to open.</returns>
15+
/// <returns>A stream representing the opened file.</returns>
1616
Stream OpenRead(string path);
1717

1818
/// <summary>
19-
/// Creates or opens a file and returns it as a writable stream as defined by the path.
19+
/// Opens a file as defined by the path and returns it as a readable stream
20+
/// that can be used for asynchronous reading.
2021
/// </summary>
2122
/// <param name="path">Path to the file to open.</param>
22-
/// <returns>A stream representing the file to open.</returns>
23+
/// <returns>A stream representing the opened file.</returns>
24+
Stream OpenReadAsynchronous(string path);
25+
26+
/// <summary>
27+
/// Creates or opens a file as defined by the path and returns it as a writable stream.
28+
/// </summary>
29+
/// <param name="path">Path to the file to open.</param>
30+
/// <returns>A stream representing the opened file.</returns>
2331
Stream Create(string path);
32+
33+
/// <summary>
34+
/// Creates or opens a file as defined by the path and returns it as a writable stream
35+
/// that can be used for asynchronous reading and writing.
36+
/// </summary>
37+
/// <param name="path">Path to the file to open.</param>
38+
/// <returns>A stream representing the opened file.</returns>
39+
Stream CreateAsynchronous(string path);
2440
}

src/ImageSharp/IO/LocalFileSystem.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
namespace SixLabors.ImageSharp.IO;
@@ -11,6 +11,24 @@ internal sealed class LocalFileSystem : IFileSystem
1111
/// <inheritdoc/>
1212
public Stream OpenRead(string path) => File.OpenRead(path);
1313

14+
/// <inheritdoc/>
15+
public Stream OpenReadAsynchronous(string path) => File.Open(path, new FileStreamOptions
16+
{
17+
Mode = FileMode.Open,
18+
Access = FileAccess.Read,
19+
Share = FileShare.Read,
20+
Options = FileOptions.Asynchronous,
21+
});
22+
1423
/// <inheritdoc/>
1524
public Stream Create(string path) => File.Create(path);
25+
26+
/// <inheritdoc/>
27+
public Stream CreateAsynchronous(string path) => File.Open(path, new FileStreamOptions
28+
{
29+
Mode = FileMode.Create,
30+
Access = FileAccess.ReadWrite,
31+
Share = FileShare.None,
32+
Options = FileOptions.Asynchronous,
33+
});
1634
}

src/ImageSharp/Image.FromFile.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public static async Task<IImageFormat> DetectFormatAsync(
7272
{
7373
Guard.NotNull(options, nameof(options));
7474

75-
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
75+
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
7676
return await DetectFormatAsync(options, stream, cancellationToken).ConfigureAwait(false);
7777
}
7878

@@ -144,7 +144,7 @@ public static async Task<ImageInfo> IdentifyAsync(
144144
CancellationToken cancellationToken = default)
145145
{
146146
Guard.NotNull(options, nameof(options));
147-
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
147+
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
148148
return await IdentifyAsync(options, stream, cancellationToken).ConfigureAwait(false);
149149
}
150150

@@ -214,7 +214,7 @@ public static async Task<Image> LoadAsync(
214214
string path,
215215
CancellationToken cancellationToken = default)
216216
{
217-
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
217+
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
218218
return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false);
219219
}
220220

@@ -291,7 +291,7 @@ public static async Task<Image<TPixel>> LoadAsync<TPixel>(
291291
Guard.NotNull(options, nameof(options));
292292
Guard.NotNull(path, nameof(path));
293293

294-
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
294+
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
295295
return await LoadAsync<TPixel>(options, stream, cancellationToken).ConfigureAwait(false);
296296
}
297297
}

src/ImageSharp/ImageExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public static async Task SaveAsync(
7070
Guard.NotNull(path, nameof(path));
7171
Guard.NotNull(encoder, nameof(encoder));
7272

73-
using Stream fs = source.GetConfiguration().FileSystem.Create(path);
73+
await using Stream fs = source.GetConfiguration().FileSystem.CreateAsynchronous(path);
7474
await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false);
7575
}
7676

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
using SixLabors.ImageSharp.IO;
@@ -11,36 +11,113 @@ public class LocalFileSystemTests
1111
public void OpenRead()
1212
{
1313
string path = Path.GetTempFileName();
14-
string testData = Guid.NewGuid().ToString();
15-
File.WriteAllText(path, testData);
14+
try
15+
{
16+
string testData = Guid.NewGuid().ToString();
17+
File.WriteAllText(path, testData);
1618

17-
var fs = new LocalFileSystem();
19+
LocalFileSystem fs = new();
1820

19-
using (var r = new StreamReader(fs.OpenRead(path)))
20-
{
21-
string data = r.ReadToEnd();
21+
using (FileStream stream = (FileStream)fs.OpenRead(path))
22+
using (StreamReader reader = new(stream))
23+
{
24+
Assert.False(stream.IsAsync);
25+
Assert.True(stream.CanRead);
26+
Assert.False(stream.CanWrite);
2227

23-
Assert.Equal(testData, data);
28+
string data = reader.ReadToEnd();
29+
30+
Assert.Equal(testData, data);
31+
}
2432
}
33+
finally
34+
{
35+
File.Delete(path);
36+
}
37+
}
2538

26-
File.Delete(path);
39+
[Fact]
40+
public async Task OpenReadAsynchronous()
41+
{
42+
string path = Path.GetTempFileName();
43+
try
44+
{
45+
string testData = Guid.NewGuid().ToString();
46+
File.WriteAllText(path, testData);
47+
48+
LocalFileSystem fs = new();
49+
50+
await using (FileStream stream = (FileStream)fs.OpenReadAsynchronous(path))
51+
using (StreamReader reader = new(stream))
52+
{
53+
Assert.True(stream.IsAsync);
54+
Assert.True(stream.CanRead);
55+
Assert.False(stream.CanWrite);
56+
57+
string data = await reader.ReadToEndAsync();
58+
59+
Assert.Equal(testData, data);
60+
}
61+
}
62+
finally
63+
{
64+
File.Delete(path);
65+
}
2766
}
2867

2968
[Fact]
3069
public void Create()
3170
{
3271
string path = Path.GetTempFileName();
33-
string testData = Guid.NewGuid().ToString();
34-
var fs = new LocalFileSystem();
72+
try
73+
{
74+
string testData = Guid.NewGuid().ToString();
75+
LocalFileSystem fs = new();
76+
77+
using (FileStream stream = (FileStream)fs.Create(path))
78+
using (StreamWriter writer = new(stream))
79+
{
80+
Assert.False(stream.IsAsync);
81+
Assert.True(stream.CanRead);
82+
Assert.True(stream.CanWrite);
3583

36-
using (var r = new StreamWriter(fs.Create(path)))
84+
writer.Write(testData);
85+
}
86+
87+
string data = File.ReadAllText(path);
88+
Assert.Equal(testData, data);
89+
}
90+
finally
3791
{
38-
r.Write(testData);
92+
File.Delete(path);
3993
}
94+
}
4095

41-
string data = File.ReadAllText(path);
42-
Assert.Equal(testData, data);
96+
[Fact]
97+
public async Task CreateAsynchronous()
98+
{
99+
string path = Path.GetTempFileName();
100+
try
101+
{
102+
string testData = Guid.NewGuid().ToString();
103+
LocalFileSystem fs = new();
104+
105+
await using (FileStream stream = (FileStream)fs.CreateAsynchronous(path))
106+
await using (StreamWriter writer = new(stream))
107+
{
108+
Assert.True(stream.IsAsync);
109+
Assert.True(stream.CanRead);
110+
Assert.True(stream.CanWrite);
111+
112+
await writer.WriteAsync(testData);
113+
}
43114

44-
File.Delete(path);
115+
string data = File.ReadAllText(path);
116+
Assert.Equal(testData, data);
117+
}
118+
finally
119+
{
120+
File.Delete(path);
121+
}
45122
}
46123
}

tests/ImageSharp.Tests/Image/ImageSaveTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public ImageSaveTests()
4444
[Fact]
4545
public void SavePath()
4646
{
47-
var stream = new MemoryStream();
47+
using MemoryStream stream = new();
4848
this.fileSystem.Setup(x => x.Create("path.png")).Returns(stream);
4949
this.image.Save("path.png");
5050

@@ -54,7 +54,7 @@ public void SavePath()
5454
[Fact]
5555
public void SavePathWithEncoder()
5656
{
57-
var stream = new MemoryStream();
57+
using MemoryStream stream = new();
5858
this.fileSystem.Setup(x => x.Create("path.jpg")).Returns(stream);
5959

6060
this.image.Save("path.jpg", this.encoderNotInFormat.Object);
@@ -73,7 +73,7 @@ public void ToBase64String()
7373
[Fact]
7474
public void SaveStreamWithMime()
7575
{
76-
var stream = new MemoryStream();
76+
using MemoryStream stream = new();
7777
this.image.Save(stream, this.localImageFormat.Object);
7878

7979
this.encoder.Verify(x => x.Encode(this.image, stream));
@@ -82,7 +82,7 @@ public void SaveStreamWithMime()
8282
[Fact]
8383
public void SaveStreamWithEncoder()
8484
{
85-
var stream = new MemoryStream();
85+
using MemoryStream stream = new();
8686

8787
this.image.Save(stream, this.encoderNotInFormat.Object);
8888

tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ protected ImageLoadTestBase()
122122
Stream StreamFactory() => this.DataStream;
123123

124124
this.LocalFileSystemMock.Setup(x => x.OpenRead(this.MockFilePath)).Returns(StreamFactory);
125+
this.LocalFileSystemMock.Setup(x => x.OpenReadAsynchronous(this.MockFilePath)).Returns(StreamFactory);
125126
this.topLevelFileSystem.AddFile(this.MockFilePath, StreamFactory);
126127
this.LocalConfiguration.FileSystem = this.LocalFileSystemMock.Object;
127128
this.TopLevelConfiguration.FileSystem = this.topLevelFileSystem;
@@ -132,6 +133,11 @@ public void Dispose()
132133
// Clean up the global object;
133134
this.localStreamReturnImageRgba32?.Dispose();
134135
this.localStreamReturnImageAgnostic?.Dispose();
136+
137+
if (this.dataStreamLazy.IsValueCreated)
138+
{
139+
this.dataStreamLazy.Value.Dispose();
140+
}
135141
}
136142

137143
protected virtual Stream CreateStream() => this.TestFormat.CreateStream(this.Marker);
Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
#nullable enable
5+
46
namespace SixLabors.ImageSharp.Tests;
57

68
/// <summary>
79
/// A test image file.
810
/// </summary>
911
public class TestFileSystem : ImageSharp.IO.IFileSystem
1012
{
11-
private readonly Dictionary<string, Func<Stream>> fileSystem = new Dictionary<string, Func<Stream>>(StringComparer.OrdinalIgnoreCase);
13+
private readonly Dictionary<string, Func<Stream>> fileSystem = new(StringComparer.OrdinalIgnoreCase);
1214

1315
public void AddFile(string path, Func<Stream> data)
1416
{
@@ -18,35 +20,39 @@ public void AddFile(string path, Func<Stream> data)
1820
}
1921
}
2022

21-
public Stream Create(string path)
23+
public Stream Create(string path) => this.GetStream(path) ?? File.Create(path);
24+
25+
public Stream CreateAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions
2226
{
23-
// if we have injected a fake file use it instead
24-
lock (this.fileSystem)
25-
{
26-
if (this.fileSystem.ContainsKey(path))
27-
{
28-
Stream stream = this.fileSystem[path]();
29-
stream.Position = 0;
30-
return stream;
31-
}
32-
}
27+
Mode = FileMode.Create,
28+
Access = FileAccess.ReadWrite,
29+
Share = FileShare.None,
30+
Options = FileOptions.Asynchronous,
31+
});
3332

34-
return File.Create(path);
35-
}
33+
public Stream OpenRead(string path) => this.GetStream(path) ?? File.OpenRead(path);
34+
35+
public Stream OpenReadAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions
36+
{
37+
Mode = FileMode.Open,
38+
Access = FileAccess.Read,
39+
Share = FileShare.Read,
40+
Options = FileOptions.Asynchronous,
41+
});
3642

37-
public Stream OpenRead(string path)
43+
private Stream? GetStream(string path)
3844
{
3945
// if we have injected a fake file use it instead
4046
lock (this.fileSystem)
4147
{
42-
if (this.fileSystem.ContainsKey(path))
48+
if (this.fileSystem.TryGetValue(path, out Func<Stream>? streamFactory))
4349
{
44-
Stream stream = this.fileSystem[path]();
50+
Stream stream = streamFactory();
4551
stream.Position = 0;
4652
return stream;
4753
}
4854
}
4955

50-
return File.OpenRead(path);
56+
return null;
5157
}
5258
}

tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,9 @@ internal class SingleStreamFileSystem : IFileSystem
1313

1414
Stream IFileSystem.Create(string path) => this.stream;
1515

16+
Stream IFileSystem.CreateAsynchronous(string path) => this.stream;
17+
1618
Stream IFileSystem.OpenRead(string path) => this.stream;
19+
20+
Stream IFileSystem.OpenReadAsynchronous(string path) => this.stream;
1721
}

0 commit comments

Comments
 (0)