Restore

O Operator
Restore

Restore file to captured state

Every captured change carries its SHA-256-addressed content in BLOB_STORE. Restoring writes that content back to the original path — current file gets backed up to <path>.drift_restore_backup_<epoch> before overwrite.

What will change
Preview -- nothing has been written yet.
Target file/var/www/vhosts/3dshawn.com/site1/named_releases.cgi
SiteDriftSense self-monitor on local
Kindlocal
Captured at2026-07-10 18:57:30
Captured SHAf66164415344aa3781705aef75160b1868140ceb883d0778e7afffe63ebb4e3f
Current state on disk
Live check just now. If SHAs match, the restore is a no-op.
File presentyes
Size8588 bytes
Current SHA3b5abff71074
Same as captured?no -- restore will change it
What restore will change — live diff
Left column shows current on-disk content; right shows what restore will write. +12 additions, -157 deletions, 39 unchanged context lines.
11#!/usr/bin/perl
2#======================================================================
3# DriftSense -- /named_releases.cgi
4#
5# List + create named releases. Each release is a bookmark that pins
6# every blob referenced by FILE_CHANGES rows in a chosen scope. A
7# release can be portfolio-wide (scope_monitor_id NULL) or scoped to
8# a single site (scope_monitor_id set). Pinned blobs are exempt from
9# auto-purge.
10#======================================================================
112use strict;
123use warnings;
134use CGI ();
145use MODS::Config; use MODS::DBConnect; use MODS::Template; use MODS::PageWrapper;
15use MODS::Webhook;
16use JSON::PP ();
176my $cgi = CGI->new; my $db = MODS::DBConnect->new; my $tpl = MODS::Template->new;
187$|=1;
198
209my $dbh = $db->db_connect or die "DB connect failed\n";
2110
22# ---- POST: create ---------------------------------------------------
11# POST: create new named release
2312if (($ENV{REQUEST_METHOD} || '') eq 'POST' && $cgi->param('save')) {
2413 my $name = $cgi->param('name') || '';
2514 my $desc = $cgi->param('description') || '';
2615 my $by = $cgi->param('released_by') || 'Operator';
27 my $scope_mid = int($cgi->param('scope_monitor_id') || 0);
28 my $scope_sid = int($cgi->param('scope_server_id') || 0);
29 my $dkind = $cgi->param('alert_delivery_kind') || 'none';
30 $dkind = 'none' unless $dkind =~ /^(none|generic_webhook|slack|discord)$/;
31 my $durl = $cgi->param('alert_delivery_url') || '';
32 my $tier = $cgi->param('tier') || 'draft';
33 $tier = 'draft' unless $tier =~ /^(draft|working|stable)$/;
34 my $vlabel = $cgi->param('version_label') || '';
35
3616 if (length $name) {
3717 my $q_name = $dbh->quote($name);
3818 my $q_desc = $dbh->quote($desc);
3919 my $q_by = $dbh->quote($by);
40 my $q_dk = $dbh->quote($dkind);
41 my $q_du = $dbh->quote($durl);
42 my $q_tier = $dbh->quote($tier);
43 my $q_vl = $dbh->quote($vlabel);
44 my $mid_expr = $scope_mid ? $scope_mid : 'NULL';
45 my $sid_expr = $scope_sid ? $scope_sid : 'NULL';
4620 $db->db_readwrite($dbh, qq~
47 INSERT INTO NAMED_RELEASES
48 (name, description, released_at, released_by, is_locked, created_at,
49 scope_monitor_id, scope_server_id,
50 alert_delivery_kind, alert_delivery_url,
51 tier, version_label)
52 VALUES
53 ($q_name, $q_desc, NOW(), $q_by, 1, NOW(),
54 $mid_expr, $sid_expr,
55 $q_dk, $q_du,
56 $q_tier, $q_vl)
21 INSERT INTO NAMED_RELEASES (name, description, released_at, released_by, is_locked, created_at)
22 VALUES ($q_name, $q_desc, NOW(), $q_by, 1, NOW())
5723 ~, __FILE__, __LINE__);
58 # Extra scopes for cross-scope releases (Wave 5 F5): multi-select
59 # scope_monitor_ids[] additive
60 my $new_rel = $db->db_readwrite($dbh, "SELECT LAST_INSERT_ID() AS id", __FILE__, __LINE__);
61 my $rid = $new_rel && $new_rel->{id};
62 if ($rid) {
63 foreach my $extra ($cgi->param('extra_monitor_ids')) {
64 my $eid = int($extra || 0);
65 next unless $eid && $eid != $scope_mid;
66 $db->db_readwrite($dbh, qq~
67 INSERT INTO NAMED_RELEASE_SCOPES
68 (named_release_id, file_monitor_list_id)
69 VALUES ($rid, $eid)
70 ~, __FILE__, __LINE__);
71 }
72 }
73 }
74 print "Status: 302 Found\nLocation: /named_releases.cgi\n\n"; exit;
75}
76
77# ---- AJAX: test alert delivery ------------------------------------
78if ($cgi->param('op') && $cgi->param('op') eq 'test_alert') {
79 my $rid = int($cgi->param('release_id') || 0);
80 print "Content-Type: application/json\nCache-Control: no-cache\n\n";
81 my $r = $db->db_readwrite($dbh, qq~
82 SELECT name, alert_delivery_kind, alert_delivery_url
83 FROM NAMED_RELEASES WHERE named_release_id = $rid LIMIT 1
84 ~, __FILE__, __LINE__);
85 unless ($r && $r->{alert_delivery_url}) {
86 print JSON::PP::encode_json({ ok => \0, message => 'no delivery URL configured' });
87 exit;
8824 }
89 my $summary = "TEST alert from DriftSense -- release '$r->{name}' -- ignore me";
90 my $body = ($r->{alert_delivery_kind} eq 'slack') ? {
91 text => "*DriftSense TEST*: $summary",
92 attachments => [{ color => '#f59e0b', text => 'This is a synthetic drift payload. No files actually drifted.' }],
93 } : ($r->{alert_delivery_kind} eq 'discord') ? {
94 content => "**DriftSense TEST**: $summary",
95 } : {
96 source => 'drift_sense', alert_type => 'test',
97 release_name => $r->{name}, summary => $summary,
98 };
99 my ($code, $rbody, $err) = MODS::Webhook::post_json(
100 $r->{alert_delivery_url}, $body,
101 timeout => 8,
102 user_agent => 'DriftSense/1.0 (test-alert)',
103 );
104 my $ok = (!$err && $code >= 200 && $code < 400) ? \1 : \0;
105 print JSON::PP::encode_json({
106 ok => $ok, http_code => $code, message => $err ? $err : "HTTP $code",
107 });
108 exit;
109}
110
111# ---- POST: promote / demote tier -----------------------------------
112if (($ENV{REQUEST_METHOD} || '') eq 'POST' && $cgi->param('promote')) {
113 my $rid = int($cgi->param('release_id') || 0);
114 my $tier = $cgi->param('to_tier') || 'stable';
115 $tier = 'draft' unless $tier =~ /^(draft|working|stable)$/;
116 my $q_tier = $dbh->quote($tier);
117 $db->db_readwrite($dbh,
118 "UPDATE NAMED_RELEASES SET tier = $q_tier WHERE named_release_id = $rid",
119 __FILE__, __LINE__);
12025 print "Status: 302 Found\nLocation: /named_releases.cgi\n\n"; exit;
12126}
12227
12328print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
12429
125# ---- Existing releases + count how many blobs each pins -------------
12630my @rows = $db->db_readwrite_multiple($dbh, q~
127 SELECT nr.named_release_id AS id, nr.name, nr.description, nr.tier, nr.version_label,
128 DATE_FORMAT(nr.released_at, '%b %d, %Y %H:%i') AS released_h,
129 UNIX_TIMESTAMP(nr.released_at) AS released_epoch,
130 nr.released_by, nr.is_locked,
131 nr.scope_monitor_id, nr.scope_server_id,
132 m.scan_name, s.server_name
133 FROM NAMED_RELEASES nr
134 LEFT JOIN FILE_MONITOR_SETTINGS m ON m.file_monitor_list_id = nr.scope_monitor_id
135 LEFT JOIN SERVERS s ON s.server_id = nr.scope_server_id
136 ORDER BY (nr.tier = 'stable') DESC,
137 (nr.tier = 'working') DESC,
138 nr.released_at DESC
31 SELECT named_release_id AS id, name, description,
32 DATE_FORMAT(released_at, '%b %d, %Y %H:%i') AS released_h,
33 released_by, is_locked
34 FROM NAMED_RELEASES
35 ORDER BY released_at DESC
13936 LIMIT 100
14037~, __FILE__, __LINE__);
14138foreach my $r (@rows) {
142 $r->{lock_pill} = $r->{is_locked} ? 'pill-ok' : 'pill-mute';
143 $r->{lock_lbl} = $r->{is_locked} ? 'PROTECTED' : 'UNLOCKED';
144 $r->{released_epoch} //= 0;
145 $r->{tier} //= 'draft';
146 $r->{tier_pill} = $r->{tier} eq 'stable' ? 'pill-ok'
147 : $r->{tier} eq 'working' ? 'pill-info'
148 : 'pill-mute';
149 $r->{tier_lbl} = uc($r->{tier});
150 $r->{display_label} = $r->{version_label} || $r->{name};
151 $r->{is_stable} = $r->{tier} eq 'stable' ? 1 : 0;
152 $r->{is_draft} = $r->{tier} eq 'draft' ? 1 : 0;
153 $r->{is_working} = $r->{tier} eq 'working'? 1 : 0;
154 $r->{restore_url} = "/restore_release.cgi?release_id=$r->{id}";
155 if ($r->{scope_monitor_id}) {
156 $r->{scope_pill} = 'pill-info';
157 $r->{scope_lbl} = 'per-site';
158 $r->{scope_h} = $r->{scan_name} || "monitor #$r->{scope_monitor_id}";
159 } elsif ($r->{scope_server_id}) {
160 $r->{scope_pill} = 'pill-info';
161 $r->{scope_lbl} = 'per-server';
162 $r->{scope_h} = $r->{server_name} || "server #$r->{scope_server_id}";
163 } else {
164 $r->{scope_pill} = 'pill-mute';
165 $r->{scope_lbl} = 'portfolio';
166 $r->{scope_h} = 'All monitored sites';
167 }
168}
169
170# ---- All monitors + servers for scope picker ------------------------
171my @monitors = $db->db_readwrite_multiple($dbh, q~
172 SELECT m.file_monitor_list_id AS mid, m.scan_name, s.server_name
173 FROM FILE_MONITOR_SETTINGS m
174 LEFT JOIN SERVERS s ON s.server_id = m.server_id
175 WHERE m.status = 1
176 ORDER BY s.server_id ASC, m.file_monitor_list_id ASC
177~, __FILE__, __LINE__);
178foreach my $m (@monitors) {
179 $m->{label} = ($m->{scan_name} // '?') . ' (on ' . ($m->{server_name} // '?') . ')';
39 $r->{lock_pill} = $r->{is_locked} ? 'pill-ok' : 'pill-mute';
40 $r->{lock_lbl} = $r->{is_locked} ? 'PROTECTED' : 'UNLOCKED';
18041}
181
18242my $total = scalar @rows;
43
18344$db->db_disconnect($dbh);
18445
185my $tvars = {
186 rows => \@rows,
187 has_rows => $total ? 1 : 0,
188 total => $total,
189 monitors => \@monitors,
190 has_monitors=> scalar(@monitors) ? 1 : 0,
191};
46my $tvars = { rows => \@rows, has_rows => $total ? 1 : 0, total => $total };
19247my @body = $tpl->template('named_releases.html', $tvars);
19348MODS::PageWrapper->new->wrapper(
19449 page_title => 'Named releases', page_key => 'named_releases',
19550 body_html => join('', @body), userinfo => { display_name => 'Operator' },
19651);
Confirm restore
Backs current content to <target>.drift_restore_backup_<epoch>, writes the captured content, verifies SHA post-write.
Cancel Logged to RESTORE_LOG regardless of outcome.