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/settings.cgi
SiteDriftSense self-monitor on local
Kindlocal
Captured at2026-07-12 00:02:14
Captured SHAc41f0faf4e6b5de233f0754d8075353fbd01ae093b2a1a94f5c0de14fd7cb30d
Current state on disk
Live check just now. If SHAs match, the restore is a no-op.
File presentyes
Size5538 bytes
Current SHA7dd39d832ea1
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. +0 additions, -27 deletions, 105 unchanged context lines.
11#!/usr/bin/perl
22#======================================================================
33# DriftSense -- /settings.cgi
44#
55# Read-only view of the resolved config file + editable form for the
66# SCAN_GLOBALS row (global ignore patterns, allowed file types, max
77# file size).
88#======================================================================
99use strict;
1010use warnings;
1111use CGI ();
1212use MODS::Config;
1313use MODS::DBConnect;
1414use MODS::Template;
1515use MODS::PageWrapper;
1616
1717my $cgi = CGI->new;
1818my $cfg = MODS::Config->new;
1919my $db = MODS::DBConnect->new;
2020my $tpl = MODS::Template->new;
2121
2222$|=1;
2323my $dbh = $db->db_connect or die "DB connect failed\n";
2424
2525# ---- POST: update globals ------------------------------------------
2626if (($ENV{REQUEST_METHOD} || '') eq 'POST' && $cgi->param('save_globals')) {
2727 my $ign = $cgi->param('global_ignore_list') // '';
2828 my $ftyp = $cgi->param('global_file_type_filter') // '';
2929 my $mx = int($cgi->param('global_max_file_size_bytes') || 0);
3030
3131 # Normalize whitespace on ignore list -> comma-joined single-line
3232 my @clean_ign;
3333 for my $tok (split /[,\r\n]+/, $ign) {
3434 $tok =~ s/^\s+|\s+$//g;
3535 push @clean_ign, $tok if length $tok;
3636 }
3737 my $normalized = join(',', @clean_ign);
3838
3939 my $q_ign = $dbh->quote($normalized);
4040 my $q_ftyp = $dbh->quote($ftyp);
4141 $mx = 'NULL' unless $mx;
4242
4343 $db->db_readwrite($dbh, qq~
4444 UPDATE SCAN_GLOBALS
4545 SET global_ignore_list = $q_ign,
4646 global_file_type_filter = $q_ftyp,
4747 global_max_file_size_bytes = $mx
4848 WHERE id = 1
4949 ~, __FILE__, __LINE__);
5050 $db->db_disconnect($dbh);
5151 print "Status: 302 Found\nLocation: /settings.cgi?saved=1\n\n";
5252 exit;
5353}
5454
5555print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
5656
5757my $all = $cfg->all;
5858
5959# Blob store stats
6060my $blob_stats = $db->db_readwrite($dbh, q~
6161 SELECT COUNT(*) AS blob_ct,
6262 COALESCE(SUM(size_uncompressed), 0) AS bytes_uncomp,
6363 COALESCE(SUM(size_compressed), 0) AS bytes_comp,
6464 COALESCE(SUM(ref_count), 0) AS total_refs
6565 FROM BLOB_STORE
66~, __FILE__, __LINE__);
67
68# Recent purge stats (last 30 days) for the tile
69my $recent_purge = $db->db_readwrite($dbh, q~
70 SELECT
71 SUM(CASE WHEN dry_run = 1 THEN 1 ELSE 0 END) AS dry_ct,
72 SUM(CASE WHEN dry_run = 1 THEN freed_bytes ELSE 0 END) AS dry_bytes,
73 SUM(CASE WHEN dry_run = 0 THEN 1 ELSE 0 END) AS live_ct,
74 SUM(CASE WHEN dry_run = 0 THEN freed_bytes ELSE 0 END) AS live_bytes,
75 DATE_FORMAT(MAX(purged_at), '%b %d %H:%i') AS last_purge_h,
76 UNIX_TIMESTAMP(MAX(purged_at)) AS last_purge_epoch
77 FROM PURGE_LOG
78 WHERE purged_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
7966~, __FILE__, __LINE__);
8067
8168# Global scan settings
8269my $g = $db->db_readwrite($dbh, q~
8370 SELECT global_ignore_list, global_file_type_filter, global_max_file_size_bytes
8471 FROM SCAN_GLOBALS WHERE id = 1 LIMIT 1
8572~, __FILE__, __LINE__);
8673my $global_ignore = ($g && $g->{global_ignore_list}) // '';
8774my $global_ftypes = ($g && $g->{global_file_type_filter}) // '';
8875my $global_max_sz = ($g && $g->{global_max_file_size_bytes}) || 0;
8976# Split the ignore list onto lines for the textarea (nicer edit UX)
9077my $ignore_pretty = join("\n", split /,/, $global_ignore);
9178
9279$db->db_disconnect($dbh);
9380
9481my @cfg_rows;
9582foreach my $k (sort keys %$all) {
9683 my $v = $all->{$k} // '';
9784 $v = length($v) > 4 ? '***' . substr($v, -4) : '****'
9885 if $k =~ /pass(word)?|secret|hmac|smtp_password/i && length $v;
9986 push @cfg_rows, { key => $k, value => $v };
10087}
10188
10289my $tvars = {
10390 cfg_rows => \@cfg_rows,
10491 blob_ct => ($blob_stats && $blob_stats->{blob_ct}) || 0,
10592 bytes_uncomp => ($blob_stats && $blob_stats->{bytes_uncomp}) || 0,
10693 bytes_comp => ($blob_stats && $blob_stats->{bytes_comp}) || 0,
10794 total_refs => ($blob_stats && $blob_stats->{total_refs}) || 0,
10895 ratio => (($blob_stats && $blob_stats->{bytes_uncomp}) ? sprintf('%.1fx', $blob_stats->{bytes_uncomp} / ($blob_stats->{bytes_comp} || 1)) : '-'),
10996 global_ignore_pretty => $ignore_pretty,
11097 global_file_type_filter => $global_ftypes,
11198 global_max_file_size_bytes => $global_max_sz,
11299 saved => $cgi->param('saved') ? 1 : 0,
113 purge_dry_ct => ($recent_purge && $recent_purge->{dry_ct}) || 0,
114 purge_dry_bytes => _fmt_bytes(($recent_purge && $recent_purge->{dry_bytes}) || 0),
115 purge_live_ct => ($recent_purge && $recent_purge->{live_ct}) || 0,
116 purge_live_bytes => _fmt_bytes(($recent_purge && $recent_purge->{live_bytes}) || 0),
117 last_purge_h => ($recent_purge && $recent_purge->{last_purge_h}) // 'never',
118 last_purge_epoch => ($recent_purge && $recent_purge->{last_purge_epoch}) || 0,
119100};
120
121sub _fmt_bytes {
122 my $n = shift; $n //= 0;
123 return "$n B" if $n < 1024;
124 return sprintf('%.1f KB', $n/1024) if $n < 1024*1024;
125 return sprintf('%.1f MB', $n/1024/1024) if $n < 1024*1024*1024;
126 return sprintf('%.1f GB', $n/1024/1024/1024);
127}
128101my @body = $tpl->template('settings.html', $tvars);
129102MODS::PageWrapper->new->wrapper(
130103 page_title => 'Settings', page_key => 'settings',
131104 body_html => join('', @body), userinfo => { display_name => 'Operator' },
132105);
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.