{"record":{"id":"51d1db5b6b83e09b","repo":"instructure/canvas-lms","slug":"failed-to-reclaim-attachment-attachment-global-id-attachment","errorCode":null,"errorMessage":"failed to reclaim attachment #{attachment.global_id}: #{attachment.errors.inspect}","messagePattern":"failed to reclaim attachment #(.+?): #(.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"lib/data_fixup/reclaim_instfs_attachments.rb","lineNumber":41,"sourceCode":"  # inst-fs enabled and had files uploaded to inst-fs need to stop using\n  # inst-fs for whatever reasons (note this is beyond just stopping new uploads\n  # to inst-fs).\n  def self.run(root_accounts)\n    Shard.partition_by_shard(root_accounts) do |shard_root_accounts|\n      instfs_attachments_for_root_accounts(shard_root_accounts).find_each do |attachment|\n        reclaim_attachment(attachment)\n      end\n    end\n  end\n\n  def self.reclaim_attachment(attachment)\n    # NOTE: this downloads the whole attachment at once into a temp file.\n    # unfortunately, this is unavoidable with how attachment_fu works\n    attachment.uploaded_data = attachment.open\n    attachment.instfs_uuid = nil\n    unless attachment.save\n      # continue with other attachments, but log this one for investigation\n      Rails.logger.warn(\"failed to reclaim attachment #{attachment.global_id}: #{attachment.errors.inspect}\")\n    end\n  end\n\n  def self.instfs_attachments_for_root_accounts(root_accounts)\n    # between all the subqueries below, we have all the enumerated context\n    # types for an attachment from attachment.rb lines 51-59 except:\n    #   * other attachments (recursive)\n    #   * eportfolios\n    #   * purgatory\n    #   * users\n    #   * specific instances of other context types (e.g. folders) that are\n    #     connected to a user instead of to an account or course\n    #\n    # we have to punt on those because either:\n    #   * unfolding the recursion would be prohibitive. there aren't many of\n    #     these anyways\n    #   * for the others, there's no way to connect the attachment to a\n    #     specific account","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/data_fixup/reclaim_instfs_attachments.rb#L23-L59","documentation":"Logged by the DataFixup::ReclaimInstfsAttachments fixup when an attachment previously stored in InstFS is re-uploaded locally (attachment.uploaded_data = attachment.open, instfs_uuid cleared) but the subsequent save fails ActiveRecord validations. The fixup logs the attachment's global_id plus attachment.errors and continues with the remaining attachments.","triggerScenarios":"attachment.save fails after re-uploading the file and clearing instfs_uuid — e.g. validation errors from invalid context/content_type, the source file being unreadable/empty during attachment.open, or quota/record-level validations failing on that Attachment row.","commonSituations":"Attachments whose file data is missing from InstFS (open raises or yields empty data), attachments belonging to soft-deleted or invalid contexts, corrupted rows failing presence/size validations, or S3/InstFS connectivity problems during the download step.","solutions":["Read the attachment.errors in the log line to see which validation failed, and fix the underlying record (e.g. restore context, fix content_type).","Verify the file exists and is retrievable from InstFS for the logged global_id; re-upload the missing object if the store lost it.","Rerun the fixup after correcting the record so it gets reclaimed.","If the file is unrecoverable, delete or mark the attachment (or skip it) so it stops failing on each run."],"exampleFix":"// before (console inspection)\natt = Attachment.find(attachment.global_id); pp att.errors\n// after — fix the failing field, then retry\natt.update!(context: Context.find(...)); DataFixup::ReclaimInstfsAttachments.run","handlingStrategy":"validation","validationCode":"att = Attachment.find(global_id)\nnext unless att.file_state != 'deleted'\nraise \"source file missing\" unless att.instfs_uuid.present? && att.open.present?\npp att.errors # inspect before attempting reclaim","typeGuard":"def reclaimable?(attachment)\n  attachment.instfs_uuid.present? &&\n    attachment.valid? &&\n    attachment.context_type.present?\nrescue StandardError\n  false\nend","tryCatchPattern":"begin\n  attachment.uploaded_data = attachment.open\n  attachment.instfs_uuid = nil\n  attachment.save!\nrescue ActiveRecord::RecordInvalid, OpenURI::HTTPError => e\n  Rails.logger.warn(\"skipping #{attachment.global_id}: #{e.message}\")\nend","preventionTips":["Fix reported validation errors (attachment.errors) before rerunning the fixup","Verify InstFS objects exist and are downloadable for the target attachment set","Run the fixup per root account in small batches to isolate failing records","Check context validity (not soft-deleted) for attachments before reclaiming"],"tags":["data-fixup","attachments","instfs","active-record-validation"],"backgroundTag":"schema-validation-failed","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}