tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
FileTransferService.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Threading;
5using System.Threading.Tasks;
6
7using Microsoft.Extensions.Logging;
8
14
16{
21 {
25 const int TicketValidityMinutes = 5;
26
31
36
41
45 readonly ILogger<FileTransferService> logger;
46
50 readonly Dictionary<string, FileUploadProvider> uploadTickets;
51
55 readonly Dictionary<string, FileDownloadProvider> downloadTickets;
56
60 readonly CancellationTokenSource disposeCts;
61
65 readonly object synchronizationLock;
66
71
76
88 ILogger<FileTransferService> logger)
89 {
90 this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
91 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
92 this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
93 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
94
95 uploadTickets = new Dictionary<string, FileUploadProvider>();
96 downloadTickets = new Dictionary<string, FileDownloadProvider>();
97
98 disposeCts = new CancellationTokenSource();
99
100 expireTask = Task.CompletedTask;
101 synchronizationLock = new object();
102 }
103
105 public async ValueTask DisposeAsync()
106 {
107 Task toAwait;
109 if (!disposed)
110 {
111 disposeCts.Cancel();
112 disposeCts.Dispose();
113 disposed = true;
114 toAwait = expireTask;
115 expireTask = Task.CompletedTask;
116 }
117 else
118 toAwait = Task.CompletedTask;
119
120 await toAwait;
121 }
122
125 {
126 ArgumentNullException.ThrowIfNull(downloadProvider);
127 ObjectDisposedException.ThrowIf(disposed, this);
128
129 logger.LogDebug("Creating download ticket for path {filePath}", downloadProvider.FilePath);
130 var ticket = cryptographySuite.GetSecureString();
131
132 lock (downloadTickets)
133 downloadTickets.Add(ticket, downloadProvider);
134
135 QueueExpiry(() =>
136 {
137 lock (downloadTickets)
138 if (downloadTickets.Remove(ticket))
139 logger.LogTrace("Expired download ticket {ticket}...", ticket);
140 });
141
142 logger.LogTrace("Created download ticket {ticket}", ticket);
143
144 return new FileTicketResponse
145 {
146 FileTicket = ticket,
147 };
148 }
149
152 {
153 ObjectDisposedException.ThrowIf(disposed, this);
154
155 logger.LogDebug("Creating upload ticket...");
156 var ticket = cryptographySuite.GetSecureString();
157 var uploadTicket = new FileUploadProvider(
159 {
160 FileTicket = ticket,
161 },
162 streamKind);
163
164 lock (uploadTickets)
165 uploadTickets.Add(ticket, uploadTicket);
166
167 QueueExpiry(() =>
168 {
169 lock (uploadTickets)
170 if (uploadTickets.Remove(ticket))
171 logger.LogTrace("Expired upload ticket {ticket}...", ticket);
172 else
173 return;
174
175 uploadTicket.Expire();
176 });
177
178 logger.LogTrace("Created upload ticket {ticket}", ticket);
179
180 return uploadTicket;
181 }
182
184 public async ValueTask<Tuple<Stream?, ErrorMessageResponse?>> RetrieveDownloadStream(FileTicketResponse ticketResponse, CancellationToken cancellationToken)
185 {
186 ArgumentNullException.ThrowIfNull(ticketResponse);
187 ObjectDisposedException.ThrowIf(disposed, this);
188
189 var ticket = ticketResponse.FileTicket ?? throw new InvalidOperationException("ticketResponse must have FileTicket!");
190 FileDownloadProvider? downloadProvider;
191 lock (downloadTickets)
192 {
193 if (!downloadTickets.TryGetValue(ticket, out downloadProvider))
194 {
195 logger.LogTrace("Download ticket {ticket} not found!", ticket);
196 return Tuple.Create<Stream?, ErrorMessageResponse?>(null, null);
197 }
198
199 downloadTickets.Remove(ticket);
200 }
201
202 var errorCode = downloadProvider.ActivationCallback();
203 if (errorCode.HasValue)
204 {
205 logger.LogDebug("Download ticket {ticket} failed activation!", ticket);
206 return Tuple.Create<Stream?, ErrorMessageResponse?>(null, new ErrorMessageResponse(errorCode.Value));
207 }
208
209 Stream stream;
210 try
211 {
212 if (downloadProvider.StreamProvider != null)
213 stream = await downloadProvider.StreamProvider(cancellationToken);
214 else
215 stream = ioManager.GetFileStream(downloadProvider.FilePath, downloadProvider.ShareWrite);
216 }
217 catch (IOException ex)
218 {
219 return Tuple.Create<Stream?, ErrorMessageResponse?>(
220 null,
222 {
223 AdditionalData = ex.ToString(),
224 });
225 }
226
227 try
228 {
229 logger.LogTrace("Ticket {ticket} downloading...", ticket);
230 return Tuple.Create<Stream?, ErrorMessageResponse?>(stream, null);
231 }
232 catch
233 {
234 await stream.DisposeAsync();
235 throw;
236 }
237 }
238
240 public async ValueTask<ErrorMessageResponse?> SetUploadStream(FileTicketResponse ticketResponse, Stream stream, CancellationToken cancellationToken)
241 {
242 ArgumentNullException.ThrowIfNull(ticketResponse);
243 ObjectDisposedException.ThrowIf(disposed, this);
244
245 var ticket = ticketResponse.FileTicket ?? throw new InvalidOperationException("ticketResponse must have FileTicket!");
246 FileUploadProvider? uploadProvider;
247 lock (uploadTickets)
248 {
249 if (!uploadTickets.TryGetValue(ticket, out uploadProvider))
250 {
251 logger.LogTrace("Upload ticket {ticket} not found!", ticket);
252 return new ErrorMessageResponse(ErrorCode.ResourceNotPresent);
253 }
254
255 uploadTickets.Remove(ticket);
256 }
257
258 return await uploadProvider.Completion(stream, cancellationToken);
259 }
260
265 void QueueExpiry(Action expireAction)
266 {
267 Task oldExpireTask;
268 async Task ExpireAsync()
269 {
270 var expireAt = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(TicketValidityMinutes);
271 try
272 {
273 await oldExpireTask.WaitAsync(disposeCts.Token);
274
275 var now = DateTimeOffset.UtcNow;
276 if (now < expireAt)
277 await asyncDelayer.Delay(expireAt - now, disposeCts.Token);
278 }
279 finally
280 {
281 expireAction();
282 }
283 }
284
286 {
287 oldExpireTask = expireTask;
288 expireTask = ExpireAsync();
289 }
290 }
291 }
292}
Represents an error message returned by the server.
Response for when file transfers are necessary.
Represents a file on disk to be downloaded.
bool ShareWrite
If the file read stream should be allowed to share writes. If this is set, the entire file will be bu...
string FilePath
The full path to the file on disk to download.
Func< CancellationToken, Task< Stream > >? StreamProvider
A Func<T, TResult> to specially provide a Task<TResult> returning the Stream of the file download....
Func< ErrorCode?> ActivationCallback
A Func<TResult> to run before providing the download. If it returns a non-null ErrorCode,...
Implementation of the file transfer service.
readonly object synchronizationLock
lock object used to update expireTask.
readonly ILogger< FileTransferService > logger
The ILogger for the FileTransferService.
async ValueTask< ErrorMessageResponse?> SetUploadStream(FileTicketResponse ticketResponse, Stream stream, CancellationToken cancellationToken)
Sets the Stream for a given ticketResponse associated with a pending upload.A ValueTask<TResult> res...
readonly Dictionary< string, FileDownloadProvider > downloadTickets
Dictionary<TKey, TValue> of FileTicketResponse.FileTickets to FileDownloadProviders.
IFileUploadTicket CreateUpload(FileUploadStreamKind streamKind)
Create a IFileUploadTicket.A new IFileUploadTicket.
const int TicketValidityMinutes
Number of minutes before transfer ticket expire.
readonly Dictionary< string, FileUploadProvider > uploadTickets
Dictionary<TKey, TValue> of FileTicketResponse.FileTickets to upload Stream TaskCompletionSource<TRes...
FileTransferService(ICryptographySuite cryptographySuite, IIOManager ioManager, IAsyncDelayer asyncDelayer, ILogger< FileTransferService > logger)
Initializes a new instance of the FileTransferService class.
readonly IIOManager ioManager
The IIOManager for the FileTransferService.
Task expireTask
Combined Task of all QueueExpiry(Action) calls.
void QueueExpiry(Action expireAction)
Queue an expireAction to run after TicketValidityMinutes.
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the FileTransferService.
async ValueTask< Tuple< Stream?, ErrorMessageResponse?> > RetrieveDownloadStream(FileTicketResponse ticketResponse, CancellationToken cancellationToken)
Gets the the Stream for a given ticketResponse associated with a pending download....
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the FileTransferService.
bool disposed
If the FileTransferService is disposed.
readonly CancellationTokenSource disposeCts
CancellationTokenSource that is triggered when IAsyncDisposable.DisposeAsync is called.
FileTicketResponse CreateDownload(FileDownloadProvider downloadProvider)
Create a FileTicketResponse for a download.A new FileTicketResponse for a download.
async ValueTask< ErrorMessageResponse?> Completion(Stream stream, CancellationToken cancellationToken)
Resolve the stream for the FileUploadProvider and awaits the upload.
Interface for using filesystems.
Definition IIOManager.cs:13
FileStream GetFileStream(string path, bool shareWrite)
Gets the Stream for a given file path .
Contains various cryptographic functions.
string GetSecureString()
Generates a 40-length secure ascii string.
Reads and writes to Streams associated with FileTicketResponses.
Service for temporarily storing files to be downloaded or uploaded.
A FileTicketResponse that waits for a pending upload.
ValueTask Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan .
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition ErrorCode.cs:12
FileUploadStreamKind
Determines the type of global::System.IO.Stream returned from IFileUploadTicket's created from IFileT...