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

O Operator
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
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 -- /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#======================================================================
13use strict;
14use warnings;
15use CGI ();
16use Compress::Zlib qw(uncompress);
17use Digest::SHA qw(sha256_hex);
18use MODS::Config;
19use MODS::DBConnect;
20use MODS::Template;
21use MODS::PageWrapper;
22use MODS::Restore;
23
24my $cgi = CGI->new;
25my $db = MODS::DBConnect->new;
26my $tpl = MODS::Template->new;
27$|=1;
28
29my $dbh = $db->db_connect or die "DB connect failed\n";
30
31my $rid = int($cgi->param('release_id') || 0);
32my $method = $ENV{REQUEST_METHOD} || 'GET';
33
34my $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
41unless ($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 ------------------------------------
55my @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 -------------------------------
70if ($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.
116foreach 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
137my $drift_ct = scalar grep { !$_->{matches} } @pins;
138my $in_sync_ct = scalar @pins - $drift_ct;
139my $ok_ct = int($cgi->param('ok') || 0);
140my $fail_ct = int($cgi->param('fail') || 0);
141my $skipped_ct = int($cgi->param('skipped') || 0);
142
143$db->db_disconnect($dbh);
144
145my $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};
162print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
163my @body = $tpl->template('restore_release.html', $tvars);
164MODS::PageWrapper->new->wrapper(
165 page_title => 'Restore release', page_key => '',
166 body_html => join('', @body), userinfo => { display_name => 'Operator' },
167);