Restore
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 |
| Site | DriftSense self-monitor on local |
| Kind | local |
| Captured at | 2026-07-12 00:02:11 |
| Captured SHA | 37a1a9fc1137e2929315a40d0069815399f0727fd4f63b83e09f71d848abea07 |
Current state on disk
Live check just now. If SHAs match, the restore is a no-op.
| File present | yes |
| Size | 4465 bytes |
| Current SHA | e108e89453cb |
| 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.
| 1 | 1 | #!/usr/bin/perl |
| 2 | 2 | #====================================================================== |
| 3 | 3 | # DriftSense -- /pin_hotspots.cgi |
| 4 | 4 | # |
| 5 | 5 | # Bulk-pin the top-N most-changed files into a new Named Release. |
| 6 | 6 | # GET shows a preview form (default N=20 over 30 days). |
| 7 | 7 | # POST creates a NAMED_RELEASES row + one NAMED_RELEASE_PINS row per |
| 8 | 8 | # top-file (references the file's latest file_changes_id). |
| 9 | 9 | #====================================================================== |
| 10 | 10 | use strict; |
| 11 | 11 | use warnings; |
| 12 | 12 | use CGI (); |
| 13 | 13 | use MODS::Config; |
| 14 | 14 | use MODS::DBConnect; |
| 15 | 15 | use MODS::Template; |
| 16 | 16 | use MODS::PageWrapper; |
| 17 | use POSIX (); | |
| 18 | 17 | |
| 19 | 18 | my $cgi = CGI->new; |
| 20 | 19 | my $db = MODS::DBConnect->new; |
| 21 | 20 | my $tpl = MODS::Template->new; |
| 22 | 21 | $|=1; |
| 23 | 22 | |
| 24 | 23 | my $dbh = $db->db_connect or die "DriftSense: DB connect failed\n"; |
| 25 | 24 | |
| 26 | 25 | my $method = $ENV{REQUEST_METHOD} || 'GET'; |
| 27 | 26 | my $limit = int($cgi->param('n') || 20); $limit = 5 if $limit < 5; $limit = 100 if $limit > 100; |
| 28 | 27 | my $days = int($cgi->param('days') || 30); $days = 1 if $days < 1; $days = 365 if $days > 365; |
| 29 | 28 | |
| 30 | 29 | # ---- Top-N most-changed files in the window ------------------------- |
| 31 | 30 | my @top = $db->db_readwrite_multiple($dbh, qq~ |
| 32 | 31 | SELECT fc.file_name, |
| 33 | 32 | m.scan_name, |
| 34 | 33 | m.file_monitor_list_id AS mid, |
| 35 | 34 | 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 | |
| 37 | 40 | FROM FILE_CHANGES fc |
| 38 | 41 | LEFT JOIN FILE_MONITOR_SETTINGS m ON m.file_monitor_list_id = fc.file_monitor_list_id |
| 39 | 42 | WHERE fc.date_time >= DATE_SUB(NOW(), INTERVAL $days DAY) |
| 40 | 43 | AND fc.blob_sha IS NOT NULL |
| 41 | 44 | GROUP BY fc.file_name, m.file_monitor_list_id, m.scan_name |
| 42 | 45 | ORDER BY change_ct DESC, latest_id DESC |
| 43 | 46 | LIMIT $limit |
| 44 | 47 | ~, __FILE__, __LINE__); |
| 45 | ||
| 46 | # Second pass: fetch each top row's latest blob_sha | |
| 47 | foreach 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 | } | |
| 54 | 48 | |
| 55 | 49 | # ---- POST: create the pin release ---------------------------------- |
| 56 | 50 | if ($method eq 'POST' && $cgi->param('confirm')) { |
| 57 | 51 | my $name = $cgi->param('name') || 'hotspot-pin-' . time(); |
| 58 | 52 | my $desc = $cgi->param('description') |
| 59 | 53 | || "Auto-pinned top-$limit files by change count over last $days days"; |
| 60 | 54 | my $by = 'Operator'; |
| 61 | 55 | |
| 62 | 56 | my $q_name = $dbh->quote($name); |
| 63 | 57 | my $q_desc = $dbh->quote($desc); |
| 64 | 58 | my $q_by = $dbh->quote($by); |
| 65 | 59 | $db->db_readwrite($dbh, qq~ |
| 66 | 60 | INSERT INTO NAMED_RELEASES |
| 67 | 61 | (name, description, released_at, released_by, is_locked, created_at) |
| 68 | 62 | VALUES |
| 69 | 63 | ($q_name, $q_desc, NOW(), $q_by, 1, NOW()) |
| 70 | 64 | ~, __FILE__, __LINE__); |
| 71 | 65 | my $rel = $db->db_readwrite($dbh, "SELECT LAST_INSERT_ID() AS id", __FILE__, __LINE__); |
| 72 | 66 | my $rid = $rel && $rel->{id}; |
| 73 | 67 | |
| 74 | 68 | my $pinned = 0; |
| 75 | 69 | if ($rid) { |
| 76 | 70 | foreach my $t (@top) { |
| 77 | 71 | next unless $t->{latest_id} && $t->{blob_sha}; |
| 78 | 72 | my $q_file = $dbh->quote($t->{file_name}); |
| 79 | 73 | my $q_sha = $dbh->quote($t->{blob_sha}); |
| 80 | 74 | $db->db_readwrite($dbh, qq~ |
| 81 | 75 | INSERT INTO NAMED_RELEASE_PINS |
| 82 | 76 | (named_release_id, file_changes_id, file_name, blob_sha) |
| 83 | 77 | VALUES |
| 84 | 78 | ($rid, $t->{latest_id}, $q_file, $q_sha) |
| 85 | 79 | ~, __FILE__, __LINE__); |
| 86 | 80 | $pinned++; |
| 87 | 81 | } |
| 88 | 82 | } |
| 89 | 83 | $db->db_disconnect($dbh); |
| 90 | 84 | print "Status: 302 Found\nLocation: /named_releases.cgi?pinned=$pinned\n\n"; |
| 91 | 85 | exit; |
| 92 | 86 | } |
| 93 | 87 | |
| 94 | 88 | # ---- GET: preview page --------------------------------------------- |
| 95 | 89 | foreach my $t (@top) { |
| 96 | 90 | $t->{file_short} = length($t->{file_name} // '') > 70 |
| 97 | 91 | ? '...' . substr($t->{file_name}, -67) : ($t->{file_name} // ''); |
| 98 | 92 | $t->{sha_short} = substr($t->{blob_sha} // '', 0, 12); |
| 99 | 93 | $t->{diff_url} = "/diff.cgi?kind=file&id=" . ($t->{latest_id} || 0); |
| 100 | 94 | } |
| 101 | 95 | $db->db_disconnect($dbh); |
| 102 | 96 | |
| 103 | my $suggest_name = 'hotspot-pin-' . POSIX::strftime('%Y%m%d-%H%M', localtime); | |
| 97 | my $suggest_name = sprintf('hotspot-pin-%s', POSIX::strftime('%Y%m%d-%H%M', localtime)); | |
| 98 | require POSIX; | |
| 99 | $suggest_name = 'hotspot-pin-' . POSIX::strftime('%Y%m%d-%H%M', localtime); | |
| 104 | 100 | |
| 105 | 101 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n"; |
| 106 | 102 | my @body = $tpl->template('pin_hotspots.html', { |
| 107 | 103 | top => \@top, |
| 108 | 104 | has_top => scalar(@top) ? 1 : 0, |
| 109 | 105 | total => scalar @top, |
| 110 | 106 | limit => $limit, |
| 111 | 107 | days => $days, |
| 112 | 108 | suggest_name => $suggest_name, |
| 113 | 109 | }); |
| 114 | 110 | MODS::PageWrapper->new->wrapper( |
| 115 | 111 | page_title => 'Pin hotspots', page_key => '', |
| 116 | 112 | body_html => join('', @body), userinfo => { display_name => 'Operator' }, |
| 117 | 113 | ); |
Confirm restore
Backs current content to <target>.drift_restore_backup_<epoch>, writes the captured content, verifies SHA post-write.