From c2acf11167ba031c19e85a3d7d25f4d1337de269 Mon Sep 17 00:00:00 2001 From: DariusIII Date: Mon, 10 Aug 2026 20:12:00 +0200 Subject: [PATCH] Use MB/GB for size settings in views --- .../Admin/AdminGroupController.php | 15 +- .../Controllers/Admin/AdminSiteController.php | 17 +- .../AdditionalCandidateQuery.php | 50 ++--- .../AdditionalProcessingOrchestrator.php | 4 +- .../Config/ProcessingConfiguration.php | 8 +- app/Services/ForkingService.php | 6 - app/Services/NfoService.php | 8 +- app/Support/SizeUnit.php | 81 +++++++ ..._192527_convert_size_settings_to_bytes.php | 68 ++++++ database/seeders/SettingsTableSeeder.php | 8 +- resources/views/admin/groups/edit.blade.php | 31 ++- .../additional-usenet-settings.blade.php | 22 +- .../nfo-processing-settings.blade.php | 22 +- .../site/sections/usenet-settings.blade.php | 28 ++- ...itionalProcessingOrchestratorClaimTest.php | 4 +- tests/Feature/AdminGroupControllerTest.php | 107 +++++++++ tests/Feature/AdminSiteControllerTest.php | 210 ++++++++++++++++++ .../CreatesProcessingConfiguration.php | 4 +- tests/Unit/Support/SizeUnitTest.php | 83 +++++++ 19 files changed, 697 insertions(+), 79 deletions(-) create mode 100644 app/Support/SizeUnit.php create mode 100644 database/migrations/2026_08_10_192527_convert_size_settings_to_bytes.php create mode 100644 tests/Feature/AdminSiteControllerTest.php create mode 100644 tests/Unit/Support/SizeUnitTest.php diff --git a/app/Http/Controllers/Admin/AdminGroupController.php b/app/Http/Controllers/Admin/AdminGroupController.php index 4e409a623..715d50ccd 100644 --- a/app/Http/Controllers/Admin/AdminGroupController.php +++ b/app/Http/Controllers/Admin/AdminGroupController.php @@ -7,6 +7,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\BasePageController; use App\Http\Requests\Admin\AdminGroupListRequest; use App\Models\UsenetGroup; +use App\Support\SizeUnit; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; @@ -75,6 +76,16 @@ class AdminGroupController extends BasePageController switch ($action) { case 'submit': + // Convert the size + unit pair to bytes; a blank input stays blank + // so the model stores null and the site-wide setting applies. + $minSizeInput = $request->input('minsizetoformrelease'); + if ($minSizeInput !== null && $minSizeInput !== '') { + $request->merge([ + 'minsizetoformrelease' => SizeUnit::toBytes($minSizeInput, $request->input('minsizetoformrelease_unit', 'MB')), + ]); + } + $request->request->remove('minsizetoformrelease_unit'); + if (empty($request->input('id'))) { // Add a new group. $request->merge(['name' => UsenetGroup::isValidGroup($request->input('name'))]); @@ -101,7 +112,9 @@ class AdminGroupController extends BasePageController break; } - return view('admin.groups.edit', compact('title', 'group')); + $groupMinSize = SizeUnit::fromBytes($group['minsizetoformrelease'] ?? 0); + + return view('admin.groups.edit', compact('title', 'group', 'groupMinSize') + ['sizeUnits' => SizeUnit::UNITS]); } /** diff --git a/app/Http/Controllers/Admin/AdminSiteController.php b/app/Http/Controllers/Admin/AdminSiteController.php index 854bfd1f7..b121d46e3 100644 --- a/app/Http/Controllers/Admin/AdminSiteController.php +++ b/app/Http/Controllers/Admin/AdminSiteController.php @@ -10,6 +10,7 @@ use App\Models\ReleaseStat; use App\Models\RoleStat; use App\Models\Settings; use App\Models\SignupStat; +use App\Support\SizeUnit; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; @@ -32,7 +33,14 @@ class AdminSiteController extends BasePageController switch ($action) { case 'submit': - Settings::settingsUpdate($request->all()); + $data = $request->all(); + + foreach (SizeUnit::SITE_SIZE_SETTINGS as $sizeKey) { + $data[$sizeKey] = SizeUnit::toBytes($data[$sizeKey] ?? null, $data[$sizeKey.'_unit'] ?? 'MB'); + unset($data[$sizeKey.'_unit']); + } + + Settings::settingsUpdate($data); return redirect()->to('admin/site-edit')->with('success', 'Settings updated successfully'); @@ -43,8 +51,15 @@ class AdminSiteController extends BasePageController $compress_headers_warning = ! str_contains(config('settings.nntp_server'), 'astra') ? 'compress_headers_warning' : ''; + $sizeFields = []; + foreach (SizeUnit::SITE_SIZE_SETTINGS as $sizeKey) { + $sizeFields[$sizeKey] = SizeUnit::fromBytes($this->viewData['site'][$sizeKey] ?? 0); + } + $this->viewData = array_merge($this->viewData, [ 'error' => $error, + 'sizeFields' => $sizeFields, + 'sizeUnits' => SizeUnit::UNITS, 'yesno' => [ 'ids' => [1, 0], 'names' => ['Yes', 'No'], diff --git a/app/Services/AdditionalProcessing/AdditionalCandidateQuery.php b/app/Services/AdditionalProcessing/AdditionalCandidateQuery.php index b2a3791ce..580f82500 100644 --- a/app/Services/AdditionalProcessing/AdditionalCandidateQuery.php +++ b/app/Services/AdditionalProcessing/AdditionalCandidateQuery.php @@ -29,11 +29,11 @@ use Illuminate\Support\Facades\Schema; */ final class AdditionalCandidateQuery { - /** Default size lower bound when the setting is empty/unset (megabytes). */ - public const int DEFAULT_MIN_SIZE_MB = 1; + /** Default size lower bound when the setting is empty/unset (bytes, 1 MB). */ + public const int DEFAULT_MIN_SIZE_BYTES = 1048576; - /** Default size upper bound when the setting is empty/unset (gigabytes). */ - public const int DEFAULT_MAX_SIZE_GB = 100; + /** Default size upper bound when the setting is empty/unset (bytes, 100 GB). */ + public const int DEFAULT_MAX_SIZE_BYTES = 107374182400; /** * Hard cap on the bucket fan-out. `leftguid` is the first character of a @@ -52,32 +52,32 @@ final class AdditionalCandidateQuery private static ?bool $supportsClaims = null; /** - * Resolve the minimum-size filter (megabytes). Returns 0 when disabled. + * Resolve the minimum-size filter (bytes). Returns 0 when disabled. * * An explicit '0' setting means "no minimum size filter". An empty/null - * setting falls back to {@see self::DEFAULT_MIN_SIZE_MB}. + * setting falls back to {@see self::DEFAULT_MIN_SIZE_BYTES}. */ - public static function minSizeMB(): int + public static function minSizeBytes(): int { $value = Settings::settingValue('minsizetopostprocess'); if ($value === '' || $value === null) { - return self::DEFAULT_MIN_SIZE_MB; + return self::DEFAULT_MIN_SIZE_BYTES; } return max(0, (int) $value); } /** - * Resolve the maximum-size filter (gigabytes). Returns 0 when disabled. + * Resolve the maximum-size filter (bytes). Returns 0 when disabled. * * An explicit '0' setting means "no maximum size filter". An empty/null - * setting falls back to {@see self::DEFAULT_MAX_SIZE_GB}. + * setting falls back to {@see self::DEFAULT_MAX_SIZE_BYTES}. */ - public static function maxSizeGB(): int + public static function maxSizeBytes(): int { $value = Settings::settingValue('maxsizetopostprocess'); if ($value === '' || $value === null) { - return self::DEFAULT_MAX_SIZE_GB; + return self::DEFAULT_MAX_SIZE_BYTES; } return max(0, (int) $value); @@ -97,22 +97,22 @@ final class AdditionalCandidateQuery Builder $query, int|string $groupID = '', string $guidChar = '', - ?int $minSizeMB = null, - ?int $maxSizeGB = null, + ?int $minSizeBytes = null, + ?int $maxSizeBytes = null, bool $includeClaimed = false, ): Builder { - $min = $minSizeMB ?? self::minSizeMB(); - $max = $maxSizeGB ?? self::maxSizeGB(); + $min = $minSizeBytes ?? self::minSizeBytes(); + $max = $maxSizeBytes ?? self::maxSizeBytes(); $query ->where('r.passwordstatus', -1) ->where('r.haspreview', -1) ->where('r.nzbstatus', 1) ->where('c.disablepreview', 0); if ($min > 0) { - $query->where('r.size', '>', $min * 1048576); + $query->where('r.size', '>', $min); } if ($max > 0) { - $query->where('r.size', '<', $max * 1073741824); + $query->where('r.size', '<', $max); } if ($groupID !== '' && $groupID !== 0 && $groupID !== '0') { $query->where('r.groups_id', $groupID); @@ -136,15 +136,15 @@ final class AdditionalCandidateQuery public static function baseBuilder( int|string $groupID = '', string $guidChar = '', - ?int $minSizeMB = null, - ?int $maxSizeGB = null, + ?int $minSizeBytes = null, + ?int $maxSizeBytes = null, bool $includeClaimed = false, ): Builder { $query = Release::query() ->from('releases as r') ->leftJoin('categories as c', 'c.id', '=', 'r.categories_id'); - return self::applyPredicates($query, $groupID, $guidChar, $minSizeMB, $maxSizeGB, $includeClaimed); + return self::applyPredicates($query, $groupID, $guidChar, $minSizeBytes, $maxSizeBytes, $includeClaimed); } /** @@ -277,16 +277,16 @@ final class AdditionalCandidateQuery int $limit, string $token, int|string $groupID = '', - ?int $minSizeMB = null, - ?int $maxSizeGB = null, + ?int $minSizeBytes = null, + ?int $maxSizeBytes = null, array $columns = ['*'], array $excludedReleaseIds = [], ): EloquentCollection { $effectiveLimit = max(1, $limit); - return DB::transaction(function () use ($guidChar, $effectiveLimit, $token, $groupID, $minSizeMB, $maxSizeGB, $columns, $excludedReleaseIds): EloquentCollection { + return DB::transaction(function () use ($guidChar, $effectiveLimit, $token, $groupID, $minSizeBytes, $maxSizeBytes, $columns, $excludedReleaseIds): EloquentCollection { $supportsClaims = self::supportsClaims(); - $query = self::baseBuilder($groupID, $guidChar, $minSizeMB, $maxSizeGB) + $query = self::baseBuilder($groupID, $guidChar, $minSizeBytes, $maxSizeBytes) ->select('r.id') ->orderByDesc('r.postdate') ->orderBy('r.id') diff --git a/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php b/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php index c73cc8d52..1e313d305 100644 --- a/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php +++ b/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php @@ -151,8 +151,8 @@ class AdditionalProcessingOrchestrator $this->config->queryLimit > 0 ? $this->config->queryLimit : 25, $this->claimToken, $groupID, - $this->config->minSizeMB, - $this->config->maxSizeGB, + $this->config->minSizeBytes, + $this->config->maxSizeBytes, [ 'id', 'guid', diff --git a/app/Services/AdditionalProcessing/Config/ProcessingConfiguration.php b/app/Services/AdditionalProcessing/Config/ProcessingConfiguration.php index a7dca06bb..9893804bf 100644 --- a/app/Services/AdditionalProcessing/Config/ProcessingConfiguration.php +++ b/app/Services/AdditionalProcessing/Config/ProcessingConfiguration.php @@ -39,9 +39,9 @@ final readonly class ProcessingConfiguration public int $maximumRarPasswordChecks; - public int $maxSizeGB; + public int $maxSizeBytes; - public int $minSizeMB; + public int $minSizeBytes; public bool $alternateNNTP; @@ -117,8 +117,8 @@ final readonly class ProcessingConfiguration // (explicit '0' means disabled, empty/null means default) are owned // in one place and shared between the bucket query and the per-worker // fetch. - $this->maxSizeGB = AdditionalCandidateQuery::maxSizeGB(); - $this->minSizeMB = AdditionalCandidateQuery::minSizeMB(); + $this->maxSizeBytes = AdditionalCandidateQuery::maxSizeBytes(); + $this->minSizeBytes = AdditionalCandidateQuery::minSizeBytes(); $this->alternateNNTP = (bool) config('nntmux_nntp.use_alternate_nntp_server'); $this->ffmpegDuration = (int) (Settings::settingValue('ffmpeg_duration') ?: 5); $this->addPAR2Files = (bool) config('nntmux_settings.add_par2'); diff --git a/app/Services/ForkingService.php b/app/Services/ForkingService.php index 51076e1ec..98fa84d62 100644 --- a/app/Services/ForkingService.php +++ b/app/Services/ForkingService.php @@ -29,16 +29,10 @@ class ForkingService protected PostProcessRunner $postProcessRunner; - protected int $maxSize; - - protected int $minSize; - protected int $maxRetries; public function __construct() { - $this->maxSize = (int) Settings::settingValue('maxsizetoprocessnfo'); - $this->minSize = (int) Settings::settingValue('minsizetoprocessnfo'); $this->maxRetries = (int) Settings::settingValue('maxnforetries') >= 0 ? -((int) Settings::settingValue('maxnforetries') + 1) : NfoService::NFO_UNPROC; diff --git a/app/Services/NfoService.php b/app/Services/NfoService.php index fefc7ced2..abd0b0010 100644 --- a/app/Services/NfoService.php +++ b/app/Services/NfoService.php @@ -830,11 +830,11 @@ class NfoService } if ($this->getMaxSize() > 0) { - $query->where('size', '<', $this->getMaxSize() * 1073741824); + $query->where('size', '<', $this->getMaxSize()); } if ($this->getMinSize() > 0) { - $query->where('size', '>', $this->getMinSize() * 1048576); + $query->where('size', '>', $this->getMinSize()); } return $query; @@ -985,8 +985,8 @@ class NfoService 'AND r.nfostatus BETWEEN %d AND %d %s %s', ($maxRetries < -8 ? -8 : $maxRetries), self::NFO_UNPROC, - ($maxSize > 0 ? ('AND r.size < '.($maxSize * 1073741824)) : ''), - ($minSize > 0 ? ('AND r.size > '.($minSize * 1048576)) : '') + ($maxSize > 0 ? ('AND r.size < '.$maxSize) : ''), + ($minSize > 0 ? ('AND r.size > '.$minSize) : '') ); } diff --git a/app/Support/SizeUnit.php b/app/Support/SizeUnit.php new file mode 100644 index 000000000..ea57a1c42 --- /dev/null +++ b/app/Support/SizeUnit.php @@ -0,0 +1,81 @@ + + */ + public const array SITE_SIZE_SETTINGS = [ + 'minsizetoformrelease', + 'maxsizetoformrelease', + 'minsizetopostprocess', + 'maxsizetopostprocess', + 'minsizetoprocessnfo', + 'maxsizetoprocessnfo', + ]; + + /** + * Convert a value expressed in the given unit to bytes. + * + * Empty and non-positive values map to 0 ("disabled" semantics). + */ + public static function toBytes(int|float|string|null $value, string $unit): int + { + $multiplier = match (strtoupper($unit)) { + 'MB' => self::MB, + 'GB' => self::GB, + default => throw new InvalidArgumentException("Unsupported size unit [$unit]."), + }; + + if ($value === null || $value === '' || ! is_numeric($value)) { + return 0; + } + + return max(0, (int) round((float) $value * $multiplier)); + } + + /** + * Split a byte count into a value + unit pair for display. + * + * GB is preferred when the value divides evenly into gibibytes, otherwise + * MB is used (rounded to two decimals when not evenly divisible). + * + * @return array{value: int|float, unit: string} + */ + public static function fromBytes(int|float|string|null $bytes): array + { + $bytes = is_numeric($bytes) ? (int) $bytes : 0; + + if ($bytes <= 0) { + return ['value' => 0, 'unit' => 'MB']; + } + + if ($bytes % self::GB === 0) { + return ['value' => intdiv($bytes, self::GB), 'unit' => 'GB']; + } + + if ($bytes % self::MB === 0) { + return ['value' => intdiv($bytes, self::MB), 'unit' => 'MB']; + } + + return ['value' => round($bytes / self::MB, 2), 'unit' => 'MB']; + } +} diff --git a/database/migrations/2026_08_10_192527_convert_size_settings_to_bytes.php b/database/migrations/2026_08_10_192527_convert_size_settings_to_bytes.php new file mode 100644 index 000000000..0d8f1a8d4 --- /dev/null +++ b/database/migrations/2026_08_10_192527_convert_size_settings_to_bytes.php @@ -0,0 +1,68 @@ + + */ + private const array GB_SETTINGS = [ + 'maxsizetopostprocess' => 1073741824, + 'maxsizetoprocessnfo' => 1073741824, + ]; + + /** + * Settings that were stored in megabytes and their byte multiplier. + * + * @var array + */ + private const array MB_SETTINGS = [ + 'minsizetopostprocess' => 1048576, + 'minsizetoprocessnfo' => 1048576, + ]; + + /** + * Convert legacy MB/GB size setting values to bytes. + * + * minsizetoformrelease / maxsizetoformrelease were always stored in bytes + * and are intentionally untouched. On a fresh install the settings table + * is still empty when migrations run, so this is a no-op and the seeder + * inserts byte defaults directly. + */ + public function up(): void + { + foreach (self::GB_SETTINGS + self::MB_SETTINGS as $name => $multiplier) { + $value = DB::table('settings')->where('name', $name)->value('value'); + + if (! is_numeric($value) || (float) $value <= 0) { + continue; + } + + DB::table('settings') + ->where('name', $name) + ->update(['value' => (string) ((int) round((float) $value * $multiplier))]); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + foreach (self::GB_SETTINGS + self::MB_SETTINGS as $name => $multiplier) { + $value = DB::table('settings')->where('name', $name)->value('value'); + + if (! is_numeric($value) || (float) $value <= 0) { + continue; + } + + DB::table('settings') + ->where('name', $name) + ->update(['value' => (string) (int) ((float) $value / $multiplier)]); + } + } +}; diff --git a/database/seeders/SettingsTableSeeder.php b/database/seeders/SettingsTableSeeder.php index 0d49fa302..da552d8ef 100644 --- a/database/seeders/SettingsTableSeeder.php +++ b/database/seeders/SettingsTableSeeder.php @@ -177,11 +177,11 @@ class SettingsTableSeeder extends Seeder ], 49 => [ 'name' => 'maxsizetopostprocess', - 'value' => '100', + 'value' => '107374182400', ], 50 => [ 'name' => 'maxsizetoprocessnfo', - 'value' => '100', + 'value' => '107374182400', ], 51 => [ 'name' => 'maxxxxprocessed', @@ -189,11 +189,11 @@ class SettingsTableSeeder extends Seeder ], 52 => [ 'name' => 'minsizetopostprocess', - 'value' => '1', + 'value' => '1048576', ], 53 => [ 'name' => 'minsizetoprocessnfo', - 'value' => '1', + 'value' => '1048576', ], 54 => [ 'name' => 'mischashedretentionhours', diff --git a/resources/views/admin/groups/edit.blade.php b/resources/views/admin/groups/edit.blade.php index 24bc33a58..70ad4b8e8 100644 --- a/resources/views/admin/groups/edit.blade.php +++ b/resources/views/admin/groups/edit.blade.php @@ -112,20 +112,31 @@
-
-
- +
+
+
+ +
+
- +

- The minimum total size in bytes to make a release. If left blank, will use the site wide setting. + The minimum total size to make a release, stored as bytes. If left blank or set to 0, will use the site wide setting.

diff --git a/resources/views/admin/site/sections/additional-usenet-settings.blade.php b/resources/views/admin/site/sections/additional-usenet-settings.blade.php index d69e5371f..e07f33d8a 100644 --- a/resources/views/admin/site/sections/additional-usenet-settings.blade.php +++ b/resources/views/admin/site/sections/additional-usenet-settings.blade.php @@ -8,11 +8,16 @@ Maximum Release Size to Post Process
- - GB +
-

The maximum size in gigabytes to postprocess a release. If set to 0, then ignored.

+

The maximum size to postprocess a release, stored as bytes. If set to 0, then ignored.

@@ -20,11 +25,16 @@ Minimum Release Size to Post Process
- - MB +
-

The minimum size in megabytes to post process (additional) a release. If set to 0, then ignored.

+

The minimum size to post process (additional) a release, stored as bytes. If set to 0, then ignored.

diff --git a/resources/views/admin/site/sections/nfo-processing-settings.blade.php b/resources/views/admin/site/sections/nfo-processing-settings.blade.php index 7cf2ebf50..33bd5566c 100644 --- a/resources/views/admin/site/sections/nfo-processing-settings.blade.php +++ b/resources/views/admin/site/sections/nfo-processing-settings.blade.php @@ -31,11 +31,16 @@ Maximum Release Size to Process NFOs
- - GB +
-

The maximum size in gigabytes of a release to process it for NFOs. If set to 0, then ignored.

+

The maximum size of a release to process it for NFOs, stored as bytes. If set to 0, then ignored.

@@ -43,11 +48,16 @@ Minimum Release Size to Process NFOs
- - MB +
-

The minimum size in megabytes of a release to process it for NFOs. If set to 0, then ignored.

+

The minimum size of a release to process it for NFOs, stored as bytes. If set to 0, then ignored.

diff --git a/resources/views/admin/site/sections/usenet-settings.blade.php b/resources/views/admin/site/sections/usenet-settings.blade.php index 733d8aa02..4435303ad 100644 --- a/resources/views/admin/site/sections/usenet-settings.blade.php +++ b/resources/views/admin/site/sections/usenet-settings.blade.php @@ -70,18 +70,34 @@ - -

The minimum total size in bytes to make a release. If set to 0, then ignored.

+
+ + +
+

The minimum total size to make a release, stored as bytes. If set to 0, then ignored.

- -

The maximum total size in bytes to make a release. If set to 0, then ignored. Only deletes during release creation.

+
+ + +
+

The maximum total size to make a release, stored as bytes. If set to 0, then ignored. Only deletes during release creation.

diff --git a/tests/Feature/AdditionalProcessingOrchestratorClaimTest.php b/tests/Feature/AdditionalProcessingOrchestratorClaimTest.php index f924b64a8..a6c409052 100644 --- a/tests/Feature/AdditionalProcessingOrchestratorClaimTest.php +++ b/tests/Feature/AdditionalProcessingOrchestratorClaimTest.php @@ -115,7 +115,7 @@ class AdditionalProcessingOrchestratorClaimTest extends TestCase $output = new RecordingConsoleOutputService; $orchestrator = new AdditionalProcessingOrchestrator( - $this->makeConfig(['queryLimit' => 25, 'minSizeMB' => 0, 'maxSizeGB' => 100]), + $this->makeConfig(['queryLimit' => 25, 'minSizeBytes' => 0, 'maxSizeBytes' => 107374182400]), $processor, $tempWorkspace, $output @@ -162,7 +162,7 @@ class AdditionalProcessingOrchestratorClaimTest extends TestCase $output = new RecordingConsoleOutputService; $orchestrator = new AdditionalProcessingOrchestrator( - $this->makeConfig(['queryLimit' => 25, 'minSizeMB' => 0, 'maxSizeGB' => 100]), + $this->makeConfig(['queryLimit' => 25, 'minSizeBytes' => 0, 'maxSizeBytes' => 107374182400]), $processor, $tempWorkspace, $output diff --git a/tests/Feature/AdminGroupControllerTest.php b/tests/Feature/AdminGroupControllerTest.php index 25765e753..5655811a3 100644 --- a/tests/Feature/AdminGroupControllerTest.php +++ b/tests/Feature/AdminGroupControllerTest.php @@ -109,6 +109,92 @@ class AdminGroupControllerTest extends TestCase $this->assertSame('No group list provided.', $response->getData()['groupmsglist']); } + public function test_edit_submit_converts_min_size_unit_to_bytes(): void + { + $this->createUsenetGroupsTable(); + $groupId = DB::table('usenet_groups')->insertGetId([ + 'name' => 'alt.binaries.test', + 'description' => 'Test group', + 'active' => 1, + 'backfill' => 1, + 'minsizetoformrelease' => null, + 'minfilestoformrelease' => null, + ]); + + $request = Request::create('/admin/group-edit', 'POST', [ + 'action' => 'submit', + 'id' => (string) $groupId, + 'name' => 'alt.binaries.test', + 'description' => 'Test group', + 'backfill_target' => '1', + 'first_record' => '0', + 'last_record' => '0', + 'active' => '1', + 'backfill' => '1', + 'minsizetoformrelease' => '2', + 'minsizetoformrelease_unit' => 'GB', + 'minfilestoformrelease' => '1', + ]); + + $response = app(AdminGroupController::class)->edit($request); + + $this->assertTrue($response->isRedirect()); + $this->assertSame(2147483648, (int) DB::table('usenet_groups')->where('id', $groupId)->value('minsizetoformrelease')); + } + + public function test_edit_submit_keeps_blank_min_size_as_null(): void + { + $this->createUsenetGroupsTable(); + $groupId = DB::table('usenet_groups')->insertGetId([ + 'name' => 'alt.binaries.test', + 'description' => 'Test group', + 'active' => 1, + 'backfill' => 1, + 'minsizetoformrelease' => 1048576, + 'minfilestoformrelease' => null, + ]); + + $request = Request::create('/admin/group-edit', 'POST', [ + 'action' => 'submit', + 'id' => (string) $groupId, + 'name' => 'alt.binaries.test', + 'description' => 'Test group', + 'backfill_target' => '1', + 'first_record' => '0', + 'last_record' => '0', + 'active' => '1', + 'backfill' => '1', + 'minsizetoformrelease' => '', + 'minsizetoformrelease_unit' => 'MB', + 'minfilestoformrelease' => '1', + ]); + + app(AdminGroupController::class)->edit($request); + + $this->assertNull(DB::table('usenet_groups')->where('id', $groupId)->value('minsizetoformrelease')); + } + + public function test_edit_view_exposes_group_min_size_split_into_value_and_unit(): void + { + $this->createUsenetGroupsTable(); + $groupId = DB::table('usenet_groups')->insertGetId([ + 'name' => 'alt.binaries.test', + 'description' => 'Test group', + 'active' => 1, + 'backfill' => 1, + 'minsizetoformrelease' => 2147483648, + 'minfilestoformrelease' => null, + ]); + + $request = Request::create('/admin/group-edit', 'GET', ['id' => (string) $groupId]); + + $response = app(AdminGroupController::class)->edit($request); + + $this->assertInstanceOf(View::class, $response); + $this->assertSame(['value' => 2, 'unit' => 'GB'], $response->getData()['groupMinSize']); + $this->assertSame(['MB', 'GB'], $response->getData()['sizeUnits']); + } + private function setEnvironmentValue(string $key, ?string $value): void { if ($value === null) { @@ -148,6 +234,27 @@ class AdminGroupControllerTest extends TestCase } } + private function createUsenetGroupsTable(): void + { + if (Schema::hasTable('usenet_groups')) { + return; + } + + Schema::create('usenet_groups', function (Blueprint $table): void { + $table->increments('id'); + $table->string('name')->default(''); + $table->string('description')->default(''); + $table->unsignedBigInteger('first_record')->default(0); + $table->unsignedBigInteger('last_record')->default(0); + $table->dateTime('last_updated')->nullable(); + $table->boolean('active')->default(false); + $table->boolean('backfill')->default(false); + $table->unsignedBigInteger('minsizetoformrelease')->nullable(); + $table->unsignedBigInteger('minfilestoformrelease')->nullable(); + $table->integer('backfill_target')->default(1); + }); + } + private function seedSettings(): void { DB::table('settings')->upsert([ diff --git a/tests/Feature/AdminSiteControllerTest.php b/tests/Feature/AdminSiteControllerTest.php new file mode 100644 index 000000000..9ee2dd807 --- /dev/null +++ b/tests/Feature/AdminSiteControllerTest.php @@ -0,0 +1,210 @@ + + */ + private array $originalEnvironment = []; + + public function createApplication() + { + $this->databasePath = sys_get_temp_dir().'/nntmux-admin-site-test.sqlite'; + + $this->originalEnvironment = [ + 'APP_ENV' => getenv('APP_ENV'), + 'DB_CONNECTION' => getenv('DB_CONNECTION'), + 'DB_DATABASE' => getenv('DB_DATABASE'), + ]; + + if (file_exists($this->databasePath)) { + unlink($this->databasePath); + } + + $pdo = new PDO('sqlite:'.$this->databasePath); + $pdo->exec('CREATE TABLE settings (name VARCHAR PRIMARY KEY, value TEXT NULL)'); + $pdo->exec("INSERT INTO settings (name, value) VALUES + ('categorizeforeign', '0'), + ('catwebdl', '0'), + ('title', 'NNTmux Test'), + ('home_link', '/')"); + + $this->setEnvironmentValue('APP_ENV', 'testing'); + $this->setEnvironmentValue('DB_CONNECTION', 'sqlite'); + $this->setEnvironmentValue('DB_DATABASE', $this->databasePath); + + $app = require __DIR__.'/../../bootstrap/app.php'; + + $app->make(Kernel::class)->bootstrap(); + + return $app; + } + + protected function setUp(): void + { + parent::setUp(); + + config([ + 'database.default' => 'sqlite', + 'database.connections.sqlite.database' => $this->databasePath, + 'app.key' => 'base64:'.base64_encode(random_bytes(32)), + ]); + + DB::purge(); + DB::reconnect(); + Cache::flush(); + + $this->createSchema(); + $this->seedSettings(); + $this->resetGlobalComposerState(); + } + + protected function tearDown(): void + { + if ($this->databasePath !== '' && file_exists($this->databasePath)) { + unlink($this->databasePath); + } + + parent::tearDown(); + + foreach ($this->originalEnvironment as $key => $value) { + $this->setEnvironmentValue($key, $value === false ? null : $value); + } + } + + public function test_submit_converts_size_units_to_bytes(): void + { + $request = Request::create('/admin/site-edit', 'POST', [ + 'action' => 'submit', + 'minsizetoformrelease' => '500', + 'minsizetoformrelease_unit' => 'MB', + 'maxsizetoformrelease' => '1.5', + 'maxsizetoformrelease_unit' => 'GB', + 'minsizetopostprocess' => '2', + 'minsizetopostprocess_unit' => 'MB', + 'maxsizetopostprocess' => '50', + 'maxsizetopostprocess_unit' => 'GB', + 'minsizetoprocessnfo' => '0', + 'minsizetoprocessnfo_unit' => 'MB', + 'maxsizetoprocessnfo' => '10', + 'maxsizetoprocessnfo_unit' => 'GB', + ]); + + $response = app(AdminSiteController::class)->edit($request); + + $this->assertSame('524288000', $this->settingValue('minsizetoformrelease')); + $this->assertSame('1610612736', $this->settingValue('maxsizetoformrelease')); + $this->assertSame('2097152', $this->settingValue('minsizetopostprocess')); + $this->assertSame('53687091200', $this->settingValue('maxsizetopostprocess')); + $this->assertSame('0', $this->settingValue('minsizetoprocessnfo')); + $this->assertSame('10737418240', $this->settingValue('maxsizetoprocessnfo')); + $this->assertNull(DB::table('settings')->where('name', 'minsizetopostprocess_unit')->value('value')); + $this->assertTrue($response->isRedirect()); + } + + public function test_submit_defaults_to_mb_when_unit_is_missing(): void + { + $request = Request::create('/admin/site-edit', 'POST', [ + 'action' => 'submit', + 'minsizetopostprocess' => '3', + ]); + + app(AdminSiteController::class)->edit($request); + + $this->assertSame('3145728', $this->settingValue('minsizetopostprocess')); + } + + public function test_view_exposes_size_fields_split_into_value_and_unit(): void + { + DB::table('settings')->where('name', 'maxsizetopostprocess')->update(['value' => '53687091200']); + DB::table('settings')->where('name', 'minsizetopostprocess')->update(['value' => '524288000']); + DB::table('settings')->where('name', 'minsizetoformrelease')->update(['value' => '1572864']); + Cache::flush(); + + $request = Request::create('/admin/site-edit', 'GET'); + + $response = app(AdminSiteController::class)->edit($request); + + $this->assertInstanceOf(View::class, $response); + $sizeFields = $response->getData()['sizeFields']; + + $this->assertSame(['value' => 50, 'unit' => 'GB'], $sizeFields['maxsizetopostprocess']); + $this->assertSame(['value' => 500, 'unit' => 'MB'], $sizeFields['minsizetopostprocess']); + $this->assertSame(['value' => 1.5, 'unit' => 'MB'], $sizeFields['minsizetoformrelease']); + $this->assertSame(['value' => 0, 'unit' => 'MB'], $sizeFields['maxsizetoformrelease']); + $this->assertSame(['MB', 'GB'], $response->getData()['sizeUnits']); + } + + private function settingValue(string $name): ?string + { + $value = DB::table('settings')->where('name', $name)->value('value'); + + return $value === null ? null : (string) $value; + } + + private function setEnvironmentValue(string $key, ?string $value): void + { + if ($value === null) { + putenv($key); + unset($_ENV[$key], $_SERVER[$key]); + + return; + } + + putenv($key.'='.$value); + $_ENV[$key] = $value; + $_SERVER[$key] = $value; + } + + private function createSchema(): void + { + if (! Schema::hasTable('settings')) { + Schema::create('settings', function (Blueprint $table): void { + $table->string('name')->primary(); + $table->text('value')->nullable(); + }); + } + } + + private function seedSettings(): void + { + DB::table('settings')->upsert([ + ['name' => 'title', 'value' => 'NNTmux Test'], + ['name' => 'home_link', 'value' => '/'], + ['name' => 'categorizeforeign', 'value' => '0'], + ['name' => 'catwebdl', 'value' => '0'], + ['name' => 'minsizetoformrelease', 'value' => '0'], + ['name' => 'maxsizetoformrelease', 'value' => '0'], + ['name' => 'minsizetopostprocess', 'value' => '1048576'], + ['name' => 'maxsizetopostprocess', 'value' => '107374182400'], + ['name' => 'minsizetoprocessnfo', 'value' => '1048576'], + ['name' => 'maxsizetoprocessnfo', 'value' => '107374182400'], + ], ['name'], ['value']); + } + + private function resetGlobalComposerState(): void + { + $reflection = new ReflectionClass(GlobalDataComposer::class); + $property = $reflection->getProperty('resolvedData'); + $property->setValue(null, null); + } +} diff --git a/tests/Unit/AdditionalProcessing/CreatesProcessingConfiguration.php b/tests/Unit/AdditionalProcessing/CreatesProcessingConfiguration.php index 66a1542f3..37929e66f 100644 --- a/tests/Unit/AdditionalProcessing/CreatesProcessingConfiguration.php +++ b/tests/Unit/AdditionalProcessing/CreatesProcessingConfiguration.php @@ -31,8 +31,8 @@ trait CreatesProcessingConfiguration 'segmentsToDownload' => 2, 'maximumRarSegments' => 3, 'maximumRarPasswordChecks' => 1, - 'maxSizeGB' => 100, - 'minSizeMB' => 0, + 'maxSizeBytes' => 107374182400, + 'minSizeBytes' => 0, 'alternateNNTP' => false, 'ffmpegDuration' => 5, 'addPAR2Files' => false, diff --git a/tests/Unit/Support/SizeUnitTest.php b/tests/Unit/Support/SizeUnitTest.php new file mode 100644 index 000000000..dc59edc29 --- /dev/null +++ b/tests/Unit/Support/SizeUnitTest.php @@ -0,0 +1,83 @@ +assertSame(1048576, SizeUnit::toBytes(1, 'MB')); + $this->assertSame(5242880, SizeUnit::toBytes('5', 'MB')); + } + + public function test_to_bytes_converts_gb(): void + { + $this->assertSame(1073741824, SizeUnit::toBytes(1, 'GB')); + $this->assertSame(107374182400, SizeUnit::toBytes(100, 'GB')); + } + + public function test_to_bytes_supports_decimal_values(): void + { + $this->assertSame(1610612736, SizeUnit::toBytes(1.5, 'GB')); + $this->assertSame(1572864, SizeUnit::toBytes('1.5', 'MB')); + } + + public function test_to_bytes_maps_empty_and_non_positive_values_to_zero(): void + { + $this->assertSame(0, SizeUnit::toBytes(null, 'MB')); + $this->assertSame(0, SizeUnit::toBytes('', 'GB')); + $this->assertSame(0, SizeUnit::toBytes(0, 'GB')); + $this->assertSame(0, SizeUnit::toBytes('0', 'MB')); + $this->assertSame(0, SizeUnit::toBytes(-5, 'GB')); + $this->assertSame(0, SizeUnit::toBytes('not-a-number', 'MB')); + } + + public function test_to_bytes_rejects_unknown_units(): void + { + $this->expectException(InvalidArgumentException::class); + + SizeUnit::toBytes(1, 'TB'); + } + + public function test_from_bytes_prefers_gb_when_evenly_divisible(): void + { + $this->assertSame(['value' => 1, 'unit' => 'GB'], SizeUnit::fromBytes(1073741824)); + $this->assertSame(['value' => 100, 'unit' => 'GB'], SizeUnit::fromBytes(107374182400)); + } + + public function test_from_bytes_uses_mb_when_not_evenly_divisible_by_gb(): void + { + $this->assertSame(['value' => 1, 'unit' => 'MB'], SizeUnit::fromBytes(1048576)); + $this->assertSame(['value' => 500, 'unit' => 'MB'], SizeUnit::fromBytes(524288000)); + } + + public function test_from_bytes_rounds_to_two_decimals_for_odd_byte_counts(): void + { + $this->assertSame(['value' => 1.5, 'unit' => 'MB'], SizeUnit::fromBytes(1572864)); + $this->assertSame(['value' => 1.43, 'unit' => 'MB'], SizeUnit::fromBytes(1500000)); + } + + public function test_from_bytes_maps_empty_and_non_positive_values_to_zero_mb(): void + { + $this->assertSame(['value' => 0, 'unit' => 'MB'], SizeUnit::fromBytes(null)); + $this->assertSame(['value' => 0, 'unit' => 'MB'], SizeUnit::fromBytes('')); + $this->assertSame(['value' => 0, 'unit' => 'MB'], SizeUnit::fromBytes(0)); + $this->assertSame(['value' => 0, 'unit' => 'MB'], SizeUnit::fromBytes(-1)); + } + + public function test_round_trip_preserves_values(): void + { + foreach ([['2', 'GB'], ['250', 'MB'], ['1.5', 'GB'], ['0.5', 'MB']] as [$value, $unit]) { + $bytes = SizeUnit::toBytes($value, $unit); + $display = SizeUnit::fromBytes($bytes); + + $this->assertSame($bytes, SizeUnit::toBytes($display['value'], $display['unit'])); + } + } +}