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

Shrine直传S3后MIME类型变为application/octet-stream且转存失败

问题:Shrine+Uppy S3直传后MIME类型识别错误导致无法转存

我们用Shrine和Uppy搭建了S3直传功能,PDF文件可正常上传到S3缓存文件夹,但赋值给Doc模型时出现两个问题:

  • 创建的Doc对象file_data列未正确识别MIME类型,被标记为application/octet-stream
  • 上述问题导致S3缓存文件无法被转存(promote)

已确认S3 CORS、环境变量配置及权限均正常,此前配置运行稳定。


相关代码片段

shrine.rb

require "shrine/storage/s3"

s3_options = {
  access_key_id:     ENV['AWS_ACCESS_KEY_ID'],
  secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
  region:            ENV['AWS_REGION'],
  bucket:            ENV['AWS_BUCKET'],
}

Shrine.storages = {
  cache: Shrine::Storage::S3.new(prefix: "cache", **s3_options), # For direct uploads
  store: Shrine::Storage::S3.new(public: true, prefix: "store", **s3_options)
}

Shrine.plugin :activerecord
Shrine.plugin :cached_attachment_data
Shrine.plugin :restore_cached_data
Shrine.plugin :determine_mime_type
Shrine.plugin :backgrounding
Shrine.plugin :remove_invalid
Shrine.plugin :presign_endpoint, presign_options: lambda { |request|
  filename = request.params['filename']
  type     = request.params['type']
{
    content_disposition: "inline; filename=\"#{filename}\"",
    content_type: type,
    content_length_range: 0..(15 * 1024 * 1024)
  }
}

file_uploader.rb

class FileUploader < Shrine
  require "content_disposition"

  plugin :validation
  plugin :validation_helpers
  plugin :derivatives

  Attacher.validate do
    validate_max_size 9.megabyte, message: 'Votre fichier ne doit pas dépasser 9Mo.'
  end

  plugin :default_storage, store: -> {
    org = record.documentable_type.constantize.find(record.documentable_id).organization
    if record.documentable_type == 'Organization'
      org = record.documentable_type.constantize
      "store_organizations/#{record.documentable_id}/documents"
    elsif record.documentable_type == 'Entity'
      "store_entities/#{record.documentable_id}/documents"
    elsif record.documentable_type == 'Project'
      "store_projects/#{record.documentable_id}/documents"
    end
  }

  plugin :dynamic_storage
  storage /store_(.+)/ do |match|
    bucket_name = "#{match[1]}"
    Shrine::Storage::S3.new(bucket: ENV["AWS_BUCKET"], prefix: bucket_name)
  end
end

upload_controller.js(Uppy部分)

[...]

this.uppy
      .use(DragDrop, {
        target: '.file-dropzone .for-DragDrop',
        }
      })

// Load to S3 cache folder -> OK
      .use(AwsS3, {
        companionUrl: this.urlValue,
        allowedMetaFields: [
          'name'
        ]
      })

// Construct uploaded file data in the format that Shrine expects
      .on('upload-success', function (file, data) {
        var filename = file.name
        var uploadedFileData = JSON.stringify({
          id: file.meta['key'].match(/^cache\/(.+)/)[1], // object key without prefix
          storage: 'cache',
          metadata: {
            size: file.size,
            filename: file.name,
            mime_type: "application/pdf" // file.type
          }
        })

// docs_controller#direct_upload
        fetch(postUrl, {
          method: "POST",
          credentials: "same-origin",
          body: JSON.stringify({
            filename: filename,
            file: uploadedFileData,
          }),
          headers: {
            'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
            'Content-Type': 'application/json'
          },
        })
      })

[...]

doc.rb

class Doc < ApplicationRecord
  include ActionView::Helpers::TagHelper
  include FileUploader::Attachment.new(:file)
end

docs_controller.rb

[...]

  def direct_upload
    # binding.pry
    @doc = Doc.new(
      name: params[:filename],
      file: params[:file],
    )
  end

[...]

排查信息

通过binding.pry确认,params[:file]中的mime_type为application/pdf,但赋值给Doc对象后,file_data列中的mime_type变为application/octet-stream。


解决方案思路

  1. 禁用缓存文件的MIME类型自动检测
    determine_mime_type插件会在附件赋值时重新检测MIME类型,覆盖前端传入的值。可修改插件配置,仅对直接上传的文件(而非缓存文件)进行检测:

    # 在shrine.rb中修改
    Shrine.plugin :determine_mime_type, cache: false
    
  2. 限制restore_cached_data的元数据恢复范围
    restore_cached_data插件会从S3对象元数据中恢复信息,如果S3缓存文件的Content-Type是application/octet-stream,会覆盖前端传入的值。可配置插件仅恢复指定元数据:

    # 在shrine.rb中修改
    Shrine.plugin :restore_cached_data, metadata: [:size, :filename] # 排除mime_type
    
  3. 验证S3预签名请求的Content-Type参数
    检查预签名lambda中request.params['type']是否正确获取到前端传递的application/pdf,确保S3缓存文件的Content-Type元数据被正确设置。可在lambda中添加日志输出确认参数值。

  4. 检查动态存储配置的影响
    确认dynamic_storage和default_storage的配置在promote阶段没有干扰元数据的传递,确保存储切换时文件元数据被完整保留。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 05:37:18