Shrine直传S3后MIME类型变为application/octet-stream且转存失败
我们用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。
解决方案思路
禁用缓存文件的MIME类型自动检测
determine_mime_type插件会在附件赋值时重新检测MIME类型,覆盖前端传入的值。可修改插件配置,仅对直接上传的文件(而非缓存文件)进行检测:# 在shrine.rb中修改 Shrine.plugin :determine_mime_type, cache: false限制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验证S3预签名请求的Content-Type参数
检查预签名lambda中request.params['type']是否正确获取到前端传递的application/pdf,确保S3缓存文件的Content-Type元数据被正确设置。可在lambda中添加日志输出确认参数值。检查动态存储配置的影响
确认dynamic_storage和default_storage的配置在promote阶段没有干扰元数据的传递,确保存储切换时文件元数据被完整保留。
内容的提问来源于stack exchange,提问作者Aurel

