@@ -47,27 +47,39 @@ def handle(self, *args, **options):
4747
4848 total_updated = 0
4949 total_skipped = 0
50+ total_errors = 0
5051 for model in self .MODELS :
51- updated , skipped = self ._backfill_model (model , dry_run )
52+ updated , skipped , errors = self ._backfill_model (model , dry_run )
5253 total_updated += updated
5354 total_skipped += skipped
55+ total_errors += errors
5456
5557 verb = "Would update" if dry_run else "Updated"
5658 _logger .info (
5759 f"backfill_original_filenames: { verb } { total_updated } filename "
5860 f"field(s); skipped { total_skipped } (already standardized — original "
59- f"name unrecoverable)."
61+ f"name unrecoverable); { total_errors } row(s) errored and were skipped ."
6062 )
6163 _logger .debug ("Completed backfill_original_filenames.py" )
6264
6365 def _backfill_model (self , model , dry_run ):
6466 """Backfill both file fields for one concrete artifact model.
6567
66- Returns a ``(num_updated, num_skipped)`` tuple counting individual
67- filename fields touched / deliberately left blank.
68+ Returns a ``(num_updated, num_skipped, num_errors)`` tuple counting
69+ individual filename fields touched / deliberately left blank / skipped
70+ because the row raised.
71+
72+ Each row is processed inside its own try/except: this iterates the
73+ whole production dataset, and a single malformed row (e.g. a null
74+ ``date`` or ``title`` makes ``generate_filename`` raise) must not abort
75+ the entire backfill and leave every later row untouched. The entrypoint
76+ has no ``set -e``, so an aborted run would fail silently (the traceback
77+ prints but startup continues) — this per-row isolation is defensive
78+ insurance against that.
6879 """
6980 num_updated = 0
7081 num_skipped = 0
82+ num_errors = 0
7183 for file_attr , original_attr in self .FILE_FIELDS :
7284 # Only rows that have a file but no captured original name yet.
7385 candidates = (
@@ -80,52 +92,73 @@ def _backfill_model(self, model, dry_run):
8092 candidates = candidates .prefetch_related ("authors" )
8193
8294 for artifact in candidates :
83- file_field = getattr (artifact , file_attr )
84- if not file_field :
85- continue
86-
87- current_basename = os .path .basename (file_field .name )
88- current_no_ext = os .path .splitext (current_basename )[0 ]
89- standardized_no_ext = Artifact .generate_filename (artifact )
90-
91- # Treat the file as already-standardized when its name equals the
92- # standardized scheme OR is a uniquified variant of it. When a
93- # standardized name collides on disk, ensure_filename_is_unique()
94- # (fileutils.py) appends "-<timestamp>" — e.g.
95- # "Lee_Talk_CHI2021-1782399772.42.pdf" — so the on-disk name
96- # still STARTS WITH the standardized base. Matching only on exact
97- # equality would misread those as never-renamed and record the
98- # standardized+suffix name as the "original" — a false positive.
99- already_standardized = (
100- current_no_ext == standardized_no_ext
101- or current_no_ext .startswith (standardized_no_ext + "-" )
102- )
103- if already_standardized :
104- # Already renamed — the original upload name is gone.
105- _logger .debug (
106- f"Skipping { model .__name__ } id={ artifact .pk } { file_attr } ="
107- f"'{ current_basename } ': already standardized."
95+ try :
96+ if self ._backfill_row (model , artifact , file_attr ,
97+ original_attr , dry_run ):
98+ num_updated += 1
99+ else :
100+ num_skipped += 1
101+ except Exception :
102+ # Log and move on — never let one row kill the batch.
103+ _logger .exception (
104+ "backfill_original_filenames: skipping %s id=%s %s due "
105+ "to an error" , model .__name__ ,
106+ getattr (artifact , "pk" , "?" ), file_attr ,
108107 )
109- num_skipped += 1
110- continue
111-
112- # Never renamed: the current on-disk name is the original.
113- if dry_run :
114- _logger .debug (
115- f"[dry-run] Would set { original_attr } ='{ current_basename } ' "
116- f"for { model .__name__ } id={ artifact .pk } '{ artifact .title } '"
117- )
118- else :
119- # Write directly via the queryset so this stays a pure data
120- # backfill — no file-rename / thumbnail side effects from the
121- # model's save().
122- model .objects .filter (pk = artifact .pk ).update (
123- ** {original_attr : current_basename }
124- )
125- _logger .debug (
126- f"Set { original_attr } ='{ current_basename } ' for "
127- f"{ model .__name__ } id={ artifact .pk } '{ artifact .title } '"
128- )
129- num_updated += 1
108+ num_errors += 1
109+
110+ return num_updated , num_skipped , num_errors
111+
112+ def _backfill_row (self , model , artifact , file_attr , original_attr , dry_run ):
113+ """Backfill one (artifact, file field) pair.
114+
115+ Returns True if the original name was (or would be) recorded, False if
116+ the row was deliberately skipped because its file is already
117+ standardized. Raises on malformed data — the caller isolates that.
118+ """
119+ file_field = getattr (artifact , file_attr )
120+ if not file_field :
121+ return False
122+
123+ current_basename = os .path .basename (file_field .name )
124+ current_no_ext = os .path .splitext (current_basename )[0 ]
125+ standardized_no_ext = Artifact .generate_filename (artifact )
126+
127+ # Treat the file as already-standardized when its name equals the
128+ # standardized scheme OR is a uniquified variant of it. When a
129+ # standardized name collides on disk, ensure_filename_is_unique()
130+ # (fileutils.py) appends "-<timestamp>" — e.g.
131+ # "Lee_Talk_CHI2021-1782399772.42.pdf" — so the on-disk name still
132+ # STARTS WITH the standardized base. Matching only on exact equality
133+ # would misread those as never-renamed and record the standardized+
134+ # suffix name as the "original" — a false positive.
135+ already_standardized = (
136+ current_no_ext == standardized_no_ext
137+ or current_no_ext .startswith (standardized_no_ext + "-" )
138+ )
139+ if already_standardized :
140+ # Already renamed — the original upload name is gone.
141+ _logger .debug (
142+ f"Skipping { model .__name__ } id={ artifact .pk } { file_attr } ="
143+ f"'{ current_basename } ': already standardized."
144+ )
145+ return False
130146
131- return num_updated , num_skipped
147+ # Never renamed: the current on-disk name is the original.
148+ if dry_run :
149+ _logger .debug (
150+ f"[dry-run] Would set { original_attr } ='{ current_basename } ' "
151+ f"for { model .__name__ } id={ artifact .pk } '{ artifact .title } '"
152+ )
153+ else :
154+ # Write directly via the queryset so this stays a pure data
155+ # backfill — no file-rename / thumbnail side effects from the
156+ # model's save().
157+ model .objects .filter (pk = artifact .pk ).update (
158+ ** {original_attr : current_basename }
159+ )
160+ _logger .debug (
161+ f"Set { original_attr } ='{ current_basename } ' for "
162+ f"{ model .__name__ } id={ artifact .pk } '{ artifact .title } '"
163+ )
164+ return True
0 commit comments