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/pin_hotspots.cgi
SiteDriftSense self-monitor on local
Kindlocal
Captured at2026-07-12 00:02:11
Captured SHA37a1a9fc1137e2929315a40d0069815399f0727fd4f63b83e09f71d848abea07
Current state on disk
Live check just now. If SHAs match, the restore is a no-op.
File presentyes
Size4465 bytes
Current SHAe108e89453cb
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. +8 additions, -12 deletions, 105 unchanged context lines.
11#!/usr/bin/perl
22#======================================================================
33# DriftSense -- /pin_hotspots.cgi
44#
55# Bulk-pin the top-N most-changed files into a new Named Release.
66# GET shows a preview form (default N=20 over 30 days).
77# POST creates a NAMED_RELEASES row + one NAMED_RELEASE_PINS row per
88# top-file (references the file's latest file_changes_id).
99#======================================================================
1010use strict;
1111use warnings;
1212use CGI ();
1313use MODS::Config;
1414use MODS::DBConnect;
1515use MODS::Template;
1616use MODS::PageWrapper;
17use POSIX ();
1817
1918my $cgi = CGI->new;
2019my $db = MODS::DBConnect->new;
2120my $tpl = MODS::Template->new;
2221$|=1;
2322
2423my $dbh = $db->db_connect or die "DriftSense: DB connect failed\n";
2524
2625my $method = $ENV{REQUEST_METHOD} || 'GET';
2726my $limit = int($cgi->param('n') || 20); $limit = 5 if $limit < 5; $limit = 100 if $limit > 100;
2827my $days = int($cgi->param('days') || 30); $days = 1 if $days < 1; $days = 365 if $days > 365;
2928
3029# ---- Top-N most-changed files in the window -------------------------
3130my @top = $db->db_readwrite_multiple($dbh, qq~
3231 SELECT fc.file_name,
3332 m.scan_name,
3433 m.file_monitor_list_id AS mid,
3534 COUNT(*) AS change_ct,
36 MAX(fc.file_changes_id) AS latest_id
35 MAX(fc.file_changes_id) AS latest_id,
36 (SELECT bs.blob_sha FROM FILE_CHANGES fc2
37 WHERE fc2.file_changes_id = MAX(fc.file_changes_id)
38 AND fc2.blob_sha IS NOT NULL
39 LIMIT 1) AS blob_sha
3740 FROM FILE_CHANGES fc
3841 LEFT JOIN FILE_MONITOR_SETTINGS m ON m.file_monitor_list_id = fc.file_monitor_list_id
3942 WHERE fc.date_time >= DATE_SUB(NOW(), INTERVAL $days DAY)
4043 AND fc.blob_sha IS NOT NULL
4144 GROUP BY fc.file_name, m.file_monitor_list_id, m.scan_name
4245 ORDER BY change_ct DESC, latest_id DESC
4346 LIMIT $limit
4447~, __FILE__, __LINE__);
45
46# Second pass: fetch each top row's latest blob_sha
47foreach my $t (@top) {
48 next unless $t->{latest_id};
49 my $r = $db->db_readwrite($dbh,
50 "SELECT blob_sha FROM FILE_CHANGES WHERE file_changes_id = " . int($t->{latest_id}) . " LIMIT 1",
51 __FILE__, __LINE__);
52 $t->{blob_sha} = $r ? $r->{blob_sha} : undef;
53}
5448
5549# ---- POST: create the pin release ----------------------------------
5650if ($method eq 'POST' && $cgi->param('confirm')) {
5751 my $name = $cgi->param('name') || 'hotspot-pin-' . time();
5852 my $desc = $cgi->param('description')
5953 || "Auto-pinned top-$limit files by change count over last $days days";
6054 my $by = 'Operator';
6155
6256 my $q_name = $dbh->quote($name);
6357 my $q_desc = $dbh->quote($desc);
6458 my $q_by = $dbh->quote($by);
6559 $db->db_readwrite($dbh, qq~
6660 INSERT INTO NAMED_RELEASES
6761 (name, description, released_at, released_by, is_locked, created_at)
6862 VALUES
6963 ($q_name, $q_desc, NOW(), $q_by, 1, NOW())
7064 ~, __FILE__, __LINE__);
7165 my $rel = $db->db_readwrite($dbh, "SELECT LAST_INSERT_ID() AS id", __FILE__, __LINE__);
7266 my $rid = $rel && $rel->{id};
7367
7468 my $pinned = 0;
7569 if ($rid) {
7670 foreach my $t (@top) {
7771 next unless $t->{latest_id} && $t->{blob_sha};
7872 my $q_file = $dbh->quote($t->{file_name});
7973 my $q_sha = $dbh->quote($t->{blob_sha});
8074 $db->db_readwrite($dbh, qq~
8175 INSERT INTO NAMED_RELEASE_PINS
8276 (named_release_id, file_changes_id, file_name, blob_sha)
8377 VALUES
8478 ($rid, $t->{latest_id}, $q_file, $q_sha)
8579 ~, __FILE__, __LINE__);
8680 $pinned++;
8781 }
8882 }
8983 $db->db_disconnect($dbh);
9084 print "Status: 302 Found\nLocation: /named_releases.cgi?pinned=$pinned\n\n";
9185 exit;
9286}
9387
9488# ---- GET: preview page ---------------------------------------------
9589foreach my $t (@top) {
9690 $t->{file_short} = length($t->{file_name} // '') > 70
9791 ? '...' . substr($t->{file_name}, -67) : ($t->{file_name} // '');
9892 $t->{sha_short} = substr($t->{blob_sha} // '', 0, 12);
9993 $t->{diff_url} = "/diff.cgi?kind=file&id=" . ($t->{latest_id} || 0);
10094}
10195$db->db_disconnect($dbh);
10296
103my $suggest_name = 'hotspot-pin-' . POSIX::strftime('%Y%m%d-%H%M', localtime);
97my $suggest_name = sprintf('hotspot-pin-%s', POSIX::strftime('%Y%m%d-%H%M', localtime));
98require POSIX;
99$suggest_name = 'hotspot-pin-' . POSIX::strftime('%Y%m%d-%H%M', localtime);
104100
105101print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
106102my @body = $tpl->template('pin_hotspots.html', {
107103 top => \@top,
108104 has_top => scalar(@top) ? 1 : 0,
109105 total => scalar @top,
110106 limit => $limit,
111107 days => $days,
112108 suggest_name => $suggest_name,
113109});
114110MODS::PageWrapper->new->wrapper(
115111 page_title => 'Pin hotspots', page_key => '',
116112 body_html => join('', @body), userinfo => { display_name => 'Operator' },
117113);
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.