From a7fed82e441a7d34cf2bfdde9855458dc6f21cac Mon Sep 17 00:00:00 2001 From: Andrii Nikitin Date: Mon, 13 Jul 2026 10:35:28 +0200 Subject: [PATCH 1/3] fix comparing dates The problem happened because of accidentally choosing a wrong line to put the format conversion --- lib/MirrorCache/WebAPI/Plugin/Dir.pm | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/MirrorCache/WebAPI/Plugin/Dir.pm b/lib/MirrorCache/WebAPI/Plugin/Dir.pm index 54909c88..89f58923 100644 --- a/lib/MirrorCache/WebAPI/Plugin/Dir.pm +++ b/lib/MirrorCache/WebAPI/Plugin/Dir.pm @@ -648,6 +648,7 @@ sub _render_dir_from_db { my $basename = $child->{name}; my $size = $child->{size}; my $mtime = $child->{mtime}; + $max_mtime = $mtime unless $max_mtime && $mtime && ($max_mtime > $mtime); my $desc = $folderDesc{$c->mcbranding}{$dir}{$basename}; if ($json) { push @files, { @@ -663,8 +664,6 @@ sub _render_dir_from_db { my $encoded = Encode::decode_utf8( './' . $basename ); my $mime_type = $dm->mime || 'text/plain'; - $max_mtime = $mtime unless $max_mtime && $mtime && ($max_mtime > $mtime); - push @files, { url => $encoded, name => $basename, From 5fd004e1f75ec4fb2c93277b3eb3fe832daf3741 Mon Sep 17 00:00:00 2001 From: Andrii Nikitin Date: Mon, 13 Jul 2026 11:02:59 +0200 Subject: [PATCH 2/3] Fix infinite loop and nested redirects for symlinked folder loops Prevent `folder_sync` and `folder_pkg_sync` tasks from scanning recursively under non-canonical symlink paths (such as `Changes -> .` loops). Previously, when scanning a non-canonical path `$path` whose `$realpath` differed from it, the tasks would register the redirect but continue recursively scanning that directory relative to `$path`. For directory loops, this resulted in an infinite loop that populated the redirect table with repeating suffixes like `/Changes/Changes/Changes`. Now, if `$realpath ne $path`, the tasks register the redirect, enqueue a new task for the canonical `$realpath` to ensure it is synchronized, schedule a mirror scan on the symlink's `Folder` record, and exit immediately. This prevents redundant scans, infinite recursive jobs, and database pollution. Also includes a new integration test (`15-local-symlink-4.sh`) verifying that directory loop symlinks do not generate nested recursive redirect records. --- lib/MirrorCache/Task/FolderPkgSync.pm | 2 ++ lib/MirrorCache/Task/FolderSync.pm | 12 ++++++++++++ t/environ/15-local-symlink-4.sh | 27 +++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100755 t/environ/15-local-symlink-4.sh diff --git a/lib/MirrorCache/Task/FolderPkgSync.pm b/lib/MirrorCache/Task/FolderPkgSync.pm index be88df73..2fbb76f1 100644 --- a/lib/MirrorCache/Task/FolderPkgSync.pm +++ b/lib/MirrorCache/Task/FolderPkgSync.pm @@ -44,6 +44,8 @@ sub _sync { $job->note($realpath => 1); $schema->resultset('Folder')->add_redirect($path, $realpath); + $minion->enqueue('folder_pkg_sync' => [$realpath] => { queue => $job->info->{queue} }); + return $job->finish("redirected to $realpath"); } return $job->finish('not dir') unless ($root->is_dir($realpath)); diff --git a/lib/MirrorCache/Task/FolderSync.pm b/lib/MirrorCache/Task/FolderSync.pm index 4a6d166a..e05bf5ae 100644 --- a/lib/MirrorCache/Task/FolderSync.pm +++ b/lib/MirrorCache/Task/FolderSync.pm @@ -49,6 +49,18 @@ sub _sync { $job->note($realpath => 1); $schema->resultset('Folder')->add_redirect($path, $realpath); + $app->backstage->enqueue('folder_sync', $realpath); + + my $otherFolder = $schema->resultset('Folder')->find({path => $path}); + if ($otherFolder) { + $otherFolder->update({ + sync_last => \'CURRENT_TIMESTAMP(3)', + sync_requested => \'coalesce(sync_requested, CURRENT_TIMESTAMP(3))', + sync_scheduled => \'coalesce(sync_scheduled, CURRENT_TIMESTAMP(3))', + scan_requested => \'CURRENT_TIMESTAMP(3)' + }); + } + return $job->finish("redirected to $realpath"); } my $proj = $schema->resultset('Rollout')->project_for_folder($path); my ($proj_type, $proj_prefix); diff --git a/t/environ/15-local-symlink-4.sh b/t/environ/15-local-symlink-4.sh new file mode 100755 index 00000000..2aa17020 --- /dev/null +++ b/t/environ/15-local-symlink-4.sh @@ -0,0 +1,27 @@ +#!lib/test-in-container-environ.sh +set -ex + +mc=$(environ mc $(pwd)) + +$mc/start +$mc/status + +# Create folder1 and a symlink pointing to '.' inside it +mkdir -p $mc/dt/folder1 +echo "hello" > $mc/dt/folder1/file1.txt +( +cd $mc/dt/folder1 +ln -s . Changes +) + +# Sync folder1 +$mc/backstage/job -e folder_sync -a '["/folder1", 1]' +$mc/backstage/shoot + +# Check if a redirect was inserted for /folder1/Changes -> /folder1 +$mc/sql_test 1 == "select count(*) from redirect where pathfrom = '/folder1/Changes' and pathto = '/folder1'" + +# Check that NO nested redirects like /folder1/Changes/Changes exist +$mc/sql_test 0 == "select count(*) from redirect where pathfrom like '%Changes/Changes%'" + +echo success From c37cde8cd3855cffba546c09a4bbbb8e903ec021 Mon Sep 17 00:00:00 2001 From: Andrii Nikitin Date: Mon, 13 Jul 2026 11:04:58 +0200 Subject: [PATCH 3/3] fixup! fix comparing dates --- t/Directory/02-dir-plugin.t | 111 ++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 t/Directory/02-dir-plugin.t diff --git a/t/Directory/02-dir-plugin.t b/t/Directory/02-dir-plugin.t new file mode 100644 index 00000000..4e99dd27 --- /dev/null +++ b/t/Directory/02-dir-plugin.t @@ -0,0 +1,111 @@ +use Mojo::Base -strict; + +use Test::More; +use POSIX; +use MirrorCache::WebAPI::Plugin::Dir; + +# Define methods for Mock classes +sub MockController::schema { shift->{schema} } +sub MockController::mc { shift->{mc} } +sub MockController::res { shift->{res} } +sub MockController::render { + my $self = shift; + if (scalar(@_) % 2 == 1) { + my $template = shift; + $self->{rendered_template} = $template; + } + $self->{rendered} = { @_ }; +} +sub MockController::mcbranding { 'suse' } + +sub MockSchema::resultset { + my ($self, $type) = @_; + return bless {}, 'MockRS'; +} + +sub MockRS::find_with_regex { + return { + 1 => { + name => 'file1.txt', + size => 1024, + mtime => 1661211960, # 22-Aug-2022 23:46:00 + }, + 2 => { + name => 'file2.txt', + size => 2048, + mtime => 1661212020, # 22-Aug-2022 23:47:00 + } + }; +} + +sub MockResponse::headers { shift->{headers} } + +sub MockHeaders::etag { + my ($self, $etag) = @_; + $self->{etag} = $etag if defined $etag; + return $self->{etag}; +} +sub MockHeaders::add { + my ($self, $key, $val) = @_; + $self->{$key} = $val; +} + +# Mock the Controller +my $c = bless { + mc => bless({ + root => bless({ + rooturl => 'http://example.com' + }, 'MockRoot') + }, 'MockMC'), + schema => bless({}, 'MockSchema'), + res => bless({ + headers => bless({}, 'MockHeaders') + }, 'MockResponse'), + rendered => undef, +}, 'MockController'; + +# Also need MockRoot methods +sub MockRoot::rooturl { shift->{rooturl} } + +# Mock the Datamodule +my $dm = bless { + c => $c, + json => 0, + browse => 0, + glob_regex => undef, + regex => undef, + folder_sync_last => undef, + folder_sync_requested => undef, + route => 'download', + mime => 'text/plain', + jsontable => 0, +}, 'MockDM'; + +sub MockDM::c { shift->{c} } +sub MockDM::json { shift->{json} } +sub MockDM::browse { shift->{browse} } +sub MockDM::glob_regex { shift->{glob_regex} } +sub MockDM::regex { shift->{regex} } +sub MockDM::folder_sync_last { shift->{folder_sync_last} } +sub MockDM::folder_sync_requested { shift->{folder_sync_requested} } +sub MockDM::route { shift->{route} } +sub MockDM::mime { shift->{mime} } +sub MockDM::jsontable { shift->{jsontable} } + +subtest 'render_dir_from_db max_mtime and warning-free comparison' => sub { + my @warnings; + { + local $SIG{__WARN__} = sub { push @warnings, shift }; + MirrorCache::WebAPI::Plugin::Dir::_render_dir_from_db($dm, 1, '/some/dir'); + } + + is_deeply \@warnings, [], "No warnings generated during rendering"; + + # Let's verify that the calculated ETag is based on the maximum numeric mtime (1661212020 = 0x630416F4) + # The ETag format is: sprintf('%X', scalar(@files)) . '-' . sprintf('%X', $max_mtime) + # 2 files, so: 2-630416F4 + my $expected_etag = sprintf('%X', 2) . '-' . sprintf('%X', 1661212020); + is $c->res->headers->etag, $expected_etag, "ETag matches expected value based on numeric mtime"; +}; + +done_testing();