Diff -- /var/www/vhosts/3dshawn.com/site1/_purge.pl

O Operator
Diff

/var/www/vhosts/3dshawn.com/site1/_purge.pl

modified on local at 2026-07-12 00:19:35

Added
+46
lines
Removed
-5
lines
Context
162
unchanged
Blobs
from 35a296e95b1a
to d2622a1d705d
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
11#!/usr/bin/perl
22#======================================================================
33# DriftSense -- auto-purge daemon
44#
55# Cron entry point (NOT a CGI). Reclaims disk in BLOB_STORE by deleting
66# blobs that are:
77#
88# * Older than the retention window (from config: retention_days,
99# default 90 days -- measured from BLOB_STORE.first_seen_at)
1010# * NOT referenced by any FILE_CHANGES row inside a Named Release's
1111# scope (portfolio-wide releases pin ALL blobs; scoped releases pin
1212# only their scope; explicit NAMED_RELEASE_PINS pin exact blobs)
1313# * NOT the current-latest capture for any active file monitor
1414# (we always keep the "most recent" state of every monitored file)
1515#
1616# Dry-run by default. Pass --live to actually delete. Every action
1717# (planned or performed) is written to PURGE_LOG for the audit trail.
1818#======================================================================
1919use strict;
2020use warnings;
2121use lib '/var/www/vhosts/3dshawn.com/site1';
2222use POSIX ();
2323use MODS::Config;
2424use MODS::DBConnect;
2525
2626$| = 1;
2727my $ts = POSIX::strftime('%Y-%m-%d %H:%M:%S', localtime);
2828
2929my $live = grep { $_ eq '--live' } @ARGV;
3030my $cfg = MODS::Config->new;
3131my $db = MODS::DBConnect->new;
3232my $dbh = $db->db_connect or die "[$ts] cannot connect to DriftSense storage DB\n";
3333
3434my $retention_days = int($cfg->settings('retention_days') || 90);
3535if ($retention_days < 1) { $retention_days = 90; }
3636
3737# ---- Pin sets -------------------------------------------------------
3838
3939# Explicit pins (NAMED_RELEASE_PINS)
4040my %pinned_sha;
4141my @pin_rows = $db->db_readwrite_multiple($dbh, q~
4242 SELECT DISTINCT blob_sha FROM NAMED_RELEASE_PINS
4343 WHERE blob_sha IS NOT NULL
4444~, __FILE__, __LINE__);
4545$pinned_sha{$_->{blob_sha}} = 1 for @pin_rows;
4646
4747# Portfolio-wide named releases: pin ALL blobs referenced by any
4848# FILE_CHANGES up to the release's released_at moment.
4949my @portfolio_releases = $db->db_readwrite_multiple($dbh, q~
5050 SELECT named_release_id, UNIX_TIMESTAMP(released_at) AS rel_epoch
5151 FROM NAMED_RELEASES
5252 WHERE is_locked = 1
5353 AND scope_monitor_id IS NULL
5454 AND scope_server_id IS NULL
5555~, __FILE__, __LINE__);
5656foreach my $rel (@portfolio_releases) {
5757 my $ep = int($rel->{rel_epoch} || 0);
5858 next unless $ep;
5959 my @refs = $db->db_readwrite_multiple($dbh, qq~
6060 SELECT DISTINCT blob_sha FROM FILE_CHANGES
6161 WHERE blob_sha IS NOT NULL
6262 AND date_time <= FROM_UNIXTIME($ep)
6363 ~, __FILE__, __LINE__);
6464 $pinned_sha{$_->{blob_sha}} = 1 for @refs;
6565}
6666
6767# Scoped named releases: pin only blobs referenced by that scope
6868my @scoped_releases = $db->db_readwrite_multiple($dbh, q~
6969 SELECT named_release_id, scope_monitor_id, scope_server_id,
7070 UNIX_TIMESTAMP(released_at) AS rel_epoch
7171 FROM NAMED_RELEASES
7272 WHERE is_locked = 1
7373 AND (scope_monitor_id IS NOT NULL OR scope_server_id IS NOT NULL)
7474~, __FILE__, __LINE__);
7575foreach my $rel (@scoped_releases) {
7676 my $ep = int($rel->{rel_epoch} || 0);
7777 next unless $ep;
7878 my $where = "date_time <= FROM_UNIXTIME($ep) AND blob_sha IS NOT NULL";
7979 if ($rel->{scope_monitor_id}) {
8080 $where .= " AND file_monitor_list_id = " . int($rel->{scope_monitor_id});
8181 }
8282 if ($rel->{scope_server_id}) {
8383 $where .= " AND server_id = " . int($rel->{scope_server_id});
8484 }
8585 my @refs = $db->db_readwrite_multiple($dbh, qq~
8686 SELECT DISTINCT blob_sha FROM FILE_CHANGES WHERE $where
8787 ~, __FILE__, __LINE__);
8888 $pinned_sha{$_->{blob_sha}} = 1 for @refs;
8989}
9090
9191# Current-latest state per file per monitor: never purge these
9292my @latest_rows = $db->db_readwrite_multiple($dbh, q~
9393 SELECT DISTINCT fc.blob_sha
9494 FROM FILE_CHANGES fc
9595 JOIN (
9696 SELECT file_monitor_list_id, file_name, MAX(file_changes_id) AS mx
9797 FROM FILE_CHANGES
9898 WHERE blob_sha IS NOT NULL
9999 GROUP BY file_monitor_list_id, file_name
100100 ) l ON l.mx = fc.file_changes_id
101101 WHERE fc.blob_sha IS NOT NULL
102102~, __FILE__, __LINE__);
103103$pinned_sha{$_->{blob_sha}} = 1 for @latest_rows;
104104
105# ---- Candidates: blobs older than retention window, NOT pinned -----
106my @candidates = $db->db_readwrite_multiple($dbh, qq~
107 SELECT blob_sha, size_compressed, size_uncompressed
108 FROM BLOB_STORE
109 WHERE first_seen_at < DATE_SUB(NOW(), INTERVAL $retention_days DAY)
105# ---- Candidates: blobs older than the applicable retention window ---
106# Per-monitor: FILE_MONITOR_SETTINGS.retention_days overrides the global
107# default. Rule: a blob is eligible iff EVERY FILE_CHANGES row referring
108# to it is older than the retention_days of that row's monitor. If any
109# reference is within its monitor's retention, keep the blob.
110
111# Pre-compute per-monitor retention (map mid -> days)
112my %monitor_retention;
113foreach my $m ($db->db_readwrite_multiple($dbh, q~
114 SELECT file_monitor_list_id, retention_days FROM FILE_MONITOR_SETTINGS
115~, __FILE__, __LINE__)) {
116 $monitor_retention{$m->{file_monitor_list_id}} =
117 (defined $m->{retention_days} && $m->{retention_days} > 0)
118 ? int($m->{retention_days}) : $retention_days;
119}
120
121# Fast path: pick blobs where the OLDEST reference's monitor retention
122# is already exceeded AND the NEWEST reference is also beyond the
123# per-monitor cutoff. Easier: iterate every blob, look at all its refs,
124# decide.
125my @candidates = $db->db_readwrite_multiple($dbh, q~
126 SELECT bs.blob_sha, bs.size_compressed, bs.size_uncompressed,
127 UNIX_TIMESTAMP(bs.first_seen_at) AS first_seen_epoch
128 FROM BLOB_STORE bs
110129~, __FILE__, __LINE__);
130# Filter candidates by per-monitor retention: keep only blobs where ALL
131# referencing FILE_CHANGES.date_time are older than the reference's
132# monitor retention days.
133my @filtered;
134foreach my $b (@candidates) {
135 my $sha = $b->{blob_sha};
136 my $q_sha = $dbh->quote($sha);
137 my @refs = $db->db_readwrite_multiple($dbh, qq~
138 SELECT file_monitor_list_id AS mid, UNIX_TIMESTAMP(date_time) AS ts
139 FROM FILE_CHANGES WHERE blob_sha = $q_sha
140 ~, __FILE__, __LINE__);
141 next unless @refs; # unreferenced blobs already handled elsewhere
142 my $now = time;
143 my $eligible = 1;
144 for my $r (@refs) {
145 my $days_for = $monitor_retention{$r->{mid}} // $retention_days;
146 my $cutoff = $now - $days_for * 86400;
147 if (($r->{ts} // 0) > $cutoff) { $eligible = 0; last; }
148 }
149 push @filtered, $b if $eligible;
150}
151@candidates = @filtered;
111152
112153my $planned = 0;
113154my $deleted = 0;
114155my $bytes_freed = 0;
115156
116157foreach my $b (@candidates) {
117158 next if $pinned_sha{$b->{blob_sha}};
118159 my $sha = $b->{blob_sha};
119160 my $q_sha = $dbh->quote($sha);
120161 my $sz = int($b->{size_compressed} || 0);
121162 my $reason = "age > $retention_days d, no pin, not current-latest";
122163
123164 if ($live) {
124165 # Delete FILE_CHANGES rows referencing this blob first (cascading)
125166 # NO -- we KEEP the FILE_CHANGES metadata (name, timestamp) even
126167 # after purging content; just null out blob_sha so the diff
127168 # viewer shows "content purged" gracefully.
128169 $db->db_readwrite($dbh, qq~
129170 UPDATE FILE_CHANGES SET blob_sha = NULL
130171 WHERE blob_sha = $q_sha
131172 ~, __FILE__, __LINE__);
132173 $db->db_readwrite($dbh, qq~
133174 DELETE FROM BLOB_STORE WHERE blob_sha = $q_sha
134175 ~, __FILE__, __LINE__);
135176 $deleted++;
136177 $bytes_freed += $sz;
137178 $db->db_readwrite($dbh, qq~
138179 INSERT INTO PURGE_LOG (blob_sha, freed_bytes, reason, dry_run)
139180 VALUES ($q_sha, $sz, @{[ $dbh->quote($reason) ]}, 0)
140181 ~, __FILE__, __LINE__);
141182 } else {
142183 $db->db_readwrite($dbh, qq~
143184 INSERT INTO PURGE_LOG (blob_sha, freed_bytes, reason, dry_run)
144185 VALUES ($q_sha, $sz, @{[ $dbh->quote($reason) ]}, 1)
145186 ~, __FILE__, __LINE__);
146187 $planned++;
147188 }
148189}
149190
150191$db->db_disconnect($dbh);
151192
152193my $mode = $live ? 'LIVE' : 'dry-run';
153194if ($live) {
154195 print "[$ts] purge $mode: deleted $deleted blobs, freed " . _fmt_bytes($bytes_freed) . "\n"
155196 if $deleted;
156197} else {
157198 print "[$ts] purge $mode: $planned blobs eligible (retention=$retention_days d) -- pass --live to execute\n"
158199 if $planned;
159200}
160201exit 0;
161202
162203sub _fmt_bytes {
163204 my $n = shift;
164205 return "$n B" if $n < 1024;
165206 return sprintf('%.1f KB', $n/1024) if $n < 1024*1024;
166207 return sprintf('%.1f MB', $n/1024/1024);
167208}