You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rails ActiveStorage在Sidekiq任务中出现文件找不到错误

ActiveStorage 集成 Sidekiq 后头像附件文件找不到问题解决

问题背景

原本用以下代码处理用户头像上传,运行完全正常:

if params[:avatar]
  authorize! :update, current_user
  type, base64_image = Utils::Attach.type_and_base64(params[:avatar], 'avatar')
  current_user.avatar.attach(io: StringIO.new(base64_image), filename: "#{current_user.id}-#{Time.current.to_i}", content_type: type)
  current_user.save!
end

但给邮件服务集成Sidekiq后,执行头像附件操作时频繁出现文件找不到的错误。

核心原因分析

  1. 异步任务触发过早:ActiveStorage还没把文件完全写入存储,Sidekiq的邮件任务就已经启动,尝试读取还不存在的文件
  2. 进程间存储配置不一致:如果用本地存储,Sidekiq进程和主应用进程的文件目录权限或路径不匹配;如果用云存储,可能存在文件同步延迟,或者Sidekiq进程的云存储配置有误
  3. StringIO临时对象的局限性:直接用StringIO上传,可能在异步场景下出现文件未持久化完成的情况

解决方案

方案一:确保附件持久化后再触发异步任务

不要在请求周期内直接调用Sidekiq邮件任务,改用after_commit回调,等数据库和存储操作都彻底完成后再触发:

修改上传代码:

if params[:avatar]
  authorize! :update, current_user
  type, base64_image = Utils::Attach.type_and_base64(params[:avatar], 'avatar')
  current_user.avatar.attach(io: StringIO.new(base64_image), filename: "#{current_user.id}-#{Time.current.to_i}", content_type: type)
  current_user.save!
  # 用run_after_commit确保所有数据库操作提交后再触发邮件
  current_user.run_after_commit do
    UserMailer.update_avatar_notification(current_user).deliver_later
  end
end

或者在User模型里加回调(更规范):

class User < ApplicationRecord
  has_one_attached :avatar

  # 仅在头像新增/更新且提交完成后发邮件
  after_commit :send_avatar_update_email, on: :update, if: -> { avatar.attached? && avatar.saved_change_to_attached? }

  private

  def send_avatar_update_email
    UserMailer.update_avatar_notification(self).deliver_later
  end
end

方案二:统一Sidekiq与主进程的存储配置

  • 本地存储:检查config/storage.yml里的:local配置,确保Sidekiq进程有权限读写存储目录(比如public/storage),且路径和主应用完全一致
  • 云存储:确认Sidekiq进程加载了正确的云存储密钥、区域等配置,必要时在邮件任务里加短暂延迟(比如sleep 1),或者利用云存储的上传完成回调触发邮件

方案三:改用临时文件上传替代StringIO

把base64内容先写入本地临时文件,再上传到ActiveStorage,避免StringIO在异步场景下的不稳定:

if params[:avatar]
  authorize! :update, current_user
  type, base64_image = Utils::Attach.type_and_base64(params[:avatar], 'avatar')
  # 创建临时文件,后缀匹配文件类型
  ext = type.split('/').last
  temp_file = Tempfile.new(["#{current_user.id}-#{Time.current.to_i}", ".#{ext}"], Rails.root.join('tmp'))
  begin
    temp_file.write(base64_image)
    temp_file.rewind # 把文件指针移到开头,确保能被读取
    current_user.avatar.attach(io: temp_file, filename: temp_file.path.split('/').last, content_type: type)
    current_user.save!
  ensure
    temp_file.close
    temp_file.unlink # 用完删除临时文件
  end
end

内容的提问来源于stack exchange,提问作者vladislav

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 00:56:03