Repository navigation
Expand file tree
/
Copy pathFileContextPdfSource.cs
More file actions
105 lines (96 loc) · 4.06 KB
/
Copy pathFileContextPdfSource.cs
File metadata and controls
105 lines (96 loc) · 4.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
using System.Buffers;
namespace ManagedCode.FileContext.Pdf;
/// <summary>Owns a bounded local PDF source; cloud inputs are staged before synchronous random reads.</summary>
public sealed class FileContextPdfSource : IAsyncDisposable
{
private const string TemporaryFilePrefix = "filecontext-pdf-";
private FileContextPdfSource(Stream stream) => Stream = stream;
public Stream Stream { get; }
/// <summary>Takes ownership of the input, including on failure. Dispose the result after parsing.</summary>
public static async Task<FileContextPdfSource> OpenAsync(Stream source, FileContextOptions options,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(options);
Stream? staged = null;
try
{
options.Validate();
cancellationToken.ThrowIfCancellationRequested();
if (source.CanSeek)
{
Validate(source, options);
}
else if (!source.CanRead)
{
throw new ArgumentException("The PDF source must be readable.", nameof(source));
}
if (options.PdfSourceStagingMode == FileContextPdfSourceStagingMode.Automatic
&& source.CanSeek && source is FileStream or MemoryStream)
{
return new FileContextPdfSource(source);
}
staged = CreateTemporaryFile(options);
await CopyAsync(source, staged, options, cancellationToken).ConfigureAwait(false);
staged.Position = 0;
await source.DisposeAsync().ConfigureAwait(false);
return new FileContextPdfSource(staged);
}
catch
{
try { if (staged is not null) { await staged.DisposeAsync().ConfigureAwait(false); } }
finally { await source.DisposeAsync().ConfigureAwait(false); }
throw;
}
}
internal static void Validate(Stream source, FileContextOptions options)
{
if (!source.CanRead || !source.CanSeek)
{
throw new ArgumentException("PDF parsing requires a readable seekable stream. Stage it with FileContextPdfSource.OpenAsync.", nameof(source));
}
options.Validate();
if (source.Length > options.MaximumPdfReadBytes)
{
throw new IOException("The PDF exceeds the read limit.");
}
source.Position = 0;
}
private static async Task CopyAsync(Stream source, Stream staged, FileContextOptions options,
CancellationToken cancellationToken)
{
var buffer = ArrayPool<byte>.Shared.Rent(options.PdfSourceBufferBytes);
try
{
int read;
while ((read = await source.ReadAsync(buffer.AsMemory(0, options.PdfSourceBufferBytes), cancellationToken)
.ConfigureAwait(false)) > 0)
{
if (staged.Length + read > options.MaximumPdfReadBytes)
{
throw new IOException("The PDF exceeds the read limit.");
}
await staged.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
}
}
finally { ArrayPool<byte>.Shared.Return(buffer, clearArray: true); }
}
private static FileStream CreateTemporaryFile(FileContextOptions options)
{
var fileOptions = new FileStreamOptions
{
Mode = FileMode.CreateNew,
Access = FileAccess.ReadWrite,
Share = FileShare.None,
BufferSize = options.PdfSourceBufferBytes,
Options = FileOptions.Asynchronous | FileOptions.DeleteOnClose
};
if (!OperatingSystem.IsWindows())
{
fileOptions.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite;
}
return new FileStream(Path.Combine(options.PdfTemporaryDirectory ?? Path.GetTempPath(),
TemporaryFilePrefix + Guid.NewGuid().ToString("N")), fileOptions);
}
public ValueTask DisposeAsync() => Stream.DisposeAsync();
}