Skip to content

Commit

Permalink
feat: Add support to download documents
Browse files Browse the repository at this point in the history
  • Loading branch information
olvr-me committed Nov 30, 2024
1 parent d0935f3 commit 846fbd8
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 0 deletions.
49 changes: 49 additions & 0 deletions source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System;
using System.Collections.Generic;
using System.IO;

Check failure on line 7 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs

View workflow job for this annotation

GitHub Actions / Resharper

RedundantUsingDirective

Using directive is not required by the code and can be safely removed
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
Expand Down Expand Up @@ -109,6 +110,36 @@ public async IAsyncEnumerable<Document<TFields>> GetAll<TFields>(int pageSize, [
return await GetCore<Document<TFields>>(id, cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc />
public async Task<DocumentContent> Download(int id, CancellationToken cancellationToken = default)
{
return await DownloadContentCore(Routes.Documents.DownloadUri(id), cancellationToken);
}

/// <inheritdoc />
public async Task<DocumentContent> DownloadOriginal(int id, CancellationToken cancellationToken = default)
{
return await DownloadContentCore(Routes.Documents.DownloadOriginalUri(id), cancellationToken);
}

/// <inheritdoc />
public async Task<DocumentContent> DownloadPreview(int id, CancellationToken cancellationToken = default)
{
return await DownloadContentCore(Routes.Documents.DownloadPreview(id), cancellationToken);
}

/// <inheritdoc />
public async Task<DocumentContent> DownloadOriginalPreview(int id, CancellationToken cancellationToken = default)
{
return await DownloadContentCore(Routes.Documents.DownloadOriginalPreview(id), cancellationToken);
}

Check warning on line 135 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L134-L135

Added lines #L134 - L135 were not covered by tests

/// <inheritdoc />
public async Task<DocumentContent> DownloadThumbnail(int id, CancellationToken cancellationToken = default)
{
return await DownloadContentCore(Routes.Documents.DownloadThumbnail(id), cancellationToken);
}

/// <inheritdoc />
public async Task<DocumentCreationResult> Create(DocumentCreation document)
{
Expand Down Expand Up @@ -268,6 +299,24 @@ private async Task<TDocument> UpdateCore<TDocument, TUpdate>(int id, TUpdate upd
return (await response.Content.ReadFromJsonAsync(_options.GetTypeInfo<TDocument>()).ConfigureAwait(false))!;
}

private async Task<DocumentContent> DownloadContentCore(Uri requestUri, CancellationToken cancellationToken = default)
{
var response = await _httpClient.GetAsync(requestUri, cancellationToken).ConfigureAwait(false);

await response.EnsureSuccessStatusCodeAsync().ConfigureAwait(false);

var headers = response.Content.Headers;

return new(
#if NET6_0_OR_GREATER
await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false),
#else
await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
#endif
headers.ContentDisposition,
headers.ContentType!);
}

private async IAsyncEnumerable<CustomField> GetCustomFieldsCore(Uri requestUri, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var fields = _httpClient.GetPaginated(requestUri, _options.GetTypeInfo<PaginatedList<CustomField>>(), cancellationToken);
Expand Down
44 changes: 44 additions & 0 deletions source/VMelnalksnis.PaperlessDotNet/Documents/DocumentContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.

using System;
using System.IO;
using System.Net.Http.Headers;

namespace VMelnalksnis.PaperlessDotNet.Documents;

/// <summary>
/// The downloaded content of a document.
/// </summary>
public sealed class DocumentContent : IDisposable
{
/// <summary>Initializes a new instance of the <see cref="DocumentContent"/> class.</summary>
/// <param name="content">The document content.</param>
/// <param name="contentDisposition">The ContentDisposition header of the download.</param>
/// <param name="mediaTypeHeaderValue">The MediaType header of the download.</param>
public DocumentContent(Stream content, ContentDispositionHeaderValue? contentDisposition, MediaTypeHeaderValue mediaTypeHeaderValue)
{
Content = content;
ContentDisposition = contentDisposition;
MediaTypeHeaderValue = mediaTypeHeaderValue;
}

/// <summary>
/// Gets the document content.
/// </summary>
public Stream Content { get; }

/// <summary>
/// Gets the ContentDisposition header of the download.
/// </summary>
public ContentDispositionHeaderValue? ContentDisposition { get; }

/// <summary>
/// Gets the MediaType header of the download.
/// </summary>
public MediaTypeHeaderValue MediaTypeHeaderValue { get; }

/// <inheritdoc />
public void Dispose() => Content.Dispose();

Check warning on line 43 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentContent.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentContent.cs#L43

Added line #L43 was not covered by tests
}
31 changes: 31 additions & 0 deletions source/VMelnalksnis.PaperlessDotNet/Documents/IDocumentClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.IO;

Check failure on line 6 in source/VMelnalksnis.PaperlessDotNet/Documents/IDocumentClient.cs

View workflow job for this annotation

GitHub Actions / Resharper

RedundantUsingDirective

Using directive is not required by the code and can be safely removed
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -48,6 +49,36 @@ public interface IDocumentClient
/// <returns>The document with the specified id if it exists; otherwise <see langword="null"/>.</returns>
Task<Document<TFields>?> Get<TFields>(int id, CancellationToken cancellationToken = default);

/// <summary>Downloads the archived file of the document.</summary>
/// <param name="id">The id of the document to download.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>The content of the document.</returns>
Task<DocumentContent> Download(int id, CancellationToken cancellationToken = default);

/// <summary>Downloads the original file of the document.</summary>
/// <param name="id">The id of the document to download.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>The content of the document.</returns>
Task<DocumentContent> DownloadOriginal(int id, CancellationToken cancellationToken = default);

/// <summary>Display the document inline, without downloading it.</summary>
/// <param name="id">The id of the document to download.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>The content of the document.</returns>
Task<DocumentContent> DownloadPreview(int id, CancellationToken cancellationToken = default);

/// <summary>Display the original document inline, without downloading it.</summary>
/// <param name="id">The id of the document to download.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>The content of the document.</returns>
Task<DocumentContent> DownloadOriginalPreview(int id, CancellationToken cancellationToken = default);

/// <summary>Download the PNG thumbnail of a document.</summary>
/// <param name="id">The id of the document to download.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>The content of the document.</returns>
Task<DocumentContent> DownloadThumbnail(int id, CancellationToken cancellationToken = default);

/// <summary>Creates a new document.</summary>
/// <param name="document">The document to create.</param>
/// <returns>Result of creating the document.</returns>
Expand Down
10 changes: 10 additions & 0 deletions source/VMelnalksnis.PaperlessDotNet/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ internal static class Documents

internal static Uri IdUri(int id) => new($"{_documents}{id}/", Relative);

internal static Uri DownloadUri(int id) => new($"{_documents}{id}/download/", Relative);

internal static Uri DownloadOriginalUri(int id) => new($"{_documents}{id}/download/?original=true", Relative);

internal static Uri DownloadPreview(int id) => new($"{_documents}{id}/preview/", Relative);

internal static Uri DownloadOriginalPreview(int id) => new($"{_documents}{id}/preview/?original=true", Relative);

Check warning on line 50 in source/VMelnalksnis.PaperlessDotNet/Routes.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Routes.cs#L50

Added line #L50 was not covered by tests

internal static Uri DownloadThumbnail(int id) => new($"{_documents}{id}/thumb/", Relative);

internal static Uri PagedUri(int pageSize) => new($"{_documents}?{_pageSize}={pageSize}", Relative);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

using NodaTime;

using NUnit.Framework.Internal;

Check failure on line 14 in tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/Documents/DocumentClientTests.cs

View workflow job for this annotation

GitHub Actions / Resharper

RedundantUsingDirective

Using directive is not required by the code and can be safely removed

using VMelnalksnis.PaperlessDotNet.Documents;
using VMelnalksnis.PaperlessDotNet.Tags;

Expand Down Expand Up @@ -110,6 +113,62 @@ await FluentActions
.WithMessage("Response status code does not indicate success: 404 (Not Found).");
}

[Test]
public async Task Download()
{
if (PaperlessVersion < new Version(2, 0))
{
Assert.Ignore($"Paperless v{PaperlessVersion} does not directly allow downloading documents.");
}

const string documentName = "Lorem Ipsum 3.txt";

await using var documentStream = typeof(DocumentClientTests).GetResource(documentName);
var documentCreation = new DocumentCreation(documentStream, documentName);

var createResult = await Client.Documents.Create(documentCreation);
var id = createResult.Should().BeOfType<DocumentCreated>().Subject.Id;

var expectedDocumentContent = await typeof(DocumentClientTests).ReadResource(documentName);
var expectedPartOfFileName = "Lorem Ipsum 3";

// Download
var documentDownload = await Client.Documents.Download(id);
documentDownload.MediaTypeHeaderValue.MediaType.Should().Be("text/plain");
documentDownload.ContentDisposition!.FileName.Should().Contain(expectedPartOfFileName);

var downloadContent = await ReadStreamContentAsString(documentDownload.Content);
downloadContent.Should().BeEquivalentTo(expectedDocumentContent);

// Download Original
var documentOriginalDownload = await Client.Documents.DownloadOriginal(id);
documentOriginalDownload.MediaTypeHeaderValue.MediaType.Should().Be("text/plain");
documentOriginalDownload.ContentDisposition!.FileName.Should().Contain(expectedPartOfFileName);

var downloadOriginalContent = await ReadStreamContentAsString(documentOriginalDownload.Content);
downloadOriginalContent.Should().BeEquivalentTo(expectedDocumentContent);

// Download Preview
var downloadPreview = await Client.Documents.DownloadPreview(id);
downloadPreview.MediaTypeHeaderValue.MediaType.Should().Be("text/plain");
downloadPreview.ContentDisposition!.FileName.Should().Contain(expectedPartOfFileName);

var downloadPreviewContent = await ReadStreamContentAsString(downloadPreview.Content);
downloadPreviewContent.Should().BeEquivalentTo(expectedDocumentContent);

// Download Preview Original
var downloadPreviewOriginal = await Client.Documents.DownloadPreview(id);
downloadPreviewOriginal.MediaTypeHeaderValue.MediaType.Should().Be("text/plain");
downloadPreviewOriginal.ContentDisposition!.FileName.Should().Contain(expectedPartOfFileName);

var downloadPreviewOrignalContent = await ReadStreamContentAsString(downloadPreviewOriginal.Content);
downloadPreviewOrignalContent.Should().BeEquivalentTo(expectedDocumentContent);

// Download thumbnail
var downloadThumbnail = await Client.Documents.DownloadThumbnail(id);
downloadThumbnail.MediaTypeHeaderValue.MediaType.Should().Be("image/webp");
}

[Test]
public async Task CustomFields()
{
Expand Down Expand Up @@ -171,4 +230,14 @@ public async Task CustomFields()
var documents = await Client.Documents.GetAll<CustomFields>().ToListAsync();
documents.Should().ContainSingle(d => d.Id == id).Which.Should().BeEquivalentTo(document);
}

private async Task<string> ReadStreamContentAsString(Stream stream)
{
await using (stream)
{
var reader = new StreamReader(stream);

return await reader.ReadToEndAsync();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi interdum libero at augue sagittis, ut aliquam arcu fringilla. Aenean elit nibh, blandit eu lacinia id, fermentum vitae sapien. Nunc efficitur cursus tempus. Phasellus consequat diam non ante pulvinar dapibus. Pellentesque ullamcorper nulla quis purus laoreet fringilla. Pellentesque vitae elit dolor. Nam elementum a leo non elementum. Cras non ante efficitur, molestie neque a, ultrices leo. Duis mollis ex fringilla elit lacinia semper. Nulla orci tellus, tristique sed iaculis non, sagittis a sem. Aenean scelerisque laoreet erat, a congue urna faucibus mollis.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<ItemGroup>
<EmbeddedResource Include="Documents\Lorem Ipsum.txt" />
<EmbeddedResource Include="Documents\Lorem Ipsum 2.txt" />
<EmbeddedResource Include="Documents\Lorem Ipsum 3.txt" />
</ItemGroup>

</Project>

0 comments on commit 846fbd8

Please sign in to comment.