This commit is contained in:
Steven Hildreth 2018-11-10 19:11:58 -06:00
parent 0344d1e604
commit a4c41e983d
17 changed files with 310 additions and 47 deletions

View file

@ -4,10 +4,14 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Roadie.Api.Services;
using Roadie.Library.Caching;
using Roadie.Library.Data;
using System;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using models = Roadie.Library.Models;
namespace Roadie.Api.Controllers
@ -18,10 +22,13 @@ namespace Roadie.Api.Controllers
[Authorize]
public class ImageController : EntityControllerBase
{
public ImageController( ILoggerFactory logger, ICacheManager cacheManager, IConfiguration configuration)
private IImageService ImageService { get; }
public ImageController(IImageService imageService, ILoggerFactory logger, ICacheManager cacheManager, IConfiguration configuration)
: base(cacheManager, configuration)
{
this._logger = logger.CreateLogger("RoadieApi.Controllers.ImageController"); ;
this.ImageService = imageService;
}
//[EnableQuery]
@ -51,5 +58,28 @@ namespace Roadie.Api.Controllers
// }
// return Ok(result);
//}
[HttpGet("{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
[Route("{id}/{width}/{height}")]
public async Task<IActionResult> Get(Guid id, int? width, int? height)
{
var result = await this.ImageService.ImageById(id, width, height);
if (result == null)
{
return NotFound();
}
if (!result.IsSuccess)
{
return StatusCode((int)HttpStatusCode.InternalServerError);
}
return File(fileContents:result.Data.Bytes,
contentType: result.ContentType,
fileDownloadName: result.Data.Caption ?? id.ToString(),
lastModified: result.LastModified,
entityTag: result.ETag);
}
}
}

View file

@ -59,9 +59,9 @@ namespace Roadie.Api.Services
sw.Stop();
return new OperationResult<Artist>(result.Messages)
{
Data = result.Data,
Errors = result.Errors,
IsSuccess = result != null,
Data = result?.Data,
Errors = result?.Errors,
IsSuccess = result?.IsSuccess ?? false,
OperationTime = sw.ElapsedMilliseconds
};
}

View file

@ -1,4 +1,5 @@
using Roadie.Library.Encoding;
using Microsoft.AspNetCore.WebUtilities;
using Roadie.Library.Encoding;
using System;
using System.Collections.Generic;
using System.Linq;
@ -23,5 +24,15 @@ namespace Roadie.Api.Services
{
return HttpUtility.UrlEncode(s);
}
public string UrlEncodeBase64(byte[] input)
{
return WebEncoders.Base64UrlEncode(input);
}
public string UrlEncodeBase64(string input)
{
return WebEncoders.Base64UrlEncode(System.Text.Encoding.ASCII.GetBytes(input));
}
}
}

View file

@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
using Microsoft.Net.Http.Headers;
using Roadie.Library;
namespace Roadie.Api.Services
{
public interface IImageService
{
Task<FileOperationResult<Library.Models.Image>> ImageById(Guid id, int? width, int? height, EntityTagHeaderValue etag = null);
}
}

View file

@ -0,0 +1,99 @@
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Roadie.Library;
using Roadie.Library.Caching;
using Roadie.Library.Configuration;
using Roadie.Library.Encoding;
using Roadie.Library.Models;
using Roadie.Library.Utility;
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using data = Roadie.Library.Data;
namespace Roadie.Api.Services
{
public class ImageService : ServiceBase, IImageService
{
public ImageService(IRoadieSettings configuration,
IHttpEncoder httpEncoder,
IHttpContext httpContext,
data.IRoadieDbContext context,
ICacheManager cacheManager,
ILogger<ArtistService> logger)
: base(configuration, httpEncoder, context, cacheManager, logger, httpContext)
{
}
public async Task<FileOperationResult<Image>> ImageById(Guid id, int? width, int? height, EntityTagHeaderValue etag = null)
{
var sw = Stopwatch.StartNew();
sw.Start();
var cacheKey = string.Format("urn:image_by_id_operation:{0}", id);
var result = await this.CacheManager.GetAsync<FileOperationResult<Image>>(cacheKey, async () =>
{
return await this.ImageByIdAction(id, etag);
}, data.Image.CacheRegionKey(id));
if (result.ETag == etag)
{
return new FileOperationResult<Image>(OperationMessages.NotModified);
}
sw.Stop();
return new FileOperationResult<Image>(result.Messages)
{
Data = result?.Data,
ETag = result.ETag,
LastModified = result.LastModified,
ContentType = result.ContentType,
Errors = result?.Errors,
IsSuccess = result?.IsSuccess ?? false,
OperationTime = sw.ElapsedMilliseconds
};
}
private async Task<FileOperationResult<Image>> ImageByIdAction(Guid id, EntityTagHeaderValue etag = null)
{
try
{
var sw = new Stopwatch();
sw.Start();
var image = this.DbContext.Images
.Include("Release")
.Include("Artist")
.FirstOrDefault(x => x.RoadieId == id);
if (image == null)
{
return new FileOperationResult<Image>(string.Format("ImageById Not Found [{0}]", id));
}
var imageEtag = EtagHelper.GenerateETag(this.HttpEncoder, image.Bytes);
if (EtagHelper.CompareETag(this.HttpEncoder, etag, imageEtag))
{
return new FileOperationResult<Image>(OperationMessages.NotModified);
}
if (!image?.Bytes?.Any() ?? false)
{
return new FileOperationResult<Image>(string.Format("ImageById Not Set [{0}]", id));
}
sw.Stop();
return new FileOperationResult<Image>(image?.Bytes?.Any() ?? false ? OperationMessages.OkMessage : OperationMessages.NoImageDataFound)
{
IsSuccess = true,
Data = image.Adapt<Image>(),
ContentType = "image/jpeg",
LastModified = (image.LastUpdated ?? image.CreatedDate),
ETag = imageEtag
};
}
catch (Exception ex)
{
this.Logger.LogError($"Error fetching Image [{ id }]", ex);
}
return new FileOperationResult<Image>(OperationMessages.ErrorOccured);
}
}
}

View file

@ -22,8 +22,6 @@ using Roadie.Library.Encoding;
using Roadie.Library.Identity;
using Roadie.Library.Utility;
using System;
using System.IO;
using System.Reflection;
using models = Roadie.Library.Models;
namespace Roadie.Api
@ -33,17 +31,6 @@ namespace Roadie.Api
private readonly IConfiguration _configuration;
private readonly ILoggerFactory _loggerFactory;
//public static string AssemblyDirectory
//{
// get
// {
// string codeBase = Assembly.GetExecutingAssembly().CodeBase;
// var uri = new UriBuilder(codeBase);
// string path = Uri.UnescapeDataString(uri.Path);
// return Path.GetDirectoryName(path);
// }
//}
public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
{
this._configuration = configuration;
@ -55,8 +42,15 @@ namespace Roadie.Api
src => src.Artist.RoadieId)
.Compile();
TypeAdapterConfig.GlobalSettings.Default.PreserveReference(true);
TypeAdapterConfig<Roadie.Library.Data.Image, Roadie.Library.Models.Image>
.NewConfig()
.Map(i => i.ArtistId,
src => src.Artist == null ? null : (Guid?)src.Artist.RoadieId)
.Map(i => i.ReleaseId,
src => src.Release == null ? null : (Guid?)src.Release.RoadieId)
.Compile();
TypeAdapterConfig.GlobalSettings.Default.PreserveReference(true);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -135,6 +129,7 @@ namespace Roadie.Api
services.AddScoped<ICollectionService, CollectionService>();
services.AddScoped<IPlaylistService, PlaylistService>();
services.AddScoped<IArtistService, ArtistService>();
services.AddScoped<IImageService, ImageService>();
var securityKey = new SymmetricSecurityKey(System.Text.Encoding.Default.GetBytes(this._configuration["Tokens:PrivateKey"]));
services.AddAuthentication(options =>

View file

@ -26,5 +26,8 @@ namespace Roadie.Library.Data
[Column("url")]
[MaxLength(500)]
public string Url { get; set; }
public Artist Artist { get; set; }
public Release Release { get; set; }
}
}

View file

@ -1,4 +1,6 @@
using Roadie.Library.Imaging;
using Microsoft.Net.Http.Headers;
using Roadie.Library.Imaging;
using Roadie.Library.Utility;
using System;
using System.Linq;
@ -6,14 +8,16 @@ namespace Roadie.Library.Data
{
public partial class Image
{
public string Etag
public static string CacheRegionKey(Guid Id)
{
return string.Format("urn:image:{0}", Id);
}
public string CacheRegion
{
get
{
using (var md5 = System.Security.Cryptography.MD5.Create())
{
return String.Concat(md5.ComputeHash(System.Text.Encoding.Default.GetBytes(string.Format("{0}{1}", this.RoadieId, this.LastUpdated))).Select(x => x.ToString("D2")));
}
return Image.CacheRegionKey(this.RoadieId);
}
}
@ -25,5 +29,7 @@ namespace Roadie.Library.Data
}
return ImageHasher.AverageHash(this.Bytes).ToString();
}
}
}

View file

@ -7,5 +7,9 @@
string UrlDecode(string s);
string UrlEncode(string s);
string UrlEncodeBase64(byte[] input);
string UrlEncodeBase64(string input);
}
}

View file

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Net.Http.Headers;
namespace Roadie.Library
{
/// <summary>
/// A OperationResult specific to a File type request.
/// </summary>
public class FileOperationResult<T> : OperationResult<T>
{
public EntityTagHeaderValue ETag { get; set; }
public DateTimeOffset? LastModified { get; set; }
public string ContentType { get; set; }
public FileOperationResult(string message)
{
this.AddMessage(message);
}
public FileOperationResult(IEnumerable<string> messages = null)
{
if (messages != null && messages.Any())
{
this.AdditionalData = new Dictionary<string, object>();
messages.ToList().ForEach(x => this.AddMessage(x));
}
}
}
}

View file

@ -14,7 +14,7 @@ namespace Roadie.Library.Models
[Serializable]
public class Artist : EntityModelBase
{
public const string DefaultIncludes = "stats,imaes,associatedartists,collections,playlists,contributions,labels";
public const string DefaultIncludes = "stats,images,associatedartists,collections,playlists,contributions,labels";
public IEnumerable<ReleaseList> ArtistContributionReleases;
public IEnumerable<LabelList> ArtistLabels;

View file

@ -8,6 +8,7 @@ namespace Roadie.Library.Models
{
public Guid? ArtistId { get; set; }
public byte[] Bytes { get; set; }
[MaxLength(100)]

View file

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Roadie.Library.Models
{
public static class OperationMessages
{
public const string Key = "Bearer ";
public const string NewKey = "__new__";
public const string NoImageDataFound = "NO_IMAGE_DATA_FOUND";
public const string NotModified = "NotModified";
public const string OkMessage = "OK";
public const string ErrorOccured = "An error has occured";
}
}

View file

@ -12,7 +12,7 @@ namespace Roadie.Library.Models.Pagination
{
get
{
return this.Message == OperationResult<T>.OkMessage;
return this.Message == OperationMessages.OkMessage;
}
}
@ -27,7 +27,7 @@ namespace Roadie.Library.Models.Pagination
public PagedResult()
{
this.Message = OperationResult<T>.OkMessage;
this.Message = OperationMessages.OkMessage;
}
}
}

View file

@ -6,12 +6,6 @@ namespace Roadie.Library
{
public class OperationResult<T>
{
public const string Key = "Bearer ";
public const string NewKey = "__new__";
public const string NoImageDataFound = "NO_IMAGE_DATA_FOUND";
public const string NotModified = "NotModified";
public const string OkMessage = "OK";
private List<Exception> _errors;
private List<string> _messages;
public Dictionary<string, object> AdditionalData { get; set; }

View file

@ -0,0 +1,56 @@
using Microsoft.Net.Http.Headers;
using Roadie.Library.Encoding;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Roadie.Library.Utility
{
public static class EtagHelper
{
public static EntityTagHeaderValue GenerateETag(IHttpEncoder encoder, byte[] bytes)
{
if(encoder == null || bytes == null || !bytes.Any())
{
return null;
}
return new EntityTagHeaderValue($"\"{ encoder.UrlEncodeBase64(HashHelper.CreateMD5(bytes)) }\"");
}
public static bool CompareETag(IHttpEncoder encoder, EntityTagHeaderValue eTagLeft, EntityTagHeaderValue eTagRight)
{
if(eTagLeft == null && eTagRight == null)
{
return true;
}
if(eTagLeft == null && eTagRight != null)
{
return false;
}
if (eTagRight == null && eTagLeft != null)
{
return false;
}
return eTagLeft == eTagRight;
}
public static bool CompareETag(IHttpEncoder encoder, EntityTagHeaderValue eTag, byte[] bytes)
{
if(eTag == null && (bytes == null || !bytes.Any()))
{
return true;
}
if(eTag == null && (bytes != null) || bytes.Any())
{
return false;
}
if(eTag != null && (bytes == null || !bytes.Any()))
{
return false;
}
var etag = EtagHelper.GenerateETag(encoder, bytes);
return eTag == etag;
}
}
}

View file

@ -1,4 +1,6 @@
using System.Text;
using Roadie.Library.Encoding;
using System.Linq;
using System.Security.Cryptography;
namespace Roadie.Library.Utility
{
@ -6,19 +8,22 @@ namespace Roadie.Library.Utility
{
public static string CreateMD5(string input)
{
// Use input string to calculate MD5 hash
using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create())
if (string.IsNullOrEmpty(input))
{
byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
byte[] hashBytes = md5.ComputeHash(inputBytes);
// Convert the byte array to hexadecimal string
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hashBytes.Length; i++)
{
sb.Append(hashBytes[i].ToString("X2"));
return null;
}
return sb.ToString();
return CreateMD5(System.Text.Encoding.ASCII.GetBytes(input));
}
public static string CreateMD5(byte[] bytes)
{
if (bytes == null || !bytes.Any())
{
return null;
}
using (var md5 = System.Security.Cryptography.MD5.Create())
{
return System.Text.Encoding.ASCII.GetString(md5.ComputeHash(bytes));
}
}
}