Diff -- /var/www/vhosts/3dshawn.com/site1/restore_release.cgi
Diff
/var/www/vhosts/3dshawn.com/site1/restore_release.cgi
added on local at 2026-07-12 00:19:38
Added
+167
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 6d93f2d3836d
to 6d93f2d3836d
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 -- /restore_release.cgi | |
| 4 | # | |
| 5 | # Restore every pinned blob in a Named Release back to its original | |
| 6 | # file path. One-click point-in-time recovery. | |
| 7 | # | |
| 8 | # GET ?release_id=N -> preview: table of every pinned file, | |
| 9 | # current-vs-pinned SHA comparison | |
| 10 | # POST ?release_id=N confirm=1 -> fan out MODS::Restore::write_blob | |
| 11 | # for each pin, log to RESTORE_LOG. | |
| 12 | #====================================================================== | |
| 13 | use strict; | |
| 14 | use warnings; | |
| 15 | use CGI (); | |
| 16 | use Compress::Zlib qw(uncompress); | |
| 17 | use Digest::SHA qw(sha256_hex); | |
| 18 | use MODS::Config; | |
| 19 | use MODS::DBConnect; | |
| 20 | use MODS::Template; | |
| 21 | use MODS::PageWrapper; | |
| 22 | use MODS::Restore; | |
| 23 | ||
| 24 | my $cgi = CGI->new; | |
| 25 | my $db = MODS::DBConnect->new; | |
| 26 | my $tpl = MODS::Template->new; | |
| 27 | $|=1; | |
| 28 | ||
| 29 | my $dbh = $db->db_connect or die "DB connect failed\n"; | |
| 30 | ||
| 31 | my $rid = int($cgi->param('release_id') || 0); | |
| 32 | my $method = $ENV{REQUEST_METHOD} || 'GET'; | |
| 33 | ||
| 34 | my $release = $rid ? $db->db_readwrite($dbh, qq~ | |
| 35 | SELECT named_release_id, name, description, released_by, | |
| 36 | DATE_FORMAT(released_at, '%b %d, %Y %H:%i') AS released_h, | |
| 37 | UNIX_TIMESTAMP(released_at) AS released_epoch | |
| 38 | FROM NAMED_RELEASES WHERE named_release_id = $rid LIMIT 1 | |
| 39 | ~, __FILE__, __LINE__) : undef; | |
| 40 | ||
| 41 | unless ($release && $release->{named_release_id}) { | |
| 42 | print "Content-Type: text/html; charset=utf-8\n\n"; | |
| 43 | my @body = $tpl->template('restore_release.html', { | |
| 44 | has_error => 1, | |
| 45 | error_msg => "Named release #$rid not found.", | |
| 46 | }); | |
| 47 | MODS::PageWrapper->new->wrapper( | |
| 48 | page_title => 'Restore release', page_key => '', | |
| 49 | body_html => join('', @body), userinfo => { display_name => 'Operator' }, | |
| 50 | ); | |
| 51 | exit; | |
| 52 | } | |
| 53 | ||
| 54 | # ---- Load pins for this release ------------------------------------ | |
| 55 | my @pins = $db->db_readwrite_multiple($dbh, qq~ | |
| 56 | SELECT p.pin_id, p.file_changes_id, p.file_name, p.blob_sha AS pinned_sha, | |
| 57 | fc.server_id, | |
| 58 | s.kind AS server_kind, | |
| 59 | s.server_name, | |
| 60 | s.ssh_host, s.ssh_user, s.ssh_port, | |
| 61 | s.ssh_key_path, s.ssh_options, s.ssh_key_blob | |
| 62 | FROM NAMED_RELEASE_PINS p | |
| 63 | LEFT JOIN FILE_CHANGES fc ON fc.file_changes_id = p.file_changes_id | |
| 64 | LEFT JOIN SERVERS s ON s.server_id = fc.server_id | |
| 65 | WHERE p.named_release_id = $rid | |
| 66 | ORDER BY p.pin_id ASC | |
| 67 | ~, __FILE__, __LINE__); | |
| 68 | ||
| 69 | # ---- POST: execute the batch restore ------------------------------- | |
| 70 | if ($method eq 'POST' && $cgi->param('confirm')) { | |
| 71 | my ($ok, $fail, $skipped) = (0, 0, 0); | |
| 72 | foreach my $pin (@pins) { | |
| 73 | next unless $pin->{pinned_sha} && $pin->{file_name}; | |
| 74 | # Fetch pinned blob content | |
| 75 | my $sth = $dbh->prepare("SELECT content_gz FROM BLOB_STORE WHERE blob_sha = ?"); | |
| 76 | $sth->execute($pin->{pinned_sha}); | |
| 77 | my $r = $sth->fetchrow_arrayref; | |
| 78 | $sth->finish; | |
| 79 | my $content = ($r && $r->[0]) ? eval { uncompress($r->[0]) } : undef; | |
| 80 | unless (defined $content) { $skipped++; next; } | |
| 81 | ||
| 82 | my ($success, $msg, $backup) = MODS::Restore::write_blob( | |
| 83 | server_row => $pin, | |
| 84 | target => $pin->{file_name}, | |
| 85 | blob_sha => $pin->{pinned_sha}, | |
| 86 | content => $content, | |
| 87 | ); | |
| 88 | ||
| 89 | # Log to RESTORE_LOG | |
| 90 | my $q_file = $dbh->quote($pin->{file_name}); | |
| 91 | my $q_sha = $dbh->quote($pin->{pinned_sha}); | |
| 92 | my $q_bak = $dbh->quote($backup // ''); | |
| 93 | my $q_stat = $success ? "'success'" : "'failed'"; | |
| 94 | my $q_err = $dbh->quote($success ? '' : ($msg // '')); | |
| 95 | my $q_by = $dbh->quote("restore-release-$rid"); | |
| 96 | my $sid = int($pin->{server_id} || 0); | |
| 97 | my $cid = int($pin->{file_changes_id} || 0); | |
| 98 | $db->db_readwrite($dbh, qq~ | |
| 99 | INSERT INTO RESTORE_LOG | |
| 100 | (source_change_id, server_id, target_file, source_blob_sha, | |
| 101 | backup_path, status, error_message, restored_by) | |
| 102 | VALUES | |
| 103 | ($cid, $sid, $q_file, $q_sha, | |
| 104 | $q_bak, $q_stat, $q_err, $q_by) | |
| 105 | ~, __FILE__, __LINE__); | |
| 106 | $success ? $ok++ : $fail++; | |
| 107 | } | |
| 108 | $db->db_disconnect($dbh); | |
| 109 | print "Status: 302 Found\nLocation: /restore_release.cgi?release_id=$rid&ok=$ok&fail=$fail&skipped=$skipped\n\n"; | |
| 110 | exit; | |
| 111 | } | |
| 112 | ||
| 113 | # ---- GET: preview page --------------------------------------------- | |
| 114 | # For each pin, get the current-latest SHA of that file on that monitor | |
| 115 | # so we can show current-vs-pinned comparison inline. | |
| 116 | foreach my $pin (@pins) { | |
| 117 | my $q_file = $dbh->quote($pin->{file_name} // ''); | |
| 118 | my $sid = int($pin->{server_id} || 0); | |
| 119 | my $cur = $db->db_readwrite($dbh, qq~ | |
| 120 | SELECT blob_sha FROM FILE_CHANGES | |
| 121 | WHERE server_id = $sid AND file_name = $q_file | |
| 122 | ORDER BY file_changes_id DESC LIMIT 1 | |
| 123 | ~, __FILE__, __LINE__); | |
| 124 | $pin->{current_sha} = $cur ? $cur->{blob_sha} : ''; | |
| 125 | $pin->{pinned_short} = substr($pin->{pinned_sha} // '', 0, 12); | |
| 126 | $pin->{current_short} = substr($pin->{current_sha} // '', 0, 12); | |
| 127 | $pin->{matches} = ($pin->{current_sha} && $pin->{current_sha} eq $pin->{pinned_sha}) ? 1 : 0; | |
| 128 | $pin->{status_pill} = $pin->{matches} ? 'pill-ok' : 'pill-warn'; | |
| 129 | $pin->{status_lbl} = $pin->{matches} ? 'in sync' : 'drifted'; | |
| 130 | $pin->{file_short} = length($pin->{file_name} // '') > 65 | |
| 131 | ? '...' . substr($pin->{file_name}, -62) : $pin->{file_name}; | |
| 132 | $pin->{server_name} //= 'local'; | |
| 133 | $pin->{diff_url} = "/diff.cgi?kind=file&id=" . ($pin->{file_changes_id} || 0); | |
| 134 | } | |
| 135 | ||
| 136 | # Aggregate | |
| 137 | my $drift_ct = scalar grep { !$_->{matches} } @pins; | |
| 138 | my $in_sync_ct = scalar @pins - $drift_ct; | |
| 139 | my $ok_ct = int($cgi->param('ok') || 0); | |
| 140 | my $fail_ct = int($cgi->param('fail') || 0); | |
| 141 | my $skipped_ct = int($cgi->param('skipped') || 0); | |
| 142 | ||
| 143 | $db->db_disconnect($dbh); | |
| 144 | ||
| 145 | my $tvars = { | |
| 146 | release_id => $rid, | |
| 147 | release_name => $release->{name}, | |
| 148 | release_desc => $release->{description}, | |
| 149 | released_by => $release->{released_by}, | |
| 150 | released_h => $release->{released_h}, | |
| 151 | released_epoch => $release->{released_epoch}, | |
| 152 | pins => \@pins, | |
| 153 | has_pins => scalar(@pins) ? 1 : 0, | |
| 154 | total_pins => scalar @pins, | |
| 155 | drift_ct => $drift_ct, | |
| 156 | in_sync_ct => $in_sync_ct, | |
| 157 | ok_ct => $ok_ct, | |
| 158 | fail_ct => $fail_ct, | |
| 159 | skipped_ct => $skipped_ct, | |
| 160 | has_result => ($ok_ct + $fail_ct + $skipped_ct) ? 1 : 0, | |
| 161 | }; | |
| 162 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n"; | |
| 163 | my @body = $tpl->template('restore_release.html', $tvars); | |
| 164 | MODS::PageWrapper->new->wrapper( | |
| 165 | page_title => 'Restore release', page_key => '', | |
| 166 | body_html => join('', @body), userinfo => { display_name => 'Operator' }, | |
| 167 | ); |