Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add document creation and tasks, fix timestamps #151

Merged
merged 2 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@
</PackageReference>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="IsExternalInit">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Nullable">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)stylecop.json">
<Link>stylecop.json</Link>
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556"/>
<PackageVersion Include="Testcontainers.Redis" Version="3.4.0"/>
<PackageVersion Include="VMelnalksnis.Testcontainers.Paperless" Version="0.2.0"/>
<PackageVersion Include="xunit" Version="2.6.5"/>
<PackageVersion Include="xunit" Version="2.6.6"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5"/>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using VMelnalksnis.PaperlessDotNet.Correspondents;
using VMelnalksnis.PaperlessDotNet.Documents;
using VMelnalksnis.PaperlessDotNet.Serialization;
using VMelnalksnis.PaperlessDotNet.Tasks;

#if NET6_0_OR_GREATER
using System.Net.Mime;
Expand Down Expand Up @@ -53,19 +54,26 @@ public static IHttpClientBuilder AddPaperlessDotNet(

return serviceCollection
.AddSingleton<PaperlessJsonSerializerOptions>()
.AddTransient<IPaperlessClient, PaperlessClient>()
.AddTransient<ICorrespondentClient, CorrespondentClient>(provider =>
.AddScoped<IPaperlessClient, PaperlessClient>()
.AddScoped<ITaskClient, TaskClient>(provider =>
{
var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient(PaperlessOptions.Name);
var options = provider.GetRequiredService<PaperlessJsonSerializerOptions>();
return new(httpClient, options);
})
.AddTransient<IDocumentClient, DocumentClient>(provider =>
.AddScoped<ICorrespondentClient, CorrespondentClient>(provider =>
{
var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient(PaperlessOptions.Name);
var options = provider.GetRequiredService<PaperlessJsonSerializerOptions>();
return new(httpClient, options);
})
.AddScoped<IDocumentClient, DocumentClient>(provider =>
{
var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient(PaperlessOptions.Name);
var options = provider.GetRequiredService<PaperlessJsonSerializerOptions>();
var taskClient = provider.GetRequiredService<ITaskClient>();
return new(httpClient, options, taskClient);
})
.AddHttpClient(PaperlessOptions.Name, (provider, client) =>
{
var options = provider.GetRequiredService<IOptionsMonitor<PaperlessOptions>>().CurrentValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,4 @@
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="IsExternalInit">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Nullable">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ public sealed class Correspondent

/// <summary>Gets or sets the instant when the last document with the correspondent was created.</summary>
[JsonPropertyName("last_correspondence")]
public Instant? LastCorrespondence { get; set; }
public OffsetDateTime? LastCorrespondence { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,25 +66,14 @@ public async Task<Correspondent> Create(CorrespondentCreation correspondent)
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/api/correspondents/", content).ConfigureAwait(false);

await EnsureSuccessStatusCode(response).ConfigureAwait(false);
await response.EnsureSuccessStatusCodeAsync().ConfigureAwait(false);
return (await response.Content.ReadFromJsonAsync(_context.Correspondent).ConfigureAwait(false))!;
}

/// <inheritdoc />
public async Task Delete(int id)
{
var response = await _httpClient.DeleteAsync($"/api/correspondents/{id}/").ConfigureAwait(false);
await EnsureSuccessStatusCode(response).ConfigureAwait(false);
}

private static async Task EnsureSuccessStatusCode(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
return;
}

var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new HttpRequestException(message);
await response.EnsureSuccessStatusCodeAsync().ConfigureAwait(false);
}
}
16 changes: 8 additions & 8 deletions source/VMelnalksnis.PaperlessDotNet/Documents/Document.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ public sealed class Document

/// <summary>Gets or sets the archive serial number.</summary>
[JsonPropertyName("archive_serial_number")]
public int? ArchiveSerialNumber { get; set; }
public uint? ArchiveSerialNumber { get; set; }

/// <summary>Gets or sets the correspondent id.</summary>
/// <summary>Gets or sets the <see cref="Correspondents.Correspondent"/> id.</summary>
[JsonPropertyName("correspondent")]
public int? CorrespondentId { get; set; }

Expand All @@ -32,14 +32,14 @@ public sealed class Document
[JsonPropertyName("original_file_name")]
public string OriginalFileName { get; set; } = null!;

/// <summary>Gets or sets the instant at which the document was added to paperless.</summary>
public Instant Added { get; set; }
/// <summary>Gets or sets the datetime when the document was added to paperless.</summary>
public OffsetDateTime Added { get; set; }

/// <summary>Gets or sets the instant at which the document was last modified at.</summary>
public Instant Modified { get; set; }
/// <summary>Gets or sets the datetime when the document was last modified at.</summary>
public OffsetDateTime Modified { get; set; }

/// <summary>Gets or sets the instant at which the document was created at.</summary>
public Instant Created { get; set; }
/// <summary>Gets or sets the datetime when the document was created at.</summary>
public OffsetDateTime Created { get; set; }

/// <summary>Gets or sets ids of the tags assigned to the document.</summary>
[JsonPropertyName("tags")]
Expand Down
87 changes: 86 additions & 1 deletion source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,39 @@
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

using NodaTime.Text;

using VMelnalksnis.PaperlessDotNet.Serialization;
using VMelnalksnis.PaperlessDotNet.Tasks;

namespace VMelnalksnis.PaperlessDotNet.Documents;

/// <inheritdoc />
public sealed class DocumentClient : IDocumentClient
{
private static readonly Version _documentIdVersion = new(1, 9, 2);

private readonly HttpClient _httpClient;
private readonly PaperlessJsonSerializerContext _context;
private readonly ITaskClient _taskClient;

/// <summary>Initializes a new instance of the <see cref="DocumentClient"/> class.</summary>
/// <param name="httpClient">Http client configured for making requests to the Paperless API.</param>
/// <param name="serializerOptions">Paperless specific instance of <see cref="JsonSerializerOptions"/>.</param>
public DocumentClient(HttpClient httpClient, PaperlessJsonSerializerOptions serializerOptions)
/// <param name="taskClient">Paperless task API client.</param>
public DocumentClient(HttpClient httpClient, PaperlessJsonSerializerOptions serializerOptions, ITaskClient taskClient)
{
_httpClient = httpClient;
_taskClient = taskClient;
_context = serializerOptions.Context;
}

Expand Down Expand Up @@ -54,4 +64,79 @@
_context.Document,
cancellationToken);
}

/// <inheritdoc />
public async Task<DocumentCreationResult> Create(DocumentCreation document)
{
var content = new MultipartFormDataContent();
content.Add(new StreamContent(document.Document), "document", document.FileName);

if (document.Title is { } title)
{
content.Add(new StringContent(title), "title");
}

if (document.Created is { } created)
{
content.Add(new StringContent(InstantPattern.General.Format(created)), "created");
}

if (document.CorrespondentId is { } correspondent)
{
content.Add(new StringContent(correspondent.ToString()), "correspondent");
}

if (document.DocumentTypeId is { } documentType)
{
content.Add(new StringContent(documentType.ToString()), "document_type");

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

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L91

Added line #L91 was not covered by tests
}

if (document.StoragePathId is { } storagePath)
{
content.Add(new StringContent(storagePath.ToString()), "storage_path");

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

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L96

Added line #L96 was not covered by tests
}

foreach (var tag in document.TagIds)
{
content.Add(new StringContent(tag.ToString()), "tags");

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

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L101

Added line #L101 was not covered by tests
}

if (document.ArchiveSerialNumber is { } archiveSerialNumber)
{
content.Add(new StringContent(archiveSerialNumber.ToString()), "archive_serial_number");
}

var response = await _httpClient.PostAsync("/api/documents/post_document/", content).ConfigureAwait(false);
await response.EnsureSuccessStatusCodeAsync().ConfigureAwait(false);

// Until v1.9.2 paperless did not return the document import task id,
// so it is not possible to get the document id
var versionHeader = response.Headers.GetValues("x-version").SingleOrDefault();
if (versionHeader is null || !Version.TryParse(versionHeader, out var version) || version <= _documentIdVersion)
{
return new ImportStarted();

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

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L117

Added line #L117 was not covered by tests
}

var id = await response.Content.ReadFromJsonAsync(_context.Guid).ConfigureAwait(false);
var task = await _taskClient.Get(id).ConfigureAwait(false);

while (task is not null && !task.Status.IsCompleted)
{
await Task.Delay(100).ConfigureAwait(false);
task = await _taskClient.Get(id).ConfigureAwait(false);
}

return task switch
{
null => new ImportFailed($"Could not find the import task by the given id {id}"),

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

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L131

Added line #L131 was not covered by tests

_ when task.RelatedDocument is { } documentId => new DocumentCreated(documentId),

_ when task.Status == PaperlessTaskStatus.Success => new ImportFailed(
$"Task status is {PaperlessTaskStatus.Success.Name}, but document id was not given"),

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

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L136

Added line #L136 was not covered by tests
_ when task.Status == PaperlessTaskStatus.Failure => new ImportFailed(task.Result),

_ => throw new ArgumentOutOfRangeException(nameof(task.Status), task.Status, "Unexpected task result"),

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

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L139

Added line #L139 was not covered by tests
};
}
}
54 changes: 54 additions & 0 deletions source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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.Diagnostics.CodeAnalysis;
using System.IO;

using NodaTime;

namespace VMelnalksnis.PaperlessDotNet.Documents;

/// <summary>Information needed to create a new <see cref="Document"/>.</summary>
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global", Justification = "Required endpoints for testing not implemented")]
public sealed class DocumentCreation
{
/// <summary>Initializes a new instance of the <see cref="DocumentCreation"/> class.</summary>
/// <param name="document">The document content.</param>
/// <param name="fileName">The name of the file.</param>
public DocumentCreation(Stream document, string fileName)
{
Document = document;
FileName = fileName;

TagIds = Array.Empty<int>();
}

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

/// <inheritdoc cref="Document.OriginalFileName"/>
public string FileName { get; }

/// <inheritdoc cref="Document.Created"/>
public Instant? Created { get; init; }

/// <inheritdoc cref="Document.Title"/>
public string? Title { get; init; }

/// <inheritdoc cref="Document.CorrespondentId"/>
public int? CorrespondentId { get; init; }

/// <inheritdoc cref="Document.DocumentTypeId"/>
public int? DocumentTypeId { get; init; }

/// <summary>Gets the id of the storage path.</summary>
public int? StoragePathId { get; init; }

/// <inheritdoc cref="Document.TagIds"/>
public int[] TagIds { get; init; }

/// <inheritdoc cref="Document.ArchivedFileName"/>
public uint? ArchiveSerialNumber { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.

namespace VMelnalksnis.PaperlessDotNet.Documents;
#pragma warning disable SA1402

/// <summary>Base class for possible results of creating a new document.</summary>
/// <seealso cref="ImportStarted"/>
/// <seealso cref="DocumentCreated"/>
/// <seealso cref="ImportFailed"/>
public abstract class DocumentCreationResult;

/// <summary>Document was successfully created.</summary>
/// <param name="id">The id of the created document.</param>
public sealed class DocumentCreated(int id) : DocumentCreationResult
{
/// <summary>Gets the id of the created document.</summary>
public int Id { get; } = id;
}

/// <summary>Document was successfully submitted and import was started.</summary>
/// <remarks>This is only returned for version below or equal to 1.9.2.</remarks>
public sealed class ImportStarted : DocumentCreationResult;

/// <summary>Document import process failed.</summary>
/// <param name="result">The result message returned by paperless.</param>
public sealed class ImportFailed(string? result) : DocumentCreationResult

Check warning on line 28 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreationResult.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreationResult.cs#L28

Added line #L28 was not covered by tests
{
/// <summary>Gets the result message returned by paperless.</summary>
public string? Result { get; } = result;

Check warning on line 31 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreationResult.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreationResult.cs#L31

Added line #L31 was not covered by tests
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,9 @@ public interface IDocumentClient
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>The document with the specified id if it exists; otherwise <see langword="null"/>.</returns>
Task<Document?> Get(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>
Task<DocumentCreationResult> Create(DocumentCreation document);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;

namespace VMelnalksnis.PaperlessDotNet.Serialization;

Expand Down Expand Up @@ -37,4 +38,15 @@
next = paginatedList.Next?.PathAndQuery;
}
}

internal static async Task EnsureSuccessStatusCodeAsync(this HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
return;
}

var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new HttpRequestException(message);

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

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Serialization/HttpClientExtensions.cs#L49-L50

Added lines #L49 - L50 were not covered by tests
}
}
Loading