summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Eva.cs (renamed from src/DB.cs)23
-rw-r--r--src/Migrations/20230112230354_InitialCreate.Designer.cs2
-rw-r--r--src/Migrations/20230115182252_DeletionKey.Designer.cs122
-rw-r--r--src/Migrations/20230115182252_DeletionKey.cs40
-rw-r--r--src/Migrations/DBModelSnapshot.cs10
-rw-r--r--src/Program.cs252
-rw-r--r--src/Tools.cs67
-rw-r--r--src/Util.cs14
-rw-r--r--src/WallE.cs74
-rw-r--r--src/wwwroot/index.css80
-rw-r--r--src/wwwroot/index.html88
11 files changed, 622 insertions, 150 deletions
diff --git a/src/DB.cs b/src/Eva.cs
index 3555a99..760231e 100644
--- a/src/DB.cs
+++ b/src/Eva.cs
@@ -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"