如何用ActiveStorage上传文件夹并保留原结构?获取webkitRelativePath遇阻
解决ActiveStorage Direct Upload无法保留文件夹结构的问题
我之前也踩过这个坑——ActiveStorage默认的direct_upload确实没考虑文件夹上传的场景,没法自动获取webkitRelativePath来保留原有的目录结构。不过咱们可以通过前端扩展上传逻辑+后端自定义存储服务的组合方案解决,一步步来:
一、前端修改:捕获webkitRelativePath并传递给后端
默认的ActiveStorage DirectUpload类不会把文件的webkitRelativePath作为元数据传给后端,所以我们需要重写它的createBlob方法,把这个路径加进去。同时,手动处理文件选择后的上传流程,最后把生成的Blob ID加到表单里提交。
1. 调整表单代码
先去掉file_field_tag上的direct_upload: true,因为我们要自己处理上传逻辑:
<%= form_tag('/action', method: "post", id:"myForm") do %> <%= file_field_tag(:file, webkitdirectory: '', directory: '', multiple: true) %> <%= submit_tag("Submit", id:"submit_btn", disabled: true) %> <% end %>
(把提交按钮设为禁用,等所有文件上传完成后再启用)
2. 自定义上传逻辑的JS代码
在页面里添加这段JavaScript,监听文件选择事件,处理每个文件的direct upload并携带相对路径:
// 监听文件选择事件 document.querySelector('input[type="file"]').addEventListener('change', function(e) { const files = Array.from(e.target.files); let uploadCount = files.length; let completedUploads = 0; const submitBtn = document.getElementById('submit_btn'); submitBtn.disabled = true; files.forEach(file => { // 初始化DirectUpload实例 const upload = new ActiveStorage.DirectUpload(file, '/rails/active_storage/direct_uploads', window); // 重写createBlob方法,把webkitRelativePath加入metadata const originalCreateBlob = upload.createBlob; upload.createBlob = function() { return originalCreateBlob.call(this).then(blob => { blob.metadata = { relative_path: file.webkitRelativePath }; return blob; }); }; // 发起上传 upload.create((error, blob) => { completedUploads += 1; if (error) { console.error(`上传文件 ${file.name} 失败: ${error.message}`); } else { // 把Blob ID添加到表单的隐藏字段中,方便后端接收 const hiddenInput = document.createElement('input'); hiddenInput.type = 'hidden'; hiddenInput.name = 'blob_ids[]'; hiddenInput.value = blob.id; document.getElementById('myForm').appendChild(hiddenInput); } // 所有文件上传完成后启用提交按钮 if (completedUploads === uploadCount) { submitBtn.disabled = false; } }); }); });
二、后端处理:根据RelativePath创建目录结构
前端把相对路径传到后端后,还需要让ActiveStorage的存储服务(本地磁盘或S3)根据这个路径来存储文件,而不是用默认的随机key。我们可以通过自定义存储服务来实现。
1. 本地磁盘存储的自定义服务
创建一个继承自DiskService的自定义服务,重写upload方法来替换存储路径:
# config/initializers/active_storage.rb class LocalWithPathStorage < ActiveStorage::Service::DiskService def upload(key, io, checksum: nil, **options) # 从上传选项中取出前端传递的relative_path relative_path = options[:metadata]&.dig(:relative_path) if relative_path # 拼接新的存储key:保留原key的目录部分,加上相对路径 # 这里可以加安全校验,比如避免路径遍历 safe_relative_path = relative_path.gsub(/^\//, '') # 去掉开头的斜杠 new_key = File.join(File.dirname(key), safe_relative_path) super(new_key, io, checksum: checksum, **options) else # 没有相对路径就用默认逻辑 super end end end # 替换默认的存储服务 ActiveStorage::Blob.service = LocalWithPathStorage.new(root: Rails.root.join("storage"))
2. S3存储的自定义服务
如果用的是S3,类似地继承S3Service重写upload方法:
# config/initializers/active_storage.rb class S3WithPathStorage < ActiveStorage::Service::S3Service def upload(key, io, checksum: nil, **options) relative_path = options[:metadata]&.dig(:relative_path) if relative_path safe_relative_path = relative_path.gsub(/^\//, '') new_key = File.join(File.dirname(key), safe_relative_path) super(new_key, io, checksum: checksum, **options) else super end end end
然后修改config/storage.yml的配置,使用这个自定义服务:
amazon: service: S3WithPathStorage access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %> secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %> bucket: <%= ENV['AWS_BUCKET'] %> region: <%= ENV['AWS_REGION'] %>
三、注意事项
- 兼容性:
webkitRelativePath是Chrome、Edge、Safari等webkit内核浏览器支持的属性,Firefox也已支持directory属性,大部分现代浏览器都没问题。 - 安全校验:一定要对
relative_path做安全处理(比如去掉开头的斜杠),避免恶意用户通过路径遍历上传文件到敏感目录。 - 变体处理:如果使用ActiveStorage的变体(Variants),变体的存储路径会基于原始Blob的key生成,所以只要原始Blob的key包含相对路径,变体也会自动继承目录结构。
内容的提问来源于stack exchange,提问作者Sandeep Garg
相关产品推荐
相关产品推荐

