private async Task <(Stream stream, bool fromStorage)> GetContextStreamAsync( TournamentBracketRequest request, TournamentBracketContent content, CompositeDisposable disposables, CancellationToken cancellationToken ) { // Validate parameters. if (request == null) { throw new ArgumentNullException(nameof(request)); } if (disposables == null) { throw new ArgumentNullException(nameof(disposables)); } // The algorithm is this: // If there is content, use that. // If there is not content, then try to get from challonge. // If there is no content, try to get from persistence // If it can't be retrieved, throw. // Check the request for content. if (content != null && content.MediaType.Type == "text" && content.MediaType.SubTypeWithoutSuffix == "html" ) { // Return a streamreader. return(content.Content, false); } // Try and get it from challonge. Stream stream = await GetStreamFromChallongeAsync( request, disposables, cancellationToken ) .ConfigureAwait(false); // If not null, return. if (stream != null) { return(stream, false); } // Get the reader from storage. stream = await GetStreamFromStorageAsync( request, disposables, cancellationToken ).ConfigureAwait(false); // If there is no stream at this point, throw. if (stream == null) { throw new InvalidOperationException( $"Could not retrieve content for URL \"{request.Url}\" from user-sumbmitted content, Challonge, or storage."); } // Return the reader. return(stream, true); }
private Task PersistContentAsync( TournamentBracketRequest request, Stream stream, CancellationToken cancellationToken ) { // Validate parameters. if (request == null) { throw new ArgumentNullException(nameof(request)); } if (stream == null) { throw new ArgumentNullException(nameof(stream)); } // Get the container reference. CloudBlobContainer container = GetStorageContainerReference(); // Get the blob filename. string file = GetBlobFilename(request); // Create the block blob. CloudBlockBlob blob = container.GetBlockBlobReference(file); // Upload the stream to the blob. return(blob.UploadFromStreamAsync(stream, cancellationToken)); }
private static string GetBlobFilename(TournamentBracketRequest request) { // Validate parameters. if (request == null) { throw new ArgumentNullException(nameof(request)); } // The filename will be based on the type. // Fir single stage tournaments, it will be. // // t/<host>?/<id>.html // // If there is no host, we will use "(root)" as the host // ID is the query string. // Look at the hostname of the URL to get the host. // // TODO: Handle event brackets. string host = "(root)"; // Is it not challonge in it's entirety? if (string.Compare(request.Url.Host, "challonge.com", StringComparison.OrdinalIgnoreCase) != 0) { // Split the host. ReadOnlySpan <string> parts = request.Url.Host.Split('.'); // If there are not three parts, throw. if (parts.Length != 3) { throw new InvalidOperationException($"The URL { request.Url } is not a valid Challonge URL."); } // Set the host with the first part. host = parts[0].ToLowerInvariant(); } // Get the path. Lowercase, and set the extension. string path = Path.ChangeExtension(request.Url.AbsolutePath.ToLowerInvariant(), ".html"); // If there are separators in the path, throw. // There is one at the beginning. if (path.IndexOf('/') >= 1) { throw new InvalidOperationException($"The URL { request.Url } is not a valid Challonge URL."); } // Put it all together. return($"t/{host}{path}"); }
private async Task <Stream> GetStreamFromStorageAsync( TournamentBracketRequest request, CompositeDisposable disposables, CancellationToken cancellationToken ) { // Validate parameters. if (request == null) { throw new ArgumentNullException(nameof(request)); } if (disposables == null) { throw new ArgumentNullException(nameof(disposables)); } // Get the container reference. CloudBlobContainer container = GetStorageContainerReference(); // Get the blob filename. string file = GetBlobFilename(request); // Create the block blob. CloudBlockBlob blob = container.GetBlockBlobReference(file); // Does it exist? If not, return null. if (!await blob.ExistsAsync(cancellationToken) .ConfigureAwait(false)) { return(null); } // The stream to be a target. var ms = new MemoryStream(); // Add to the disposables. disposables.Add(ms); // Download. await blob.DownloadToStreamAsync(ms, cancellationToken) .ConfigureAwait(false); // Reset the stream. ms.Position = 0; // Return the stream. return(ms); }
private async Task <Stream> GetStreamFromChallongeAsync( TournamentBracketRequest request, CompositeDisposable disposables, CancellationToken cancellationToken ) { // Validate parameters. if (request == null) { throw new ArgumentNullException(nameof(request)); } // Try to get from challonge. HttpResponseMessage response = await _httpClient .GetAsync(request.Url, cancellationToken) .ConfigureAwait(false); // If there is not a X-Challonge-Cache-ID header, return // null. if (!response.Headers.TryGetValues("X-Challonge-Cache-ID", out var _)) { // Return null. return(null); } // Add to the disposables. disposables.Add(response); // Get the stream. Stream stream = await response.Content .ReadAsStreamAsync() .ConfigureAwait(false); // Return. return(stream); }
public async Task <TournamentBracketResponse> GetTournamentBracketAsync( TournamentBracketRequest request, TournamentBracketContent content, CancellationToken cancellationToken) { // Validate parameters. // Note: The content parameter can be null. if (request == null) { throw new ArgumentNullException(nameof(request)); } // If the url is null, throw. if (request.Url == null) { throw new InvalidOperationException( $"The {nameof(request.Url)} property on the {nameof(request)} parameter cannot be null."); } // The disposables. using var disposables = new CompositeDisposable(); // The page content. TournamentBracketPage page; // Get the content stream. (Stream rawStream, bool fromStorage) = await GetContextStreamAsync( request, content, disposables, cancellationToken ).ConfigureAwait(false); // Add the raw stream to the disposables. disposables.Add(rawStream); // If there is no stream, copy it. if (!(rawStream is MemoryStream stream)) { // Create a new one. stream = new MemoryStream(); // Copy. await rawStream.CopyToAsync(stream, 4096, cancellationToken) .ConfigureAwait(false); } // Add the stream to the disposables. disposables.Add(stream); // Set the position of the stream back. stream.Position = 0; // Create a reader. using TextReader reader = new StreamReader(stream); // Parse. page = await _parser.ParseTournamentBracketPageAsync(reader, cancellationToken) .ConfigureAwait(false); // Everything was successful at this point, reset the stream and persist. stream.Position = 0; // Persist if this wasn't from storage to begin with. if (!fromStorage) { await PersistContentAsync(request, stream, cancellationToken) .ConfigureAwait(false); } // Return. return(new TournamentBracketResponse { Request = request, PageContent = page.PageContent, Bracket = page.Bracket, TargetingKeyValues = page.TargetingKeyValues, MetaList = page.MetaList }); }