Commit fbf5228
authored
fix: user photos survive redeploys — boot-time upload self-heal (#397)
## The bug
User avatars and cover images disappeared for everyone on **every
deploy**. This started after migrating to a replacement Railway service.
## Root cause
Avatars/covers are stored on the upload volume (not R2-primary); the DB
only holds the relative path (`/uploads/...`). On a service whose
uploads land on **ephemeral container disk** — no persistent volume
attached, or `ALLOW_EPHEMERAL_UPLOADS=true` — each redeploy starts with
an empty uploads dir while the rows still point at the files, so every
image 404s platform-wide. The volume→R2 mirror already existed, but
recovery was a **manual script** and the first backup pass only fired
24h after boot, so nothing self-healed.
## Fix (self-healing, defense in depth)
- **`uploadVolumeRestore.js`** (new): shared restore core — lists the R2
mirror and writes back any file missing locally (skip-if-exists,
traversal-guarded, **streams to a `.tmp` then atomic-renames** so an
OOM/eviction mid-write can't leave a truncated file). `restoreOnBoot()`
runs it non-blocking on startup (heartbeat-wrapped) so a fresh/wiped
container re-hydrates its photos with **no operator action**. A healthy
persistent volume pays only the R2 LIST cost.
- **`restoreVolumeFromR2.js`** (manual DR CLI) now delegates to that
core — the traversal guards live in one place, not two copies.
- **First backup pass ~2 min after boot**
(`UPLOAD_BACKUP_FIRST_PASS_DELAY_MS`) instead of a full interval later.
- **`storage.js`**: ephemeral-storage-in-production now logs an error +
Sentry alert (`storage.ephemeral_in_production`) instead of a silent
info line.
- `secretValidator` + `.env.example` document the two new env vars.
The durable config (persistent `/data` volume + all four R2 vars) is an
**operator action** documented in `RUNBOOK_DB_RESTORE.md` ("Photos
vanish on EVERY redeploy"). The code makes it self-healing once R2 is
configured; files that only ever lived on the wiped ephemeral disk can't
be recovered and need re-upload.
## Tests
- New `uploadVolumeRestore.test.js`: restore, skip-if-exists, force,
traversal refusal, pagination, dry-run, boot-enable policy.
- Full backend suite green (**3514 passed**); reviewed by code-reviewer
(2 findings — heap-OOM + non-atomic write — both fixed before commit).
## Summary by Sourcery
Add a shared upload-volume restore core and boot-time self-healing to
ensure user-uploaded files are automatically rehydrated from the R2
mirror after redeploys, and surface misconfigured ephemeral storage and
backup timing more loudly.
Bug Fixes:
- Ensure user avatars, covers, and other uploads are restored from the
R2 mirror on service startup so they no longer disappear after redeploys
when the uploads directory has been wiped or remounted.
Enhancements:
- Extract upload-volume restore logic into a reusable job module shared
by the disaster-recovery CLI and boot-time self-heal, including
traversal guards, streaming writes, and atomic file replacement.
- Run the first upload-volume backup pass shortly after boot instead of
waiting a full interval, reducing the window where new uploads exist
only on local disk.
- Raise a prominent error log and Sentry alert when running with
ephemeral upload storage in production instead of only logging at info
level.
Documentation:
- Document the new upload backup/restore configuration flags and the
photo-loss scenario in the release log and secret/env documentation.
Tests:
- Add unit tests for the upload-volume restore job covering restore
behavior, skip/force semantics, traversal protection, pagination,
dry-run mode, and boot-enable policy.
- Update storage validation tests to assert the new error-level logging
and Sentry alert when ephemeral storage is enabled in production.1 parent 8c14f52 commit fbf5228
10 files changed
Lines changed: 508 additions & 129 deletions
File tree
- backend
- scripts
- src
- lib
- jobs
- test
- unit
- docs
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
383 | 383 | | |
384 | 384 | | |
385 | 385 | | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
5 | 10 | | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
22 | 15 | | |
23 | 16 | | |
24 | 17 | | |
| |||
27 | 20 | | |
28 | 21 | | |
29 | 22 | | |
30 | | - | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
31 | 26 | | |
32 | 27 | | |
33 | 28 | | |
34 | 29 | | |
35 | 30 | | |
36 | 31 | | |
37 | | - | |
| 32 | + | |
38 | 33 | | |
39 | | - | |
40 | 34 | | |
41 | | - | |
42 | 35 | | |
43 | 36 | | |
44 | 37 | | |
45 | | - | |
46 | | - | |
47 | 38 | | |
48 | 39 | | |
49 | 40 | | |
| |||
55 | 46 | | |
56 | 47 | | |
57 | 48 | | |
58 | | - | |
59 | | - | |
| 49 | + | |
60 | 50 | | |
61 | 51 | | |
62 | 52 | | |
63 | 53 | | |
64 | 54 | | |
65 | 55 | | |
66 | | - | |
67 | | - | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
76 | | - | |
77 | | - | |
78 | | - | |
79 | | - | |
80 | 56 | | |
| 57 | + | |
| 58 | + | |
81 | 59 | | |
82 | 60 | | |
83 | 61 | | |
84 | 62 | | |
85 | 63 | | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | | - | |
122 | | - | |
123 | | - | |
124 | | - | |
125 | | - | |
126 | | - | |
127 | | - | |
128 | | - | |
129 | | - | |
130 | | - | |
131 | | - | |
132 | | - | |
133 | | - | |
134 | | - | |
135 | | - | |
136 | | - | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
152 | | - | |
153 | | - | |
154 | | - | |
155 | | - | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
| 64 | + | |
164 | 65 | | |
165 | 66 | | |
166 | 67 | | |
167 | | - | |
168 | | - | |
169 | | - | |
170 | | - | |
171 | | - | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
172 | 73 | | |
173 | 74 | | |
174 | 75 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
905 | 905 | | |
906 | 906 | | |
907 | 907 | | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
908 | 921 | | |
909 | 922 | | |
910 | 923 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
202 | 202 | | |
203 | 203 | | |
204 | 204 | | |
205 | | - | |
| 205 | + | |
206 | 206 | | |
207 | 207 | | |
208 | 208 | | |
| |||
211 | 211 | | |
212 | 212 | | |
213 | 213 | | |
214 | | - | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
215 | 228 | | |
216 | 229 | | |
217 | 230 | | |
| |||
0 commit comments