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}