-
Notifications
You must be signed in to change notification settings - Fork 2
/
AtlasFunctionsStorageManager.cs
132 lines (106 loc) · 4.13 KB
/
AtlasFunctionsStorageManager.cs
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
using System.Diagnostics.CodeAnalysis;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Realms.Sync;
// ReSharper disable ClassNeverInstantiated.Local
// ReSharper disable UnusedAutoPropertyAccessor.Local
namespace Realms.LFS.Functions;
/// <summary>
/// An implementation of the <see cref="RemoteStorageManager"/> that uploads data to a url supplied
/// by an Atlas Function.
/// </summary>
public class AtlasFunctionsStorageManager : RemoteStorageManager
{
private readonly string _function;
private readonly HttpClient _client;
private readonly User _user;
/// <summary>
/// Initializes a new instance of the <see cref="AtlasFunctionsStorageManager"/> class.
/// </summary>
/// <param name="config">The config of the Realm this file manager is tracking.</param>
/// <param name="function">The Atlas Function that will be called to obtain pre-signed urls.</param>
/// <param name="httpHandler">The http handler to be used when making the http requests.</param>
public AtlasFunctionsStorageManager(RealmConfigurationBase config, string function, HttpMessageHandler? httpHandler = null)
: base(config)
{
_function = function;
_user = config switch
{
SyncConfigurationBase syncConfig => syncConfig.User,
_ => throw new NotSupportedException("This manager only supports synchronized Realms")
};
_client = httpHandler == null ? new HttpClient() : new HttpClient(httpHandler);
}
/// <inheritdoc/>
protected override async Task DeleteFileCore(string id)
{
await CallSignFunction<DeleteResponse>(id, OperationType.Delete);
}
/// <inheritdoc/>
protected override async Task DownloadFileCore(string id, string file)
{
var response = await CallSignFunction<DownloadResponse>(id, OperationType.Download);
var url = response.Url;
var stream = await _client.GetStreamAsync(new Uri(url));
var fileStream = new FileStream(file, FileMode.Create);
await stream.CopyToAsync(fileStream);
}
/// <inheritdoc/>
protected override async Task<string> UploadFileCore(string id, string file)
{
var response = await CallSignFunction<UploadResponse>(id, OperationType.Upload);
var fileStream = new FileStream(file, FileMode.Open);
var streamContent = new StreamContent(fileStream);
// TODO: this doesn't do multipart uploads. See
// https://stackoverflow.com/questions/29974416/how-do-i-upload-to-amazon-s3-using-net-httpclient-without-using-their-sdk
// for example how to do it.
await _client.PutAsync(new Uri(response.PresignedUrl), streamContent);
return response.CanonicalUrl;
}
private async Task<T> CallSignFunction<T>(string id, OperationType operation)
where T : ResponseBase
{
var payload = new FunctionPayload(id, operation);
var response = await _user.Functions.CallAsync<T>(_function, payload);
if (!response.Success)
{
throw new Exception($"Failed to {operation} object with Id: {id}: {response.Error}");
}
return response;
}
private class FunctionPayload
{
public string FileId { get; set; }
[BsonRepresentation(BsonType.String)]
public OperationType Operation { get; set; }
public FunctionPayload(string id, OperationType operation)
{
FileId = id;
Operation = operation;
}
}
private abstract class ResponseBase
{
[MemberNotNullWhen(false, nameof(Error))]
public bool Success { get; set; }
public string? Error { get; set; }
}
private class UploadResponse : ResponseBase
{
public required string PresignedUrl { get; set; }
public required string CanonicalUrl { get; set; }
}
private class DownloadResponse : ResponseBase
{
public required string Url { get; set; }
}
private class DeleteResponse : ResponseBase
{
}
private enum OperationType
{
Upload,
Download,
Delete
}
}