+ * 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 格式转换,特备份该实现。
+ *
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 @@