Polianin Nikita b40e394bcf
Some checks failed
.NET Test Pipeline / build-and-test (push) Has been cancelled
Build and Deploy Docker Container / build-and-deploy (push) Successful in 1m45s
fix: System.ObjectDisposedException for db context into sync secrvice
2025-02-03 10:55:47 +03:00

212 lines
8.5 KiB
C#

using Asp.Versioning;
using Cronos;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Mirea.Api.DataAccess.Persistence;
using Mirea.Api.Dto.Common;
using Mirea.Api.Dto.Responses.Configuration;
using Mirea.Api.Endpoint.Common.Exceptions;
using Mirea.Api.Endpoint.Common.MapperDto;
using Mirea.Api.Endpoint.Common.Services;
using Mirea.Api.Endpoint.Configuration.Model;
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
using Mirea.Api.Endpoint.Sync;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Mirea.Api.Endpoint.Controllers.V1.Configuration;
[ApiVersion("1.0")]
public class ScheduleController(ILogger<ScheduleController> logger, IOptionsSnapshot<GeneralConfig> config, UberDbContext dbContext, IServiceProvider provider) : ConfigurationBaseController
{
/// <summary>
/// Retrieves the cron update schedule and calculates the next scheduled tasks based on the provided depth.
/// </summary>
/// <param name="depth">The depth of the next tasks to retrieve.</param>
/// <returns>Cron expression and the list of next scheduled task dates.</returns>
[HttpGet("CronUpdateSchedule")]
public ActionResult<CronUpdateScheduleResponse> CronUpdateSchedule([FromQuery][Range(0, 10)] int depth = 5)
{
var cronExpression = CronExpression.Parse(config.Value.ScheduleSettings!.CronUpdateSchedule);
var nextTasks = config.Value.ScheduleSettings!.CronUpdateSkipDateList.GetNextTask(cronExpression, depth);
return new CronUpdateScheduleResponse()
{
Cron = config.Value.ScheduleSettings!.CronUpdateSchedule,
NextStart = nextTasks.Select(x => DateTime.SpecifyKind(x.DateTime, DateTimeKind.Local)).ToList()
};
}
/// <summary>
/// Updates the cron update schedule with the provided cron expression.
/// </summary>
/// <param name="cron">The cron expression to set as the new schedule.</param>
/// <returns>Cron expression and the list of next scheduled task dates.</returns>
/// <exception cref="ControllerArgumentException">Thrown if the provided cron expression is invalid.</exception>
[HttpPost("CronUpdateSchedule")]
public ActionResult<CronUpdateScheduleResponse> CronUpdateSchedule([FromQuery] string cron)
{
cron = cron.Trim();
if (!CronExpression.TryParse(cron, CronFormat.Standard, out _))
throw new ControllerArgumentException("Incorrect cron value.");
if (config.Value.ScheduleSettings!.CronUpdateSchedule == cron)
return CronUpdateSchedule();
config.Value.ScheduleSettings!.CronUpdateSchedule = cron;
config.Value.SaveSetting();
return CronUpdateSchedule();
}
/// <summary>
/// Retrieves the start term date from the configuration.
/// </summary>
/// <returns>Start term date.</returns>
[HttpGet("StartTerm")]
public ActionResult<DateOnly> StartTerm() =>
config.Value.ScheduleSettings!.StartTerm;
/// <summary>
/// Updates the start term date in the configuration.
/// </summary>
/// <param name="startTerm">The new start term date to set.</param>
/// <param name="force">If true, forces an update by deleting all existing lessons.</param>
/// <returns>Success or failure.</returns>
/// <exception cref="ControllerArgumentException">Thrown if the start term date is more than 6 months in the past or future.</exception>
[HttpPost("StartTerm")]
public ActionResult StartTerm([FromQuery] DateOnly startTerm, [FromQuery] bool force = false)
{
var differentByTime = DateTime.Now - startTerm.ToDateTime(new TimeOnly(0, 0, 0));
if (differentByTime > TimeSpan.FromDays(190) || differentByTime.Multiply(-1) > TimeSpan.FromDays(190))
throw new ControllerArgumentException("The semester can't start more than 6 months from now, and it can't have started more than 6 months ago either.");
config.Value.ScheduleSettings!.StartTerm = startTerm;
config.Value.SaveSetting();
if (!force)
return Ok();
logger.LogWarning("A force update is being performed at the beginning of the semester (all classes will be deleted).");
dbContext.Lessons.RemoveRange(dbContext.Lessons.ToList());
dbContext.SaveChanges();
return Ok();
}
/// <summary>
/// Retrieves the list of cron update skip dates filtered by the current date.
/// </summary>
/// <returns>Cron update skip dates.</returns>
[HttpGet("CronUpdateSkip")]
public ActionResult<List<CronUpdateSkip>> CronUpdateSkip()
{
var generalConfig = config.Value;
generalConfig.ScheduleSettings!.CronUpdateSkipDateList =
generalConfig.ScheduleSettings.CronUpdateSkipDateList.Filter();
generalConfig.SaveSetting();
return generalConfig.ScheduleSettings!.CronUpdateSkipDateList
.ConvertToDto();
}
/// <summary>
/// Updates the list of cron update skip dates in the configuration.
/// </summary>
/// <param name="cronUpdateDate">The list of cron update skip dates to set.</param>
/// <returns>Success or failure.</returns>
/// <exception cref="ControllerArgumentException">Thrown if the provided list of cron update skip dates is invalid.</exception>
[HttpPost("CronUpdateSkip")]
public ActionResult CronUpdateSkip([FromBody] List<CronUpdateSkip> cronUpdateDate)
{
List<ScheduleSettings.CronUpdateSkip> result;
try
{
result = cronUpdateDate.ConvertFromDto();
}
catch (ArgumentException ex)
{
throw new ControllerArgumentException(ex.Message);
}
config.Value.ScheduleSettings!.CronUpdateSkipDateList = result.Filter();
config.Value.SaveSetting();
return Ok();
}
/// <summary>
/// Uploads schedule files and initiates synchronization.
/// </summary>
/// <param name="files">The list of schedule files to upload.</param>
/// <param name="defaultCampus">The default campus for each uploaded file. Must match the number of files.</param>
/// <param name="force">If true, removes all existing lessons before synchronization. Default is false.</param>
/// <returns>Success or failure.</returns>
/// <exception cref="ControllerArgumentException">
/// Thrown if:
/// - No files are provided.
/// - The number of default campuses does not match the number of files.
/// - Any default campus is null or empty.
/// </exception>
[HttpPost("Upload")]
public async Task<ActionResult> UploadScheduleFiles(List<IFormFile>? files, [FromQuery] string[]? defaultCampus, [FromQuery] bool force = false)
{
if (files == null || files.Count == 0)
throw new ControllerArgumentException("No files were found.");
if (defaultCampus == null || files.Count != defaultCampus.Length)
throw new ControllerArgumentException("No default campuses are specified for the file.");
if (defaultCampus.Any(string.IsNullOrEmpty))
throw new ControllerArgumentException("Each file should have a default campus.");
var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
if (!Directory.Exists(tempDirectory))
Directory.CreateDirectory(tempDirectory);
List<(string, string)> filePaths = [];
for (var i = 0; i < files.Count; i++)
{
if (files[i].Length <= 0)
continue;
var filePath = Path.Combine(tempDirectory, files[i].FileName);
await using var stream = new FileStream(filePath, FileMode.Create);
await files[i].CopyToAsync(stream);
filePaths.Add((filePath, defaultCampus[i]));
}
if (force)
{
dbContext.Lessons.RemoveRange(await dbContext.Lessons.ToListAsync());
await dbContext.SaveChangesAsync();
}
var scopeFactory = provider.GetRequiredService<IServiceScopeFactory>();
_ = Task.Run(async () =>
{
using var scope = scopeFactory.CreateScope();
var sync = scope.ServiceProvider.GetRequiredService<ScheduleSynchronizer>();
await sync.StartSync(filePaths, CancellationToken.None);
}, CancellationToken.None);
return Ok();
}
}