added on local at 2026-07-01 22:10:09
| 1 | #!/usr/bin/perl | |
| 2 | #====================================================================== | |
| 3 | # ShopCart -- storage alert worker (cron, hourly). | |
| 4 | # | |
| 5 | # Two responsibilities: | |
| 6 | # 1. For each user at or above the soft-alert threshold who has not | |
| 7 | # been alerted in the last 24h, INSERT a notifications row, | |
| 8 | # send a usage-warning email, and stamp users.last_storage_alert_at | |
| 9 | # so we do not spam them. | |
| 10 | # 2. Recompute users.storage_used_bytes for any user whose | |
| 11 | # storage_used_at_recompute is NULL or older than 24h. This drifts | |
| 12 | # the denormalized total back toward the authoritative SUM(...) of | |
| 13 | # the source tables (model_files, page_images, support_attachments, | |
| 14 | # and model_images via filesystem stat()). | |
| 15 | # | |
| 16 | # Cron: 0 * * * * cd /var/www/vhosts/3dshawn.com/shop.3dshawn.com && /usr/bin/perl _storage_alert_worker.pl >> _storage_alert.log 2>&1 | |
| 17 | #====================================================================== | |
| 18 | use strict; | |
| 19 | use warnings; | |
| 20 | ||
| 21 | use lib '/var/www/vhosts/3dshawn.com/shop.3dshawn.com'; | |
| 22 | use MODS::DBConnect; | |
| 23 | use MODS::ShopCart::Config; | |
| 24 | use MODS::ShopCart::Storage; | |
| 25 | use MODS::ShopCart::Mailer; | |
| 26 | ||
| 27 | my $db = MODS::DBConnect->new; | |
| 28 | my $config = MODS::ShopCart::Config->new; | |
| 29 | my $sto = MODS::ShopCart::Storage->new; | |
| 30 | my $DB = $config->settings('database_name') || 'shop3dshawn'; | |
| 31 | ||
| 32 | my $dbh = $db->db_connect(); | |
| 33 | die "can't connect to DB\n" unless $dbh; | |
| 34 | ||
| 35 | my $when = scalar localtime; | |
| 36 | print "[$when] storage_alert_worker start\n"; | |
| 37 | ||
| 38 | # ---------------------------------------------------------------------- | |
| 39 | # 1) Recompute stale totals first so soft-alert decisions are accurate. | |
| 40 | # ---------------------------------------------------------------------- | |
| 41 | my @stale = $sto->users_needing_recompute($db, $dbh, $DB); | |
| 42 | my $n_recomp = 0; | |
| 43 | foreach my $uid (@stale) { | |
| 44 | my $bytes = $sto->recompute_usage($db, $dbh, $DB, $uid); | |
| 45 | $n_recomp++; | |
| 46 | # log only periodically to keep the log file small | |
| 47 | print " recomputed uid=$uid -> $bytes bytes\n" if $n_recomp <= 25; | |
| 48 | } | |
| 49 | print "[$when] recomputed $n_recomp user(s)\n"; | |
| 50 | ||
| 51 | # ---------------------------------------------------------------------- | |
| 52 | # 2) Soft alerts -- email + notifications row for anyone over threshold. | |
| 53 | # ---------------------------------------------------------------------- | |
| 54 | my @needing = $sto->users_needing_soft_alert($db, $dbh, $DB); | |
| 55 | my $brand = $config->settings('brand_name') || 'ShopCart'; | |
| 56 | my $n_sent = 0; | |
| 57 | ||
| 58 | foreach my $u (@needing) { | |
| 59 | my $uid = $u->{uid}; | |
| 60 | my $email = $u->{email} || ''; | |
| 61 | next unless $email && $email =~ /\@/; | |
| 62 | ||
| 63 | my $used_gb = sprintf('%.1f GB', $u->{used_bytes} / 1073741824); | |
| 64 | my $cap_gb = sprintf('%.0f GB', $u->{hard_cap_bytes} / 1073741824); | |
| 65 | my $pct = $u->{pct}; | |
| 66 | ||
| 67 | # In-app notification row | |
| 68 | my $title = "$brand: storage at $pct% of your $u->{plan_tier} plan cap"; | |
| 69 | my $body = "You've used $used_gb of your $cap_gb storage cap (about $pct\%).\n\n" | |
| 70 | . "Once you hit 100\%, new uploads will be blocked until you free up space or upgrade.\n\n" | |
| 71 | . "Free space: delete unused models or product images on your dashboard.\n" | |
| 72 | . "Upgrade: visit your Billing page to raise your cap."; | |
| 73 | my $title_q = $dbh->quote($title); | |
| 74 | my $body_q = $dbh->quote($body); | |
| 75 | my $uid_q = $dbh->quote($uid); | |
| 76 | $db->db_readwrite($dbh, qq~ | |
| 77 | INSERT INTO `${DB}`.notifications SET | |
| 78 | user_id = $uid_q, | |
| 79 | kind = 'storage_soft_alert', | |
| 80 | title = $title_q, | |
| 81 | body = $body_q, | |
| 82 | link_url = '/billing.cgi', | |
| 83 | created_at = NOW() | |
| 84 | ~, 'storage_alert_worker.pl', __LINE__); | |
| 85 | ||
| 86 | # Outbound email | |
| 87 | eval { | |
| 88 | MODS::ShopCart::Mailer->new->send( | |
| 89 | to => $email, | |
| 90 | subject => 'Storage usage approaching your plan limit', | |
| 91 | body_text => "Hi,\n\n" | |
| 92 | . "Your $brand storage is at $pct% of your $u->{plan_tier} plan cap " | |
| 93 | . "($used_gb of $cap_gb used).\n\n" | |
| 94 | . "Once you reach 100% of the cap, new uploads will be rejected " | |
| 95 | . "until you free up space or upgrade your plan.\n\n" | |
| 96 | . "Free space: open your dashboard and delete unused models or images.\n" | |
| 97 | . "Upgrade your plan: https://shop.3dshawn.com/billing.cgi\n\n" | |
| 98 | . "Thanks,\n" | |
| 99 | . "The $brand team\n", | |
| 100 | ); | |
| 101 | }; | |
| 102 | if ($@) { | |
| 103 | print " email failed for uid=$uid ($email): $@\n"; | |
| 104 | } | |
| 105 | ||
| 106 | # Debounce so we don't re-fire next hour | |
| 107 | $sto->stamp_soft_alert_sent($db, $dbh, $DB, $uid); | |
| 108 | $n_sent++; | |
| 109 | print " alerted uid=$uid email=$email used=$used_gb pct=$pct\%\n"; | |
| 110 | } | |
| 111 | print "[$when] sent $n_sent soft alert(s)\n"; | |
| 112 | ||
| 113 | $db->db_disconnect($dbh); | |
| 114 | print "[$when] storage_alert_worker end\n"; | |
| 115 | exit 0; |