From 035c9273268aa01def3dcb297e1b50361c79fee3 Mon Sep 17 00:00:00 2001 From: Uncled1023 Date: Sun, 13 Mar 2022 00:28:21 -0800 Subject: [PATCH] Added cache for Upload objects and background processing of download increments --- .../Admin/Controllers/AdminController.cs | 4 +- .../Upload/Controllers/UploadController.cs | 36 +++++----- Teknik/Areas/Upload/UploadHelper.cs | 66 +++++++++++++++++-- .../Areas/User/Controllers/UserController.cs | 4 +- Utilities/ObjectCache.cs | 57 ++++++++++++++++ 5 files changed, 143 insertions(+), 24 deletions(-) create mode 100644 Utilities/ObjectCache.cs diff --git a/Teknik/Areas/Admin/Controllers/AdminController.cs b/Teknik/Areas/Admin/Controllers/AdminController.cs index 9d373e0..c6f1134 100644 --- a/Teknik/Areas/Admin/Controllers/AdminController.cs +++ b/Teknik/Areas/Admin/Controllers/AdminController.cs @@ -262,7 +262,7 @@ namespace Teknik.Areas.Admin.Controllers [HttpPost] [ValidateAntiForgeryToken] - public IActionResult DeleteData(string type, string id) + public IActionResult DeleteData(string type, string id, [FromServices] IBackgroundTaskQueue queue) { var context = new ControllerContext(); context.HttpContext = Request.HttpContext; @@ -272,7 +272,7 @@ namespace Teknik.Areas.Admin.Controllers switch (type) { case "upload": - var uploadController = new Upload.Controllers.UploadController(_logger, _config, _dbContext); + var uploadController = new Upload.Controllers.UploadController(_logger, _config, _dbContext, queue); uploadController.ControllerContext = context; return uploadController.Delete(id); case "paste": diff --git a/Teknik/Areas/Upload/Controllers/UploadController.cs b/Teknik/Areas/Upload/Controllers/UploadController.cs index fb42839..7e1c3b6 100644 --- a/Teknik/Areas/Upload/Controllers/UploadController.cs +++ b/Teknik/Areas/Upload/Controllers/UploadController.cs @@ -32,7 +32,15 @@ namespace Teknik.Areas.Upload.Controllers [Area("Upload")] public class UploadController : DefaultController { - public UploadController(ILogger logger, Config config, TeknikEntities dbContext) : base(logger, config, dbContext) { } + private const int _cacheLength = 300; + private readonly ObjectCache _uploadCache; + private readonly IBackgroundTaskQueue _queue; + + public UploadController(ILogger logger, Config config, TeknikEntities dbContext, IBackgroundTaskQueue queue) : base(logger, config, dbContext) + { + _uploadCache = new ObjectCache(_cacheLength); + _queue = queue; + } [HttpGet] [AllowAnonymous] @@ -225,19 +233,18 @@ namespace Teknik.Areas.Upload.Controllers long contentLength = 0; DateTime dateUploaded = new DateTime(); - Models.Upload upload = _dbContext.Uploads.Where(up => up.Url == file).FirstOrDefault(); + var upload = UploadHelper.GetUpload(_dbContext, file); if (upload != null) { // Check Expiration if (UploadHelper.CheckExpiration(upload)) { - UploadHelper.DeleteFile(_dbContext, _config, _logger, upload); + UploadHelper.DeleteFile(_dbContext, _config, _logger, file); return new StatusCodeResult(StatusCodes.Status404NotFound); } - upload.Downloads += 1; - _dbContext.Entry(upload).State = EntityState.Modified; - _dbContext.SaveChanges(); + // Increment the download count for this upload + UploadHelper.IncrementDownloadCount(_queue, _config, file); fileName = upload.FileName; url = upload.Url; @@ -431,13 +438,13 @@ namespace Teknik.Areas.Upload.Controllers { if (_config.UploadConfig.DownloadEnabled) { - Models.Upload upload = _dbContext.Uploads.Where(up => up.Url == file).FirstOrDefault(); + Models.Upload upload = UploadHelper.GetUpload(_dbContext, file); if (upload != null) { // Check Expiration if (UploadHelper.CheckExpiration(upload)) { - UploadHelper.DeleteFile(_dbContext, _config, _logger, upload); + UploadHelper.DeleteFile(_dbContext, _config, _logger, file); return Json(new { error = new { message = "File Does Not Exist" } }); } @@ -485,14 +492,14 @@ namespace Teknik.Areas.Upload.Controllers public IActionResult DeleteByKey(string file, string key) { ViewBag.Title = "File Delete | " + file ; - Models.Upload upload = _dbContext.Uploads.Where(up => up.Url == file).FirstOrDefault(); + Models.Upload upload = UploadHelper.GetUpload(_dbContext, file); if (upload != null) { DeleteViewModel model = new DeleteViewModel(); model.File = file; if (!string.IsNullOrEmpty(upload.DeleteKey) && upload.DeleteKey == key) { - UploadHelper.DeleteFile(_dbContext, _config, _logger, upload); + UploadHelper.DeleteFile(_dbContext, _config, _logger, file); model.Deleted = true; } else @@ -507,16 +514,13 @@ namespace Teknik.Areas.Upload.Controllers [HttpPost] public IActionResult GenerateDeleteKey(string file) { - Models.Upload upload = _dbContext.Uploads.Where(up => up.Url == file).FirstOrDefault(); + Models.Upload upload = UploadHelper.GetUpload(_dbContext, file); if (upload != null) { if (upload.User?.Username == User.Identity.Name || User.IsInRole("Admin")) { - string delKey = StringHelper.RandomString(_config.UploadConfig.DeleteKeyLength); - upload.DeleteKey = delKey; - _dbContext.Entry(upload).State = EntityState.Modified; - _dbContext.SaveChanges(); + var delKey = UploadHelper.GenerateDeleteKey(_dbContext, _config, file); return Json(new { result = new { url = Url.SubRouteUrl("u", "Upload.DeleteByKey", new { file = file, key = delKey }) } }); } return Json(new { error = new { message = "You do not have permission to delete this Upload" } }); @@ -534,7 +538,7 @@ namespace Teknik.Areas.Upload.Controllers if (foundUpload.User?.Username == User.Identity.Name || User.IsInRole("Admin")) { - UploadHelper.DeleteFile(_dbContext, _config, _logger, foundUpload); + UploadHelper.DeleteFile(_dbContext, _config, _logger, id); return Json(new { result = true }); } return Json(new { error = new { message = "You do not have permission to delete this Upload" } }); diff --git a/Teknik/Areas/Upload/UploadHelper.cs b/Teknik/Areas/Upload/UploadHelper.cs index da99fda..206b668 100644 --- a/Teknik/Areas/Upload/UploadHelper.cs +++ b/Teknik/Areas/Upload/UploadHelper.cs @@ -12,11 +12,15 @@ using Teknik.Data; using Teknik.StorageService; using Teknik.Logging; using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore; namespace Teknik.Areas.Upload { public static class UploadHelper { + private static object _cacheLock = new object(); + private readonly static ObjectCache _uploadCache = new ObjectCache(300); + public static Models.Upload SaveFile(TeknikEntities db, Config config, Stream file, string contentType, long contentLength, bool encrypt, ExpirationUnit expirationUnit, int expirationLength) { return SaveFile(db, config, file, contentType, contentLength, encrypt, expirationUnit, expirationLength, string.Empty, null, null, 256, 128); @@ -119,6 +123,19 @@ namespace Teknik.Areas.Upload return upload; } + public static string GenerateDeleteKey(TeknikEntities db, Config config, string url) + { + var upload = db.Uploads.FirstOrDefault(up => up.Url == url); + if (upload != null) + { + string delKey = StringHelper.RandomString(config.UploadConfig.DeleteKeyLength); + upload.DeleteKey = delKey; + ModifyUpload(db, upload); + return delKey; + } + return null; + } + public static bool CheckExpiration(Models.Upload upload) { if (upload.ExpireDate != null && DateTime.Now >= upload.ExpireDate) @@ -129,15 +146,37 @@ namespace Teknik.Areas.Upload return false; } - public static Models.Upload GetUpload(TeknikEntities db, string url) + public static void IncrementDownloadCount(IBackgroundTaskQueue queue, Config config, string url) { - Models.Upload upload = db.Uploads.Where(up => up.Url == url).FirstOrDefault(); + // Fire and forget updating of the download count + queue.QueueBackgroundWorkItem(async token => + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer(config.DbConnection); - return upload; + using (TeknikEntities db = new TeknikEntities(optionsBuilder.Options)) + { + var upload = db.Uploads.FirstOrDefault(up => up.Url == url); + if (upload != null) + { + upload.Downloads++; + ModifyUpload(db, upload); + } + } + }); } - public static void DeleteFile(TeknikEntities db, Config config, ILogger logger, Models.Upload upload) + public static Models.Upload GetUpload(TeknikEntities db, string url) { + lock (_cacheLock) + { + return _uploadCache.GetObject(url, (key) => db.Uploads.FirstOrDefault(up => up.Url == key)); + } + } + + public static void DeleteFile(TeknikEntities db, Config config, ILogger logger, string url) + { + var upload = db.Uploads.FirstOrDefault(up => up.Url == url); try { var storageService = StorageServiceFactory.GetStorageService(config.UploadConfig.StorageConfig); @@ -148,9 +187,28 @@ namespace Teknik.Areas.Upload logger.LogError(ex, "Unable to delete file: {0}", upload.FileName); } + // Remove from the cache + lock (_cacheLock) + { + _uploadCache.DeleteObject(upload.FileName); + } + // Delete from the DB db.Uploads.Remove(upload); db.SaveChanges(); } + + public static void ModifyUpload(TeknikEntities db, Models.Upload upload) + { + // Update the cache's copy + lock (_cacheLock) + { + _uploadCache.UpdateObject(upload.Url, upload); + } + + // Update the database + db.Entry(upload).State = EntityState.Modified; + db.SaveChanges(); + } } } diff --git a/Teknik/Areas/User/Controllers/UserController.cs b/Teknik/Areas/User/Controllers/UserController.cs index 0fbcc89..fb69f31 100644 --- a/Teknik/Areas/User/Controllers/UserController.cs +++ b/Teknik/Areas/User/Controllers/UserController.cs @@ -1421,7 +1421,7 @@ namespace Teknik.Areas.Users.Controllers [HttpPost] [ValidateAntiForgeryToken] - public IActionResult DeleteData(string type, string id) + public IActionResult DeleteData(string type, string id, [FromServices] IBackgroundTaskQueue queue) { var context = new ControllerContext(); context.HttpContext = Request.HttpContext; @@ -1431,7 +1431,7 @@ namespace Teknik.Areas.Users.Controllers switch (type) { case "upload": - var uploadController = new Upload.Controllers.UploadController(_logger, _config, _dbContext); + var uploadController = new Upload.Controllers.UploadController(_logger, _config, _dbContext, queue); uploadController.ControllerContext = context; return uploadController.Delete(id); case "paste": diff --git a/Utilities/ObjectCache.cs b/Utilities/ObjectCache.cs new file mode 100644 index 0000000..d951099 --- /dev/null +++ b/Utilities/ObjectCache.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Teknik.Utilities +{ + public class ObjectCache + { + private readonly static Dictionary> objectCache = new Dictionary>(); + private readonly int _cacheSeconds; + + public ObjectCache(int cacheSeconds) + { + _cacheSeconds = cacheSeconds; + } + + public T GetObject(string key, Func getObjectFunc) + { + T foundObject; + var cacheDate = DateTime.UtcNow; + if (objectCache.TryGetValue(key, out var result) && + result.Item1 > cacheDate.Subtract(new TimeSpan(0, 0, _cacheSeconds))) + { + cacheDate = result.Item1; + foundObject = (T)result.Item2; + } + else + { + foundObject = getObjectFunc(key); + } + + if (foundObject != null) + objectCache[key] = new Tuple(cacheDate, foundObject); + + return foundObject; + } + + public void UpdateObject(string key, T update) + { + var cacheDate = DateTime.UtcNow; + if (objectCache.TryGetValue(key, out var result)) + { + if (result.Item1 <= cacheDate.Subtract(new TimeSpan(0, 0, _cacheSeconds))) + DeleteObject(key); + else + objectCache[key] = new Tuple(result.Item1, update); + } + } + + public void DeleteObject(string key) + { + objectCache.Remove(key); + } + } +}