1
0
mirror of https://git.teknik.io/Teknikode/Teknik.git synced 2023-08-02 14:16:22 +02:00

- Added cache control for pastes.

- Made updates not need to re-query for data.
This commit is contained in:
Uncled1023 2022-03-13 23:21:00 -07:00
parent 035c927326
commit c7fc3b8658
8 changed files with 133 additions and 51 deletions

View File

@ -276,7 +276,7 @@ namespace Teknik.Areas.Admin.Controllers
uploadController.ControllerContext = context; uploadController.ControllerContext = context;
return uploadController.Delete(id); return uploadController.Delete(id);
case "paste": case "paste":
var pasteController = new Paste.Controllers.PasteController(_logger, _config, _dbContext); var pasteController = new Paste.Controllers.PasteController(_logger, _config, _dbContext, queue);
pasteController.ControllerContext = context; pasteController.ControllerContext = context;
return pasteController.Delete(id); return pasteController.Delete(id);
case "shortenedUrl": case "shortenedUrl":

View File

@ -30,7 +30,12 @@ namespace Teknik.Areas.Paste.Controllers
[Area("Paste")] [Area("Paste")]
public class PasteController : DefaultController public class PasteController : DefaultController
{ {
public PasteController(ILogger<Logger> logger, Config config, TeknikEntities dbContext) : base(logger, config, dbContext) { } private readonly IBackgroundTaskQueue _queue;
public PasteController(ILogger<Logger> logger, Config config, TeknikEntities dbContext, IBackgroundTaskQueue queue) : base(logger, config, dbContext)
{
_queue = queue;
}
[AllowAnonymous] [AllowAnonymous]
[TrackPageView] [TrackPageView]
@ -46,23 +51,29 @@ namespace Teknik.Areas.Paste.Controllers
[TrackPageView] [TrackPageView]
public async Task<IActionResult> ViewPaste(string type, string url, string password) public async Task<IActionResult> 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) if (paste != null)
{ {
ViewBag.Title = (string.IsNullOrEmpty(paste.Title)) ? "Untitled Paste" : paste.Title + " | Pastebin"; 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."; 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; string fileName = paste.FileName;
_dbContext.Entry(paste).State = EntityState.Modified; string key = paste.Key;
_dbContext.SaveChanges(); string iv = paste.IV;
int blockSize = paste.BlockSize;
int keySize = paste.KeySize;
string hashedPass = paste.HashedPassword;
// Check Expiration // Check Expiration
if (PasteHelper.CheckExpiration(paste)) if (PasteHelper.CheckExpiration(paste))
{ {
PasteHelper.DeleteFile(_dbContext, _config, _logger, paste); PasteHelper.DeleteFile(_dbContext, _config, _logger, url);
return new StatusCodeResult(StatusCodes.Status404NotFound); return new StatusCodeResult(StatusCodes.Status404NotFound);
} }
// Increment View Count
PasteHelper.IncrementViewCount(_queue, _config, url);
PasteViewModel model = new PasteViewModel(); PasteViewModel model = new PasteViewModel();
model.Url = url; model.Url = url;
model.Title = paste.Title; 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[] ivBytes = (string.IsNullOrEmpty(iv)) ? new byte[blockSize] : Encoding.Unicode.GetBytes(iv);
byte[] keyBytes = (string.IsNullOrEmpty(paste.Key)) ? new byte[paste.KeySize] : AesCounterManaged.CreateKey(paste.Key, ivBytes, paste.KeySize); byte[] keyBytes = (string.IsNullOrEmpty(key)) ? new byte[keySize] : AesCounterManaged.CreateKey(key, ivBytes, keySize);
// The paste has a password set // The paste has a password set
if (!string.IsNullOrEmpty(paste.HashedPassword)) if (!string.IsNullOrEmpty(hashedPass))
{ {
if (string.IsNullOrEmpty(password)) if (string.IsNullOrEmpty(password))
{ {
@ -93,17 +104,17 @@ namespace Teknik.Areas.Paste.Controllers
string hash = string.Empty; string hash = string.Empty;
if (!string.IsNullOrEmpty(password)) if (!string.IsNullOrEmpty(password))
{ {
hash = Crypto.HashPassword(paste.Key, password); hash = Crypto.HashPassword(key, password);
keyBytes = AesCounterManaged.CreateKey(password, ivBytes, paste.KeySize); keyBytes = AesCounterManaged.CreateKey(password, ivBytes, keySize);
} }
if (string.IsNullOrEmpty(password) || hash != paste.HashedPassword) if (string.IsNullOrEmpty(password) || hash != hashedPass)
{ {
PasswordViewModel passModel = new PasswordViewModel(); PasswordViewModel passModel = new PasswordViewModel();
passModel.ActionUrl = Url.SubRouteUrl("p", "Paste.View", new { type = type, url = url }); passModel.ActionUrl = Url.SubRouteUrl("p", "Paste.View", new { type = type, url = url });
passModel.Url = url; passModel.Url = url;
passModel.Type = type; passModel.Type = type;
if (!string.IsNullOrEmpty(password) && hash != paste.HashedPassword) if (!string.IsNullOrEmpty(password) && hash != hashedPass)
{ {
passModel.Error = true; passModel.Error = true;
passModel.ErrorMessage = "Invalid Password"; passModel.ErrorMessage = "Invalid Password";
@ -118,10 +129,10 @@ namespace Teknik.Areas.Paste.Controllers
CachePassword(url, password); CachePassword(url, password);
// Read in the file // Read in the file
if (string.IsNullOrEmpty(paste.FileName)) if (string.IsNullOrEmpty(fileName))
return new StatusCodeResult(StatusCodes.Status404NotFound); return new StatusCodeResult(StatusCodes.Status404NotFound);
var storageService = StorageServiceFactory.GetStorageService(_config.PasteConfig.StorageConfig); var storageService = StorageServiceFactory.GetStorageService(_config.PasteConfig.StorageConfig);
var fileStream = storageService.GetFile(paste.FileName); var fileStream = storageService.GetFile(fileName);
if (fileStream == null) if (fileStream == null)
return new StatusCodeResult(StatusCodes.Status404NotFound); return new StatusCodeResult(StatusCodes.Status404NotFound);
@ -205,7 +216,7 @@ namespace Teknik.Areas.Paste.Controllers
[TrackPageView] [TrackPageView]
public async Task<IActionResult> Edit(string url, string password) public async Task<IActionResult> 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 != null)
{ {
if (paste.User?.Username != User.Identity.Name) if (paste.User?.Username != User.Identity.Name)
@ -217,7 +228,7 @@ namespace Teknik.Areas.Paste.Controllers
// Check Expiration // Check Expiration
if (PasteHelper.CheckExpiration(paste)) if (PasteHelper.CheckExpiration(paste))
{ {
PasteHelper.DeleteFile(_dbContext, _config, _logger, paste); PasteHelper.DeleteFile(_dbContext, _config, _logger, url);
return new StatusCodeResult(StatusCodes.Status404NotFound); return new StatusCodeResult(StatusCodes.Status404NotFound);
} }
@ -292,7 +303,7 @@ namespace Teknik.Areas.Paste.Controllers
{ {
try 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 != null)
{ {
if (paste.User?.Username != User.Identity.Name) if (paste.User?.Username != User.Identity.Name)
@ -350,8 +361,7 @@ namespace Teknik.Areas.Paste.Controllers
paste.Syntax = model.Syntax; paste.Syntax = model.Syntax;
paste.DateEdited = DateTime.Now; paste.DateEdited = DateTime.Now;
_dbContext.Entry(paste).State = EntityState.Modified; PasteHelper.ModifyPaste(_dbContext, paste);
_dbContext.SaveChanges();
// Delete the old file // Delete the old file
storageService.DeleteFile(oldFile); storageService.DeleteFile(oldFile);
@ -371,13 +381,13 @@ namespace Teknik.Areas.Paste.Controllers
[HttpOptions] [HttpOptions]
public IActionResult Delete(string id) 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 != null)
{ {
if (foundPaste.User?.Username == User.Identity.Name || if (foundPaste.User?.Username == User.Identity.Name ||
User.IsInRole("Admin")) 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") }); return Json(new { result = true, redirect = Url.SubRouteUrl("p", "Paste.Index") });
} }

View File

@ -12,11 +12,15 @@ using System.IO;
using Teknik.StorageService; using Teknik.StorageService;
using Teknik.Logging; using Teknik.Logging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
namespace Teknik.Areas.Paste namespace Teknik.Areas.Paste
{ {
public static class PasteHelper 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 = "") 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(); Models.Paste paste = new Models.Paste();
@ -120,8 +124,42 @@ namespace Teknik.Areas.Paste
} }
} }
public static void DeleteFile(TeknikEntities db, Config config, ILogger<Logger> 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<TeknikEntities>();
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> logger, string url)
{
var paste = GetPaste(db, url);
try try
{ {
var storageService = StorageServiceFactory.GetStorageService(config.PasteConfig.StorageConfig); var storageService = StorageServiceFactory.GetStorageService(config.PasteConfig.StorageConfig);
@ -135,6 +173,28 @@ namespace Teknik.Areas.Paste
// Delete from the DB // Delete from the DB
db.Pastes.Remove(paste); db.Pastes.Remove(paste);
db.SaveChanges(); 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();
} }
} }
} }

View File

@ -32,13 +32,10 @@ namespace Teknik.Areas.Upload.Controllers
[Area("Upload")] [Area("Upload")]
public class UploadController : DefaultController public class UploadController : DefaultController
{ {
private const int _cacheLength = 300;
private readonly ObjectCache _uploadCache;
private readonly IBackgroundTaskQueue _queue; private readonly IBackgroundTaskQueue _queue;
public UploadController(ILogger<Logger> logger, Config config, TeknikEntities dbContext, IBackgroundTaskQueue queue) : base(logger, config, dbContext) public UploadController(ILogger<Logger> logger, Config config, TeknikEntities dbContext, IBackgroundTaskQueue queue) : base(logger, config, dbContext)
{ {
_uploadCache = new ObjectCache(_cacheLength);
_queue = queue; _queue = queue;
} }

View File

@ -156,7 +156,7 @@ namespace Teknik.Areas.Upload
using (TeknikEntities db = new TeknikEntities(optionsBuilder.Options)) using (TeknikEntities db = new TeknikEntities(optionsBuilder.Options))
{ {
var upload = db.Uploads.FirstOrDefault(up => up.Url == url); var upload = GetUpload(db, url);
if (upload != null) if (upload != null)
{ {
upload.Downloads++; upload.Downloads++;
@ -170,13 +170,18 @@ namespace Teknik.Areas.Upload
{ {
lock (_cacheLock) 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> logger, string url) public static void DeleteFile(TeknikEntities db, Config config, ILogger<Logger> logger, string url)
{ {
var upload = db.Uploads.FirstOrDefault(up => up.Url == url); var upload = GetUpload(db, url);
try try
{ {
var storageService = StorageServiceFactory.GetStorageService(config.UploadConfig.StorageConfig); 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); logger.LogError(ex, "Unable to delete file: {0}", upload.FileName);
} }
// Remove from the cache
lock (_cacheLock)
{
_uploadCache.DeleteObject(upload.FileName);
}
// Delete from the DB // Delete from the DB
db.Uploads.Remove(upload); db.Uploads.Remove(upload);
db.SaveChanges(); db.SaveChanges();
// Remove from the cache
lock (_cacheLock)
{
_uploadCache.DeleteObject(url);
}
} }
public static void ModifyUpload(TeknikEntities db, Models.Upload upload) public static void ModifyUpload(TeknikEntities db, Models.Upload upload)

View File

@ -1435,7 +1435,7 @@ namespace Teknik.Areas.Users.Controllers
uploadController.ControllerContext = context; uploadController.ControllerContext = context;
return uploadController.Delete(id); return uploadController.Delete(id);
case "paste": case "paste":
var pasteController = new Paste.Controllers.PasteController(_logger, _config, _dbContext); var pasteController = new Paste.Controllers.PasteController(_logger, _config, _dbContext, queue);
pasteController.ControllerContext = context; pasteController.ControllerContext = context;
return pasteController.Delete(id); return pasteController.Delete(id);
case "shortenedUrl": case "shortenedUrl":

View File

@ -165,5 +165,10 @@ namespace Teknik.Data
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
} }
public bool Exists<T>(T entity) where T : class
{
return this.Set<T>().Local.Any(e => e == entity);
}
} }
} }

View File

@ -23,35 +23,40 @@ namespace Teknik.Utilities
if (objectCache.TryGetValue(key, out var result) && if (objectCache.TryGetValue(key, out var result) &&
result.Item1 > cacheDate.Subtract(new TimeSpan(0, 0, _cacheSeconds))) result.Item1 > cacheDate.Subtract(new TimeSpan(0, 0, _cacheSeconds)))
{ {
cacheDate = result.Item1; return (T)result.Item2;
foundObject = (T)result.Item2;
} }
else else
{ {
foundObject = getObjectFunc(key); foundObject = getObjectFunc(key);
// Update the cache for this key
if (foundObject != null)
UpdateObject(key, foundObject, cacheDate);
} }
if (foundObject != null)
objectCache[key] = new Tuple<DateTime, object>(cacheDate, foundObject);
return foundObject; return foundObject;
} }
public void UpdateObject<T>(string key, T update) public void UpdateObject<T>(string key, T update)
{ {
var cacheDate = DateTime.UtcNow; UpdateObject(key, update, DateTime.UtcNow);
if (objectCache.TryGetValue(key, out var result)) }
{
if (result.Item1 <= cacheDate.Subtract(new TimeSpan(0, 0, _cacheSeconds))) public void UpdateObject<T>(string key, T update, DateTime cacheTime)
DeleteObject(key); {
else objectCache[key] = new Tuple<DateTime, object>(cacheTime, update);
objectCache[key] = new Tuple<DateTime, object>(result.Item1, update);
}
} }
public void DeleteObject(string key) public void DeleteObject(string key)
{ {
objectCache.Remove(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;
}
} }
} }