diff options
| -rw-r--r-- | src/Eva.cs (renamed from src/DB.cs) | 23 | ||||
| -rw-r--r-- | src/Migrations/20230112230354_InitialCreate.Designer.cs | 2 | ||||
| -rw-r--r-- | src/Migrations/20230115182252_DeletionKey.Designer.cs | 122 | ||||
| -rw-r--r-- | src/Migrations/20230115182252_DeletionKey.cs | 40 | ||||
| -rw-r--r-- | src/Migrations/DBModelSnapshot.cs | 10 | ||||
| -rw-r--r-- | src/Program.cs | 252 | ||||
| -rw-r--r-- | src/Tools.cs | 67 | ||||
| -rw-r--r-- | src/Util.cs | 14 | ||||
| -rw-r--r-- | src/WallE.cs | 74 | ||||
| -rw-r--r-- | src/wwwroot/index.css | 80 | ||||
| -rw-r--r-- | src/wwwroot/index.html | 88 |
11 files changed, 622 insertions, 150 deletions
@@ -2,10 +2,15 @@ using Microsoft.EntityFrameworkCore; namespace BlobBin; -public sealed class DB : DbContext +public sealed class Eva : DbContext { - public DB(DbContextOptions<DB> options) : base(options) { - Database.Migrate(); + private readonly bool migrated; + + public Eva(DbContextOptions<Eva> options) : base(options) { + if (!migrated) { + Database.Migrate(); + migrated = true; + } } public DbSet<File> Files { get; set; } @@ -33,17 +38,15 @@ public class UploadEntityBase public bool Singleton { get; set; } public string? AutoDeleteAfter { get; set; } public string? MimeType { get; set; } -} - -public class File : UploadEntityBase -{ + public string DeletionKey { get; set; } 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 +} + +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 @@ +// <auto-generated /> +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 + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); + + modelBuilder.Entity("BlobBin.File", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property<string>("AutoDeleteAfter") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT"); + + b.Property<string>("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT"); + + b.Property<string>("DeletionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<long>("Length") + .HasColumnType("INTEGER"); + + b.Property<string>("MimeType") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .HasColumnType("TEXT"); + + b.Property<string>("PasswordHash") + .HasColumnType("TEXT"); + + b.Property<string>("PublicId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<bool>("Singleton") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("BlobBin.Paste", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property<string>("AutoDeleteAfter") + .HasColumnType("TEXT"); + + b.Property<string>("Content") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("TEXT"); + + b.Property<string>("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<DateTime?>("DeletedAt") + .HasColumnType("TEXT"); + + b.Property<string>("DeletionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<long>("Length") + .HasColumnType("INTEGER"); + + b.Property<string>("MimeType") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .HasColumnType("TEXT"); + + b.Property<string>("PasswordHash") + .HasColumnType("TEXT"); + + b.Property<string>("PublicId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<bool>("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 +{ + /// <inheritdoc /> + public partial class DeletionKey : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<string>( + name: "DeletionKey", + table: "Pastes", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn<string>( + name: "DeletionKey", + table: "Files", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + + /// <inheritdoc /> + 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<DateTime?>("DeletedAt") .HasColumnType("TEXT"); + b.Property<string>("DeletionKey") + .IsRequired() + .HasColumnType("TEXT"); + b.Property<long>("Length") .HasColumnType("INTEGER"); @@ -82,6 +86,10 @@ namespace BlobBin.Migrations b.Property<DateTime?>("DeletedAt") .HasColumnType("TEXT"); + b.Property<string>("DeletionKey") + .IsRequired() + .HasColumnType("TEXT"); + b.Property<long>("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<DB>(); -builder.WebHost.UseKestrel(o => { o.Limits.MaxRequestBodySize = 100_000_000; }); +builder.Services.AddDbContext<Eva>(); +builder.Services.AddHostedService<WallE>(); +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($""" +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <link rel="stylesheet" href="/index.css"> + <title>{upload.PublicId} - Confirm deletion - Blobbin</title> +</head> +<body> + <p>Are you sure you want to delete {upload.Name}?</p> + <a href="/{context.Request.Path.ToString()}&confirmed=true">Yes</a> + <span> </span> + <a href="/">No, cancel</a> +</body> +</html> +""", "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<IResult> UploadSimple(HttpContext context, DB db) { +async Task<IResult> 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<IResult> 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<IResult> 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<IResult> 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<IResult> 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($""" +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <link rel="stylesheet" href="/index.css"> + <title>{paste.PublicId} - Authenticate - Blobbin</title> +</head> +<body> +<form action="/p/{paste.PublicId}" method="post"> + <p>Authenticate to access this paste:</p> + <input type="password" name="password" placeholder="Password"> + <button type="submit">Unlock</button> +</form> +</body> +</html> +""", "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<IResult> GetBlob(string id, DB db) { +async Task<IResult> 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($""" +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <link rel="stylesheet" href="/index.css"> + <title>{file.PublicId} - Authenticate - Blobbin</title> +</head> +<body> +<form action="/b/{file.PublicId}" method="post"> + <p>Authenticate to access this file:</p> + <input type="password" name="password" placeholder="Password"> + <button type="submit">Unlock</button> +</form> +</body> +</html> +""", "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<WallE> _logger; + + public WallE(IServiceProvider services, + ILogger<WallE> 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<Eva>(); + 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 @@ <html lang="en"> <head> <meta charset="UTF-8"> - <style> - body { - font-family: sans-serif; - } - - 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; - cursor: pointer; - } - - 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); - } - </style> + <link rel="stylesheet" href="/index.css"> <title>Blobbin</title> </head> <body> @@ -89,7 +11,7 @@ <main id="forms"> <div class="wrapper"> <h2>Upload a file</h2> - <form action="/upload" enctype="multipart/form-data" id="file-form" method="post" autocomplete="off" + <form action="/file" enctype="multipart/form-data" id="file-form" method="post" autocomplete="off" autocapitalize="off"> <input type="file" maxlength="104857600" id="file" name="files" required> <label for="file-password">Password (optional)</label> @@ -97,7 +19,7 @@ <label for="file-auto-delete"> Automatically delete after (optional) <span class="label-description" - title="blank=never, <number><unit>, unit can be d=day,w=week,h=hour,m=minute">?</span> + title="<number><unit>, Unit can be seconds(s,second), minutes(m,minute), hours(h,hour), days(d,day), weeks(w,week), years(y,year). Number can be non-negative whole number">?</span> </label> <input type="text" id="file-auto-delete" @@ -111,7 +33,7 @@ <div class="wrapper"> <h2>Upload some text</h2> <form action="/text" method="post" id="text-form" autocomplete="off" autocapitalize="off"> - <textarea id="text" name="text" required></textarea> + <textarea id="text" name="content" required></textarea> <label for="text-password">Mimetype (default: text/plain)</label> <input type="password" name="mime" id="text-mimetype"> <label for="text-password">Password (optional)</label> @@ -119,7 +41,7 @@ <label for="text-auto-delete"> Automatically delete after (optional) <span class="label-description" - title="blank=never, <number><unit>, unit can be d=day,w=week,h=hour,m=minute">?</span> + title="<number><unit>, Unit can be seconds(s,second), minutes(m,minute), hours(h,hour), days(d,day), weeks(w,week), years(y,year). Number can be non-negative whole number">?</span> </label> <input type="text" id="text-auto-delete" |
