001/* 002 * Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com). 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.agentsflex.core.file2text.source; 017 018 019import com.agentsflex.core.file2text.util.IOUtils; 020 021import java.io.*; 022import java.nio.file.Files; 023import java.util.Objects; 024import java.util.logging.Logger; 025 026/** 027 * 将输入流保存到临时文件的 DocumentSource 028 * 适用于大文件,避免内存溢出 029 * 线程安全,文件名唯一,支持自动清理 030 */ 031public class TemporaryFileStreamDocumentSource implements DocumentSource { 032 private static final Logger log = Logger.getLogger(TemporaryFileStreamDocumentSource.class.getName()); 033 private static final long DEFAULT_MAX_SIZE = 100 * 1024 * 1024; // 100MB 034 035 private final File tempFile; 036 private final String fileName; 037 private final String mimeType; 038 039 /** 040 * 创建临时文件源(默认最大 100MB) 041 */ 042 public TemporaryFileStreamDocumentSource(InputStream inputStream, String fileName, String mimeType) throws IOException { 043 this(inputStream, fileName, mimeType, DEFAULT_MAX_SIZE); 044 } 045 046 /** 047 * 创建临时文件源(可指定最大大小) 048 * 049 * @param inputStream 输入流 050 * @param fileName 建议文件名(用于日志和扩展名推断) 051 * @param mimeType MIME 类型(可选) 052 * @param maxSize 最大允许大小(字节) 053 * @throws IOException 文件过大或 I/O 错误 054 */ 055 public TemporaryFileStreamDocumentSource( 056 InputStream inputStream, 057 String fileName, 058 String mimeType, 059 long maxSize) throws IOException { 060 061 Objects.requireNonNull(inputStream, "InputStream cannot be null"); 062 063 this.fileName = sanitizeFileName(fileName); 064 this.mimeType = mimeType; 065 066 // 推断后缀(用于调试) 067 String suffix = inferSuffix(this.fileName); 068 069 // 创建唯一临时文件 070 this.tempFile = File.createTempFile("doc-", suffix); 071 this.tempFile.deleteOnExit(); // JVM 退出时清理 072 073 log.info("Creating temp file for " + this.fileName + ": " + tempFile.getAbsolutePath()); 074 075 // 复制流(带大小限制) 076 try (FileOutputStream fos = new FileOutputStream(tempFile)) { 077 IOUtils.copyStream(inputStream, fos, maxSize); 078 } catch (IOException e) { 079 // 清理失败的临时文件 080 boolean deleted = tempFile.delete(); 081 log.warning("Failed to write temp file, deleted: " + deleted); 082 throw e; 083 } 084 085 log.fine("Temp file created: " + tempFile.length() + " bytes"); 086 } 087 088 @Override 089 public String getFileName() { 090 return fileName; 091 } 092 093 @Override 094 public String getMimeType() { 095 return mimeType; 096 } 097 098 099 @Override 100 public InputStream openStream() throws IOException { 101 if (!tempFile.exists()) { 102 throw new FileNotFoundException("Temp file not found: " + tempFile.getAbsolutePath()); 103 } 104 return Files.newInputStream(tempFile.toPath()); 105 } 106 107 @Override 108 public void cleanup() { 109 if (tempFile.exists()) { 110 boolean deleted = tempFile.delete(); 111 if (!deleted) { 112 log.warning("Failed to delete temp file: " + tempFile.getAbsolutePath()); 113 } else { 114 log.fine("Cleaned up temp file: " + tempFile.getAbsolutePath()); 115 } 116 } 117 } 118 119 // ======================== 120 // 工具方法 121 // ======================== 122 123 /** 124 * 推断文件后缀(用于临时文件命名,便于调试) 125 */ 126 private String inferSuffix(String fileName) { 127 if (fileName == null || !fileName.contains(".")) { 128 return ".tmp"; 129 } 130 int lastDot = fileName.lastIndexOf('.'); 131 String ext = fileName.substring(lastDot); // 包含 . 132 if (ext.length() > 1 && ext.length() <= 10 && ext.matches("\\.[a-zA-Z0-9]{1,10}")) { 133 return ext; 134 } 135 return ".tmp"; 136 } 137 138 /** 139 * 清理文件名中的非法字符 140 */ 141 private String sanitizeFileName(String fileName) { 142 if (fileName == null) return "unknown"; 143 return fileName 144 .replaceAll("[\\\\/:*?\"<>|]", "_") 145 .replaceAll("\\.\\.", "_") 146 .replaceAll("^\\s+|\\s+$", "") 147 .isEmpty() ? "file" : fileName; 148 } 149}