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/schema_changes.cgi
SiteDriftSense self-monitor on local
Kindlocal
Captured at2026-07-10 22:19:07
Captured SHA9efbed09b8bf9803dc8ea232e1a8b7e672b0ac59ddfcb4e8348ef0ccceff0b09
Current state on disk
Live check just now. If SHAs match, the restore is a no-op.
File presentyes
Size3827 bytes
Current SHAdbd198527dfc
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. +18 additions, -34 deletions, 75 unchanged context lines.
11#!/usr/bin/perl
22use strict;
33use warnings;
44use CGI ();
55use MODS::Config; use MODS::DBConnect; use MODS::Template; use MODS::PageWrapper;
66use MODS::Charts;
7use MODS::RangePicker;
87use POSIX ();
98my $cgi = CGI->new; my $db = MODS::DBConnect->new; my $tpl = MODS::Template->new;
109$|=1; print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
1110my $dbh = $db->db_connect or die "DB connect failed\n";
1211
13my $r = MODS::RangePicker::parse($cgi);
14my $q = $cgi->param('q') || '';
12my $days = int($cgi->param('days') || 30); $days = 1 if $days < 1; $days = 365 if $days > 365;
13my $q = $cgi->param('q') || '';
1514
16my $where = $r->{sql_where}->('sc.change_datetime');
17$where .= " AND (sc.database_name LIKE " . $dbh->quote('%'.$q.'%')
18 . " OR sc.table_name LIKE " . $dbh->quote('%'.$q.'%') . ")" if length $q;
15my $where = "change_datetime >= DATE_SUB(NOW(), INTERVAL $days DAY)";
16$where .= " AND (database_name LIKE " . $dbh->quote('%'.$q.'%') . " OR table_name LIKE " . $dbh->quote('%'.$q.'%') . ")" if length $q;
1917
2018my @rows = $db->db_readwrite_multiple($dbh, qq~
2119 SELECT sc.schema_change_id AS id, sc.database_name, sc.table_name, sc.changes,
22 UNIX_TIMESTAMP(sc.change_datetime) AS ts_epoch,
2320 sc.change_datetime, sc.is_ts_only, s.server_name
2421 FROM SCHEMA_CHANGE sc
2522 LEFT JOIN SERVERS s ON s.server_id = sc.server_id
2623 WHERE $where
2724 ORDER BY sc.change_datetime DESC
2825 LIMIT 200
2926~, __FILE__, __LINE__);
30
27# --- 30-day sparkline per database.table (schema DDL is much sparser
28# than file activity, so one bar per week reads better than one
29# per day, but we still ask for 30 days of buckets to reuse the
30# helper unchanged).
3131my %spark;
3232{
3333 my %pairs;
34 foreach my $row (@rows) {
35 my $key = ($row->{database_name} // '') . '|' . ($row->{table_name} // '');
36 $pairs{$key} = [ $row->{database_name}, $row->{table_name} ];
34 foreach my $r (@rows) {
35 my $key = ($r->{database_name} // '') . '|' . ($r->{table_name} // '');
36 $pairs{$key} = [ $r->{database_name}, $r->{table_name} ];
3737 }
3838 my @conds;
3939 foreach my $p (values %pairs) {
4040 my $qd = $dbh->quote($p->[0] // '');
4141 my $qt = $dbh->quote($p->[1] // '');
4242 push @conds, "(database_name = $qd AND table_name = $qt)";
4343 }
4444 if (@conds) {
4545 my $or = join(' OR ', @conds);
4646 my @day_rows = $db->db_readwrite_multiple($dbh, qq~
4747 SELECT database_name, table_name,
4848 DATE_FORMAT(change_datetime, '%Y-%m-%d') AS d,
4949 COUNT(*) AS n
5050 FROM SCHEMA_CHANGE
5151 WHERE ($or)
5252 AND change_datetime >= DATE_SUB(NOW(), INTERVAL 30 DAY)
5353 GROUP BY database_name, table_name, d
5454 ~, __FILE__, __LINE__);
5555 my %by;
5656 foreach my $dr (@day_rows) {
5757 my $key = ($dr->{database_name} // '') . '|' . ($dr->{table_name} // '');
5858 $by{$key}{$dr->{d}} = $dr->{n};
5959 }
6060 my $now = time;
6161 my @day_keys;
6262 for (my $i = 29; $i >= 0; $i--) {
6363 push @day_keys, POSIX::strftime('%Y-%m-%d', localtime($now - $i * 86400));
6464 }
6565 foreach my $key (keys %pairs) {
6666 my @series;
6767 for my $k (@day_keys) { push @series, ($by{$key}{$k} || 0); }
6868 $spark{$key} = \@series;
6969 }
7070 }
7171}
7272
73foreach my $row (@rows) {
74 my $c = $row->{changes} // '';
73foreach my $r (@rows) {
74 my $c = $r->{changes} // '';
7575 $c =~ s/\s+/ /g;
76 $row->{changes_preview} = length($c) > 100 ? substr($c, 0, 97) . '...' : $c;
77 $row->{diff_url} = "/diff.cgi?kind=schema&id=$row->{id}";
78 $row->{ts_epoch} //= 0;
79 my $key = ($row->{database_name} // '') . '|' . ($row->{table_name} // '');
76 $r->{changes_preview} = length($c) > 100 ? substr($c, 0, 97) . '...' : $c;
77 $r->{diff_url} = "/diff.cgi?kind=schema&id=$r->{id}";
78 my $key = ($r->{database_name} // '') . '|' . ($r->{table_name} // '');
8079 my $series = $spark{$key};
81 $row->{sparkline_svg} = $series
80 $r->{sparkline_svg} = $series
8281 ? MODS::Charts::sparkline(values => $series, width => 88, height => 20, color => '#f59e0b')
8382 : '';
8483}
8584my $total = scalar @rows;
8685
8786$db->db_disconnect($dbh);
88
89my $picker_html = MODS::RangePicker::picker_html(
90 action => '/schema_changes.cgi',
91 range => $r,
92 hidden => { q => $q },
93);
9487
95my $tvars = {
96 rows => \@rows,
97 has_rows => $total ? 1 : 0,
98 total => $total,
99 q => $q,
100 picker_html => $picker_html,
101 range_label => $r->{range_label},
102 is_day_view => $r->{is_day} ? 1 : 0,
103 day_label => $r->{on},
104};
88my $tvars = { rows => \@rows, has_rows => $total ? 1 : 0, total => $total, days => $days, q => $q };
10589my @body = $tpl->template('schema_changes.html', $tvars);
10690MODS::PageWrapper->new->wrapper(
10791 page_title => 'Schema changes', page_key => 'schema_changes',
10892 body_html => join('', @body), userinfo => { display_name => 'Operator' },
10993);
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.