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/alert_test_catcher.cgi |
| Site | DriftSense self-monitor on local |
| Kind | local |
| Captured at | 2026-07-11 18:55:00 |
| Captured SHA | 0c1ec2c322e7b8b52f3f78b84ff0134d2b87b54aeae1adb7fd4119af6ecdaba9 |
Current state on disk
Live check just now. If SHAs match, the restore is a no-op.
| File present | yes |
| Size | 3599 bytes |
| Current SHA | 215f8f105f61 |
| 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. +2 additions, -12 deletions, 83 unchanged context lines.
| 1 | 1 | #!/usr/bin/perl |
| 2 | 2 | #====================================================================== |
| 3 | 3 | # DriftSense -- /alert_test_catcher.cgi |
| 4 | 4 | # |
| 5 | 5 | # Tiny endpoint that accepts POSTs from the alert dispatcher and appends |
| 6 | 6 | # the raw body + headers to /var/log/drift_sense/test_catcher.log. Used |
| 7 | 7 | # for end-to-end alert testing without needing an external webhook. |
| 8 | 8 | # |
| 9 | 9 | # GET /alert_test_catcher.cgi -> HTML view of the last N events |
| 10 | 10 | # POST anything -> log it, return 200 JSON |
| 11 | 11 | # GET /alert_test_catcher.cgi?raw=1 -> tail the log as text/plain |
| 12 | 12 | #====================================================================== |
| 13 | 13 | use strict; |
| 14 | 14 | use warnings; |
| 15 | 15 | use CGI (); |
| 16 | 16 | use POSIX (); |
| 17 | 17 | |
| 18 | 18 | my $LOG = '/var/log/drift_sense/test_catcher.log'; |
| 19 | 19 | |
| 20 | 20 | $|=1; |
| 21 | 21 | my $cgi = CGI->new; |
| 22 | 22 | my $method = $ENV{REQUEST_METHOD} || 'GET'; |
| 23 | 23 | |
| 24 | 24 | if ($method eq 'POST') { |
| 25 | 25 | my $body = ''; |
| 26 | # Prefer CGI.pm's POSTDATA -- handles Plesk/nginx buffering + suEXEC | |
| 27 | # STDIN quirks reliably; fall back to sysread if that returns empty. | |
| 28 | $body = $cgi->param('POSTDATA') // ''; | |
| 29 | if ($body eq '' && defined $ENV{CONTENT_LENGTH} && $ENV{CONTENT_LENGTH} > 0) { | |
| 30 | binmode STDIN; | |
| 31 | my $want = int($ENV{CONTENT_LENGTH}); | |
| 32 | while (length($body) < $want) { | |
| 33 | my $chunk = ''; | |
| 34 | my $n = sysread(STDIN, $chunk, $want - length($body)); | |
| 35 | last unless $n; | |
| 36 | $body .= $chunk; | |
| 37 | } | |
| 26 | if (defined $ENV{CONTENT_LENGTH} && $ENV{CONTENT_LENGTH} > 0) { | |
| 27 | read(STDIN, $body, $ENV{CONTENT_LENGTH}); | |
| 38 | 28 | } |
| 39 | 29 | my $stamp = POSIX::strftime('%Y-%m-%d %H:%M:%S', localtime); |
| 40 | 30 | my @hdrs; |
| 41 | 31 | foreach my $k (sort keys %ENV) { |
| 42 | 32 | next unless $k =~ /^(HTTP_|CONTENT_|REQUEST_|REMOTE_)/; |
| 43 | 33 | push @hdrs, " $k = $ENV{$k}"; |
| 44 | 34 | } |
| 45 | 35 | if (open(my $fh, '>>', $LOG)) { |
| 46 | 36 | print $fh "==================================================\n"; |
| 47 | 37 | print $fh "[$stamp] delivery received\n"; |
| 48 | 38 | print $fh join("\n", @hdrs), "\n"; |
| 49 | 39 | print $fh "-- body ($ENV{CONTENT_LENGTH} bytes) --\n"; |
| 50 | 40 | print $fh $body, "\n"; |
| 51 | 41 | close($fh); |
| 52 | 42 | } |
| 53 | 43 | print "Content-Type: application/json\n\n"; |
| 54 | 44 | print '{"ok":true,"received_bytes":' . length($body) . '}'; |
| 55 | 45 | exit 0; |
| 56 | 46 | } |
| 57 | 47 | |
| 58 | 48 | # GET modes |
| 59 | 49 | if ($cgi->param('raw')) { |
| 60 | 50 | print "Content-Type: text/plain; charset=utf-8\nCache-Control: no-cache\n\n"; |
| 61 | 51 | if (open(my $fh, '<', $LOG)) { |
| 62 | 52 | my @lines = <$fh>; close($fh); |
| 63 | 53 | # tail last 500 lines |
| 64 | 54 | my $start = @lines > 500 ? scalar(@lines) - 500 : 0; |
| 65 | 55 | print join('', @lines[$start .. $#lines]); |
| 66 | 56 | } else { |
| 67 | 57 | print "(no deliveries yet)\n"; |
| 68 | 58 | } |
| 69 | 59 | exit 0; |
| 70 | 60 | } |
| 71 | 61 | |
| 72 | 62 | # HTML view |
| 73 | 63 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n"; |
| 74 | 64 | my $body = ''; |
| 75 | 65 | if (open(my $fh, '<', $LOG)) { |
| 76 | 66 | local $/; $body = <$fh>; close($fh); |
| 77 | 67 | } |
| 78 | 68 | my $entries = 0; |
| 79 | 69 | $entries++ while $body =~ /===+/g; |
| 80 | 70 | |
| 81 | 71 | my $esc = $body // ''; |
| 82 | 72 | $esc =~ s/&/&/g; $esc =~ s/</</g; $esc =~ s/>/>/g; |
| 83 | 73 | |
| 84 | 74 | print <<"HTML"; |
| 85 | 75 | <!doctype html><html><head><meta charset="utf-8"><title>Alert catcher -- DriftSense</title> |
| 86 | 76 | <link rel="stylesheet" href="/assets/styles/drift_sense.css"> |
| 87 | 77 | <meta http-equiv="refresh" content="10"> |
| 88 | 78 | </head><body> |
| 89 | 79 | <div style="padding:24px;max-width:1200px;margin:0 auto"> |
| 90 | 80 | <h1 style="color:var(--accent);font-family:var(--sans)">Alert test catcher</h1> |
| 91 | 81 | <p style="color:var(--text-dim)">POST anything here and it gets appended to <code>$LOG</code>. Auto-refreshes every 10s. <a href="?raw=1" style="color:var(--accent-hi)">raw view</a></p> |
| 92 | 82 | <div style="color:var(--text);font-size:12.5px;font-family:var(--mono);background:var(--bg-2);border:1px solid var(--border);border-radius:12px;padding:14px 16px;white-space:pre-wrap;max-height:75vh;overflow:auto">$esc</div> |
| 93 | 83 | </div> |
| 94 | 84 | </body></html> |
| 95 | 85 | HTML |
Confirm restore
Backs current content to <target>.drift_restore_backup_<epoch>, writes the captured content, verifies SHA post-write.