tgstation-server 5.12.7
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
15
17{
22 {
26 const int TicketValidityMinutes = 5;
27
32
37
42
46 readonly ILogger<FileTransferService> logger;
47
51 readonly Dictionary<string, FileUploadProvider> uploadTickets;
52
56 readonly Dictionary<string, FileDownloadProvider> downloadTickets;
57
61 readonly CancellationTokenSource disposeCts;
62
66 readonly object synchronizationLock;
67
72
84 ILogger<FileTransferService> logger)
85 {
86 this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
87 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
88 this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
89 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
90
91 uploadTickets = new Dictionary<string, FileUploadProvider>();
92 downloadTickets = new Dictionary<string, FileDownloadProvider>();
93
94 disposeCts = new CancellationTokenSource();
95
96 expireTask = Task.CompletedTask;
97 synchronizationLock = new object();
98 }
99
101 public async ValueTask DisposeAsync()
102 {
103 Task toAwait;
105 if (expireTask != null)
106 {
107 disposeCts.Cancel();
108 disposeCts.Dispose();
109 toAwait = expireTask;
110 expireTask = null;
111 }
112 else
113 toAwait = Task.CompletedTask;
114
115 await toAwait;
116 }
117
120 {
121 ArgumentNullException.ThrowIfNull(downloadProvider);
122
123 logger.LogDebug("Creating download ticket for path {filePath}", downloadProvider.FilePath);
124 var ticketResult = CreateTicket();
125
126 lock (downloadTickets)
127 downloadTickets.Add(ticketResult.FileTicket, downloadProvider);
128
129 QueueExpiry(() =>
130 {
131 lock (downloadTickets)
132 if (downloadTickets.Remove(ticketResult.FileTicket))
133 logger.LogTrace("Expired download ticket {ticket}...", ticketResult.FileTicket);
134 });
135
136 logger.LogTrace("Created download ticket {ticket}", ticketResult.FileTicket);
137
138 return ticketResult;
139 }
140
143 {
144 logger.LogDebug("Creating upload ticket...");
145 var uploadTicket = new FileUploadProvider(CreateTicket(), streamKind);
146
147 lock (uploadTickets)
148 uploadTickets.Add(uploadTicket.Ticket.FileTicket, uploadTicket);
149
150 QueueExpiry(() =>
151 {
152 lock (uploadTickets)
153 if (uploadTickets.Remove(uploadTicket.Ticket.FileTicket))
154 logger.LogTrace("Expired upload ticket {ticket}...", uploadTicket.Ticket.FileTicket);
155 else
156 return;
157
158 uploadTicket.Expire();
159 });
160
161 logger.LogTrace("Created upload ticket {ticket}", uploadTicket.Ticket.FileTicket);
162
163 return uploadTicket;
164 }
165
167 public async Task<Tuple<Stream, ErrorMessageResponse>> RetrieveDownloadStream(FileTicketResponse ticket, CancellationToken cancellationToken)
168 {
169 ArgumentNullException.ThrowIfNull(ticket);
170
171 FileDownloadProvider downloadProvider;
172 lock (downloadTickets)
173 {
174 if (!downloadTickets.TryGetValue(ticket.FileTicket, out downloadProvider))
175 {
176 logger.LogTrace("Download ticket {ticket} not found!", ticket.FileTicket);
177 return Tuple.Create<Stream, ErrorMessageResponse>(null, null);
178 }
179
180 downloadTickets.Remove(ticket.FileTicket);
181 }
182
183 var errorCode = downloadProvider.ActivationCallback();
184 if (errorCode.HasValue)
185 {
186 logger.LogDebug("Download ticket {ticket} failed activation!", ticket.FileTicket);
187 return Tuple.Create<Stream, ErrorMessageResponse>(null, new ErrorMessageResponse(errorCode.Value));
188 }
189
190 Stream stream;
191 try
192 {
193 if (downloadProvider.StreamProvider != null)
194 stream = await downloadProvider.StreamProvider(cancellationToken);
195 else
196 stream = ioManager.GetFileStream(downloadProvider.FilePath, downloadProvider.ShareWrite);
197 }
198 catch (IOException ex)
199 {
200 return Tuple.Create<Stream, ErrorMessageResponse>(
201 null,
203 {
204 AdditionalData = ex.ToString(),
205 });
206 }
207
208 try
209 {
210 logger.LogTrace("Ticket {ticket} downloading...", ticket.FileTicket);
211 return Tuple.Create<Stream, ErrorMessageResponse>(stream, null);
212 }
213 catch
214 {
215 await stream.DisposeAsync();
216 throw;
217 }
218 }
219
221 public async Task<ErrorMessageResponse> SetUploadStream(FileTicketResponse ticket, Stream stream, CancellationToken cancellationToken)
222 {
223 ArgumentNullException.ThrowIfNull(ticket);
224
225 FileUploadProvider uploadProvider;
226 lock (uploadTickets)
227 {
228 if (!uploadTickets.TryGetValue(ticket.FileTicket, out uploadProvider))
229 {
230 logger.LogTrace("Upload ticket {ticket} not found!", ticket.FileTicket);
231 return new ErrorMessageResponse(ErrorCode.ResourceNotPresent);
232 }
233
234 uploadTickets.Remove(ticket.FileTicket);
235 }
236
237 return await uploadProvider.Completion(stream, cancellationToken);
238 }
239
245 {
246 FileTicket = cryptographySuite.GetSecureString(),
247 };
248
253 void QueueExpiry(Action expireAction)
254 {
255 Task oldExpireTask;
256 async Task ExpireAsync()
257 {
258 var expireAt = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(TicketValidityMinutes);
259 try
260 {
261 await oldExpireTask.WithToken(disposeCts.Token);
262
263 var now = DateTimeOffset.UtcNow;
264 if (now < expireAt)
265 await asyncDelayer.Delay(expireAt - now, disposeCts.Token);
266 }
267 finally
268 {
269 expireAction();
270 }
271 }
272
274 {
275 oldExpireTask = expireTask;
276 expireTask = ExpireAsync();
277 }
278 }
279 }
280}
Represents an error message returned by the server.
Response for when file transfers are necessary.
virtual ? string FileTicket
The ticket to use to access the Routes.Transfer controller.
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.
async Task< Tuple< Stream, ErrorMessageResponse > > RetrieveDownloadStream(FileTicketResponse ticket, CancellationToken cancellationToken)
Gets the the Stream for a given ticket associated with a pending download. A Tuple<T1,...
readonly object synchronizationLock
lock object used to update expireTask.
readonly ILogger< FileTransferService > logger
The ILogger for the FileTransferService.
readonly Dictionary< string, FileDownloadProvider > downloadTickets
Dictionary<TKey, TValue> of FileTicketResponse.FileTickets to FileDownloadProviders.
async Task< ErrorMessageResponse > SetUploadStream(FileTicketResponse ticket, Stream stream, CancellationToken cancellationToken)
Sets the Stream for a given ticket associated with a pending upload. null if the upload completed su...
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.
FileTicketResponse CreateTicket()
Creates a new FileTicketResponse.
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.
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the FileTransferService.
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 Task< 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.
Task 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:11
FileUploadStreamKind
Determines the type of global::System.IO.Stream returned from IFileUploadTicket's created from IFileT...