Diff -- /var/www/vhosts/3dshawn.com/site1/sync.cgi
Diff
/var/www/vhosts/3dshawn.com/site1/sync.cgi
added on local at 2026-07-12 00:02:16
Added
+171
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 13830ac21919
to 13830ac21919
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.| 1 | #!/usr/bin/perl | |
| 2 | #====================================================================== | |
| 3 | # DriftSense -- /sync.cgi | |
| 4 | # | |
| 5 | # Cross-site "make N sites match this file" workflow. Given a source | |
| 6 | # file_changes_id, find every other monitor whose latest capture of a | |
| 7 | # same-basename file has a DIFFERENT SHA. Multi-select the targets + | |
| 8 | # confirm -> DriftSense writes the source blob content to each target's | |
| 9 | # file path in one pass, using MODS::Restore under the hood (which | |
| 10 | # handles the local / ssh_agent branch + backup + verify per target). | |
| 11 | # | |
| 12 | # GET /sync.cgi?id=N -> preview + multi-select | |
| 13 | # POST id=N confirm=1 targets=N,N -> execute the fanned-out restores | |
| 14 | #====================================================================== | |
| 15 | use strict; | |
| 16 | use warnings; | |
| 17 | use CGI (); | |
| 18 | use Compress::Zlib qw(uncompress); | |
| 19 | use MODS::Config; | |
| 20 | use MODS::DBConnect; | |
| 21 | use MODS::Template; | |
| 22 | use MODS::PageWrapper; | |
| 23 | use MODS::Restore; | |
| 24 | ||
| 25 | my $cgi = CGI->new; | |
| 26 | my $db = MODS::DBConnect->new; | |
| 27 | my $tpl = MODS::Template->new; | |
| 28 | $|=1; | |
| 29 | ||
| 30 | my $dbh = $db->db_connect or die "DB connect failed\n"; | |
| 31 | ||
| 32 | my $method = $ENV{REQUEST_METHOD} || 'GET'; | |
| 33 | my $id = int($cgi->param('id') || 0); | |
| 34 | ||
| 35 | # ---- Source row ---------------------------------------------------- | |
| 36 | my $src = $id ? $db->db_readwrite($dbh, qq~ | |
| 37 | SELECT fc.file_changes_id AS id, fc.file_name, fc.blob_sha, fc.status, | |
| 38 | fc.date_time, fc.file_monitor_list_id, | |
| 39 | m.scan_name AS src_scan_name, | |
| 40 | s.server_name AS src_server_name | |
| 41 | FROM FILE_CHANGES fc | |
| 42 | LEFT JOIN FILE_MONITOR_SETTINGS m ON m.file_monitor_list_id = fc.file_monitor_list_id | |
| 43 | LEFT JOIN SERVERS s ON s.server_id = fc.server_id | |
| 44 | WHERE fc.file_changes_id = $id LIMIT 1 | |
| 45 | ~, __FILE__, __LINE__) : undef; | |
| 46 | ||
| 47 | unless ($src && $src->{blob_sha}) { | |
| 48 | print "Content-Type: text/html; charset=utf-8\n\n"; | |
| 49 | my @body = $tpl->template('sync.html', { | |
| 50 | has_error => 1, | |
| 51 | error_msg => "Change #$id not found or has no blob.", | |
| 52 | }); | |
| 53 | MODS::PageWrapper->new->wrapper( | |
| 54 | page_title => 'Sync', page_key => '', | |
| 55 | body_html => join('', @body), userinfo => { display_name => 'Operator' }, | |
| 56 | ); | |
| 57 | exit; | |
| 58 | } | |
| 59 | ||
| 60 | # ---- Find candidate targets ---------------------------------------- | |
| 61 | my $basename = $src->{file_name}; $basename =~ s!.*/!!; | |
| 62 | my $q_base_like = $dbh->quote('%/' . $basename); | |
| 63 | my @targets = $db->db_readwrite_multiple($dbh, qq~ | |
| 64 | SELECT fc.file_changes_id AS latest_id, | |
| 65 | fc.file_name, | |
| 66 | fc.blob_sha, | |
| 67 | fc.status, | |
| 68 | fc.file_monitor_list_id AS mid, | |
| 69 | fc.server_id, | |
| 70 | m.scan_name, | |
| 71 | s.server_name, | |
| 72 | s.kind AS server_kind, | |
| 73 | s.ssh_host, s.ssh_user, s.ssh_port, | |
| 74 | s.ssh_key_path, s.ssh_options, s.ssh_key_blob | |
| 75 | FROM FILE_CHANGES fc | |
| 76 | JOIN ( | |
| 77 | SELECT file_monitor_list_id, file_name, MAX(file_changes_id) AS max_id | |
| 78 | FROM FILE_CHANGES | |
| 79 | WHERE file_name LIKE $q_base_like | |
| 80 | GROUP BY file_monitor_list_id, file_name | |
| 81 | ) latest ON latest.max_id = fc.file_changes_id | |
| 82 | LEFT JOIN FILE_MONITOR_SETTINGS m ON m.file_monitor_list_id = fc.file_monitor_list_id | |
| 83 | LEFT JOIN SERVERS s ON s.server_id = fc.server_id | |
| 84 | WHERE fc.file_monitor_list_id != $src->{file_monitor_list_id} | |
| 85 | AND fc.blob_sha IS NOT NULL | |
| 86 | AND fc.blob_sha != @{[ $dbh->quote($src->{blob_sha}) ]} | |
| 87 | AND fc.status != 'deleted' | |
| 88 | ORDER BY m.scan_name ASC | |
| 89 | ~, __FILE__, __LINE__); | |
| 90 | ||
| 91 | # ---- POST: execute the fanned-out writes --------------------------- | |
| 92 | if ($method eq 'POST' && $cgi->param('confirm')) { | |
| 93 | my %wanted = map { int($_) => 1 } $cgi->param('targets'); | |
| 94 | # Fetch the source blob content ONCE | |
| 95 | my $sth = $dbh->prepare("SELECT content_gz FROM BLOB_STORE WHERE blob_sha = ?"); | |
| 96 | $sth->execute($src->{blob_sha}); | |
| 97 | my $blob_row = $sth->fetchrow_arrayref; | |
| 98 | $sth->finish; | |
| 99 | my $content = ($blob_row && $blob_row->[0]) ? eval { uncompress($blob_row->[0]) } : undef; | |
| 100 | unless (defined $content) { | |
| 101 | $db->db_disconnect($dbh); | |
| 102 | print "Status: 302 Found\nLocation: /sync.cgi?id=$id&fail=99\n\n"; | |
| 103 | exit; | |
| 104 | } | |
| 105 | ||
| 106 | my ($ok_ct, $fail_ct) = (0, 0); | |
| 107 | foreach my $t (@targets) { | |
| 108 | next unless $wanted{$t->{latest_id}}; | |
| 109 | my ($ok, $msg, $backup) = MODS::Restore::write_blob( | |
| 110 | server_row => $t, | |
| 111 | target => $t->{file_name}, | |
| 112 | blob_sha => $src->{blob_sha}, | |
| 113 | content => $content, | |
| 114 | ); | |
| 115 | # Log a RESTORE_LOG row per target | |
| 116 | my $q_file = $dbh->quote($t->{file_name}); | |
| 117 | my $q_sha = $dbh->quote($src->{blob_sha}); | |
| 118 | my $q_bak = $dbh->quote($backup // ''); | |
| 119 | my $q_stat = $ok ? "'success'" : "'failed'"; | |
| 120 | my $q_err = $dbh->quote($ok ? '' : ($msg // '')); | |
| 121 | my $q_by = $dbh->quote('sync-from-' . $src->{id}); | |
| 122 | $db->db_readwrite($dbh, qq~ | |
| 123 | INSERT INTO RESTORE_LOG | |
| 124 | (source_change_id, server_id, target_file, source_blob_sha, | |
| 125 | backup_path, status, error_message, restored_by) | |
| 126 | VALUES | |
| 127 | ($src->{id}, $t->{server_id}, $q_file, $q_sha, | |
| 128 | $q_bak, $q_stat, $q_err, $q_by) | |
| 129 | ~, __FILE__, __LINE__); | |
| 130 | $ok ? $ok_ct++ : $fail_ct++; | |
| 131 | } | |
| 132 | $db->db_disconnect($dbh); | |
| 133 | print "Status: 302 Found\nLocation: /sync.cgi?id=$id&ok=$ok_ct&fail=$fail_ct\n\n"; | |
| 134 | exit; | |
| 135 | } | |
| 136 | ||
| 137 | # ---- GET: preview -------------------------------------------------- | |
| 138 | foreach my $t (@targets) { | |
| 139 | $t->{sha_short} = substr($t->{blob_sha} // '', 0, 12); | |
| 140 | $t->{file_short} = length($t->{file_name} // '') > 60 | |
| 141 | ? '...' . substr($t->{file_name}, -57) : $t->{file_name}; | |
| 142 | $t->{diff_url} = "/diff.cgi?kind=file&id=$t->{latest_id}"; | |
| 143 | $t->{kind_pill} = ($t->{server_kind} // '') eq 'ssh_agent' ? 'pill-info' : 'pill-mute'; | |
| 144 | } | |
| 145 | my $ok_ct = int($cgi->param('ok') || 0); | |
| 146 | my $fail_ct = int($cgi->param('fail') || 0); | |
| 147 | ||
| 148 | $db->db_disconnect($dbh); | |
| 149 | ||
| 150 | my $tvars = { | |
| 151 | src_id => $src->{id}, | |
| 152 | src_file => $src->{file_name}, | |
| 153 | src_scan => $src->{src_scan_name}, | |
| 154 | src_server => $src->{src_server_name}, | |
| 155 | src_sha => substr($src->{blob_sha}, 0, 12), | |
| 156 | src_sha_full => $src->{blob_sha}, | |
| 157 | basename => $basename, | |
| 158 | targets => \@targets, | |
| 159 | has_targets => scalar(@targets) ? 1 : 0, | |
| 160 | total_targets => scalar @targets, | |
| 161 | ok_ct => $ok_ct, | |
| 162 | fail_ct => $fail_ct, | |
| 163 | has_result => ($ok_ct + $fail_ct) ? 1 : 0, | |
| 164 | src_diff_url => "/diff.cgi?kind=file&id=$src->{id}", | |
| 165 | }; | |
| 166 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n"; | |
| 167 | my @body = $tpl->template('sync.html', $tvars); | |
| 168 | MODS::PageWrapper->new->wrapper( | |
| 169 | page_title => 'Sync', page_key => '', | |
| 170 | body_html => join('', @body), userinfo => { display_name => 'Operator' }, | |
| 171 | ); |