mirror of
https://github.com/sphildreth/roadie
synced 2024-11-22 12:13:10 +00:00
WIP
This commit is contained in:
parent
0344d1e604
commit
a4c41e983d
17 changed files with 310 additions and 47 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
12
RoadieApi/Services/IImageService.cs
Normal file
12
RoadieApi/Services/IImageService.cs
Normal 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);
|
||||
}
|
||||
}
|
99
RoadieApi/Services/ImageService.cs
Normal file
99
RoadieApi/Services/ImageService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 =>
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -7,5 +7,9 @@
|
|||
string UrlDecode(string s);
|
||||
|
||||
string UrlEncode(string s);
|
||||
|
||||
string UrlEncodeBase64(byte[] input);
|
||||
|
||||
string UrlEncodeBase64(string input);
|
||||
}
|
||||
}
|
31
RoadieLibrary/FileOperationResult.cs
Normal file
31
RoadieLibrary/FileOperationResult.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -8,6 +8,7 @@ namespace Roadie.Library.Models
|
|||
{
|
||||
public Guid? ArtistId { get; set; }
|
||||
|
||||
|
||||
public byte[] Bytes { get; set; }
|
||||
|
||||
[MaxLength(100)]
|
||||
|
|
16
RoadieLibrary/Models/OperationMessages.cs
Normal file
16
RoadieLibrary/Models/OperationMessages.cs
Normal 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";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
56
RoadieLibrary/Utility/EtagHelper.cs
Normal file
56
RoadieLibrary/Utility/EtagHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue