Skip to content

Commit

Permalink
[57] get and clean images (#84)
Browse files Browse the repository at this point in the history
* 57 Integrate gql upload mutation

* Fix createdAt field

* Add non null to singleUpload mutation

* add endpoints to get single file and recent files

* Add fileBlob cleanup with Hangfire

* Fix daily blob delete schedule

* missing rest controller init; FileBlob seed data

* Fix PR comment about configurable max_file_size

* Fix PR comments: fix test, refactor recurringJobs

* Update DeleteOldFileBlobsJob.cs

---------

Co-authored-by: Ion Dormenco <[email protected]>
  • Loading branch information
nicolaes and idormenco authored Dec 8, 2023
1 parent f46f40d commit 4158a36
Show file tree
Hide file tree
Showing 36 changed files with 763 additions and 57 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,10 @@ ASALocalRun/
# Project specific
postgres_data
.env

# VSCode
.vscode

# MacOS
.DS_Store
Thumbs.db
25 changes: 25 additions & 0 deletions TeacherWorkout.Api/Controllers/FileController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TeacherWorkout.Data;

namespace TeacherWorkout.Api.Controllers
{
[Route("file")]
[ApiController]
public class FileController(TeacherWorkoutContext context) : ControllerBase
{
private readonly TeacherWorkoutContext _context = context;

[HttpGet("{id}")]
public async Task<IActionResult> GetImage(string id)
{
var fileBlob = await _context.FileBlobs.FindAsync(id);
if (fileBlob == null)
{
return NotFound();
}

return File(fileBlob.Content, fileBlob.Mimetype);
}
}
}
38 changes: 36 additions & 2 deletions TeacherWorkout.Api/GraphQL/TeacherWorkoutMutation.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
using System.ComponentModel.DataAnnotations;
using System.IO;
using GraphQL;
using GraphQL.Types;
using GraphQL.Upload.AspNetCore;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using TeacherWorkout.Api.GraphQL.Resolvers;
using TeacherWorkout.Api.GraphQL.Types.Inputs;
using TeacherWorkout.Api.GraphQL.Types.Payloads;
using TeacherWorkout.Domain.FileBlobs;
using TeacherWorkout.Domain.Lessons;
using TeacherWorkout.Domain.Models.Inputs;
using TeacherWorkout.Domain.Models.Payloads;
Expand All @@ -15,7 +21,9 @@ public class TeacherWorkoutMutation : ObjectGraphType<object>
{
public TeacherWorkoutMutation(CompleteStep completeStep,
CreateTheme createTheme,
UpdateTheme updateTheme)
UpdateTheme updateTheme,
SingleUpload singleUpload,
IConfiguration configuration)
{
Name = "Mutation";

Expand Down Expand Up @@ -50,6 +58,32 @@ public TeacherWorkoutMutation(CompleteStep completeStep,
var input = context.GetArgument<ThemeUpdateInput>("input");
return updateTheme.Execute(input);
});


Field<SingleUploadPayloadType>("singleUpload")
.Argument<NonNullGraphType<UploadGraphType>>(Name = "file")
.Resolve(context =>
{
var file = context.GetArgument<IFormFile>("file");
var maxFileSizeMb = configuration.GetValue("TeacherWorkout:MaxFileSizeMb", 5);
if (file.Length > maxFileSizeMb * 1024 * 1024)
{
throw new ValidationException($"File size exceeds the limit of {maxFileSizeMb}MB.");
}
using var memoryStream = new MemoryStream();
file.CopyTo(memoryStream);
var fileBytes = memoryStream.ToArray();
return singleUpload.Execute(new SingleUploadInput
{
Content = fileBytes,
Mimetype = file.ContentType,
FileName = file.FileName,
});
});

}
}
}
}
9 changes: 8 additions & 1 deletion TeacherWorkout.Api/GraphQL/TeacherWorkoutQuery.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using GraphQL;
using GraphQL.Types;
using TeacherWorkout.Api.GraphQL.Types;
using TeacherWorkout.Api.GraphQL.Utils;
using TeacherWorkout.Domain.Common;
using TeacherWorkout.Domain.FileBlobs;
using TeacherWorkout.Domain.Lessons;
using TeacherWorkout.Domain.Themes;

Expand All @@ -12,7 +14,8 @@ public class TeacherWorkoutQuery : ObjectGraphType<object>
public TeacherWorkoutQuery(GetThemes getThemes,
GetLessons getLessons,
GetLessonStatuses getLessonStatuses,
GetStep getStep)
GetStep getStep,
GetFileBlobs getFileBlobs)
{
Name = "Query";

Expand All @@ -35,6 +38,10 @@ public TeacherWorkoutQuery(GetThemes getThemes,
Field<ListGraphType<NonNullGraphType<LessonStatusType>>>("lessonStatuses")
.Argument<NonNullGraphType<ListGraphType<NonNullGraphType<IdGraphType>>>>(Name = "lessonIds", Description = "Id's of leassons")
.Resolve(context => getLessonStatuses.Execute(context.ToInput<LessonStatusFilter>()));

Field<ListGraphType<NonNullGraphType<FileBlobType>>>("recentImageUploads")
.Argument<NonNullGraphType<IntGraphType>>("limit", "The number of recent images to return.")
.Resolve(context => getFileBlobs.Execute(context.GetArgument<int>("limit")));
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions TeacherWorkout.Api/GraphQL/Types/FileBlobType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using GraphQL.Types;
using TeacherWorkout.Domain.Models;

namespace TeacherWorkout.Api.GraphQL.Types
{
public class FileBlobType : ObjectGraphType<FileBlob>
{
public FileBlobType()
{
Name = "FileBlob";

Field(x => x.Id, nullable: false).Description("The unique identifier of the file blob.");
Field(x => x.CreatedAt, nullable: false).Description("The creation time of the file blob.");
}
}
}
3 changes: 2 additions & 1 deletion TeacherWorkout.Api/GraphQL/Types/ImageType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ public ImageType()
{
Name = "Image";

Field(x => x.Url, true).Description("URL to the image.");
Field(x => x.Url, true).Description("URL to the image. If null, use FileBlob ID to generate an URL: /file/<fileBlobId>");
Field(x => x.Description, true).Description("Image description for accessibility.");
Field(x => x.FileBlobId, true).Description("Reference to local file. If null, use Url property.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ public ThemeCreateInputType()
{
Name = "ThemeCreateInput";

Field(x => x.ThumbnailId, type: typeof(IdGraphType));
Field(x => x.Title);
Field(x => x.FileBlobId, true, type: typeof(IdGraphType))
.Description("Id of uploaded image blob (precedes and overwrites ThumbnailId)");
Field(x => x.ThumbnailId, true, type: typeof(IdGraphType))
.Description("Id of existing thumbnail");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using GraphQL.Types;
using TeacherWorkout.Domain.Models.Payloads;

namespace TeacherWorkout.Api.GraphQL.Types.Payloads
{
public class SingleUploadPayloadType : ObjectGraphType<SingleUploadPayload>
{
public SingleUploadPayloadType()
{
Name = "SingleUploadPayload";

Field(x => x.FileBlobId).Description("The ID of the created file blob.");
}
}
}
7 changes: 7 additions & 0 deletions TeacherWorkout.Api/Jobs/Config/DeleteOldFileBlobsConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TeacherWorkout.Api.Jobs.Config
{
public record DeleteOldFileBlobsConfig : RecurringJobConfig
{
public int DaysInThePast { get; set; }
}
}
8 changes: 8 additions & 0 deletions TeacherWorkout.Api/Jobs/Config/RecurringJobConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TeacherWorkout.Api.Jobs.Config
{
public record RecurringJobConfig
{
public bool IsEnabled { get; set; }
public string CronExpression { get; set; }
}
}
20 changes: 20 additions & 0 deletions TeacherWorkout.Api/Jobs/DeleteOldFileBlobsJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.Extensions.Options;
using TeacherWorkout.Api.Jobs.Interfaces;
using TeacherWorkout.Domain.FileBlobs;
using TeacherWorkout.Api.Jobs.Config;

namespace TeacherWorkout.Api.Jobs
{

public class DeleteOldFileBlobsJob(IFileBlobRepository repository, IOptions<DeleteOldFileBlobsConfig> config) : IDeleteOldFileBlobsJob
{
private readonly IFileBlobRepository _repository = repository;
private readonly DeleteOldFileBlobsConfig _config = config.Value;

public void Run()
{
;
_repository.DeleteOldEntries(_config.DaysInThePast);
}
}
}
10 changes: 10 additions & 0 deletions TeacherWorkout.Api/Jobs/Interfaces/IDeleteOldFileBlobsJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading;
using System.Threading.Tasks;

namespace TeacherWorkout.Api.Jobs.Interfaces
{
public interface IDeleteOldFileBlobsJob
{
void Run();
}
}
49 changes: 43 additions & 6 deletions TeacherWorkout.Api/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
using System.Linq;
using GraphQL;
using GraphQL.Types;
using Hangfire;
using Hangfire.PostgreSql;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using TeacherWorkout.Api.GraphQL;
using TeacherWorkout.Api.Jobs;
using TeacherWorkout.Api.Jobs.Config;
using TeacherWorkout.Api.Jobs.Interfaces;
using TeacherWorkout.Data;
using TeacherWorkout.Domain.Common;

Expand Down Expand Up @@ -41,18 +46,36 @@ public void ConfigureServices(IServiceCollection services)
AddOperations(services);
AddRepositories(services, "TeacherWorkout.Data");

services.AddControllers();
services.AddHttpContextAccessor();
services.AddGraphQL(b => b
.AddErrorInfoProvider(opt => opt.ExposeExceptionDetails = true)
.AddGraphTypes()
.AddSystemTextJson());
services
.AddGraphQLUpload()
.AddGraphQL(b => b
.AddErrorInfoProvider(opt => opt.ExposeExceptionDetails = true)
.AddGraphTypes()
.AddSystemTextJson());

services.AddDbContext<TeacherWorkoutContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("TeacherWorkoutContext")));

services.AddHangfire(configuration =>
{
configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_170);
configuration.UseSimpleAssemblyNameTypeSerializer();
configuration.UseRecommendedSerializerSettings();
// Initialize JobStorage
configuration.UsePostgreSqlStorage(c =>
c.UseNpgsqlConnection(Configuration.GetConnectionString("TeacherWorkoutContext")));
});
services.AddHangfireServer(config => config.WorkerCount = 1);

services.Configure<DeleteOldFileBlobsConfig>(Configuration.GetSection("TeacherWorkout:RecurringJobs:DeleteOldFileBlobs"));
services.AddScoped<IDeleteOldFileBlobsJob, DeleteOldFileBlobsJob>();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, TeacherWorkoutContext db)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, TeacherWorkoutContext db, IServiceProvider serviceProvider)
{
app.UseCors();

Expand All @@ -66,9 +89,23 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, TeacherW
}

app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapControllers());

app.UseGraphQL<ISchema>();
app.UseGraphQLUpload<ISchema>()
.UseGraphQL<ISchema>();
app.UseGraphQLGraphiQL();

var deleteOldFileBlobsConfig = Configuration.GetSection("TeacherWorkout:RecurringJobs:DeleteOldFileBlobs")
.Get<DeleteOldFileBlobsConfig>();
if (deleteOldFileBlobsConfig.IsEnabled)
{
RecurringJob.AddOrUpdate<IDeleteOldFileBlobsJob>(
nameof(DeleteOldFileBlobsJob),
job => job.Run(),
deleteOldFileBlobsConfig.CronExpression,
new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc }
);
}
}

private static void AddOperations(IServiceCollection services)
Expand Down
3 changes: 3 additions & 0 deletions TeacherWorkout.Api/TeacherWorkout.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<ItemGroup>
<PackageReference Include="GraphQL.Server.All" Version="7.6.0" />
<PackageReference Include="GraphQL.Server.Ui.Graphiql" Version="7.6.0" />
<PackageReference Include="GraphQL.Upload.AspNetCore" Version="3.0.3" />
<PackageReference Include="Hangfire" Version="1.8.6" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.20.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
10 changes: 10 additions & 0 deletions TeacherWorkout.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,15 @@
"ConnectionStrings": {
//connection string for release (docker-compose) enviroment
"TeacherWorkoutContext": "Server=postgres;Port=5432;Database=teacher_workout;User Id=docker;Password=docker;"
},
"TeacherWorkout": {
"MaxFileSizeMb": 5,
"RecurringJobs": {
"DeleteOldFileBlobs": {
"IsEnabled": "true",
"CronExpression": "0 0 * * *",
"DaysInThePast": "1"
}
}
}
}
Loading

0 comments on commit 4158a36

Please sign in to comment.