Skip to content

Commit

Permalink
New upload and download API (#106)
Browse files Browse the repository at this point in the history
* New API interface (2 versions for discussion).

* Finalize upload API.

* Fix default implementation of the upload API.

* Upload stream and text.

* UploadResult class.

* refactor

* Upload unit and integration tests.

* New abstraction ContentRequest. UploadRequest is not ContentRequest.

* Refactor.

* Low level API for downloading streams.

* Add StreamProperties to the callback of the download method.

* Fix tests

* High level downloading API.

* DownloadRequest.MediaSrc

* Documentation.

* Add Obsolete attributes.

* Remove duplicated retriers.

* Small docs changes.

---------

Co-authored-by: tusmester <[email protected]>
  • Loading branch information
kavics and tusmester authored Apr 25, 2023
1 parent 2e14f8d commit 7d832cc
Show file tree
Hide file tree
Showing 25 changed files with 2,798 additions and 1,254 deletions.
283 changes: 283 additions & 0 deletions src/SenseNet.Client.IntegrationTests/UploadDownloadTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
using Microsoft.Extensions.Logging;
using SenseNet.Extensions.DependencyInjection;
using SenseNet.Testing;

namespace SenseNet.Client.IntegrationTests;

[TestClass]
public class UploadDownloadTests : IntegrationTestBase
{
private readonly CancellationToken _cancel = new CancellationTokenSource(TimeSpan.FromMinutes(5)).Token;

private async Task<Content> EnsureUploadFolder(IRepository repository)
{
var uploadRootPath = "/Root/UploadTests";
var uploadFolder = await repository.LoadContentAsync(uploadRootPath, _cancel).ConfigureAwait(false);
if (uploadFolder == null)
{
uploadFolder = repository.CreateContent("/Root", "SystemFolder", "UploadTests");
await uploadFolder.SaveAsync(_cancel).ConfigureAwait(false);
}

return uploadFolder;
}

private Task GetStreamResponseAsync(IRepository repository, int contentId, Func<HttpResponseMessage, CancellationToken, Task> responseProcessor, CancellationToken cancel)
{
return GetStreamResponseAsync(repository, contentId, null, null, responseProcessor, cancel);
}
private async Task GetStreamResponseAsync(IRepository repository, int contentId, string? version, string? propertyName, Func<HttpResponseMessage, CancellationToken, Task> responseProcessor, CancellationToken cancel)
{
var url = $"/binaryhandler.ashx?nodeid={contentId}&propertyname={propertyName ?? "Binary"}";
if (!string.IsNullOrEmpty(version))
url += "&version=" + version;

await repository.ProcessWebResponseAsync(url, HttpMethod.Get, null, null, responseProcessor, cancel)
.ConfigureAwait(false);
}

private async Task<string?> DownloadAsString(IRepository repository, int contentId, CancellationToken cancel)
{
string? downloadedFileContent = null;
await GetStreamResponseAsync(repository, contentId, async (response, cancellationToken) =>
{
if (response == null)
return;
using (var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false))
using (var reader = new StreamReader(stream))
downloadedFileContent = await reader.ReadToEndAsync().ConfigureAwait(false);
}, cancel);
return downloadedFileContent;
}

/* =============================================================================== UPLOAD */

[TestMethod]
public async Task IT_Upload_ByPath()
{
var fileContent = "Lorem ipsum dolor sit amet...";
var repository = await GetRepositoryCollection().GetRepositoryAsync("local", _cancel).ConfigureAwait(false);
var uploadFolder = await EnsureUploadFolder(repository).ConfigureAwait(false);
var uploadRootPath = uploadFolder.Path;
var fileName = Guid.NewGuid() + ".txt";

// ACTION
UploadResult uploadedContent;
await using (var uploadStream = Tools.GenerateStreamFromString(fileContent))
{
var request = new UploadRequest {ParentPath = uploadRootPath, ContentName = fileName };
uploadedContent = await repository.UploadAsync(request, uploadStream, _cancel).ConfigureAwait(false);
}

// ASSERT
var downloadedFileContent = await DownloadAsString(repository, uploadedContent.Id, _cancel)
.ConfigureAwait(false);
Assert.AreEqual(fileContent, downloadedFileContent);
}
[TestMethod]
public async Task IT_Upload_ById()
{
var fileContent = "Lorem ipsum dolor sit amet...";
var repository = await GetRepositoryCollection().GetRepositoryAsync("local", _cancel).ConfigureAwait(false);
var uploadFolder = await EnsureUploadFolder(repository).ConfigureAwait(false);
var uploadRootId = uploadFolder.Id;
var fileName = Guid.NewGuid() + ".txt";

// ACT
UploadResult uploadedContent;
await using (var uploadStream = Tools.GenerateStreamFromString(fileContent))
{
var request = new UploadRequest { ParentId = uploadRootId, ContentName = fileName };
uploadedContent = await repository.UploadAsync(request, uploadStream, _cancel).ConfigureAwait(false);
}

// ASSERT
var downloadedFileContent = await DownloadAsString(repository, uploadedContent.Id, _cancel)
.ConfigureAwait(false);
Assert.AreEqual(fileContent, downloadedFileContent);
}

[TestMethod]
public async Task IT_Upload_ChunksAndProgress()
{
var fileContent = "111111111122222222223333333333444";
var repository = await GetRepositoryCollection().GetRepositoryAsync("local", _cancel).ConfigureAwait(false);
var uploadFolder = await EnsureUploadFolder(repository).ConfigureAwait(false);
var uploadRootPath = uploadFolder.Path;
var fileName = Guid.NewGuid() + ".txt";

UploadResult uploadedContent;
var progressValues = new List<int>();
using (new Swindler<int>(
hack: 10,
getter: () => ClientContext.Current.ChunkSizeInBytes,
setter: value => ClientContext.Current.ChunkSizeInBytes = value))
{
// ACTION
await using (var uploadStream = Tools.GenerateStreamFromString(fileContent))
{
var request = new UploadRequest { ParentPath = uploadRootPath, ContentName = fileName };
uploadedContent = await repository.UploadAsync(request, uploadStream,
progress => { progressValues.Add(progress); }, _cancel).ConfigureAwait(false);
}

}

// ASSERT
var downloadedFileContent = await DownloadAsString(repository, uploadedContent.Id, _cancel)
.ConfigureAwait(false);
Assert.AreEqual(fileContent, downloadedFileContent);
var actualProgress = string.Join(",", progressValues.Select(x => x.ToString()));
Assert.AreEqual("10,20,30,33", actualProgress);
}

[TestMethod]
public async Task IT_Upload_Text_ByPath()
{
var fileContent = "Lorem ipsum dolor sit amet...";
var repository = await GetRepositoryCollection().GetRepositoryAsync("local", _cancel).ConfigureAwait(false);
var uploadFolder = await EnsureUploadFolder(repository).ConfigureAwait(false);
var uploadRootPath = uploadFolder.Path;
var fileName = Guid.NewGuid() + ".txt";

// ACT
var request = new UploadRequest { ParentPath = uploadRootPath, ContentName = fileName };
var uploadedContent = await repository.UploadAsync(request, fileContent, _cancel).ConfigureAwait(false);

// ASSERT
var downloadedFileContent = await DownloadAsString(repository, uploadedContent.Id, _cancel)
.ConfigureAwait(false);
Assert.AreEqual(fileContent, downloadedFileContent);
}
[TestMethod]
public async Task IT_Upload_Text_ById()
{
var fileContent = "Lorem ipsum dolor sit amet...";
var repository = await GetRepositoryCollection().GetRepositoryAsync("local", _cancel).ConfigureAwait(false);
var uploadFolder = await EnsureUploadFolder(repository).ConfigureAwait(false);
var uploadRootId = uploadFolder.Id;
var fileName = Guid.NewGuid() + ".txt";

// ACTION
var request = new UploadRequest { ParentId = uploadRootId, ContentName = fileName };
var uploadedContent = await repository.UploadAsync(request, fileContent, _cancel).ConfigureAwait(false);

// ASSERT
var downloadedFileContent = await DownloadAsString(repository, uploadedContent.Id, _cancel)
.ConfigureAwait(false);
Assert.AreEqual(fileContent, downloadedFileContent);
}

/* =============================================================================== DOWNLOAD */

#region nested content classes

public class ContentType : Content
{
public ContentType(IRestCaller restCaller, ILogger<Content> logger) : base(restCaller, logger) { }

public Binary? Binary { get; set; }
}

#endregion

[TestMethod]
public async Task IT_Download_HighLevel()
{
var cancel = new CancellationTokenSource(TimeSpan.FromMinutes(10)).Token;
var repository = await GetRepositoryCollection(services =>
{
services.RegisterGlobalContentType<ContentType>();
}).GetRepositoryAsync("local", cancel).ConfigureAwait(false);
var content = await repository.LoadContentAsync<ContentType>(
new LoadContentRequest {Path = "/Root/System/Schema/ContentTypes/GenericContent/File"}, cancel)
.ConfigureAwait(false);
if(content?.Binary == null)
Assert.Fail("Content or Binary not found.");
string? text = null;
StreamProperties? properties = null;
var streamLength = 0L;

// ACT
await content.Binary.DownloadAsync(async (stream, props) =>
{
properties = props;
streamLength = stream.Length;
using var reader = new StreamReader(stream);
text = await reader.ReadToEndAsync().ConfigureAwait(false);
}, cancel).ConfigureAwait(false);

// ASSERT
Assert.IsNotNull(text);
Assert.IsTrue(text.Contains("<ContentType name=\"File\""));
Assert.IsNotNull(properties);
Assert.AreEqual("text/xml", properties.MediaType);
Assert.AreEqual("File.ContentType", properties.FileName);
Assert.AreEqual(streamLength, properties.ContentLength);
}

[TestMethod]
public async Task IT_Download_LowLevel_ById()
{
var cancel = new CancellationTokenSource(TimeSpan.FromMinutes(10)).Token;
var repository = await GetRepositoryCollection().GetRepositoryAsync("local", cancel).ConfigureAwait(false);
var content = await repository.LoadContentAsync(
new LoadContentRequest
{
Path = "/Root/System/Schema/ContentTypes/GenericContent/File",
Select = new[] {"Id"}
}, cancel)
.ConfigureAwait(false);
var contentId = content.Id;

// ACT
string? text = null;
StreamProperties? properties = null;
long streamLength = 0L;
var request = new DownloadRequest { ContentId = contentId };
await repository.DownloadAsync(request, async (stream, props) =>
{
properties = props;
streamLength = stream.Length;
using var reader = new StreamReader(stream);
text = await reader.ReadToEndAsync().ConfigureAwait(false);
}, cancel).ConfigureAwait(false);

// ASSERT
Assert.IsNotNull(text);
Assert.IsTrue(text.Contains("<ContentType name=\"File\""));
Assert.IsNotNull(properties);
Assert.AreEqual("text/xml", properties.MediaType);
Assert.AreEqual("File.ContentType", properties.FileName);
Assert.AreEqual(streamLength, properties.ContentLength);
}

[TestMethod]
public async Task IT_Download_LowLevel_ByPath()
{
var cancel = new CancellationTokenSource(TimeSpan.FromMinutes(10)).Token;
var repository = await GetRepositoryCollection().GetRepositoryAsync("local", cancel).ConfigureAwait(false);

// ACT
string? text = null;
StreamProperties? properties = null;
long streamLength = 0L;
var request = new DownloadRequest { Path = "/Root/System/Schema/ContentTypes/GenericContent/File" };
await repository.DownloadAsync(request, async (stream, props) =>
{
properties = props;
streamLength = stream.Length;
using var reader = new StreamReader(stream);
text = await reader.ReadToEndAsync().ConfigureAwait(false);
}, cancel).ConfigureAwait(false);

// ASSERT
Assert.IsNotNull(text);
Assert.IsTrue(text.Contains("<ContentType name=\"File\""));
Assert.IsNotNull(properties);
Assert.AreEqual("text/xml", properties.MediaType);
Assert.AreEqual("File.ContentType", properties.FileName);
Assert.AreEqual(streamLength, properties.ContentLength);
}

}
49 changes: 0 additions & 49 deletions src/SenseNet.Client.Tests/UnitTests/ContentLoadingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,6 @@
using Microsoft.Extensions.DependencyInjection;
using SenseNet.Extensions.DependencyInjection;
using static SenseNet.Client.Tests.UnitTests.RepositoryTests;
using Microsoft.Extensions.Configuration;
using AngleSharp.Dom;
using System.Net.Mime;
using AngleSharp.Io;
using Microsoft.IdentityModel.Tokens;
using NSubstitute.Core;
using SenseNet.Diagnostics;
using SenseNet.Tools;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using Newtonsoft.Json.Linq;
// ReSharper disable InconsistentNaming
// ReSharper disable ClassNeverInstantiated.Local
Expand Down Expand Up @@ -1224,38 +1209,4 @@ public async Task LoadContent_T_Error_UnknownType()
Assert.IsNotNull(requestedUri);
Assert.AreEqual("/OData.svc/content(999543)?metadata=no&$select=Id,Type,Name", requestedUri.PathAndQuery);
}

/* ====================================================================== TOOLS */

private static IRepositoryCollection GetRepositoryCollection(Action<IServiceCollection>? addServices = null)
{
var services = new ServiceCollection();

services.AddLogging(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Trace);
});

var config = new ConfigurationBuilder()
.AddJsonFile("appSettings.json", optional: true)
.AddUserSecrets<RepositoryTests>()
.Build();

services
.AddSingleton<IConfiguration>(config)
.AddSenseNetClient()
//.AddSingleton<ITokenProvider, TestTokenProvider>()
//.AddSingleton<ITokenStore, TokenStore>()
.ConfigureSenseNetRepository("local", repositoryOptions =>
{
// set test url and authentication in user secret
config.GetSection("sensenet:repository").Bind(repositoryOptions);
});

addServices?.Invoke(services);

var provider = services.BuildServiceProvider();
return provider.GetRequiredService<IRepositoryCollection>();
}
}
39 changes: 1 addition & 38 deletions src/SenseNet.Client.Tests/UnitTests/ContentSavingTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System.Security.Cryptography.X509Certificates;
using AngleSharp.Common;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
Expand Down Expand Up @@ -984,38 +981,4 @@ private async Task TestStronglyTypedReferencesError(string fieldName, Action<Tes
$"FieldName: '{fieldName}'.", e.InnerException?.Message);
}
}

/* ====================================================================== TOOLS */

private static IRepositoryCollection GetRepositoryCollection(Action<IServiceCollection>? addServices = null)
{
var services = new ServiceCollection();

services.AddLogging(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Trace);
});

var config = new ConfigurationBuilder()
.AddJsonFile("appSettings.json", optional: true)
.AddUserSecrets<RepositoryTests>()
.Build();

services
.AddSingleton<IConfiguration>(config)
.AddSenseNetClient()
//.AddSingleton<ITokenProvider, TestTokenProvider>()
//.AddSingleton<ITokenStore, TokenStore>()
.ConfigureSenseNetRepository("local", repositoryOptions =>
{
// set test url and authentication in user secret
config.GetSection("sensenet:repository").Bind(repositoryOptions);
});

addServices?.Invoke(services);

var provider = services.BuildServiceProvider();
return provider.GetRequiredService<IRepositoryCollection>();
}
}
Loading

0 comments on commit 7d832cc

Please sign in to comment.