Skip to content

Commit 4029088

Browse files
authored
Fix the stream was disposed before its real end (OrchardCMS#17878)
1 parent 0285591 commit 4029088

File tree

3 files changed

+208
-98
lines changed

3 files changed

+208
-98
lines changed

src/OrchardCore.Modules/OrchardCore.Media/Controllers/AdminController.cs

Lines changed: 81 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -201,96 +201,90 @@ public async Task<IActionResult> Upload(string path, string extensions)
201201
(_, _, _) => Task.FromResult<IActionResult>(Ok(new { })),
202202
async (files) =>
203203
{
204-
if (string.IsNullOrEmpty(path))
204+
var result = await ProcessMediaUploadAsync(path, files, allowedExtensions);
205+
206+
return Ok(new { files = result.ToArray() });
207+
});
208+
}
209+
210+
private async Task<List<object>> ProcessMediaUploadAsync(string path, IEnumerable<IFormFile> files, HashSet<string> allowedExtensions)
211+
{
212+
if (string.IsNullOrEmpty(path))
213+
{
214+
path = string.Empty;
215+
}
216+
217+
var result = new List<object>();
218+
219+
// Loop through each file in the request.
220+
foreach (var file in files)
221+
{
222+
var extension = Path.GetExtension(file.FileName);
223+
224+
if (!allowedExtensions.Contains(extension))
225+
{
226+
result.Add(new
227+
{
228+
name = file.FileName,
229+
size = file.Length,
230+
folder = path,
231+
error = S["This file extension is not allowed: {0}", extension].ToString(),
232+
});
233+
234+
if (_logger.IsEnabled(LogLevel.Information))
205235
{
206-
path = string.Empty;
236+
_logger.LogInformation("File extension not allowed: '{File}'", file.FileName);
207237
}
208238

209-
var result = new List<object>();
239+
continue;
240+
}
241+
242+
var fileName = _mediaNameNormalizerService.NormalizeFileName(file.FileName);
210243

211-
// Loop through each file in the request.
212-
foreach (var file in files)
244+
Stream stream = null;
245+
try
246+
{
247+
var mediaFilePath = _mediaFileStore.Combine(path, fileName);
248+
stream = file.OpenReadStream();
249+
mediaFilePath = await _mediaFileStore.CreateFileFromStreamAsync(mediaFilePath, stream);
250+
251+
var mediaFile = await _mediaFileStore.GetFileInfoAsync(mediaFilePath);
252+
253+
await PreCacheRemoteMedia(mediaFile);
254+
255+
result.Add(CreateFileResult(mediaFile));
256+
}
257+
catch (ExistsFileStoreException ex)
258+
{
259+
_logger.LogWarning(ex, "An error occurred while uploading a media");
260+
261+
result.Add(new
213262
{
214-
var extension = Path.GetExtension(file.FileName);
215-
216-
if (!allowedExtensions.Contains(extension))
217-
{
218-
result.Add(new
219-
{
220-
name = file.FileName,
221-
size = file.Length,
222-
folder = path,
223-
error = S["This file extension is not allowed: {0}", extension].ToString(),
224-
});
225-
226-
if (_logger.IsEnabled(LogLevel.Information))
227-
{
228-
_logger.LogInformation("File extension not allowed: '{File}'", file.FileName);
229-
}
230-
231-
continue;
232-
}
233-
234-
var fileName = _mediaNameNormalizerService.NormalizeFileName(file.FileName);
235-
236-
Stream stream = null;
237-
try
238-
{
239-
var mediaFilePath = _mediaFileStore.Combine(path, fileName);
240-
stream = file.OpenReadStream();
241-
mediaFilePath = await _mediaFileStore.CreateFileFromStreamAsync(mediaFilePath, stream);
242-
243-
var mediaFile = await _mediaFileStore.GetFileInfoAsync(mediaFilePath);
244-
245-
// The .NET AWS SDK, and only that from the built-in ones (but others maybe too), disposes
246-
// the stream. There's no better way to check for that than handling the exception. An
247-
// alternative would be to re-read the file for every other storage provider as well but
248-
// that would be wasteful.
249-
try
250-
{
251-
stream.Position = 0;
252-
}
253-
catch (ObjectDisposedException)
254-
{
255-
stream = null;
256-
}
257-
258-
await PreCacheRemoteMedia(mediaFile, stream);
259-
260-
result.Add(CreateFileResult(mediaFile));
261-
}
262-
catch (ExistsFileStoreException ex)
263-
{
264-
_logger.LogWarning(ex, "An error occurred while uploading a media");
265-
266-
result.Add(new
267-
{
268-
name = fileName,
269-
size = file.Length,
270-
folder = path,
271-
error = ex.Message,
272-
});
273-
}
274-
catch (Exception ex)
275-
{
276-
_logger.LogError(ex, "An error occurred while uploading a media");
277-
278-
result.Add(new
279-
{
280-
name = fileName,
281-
size = file.Length,
282-
folder = path,
283-
error = ex.Message,
284-
});
285-
}
286-
finally
287-
{
288-
stream?.Dispose();
289-
}
290-
}
263+
name = fileName,
264+
size = file.Length,
265+
folder = path,
266+
error = ex.Message,
267+
});
268+
}
269+
catch (Exception ex)
270+
{
271+
_logger.LogError(ex, "An error occurred while uploading a media");
291272

292-
return Ok(new { files = result.ToArray() });
293-
});
273+
result.Add(new
274+
{
275+
name = fileName,
276+
size = file.Length,
277+
folder = path,
278+
error = ex.Message,
279+
});
280+
}
281+
finally
282+
{
283+
stream?.Dispose();
284+
}
285+
}
286+
287+
return result;
294288
}
295289

296290
[HttpPost]
@@ -572,24 +566,19 @@ private string GetCacheBustingMediaPublicUrl(string path) =>
572566
// this, the Media Library page will try to load the thumbnail without a cache busting parameter, since
573567
// ShellFileVersionProvider won't find it in the local cache.
574568
// This is not required for files moved across folders, because the folder will be reopened anyway.
575-
private async Task PreCacheRemoteMedia(IFileStoreEntry mediaFile, Stream stream = null)
569+
private async Task PreCacheRemoteMedia(IFileStoreEntry mediaFile)
576570
{
577571
var mediaFileStoreCache = _serviceProvider.GetService<IMediaFileStoreCache>();
578572
if (mediaFileStoreCache == null)
579573
{
580574
return;
581575
}
582576

583-
Stream localStream = null;
584-
585-
if (stream == null)
586-
{
587-
stream = localStream = await _mediaFileStore.GetFileStreamAsync(mediaFile);
588-
}
577+
var localStream = await _mediaFileStore.GetFileStreamAsync(mediaFile);
589578

590579
try
591580
{
592-
await mediaFileStoreCache.SetCacheAsync(stream, mediaFile, HttpContext.RequestAborted);
581+
await mediaFileStoreCache.SetCacheAsync(localStream, mediaFile, HttpContext.RequestAborted);
593582
}
594583
finally
595584
{

src/OrchardCore/OrchardCore.Media.Core/DefaultMediaFileStore.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,16 +169,22 @@ public virtual async Task<string> CreateFileFromStreamAsync(string path, Stream
169169

170170
foreach (var mediaCreatingEventHandler in _mediaCreatingEventHandlers)
171171
{
172-
// Creating stream disposed by using.
173-
using var creatingStream = outputStream;
174-
175-
// Stop disposal of inputStream, as creating stream is the object to dispose.
176-
inputStream = null;
172+
var creatingStream = outputStream;
177173

178174
// Outputstream must be created by event handler.
179175
outputStream = null;
180176

181-
outputStream = await mediaCreatingEventHandler.MediaCreatingAsync(context, creatingStream);
177+
try
178+
{
179+
outputStream = await mediaCreatingEventHandler.MediaCreatingAsync(context, creatingStream);
180+
}
181+
finally
182+
{
183+
if (creatingStream != outputStream && creatingStream != inputStream)
184+
{
185+
creatingStream.Dispose();
186+
}
187+
}
182188
}
183189

184190
return await _fileStore.CreateFileFromStreamAsync(context.Path, outputStream, overwrite);
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System.IO;
2+
using System.Threading.Tasks;
3+
using System.Collections.Generic;
4+
using Moq;
5+
using Xunit;
6+
using OrchardCore.Media.Core;
7+
using OrchardCore.FileStorage;
8+
using OrchardCore.Media.Events;
9+
using Microsoft.Extensions.Logging;
10+
11+
namespace OrchardCore.Tests.Modules.OrchardCore.Media;
12+
13+
public class DefaultMediaFileStoreTests
14+
{
15+
[Fact]
16+
public async Task CreateFileFromStreamAsync_NoHandlers_CallsFileStoreDirectly()
17+
{
18+
// Arrange
19+
var fileStoreMock = new Mock<IFileStore>();
20+
var loggerMock = new Mock<ILogger<DefaultMediaFileStore>>();
21+
var inputStream = new MemoryStream();
22+
fileStoreMock
23+
.Setup(x => x.CreateFileFromStreamAsync("test.txt", inputStream, false))
24+
.ReturnsAsync("result");
25+
26+
var store = new DefaultMediaFileStore(
27+
fileStoreMock.Object,
28+
"",
29+
"",
30+
new List<IMediaEventHandler>(),
31+
new List<IMediaCreatingEventHandler>(),
32+
loggerMock.Object);
33+
34+
// Act
35+
var result = await store.CreateFileFromStreamAsync("test.txt", inputStream);
36+
37+
// Assert
38+
Assert.Equal("result", result);
39+
fileStoreMock.Verify(x => x.CreateFileFromStreamAsync("test.txt", inputStream, false), Times.Once);
40+
}
41+
42+
[Fact]
43+
public async Task CreateFileFromStreamAsync_HandlerReturnsInputStream_PassesInputStreamToFileStore()
44+
{
45+
// Arrange
46+
var fileStoreMock = new Mock<IFileStore>();
47+
var loggerMock = new Mock<ILogger<DefaultMediaFileStore>>();
48+
var inputStream = new MemoryStream();
49+
var handlerMock = new Mock<IMediaCreatingEventHandler>();
50+
handlerMock
51+
.Setup(x => x.MediaCreatingAsync(It.IsAny<MediaCreatingContext>(), inputStream))
52+
.ReturnsAsync(inputStream);
53+
54+
fileStoreMock
55+
.Setup(x => x.CreateFileFromStreamAsync("test.txt", inputStream, false))
56+
.ReturnsAsync("result");
57+
58+
var store = new DefaultMediaFileStore(
59+
fileStoreMock.Object,
60+
"",
61+
"",
62+
new List<IMediaEventHandler>(),
63+
new[] { handlerMock.Object },
64+
loggerMock.Object);
65+
66+
// Act
67+
var result = await store.CreateFileFromStreamAsync("test.txt", inputStream);
68+
69+
// Assert
70+
Assert.Equal("result", result);
71+
fileStoreMock.Verify(x => x.CreateFileFromStreamAsync("test.txt", inputStream, false), Times.Once);
72+
}
73+
74+
[Fact]
75+
public async Task CreateFileFromStreamAsync_HandlersCreateNewStreams_PassesCorrectStreams()
76+
{
77+
// Arrange
78+
var fileStoreMock = new Mock<IFileStore>();
79+
var loggerMock = new Mock<ILogger<DefaultMediaFileStore>>();
80+
var inputStream = new MemoryStream();
81+
var stream1 = new MemoryStream();
82+
var stream2 = new MemoryStream();
83+
84+
var handler1 = new Mock<IMediaCreatingEventHandler>();
85+
var handler2 = new Mock<IMediaCreatingEventHandler>();
86+
87+
handler1
88+
.Setup(x => x.MediaCreatingAsync(It.IsAny<MediaCreatingContext>(), inputStream))
89+
.ReturnsAsync(stream1);
90+
handler2
91+
.Setup(x => x.MediaCreatingAsync(It.IsAny<MediaCreatingContext>(), stream1))
92+
.ReturnsAsync(stream2);
93+
94+
fileStoreMock
95+
.Setup(x => x.CreateFileFromStreamAsync("test.txt", stream2, false))
96+
.ReturnsAsync("result");
97+
98+
var store = new DefaultMediaFileStore(
99+
fileStoreMock.Object,
100+
"",
101+
"",
102+
new List<IMediaEventHandler>(),
103+
new[] { handler1.Object, handler2.Object },
104+
loggerMock.Object);
105+
106+
// Act
107+
var result = await store.CreateFileFromStreamAsync("test.txt", inputStream);
108+
109+
// Assert
110+
Assert.Equal("result", result);
111+
handler1.Verify(x => x.MediaCreatingAsync(It.IsAny<MediaCreatingContext>(), inputStream), Times.Once);
112+
handler2.Verify(x => x.MediaCreatingAsync(It.IsAny<MediaCreatingContext>(), stream1), Times.Once);
113+
fileStoreMock.Verify(x => x.CreateFileFromStreamAsync("test.txt", stream2, false), Times.Once);
114+
}
115+
}

0 commit comments

Comments
 (0)