datai/datai-scenes/datai-scene-salesforce/docs/prompts/017-file-storage-and-extract.md
Kris 2e6f087732 docs: 完成REQ-010-17和REQ-010-2的文档创建
- 完成REQ-010-17(性能优化和限流处理)的所有6个阶段
  - 创建ADR文档:0026-performance-optimization.md
  - 创建Prompt文档:027-performance-optimization.md
  - 创建会话记录:20260119-performance-optimization.md
  - 创建变更记录:20260119-performance-optimization.md
  - 创建复盘报告:20260119-performance-optimization-retro.md
  - 更新index.md和CHANGELOG.md

- 完成REQ-010-2(基础实体类和Mapper创建)的前3个阶段
  - 更新ADR文档:0011-entity-mapper-create.md
  - 创建Prompt文档:002-entity-mapper-create.md
  - 更新index.md

所有文档均按照SSOT方法论创建,包括需求定义、架构决策、提示词资产化、执行会话、变更记录和闭环复盘。
2026-01-19 10:06:09 +08:00

22 KiB
Raw Blame History

Prompt - 文件存储和解压处理实现

输入引用

引用相关的 docs 文档链接:

Context Maps

强制列出本次 Prompt 依赖的 Canvas 文件:

目标

实现文件存储和解压处理功能,包括:

  1. 实现 Zip 文件存储功能,支持本地文件系统和 OSS 存储
  2. 实现文件解压功能,支持大文件处理和流式解压
  3. 实现文件路径管理,支持路径规范化和安全性验证
  4. 实现存储空间管理,支持存储空间监控和告警
  5. 实现文件清理功能,支持过期文件自动清理和手动清理

输出格式

代码结构

datai-salesforce-metadata/
├── src/main/java/com/datai/salesforce/metadata/file/
│   ├── service/
│   │   ├── IFileStorageService.java          # 文件存储服务接口
│   │   ├── LocalStorageService.java          # 本地文件系统存储实现
│   │   ├── OssStorageService.java            # OSS 存储实现
│   │   ├── IFileExtractService.java          # 文件解压服务接口
│   │   ├── FileExtractServiceImpl.java       # 文件解压服务实现
│   │   ├── IPathValidator.java               # 路径验证接口
│   │   ├── PathValidator.java                # 路径验证实现
│   │   ├── IStorageMonitorService.java       # 存储空间监控服务接口
│   │   ├── StorageMonitorServiceImpl.java    # 存储空间监控服务实现
│   │   ├── IFileCleanupService.java          # 文件清理服务接口
│   │   └── FileCleanupServiceImpl.java       # 文件清理服务实现
│   ├── dto/
│   │   ├── FileStorageRequest.java           # 文件存储请求
│   │   ├── FileStorageResponse.java          # 文件存储响应
│   │   ├── FileExtractRequest.java           # 文件解压请求
│   │   ├── FileExtractResponse.java          # 文件解压响应
│   │   ├── ExtractProgressCallback.java      # 解压进度回调
│   │   ├── StorageSpaceInfo.java             # 存储空间信息
│   │   ├── FileCleanupRequest.java           # 文件清理请求
│   │   └── FileCleanupResponse.java          # 文件清理响应
│   ├── enums/
│   │   ├── StorageType.java                  # 存储类型枚举
│   │   └── ExtractStatus.java                # 解压状态枚举
│   ├── config/
│   │   ├── FileStorageConfig.java            # 文件存储配置
│   │   └── OssConfig.java                    # OSS 配置
│   ├── exception/
│   │   ├── FileStorageException.java        # 文件存储异常
│   │   ├── FileExtractException.java        # 文件解压异常
│   │   └── PathSecurityException.java       # 路径安全异常
│   └── util/
│       ├── FileUtil.java                     # 文件工具类
│       └── ZipUtil.java                      # Zip 工具类
├── src/main/resources/
│   ├── application.yml                       # 应用配置
│   └── logback-spring.xml                    # 日志配置
└── src/test/java/com/datai/salesforce/metadata/file/
    ├── service/
    │   ├── LocalStorageServiceTest.java      # 本地存储服务测试
    │   ├── FileExtractServiceTest.java      # 文件解压服务测试
    │   ├── PathValidatorTest.java             # 路径验证测试
    │   ├── StorageMonitorServiceTest.java    # 存储监控服务测试
    │   └── FileCleanupServiceTest.java       # 文件清理服务测试
    └── util/
        ├── FileUtilTest.java                 # 文件工具类测试
        └── ZipUtilTest.java                  # Zip 工具类测试

代码示例

1. IFileStorageService.java

package com.datai.salesforce.metadata.file.service;

import java.io.InputStream;
import java.nio.file.Path;

public interface IFileStorageService {
    
    String store(String fileName, InputStream inputStream, String subPath);
    
    InputStream retrieve(String filePath);
    
    boolean delete(String filePath);
    
    boolean exists(String filePath);
    
    Path getStoragePath(String subPath);
}

2. LocalStorageService.java

package com.datai.salesforce.metadata.file.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;

import java.io.*;
import java.nio.file.*;

@Slf4j
@Service
@ConditionalOnProperty(name = "file.storage.type", havingValue = "local")
public class LocalStorageService implements IFileStorageService {
    
    @Value("${file.storage.local.base-path}")
    private String basePath;
    
    @Override
    public String store(String fileName, InputStream inputStream, String subPath) {
        try {
            Path targetPath = Paths.get(basePath, subPath);
            Files.createDirectories(targetPath);
            
            Path filePath = targetPath.resolve(fileName);
            Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING);
            
            log.info("File stored successfully: {}", filePath);
            return filePath.toString();
        } catch (IOException e) {
            log.error("Failed to store file: {}", fileName, e);
            throw new RuntimeException("Failed to store file", e);
        }
    }
    
    @Override
    public InputStream retrieve(String filePath) {
        try {
            return Files.newInputStream(Paths.get(filePath));
        } catch (IOException e) {
            log.error("Failed to retrieve file: {}", filePath, e);
            throw new RuntimeException("Failed to retrieve file", e);
        }
    }
    
    @Override
    public boolean delete(String filePath) {
        try {
            boolean deleted = Files.deleteIfExists(Paths.get(filePath));
            log.info("File deleted: {}, result: {}", filePath, deleted);
            return deleted;
        } catch (IOException e) {
            log.error("Failed to delete file: {}", filePath, e);
            return false;
        }
    }
    
    @Override
    public boolean exists(String filePath) {
        return Files.exists(Paths.get(filePath));
    }
    
    @Override
    public Path getStoragePath(String subPath) {
        return Paths.get(basePath, subPath);
    }
}

3. IFileExtractService.java

package com.datai.salesforce.metadata.file.service;

import java.io.InputStream;

public interface IFileExtractService {
    
    void extract(InputStream zipInputStream, String targetPath);
    
    void extract(InputStream zipInputStream, String targetPath, ExtractProgressCallback callback);
    
    void extract(InputStream zipInputStream, String targetPath, ExtractProgressCallback callback, boolean cancelFlag);
}

4. FileExtractServiceImpl.java

package com.datai.salesforce.metadata.file.service;

import com.datai.salesforce.metadata.file.dto.ExtractProgressCallback;
import com.datai.salesforce.metadata.file.exception.FileExtractException;
import com.datai.salesforce.metadata.file.util.PathValidator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.*;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

@Slf4j
@Service
public class FileExtractServiceImpl implements IFileExtractService {
    
    @Autowired
    private PathValidator pathValidator;
    
    private static final int BUFFER_SIZE = 8192;
    
    @Override
    public void extract(InputStream zipInputStream, String targetPath) {
        extract(zipInputStream, targetPath, null);
    }
    
    @Override
    public void extract(InputStream zipInputStream, String targetPath, ExtractProgressCallback callback) {
        extract(zipInputStream, targetPath, callback, false);
    }
    
    @Override
    public void extract(InputStream zipInputStream, String targetPath, ExtractProgressCallback callback, boolean cancelFlag) {
        Path basePath = Paths.get(targetPath);
        
        try {
            Files.createDirectories(basePath);
            
            try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(zipInputStream))) {
                ZipEntry entry;
                int totalEntries = 0;
                int processedEntries = 0;
                
                while ((entry = zis.getNextEntry()) != null) {
                    if (cancelFlag) {
                        log.info("Extract cancelled");
                        break;
                    }
                    
                    totalEntries++;
                    Path entryPath = basePath.resolve(entry.getName());
                    
                    pathValidator.validatePath(basePath, entryPath);
                    
                    if (entry.isDirectory()) {
                        Files.createDirectories(entryPath);
                    } else {
                        Files.createDirectories(entryPath.getParent());
                        
                        try (BufferedOutputStream bos = new BufferedOutputStream(
                                new FileOutputStream(entryPath.toFile()), BUFFER_SIZE)) {
                            byte[] buffer = new byte[BUFFER_SIZE];
                            int len;
                            while ((len = zis.read(buffer)) > 0) {
                                bos.write(buffer, 0, len);
                            }
                        }
                    }
                    
                    processedEntries++;
                    if (callback != null) {
                        callback.onProgress(processedEntries, totalEntries, entry.getName());
                    }
                    
                    zis.closeEntry();
                }
            }
            
            log.info("Extract completed: {} entries processed", processedEntries);
            
            if (callback != null) {
                callback.onComplete(processedEntries);
            }
        } catch (IOException e) {
            log.error("Failed to extract file", e);
            throw new FileExtractException("Failed to extract file", e);
        }
    }
}

5. PathValidator.java

package com.datai.salesforce.metadata.file.util;

import com.datai.salesforce.metadata.file.exception.PathSecurityException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.regex.Pattern;

@Slf4j
@Component
public class PathValidator {
    
    private static final Pattern FILENAME_PATTERN = Pattern.compile(
            "^[a-zA-Z0-9._\\-]+$"
    );
    
    private static final Pattern PATH_PATTERN = Pattern.compile(
            "^[a-zA-Z0-9._\\-/\\\\]+$"
    );
    
    public void validatePath(Path basePath, Path targetPath) {
        Path normalizedPath = targetPath.normalize();
        
        if (!normalizedPath.startsWith(basePath.normalize())) {
            log.error("Path traversal attempt detected: {}", targetPath);
            throw new PathSecurityException("Path traversal attempt detected");
        }
        
        if (targetPath.toString().contains("..")) {
            log.error("Path contains '..': {}", targetPath);
            throw new PathSecurityException("Path contains '..'");
        }
        
        String fileName = targetPath.getFileName().toString();
        if (!FILENAME_PATTERN.matcher(fileName).matches()) {
            log.error("Invalid filename: {}", fileName);
            throw new PathSecurityException("Invalid filename");
        }
    }
    
    public void validatePathString(String path) {
        if (!PATH_PATTERN.matcher(path).matches()) {
            log.error("Invalid path: {}", path);
            throw new PathSecurityException("Invalid path");
        }
        
        if (path.contains("..")) {
            log.error("Path contains '..': {}", path);
            throw new PathSecurityException("Path contains '..'");
        }
    }
    
    public Path normalizePath(Path path) {
        return path.normalize();
    }
}

6. StorageMonitorServiceImpl.java

package com.datai.salesforce.metadata.file.service;

import com.datai.salesforce.metadata.file.dto.StorageSpaceInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.io.File;
import java.nio.file.FileStore;
import java.nio.file.Files;

@Slf4j
@Service
public class StorageMonitorServiceImpl implements IStorageMonitorService {
    
    @Autowired
    private INotificationService notificationService;
    
    @Value("${file.storage.warning-threshold:80}")
    private int warningThreshold;
    
    @Value("${file.storage.local.base-path}")
    private String basePath;
    
    @Override
    public StorageSpaceInfo getStorageSpaceInfo() {
        try {
            File file = new File(basePath);
            FileStore store = Files.getFileStore(file.toPath());
            
            long totalSpace = store.getTotalSpace();
            long usableSpace = store.getUsableSpace();
            long usedSpace = totalSpace - usableSpace;
            
            double usagePercentage = (double) usedSpace / totalSpace * 100;
            
            StorageSpaceInfo info = new StorageSpaceInfo();
            info.setTotalSpace(totalSpace);
            info.setUsedSpace(usedSpace);
            info.setUsableSpace(usableSpace);
            info.setUsagePercentage(usagePercentage);
            info.setWarning(usagePercentage >= warningThreshold);
            
            return info;
        } catch (IOException e) {
            log.error("Failed to get storage space info", e);
            throw new RuntimeException("Failed to get storage space info", e);
        }
    }
    
    @Scheduled(cron = "${file.storage.monitor.cron:0 0 * * * ?}")
    public void monitorStorageSpace() {
        StorageSpaceInfo info = getStorageSpaceInfo();
        
        if (info.isWarning()) {
            log.warn("Storage space usage is high: {}%", info.getUsagePercentage());
            notificationService.sendNotification(
                    "Storage space warning",
                    String.format("Storage space usage is %.2f%%", info.getUsagePercentage())
            );
        }
    }
}

7. FileCleanupServiceImpl.java

package com.datai.salesforce.metadata.file.service;

import com.datai.salesforce.metadata.file.dto.FileCleanupRequest;
import com.datai.salesforce.metadata.file.dto.FileCleanupResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class FileCleanupServiceImpl implements IFileCleanupService {
    
    @Autowired
    private IFileStorageService fileStorageService;
    
    @Value("${file.storage.local.base-path}")
    private String basePath;
    
    @Value("${file.cleanup.retention-days:30}")
    private int retentionDays;
    
    @Override
    public FileCleanupResponse cleanupFiles(FileCleanupRequest request) {
        FileCleanupResponse response = new FileCleanupResponse();
        List<String> deletedFiles = new ArrayList<>();
        long totalSize = 0;
        
        try {
            Path baseDir = Paths.get(basePath);
            Instant cutoffTime = Instant.now().minus(retentionDays, ChronoUnit.DAYS);
            
            Files.walk(baseDir)
                    .filter(Files::isRegularFile)
                    .filter(path -> {
                        try {
                            FileTime lastModifiedTime = Files.getLastModifiedTime(path);
                            return lastModifiedTime.toInstant().isBefore(cutoffTime);
                        } catch (IOException e) {
                            log.error("Failed to get last modified time for: {}", path, e);
                            return false;
                        }
                    })
                    .forEach(path -> {
                        try {
                            long size = Files.size(path);
                            if (fileStorageService.delete(path.toString())) {
                                deletedFiles.add(path.toString());
                                totalSize += size;
                                log.info("Deleted file: {}", path);
                            }
                        } catch (IOException e) {
                            log.error("Failed to delete file: {}", path, e);
                        }
                    });
            
            response.setDeletedCount(deletedFiles.size());
            response.setDeletedFiles(deletedFiles);
            response.setTotalSize(totalSize);
            response.setSuccess(true);
            
            log.info("File cleanup completed: {} files deleted, total size: {} bytes",
                    deletedFiles.size(), totalSize);
            
        } catch (IOException e) {
            log.error("Failed to cleanup files", e);
            response.setSuccess(false);
            response.setErrorMessage(e.getMessage());
        }
        
        return response;
    }
    
    @Scheduled(cron = "${file.cleanup.cron:0 0 2 * * ?}")
    public void scheduledCleanup() {
        log.info("Starting scheduled file cleanup");
        FileCleanupRequest request = new FileCleanupRequest();
        request.setRetentionDays(retentionDays);
        cleanupFiles(request);
    }
}

8. application.yml

file:
  storage:
    type: local  # local or oss
    local:
      base-path: /data/metadata
    oss:
      endpoint: https://oss-cn-hangzhou.aliyuncs.com
      access-key-id: ${OSS_ACCESS_KEY_ID}
      access-key-secret: ${OSS_ACCESS_KEY_SECRET}
      bucket-name: datai-metadata
      base-path: metadata/
    warning-threshold: 80
    monitor:
      cron: "0 0 * * * ?"
  cleanup:
    retention-days: 30
    cron: "0 0 2 * * ?"

约束

技术栈约束

  • 必须基于 Spring Boot 3
  • 必须使用 Java 17+
  • 必须使用 MyBatis Plus
  • 必须使用 MySQL

性能约束

  • 文件存储速度:本地文件系统 > 50MB/sOSS > 20MB/s
  • 文件解压速度 > 30MB/s
  • 内存使用 < 512MB
  • 支持大文件处理(> 100MB

安全性约束

  • 必须验证路径安全性,防止路径遍历攻击
  • 必须验证文件名格式,防止文件名注入攻击
  • 必须限制文件大小,防止 DoS 攻击
  • 必须记录所有文件操作日志

兼容性约束

  • 必须支持 Windows 和 Linux 操作系统
  • 必须支持 Java NIO 的 Path API
  • 必须支持 OSS SDK

Rule Set

"请严格参考 @Authentication.canvas 中的状态机转移逻辑,不要自行发挥。"

具体规则

  • 必须使用 Canvas 中定义的类名和方法名
  • 必须遵循 Canvas 中定义的调用关系
  • 必须参考 Canvas 中的流程图逻辑
  • 必须使用策略模式实现文件存储
  • 必须使用流式处理实现文件解压
  • 必须使用 Path API 处理路径
  • 必须使用定时任务实现存储空间监控和文件清理

验收标准

功能验收标准

  • 本地文件系统存储成功
  • OSS 存储成功
  • 文件解压成功
  • 支持大文件处理(> 100MB
  • 支持流式解压
  • 内存使用合理(< 512MB
  • 文件路径管理正常工作
  • 路径安全性验证通过
  • 存储空间监控正常工作
  • 文件清理功能正常工作

性能验收标准

  • 本地文件系统存储速度 > 50MB/s
  • OSS 存储速度 > 20MB/s
  • 解压速度 > 30MB/s
  • 并发存储 10 个文件无性能下降
  • 并发解压 5 个文件无性能下降

安全性验收标准

  • 防止路径遍历攻击
  • 防止文件名注入攻击
  • 防止符号链接攻击
  • 文件操作日志记录完整

代码质量验收标准

  • 代码符合项目编码规范
  • 有清晰的注释
  • 单元测试覆盖率 > 80%
  • 集成测试覆盖主要场景
  • 通过代码审查

风险

大文件处理风险

  • 风险描述: 大文件处理不当可能导致内存溢出
  • 缓解措施: 使用流式处理,限制内存占用
  • 监控指标: 监控 JVM 内存使用情况

路径安全风险

  • 风险描述: 路径验证不完善可能导致路径遍历攻击
  • 缓解措施: 使用 Path API 和正则表达式双重验证
  • 监控指标: 记录所有文件操作日志

OSS 存储风险

  • 风险描述: OSS 存储不稳定可能导致文件存储失败
  • 缓解措施: 实现重试机制和降级策略
  • 监控指标: 监控 OSS 存储成功率和响应时间

存储空间风险

  • 风险描述: 存储空间不足可能导致文件存储失败
  • 缓解措施: 实现存储空间监控和告警机制
  • 监控指标: 监控存储空间使用率

文件清理风险

  • 风险描述: 文件清理策略不当可能导致重要文件被删除
  • 缓解措施: 清理前记录日志,支持文件恢复
  • 监控指标: 记录清理操作日志

使用记录

日期 使用场景 输入参数 输出结果 反馈 改进措施
2026-01-19 实现文件存储和解压处理功能 REQ-010-7 需求文档、ADR-0016 决策文档 完整的代码实现和测试用例 待使用 待改进