Diff -- /var/www/vhosts/3dshawn.com/site1/sync.cgi

O Operator
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
Restore this content →
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#======================================================================
15use strict;
16use warnings;
17use CGI ();
18use Compress::Zlib qw(uncompress);
19use MODS::Config;
20use MODS::DBConnect;
21use MODS::Template;
22use MODS::PageWrapper;
23use MODS::Restore;
24
25my $cgi = CGI->new;
26my $db = MODS::DBConnect->new;
27my $tpl = MODS::Template->new;
28$|=1;
29
30my $dbh = $db->db_connect or die "DB connect failed\n";
31
32my $method = $ENV{REQUEST_METHOD} || 'GET';
33my $id = int($cgi->param('id') || 0);
34
35# ---- Source row ----------------------------------------------------
36my $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
47unless ($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 ----------------------------------------
61my $basename = $src->{file_name}; $basename =~ s!.*/!!;
62my $q_base_like = $dbh->quote('%/' . $basename);
63my @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 ---------------------------
92if ($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 --------------------------------------------------
138foreach 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}
145my $ok_ct = int($cgi->param('ok') || 0);
146my $fail_ct = int($cgi->param('fail') || 0);
147
148$db->db_disconnect($dbh);
149
150my $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};
166print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
167my @body = $tpl->template('sync.html', $tvars);
168MODS::PageWrapper->new->wrapper(
169 page_title => 'Sync', page_key => '',
170 body_html => join('', @body), userinfo => { display_name => 'Operator' },
171);