mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-04-03 05:51:50 -04:00
355 lines
13 KiB
C#
355 lines
13 KiB
C#
using Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests;
|
|
using Cleanuparr.Domain.Enums;
|
|
using Cleanuparr.Persistence;
|
|
using Cleanuparr.Persistence.Models.Configuration;
|
|
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
|
|
|
|
namespace Cleanuparr.Api.Features.DownloadCleaner.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/seeding-rules")]
|
|
[Authorize]
|
|
public class SeedingRulesController : ControllerBase
|
|
{
|
|
private readonly ILogger<SeedingRulesController> _logger;
|
|
private readonly DataContext _dataContext;
|
|
|
|
public SeedingRulesController(
|
|
ILogger<SeedingRulesController> logger,
|
|
DataContext dataContext)
|
|
{
|
|
_logger = logger;
|
|
_dataContext = dataContext;
|
|
}
|
|
|
|
[HttpGet("{downloadClientId}")]
|
|
public async Task<IActionResult> GetSeedingRules(Guid downloadClientId)
|
|
{
|
|
await DataContext.Lock.WaitAsync();
|
|
try
|
|
{
|
|
var client = await _dataContext.DownloadClients
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(c => c.Id == downloadClientId);
|
|
|
|
if (client is null)
|
|
{
|
|
return NotFound(new { Message = $"Download client with ID {downloadClientId} not found" });
|
|
}
|
|
|
|
var rules = await SeedingRuleHelper.GetForClientAsync(_dataContext, client);
|
|
|
|
return Ok(rules);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to retrieve seeding rules for client {ClientId}", downloadClientId);
|
|
return StatusCode(500, new { Message = "Failed to retrieve seeding rules", Error = ex.Message });
|
|
}
|
|
finally
|
|
{
|
|
DataContext.Lock.Release();
|
|
}
|
|
}
|
|
|
|
[HttpPost("{downloadClientId}")]
|
|
public async Task<IActionResult> CreateSeedingRule(Guid downloadClientId, [FromBody] SeedingRuleRequest ruleDto)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return BadRequest(ModelState);
|
|
}
|
|
|
|
await DataContext.Lock.WaitAsync();
|
|
try
|
|
{
|
|
var client = await _dataContext.DownloadClients
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(c => c.Id == downloadClientId);
|
|
|
|
if (client is null)
|
|
{
|
|
return NotFound(new { Message = $"Download client with ID {downloadClientId} not found" });
|
|
}
|
|
|
|
var existingRules = await SeedingRuleHelper.GetForClientAsync(_dataContext, client);
|
|
var duplicate = existingRules.FirstOrDefault(r =>
|
|
r.Name.Equals(ruleDto.Name.Trim(), StringComparison.OrdinalIgnoreCase) &&
|
|
r.PrivacyType == ruleDto.PrivacyType);
|
|
|
|
if (duplicate is not null)
|
|
{
|
|
return BadRequest(new { Message = "A seeding rule with this name and privacy type already exists for this client" });
|
|
}
|
|
|
|
var overlapError = GetPrivacyTypeOverlapError(ruleDto.Name.Trim(), ruleDto.PrivacyType, existingRules, excludeId: null);
|
|
if (overlapError is not null)
|
|
{
|
|
return BadRequest(new { Message = overlapError });
|
|
}
|
|
|
|
var rule = CreateRule(client.TypeName, client.Id, ruleDto);
|
|
rule.Validate();
|
|
|
|
AddRuleToDbSet(rule);
|
|
await _dataContext.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Created seeding rule: {RuleName} with ID: {RuleId} for client {ClientId}",
|
|
rule.Name, rule.Id, downloadClientId);
|
|
|
|
return CreatedAtAction(nameof(GetSeedingRules), new { downloadClientId }, rule);
|
|
}
|
|
catch (ValidationException ex)
|
|
{
|
|
_logger.LogWarning("Validation failed for seeding rule creation: {Message}", ex.Message);
|
|
return BadRequest(new { Message = ex.Message });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to create seeding rule: {RuleName} for client {ClientId}",
|
|
ruleDto.Name, downloadClientId);
|
|
return StatusCode(500, new { Message = "Failed to create seeding rule", Error = ex.Message });
|
|
}
|
|
finally
|
|
{
|
|
DataContext.Lock.Release();
|
|
}
|
|
}
|
|
|
|
[HttpPut("{id}")]
|
|
public async Task<IActionResult> UpdateSeedingRule(Guid id, [FromBody] SeedingRuleRequest ruleDto)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return BadRequest(ModelState);
|
|
}
|
|
|
|
await DataContext.Lock.WaitAsync();
|
|
try
|
|
{
|
|
var (existingRule, _) = await SeedingRuleHelper.FindByIdAsync(_dataContext, id);
|
|
|
|
if (existingRule is null)
|
|
{
|
|
return NotFound(new { Message = $"Seeding rule with ID {id} not found" });
|
|
}
|
|
|
|
// Check for duplicate name+privacyType on the same client, excluding this rule
|
|
var clientRules = await SeedingRuleHelper.GetForClientIdAsync(_dataContext, existingRule.DownloadClientConfigId);
|
|
var duplicate = clientRules.FirstOrDefault(r =>
|
|
r.Id != id &&
|
|
r.Name.Equals(ruleDto.Name.Trim(), StringComparison.OrdinalIgnoreCase) &&
|
|
r.PrivacyType == ruleDto.PrivacyType);
|
|
|
|
if (duplicate is not null)
|
|
{
|
|
return BadRequest(new { Message = "A seeding rule with this name and privacy type already exists for this client" });
|
|
}
|
|
|
|
var overlapError = GetPrivacyTypeOverlapError(ruleDto.Name.Trim(), ruleDto.PrivacyType, clientRules, excludeId: id);
|
|
if (overlapError is not null)
|
|
{
|
|
return BadRequest(new { Message = overlapError });
|
|
}
|
|
|
|
existingRule.Name = ruleDto.Name.Trim();
|
|
existingRule.PrivacyType = ruleDto.PrivacyType;
|
|
existingRule.MaxRatio = ruleDto.MaxRatio;
|
|
existingRule.MinSeedTime = ruleDto.MinSeedTime;
|
|
existingRule.MaxSeedTime = ruleDto.MaxSeedTime;
|
|
existingRule.DeleteSourceFiles = ruleDto.DeleteSourceFiles;
|
|
|
|
existingRule.Validate();
|
|
|
|
await _dataContext.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Updated seeding rule: {RuleName} with ID: {RuleId}", existingRule.Name, id);
|
|
|
|
return Ok(existingRule);
|
|
}
|
|
catch (ValidationException ex)
|
|
{
|
|
_logger.LogWarning("Validation failed for seeding rule update: {Message}", ex.Message);
|
|
return BadRequest(new { Message = ex.Message });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to update seeding rule with ID: {RuleId}", id);
|
|
return StatusCode(500, new { Message = "Failed to update seeding rule", Error = ex.Message });
|
|
}
|
|
finally
|
|
{
|
|
DataContext.Lock.Release();
|
|
}
|
|
}
|
|
|
|
[HttpDelete("{id}")]
|
|
public async Task<IActionResult> DeleteSeedingRule(Guid id)
|
|
{
|
|
await DataContext.Lock.WaitAsync();
|
|
try
|
|
{
|
|
var (existingRule, _) = await SeedingRuleHelper.FindByIdAsync(_dataContext, id);
|
|
|
|
if (existingRule is null)
|
|
{
|
|
return NotFound(new { Message = $"Seeding rule with ID {id} not found" });
|
|
}
|
|
|
|
RemoveRuleFromDbSet(existingRule);
|
|
await _dataContext.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Deleted seeding rule: {RuleName} with ID: {RuleId}", existingRule.Name, id);
|
|
|
|
return NoContent();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to delete seeding rule with ID: {RuleId}", id);
|
|
return StatusCode(500, new { Message = "Failed to delete seeding rule", Error = ex.Message });
|
|
}
|
|
finally
|
|
{
|
|
DataContext.Lock.Release();
|
|
}
|
|
}
|
|
|
|
private static string? GetPrivacyTypeOverlapError(
|
|
string name,
|
|
TorrentPrivacyType privacyType,
|
|
IEnumerable<ISeedingRule> existingRules,
|
|
Guid? excludeId)
|
|
{
|
|
if (privacyType == TorrentPrivacyType.Both)
|
|
{
|
|
var hasConflict = existingRules.Any(r =>
|
|
r.Id != excludeId &&
|
|
r.Name.Equals(name, StringComparison.OrdinalIgnoreCase) &&
|
|
r.PrivacyType != TorrentPrivacyType.Both);
|
|
|
|
return hasConflict
|
|
? "A 'Both' rule cannot coexist with a Public or Private rule for the same category"
|
|
: null;
|
|
}
|
|
else
|
|
{
|
|
var hasConflict = existingRules.Any(r =>
|
|
r.Id != excludeId &&
|
|
r.Name.Equals(name, StringComparison.OrdinalIgnoreCase) &&
|
|
r.PrivacyType == TorrentPrivacyType.Both);
|
|
|
|
return hasConflict
|
|
? "A Public or Private rule cannot coexist with a 'Both' rule for the same category"
|
|
: null;
|
|
}
|
|
}
|
|
|
|
private ISeedingRule CreateRule(DownloadClientTypeName typeName, Guid clientId, SeedingRuleRequest dto)
|
|
{
|
|
return typeName switch
|
|
{
|
|
DownloadClientTypeName.qBittorrent => new QBitSeedingRule
|
|
{
|
|
DownloadClientConfigId = clientId,
|
|
Name = dto.Name.Trim(),
|
|
PrivacyType = dto.PrivacyType,
|
|
MaxRatio = dto.MaxRatio,
|
|
MinSeedTime = dto.MinSeedTime,
|
|
MaxSeedTime = dto.MaxSeedTime,
|
|
DeleteSourceFiles = dto.DeleteSourceFiles,
|
|
},
|
|
DownloadClientTypeName.Deluge => new DelugeSeedingRule
|
|
{
|
|
DownloadClientConfigId = clientId,
|
|
Name = dto.Name.Trim(),
|
|
PrivacyType = dto.PrivacyType,
|
|
MaxRatio = dto.MaxRatio,
|
|
MinSeedTime = dto.MinSeedTime,
|
|
MaxSeedTime = dto.MaxSeedTime,
|
|
DeleteSourceFiles = dto.DeleteSourceFiles,
|
|
},
|
|
DownloadClientTypeName.Transmission => new TransmissionSeedingRule
|
|
{
|
|
DownloadClientConfigId = clientId,
|
|
Name = dto.Name.Trim(),
|
|
PrivacyType = dto.PrivacyType,
|
|
MaxRatio = dto.MaxRatio,
|
|
MinSeedTime = dto.MinSeedTime,
|
|
MaxSeedTime = dto.MaxSeedTime,
|
|
DeleteSourceFiles = dto.DeleteSourceFiles,
|
|
},
|
|
DownloadClientTypeName.uTorrent => new UTorrentSeedingRule
|
|
{
|
|
DownloadClientConfigId = clientId,
|
|
Name = dto.Name.Trim(),
|
|
PrivacyType = dto.PrivacyType,
|
|
MaxRatio = dto.MaxRatio,
|
|
MinSeedTime = dto.MinSeedTime,
|
|
MaxSeedTime = dto.MaxSeedTime,
|
|
DeleteSourceFiles = dto.DeleteSourceFiles,
|
|
},
|
|
DownloadClientTypeName.rTorrent => new RTorrentSeedingRule
|
|
{
|
|
DownloadClientConfigId = clientId,
|
|
Name = dto.Name.Trim(),
|
|
PrivacyType = dto.PrivacyType,
|
|
MaxRatio = dto.MaxRatio,
|
|
MinSeedTime = dto.MinSeedTime,
|
|
MaxSeedTime = dto.MaxSeedTime,
|
|
DeleteSourceFiles = dto.DeleteSourceFiles,
|
|
},
|
|
_ => throw new ArgumentOutOfRangeException(nameof(typeName), typeName, "Unsupported download client type")
|
|
};
|
|
}
|
|
|
|
private void AddRuleToDbSet(ISeedingRule rule)
|
|
{
|
|
switch (rule)
|
|
{
|
|
case QBitSeedingRule qbit:
|
|
_dataContext.QBitSeedingRules.Add(qbit);
|
|
break;
|
|
case DelugeSeedingRule deluge:
|
|
_dataContext.DelugeSeedingRules.Add(deluge);
|
|
break;
|
|
case TransmissionSeedingRule transmission:
|
|
_dataContext.TransmissionSeedingRules.Add(transmission);
|
|
break;
|
|
case UTorrentSeedingRule utorrent:
|
|
_dataContext.UTorrentSeedingRules.Add(utorrent);
|
|
break;
|
|
case RTorrentSeedingRule rtorrent:
|
|
_dataContext.RTorrentSeedingRules.Add(rtorrent);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void RemoveRuleFromDbSet(ISeedingRule rule)
|
|
{
|
|
switch (rule)
|
|
{
|
|
case QBitSeedingRule qbit:
|
|
_dataContext.QBitSeedingRules.Remove(qbit);
|
|
break;
|
|
case DelugeSeedingRule deluge:
|
|
_dataContext.DelugeSeedingRules.Remove(deluge);
|
|
break;
|
|
case TransmissionSeedingRule transmission:
|
|
_dataContext.TransmissionSeedingRules.Remove(transmission);
|
|
break;
|
|
case UTorrentSeedingRule utorrent:
|
|
_dataContext.UTorrentSeedingRules.Remove(utorrent);
|
|
break;
|
|
case RTorrentSeedingRule rtorrent:
|
|
_dataContext.RTorrentSeedingRules.Remove(rtorrent);
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|