Skip to content

feat: 从服务器导入文件(引用模式) #167

@sharkAndshark

Description

@sharkAndshark

背景

用户上传大文件时,通过浏览器上传可能很慢或失败。如果文件已经在服务器上,应该可以直接引用导入,而不是重新上传。

⚠️ 前置条件

此功能需要等待 #166 (Workspace feature) 合并后再开始实现。

原因:

  • 文件导入需要关联到当前 workspace
  • 依赖 files.workspace_id 字段和 workspace 切换机制

需求

在 workspace 中增加"从服务器导入"功能,允许用户直接引用服务器本地目录中的空间数据文件,无需上传。

设计决策

决策 结论
目录归属 全局数据目录(环境变量配置,默认 ./data/
权限 所有 workspace member 可操作
文件处理 仅引用(不复制,节省磁盘和时间)
UI 入口 独立按钮"从服务器导入"

UI 设计

文件列表页

┌─────────────────────────────────────────────────┐
│  MapFlow                    [工作空间 ▼] [⚙️]   │
├─────────────────────────────────────────────────┤
│                                                  │
│  📁 文件列表                                     │
│  ┌────────────────────────────────────────────┐ │
│  │ 📄 chengdu_districts.geojson    ✅ 已就绪  │ │
│  │ 📄 sichuan_roads.pmtiles        ✅ 已就绪  │ │
│  └────────────────────────────────────────────┘ │
│                                                  │
│  [📤 上传文件]  [💾 从服务器导入]    ← 独立按钮 │
└─────────────────────────────────────────────────┘

点击"从服务器导入"后弹出

┌─────────────────────────────────────────────────┐
│ 💾 从服务器导入                              ✕  │
├─────────────────────────────────────────────────┤
│  ⚠️ 文件将被引用,不会被复制。原文件变更会影响数据。│
├─────────────────────────────────────────────────┤
│  📂 /data/maps                          [🔍]   │
│  ────────────────────────────────────────────  │
│  📁 china_province/                   >        │
│  📁 sichuan_cities/                   >        │
│  ☑ 📄 chengdu_buildings.geojson    128 MB     │
│  ☑ 📄 roads_2024.shp.zip           2.1 GB     │
│  ☐ 📄 census_2020.geojson           45 MB     │
│                                                  │
│  已选择 2 个文件 (共 2.2 GB)                    │
├─────────────────────────────────────────────────┤
│                     [取消]  [导入选中文件]      │
└─────────────────────────────────────────────────┘

导入后

┌─────────────────────────────────────────────────┐
│ 📄 chengdu_buildings.geojson                   │
│    状态: 🔄 处理中                              │
│    来源: 📂 /data/maps (引用)                   │
└─────────────────────────────────────────────────┘

API 设计

1. 获取可导入目录列表

GET /api/server-files/directories

Response:

{
  "directories": [
    { "path": "/data/maps", "name": "maps" },
    { "path": "/data/backup", "name": "backup" }
  ]
}

2. 浏览目录内容

GET /api/server-files/browse?path=/data/maps

Response:

{
  "currentPath": "/data/maps",
  "parentPath": "/data",
  "items": [
    { "name": "china_province", "type": "directory" },
    { "name": "sichuan_cities", "type": "directory" },
    { "name": "chengdu_buildings.geojson", "type": "file", "size": 134217728, "ext": ".geojson" },
    { "name": "roads_2024.shp.zip", "type": "file", "size": 2254857830, "ext": ".zip" }
  ]
}

3. 导入文件(引用)

POST /api/server-files/import

Request:

{
  "files": [
    { "path": "/data/maps/chengdu_buildings.geojson" },
    { "path": "/data/maps/roads_2024.shp.zip" }
  ]
}

Response:

{
  "imported": [
    { "id": "uuid-1", "name": "chengdu_buildings", "path": "/data/maps/chengdu_buildings.geojson", "status": "processing" },
    { "id": "uuid-2", "name": "roads_2024", "path": "/data/maps/roads_2024.shp.zip", "status": "uploaded" }
  ]
}

行为边界

安全

规则 描述
路径白名单 只能访问配置的数据目录及其子目录
路径遍历防护 拒绝 ..、符号链接跳出白名单
文件类型验证 与上传一致(.geojson, .zip, .mbtiles, .pmtiles 等)
权限检查 必须登录且属于当前 workspace

文件处理

规则 描述
引用模式 文件路径直接存入 files.path,不复制
相对路径 存储相对于数据目录的路径,便于迁移
原文件检测 导入时检查文件是否存在且可读
状态流转 uploaded → processing → ready/failed(与上传一致)

错误处理

场景 响应
路径不在白名单 403 Forbidden
文件不存在 404 Not Found
文件类型不支持 400 Bad Request
原文件被删除/移动 瓦片请求返回 404,状态标记为 unavailable

配置

# 环境变量
SERVER_DATA_DIRS=/data/maps,/data/backup  # 逗号分隔的白名单目录

数据库变更

-- files 表增加字段标记来源
ALTER TABLE files ADD COLUMN source_type VARCHAR DEFAULT 'upload';
-- source_type: 'upload' | 'server_import'

测试用例

后端测试

#[test]
async fn test_import_server_file_success() {
    // 导入白名单内的文件 → 成功
}

#[test]
async fn test_import_file_outside_whitelist() {
    // 尝试导入白名单外的文件 → 403
}

#[test]
async fn test_import_with_path_traversal() {
    // 尝试使用 .. 跳出白名单 → 403
}

#[test]
async fn test_import_unsupported_file_type() {
    // 尝试导入 .txt 文件 → 400
}

#[test]
async fn test_import_nonexistent_file() {
    // 文件路径不存在 → 404
}

#[test]
async fn test_browse_directory() {
    // 浏览目录 → 返回正确的文件/文件夹列表
}

#[test]
async fn test_workspace_isolation() {
    // 导入的文件只对当前 workspace 可见
}

E2E 测试

test '可以从服务器导入文件', async ({ page }) => {
  // 1. 点击"从服务器导入"按钮
  // 2. 浏览目录
  // 3. 选择文件
  // 4. 确认导入
  // 5. 验证文件出现在列表中
});

相关

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions