Browse Source

- Added cache control for pastes.

- Made updates not need to re-query for data.
master
Teknikode 2 months ago
parent
commit
c7fc3b8658
  1. 2
      Teknik/Areas/Admin/Controllers/AdminController.cs
  2. 56
      Teknik/Areas/Paste/Controllers/PasteController.cs
  3. 62
      Teknik/Areas/Paste/PasteHelper.cs
  4. 3
      Teknik/Areas/Upload/Controllers/UploadController.cs
  5. 21
      Teknik/Areas/Upload/UploadHelper.cs
  6. 2
      Teknik/Areas/User/Controllers/UserController.cs
  7. 5
      Teknik/Data/TeknikEntities.cs
  8. 31
      Utilities/ObjectCache.cs

2
Teknik/Areas/Admin/Controllers/AdminController.cs

@ -276,7 +276,7 @@ namespace Teknik.Areas.Admin.Controllers @@ -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":

56
Teknik/Areas/Paste/Controllers/PasteController.cs

@ -30,7 +30,12 @@ namespace Teknik.Areas.Paste.Controllers @@ -30,7 +30,12 @@ namespace Teknik.Areas.Paste.Controllers
[Area("Paste")]
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]
[TrackPageView]
@ -46,23 +51,29 @@ namespace Teknik.Areas.Paste.Controllers @@ -46,23 +51,29 @@ namespace Teknik.Areas.Paste.Controllers
[TrackPageView]
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)
{
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 @@ -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 @@ -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 @@ -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 @@ -205,7 +216,7 @@ namespace Teknik.Areas.Paste.Controllers
[TrackPageView]
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.User?.Username != User.Identity.Name)
@ -217,7 +228,7 @@ namespace Teknik.Areas.Paste.Controllers @@ -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 @@ -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 @@ -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 @@ -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") });
}

62
Teknik/Areas/Paste/PasteHelper.cs

@ -12,11 +12,15 @@ using System.IO; @@ -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 @@ -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
{
var storageService = StorageServiceFactory.GetStorageService(config.PasteConfig.StorageConfig);
@ -135,6 +173,28 @@ namespace Teknik.Areas.Paste @@ -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();
}
}
}

3
Teknik/Areas/Upload/Controllers/UploadController.cs

@ -32,13 +32,10 @@ namespace Teknik.Areas.Upload.Controllers @@ -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> logger, Config config, TeknikEntities dbContext, IBackgroundTaskQueue queue) : base(logger, config, dbContext)
{
_uploadCache = new ObjectCache(_cacheLength);
_queue = queue;
}

21
Teknik/Areas/Upload/UploadHelper.cs

@ -156,7 +156,7 @@ namespace Teknik.Areas.Upload @@ -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 @@ -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> 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 @@ -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)

2
Teknik/Areas/User/Controllers/UserController.cs

@ -1435,7 +1435,7 @@ namespace Teknik.Areas.Users.Controllers @@ -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":

5
Teknik/Data/TeknikEntities.cs

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

31
Utilities/ObjectCache.cs

@ -23,35 +23,40 @@ namespace Teknik.Utilities @@ -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<DateTime, object>(cacheDate, foundObject);
return foundObject;
}
public void UpdateObject<T>(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<DateTime, object>(result.Item1, update);
}
UpdateObject(key, update, DateTime.UtcNow);
}
public void UpdateObject<T>(string key, T update, DateTime cacheTime)
{
objectCache[key] = new Tuple<DateTime, object>(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;
}
}
}

Loading…
Cancel
Save