From be17196ef3c9888167bf587013383c21da6f7ebf Mon Sep 17 00:00:00 2001 From: Charles7c Date: Mon, 1 Dec 2025 22:06:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=96=B0=E5=A2=9E=20MultipartFil?= =?UTF-8?q?eUtils=EF=BC=8C=E6=94=AF=E6=8C=81=20File=20=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E4=B8=BA=20MultipartFile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- continew-starter-core/pom.xml | 7 + .../util/multipart/CommonsMultipartFile.java | 238 ++++++++++++++++++ .../util/multipart/MultipartFileUtils.java | 96 +++++++ continew-starter-dependencies/pom.xml | 9 + 4 files changed, 350 insertions(+) create mode 100644 continew-starter-core/src/main/java/top/continew/starter/core/util/multipart/CommonsMultipartFile.java create mode 100644 continew-starter-core/src/main/java/top/continew/starter/core/util/multipart/MultipartFileUtils.java diff --git a/continew-starter-core/pom.xml b/continew-starter-core/pom.xml index 3047b454..1bdad921 100644 --- a/continew-starter-core/pom.xml +++ b/continew-starter-core/pom.xml @@ -63,6 +63,13 @@ mica-ip2region + + + commons-fileupload + commons-fileupload + true + + cn.hutool diff --git a/continew-starter-core/src/main/java/top/continew/starter/core/util/multipart/CommonsMultipartFile.java b/continew-starter-core/src/main/java/top/continew/starter/core/util/multipart/CommonsMultipartFile.java new file mode 100644 index 00000000..bf6c2d70 --- /dev/null +++ b/continew-starter-core/src/main/java/top/continew/starter/core/util/multipart/CommonsMultipartFile.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.continew.starter.core.util.multipart; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItem; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.log.LogFormatUtils; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * {@link MultipartFile} implementation for Apache Commons FileUpload. + *

+ * Spring Boot3(Spring 6)移除了 CommonsMultipartFile 和 CommonsMultipartResolver,彻底废弃了对 Apache Commons FileUpload 的依赖,只保留基于 + * Servlet 3.1+ 的标准上传机制 StandardMultipartFile。 + *
为了方便在项目中进行 MultipartFile 格式转换,特备份该实现。 + *

+ * + * @author Trevor D. Cook + * @author Juergen Hoeller + * @author Charles7c + * @since 2.15.0 + */ +@SuppressWarnings("serial") +public class CommonsMultipartFile implements MultipartFile, Serializable { + + protected static final Log logger = LogFactory.getLog(CommonsMultipartFile.class); + + private final FileItem fileItem; + + private final long size; + + private boolean preserveFilename = false; + + /** + * Create an instance wrapping the given FileItem. + * + * @param fileItem the FileItem to wrap + */ + public CommonsMultipartFile(FileItem fileItem) { + this.fileItem = fileItem; + this.size = this.fileItem.getSize(); + } + + /** + * Return the underlying {@code org.apache.commons.fileupload.FileItem} + * instance. There is hardly any need to access this. + */ + public final FileItem getFileItem() { + return this.fileItem; + } + + /** + * Set whether to preserve the filename as sent by the client, not stripping off + * path information in {@link CommonsMultipartFile#getOriginalFilename()}. + *

Default is "false", stripping off path information that may prefix the + * actual filename e.g. from Opera. Switch this to "true" for preserving the + * client-specified filename as-is, including potential path separators. + * + * @since 4.3.5 + * @see #getOriginalFilename() + */ + public void setPreserveFilename(boolean preserveFilename) { + this.preserveFilename = preserveFilename; + } + + @Override + public String getName() { + return this.fileItem.getFieldName(); + } + + @Override + public String getOriginalFilename() { + String filename = this.fileItem.getName(); + if (filename == null) { + // Should never happen. + return ""; + } + if (this.preserveFilename) { + // Do not try to strip off a path... + return filename; + } + + // Check for Unix-style path + int unixSep = filename.lastIndexOf('/'); + // Check for Windows-style path + int winSep = filename.lastIndexOf('\\'); + // Cut off at latest possible point + int pos = Math.max(winSep, unixSep); + if (pos != -1) { + // Any sort of path separator found... + return filename.substring(pos + 1); + } else { + // A plain name + return filename; + } + } + + @Override + public String getContentType() { + return this.fileItem.getContentType(); + } + + @Override + public boolean isEmpty() { + return (this.size == 0); + } + + @Override + public long getSize() { + return this.size; + } + + @Override + public byte[] getBytes() { + if (!isAvailable()) { + throw new IllegalStateException("File has been moved - cannot be read again"); + } + byte[] bytes = this.fileItem.get(); + return (bytes != null ? bytes : new byte[0]); + } + + @Override + public InputStream getInputStream() throws IOException { + if (!isAvailable()) { + throw new IllegalStateException("File has been moved - cannot be read again"); + } + InputStream inputStream = this.fileItem.getInputStream(); + return (inputStream != null ? inputStream : InputStream.nullInputStream()); + } + + @Override + public void transferTo(File dest) throws IOException, IllegalStateException { + if (!isAvailable()) { + throw new IllegalStateException("File has already been moved - cannot be transferred again"); + } + + if (dest.exists() && !dest.delete()) { + throw new IOException("Destination file [" + dest + .getAbsolutePath() + "] already exists and could not be deleted"); + } + + try { + this.fileItem.write(dest); + LogFormatUtils.traceDebug(logger, traceOn -> { + String action = "transferred"; + if (!this.fileItem.isInMemory()) { + action = (isAvailable() ? "copied" : "moved"); + } + return "Part '" + getName() + "', filename '" + getOriginalFilename() + "'" + (Boolean.TRUE + .equals(traceOn) ? ", stored " + getStorageDescription() : "") + ": " + action + " to [" + dest + .getAbsolutePath() + "]"; + }); + } catch (FileUploadException ex) { + throw new IllegalStateException(ex.getMessage(), ex); + } catch (IllegalStateException | IOException ex) { + // Pass through IllegalStateException when coming from FileItem directly, + // or propagate an exception from I/O operations within FileItem.write + throw ex; + } catch (Exception ex) { + throw new IOException("File transfer failed", ex); + } + } + + @Override + public void transferTo(Path dest) throws IOException, IllegalStateException { + if (!isAvailable()) { + throw new IllegalStateException("File has already been moved - cannot be transferred again"); + } + + FileCopyUtils.copy(this.fileItem.getInputStream(), Files.newOutputStream(dest)); + } + + /** + * Determine whether the multipart content is still available. + * If a temporary file has been moved, the content is no longer available. + */ + protected boolean isAvailable() { + // If in memory, it's available. + if (this.fileItem.isInMemory()) { + return true; + } + // Check actual existence of temporary file. + if (this.fileItem instanceof DiskFileItem df) { + return df.getStoreLocation().exists(); + } + // Check whether current file size is different than original one. + return (this.fileItem.getSize() == this.size); + } + + /** + * Return a description for the storage location of the multipart content. + * Tries to be as specific as possible: mentions the file location in case + * of a temporary file. + */ + public String getStorageDescription() { + if (this.fileItem.isInMemory()) { + return "in memory"; + } else if (this.fileItem instanceof DiskFileItem df) { + return "at [" + df.getStoreLocation().getAbsolutePath() + "]"; + } else { + return "on disk"; + } + } + + @Override + public String toString() { + return "MultipartFile[field=\"" + this.fileItem.getFieldName() + "\"" + (this.fileItem.getName() != null + ? ", filename=" + this.fileItem.getName() + : "") + (this.fileItem.getContentType() != null + ? ", contentType=" + this.fileItem.getContentType() + : "") + ", size=" + this.fileItem.getSize() + "]"; + } +} \ No newline at end of file diff --git a/continew-starter-core/src/main/java/top/continew/starter/core/util/multipart/MultipartFileUtils.java b/continew-starter-core/src/main/java/top/continew/starter/core/util/multipart/MultipartFileUtils.java new file mode 100644 index 00000000..4ce79f41 --- /dev/null +++ b/continew-starter-core/src/main/java/top/continew/starter/core/util/multipart/MultipartFileUtils.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.continew.starter.core.util.multipart; + +import cn.hutool.core.io.IoUtil; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.springframework.http.MediaType; +import org.springframework.web.multipart.MultipartFile; +import top.continew.starter.core.exception.BaseException; + +import java.io.*; +import java.nio.file.Files; + +/** + * MultipartFile 工具类 + * + * @author Charles7c + * @since 2.15.0 + */ +public class MultipartFileUtils { + + private MultipartFileUtils() { + } + + /** + * 转换为 MultipartFile + * + * @param file 文件 + * @return MultipartFile + */ + public static MultipartFile toMultipartFile(File file) throws IOException { + FileItem fileItem = createFileItem(Files.newInputStream(file.toPath()), file.getName()); + return new CommonsMultipartFile(fileItem); + } + + /** + * 转换为 MultipartFile + * + * @param bytes 文件字节 + * @param fileName 文件名 + * @return MultipartFile + */ + public static MultipartFile toMultipartFile(byte[] bytes, String fileName) { + FileItem fileItem = createFileItem(new ByteArrayInputStream(bytes), fileName); + return new CommonsMultipartFile(fileItem); + } + + /** + * 创建 FileItem + * + * @param is 输入流 + * @param fileName 文件名 + * @return FileItem + */ + public static FileItem createFileItem(InputStream is, String fileName) { + return createFileItem(is, "file", fileName, MediaType.MULTIPART_FORM_DATA_VALUE); + } + + /** + * 创建 FileItem + * + * @param is 输入流 + * @param fieldName 字段名 + * @param fileName 文件名 + * @param contentType 内容类型 + * @return FileItem + */ + public static FileItem createFileItem(InputStream is, String fieldName, String fileName, String contentType) { + DiskFileItemFactory factory = new DiskFileItemFactory(); + FileItem fileItem = factory.createItem(fieldName, contentType, true, fileName); + // 拷贝流 + try (OutputStream os = fileItem.getOutputStream()) { + IoUtil.copy(is, os); + } catch (IOException e) { + throw new BaseException("创建文件项失败", e); + } finally { + IoUtil.close(is); + } + return fileItem; + } +} diff --git a/continew-starter-dependencies/pom.xml b/continew-starter-dependencies/pom.xml index 5ad07a6a..6f173371 100644 --- a/continew-starter-dependencies/pom.xml +++ b/continew-starter-dependencies/pom.xml @@ -80,6 +80,8 @@ 2.5 15.7 + 1.6.0 + 1.11.0 2.20.0 @@ -404,6 +406,13 @@ ${nashorn.version} + + + commons-fileupload + commons-fileupload + ${commons-fileupload.version} + + commons-beanutils