diff --git a/.gitignore b/.gitignore index 256a3d9..cc1de40 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.db # BLOGS -blogs/ +md/ # Created by https://www.toptal.com/developers/gitignore/api/csharp,aspnetcore,rider,visualstudiocode # Edit at https://www.toptal.com/developers/gitignore?templates=csharp,aspnetcore,rider,visualstudiocode diff --git a/src/FastBlog.Core/Abstractions/Repositories/Blogs/IBlogFileRepository.cs b/src/FastBlog.Core/Abstractions/Repositories/Blogs/IBlogFileRepository.cs new file mode 100644 index 0000000..6a71167 --- /dev/null +++ b/src/FastBlog.Core/Abstractions/Repositories/Blogs/IBlogFileRepository.cs @@ -0,0 +1,10 @@ +using FastBlog.Core.Models; + +namespace FastBlog.Core.Abstractions.Repositories.Blogs; + +public interface IBlogFileRepository +{ + Task Read(string path); + Task> Write(string path, string text); + public Task Delete(string path); +} \ No newline at end of file diff --git a/src/FastBlog.Core/Abstractions/Repositories/Blogs/IBlogMetaRepository.cs b/src/FastBlog.Core/Abstractions/Repositories/Blogs/IBlogMetaRepository.cs new file mode 100644 index 0000000..b5e2086 --- /dev/null +++ b/src/FastBlog.Core/Abstractions/Repositories/Blogs/IBlogMetaRepository.cs @@ -0,0 +1,12 @@ +using FastBlog.Core.Models; +using FastBlog.Core.Models.Blogs; + +namespace FastBlog.Core.Abstractions.Repositories.Blogs; + +public interface IBlogMetaRepository +{ + Task Get(string? slug); + Task GetForEdit(int id); + Task> Add(BlogMeta meta); + Task> Update(BlogMeta meta); +} \ No newline at end of file diff --git a/src/FastBlog.Core/Abstractions/Repositories/Files/IFileMetaRepository.cs b/src/FastBlog.Core/Abstractions/Repositories/Files/IFileMetaRepository.cs new file mode 100644 index 0000000..1faa6a5 --- /dev/null +++ b/src/FastBlog.Core/Abstractions/Repositories/Files/IFileMetaRepository.cs @@ -0,0 +1,12 @@ +using FastBlog.Core.Models; +using FastBlog.Core.Models.Files; + +namespace FastBlog.Core.Abstractions.Repositories.Files; + +public interface IFileMetaRepository +{ + Task> List(PagedRequest request); + Task Get(int id); + Task> Add(FileMeta meta); + Task Delete(FileMeta meta); +} \ No newline at end of file diff --git a/src/FastBlog.Core/Abstractions/Repositories/Files/IFileRepository.cs b/src/FastBlog.Core/Abstractions/Repositories/Files/IFileRepository.cs new file mode 100644 index 0000000..b41d499 --- /dev/null +++ b/src/FastBlog.Core/Abstractions/Repositories/Files/IFileRepository.cs @@ -0,0 +1,10 @@ +using FastBlog.Core.Abstractions.Repositories.Blogs; +using FastBlog.Core.Models; + +namespace FastBlog.Core.Abstractions.Repositories.Files; + +public interface IFileRepository +{ + Task> Write(string path, Stream fileStream); + public Task Delete(string path); +} diff --git a/src/FastBlog.Core/Db/Migrations/202409212014_Init.cs b/src/FastBlog.Core/Db/Migrations/202409212014_Init.cs index 026a008..974a66d 100644 --- a/src/FastBlog.Core/Db/Migrations/202409212014_Init.cs +++ b/src/FastBlog.Core/Db/Migrations/202409212014_Init.cs @@ -12,7 +12,7 @@ public class Init : Migration .WithColumn("Title").AsString() .WithColumn("SourceLocation").AsString() .WithColumn("Slug").AsString().Unique().Nullable() - .WithColumn("ShowDetails").AsBoolean() + .WithColumn("ShowDetails").AsBoolean() .WithColumn("ImageUrl").AsString().Nullable() .WithColumn("CreatedAt").AsDateTime() .WithColumn("ModifiedAt").AsDateTime() diff --git a/src/FastBlog.Core/Db/Migrations/202409252200_AddFileMeta.cs b/src/FastBlog.Core/Db/Migrations/202409252200_AddFileMeta.cs new file mode 100644 index 0000000..0995342 --- /dev/null +++ b/src/FastBlog.Core/Db/Migrations/202409252200_AddFileMeta.cs @@ -0,0 +1,21 @@ +using FluentMigrator; + +namespace FastBlog.Core.Db.Migrations; + +[Migration(202409252200, "Add file meta table")] +public class AddFileMeta : Migration +{ + public override void Up() + { + Create.Table("Files") + .WithColumn("Id").AsInt32().PrimaryKey().Unique().Identity() + .WithColumn("SourceLocation").AsString().Unique() + .WithColumn("MimeType").AsString() + .WithColumn("CreatedAt").AsDateTime(); + } + + public override void Down() + { + Delete.Table("Files"); + } +} \ No newline at end of file diff --git a/src/FastBlog.Core/DependencyInjection.cs b/src/FastBlog.Core/DependencyInjection.cs index 11aea5b..9594c0a 100644 --- a/src/FastBlog.Core/DependencyInjection.cs +++ b/src/FastBlog.Core/DependencyInjection.cs @@ -1,7 +1,7 @@ using System.Data; using FastBlog.Core.Abstractions.Repositories.Blogs; +using FastBlog.Core.Abstractions.Repositories.Files; using FastBlog.Core.Db; -using FastBlog.Core.Errors.Blogs; using FastBlog.Core.Models.Blogs; using FastBlog.Core.Options; using FastBlog.Core.Repositories; @@ -19,13 +19,16 @@ public static class DependencyInjection { // applicatrion services.AddSingleton(); + services.AddSingleton(); // options services.Configure(configuration.GetSection("FileStore")); // infrastructure services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // storage var connectionString = configuration.GetConnectionString("Sqlite"); @@ -52,7 +55,7 @@ public static class DependencyInjection var blogService = scope.ServiceProvider.GetRequiredService(); var mainPage = await blogService.Get(null); - if (mainPage.IsOk) + if (mainPage is not null) return; await blogService.UpdateBlog(new Blog diff --git a/src/FastBlog.Core/Errors/FileError.cs b/src/FastBlog.Core/Errors/FileError.cs deleted file mode 100644 index 5e2d5a8..0000000 --- a/src/FastBlog.Core/Errors/FileError.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace FastBlog.Core.Errors; - -public enum FileError -{ - NotFound, - AlreadyExists, - InvalidDirectory -} \ No newline at end of file diff --git a/src/FastBlog.Core/Models/Blogs/Blog.cs b/src/FastBlog.Core/Models/Blogs/Blog.cs new file mode 100644 index 0000000..6322795 --- /dev/null +++ b/src/FastBlog.Core/Models/Blogs/Blog.cs @@ -0,0 +1,7 @@ +namespace FastBlog.Core.Models.Blogs; + +public class Blog +{ + public required string Text { get; init; } + public required BlogMeta Metadata { get; init; } +} \ No newline at end of file diff --git a/src/FastBlog.Core/Models/Blogs/BlogMeta.cs b/src/FastBlog.Core/Models/Blogs/BlogMeta.cs new file mode 100644 index 0000000..2f270eb --- /dev/null +++ b/src/FastBlog.Core/Models/Blogs/BlogMeta.cs @@ -0,0 +1,17 @@ +namespace FastBlog.Core.Models.Blogs; + +public sealed class BlogMeta +{ + public int? Id { get; init; } + public required string Title { get; init; } + public required string SourceLocation { get; init; } + public string? Slug { get; init; } + public bool ShowDetails { get; init; } = true; + public string? ImageUrl { get; init; } + public DateTime CreatedAt { get; init; } + public DateTime ModifiedAt { get; init; } + public string? Signature { get; init; } + public bool FullWidth { get; init; } = false; + public bool Deleted { get; init; } = false; + public bool Visible { get; init; } = false; +} \ No newline at end of file diff --git a/src/FastBlog.Core/Models/BusinessError.cs b/src/FastBlog.Core/Models/BusinessError.cs new file mode 100644 index 0000000..50a3a46 --- /dev/null +++ b/src/FastBlog.Core/Models/BusinessError.cs @@ -0,0 +1,15 @@ +namespace FastBlog.Core.Models; + +public sealed class BusinessError : Exception +{ + public string ShortCode { get; set; } + public BusinessError(string message, string shortCode) : base(message) + { + ShortCode = shortCode; + } + + public BusinessError(string shortCode) : base(shortCode) + { + ShortCode = shortCode; + } +} \ No newline at end of file diff --git a/src/FastBlog.Core/Models/Files/FileMeta.cs b/src/FastBlog.Core/Models/Files/FileMeta.cs new file mode 100644 index 0000000..8dee1d6 --- /dev/null +++ b/src/FastBlog.Core/Models/Files/FileMeta.cs @@ -0,0 +1,9 @@ +namespace FastBlog.Core.Models.Files; + +public sealed class FileMeta +{ + public int? Id { get; init; } + public required string SourceLocation { get; init; } + public DateTime CreatedAt { get; init; } + public string MimeType { get; init; } +} \ No newline at end of file diff --git a/src/FastBlog.Core/Models/PagedRequest.cs b/src/FastBlog.Core/Models/PagedRequest.cs new file mode 100644 index 0000000..f546708 --- /dev/null +++ b/src/FastBlog.Core/Models/PagedRequest.cs @@ -0,0 +1,21 @@ +namespace FastBlog.Core.Models; + +public class PagedRequest +{ + public int Amount { get; } + public int Offset { get; } + + public PagedRequest(int amount, int offset) + { + if(amount < 1) + { + throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be greater than 0"); + } + if(offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "Offset must be greater than or equal to 0"); + } + Amount = amount; + Offset = offset; + } +} \ No newline at end of file diff --git a/src/FastBlog.Core/Models/PagedResponse.cs b/src/FastBlog.Core/Models/PagedResponse.cs new file mode 100644 index 0000000..bfcffb0 --- /dev/null +++ b/src/FastBlog.Core/Models/PagedResponse.cs @@ -0,0 +1,27 @@ +namespace FastBlog.Core.Models; + +public class PagedResponse +{ + public long Total { get; } + public int Amount { get; } + public int Offset { get; } + public T[] Data { get; } + + public PagedResponse(long total, int amount, int offset, T[] data) + { + Total = total; + Amount = amount; + Offset = offset; + Data = data; + } + + public static PagedResponse Create(long total, int amount, int offset, T[] data) + { + return new PagedResponse(total, amount, offset, data); + } + + public static PagedResponse Empty() + { + return new PagedResponse(0, 0, 0, []); + } +} \ No newline at end of file diff --git a/src/FastBlog.Core/Options/FileStoreOptions.cs b/src/FastBlog.Core/Options/FileStoreOptions.cs index 33eaa06..e5de713 100644 --- a/src/FastBlog.Core/Options/FileStoreOptions.cs +++ b/src/FastBlog.Core/Options/FileStoreOptions.cs @@ -2,5 +2,6 @@ public class FileStoreOptions { - public required string SourcePath { get; init; } + public required string BlogSourcePath { get; init; } + public required string StaticFilesPath { get; init; } } \ No newline at end of file diff --git a/src/FastBlog.Core/Repositories/BlogBlogFileRepository.cs b/src/FastBlog.Core/Repositories/BlogBlogFileRepository.cs new file mode 100644 index 0000000..2a1a42f --- /dev/null +++ b/src/FastBlog.Core/Repositories/BlogBlogFileRepository.cs @@ -0,0 +1,41 @@ +using FastBlog.Core.Abstractions.Repositories.Blogs; +using FastBlog.Core.Models; +using FastBlog.Core.Options; +using Microsoft.Extensions.Options; + +namespace FastBlog.Core.Repositories; + +public sealed class BlogBlogFileRepository(IOptionsMonitor fileOptions) : IBlogFileRepository +{ + public async Task Read(string path) + { + var targetPath = Path.Combine(fileOptions.CurrentValue.BlogSourcePath, path); + if (!File.Exists(targetPath)) return null; + return await File.ReadAllTextAsync(targetPath); + } + + public async Task> Write(string path, string text) + { + var targetPath = Path.Combine(fileOptions.CurrentValue.BlogSourcePath, path); + + var directoryPath = Path.GetDirectoryName(targetPath); + + if (!Directory.Exists(directoryPath)) + Directory.CreateDirectory(directoryPath!); + + if (File.Exists(targetPath)) + return new BusinessError("already_exists", "File already exists"); + + await File.WriteAllTextAsync(targetPath, text); + return true; + } + + public Task Delete(string path) + { + var targetPath = Path.Combine(fileOptions.CurrentValue.BlogSourcePath, path); + if (!File.Exists(targetPath)) + return Task.FromResult(false); + File.Delete(targetPath); + return Task.FromResult(true); + } +} \ No newline at end of file diff --git a/src/FastBlog.Core/Repositories/BlogMetaRepository.cs b/src/FastBlog.Core/Repositories/BlogMetaRepository.cs index a518f90..7d0354b 100644 --- a/src/FastBlog.Core/Repositories/BlogMetaRepository.cs +++ b/src/FastBlog.Core/Repositories/BlogMetaRepository.cs @@ -1,7 +1,6 @@ using Dapper; using FastBlog.Core.Abstractions.Repositories.Blogs; using FastBlog.Core.Db; -using FastBlog.Core.Errors.Blogs; using FastBlog.Core.Models; using FastBlog.Core.Models.Blogs; using Microsoft.Data.Sqlite; @@ -10,7 +9,7 @@ namespace FastBlog.Core.Repositories; public sealed class BlogMetaRepository(SqliteConnectionFactory connectionFactory) : IBlogMetaRepository { - public async Task> Get(string? slug) + public async Task Get(string? slug) { using var connection = connectionFactory.Create(); @@ -28,19 +27,11 @@ public sealed class BlogMetaRepository(SqliteConnectionFactory connectionFactory """; if (slug is null) - { - var resultNull = await connection.QueryFirstOrDefaultAsync(sqlNull); - if (resultNull is null) return BlogError.NotFound; - return resultNull; - } - - var result = await connection.QueryFirstOrDefaultAsync(sql, new { slug }); - if (result is null) return BlogError.NotFound; - - return result; + return await connection.QueryFirstOrDefaultAsync(sqlNull); + return await connection.QueryFirstOrDefaultAsync(sql, new { slug }); } - - public async Task> GetForEdit(int id) + + public async Task GetForEdit(int id) { using var connection = connectionFactory.Create(); @@ -50,13 +41,10 @@ public sealed class BlogMetaRepository(SqliteConnectionFactory connectionFactory and deleted = 0 """; - var result = await connection.QueryFirstOrDefaultAsync(sql, new { id }); - if (result is null) return BlogError.NotFound; - - return result; + return await connection.QueryFirstOrDefaultAsync(sql, new { id }); } - public async Task> Add(BlogMeta meta) + public async Task> Add(BlogMeta meta) { using var connection = connectionFactory.Create(); @@ -67,20 +55,18 @@ public sealed class BlogMetaRepository(SqliteConnectionFactory connectionFactory """; try { - var result = await connection.QueryFirstOrDefaultAsync(sql, meta); - if (result is null) return BlogError.NotFound; - return result; + return await connection.QueryFirstAsync(sql, meta); } catch (SqliteException ex) { if (ex.SqliteErrorCode == 19) // Unique constraint violation - return BlogError.AlreadyExists; // You need to define this error in the BlogError enum + return new BusinessError("already_exists","Slug already exists"); throw; } } - public async Task> Update(BlogMeta meta) + public async Task> Update(BlogMeta meta) { using var connection = connectionFactory.Create(); @@ -93,15 +79,12 @@ public sealed class BlogMetaRepository(SqliteConnectionFactory connectionFactory try { - var result = await connection.QueryFirstOrDefaultAsync(sql, meta); - if (result is null) return BlogError.NotFound; - return result; + return await connection.QueryFirstAsync(sql, meta); } catch (SqliteException ex) { if (ex.SqliteErrorCode == 19) // Unique constraint violation - return BlogError.AlreadyExists; - + return new BusinessError("already_exists","Slug already exists"); throw; } } diff --git a/src/FastBlog.Core/Repositories/BlogSourceRepository.cs b/src/FastBlog.Core/Repositories/BlogSourceRepository.cs deleted file mode 100644 index d39f9b8..0000000 --- a/src/FastBlog.Core/Repositories/BlogSourceRepository.cs +++ /dev/null @@ -1,44 +0,0 @@ -using FastBlog.Core.Abstractions.Repositories.Blogs; -using FastBlog.Core.Errors; -using FastBlog.Core.Models; -using FastBlog.Core.Options; -using Microsoft.Extensions.Options; - -namespace FastBlog.Core.Repositories; - -public sealed class BlogSourceRepository(IOptionsMonitor fileOptions) : IBlogSourceRepository -{ - public async Task> Read(string path) - { - var targetPath = Path.Combine(fileOptions.CurrentValue.SourcePath, path); - if (!File.Exists(targetPath)) - return FileError.NotFound; - return await File.ReadAllTextAsync(targetPath); - } - - public async Task> Write(string path, string text) - { - var targetPath = Path.Combine(fileOptions.CurrentValue.SourcePath, path); - - var directoryPath = Path.GetDirectoryName(targetPath); - - if (!Directory.Exists(directoryPath)) - Directory.CreateDirectory(directoryPath!); - - if (File.Exists(targetPath)) - return FileError.AlreadyExists; - - await File.WriteAllTextAsync(targetPath, text); - return true; - } - - public Result DeleteSync(string path) - { - //TODO: If folder is empty, delete - var targetPath = Path.Combine(fileOptions.CurrentValue.SourcePath, path); - if (!File.Exists(targetPath)) - return FileError.NotFound; - File.Delete(targetPath); - return true; - } -} \ No newline at end of file diff --git a/src/FastBlog.Core/Repositories/FileMetaRepository.cs b/src/FastBlog.Core/Repositories/FileMetaRepository.cs new file mode 100644 index 0000000..0264216 --- /dev/null +++ b/src/FastBlog.Core/Repositories/FileMetaRepository.cs @@ -0,0 +1,77 @@ +using Dapper; +using FastBlog.Core.Abstractions.Repositories.Files; +using FastBlog.Core.Db; +using FastBlog.Core.Models; +using FastBlog.Core.Models.Files; +using Microsoft.Data.Sqlite; + +namespace FastBlog.Core.Repositories; + +public sealed class FileMetaRepository(SqliteConnectionFactory connectionFactory) : IFileMetaRepository +{ + public async Task> List(PagedRequest request) + { + const string sql = """ + select COUNT(*) from Files; + select * from Files + order by CreatedAt desc + limit @Amount offset @Offset; + """; + + using var connection = connectionFactory.Create(); + + await using var multi = await connection.QueryMultipleAsync(sql, request); + + var total = await multi.ReadFirstAsync(); + var result = (await multi.ReadAsync()).ToArray(); + + return PagedResponse.Create(total, request.Amount, request.Offset, result); + } + + public async Task Get(int id) + { + const string sql = """ + select * from Files + where Id = @Id; + """; + + using var connection = connectionFactory.Create(); + return await connection.QueryFirstOrDefaultAsync(sql, new { Id = id }); + } + + public async Task> Add(FileMeta meta) + { + const string sql = """ + insert into Files (SourceLocation, CreatedAt, MimeType) + values (@SourceLocation, @CreatedAt, @MimeType) + returning *; + """; + + using var connection = connectionFactory.Create(); + try + { + var result = await connection.QueryFirstAsync(sql, meta); + return result; + } + catch (SqliteException ex) + { + if (ex.SqliteErrorCode == 19) // Unique constraint violation + return new BusinessError("already_exists", "File already exists"); + + throw; + } + } + + public async Task Delete(FileMeta meta) + { + using var connection = connectionFactory.Create(); + + const string sql = """ + delete from Files + where Id = @Id; + """; + + var result = await connection.ExecuteAsync(sql, meta); + return result > 0; + } +} \ No newline at end of file diff --git a/src/FastBlog.Core/Repositories/FileRepository.cs b/src/FastBlog.Core/Repositories/FileRepository.cs new file mode 100644 index 0000000..c9f7973 --- /dev/null +++ b/src/FastBlog.Core/Repositories/FileRepository.cs @@ -0,0 +1,35 @@ +using FastBlog.Core.Abstractions.Repositories.Files; +using FastBlog.Core.Models; +using FastBlog.Core.Options; +using Microsoft.Extensions.Options; + +namespace FastBlog.Core.Repositories; + +public sealed class FileRepository(IOptionsMonitor fileOptions) : IFileRepository +{ + public async Task> Write(string path, Stream fileStream) + { + var targetPath = Path.Combine(fileOptions.CurrentValue.StaticFilesPath, path); + var directoryPath = Path.GetDirectoryName(targetPath); + + if (!Directory.Exists(directoryPath)) + Directory.CreateDirectory(directoryPath!); + + if (File.Exists(targetPath)) + return new BusinessError("already_exists", "File already exists"); + + await using var file = File.Create(targetPath); + await fileStream.CopyToAsync(file); + return true; + + } + public Task Delete(string path) + { + var targetPath = Path.Combine(fileOptions.CurrentValue.StaticFilesPath, path); + if (!File.Exists(targetPath)) + return Task.FromResult(false); + File.Delete(targetPath); + return Task.FromResult(true); + } + +} \ No newline at end of file diff --git a/src/FastBlog.Core/Services/BlogService.cs b/src/FastBlog.Core/Services/BlogService.cs index a77c0cf..71ae53d 100644 --- a/src/FastBlog.Core/Services/BlogService.cs +++ b/src/FastBlog.Core/Services/BlogService.cs @@ -1,74 +1,67 @@ using System.Diagnostics; using FastBlog.Core.Abstractions.Repositories.Blogs; -using FastBlog.Core.Errors; -using FastBlog.Core.Errors.Blogs; +using FastBlog.Core.Abstractions.Repositories.Files; using FastBlog.Core.Models; using FastBlog.Core.Models.Blogs; namespace FastBlog.Core.Services; -public sealed class BlogService(IBlogMetaRepository metaRepository, IBlogSourceRepository sourceRepository) +public sealed class BlogService(IBlogMetaRepository metaRepository, IBlogFileRepository blogFileRepository) { - public async Task> Get(string? slug) + public async Task Get(string? slug) { var metaResult = await metaRepository.Get(slug); - if (metaResult.IsError) - return metaResult.AsError; - var sourceResult = await sourceRepository.Read(metaResult.AsOk.SourceLocation); + if (metaResult is null) + return null; + var sourceResult = await blogFileRepository.Read(metaResult.SourceLocation); - if (sourceResult.IsError) + if (sourceResult is null) { - throw new FileNotFoundException("Blog with metadata cannot be found"); + throw new FileNotFoundException("File with metadata cannot be found"); } return new Blog { - Metadata = metaResult.AsOk, - Text = sourceResult.AsOk + Metadata = metaResult, + Text = sourceResult }; } - public async Task> GetForEdit(int id) + public async Task GetForEdit(int id) { var metaResult = await metaRepository.GetForEdit(id); - if (metaResult.IsError) - return metaResult.AsError; - var sourceResult = await sourceRepository.Read(metaResult.AsOk.SourceLocation); + + if (metaResult is null) + return null; + + var sourceResult = await blogFileRepository.Read(metaResult.SourceLocation); - if (sourceResult.IsError) - { - throw new FileNotFoundException("Blog with metadata cannot be found"); - } + if (sourceResult is null) + throw new FileNotFoundException("File with metadata cannot be found"); return new Blog { - Metadata = metaResult.AsOk, - Text = sourceResult.AsOk + Metadata = metaResult, + Text = sourceResult }; } - public Task> UpdateBlog(Blog blog) + public Task> UpdateBlog(Blog blog) { return blog.Metadata.Id is null ? CreateBlog(blog) : UpdateBlogInternal(blog); } - private async Task> CreateBlog(Blog newBlog) + private async Task> CreateBlog(Blog newBlog) { var metaResult = await metaRepository.Add(newBlog.Metadata); if (metaResult.IsError) return metaResult.AsError; - var sourceResult = await sourceRepository.Write(metaResult.AsOk.SourceLocation, newBlog.Text); + var sourceResult = await blogFileRepository.Write(metaResult.AsOk.SourceLocation, newBlog.Text); if (sourceResult.IsError) - return sourceResult.AsError switch - { - FileError.AlreadyExists => BlogError.AlreadyExists, - FileError.InvalidDirectory => throw new Exception( - "Invalid configuration: Directory for blog storage not found"), - _ => throw new UnreachableException() - }; + return sourceResult.AsError; return new Blog { @@ -78,30 +71,24 @@ public sealed class BlogService(IBlogMetaRepository metaRepository, IBlogSourceR } - private async Task> UpdateBlogInternal(Blog blog) + private async Task> UpdateBlogInternal(Blog blog) { var oldMeta = await metaRepository.GetForEdit(blog.Metadata.Id!.Value); - if (oldMeta.IsError) - return BlogError.NotFound; + if (oldMeta is null) + return new BusinessError("not_found", "Blog not found"); - var toDelete = oldMeta.AsOk.SourceLocation; + var toDelete = oldMeta.SourceLocation; var metaResult = await metaRepository.Update(blog.Metadata); if (metaResult.IsError) return metaResult.AsError; - _ = sourceRepository.DeleteSync(toDelete); + _ = await blogFileRepository.Delete(toDelete); - var sourceResult = await sourceRepository.Write(metaResult.AsOk.SourceLocation, blog.Text); + var sourceResult = await blogFileRepository.Write(metaResult.AsOk.SourceLocation, blog.Text); if (sourceResult.IsError) - return sourceResult.AsError switch - { - FileError.AlreadyExists => BlogError.AlreadyExists, - FileError.InvalidDirectory => throw new Exception( - "Invalid configuration: Directory for blog storage not found"), - _ => throw new UnreachableException() - }; + return sourceResult.AsError; return new Blog { diff --git a/src/FastBlog.Core/Services/FileService.cs b/src/FastBlog.Core/Services/FileService.cs new file mode 100644 index 0000000..92871e7 --- /dev/null +++ b/src/FastBlog.Core/Services/FileService.cs @@ -0,0 +1,33 @@ +using FastBlog.Core.Abstractions.Repositories.Files; +using FastBlog.Core.Models; +using FastBlog.Core.Models.Files; + +namespace FastBlog.Core.Services; + +public sealed class FileService(IFileMetaRepository metaRepository, IFileRepository repository) +{ + public Task> GetFiles(PagedRequest request) => metaRepository.List(request); + + public async Task DeleteFile(int metaId) + { + var meta = await metaRepository.Get(metaId); + + if (meta is null) + return false; + + return await metaRepository.Delete(meta); + } + + public async Task> AddFile(FileMeta meta, Func streamProvider) + { + var metaResult = await metaRepository.Add(meta); + + if (metaResult.IsError) + return metaResult.AsError; + + await using var stream = streamProvider(); + var result = await repository.Write(meta.SourceLocation, stream); + + return result.IsError ? metaResult.AsError : metaResult; + } +} \ No newline at end of file diff --git a/src/FastBlog.Web/Controllers/BlogController.cs b/src/FastBlog.Web/Controllers/BlogsController.cs similarity index 73% rename from src/FastBlog.Web/Controllers/BlogController.cs rename to src/FastBlog.Web/Controllers/BlogsController.cs index 13d75b1..af62e93 100644 --- a/src/FastBlog.Web/Controllers/BlogController.cs +++ b/src/FastBlog.Web/Controllers/BlogsController.cs @@ -1,5 +1,5 @@ using System.Diagnostics; -using FastBlog.Core.Errors.Blogs; +using FastBlog.Core.Abstractions.Repositories.Blogs; using FastBlog.Core.Models.Blogs; using FastBlog.Core.Services; using FastBlog.Web.Models; @@ -8,8 +8,8 @@ using Microsoft.AspNetCore.Mvc; namespace FastBlog.Web.Controllers; [Route("")] -[Route("blog")] -public class BlogController(BlogService service) : Controller +[Route("blogs")] +public class BlogsController(BlogService service) : Controller { [HttpGet] [Route("{*slug}")] @@ -20,16 +20,10 @@ public class BlogController(BlogService service) : Controller var blog = await service.Get(slug); - if (blog.IsError) - { - return blog.AsError switch - { - BlogError.NotFound => NotFound(), - _ => throw new ArgumentOutOfRangeException() - }; - } + if (blog is null) + return NotFound(); - return View(blog.AsOk); + return View(blog); } [HttpGet] @@ -38,16 +32,10 @@ public class BlogController(BlogService service) : Controller { var blog = await service.Get(null); - if (blog.IsError) - { - return blog.AsError switch - { - BlogError.NotFound => NotFound(), - _ => throw new ArgumentOutOfRangeException() - }; - } + if (blog is null) + return NotFound(); - return View(blog.AsOk); + return View(blog); } [HttpGet] @@ -73,18 +61,11 @@ public class BlogController(BlogService service) : Controller ); } - var result = await service.GetForEdit(id.Value); - if (result.IsError) - { - return result.AsError switch - { - BlogError.NotFound => NotFound(), - _ => throw new UnreachableException() - }; - } - - var blog = result.AsOk; + var blog = await service.GetForEdit(id.Value); + if (blog is null) + return NotFound(); + return View(new EditBlog { Id = blog.Metadata.Id, @@ -127,14 +108,15 @@ public class BlogController(BlogService service) : Controller if (result.IsError) { - return result.AsError switch + return result.AsError.ShortCode switch { - BlogError.AlreadyExists => Conflict(), - _ => throw new ArgumentOutOfRangeException() + "already_exists" => Conflict(), + "not_found" => NotFound(), + _ => throw result.AsError }; } - return Redirect($"/blog/{editBlog.Slug}"); + return Redirect($"/blogs/{editBlog.Slug}"); } [HttpPost] diff --git a/src/FastBlog.Web/Controllers/FilesController.cs b/src/FastBlog.Web/Controllers/FilesController.cs new file mode 100644 index 0000000..3293ed3 --- /dev/null +++ b/src/FastBlog.Web/Controllers/FilesController.cs @@ -0,0 +1,70 @@ +using FastBlog.Core.Models; +using FastBlog.Core.Models.Files; +using FastBlog.Core.Services; +using Microsoft.AspNetCore.Mvc; + +namespace FastBlog.Web.Controllers; + +[Route("files")] +public class FilesController(FileService fileService) : Controller +{ + private const long MaxFileSize = 8L * 1024L * 1024L * 1024L; + + [HttpGet] + public async Task Index() + { + var files = await fileService.GetFiles(new PagedRequest(25, 0)); + return View("Index", files); + } + + [HttpGet("{offset:int}")] + public async Task Page(int offset) + { + var files = await fileService.GetFiles(new PagedRequest(25, offset)); + return View("Index", files); + } + + [HttpDelete("{metaId:int}")] + public async Task Delete(int metaId) + { + var result = await fileService.DeleteFile(metaId); + return result ? await Index() : NotFound(); + } + + + + [HttpPost] + public async Task Upload([FromForm(Name = "sourcePath")] string? sourcePath) + { + // TODO: Validate path to prevent directory traversal + // rn I am the only user so I dont care for now + + if (HttpContext.Request.Form.Files.Count < 1) + return BadRequest("File is required."); + + var file = HttpContext.Request.Form.Files[0]; + + if (file.Length > MaxFileSize) + return BadRequest("File is too large."); + + var meta = new FileMeta + { + SourceLocation = Path.Combine(sourcePath ?? "", file.FileName), + CreatedAt = DateTime.Now, + MimeType = file.ContentType + }; + + var result = await fileService.AddFile(meta, file.OpenReadStream); + + if (result.IsError) + { + return result.AsError.ShortCode switch + { + "already_exists" => Conflict(), + _ => throw result.AsError + }; + } + + return await Index(); + } +} \ No newline at end of file diff --git a/src/FastBlog.Web/FastBlog.Web.csproj b/src/FastBlog.Web/FastBlog.Web.csproj index 3395706..658088f 100644 --- a/src/FastBlog.Web/FastBlog.Web.csproj +++ b/src/FastBlog.Web/FastBlog.Web.csproj @@ -18,38 +18,6 @@ - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.min.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.min.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.min.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-grid.rtl.min.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.min.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.min.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.min.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-reboot.rtl.min.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.min.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.min.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.min.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap-utilities.rtl.min.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.min.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.min.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.min.css" /> - <_ContentIncludedByDefault Remove="wwwroot\lib\bootstrap\dist\css\bootstrap.rtl.min.css.map" /> <_ContentIncludedByDefault Remove="Views\Home\Index.cshtml" /> <_ContentIncludedByDefault Remove="Views\Home\Privacy.cshtml" /> diff --git a/src/FastBlog.Web/Helpers/PropertyHelper.cs b/src/FastBlog.Web/Helpers/PropertyHelper.cs new file mode 100644 index 0000000..6151526 --- /dev/null +++ b/src/FastBlog.Web/Helpers/PropertyHelper.cs @@ -0,0 +1,9 @@ +namespace FastBlog.Web.Helpers; + +public static class PropertyHelper +{ + public static string If(bool condition, string property) + { + return condition ? property : string.Empty; + } +} \ No newline at end of file diff --git a/src/FastBlog.Web/Program.cs b/src/FastBlog.Web/Program.cs index 9bac713..ca27467 100644 --- a/src/FastBlog.Web/Program.cs +++ b/src/FastBlog.Web/Program.cs @@ -28,7 +28,7 @@ app.UseAuthorization(); app.MapControllerRoute( name: "default", - pattern: "{controller=Blog}/{action=Index}/{slug?}"); + pattern: "{controller=Blogs}/{action=Index}/{slug?}"); await app.Services.MigrateUp(app.Configuration); diff --git a/src/FastBlog.Web/Views/Blog/Edit.cshtml b/src/FastBlog.Web/Views/Blogs/Edit.cshtml similarity index 95% rename from src/FastBlog.Web/Views/Blog/Edit.cshtml rename to src/FastBlog.Web/Views/Blogs/Edit.cshtml index fc942d9..9b2c793 100644 --- a/src/FastBlog.Web/Views/Blog/Edit.cshtml +++ b/src/FastBlog.Web/Views/Blogs/Edit.cshtml @@ -13,7 +13,7 @@ else

Edit "@Model.Title"

} -
+
diff --git a/src/FastBlog.Web/Views/Blog/Edit.cshtml.css b/src/FastBlog.Web/Views/Blogs/Edit.cshtml.css similarity index 100% rename from src/FastBlog.Web/Views/Blog/Edit.cshtml.css rename to src/FastBlog.Web/Views/Blogs/Edit.cshtml.css diff --git a/src/FastBlog.Web/Views/Blog/Index.cshtml b/src/FastBlog.Web/Views/Blogs/Index.cshtml similarity index 100% rename from src/FastBlog.Web/Views/Blog/Index.cshtml rename to src/FastBlog.Web/Views/Blogs/Index.cshtml diff --git a/src/FastBlog.Web/Views/Blog/Preview.cshtml b/src/FastBlog.Web/Views/Blogs/Preview.cshtml similarity index 100% rename from src/FastBlog.Web/Views/Blog/Preview.cshtml rename to src/FastBlog.Web/Views/Blogs/Preview.cshtml diff --git a/src/FastBlog.Web/Views/Files/Index.cshtml b/src/FastBlog.Web/Views/Files/Index.cshtml new file mode 100644 index 0000000..60d5cb2 --- /dev/null +++ b/src/FastBlog.Web/Views/Files/Index.cshtml @@ -0,0 +1,78 @@ +@using FastBlog.Web.Helpers +@model FastBlog.Core.Models.PagedResponse + + +@{ + ViewBag.Title = "Files"; +} + +
+ + +
+ +
+
+

Upload a file

+
+ + + + + + +
+ +
+
+

Manage files

+
+ + + @if (Model.Data.Length is 0) + { +

No files have been found

+ } + + @foreach (var file in Model.Data) + { + + } + + +
+
\ No newline at end of file diff --git a/src/FastBlog.Web/appsettings.json b/src/FastBlog.Web/appsettings.json index f4a5d1b..150d393 100644 --- a/src/FastBlog.Web/appsettings.json +++ b/src/FastBlog.Web/appsettings.json @@ -7,9 +7,17 @@ }, "AllowedHosts": "*", "FileStore": { - "SourcePath": "blogs" + "BlogSourcePath": "md", + "StaticFilesPath": "wwwroot/static" }, "ConnectionStrings": { "Sqlite": "Data Source=app.db" + }, + "Display": { + "Title": "FastBlog", + "Links": { + "GitHub": "https://github.com", + "Mail": "mailto:mail@example.com" + } } } diff --git a/src/FastBlog.Web/wwwroot/css/site.css b/src/FastBlog.Web/wwwroot/css/site.css index 4c29762..dc67d3e 100644 --- a/src/FastBlog.Web/wwwroot/css/site.css +++ b/src/FastBlog.Web/wwwroot/css/site.css @@ -3,13 +3,50 @@ src: url('fonts/FiraCode-VF.woff2') format('woff2'); } +/* Orange color for dark color scheme (Forced) */ +/* Enabled if forced with data-theme="dark" */ +[data-theme=dark] { + --pico-text-selection-color: rgba(195, 234, 255, 0.37); + --pico-primary: #C3EAFF; + --pico-primary-background: rgba(126, 209, 255, 0.32); + --pico-primary-underline: rgba(195, 234, 255, 0.25); + --pico-primary-hover: #C3EAFF; + --pico-primary-hover-background: rgba(126, 209, 255, 0.60); + --pico-primary-focus: rgba(195, 234, 255, 0.90); + --pico-primary-inverse: #fff; + + --pico-secondary: #C3EAFF; + --pico-secondary-background: rgba(195, 234, 255, 0.16); + --pico-secondary-underline: rgba(195, 234, 255, 0.25); + --pico-secondary-hover: #C3EAFF; + --pico-secondary-hover-background: rgba(195, 234, 255, 0.40); + --pico-secondary-focus: rgba(195, 234, 255, 0.90); + --pico-secondary-inverse: #fff; + + --pico-background-color: #0f1a27; + --pico-card-background-color: #132232; + --pico-card-sectioning-background-color: #17273a; +} + h1, h2, h3, h4, h5, h6 { --pico-font-family: 'Fira Code', Roboto, sans-serif; } +a { + color: inherit; + text-decoration-color: inherit; +} + +a { + color: #C3EAFF; +} + +a:not(:hover) { + text-decoration: none; +} + html { font-size: 14px; - } html { @@ -22,12 +59,9 @@ body { } .nav-container { - margin-left: 20px; - margin-right: 20px; - margin-bottom: 20px; - margin-top: 0px; - padding-top: 0px; - padding-bottom: 0px; + margin: 0 20px 20px; + padding-top: 0; + padding-bottom: 0; } .nav-container li { @@ -36,9 +70,9 @@ body { } .nav-container li a { - font-size: 17px; + font-size: 16px; color: inherit; - font-family: 'Fira Code', Roboto, sans-serif !important; + font-family: 'Fira Code', Roboto, sans-serif; } @@ -54,7 +88,11 @@ body { } .markdown p { - font-size: 17px; + font-size: 16px; +} + +.markdown a { + color: #C3EAFF; } .markdown hr { @@ -69,4 +107,23 @@ body { max-height: 30rem; border: 7px solid rgba(0, 0, 0, 0); border-radius: 0; box-shadow: 0 0 0 5px #C3EAFF; +} + +.btn-fw { + width: 100%; +} + +article header h1, +article header h2, +article header h3, +article header h4, +article header h5, +article header h6 { + margin: 0; + padding: 5px 8px; +} + +.no-margin { + margin: 0; + padding: 3px 0; } \ No newline at end of file diff --git a/src/FastBlog.Web/wwwroot/static/craborobo.png b/src/FastBlog.Web/wwwroot/static/craborobo.png new file mode 100644 index 0000000..90bcce0 Binary files /dev/null and b/src/FastBlog.Web/wwwroot/static/craborobo.png differ diff --git a/src/FastBlog.Web/wwwroot/static/dog.png b/src/FastBlog.Web/wwwroot/static/dog.png new file mode 100644 index 0000000..107ce03 Binary files /dev/null and b/src/FastBlog.Web/wwwroot/static/dog.png differ diff --git a/src/FastBlog.Web/wwwroot/static/eclipse.png b/src/FastBlog.Web/wwwroot/static/eclipse.png new file mode 100644 index 0000000..4a1bfce Binary files /dev/null and b/src/FastBlog.Web/wwwroot/static/eclipse.png differ diff --git a/src/FastBlog.Web/wwwroot/static/gay.png b/src/FastBlog.Web/wwwroot/static/gay.png new file mode 100644 index 0000000..87c8831 Binary files /dev/null and b/src/FastBlog.Web/wwwroot/static/gay.png differ diff --git a/src/FastBlog.Web/wwwroot/static/hope512.png b/src/FastBlog.Web/wwwroot/static/hope512.png new file mode 100644 index 0000000..7b283ad Binary files /dev/null and b/src/FastBlog.Web/wwwroot/static/hope512.png differ diff --git a/src/FastBlog.Web/wwwroot/static/insomnia2.png b/src/FastBlog.Web/wwwroot/static/insomnia2.png new file mode 100644 index 0000000..0f81510 Binary files /dev/null and b/src/FastBlog.Web/wwwroot/static/insomnia2.png differ diff --git a/src/FastBlog.Web/wwwroot/static/insomnia_pattern.png b/src/FastBlog.Web/wwwroot/static/insomnia_pattern.png new file mode 100644 index 0000000..0f81510 Binary files /dev/null and b/src/FastBlog.Web/wwwroot/static/insomnia_pattern.png differ diff --git a/src/FastBlog.Web/wwwroot/static/inu/dog.png b/src/FastBlog.Web/wwwroot/static/inu/dog.png new file mode 100644 index 0000000..107ce03 Binary files /dev/null and b/src/FastBlog.Web/wwwroot/static/inu/dog.png differ diff --git a/src/FastBlog.Web/wwwroot/static/photo_2024-03-16_23-21-57.jpg b/src/FastBlog.Web/wwwroot/static/photo_2024-03-16_23-21-57.jpg new file mode 100644 index 0000000..bfb967f Binary files /dev/null and b/src/FastBlog.Web/wwwroot/static/photo_2024-03-16_23-21-57.jpg differ diff --git a/src/FastBlog.Web/wwwroot/static/rat_sketch.png b/src/FastBlog.Web/wwwroot/static/rat_sketch.png new file mode 100644 index 0000000..668be59 Binary files /dev/null and b/src/FastBlog.Web/wwwroot/static/rat_sketch.png differ