diff --git a/Teknik/Areas/Admin/Controllers/AdminController.cs b/Teknik/Areas/Admin/Controllers/AdminController.cs index c6f1134..e41d756 100644 --- a/Teknik/Areas/Admin/Controllers/AdminController.cs +++ b/Teknik/Areas/Admin/Controllers/AdminController.cs @@ -276,7 +276,7 @@ namespace Teknik.Areas.Admin.Controllers uploadController.ControllerContext = context; return uploadController.Delete(id); case "paste": - var pasteController = new Paste.Controllers.PasteController(_logger, _config, _dbContext); + var pasteController = new Paste.Controllers.PasteController(_logger, _config, _dbContext, queue); pasteController.ControllerContext = context; return pasteController.Delete(id); case "shortenedUrl": diff --git a/Teknik/Areas/Paste/Controllers/PasteController.cs b/Teknik/Areas/Paste/Controllers/PasteController.cs index 3b57b6c..44b0821 100644 --- a/Teknik/Areas/Paste/Controllers/PasteController.cs +++ b/Teknik/Areas/Paste/Controllers/PasteController.cs @@ -30,7 +30,12 @@ namespace Teknik.Areas.Paste.Controllers [Area("Paste")] public class PasteController : DefaultController { - public PasteController(ILogger logger, Config config, TeknikEntities dbContext) : base(logger, config, dbContext) { } + private readonly IBackgroundTaskQueue _queue; + + public PasteController(ILogger logger, Config config, TeknikEntities dbContext, IBackgroundTaskQueue queue) : base(logger, config, dbContext) + { + _queue = queue; + } [AllowAnonymous] [TrackPageView] @@ -46,23 +51,29 @@ namespace Teknik.Areas.Paste.Controllers [TrackPageView] public async Task ViewPaste(string type, string url, string password) { - Models.Paste paste = _dbContext.Pastes.Where(p => p.Url == url).FirstOrDefault(); + Models.Paste paste = PasteHelper.GetPaste(_dbContext, url); if (paste != null) { ViewBag.Title = (string.IsNullOrEmpty(paste.Title)) ? "Untitled Paste" : paste.Title + " | Pastebin"; ViewBag.Description = "Paste your code or text easily and securely. Set an expiration, set a password, or leave it open for the world to see."; - // Increment Views - paste.Views += 1; - _dbContext.Entry(paste).State = EntityState.Modified; - _dbContext.SaveChanges(); + + string fileName = paste.FileName; + string key = paste.Key; + string iv = paste.IV; + int blockSize = paste.BlockSize; + int keySize = paste.KeySize; + string hashedPass = paste.HashedPassword; // Check Expiration if (PasteHelper.CheckExpiration(paste)) { - PasteHelper.DeleteFile(_dbContext, _config, _logger, paste); + PasteHelper.DeleteFile(_dbContext, _config, _logger, url); return new StatusCodeResult(StatusCodes.Status404NotFound); } + // Increment View Count + PasteHelper.IncrementViewCount(_queue, _config, url); + PasteViewModel model = new PasteViewModel(); model.Url = url; model.Title = paste.Title; @@ -79,11 +90,11 @@ namespace Teknik.Areas.Paste.Controllers } } - byte[] ivBytes = (string.IsNullOrEmpty(paste.IV)) ? new byte[paste.BlockSize] : Encoding.Unicode.GetBytes(paste.IV); - byte[] keyBytes = (string.IsNullOrEmpty(paste.Key)) ? new byte[paste.KeySize] : AesCounterManaged.CreateKey(paste.Key, ivBytes, paste.KeySize); + byte[] ivBytes = (string.IsNullOrEmpty(iv)) ? new byte[blockSize] : Encoding.Unicode.GetBytes(iv); + byte[] keyBytes = (string.IsNullOrEmpty(key)) ? new byte[keySize] : AesCounterManaged.CreateKey(key, ivBytes, keySize); // The paste has a password set - if (!string.IsNullOrEmpty(paste.HashedPassword)) + if (!string.IsNullOrEmpty(hashedPass)) { if (string.IsNullOrEmpty(password)) { @@ -93,17 +104,17 @@ namespace Teknik.Areas.Paste.Controllers string hash = string.Empty; if (!string.IsNullOrEmpty(password)) { - hash = Crypto.HashPassword(paste.Key, password); - keyBytes = AesCounterManaged.CreateKey(password, ivBytes, paste.KeySize); + hash = Crypto.HashPassword(key, password); + keyBytes = AesCounterManaged.CreateKey(password, ivBytes, keySize); } - if (string.IsNullOrEmpty(password) || hash != paste.HashedPassword) + if (string.IsNullOrEmpty(password) || hash != hashedPass) { PasswordViewModel passModel = new PasswordViewModel(); passModel.ActionUrl = Url.SubRouteUrl("p", "Paste.View", new { type = type, url = url }); passModel.Url = url; passModel.Type = type; - if (!string.IsNullOrEmpty(password) && hash != paste.HashedPassword) + if (!string.IsNullOrEmpty(password) && hash != hashedPass) { passModel.Error = true; passModel.ErrorMessage = "Invalid Password"; @@ -118,10 +129,10 @@ namespace Teknik.Areas.Paste.Controllers CachePassword(url, password); // Read in the file - if (string.IsNullOrEmpty(paste.FileName)) + if (string.IsNullOrEmpty(fileName)) return new StatusCodeResult(StatusCodes.Status404NotFound); var storageService = StorageServiceFactory.GetStorageService(_config.PasteConfig.StorageConfig); - var fileStream = storageService.GetFile(paste.FileName); + var fileStream = storageService.GetFile(fileName); if (fileStream == null) return new StatusCodeResult(StatusCodes.Status404NotFound); @@ -205,7 +216,7 @@ namespace Teknik.Areas.Paste.Controllers [TrackPageView] public async Task Edit(string url, string password) { - Models.Paste paste = _dbContext.Pastes.Where(p => p.Url == url).FirstOrDefault(); + Models.Paste paste = PasteHelper.GetPaste(_dbContext, url); if (paste != null) { if (paste.User?.Username != User.Identity.Name) @@ -217,7 +228,7 @@ namespace Teknik.Areas.Paste.Controllers // Check Expiration if (PasteHelper.CheckExpiration(paste)) { - PasteHelper.DeleteFile(_dbContext, _config, _logger, paste); + PasteHelper.DeleteFile(_dbContext, _config, _logger, url); return new StatusCodeResult(StatusCodes.Status404NotFound); } @@ -292,7 +303,7 @@ namespace Teknik.Areas.Paste.Controllers { try { - Models.Paste paste = _dbContext.Pastes.Where(p => p.Url == model.Url).FirstOrDefault(); + Models.Paste paste = PasteHelper.GetPaste(_dbContext, model.Url); if (paste != null) { if (paste.User?.Username != User.Identity.Name) @@ -350,8 +361,7 @@ namespace Teknik.Areas.Paste.Controllers paste.Syntax = model.Syntax; paste.DateEdited = DateTime.Now; - _dbContext.Entry(paste).State = EntityState.Modified; - _dbContext.SaveChanges(); + PasteHelper.ModifyPaste(_dbContext, paste); // Delete the old file storageService.DeleteFile(oldFile); @@ -371,13 +381,13 @@ namespace Teknik.Areas.Paste.Controllers [HttpOptions] public IActionResult Delete(string id) { - Models.Paste foundPaste = _dbContext.Pastes.Where(p => p.Url == id).FirstOrDefault(); + Models.Paste foundPaste = PasteHelper.GetPaste(_dbContext, id); if (foundPaste != null) { if (foundPaste.User?.Username == User.Identity.Name || User.IsInRole("Admin")) { - PasteHelper.DeleteFile(_dbContext, _config, _logger, foundPaste); + PasteHelper.DeleteFile(_dbContext, _config, _logger, id); return Json(new { result = true, redirect = Url.SubRouteUrl("p", "Paste.Index") }); } diff --git a/Teknik/Areas/Paste/PasteHelper.cs b/Teknik/Areas/Paste/PasteHelper.cs index 0599a20..4d90010 100644 --- a/Teknik/Areas/Paste/PasteHelper.cs +++ b/Teknik/Areas/Paste/PasteHelper.cs @@ -12,11 +12,15 @@ using System.IO; using Teknik.StorageService; using Teknik.Logging; using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore; namespace Teknik.Areas.Paste { public static class PasteHelper { + private static object _cacheLock = new object(); + private readonly static ObjectCache _pasteCache = new ObjectCache(300); + public static Models.Paste CreatePaste(Config config, TeknikEntities db, string content, string title = "", string syntax = "text", ExpirationUnit expireUnit = ExpirationUnit.Never, int expireLength = 1, string password = "") { Models.Paste paste = new Models.Paste(); @@ -120,8 +124,42 @@ namespace Teknik.Areas.Paste } } - public static void DeleteFile(TeknikEntities db, Config config, ILogger logger, Models.Paste paste) + public static void IncrementViewCount(IBackgroundTaskQueue queue, Config config, string url) + { + // Fire and forget updating of the download count + queue.QueueBackgroundWorkItem(async token => + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer(config.DbConnection); + + using (TeknikEntities db = new TeknikEntities(optionsBuilder.Options)) + { + var paste = GetPaste(db, url); + if (paste != null) + { + paste.Views++; + ModifyPaste(db, paste); + } + } + }); + } + + public static Models.Paste GetPaste(TeknikEntities db, string url) + { + lock (_cacheLock) + { + var paste = _pasteCache.GetObject(url, (key) => db.Pastes.FirstOrDefault(up => up.Url == key)); + + if (!db.Exists(paste)) + db.Attach(paste); + + return paste; + } + } + + public static void DeleteFile(TeknikEntities db, Config config, ILogger logger, string url) { + var paste = GetPaste(db, url); try { var storageService = StorageServiceFactory.GetStorageService(config.PasteConfig.StorageConfig); @@ -135,6 +173,28 @@ namespace Teknik.Areas.Paste // Delete from the DB db.Pastes.Remove(paste); db.SaveChanges(); + + // Remove from the cache + lock (_cacheLock) + { + _pasteCache.DeleteObject(url); + } + } + + public static void ModifyPaste(TeknikEntities db, Models.Paste paste) + { + // Update the cache's copy + lock (_cacheLock) + { + _pasteCache.UpdateObject(paste.Url, paste); + } + + if (!db.Exists(paste)) + db.Attach(paste); + + // Update the database + db.Entry(paste).State = EntityState.Modified; + db.SaveChanges(); } } } \ No newline at end of file diff --git a/Teknik/Areas/Upload/Controllers/UploadController.cs b/Teknik/Areas/Upload/Controllers/UploadController.cs index 7e1c3b6..9b10ac8 100644 --- a/Teknik/Areas/Upload/Controllers/UploadController.cs +++ b/Teknik/Areas/Upload/Controllers/UploadController.cs @@ -32,13 +32,10 @@ namespace Teknik.Areas.Upload.Controllers [Area("Upload")] public class UploadController : DefaultController { - 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; } diff --git a/Teknik/Areas/Upload/UploadHelper.cs b/Teknik/Areas/Upload/UploadHelper.cs index 206b668..e317404 100644 --- a/Teknik/Areas/Upload/UploadHelper.cs +++ b/Teknik/Areas/Upload/UploadHelper.cs @@ -156,7 +156,7 @@ namespace Teknik.Areas.Upload using (TeknikEntities db = new TeknikEntities(optionsBuilder.Options)) { - var upload = db.Uploads.FirstOrDefault(up => up.Url == url); + var upload = GetUpload(db, url); if (upload != null) { upload.Downloads++; @@ -170,13 +170,18 @@ namespace Teknik.Areas.Upload { lock (_cacheLock) { - return _uploadCache.GetObject(url, (key) => db.Uploads.FirstOrDefault(up => up.Url == key)); + var upload = _uploadCache.GetObject(url, (key) => db.Uploads.FirstOrDefault(up => up.Url == key)); + + if (!db.Exists(upload)) + db.Attach(upload); + + return upload; } } public static void DeleteFile(TeknikEntities db, Config config, ILogger logger, string url) { - var upload = db.Uploads.FirstOrDefault(up => up.Url == url); + var upload = GetUpload(db, url); try { var storageService = StorageServiceFactory.GetStorageService(config.UploadConfig.StorageConfig); @@ -187,15 +192,15 @@ namespace Teknik.Areas.Upload logger.LogError(ex, "Unable to delete file: {0}", upload.FileName); } + // Delete from the DB + db.Uploads.Remove(upload); + db.SaveChanges(); + // Remove from the cache lock (_cacheLock) { - _uploadCache.DeleteObject(upload.FileName); + _uploadCache.DeleteObject(url); } - - // Delete from the DB - db.Uploads.Remove(upload); - db.SaveChanges(); } public static void ModifyUpload(TeknikEntities db, Models.Upload upload) diff --git a/Teknik/Areas/User/Controllers/UserController.cs b/Teknik/Areas/User/Controllers/UserController.cs index fb69f31..1fc823a 100644 --- a/Teknik/Areas/User/Controllers/UserController.cs +++ b/Teknik/Areas/User/Controllers/UserController.cs @@ -1435,7 +1435,7 @@ namespace Teknik.Areas.Users.Controllers uploadController.ControllerContext = context; return uploadController.Delete(id); case "paste": - var pasteController = new Paste.Controllers.PasteController(_logger, _config, _dbContext); + var pasteController = new Paste.Controllers.PasteController(_logger, _config, _dbContext, queue); pasteController.ControllerContext = context; return pasteController.Delete(id); case "shortenedUrl": diff --git a/Teknik/Data/TeknikEntities.cs b/Teknik/Data/TeknikEntities.cs index 32f3681..1fe38ce 100644 --- a/Teknik/Data/TeknikEntities.cs +++ b/Teknik/Data/TeknikEntities.cs @@ -165,5 +165,10 @@ namespace Teknik.Data base.OnModelCreating(modelBuilder); } + + public bool Exists(T entity) where T : class + { + return this.Set().Local.Any(e => e == entity); + } } } diff --git a/Utilities/ObjectCache.cs b/Utilities/ObjectCache.cs index d951099..7898241 100644 --- a/Utilities/ObjectCache.cs +++ b/Utilities/ObjectCache.cs @@ -23,35 +23,40 @@ namespace Teknik.Utilities if (objectCache.TryGetValue(key, out var result) && result.Item1 > cacheDate.Subtract(new TimeSpan(0, 0, _cacheSeconds))) { - cacheDate = result.Item1; - foundObject = (T)result.Item2; + return (T)result.Item2; } else { foundObject = getObjectFunc(key); + // Update the cache for this key + if (foundObject != null) + UpdateObject(key, foundObject, cacheDate); } - 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); - } + UpdateObject(key, update, DateTime.UtcNow); + } + + public void UpdateObject(string key, T update, DateTime cacheTime) + { + objectCache[key] = new Tuple(cacheTime, update); } public void DeleteObject(string key) { objectCache.Remove(key); } + + public bool CacheValid(string key) + { + if (objectCache.TryGetValue(key, out var result) && + result.Item1 > DateTime.UtcNow.Subtract(new TimeSpan(0, 0, _cacheSeconds))) + return true; + return false; + } } }