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

如何用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 04:24:47