|
25 | 25 | from src.markdown_parser import convert_github_markdown_to_asana_xml |
26 | 26 |
|
27 | 27 | AttachmentData = collections.namedtuple( |
28 | | - "AttachmentData", "file_name file_url file_type" |
| 28 | + "AttachmentData", "file_name file_url file_type original_asset_id" |
29 | 29 | ) |
30 | 30 |
|
31 | 31 | StatusReason = collections.namedtuple("StatusReason", "is_complete reason") |
@@ -272,6 +272,31 @@ def _get_file_extension_from_url(url: str) -> str: |
272 | 272 | return "." + url.split("?")[0].split(".")[-1] |
273 | 273 |
|
274 | 274 |
|
| 275 | +def _get_original_asset_id_from_url(url: str) -> Optional[str]: |
| 276 | + """ |
| 277 | + Extract original asset ID from a URL. |
| 278 | + For GitHub asset URLs, this extracts the UUID: |
| 279 | + - https://api.github.com/assets/long-unique-uuid.png?token=123321 |
| 280 | + - https://github.com/user-attachments/assets/uuid-here |
| 281 | + For non-GitHub URLs, this returns the entire URL as the asset ID. |
| 282 | + """ |
| 283 | + if "api.github.com/assets/" in url: |
| 284 | + # Extract the UUID from URLs like https://api.github.com/assets/long-unique-uuid.png?token=123321 |
| 285 | + parts = url.split("/assets/") |
| 286 | + if len(parts) >= 2: |
| 287 | + asset_part = parts[1].split("?")[0] # Remove query parameters |
| 288 | + asset_id = asset_part.split(".")[0] # Remove file extension |
| 289 | + return asset_id |
| 290 | + elif "github.com/user-attachments/assets/" in url: |
| 291 | + # Extract the UUID from URLs like https://github.com/user-attachments/assets/uuid-here |
| 292 | + parts = url.split("/assets/") |
| 293 | + if len(parts) >= 2: |
| 294 | + return parts[1].split("/")[0] # Take the first part after /assets/ |
| 295 | + |
| 296 | + # For non-GitHub URLs, use the entire URL as the asset ID |
| 297 | + return url |
| 298 | + |
| 299 | + |
275 | 300 | def _extract_attachments(body_html: str) -> List[AttachmentData]: |
276 | 301 | """ |
277 | 302 | Finds, but does not replace, all the image/video attachment URLs in the body_html. Handles: |
@@ -311,29 +336,88 @@ def _extract_attachments(body_html: str) -> List[AttachmentData]: |
311 | 336 | else: |
312 | 337 | file_name = file_title + file_ext |
313 | 338 |
|
| 339 | + # Extract original asset ID |
| 340 | + original_asset_id = _get_original_asset_id_from_url(file_url_str) |
| 341 | + |
314 | 342 | attachments.append( |
315 | 343 | AttachmentData( |
316 | | - file_name=file_name, file_url=file_url_str, file_type=file_type |
| 344 | + file_name=file_name, |
| 345 | + file_url=file_url_str, |
| 346 | + file_type=file_type, |
| 347 | + original_asset_id=original_asset_id, |
317 | 348 | ) |
318 | 349 | ) |
319 | 350 |
|
320 | 351 | return attachments |
321 | 352 |
|
322 | 353 |
|
323 | | -def create_attachments(body_html: str, task_id: str) -> None: |
324 | | - attachments = _extract_attachments(body_html) |
325 | | - for attachment in attachments: |
| 354 | +def sync_attachments(body_html: str, task_id: str, github_node_id: str) -> None: |
| 355 | + """ |
| 356 | + Syncs attachments between GitHub content and Asana task, tracking mappings in DynamoDB. |
| 357 | + This will: |
| 358 | + 1. Extract current attachments from the GitHub HTML |
| 359 | + 2. Compare with existing tracked attachments for this GitHub node |
| 360 | + 3. Delete attachments that are no longer present |
| 361 | + 4. Create new attachments that aren't tracked |
| 362 | + 5. Update the DynamoDB mappings |
| 363 | + """ |
| 364 | + current_attachments = _extract_attachments(body_html) |
| 365 | + existing_attachments = dynamodb_client.get_attachments_for_github_node( |
| 366 | + github_node_id |
| 367 | + ) |
| 368 | + |
| 369 | + # Create sets for comparison |
| 370 | + current_asset_ids = { |
| 371 | + att.original_asset_id for att in current_attachments if att.original_asset_id |
| 372 | + } |
| 373 | + existing_asset_ids = set(existing_attachments.keys()) |
| 374 | + |
| 375 | + # Find attachments to delete (exist in DynamoDB but not in current content) |
| 376 | + assets_to_delete = existing_asset_ids - current_asset_ids |
| 377 | + for asset_id in assets_to_delete: |
| 378 | + asana_attachment_id = existing_attachments[asset_id] |
326 | 379 | try: |
327 | | - with urllib.request.urlopen(attachment.file_url) as f: |
328 | | - attachment_contents = f.read() |
329 | | - asana_client.create_attachment_on_task( |
330 | | - task_id, |
331 | | - attachment_contents, |
332 | | - attachment.file_name, |
333 | | - attachment.file_type, |
| 380 | + asana_client.delete_attachment(asana_attachment_id) |
| 381 | + logger.info( |
| 382 | + f"Deleted attachment {asana_attachment_id} for asset {asset_id}" |
| 383 | + ) |
| 384 | + except Exception as e: |
| 385 | + logger.warning(f"Failed to delete attachment {asana_attachment_id}: {e}") |
| 386 | + |
| 387 | + # Find attachments to create (exist in current content but not in DynamoDB) |
| 388 | + assets_to_create = current_asset_ids - existing_asset_ids |
| 389 | + |
| 390 | + # Build new attachments mapping from current content only |
| 391 | + new_attachments = {} |
| 392 | + |
| 393 | + for attachment in current_attachments: |
| 394 | + if attachment.original_asset_id in assets_to_create: |
| 395 | + # Create new attachment |
| 396 | + try: |
| 397 | + with urllib.request.urlopen(attachment.file_url) as f: |
| 398 | + attachment_contents = f.read() |
| 399 | + asana_attachment_id = asana_client.create_attachment_on_task( |
| 400 | + task_id, |
| 401 | + attachment_contents, |
| 402 | + attachment.file_name, |
| 403 | + attachment.file_type, |
| 404 | + ) |
| 405 | + new_attachments[attachment.original_asset_id] = asana_attachment_id |
| 406 | + logger.info( |
| 407 | + f"Created attachment {asana_attachment_id} for asset {attachment.original_asset_id}" |
| 408 | + ) |
| 409 | + except Exception as e: |
| 410 | + logger.warning( |
| 411 | + f"Attachment creation failed for {attachment.original_asset_id}: {e}" |
334 | 412 | ) |
335 | | - except Exception: |
336 | | - logger.warning("Attachment creation failed. Creating task comment anyway.") |
| 413 | + elif attachment.original_asset_id in existing_attachments: |
| 414 | + # Keep existing attachment |
| 415 | + new_attachments[attachment.original_asset_id] = existing_attachments[ |
| 416 | + attachment.original_asset_id |
| 417 | + ] |
| 418 | + |
| 419 | + # Update the attachments mapping in DynamoDB |
| 420 | + dynamodb_client.update_attachments_for_github_node(github_node_id, new_attachments) |
337 | 421 |
|
338 | 422 |
|
339 | 423 | _review_action_to_text_map: Dict[ReviewState, str] = { |
|
0 commit comments