Adds multiple media urls for content

This commit is contained in:
Jonathan Bourdon
2024-07-31 17:38:58 -04:00
parent 042fd53463
commit bbcc7a8a33
19 changed files with 319 additions and 111 deletions

View File

@@ -6,7 +6,7 @@ public class Content
public Guid CreatedBy { get; init; }
public DateTimeOffset CreatedAt { get; init; }
public string? Title { get; init; }
public string? Description { get; init; }
public string? Uri { get; init; }
public string Title { get; set; }
public string Description { get; set; }
public string[]? Urls { get; init; }
}

View File

@@ -0,0 +1,114 @@
using System.Collections.Concurrent;
using FastEndpoints;
using FluentValidation;
using Hutopy.Application.AzureBlobStorage.Constants;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Web.Common;
using Hutopy.Web.Features.Contents.Data;
namespace Hutopy.Web.Features.Contents.Handlers;
public record PostContentRequest(
Guid Id,
Guid CreatorId,
string Title,
string Description,
IFormFileCollection Files);
public sealed class PostContentRequestValidator : Validator<PostContentRequest>
{
public PostContentRequestValidator()
{
RuleFor(r => r.Id)
.NotNull().WithMessage("You should specify the Id")
.NotEmpty().WithMessage("You should specify a valid/not empty Id");
RuleFor(r => r.CreatorId)
.NotNull().WithMessage("You should specify the CreatorId")
.NotEmpty().WithMessage("You should specify a valid/not empty CreatorId");
RuleFor(r => r.Title)
.NotNull().WithMessage("You should specify the Title")
.NotEmpty().WithMessage("You should specify a valid/not empty Title");
RuleFor(r => r.Description)
.NotNull().WithMessage("You should specify the Description")
.NotEmpty().WithMessage("You should specify a valid/not empty Description");
}
}
public sealed class PostContent(
IAzureBlobStorageService blobStorage,
ContentDbContext context)
: Endpoint<PostContentRequest>
{
public override void Configure()
{
Post("/api/contents");
Options(o => o.WithTags("Contents"));
AllowFileUploads();
}
public override async Task HandleAsync(
PostContentRequest req,
CancellationToken ct)
{
var urls = new ConcurrentBag<string>();
await Parallel.ForEachAsync(
req.Files,
ct,
async (file, ict) =>
{
try
{
var contentUrl = await SaveFileAsync(
req.CreatorId,
req.Id,
file,
ict);
urls.Add(contentUrl);
}
catch (Exception ex)
{
Logger.LogError("{ErrorMessage}", ex.Message);
}
});
await context.Contents.AddAsync(
new()
{
Id = req.Id,
CreatedBy = User.GetUserId(),
Title = req.Title,
Description = req.Description,
Urls = urls.ToArray()
},
ct);
await context.SaveChangesAsync(ct);
await SendOkAsync(ct);
}
private async Task<string> SaveFileAsync(
Guid creatorId,
Guid contentId,
IFormFile file,
CancellationToken ct = default)
{
var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream, ct);
// TODO: I would like us to use ContainerNames.Creators but it seems we are missing configurations @jbourdon
var url = await blobStorage.UploadFileAsync(
ContainerNames.Users,
$"{creatorId}/{SubDirectoryNames.Contents}/{contentId}/{file.FileName}",
memoryStream,
file.ContentType,
ct: ct);
return url;
}
}

View File

@@ -3,11 +3,11 @@ using FluentValidation;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Application.Common.Models;
namespace Hutopy.Web.Features.Creators.Handlers;
namespace Hutopy.Web.Features.Contents.Handlers;
public sealed class GetCreatorByAliasRequest
{
public string CreatorAlias { get; set; }
public string CreatorAlias { get; init; }
}
public sealed class GetCreatorByAliasRequestValidator
@@ -16,7 +16,8 @@ public sealed class GetCreatorByAliasRequestValidator
public GetCreatorByAliasRequestValidator()
{
RuleFor(r => r.CreatorAlias)
.NotNull().WithMessage("You must specify a creator-alias");
.NotNull().WithMessage("You should specify the CreatorAlias")
.NotEmpty().WithMessage("You should specify a valid/not empty CreatorAlias");
}
}

View File

@@ -1,39 +0,0 @@
using FastEndpoints;
using Hutopy.Web.Common;
using Hutopy.Web.Features.Contents.Data;
namespace Hutopy.Web.Features.Contents.Handlers;
public record struct PostContentRequest(
string? Title,
string? Description,
string? Uri);
public class PostContent(
ContentDbContext context)
: Endpoint<PostContentRequest>
{
public override void Configure()
{
Post("/api/contents");
Options( o => o.WithTags("Contents"));
}
public override async Task HandleAsync(
PostContentRequest req,
CancellationToken ct)
{
await context.Contents.AddAsync(
new Content
{
Id = GuidHelper.GenerateUuidV7(),
CreatedBy = User.GetUserId(),
Title = req.Title,
Description = req.Description,
Uri = req.Uri
},
ct);
await context.SaveChangesAsync(ct);
}
}

View File

@@ -0,0 +1,59 @@
// <auto-generated />
using System;
using Hutopy.Web.Features.Contents.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Hutopy.Web.Features.Contents.Migrations
{
[DbContext(typeof(ContentDbContext))]
[Migration("20240725022229_AddMultipleMediaUrlsToContent")]
partial class AddMultipleMediaUrlsToContent
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Content")
.HasAnnotation("ProductVersion", "8.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.Property<string[]>("Urls")
.HasColumnType("text[]");
b.HasKey("Id");
b.ToTable("Contents", "Content");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Hutopy.Web.Features.Contents.Migrations
{
/// <inheritdoc />
public partial class AddMultipleMediaUrlsToContent : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Uri",
schema: "Content",
table: "Contents");
migrationBuilder.AddColumn<string[]>(
name: "Urls",
schema: "Content",
table: "Contents",
type: "text[]",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Urls",
schema: "Content",
table: "Contents");
migrationBuilder.AddColumn<string>(
name: "Uri",
schema: "Content",
table: "Contents",
type: "text",
nullable: true);
}
}
}

View File

@@ -23,7 +23,7 @@ namespace Hutopy.Web.Contents.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hutopy.Web.Contents.Data.Content", b =>
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -43,8 +43,8 @@ namespace Hutopy.Web.Contents.Migrations
b.Property<string>("Title")
.HasColumnType("text");
b.Property<string>("Uri")
.HasColumnType("text");
b.Property<string[]>("Urls")
.HasColumnType("text[]");
b.HasKey("Id");