From 494fc9d7d1f08e05e1ee196bd0746900343b51dd Mon Sep 17 00:00:00 2001 From: ivarlovlie Date: Mon, 16 Jan 2023 00:13:54 +0100 Subject: feat: Fully implemented text/file api, almost finished autodelete service --- src/DB.cs | 49 ---- src/Eva.cs | 52 +++++ .../20230112230354_InitialCreate.Designer.cs | 2 +- .../20230115182252_DeletionKey.Designer.cs | 122 ++++++++++ src/Migrations/20230115182252_DeletionKey.cs | 40 ++++ src/Migrations/DBModelSnapshot.cs | 10 +- src/Program.cs | 252 +++++++++++++++++---- src/Tools.cs | 67 ++++++ src/Util.cs | 14 -- src/WallE.cs | 74 ++++++ src/wwwroot/index.css | 80 +++++++ src/wwwroot/index.html | 88 +------ 12 files changed, 661 insertions(+), 189 deletions(-) delete mode 100644 src/DB.cs create mode 100644 src/Eva.cs create mode 100644 src/Migrations/20230115182252_DeletionKey.Designer.cs create mode 100644 src/Migrations/20230115182252_DeletionKey.cs create mode 100644 src/Tools.cs delete mode 100644 src/Util.cs create mode 100644 src/WallE.cs create mode 100644 src/wwwroot/index.css (limited to 'src') diff --git a/src/DB.cs b/src/DB.cs deleted file mode 100644 index 3555a99..0000000 --- a/src/DB.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace BlobBin; - -public sealed class DB : DbContext -{ - public DB(DbContextOptions options) : base(options) { - Database.Migrate(); - } - - public DbSet Files { get; set; } - public DbSet Pastes { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - optionsBuilder.UseSqlite("data source = AppData/main.db"); - base.OnConfiguring(optionsBuilder); - } -} - -public class UploadEntityBase -{ - public UploadEntityBase() { - Id = Guid.NewGuid(); - CreatedAt = DateTime.UtcNow; - } - - public Guid Id { get; set; } - public string PublicId { get; set; } - public DateTime CreatedAt { get; set; } - public string CreatedBy { get; set; } - public DateTime? DeletedAt { get; set; } - public string? PasswordHash { get; set; } - public bool Singleton { get; set; } - public string? AutoDeleteAfter { get; set; } - public string? MimeType { get; set; } -} - -public class File : UploadEntityBase -{ - public string? Name { get; set; } - public long Length { get; set; } -} - -public class Paste : UploadEntityBase -{ - public string? Name { get; set; } - public string? Content { get; set; } - public long Length { get; set; } -} \ No newline at end of file diff --git a/src/Eva.cs b/src/Eva.cs new file mode 100644 index 0000000..760231e --- /dev/null +++ b/src/Eva.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore; + +namespace BlobBin; + +public sealed class Eva : DbContext +{ + private readonly bool migrated; + + public Eva(DbContextOptions options) : base(options) { + if (!migrated) { + Database.Migrate(); + migrated = true; + } + } + + public DbSet Files { get; set; } + public DbSet Pastes { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { + optionsBuilder.UseSqlite("data source = AppData/main.db"); + base.OnConfiguring(optionsBuilder); + } +} + +public class UploadEntityBase +{ + public UploadEntityBase() { + Id = Guid.NewGuid(); + CreatedAt = DateTime.UtcNow; + } + + public Guid Id { get; set; } + public string PublicId { get; set; } + public DateTime CreatedAt { get; set; } + public string CreatedBy { get; set; } + public DateTime? DeletedAt { get; set; } + public string? PasswordHash { get; set; } + public bool Singleton { get; set; } + public string? AutoDeleteAfter { get; set; } + public string? MimeType { get; set; } + public string DeletionKey { get; set; } + public string? Name { get; set; } + public long Length { get; set; } +} + +public class Paste : UploadEntityBase +{ + public string? Content { get; set; } +} + +public class File : UploadEntityBase +{ } \ No newline at end of file diff --git a/src/Migrations/20230112230354_InitialCreate.Designer.cs b/src/Migrations/20230112230354_InitialCreate.Designer.cs index b5a06c3..1928f19 100644 --- a/src/Migrations/20230112230354_InitialCreate.Designer.cs +++ b/src/Migrations/20230112230354_InitialCreate.Designer.cs @@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace BlobBin.Migrations { - [DbContext(typeof(DB))] + [DbContext(typeof(Eva))] [Migration("20230112230354_InitialCreate")] partial class InitialCreate { diff --git a/src/Migrations/20230115182252_DeletionKey.Designer.cs b/src/Migrations/20230115182252_DeletionKey.Designer.cs new file mode 100644 index 0000000..baf9730 --- /dev/null +++ b/src/Migrations/20230115182252_DeletionKey.Designer.cs @@ -0,0 +1,122 @@ +// +using System; +using BlobBin; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace BlobBin.Migrations +{ + [DbContext(typeof(Eva))] + [Migration("20230115182252_DeletionKey")] + partial class DeletionKey + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); + + modelBuilder.Entity("BlobBin.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AutoDeleteAfter") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DeletionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Length") + .HasColumnType("INTEGER"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PublicId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Singleton") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("BlobBin.Paste", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AutoDeleteAfter") + .HasColumnType("TEXT"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DeletionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Length") + .HasColumnType("INTEGER"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PublicId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Singleton") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Pastes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Migrations/20230115182252_DeletionKey.cs b/src/Migrations/20230115182252_DeletionKey.cs new file mode 100644 index 0000000..3feba6b --- /dev/null +++ b/src/Migrations/20230115182252_DeletionKey.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BlobBin.Migrations +{ + /// + public partial class DeletionKey : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DeletionKey", + table: "Pastes", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "DeletionKey", + table: "Files", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DeletionKey", + table: "Pastes"); + + migrationBuilder.DropColumn( + name: "DeletionKey", + table: "Files"); + } + } +} diff --git a/src/Migrations/DBModelSnapshot.cs b/src/Migrations/DBModelSnapshot.cs index 8c9fb26..e63af59 100644 --- a/src/Migrations/DBModelSnapshot.cs +++ b/src/Migrations/DBModelSnapshot.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace BlobBin.Migrations { - [DbContext(typeof(DB))] + [DbContext(typeof(Eva))] partial class DBModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) @@ -36,6 +36,10 @@ namespace BlobBin.Migrations b.Property("DeletedAt") .HasColumnType("TEXT"); + b.Property("DeletionKey") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("Length") .HasColumnType("INTEGER"); @@ -82,6 +86,10 @@ namespace BlobBin.Migrations b.Property("DeletedAt") .HasColumnType("TEXT"); + b.Property("DeletionKey") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("Length") .HasColumnType("INTEGER"); diff --git a/src/Program.cs b/src/Program.cs index 879afb1..9d5cfdd 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,24 +1,76 @@ global using BlobBin; +using System.Text; +using System.Text.Json; using IOL.Helpers; -using Microsoft.AspNetCore.Http.Features; using File = BlobBin.File; +const long MAX_REQUEST_BODY_SIZE = 104_857_600; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddDbContext(); -builder.WebHost.UseKestrel(o => { o.Limits.MaxRequestBodySize = 100_000_000; }); +builder.Services.AddDbContext(); +builder.Services.AddHostedService(); +builder.WebHost.UseKestrel(o => { o.Limits.MaxRequestBodySize = MAX_REQUEST_BODY_SIZE; }); var app = builder.Build(); - app.UseFileServer(); app.UseStatusCodePages(); -app.MapGet("/upload-link", GetUploadLink); -app.MapPost("/upload/{id}", UploadBig); -app.MapPost("/upload", UploadSimple); +app.MapGet("/upload-link", GetFileUploadLink); +app.MapPost("/file/{id}", UploadFilePart); +app.MapPost("/file", UploadFile); app.MapPost("/text", UploadText); -app.MapGet("/b/{id}", GetBlob); -Util.GetFilesDirectoryPath(true); +app.MapGet("/b/{id}/delete", DeleteUpload); +app.MapGet("/p/{id}/delete", DeleteUpload); +app.MapPost("/b/{id}", GetFile); +app.MapGet("/b/{id}", GetFile); +app.MapGet("/p/{id}", GetPaste); +app.MapPost("/p/{id}", GetPaste); +Tools.GetFilesDirectoryPath(true); app.Run(); -IResult GetUploadLink(HttpContext context, DB db) { +IResult DeleteUpload(HttpContext context, Eva db, string id, string key = default, bool confirmed = false) { + if (key.IsNullOrWhiteSpace()) { + return Results.BadRequest("No key was found"); + } + + var isPaste = context.Request.Path.StartsWithSegments("/p"); + UploadEntityBase? upload = isPaste + ? db.Pastes.FirstOrDefault(c => c.PublicId == id) + : db.Files.FirstOrDefault(c => c.PublicId == id); + + if (upload is not {DeletedAt: null}) { + return Results.NotFound(); + } + + if (upload.DeletionKey != key) { + return Results.Text("Invalid key", default, default, 400); + } + + if (!confirmed) { + return Results.Content($""" + + + + + + {upload.PublicId} - Confirm deletion - Blobbin + + +

Are you sure you want to delete {upload.Name}?

+ Yes + + No, cancel + + +""", "text/html"); + } + + upload.DeletedAt = DateTime.UtcNow; + db.SaveChanges(); + + return Results.Text(""" +The file is marked for deletion and cannot be accessed any more, all traces off it will be gone from our systems within 7 days. +"""); +} + +IResult GetFileUploadLink(HttpContext context, Eva db) { var file = new File { CreatedBy = context.Request.Headers["X-Forwarded-For"].ToString() }; @@ -31,7 +83,7 @@ IResult GetUploadLink(HttpContext context, DB db) { ); } -async Task UploadSimple(HttpContext context, DB db) { +async Task UploadFile(HttpContext context, Eva db) { if (!context.Request.Form.Files.Any()) { return Results.BadRequest("No files was found in request"); } @@ -43,7 +95,8 @@ async Task UploadSimple(HttpContext context, DB db) { Length = context.Request.Form.Files[0].Length, Name = context.Request.Form.Files[0].FileName, MimeType = context.Request.Form.Files[0].ContentType, - PublicId = GetUnusedBlobId(db) + PublicId = GetUnusedPublicFileId(db), + DeletionKey = RandomString.Generate(6), }; if (context.Request.Form["password"].ToString().HasValue()) { @@ -51,39 +104,164 @@ async Task UploadSimple(HttpContext context, DB db) { } await using var write = System.IO.File.OpenWrite( - Path.Combine(Util.GetFilesDirectoryPath(), file.Id.ToString()) + Path.Combine(Tools.GetFilesDirectoryPath(), file.Id.ToString()) ); await context.Request.Form.Files[0].CopyToAsync(write); db.Files.Add(file); db.SaveChanges(); - return Results.Text( - context.Request.GetRequestHost() - + "/b/" - + file.PublicId - ); + var deletionNote = "The file is only deleted when you request it."; + if (file.AutoDeleteAfter.HasValue()) { + var relativeDateTime = file.CreatedAt.Add(Tools.ParseHumanTimeSpan(file.AutoDeleteAfter)); + deletionNote = $"The file will be automatically deleted at {relativeDateTime:u}"; + } + + return Results.Text($""" +Your file is available here: {context.Request.GetRequestHost()}/b/{file.PublicId} + +To delete the file, open this url in a browser {context.Request.GetRequestHost()}/b/{file.PublicId}/delete?key={file.DeletionKey}. +{deletionNote} +"""); } -IResult UploadBig(HttpContext context, DB db) { +IResult UploadFilePart(HttpContext context, Eva db) { return Results.Ok(); } -IResult UploadText(HttpContext context, DB db) { - return Results.Ok(); +async Task UploadText(HttpContext context, Eva db) { + if (context.Request.Form["content"].ToString().IsNullOrWhiteSpace()) { + return Results.Text("No content was found in request", default, default, 400); + } + + var paste = new Paste { + CreatedBy = context.Request.Headers["X-Forwarded-For"].ToString(), + Singleton = context.Request.Form["singleton"] == "on", + AutoDeleteAfter = context.Request.Form["autoDeleteAfter"], + Length = context.Request.Form["content"].Count, + Name = context.Request.Form["name"], + MimeType = context.Request.Form["mime"], + PublicId = GetUnusedPublicPasteId(db), + Content = context.Request.Form["content"], + DeletionKey = RandomString.Generate(6), + }; + + if (paste.MimeType.IsNullOrWhiteSpace()) { + paste.MimeType = "text/plain"; + } + + if (context.Request.Form["password"].ToString().HasValue()) { + paste.PasswordHash = PasswordHelper.HashPassword(context.Request.Form["password"]); + } + + db.Pastes.Add(paste); + db.SaveChanges(); + var deletionNote = "The paste is only deleted when you request it."; + if (paste.AutoDeleteAfter.HasValue()) { + var relativeDateTime = paste.CreatedAt.Add(Tools.ParseHumanTimeSpan(paste.AutoDeleteAfter)); + deletionNote = $"The paste will be automatically deleted at {relativeDateTime:u}"; + } + + return Results.Text($""" +Your paste is available here: {context.Request.GetRequestHost()}/p/{paste.PublicId} + +To delete the paste, open this url in a browser {context.Request.GetRequestHost()}/p/{paste.PublicId}/delete?key={paste.DeletionKey}. +{deletionNote} +"""); +} + +async Task GetPaste(HttpContext context, string id, Eva db) { + var paste = db.Pastes.FirstOrDefault(c => c.PublicId == id.Trim()); + if (paste is not {DeletedAt: null}) return Results.NotFound(); + if (paste.PasswordHash.HasValue()) { + var password = context.Request.Method == "POST" ? context.Request.Form["password"].ToString() : ""; + if (password.IsNullOrWhiteSpace() || !PasswordHelper.Verify(password, paste.PasswordHash)) { + return Results.Content($""" + + + + + + {paste.PublicId} - Authenticate - Blobbin + + +
+

Authenticate to access this paste:

+ + +
+ + +""", "text/html"); + } + } + + if (paste.Singleton) { + paste.DeletedAt = DateTime.UtcNow; + db.SaveChanges(); + } + + if (ShouldDeleteUpload(paste)) { + paste.DeletedAt = DateTime.UtcNow; + db.SaveChanges(); + } + + Console.WriteLine(JsonSerializer.Serialize(paste)); + return Results.Content(paste.Content, paste.MimeType, Encoding.UTF8); } -async Task GetBlob(string id, DB db) { +async Task GetFile(HttpContext context, Eva db, string id, bool download = false) { var file = db.Files.FirstOrDefault(c => c.PublicId == id.Trim()); - if (file == default) return Results.NotFound(); + if (file is not {DeletedAt: null}) return Results.NotFound(); + if (file.PasswordHash.HasValue()) { + var password = context.Request.Method == "POST" ? context.Request.Form["password"].ToString() : ""; + if (password.IsNullOrWhiteSpace() || !PasswordHelper.Verify(password, file.PasswordHash)) { + return Results.Content($""" + + + + + + {file.PublicId} - Authenticate - Blobbin + + +
+

Authenticate to access this file:

+ + +
+ + +""", "text/html"); + } + } + + if (file.Singleton) { + file.DeletedAt = DateTime.UtcNow; + db.SaveChanges(); + } + + if (ShouldDeleteUpload(file)) { + file.DeletedAt = DateTime.UtcNow; + db.SaveChanges(); + } + var reader = await System.IO.File.ReadAllBytesAsync( Path.Combine( - Util.GetFilesDirectoryPath(), file.Id.ToString() + Tools.GetFilesDirectoryPath(), file.Id.ToString() ) ); - return Results.File(reader, file.MimeType, file.Name); + return download ? Results.File(reader, file.MimeType, file.Name) : Results.Bytes(reader, file.MimeType); } +bool ShouldDeleteUpload(UploadEntityBase entity) { + if (entity.AutoDeleteAfter.IsNullOrWhiteSpace()) { + return false; + } + + var deletedDateTime = entity.CreatedAt.Add(Tools.ParseHumanTimeSpan(entity.AutoDeleteAfter)); + return DateTime.Compare(DateTime.UtcNow, deletedDateTime) > 0; +} -string GetUnusedBlobId(DB db) { +string GetUnusedPublicFileId(Eva db) { string id() => RandomString.Generate(3); var res = id(); while (db.Files.Any(c => c.PublicId == res)) { @@ -93,20 +271,12 @@ string GetUnusedBlobId(DB db) { return res; } -class BlobBase -{ - public string Password { get; set; } - public bool Singleton { get; set; } - public string AutoDeleteAfter { get; set; } -} - -class PasteRequest : BlobBase -{ - public string Text { get; set; } - public string Mime { get; set; } -} +string GetUnusedPublicPasteId(Eva db) { + string id() => RandomString.Generate(3); + var res = id(); + while (db.Pastes.Any(c => c.PublicId == res)) { + res = id(); + } -class UploadRequest : BlobBase -{ - public IFormFile? File { get; set; } + return res; } \ No newline at end of file diff --git a/src/Tools.cs b/src/Tools.cs new file mode 100644 index 0000000..8aaedc9 --- /dev/null +++ b/src/Tools.cs @@ -0,0 +1,67 @@ +using System.Text.RegularExpressions; + +namespace BlobBin; + +public static class Tools +{ + public static string GetFilesDirectoryPath(bool createIfNotExists = false) { + var filesDirectoryPath = Path.Combine( + Directory.GetCurrentDirectory(), + "AppData", + "files" + ); + if (createIfNotExists) Directory.CreateDirectory(filesDirectoryPath); + return filesDirectoryPath; + } + + public static TimeSpan ParseHumanTimeSpan(string dateTime) { + var ts = TimeSpan.Zero; + var currentString = ""; + var currentNumber = ""; + foreach (var ch in dateTime + ' ') { + currentString += ch; + if (Regex.IsMatch(currentString, @"^(years(\d|\s)|year(\d|\s)|y(\d|\s))", RegexOptions.IgnoreCase)) { + ts = ts.Add(TimeSpan.FromDays(365 * int.Parse(currentNumber))); + currentString = ""; + currentNumber = ""; + } + + if (Regex.IsMatch(currentString, @"^(weeks(\d|\s)|week(\d|\s)|w(\d|\s))", RegexOptions.IgnoreCase)) { + ts = ts.Add(TimeSpan.FromDays(7 * int.Parse(currentNumber))); + currentString = ""; + currentNumber = ""; + } + + if (Regex.IsMatch(currentString, @"^(days(\d|\s)|day(\d|\s)|d(\d|\s))", RegexOptions.IgnoreCase)) { + ts = ts.Add(TimeSpan.FromDays(int.Parse(currentNumber))); + currentString = ""; + currentNumber = ""; + } + + if (Regex.IsMatch(currentString, @"^(hours(\d|\s)|hour(\d|\s)|h(\d|\s))", RegexOptions.IgnoreCase)) { + ts = ts.Add(TimeSpan.FromHours(int.Parse(currentNumber))); + currentString = ""; + currentNumber = ""; + } + + if (Regex.IsMatch(currentString, @"^(mins(\d|\s)|min(\d|\s)|m(\d|\s))", RegexOptions.IgnoreCase)) { + ts = ts.Add(TimeSpan.FromMinutes(int.Parse(currentNumber))); + currentString = ""; + currentNumber = ""; + } + + if (Regex.IsMatch(currentString, @"^(secs(\d|\s)|sec(\d|\s)|s(\d|\s))", RegexOptions.IgnoreCase)) { + ts = ts.Add(TimeSpan.FromSeconds(int.Parse(currentNumber))); + currentString = ""; + currentNumber = ""; + } + + if (Regex.IsMatch(ch.ToString(), @"\d")) { + currentNumber += ch; + currentString = ""; + } + } + + return ts; + } +} \ No newline at end of file diff --git a/src/Util.cs b/src/Util.cs deleted file mode 100644 index 19a433a..0000000 --- a/src/Util.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace BlobBin; - -public static class Util -{ - public static string GetFilesDirectoryPath(bool createIfNotExists = false) { - var filesDirectoryPath = Path.Combine( - Directory.GetCurrentDirectory(), - "AppData", - "files" - ); - if (createIfNotExists) Directory.CreateDirectory(filesDirectoryPath); - return filesDirectoryPath; - } -} \ No newline at end of file diff --git a/src/WallE.cs b/src/WallE.cs new file mode 100644 index 0000000..ae78050 --- /dev/null +++ b/src/WallE.cs @@ -0,0 +1,74 @@ +using IOL.Helpers; + +namespace BlobBin; + +public class WallE : BackgroundService +{ + public IServiceProvider Services { get; } + private readonly ILogger _logger; + + public WallE(IServiceProvider services, + ILogger logger) { + Services = services; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + _logger.LogInformation("WallE is running."); + await DoWork(stoppingToken); + } + + private async Task DoWork(CancellationToken stoppingToken) { + _logger.LogInformation("WallE is working."); + using var scope = Services.CreateScope(); + var eva = scope.ServiceProvider.GetRequiredService(); + var pastes = eva.Pastes.Where(c => c.DeletedAt != default || c.AutoDeleteAfter != default) + .Select(c => new Paste() { + Id = c.Id, + DeletedAt = c.DeletedAt, + AutoDeleteAfter = c.AutoDeleteAfter, + CreatedAt = c.CreatedAt + }) + .ToList(); + var files = eva.Files + .Where(c => c.DeletedAt != default || c.AutoDeleteAfter != default) + .Select(c => new File() { + Id = c.Id, + DeletedAt = c.DeletedAt, + AutoDeleteAfter = c.AutoDeleteAfter, + CreatedAt = c.CreatedAt + }) + .ToList(); + var now = DateTime.UtcNow; + foreach (var file in files) { + var path = Path.Combine(Tools.GetFilesDirectoryPath(), file.Id.ToString()); + if (DateTime.Compare(now, file.DeletedAt?.AddDays(7) ?? DateTime.MinValue) > 0) { + System.IO.File.Delete(path); + eva.Files.Remove(file); + } else if (file.AutoDeleteAfter.HasValue()) { + var autoDeleteDateTime = file.CreatedAt.Add(Tools.ParseHumanTimeSpan(file.AutoDeleteAfter)); + if (DateTime.Compare(now, autoDeleteDateTime) > 0) { + System.IO.File.Delete(path); + eva.Files.Remove(file); + } + } + } + + // If the file has lived more than +7 days from when it should be automatically deleted, delete the file completely. + foreach (var paste in pastes) { + if (DateTime.Compare(now, paste.DeletedAt?.AddDays(7) ?? DateTime.MinValue) > 0) { + eva.Pastes.Remove(paste); + } else if (paste.AutoDeleteAfter.HasValue()) { + var autoDeleteDateTime = paste.CreatedAt.Add(Tools.ParseHumanTimeSpan(paste.AutoDeleteAfter)); + if (DateTime.Compare(now, autoDeleteDateTime) > 0) { + eva.Pastes.Remove(paste); + } + } + } + } + + public override async Task StopAsync(CancellationToken stoppingToken) { + _logger.LogInformation("WallE is stopping."); + await base.StopAsync(stoppingToken); + } +} \ No newline at end of file diff --git a/src/wwwroot/index.css b/src/wwwroot/index.css new file mode 100644 index 0000000..83d19bb --- /dev/null +++ b/src/wwwroot/index.css @@ -0,0 +1,80 @@ +body { + font-family: sans-serif; + background: white; + color: #333; +} + +form { + width: 100%; + max-width: 300px; + display: flex; + flex-direction: column; + gap: 5px; +} + +#forms { + display: flex; + flex-direction: row; + gap: 15px; + flex-wrap: wrap; +} + +#forms .wrapper { + width: 300px; +} + +textarea { + overflow-y: hidden; + min-height: calc(1.5em + .75rem + 2px); + resize: vertical; +} + +.btn { + padding: 4px 12px; + min-width: 88px; + border: none; + font: inherit; + color: #373030; + border-radius: 4px; + outline: none; + text-decoration: none; + cursor: default; + font-weight: 400; + background: #fff; + box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.3), 0px 1px 1px rgba(0, 0, 0, 0.4); +} + +.btn:active { + background: linear-gradient(#4faefc, #006bff); + color: #fff; + position: relative; +} + +.btn.btn-blue { + color: #fff; + background: linear-gradient(#81c5fd, #3389ff); +} + +.btn.btn-blue:active { + background: linear-gradient(#4faefc, #006bff); +} + +.btn.btn-red { + color: #fff; + background: linear-gradient(#fd6c6f, #e70307); +} + +.btn.btn-red:active { + background: linear-gradient(#fc2125, #b50206); +} + +.btn.btn-green { + color: #fff; + background: linear-gradient(#89e36b, #44ae21); +} + +.btn.btn-green:active { + background: linear-gradient(#56d72b, #338319); +} + +.text-input {} \ No newline at end of file diff --git a/src/wwwroot/index.html b/src/wwwroot/index.html index da0da53..390ef4c 100644 --- a/src/wwwroot/index.html +++ b/src/wwwroot/index.html @@ -2,85 +2,7 @@ - + Blobbin @@ -89,7 +11,7 @@

Upload a file

-
@@ -97,7 +19,7 @@

Upload some text

- + @@ -119,7 +41,7 @@