文章目录
- 一、运行界面
- 1、登录
- 2、展示
- 二、源码传送
- 1、使用技术
- 2、代码结构
- 3、源码
- 三、运行部署
- 1、jar方式
- 2、docker方式
- 3、docker-compose方式
- 四、优化方向
一、运行界面
1、登录
后台查看日志,获取token值
2、展示
批量上传文件或者点击链接下载
二、源码传送
1、使用技术
- springboot、knife4j、zxing(二维码)
- thymeleaf
- docker、docker-compose
2、代码结构
图片请手工放入 src/main/resources/img/dog.jpg
3、源码
如何使用下面的备份文件恢复成原始的项目代码,请移步查阅:神奇代码恢复工具
//goto docker\docker-compose.yml
version: '3'
services:
file-send:
image: registry.cn-shanghai.aliyuncs.com/00fly/springboot-file-send:1.0.0
container_name: file-send
deploy:
resources:
limits:
cpus: '1'
memory: 300M
reservations:
cpus: '0.05'
memory: 200M
ports:
- 80:8080
restart: on-failure
logging:
driver: json-file
options:
max-size: 5m
max-file: '1'
//goto docker\restart.sh
#!/bin/bash
docker-compose down && docker system prune -f && docker-compose --compatibility up -d
//goto docker\stop.sh
#!/bin/bash
docker-compose down
//goto Dockerfile
#基础镜像
FROM openjdk:8-jre-alpine
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
#拷贝发布包
COPY target/*.jar /app.jar
EXPOSE 8080
CMD ["--server.port=8080"]
#启动脚本
ENTRYPOINT ["java","-Xmx128m","-Xms128m","-jar","/app.jar"]
//goto pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.fly</groupId>
<artifactId>springboot-file-send</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.build.timestamp.format>yyyyMMdd-HH</maven.build.timestamp.format>
<docker.hub>registry.cn-shanghai.aliyuncs.com</docker.hub>
<java.version>1.8</java.version>
<skipTests>true</skipTests>
</properties>
<dependencies>
<!-- Compile -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.8</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.0</version>
</dependency>
<!-- Provided -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- 添加docker-maven插件 -->
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.40.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build</goal>
<goal>push</goal>
<goal>remove</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 连接到带docker环境的linux服务器编译image -->
<!--<dockerHost>http://192.168.182.10:2375</dockerHost>-->
<!-- Docker 推送镜像仓库地址 -->
<pushRegistry>${docker.hub}</pushRegistry>
<images>
<image>
<!--推送到私有镜像仓库,镜像名需要添加仓库地址 -->
<name>
${docker.hub}/00fly/${project.artifactId}:${project.version}</name>
<!--定义镜像构建行为 -->
<build>
<dockerFileDir>${project.basedir}</dockerFileDir>
</build>
</image>
</images>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/**</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
//goto src\main\java\com\fly\core\auth\AuthInterceptor.java
package com.fly.core.auth;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fly.core.utils.TokenUtils;
import com.fly.web.entity.JsonResult;
/**
*
* AuthInterceptor
*
* @author 00fly
* @version [版本号, 2019年7月21日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Component
public class AuthInterceptor extends HandlerInterceptorAdapter
{
private ObjectMapper mapper = new ObjectMapper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception
{
String token = (String)request.getSession().getAttribute("token");
if (!TokenUtils.valide(token))
{
JsonResult<?> result = JsonResult.error("系统登录状态失效,请重新登录");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.getWriter().print(mapper.writeValueAsString(result));
return false;
}
return true;
}
}
//goto src\main\java\com\fly\core\config\Knife4jConfig.java
package com.fly.core.config;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import io.swagger.annotations.ApiOperation;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
/**
* Knife4jConfig
*
*/
@EnableKnife4j
@Configuration
@EnableSwagger2WebMvc
@ConditionalOnWebApplication
@Import(BeanValidatorPluginsConfiguration.class)
public class Knife4jConfig
{
/**
* 开发、测试环境接口文档打开
*
* @return
* @see [类、类#方法、类#成员]
*/
@Bean
Docket createRestApi()
{
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.enable(true)
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any()) // 包下的类,生成接口文档
.build()
.securitySchemes(security());
}
private ApiInfo apiInfo()
{
return new ApiInfoBuilder().title("数据接口API").description("接口文档").termsOfServiceUrl("http://00fly.online/").version("1.0.0").build();
}
private List<ApiKey> security()
{
return Collections.singletonList(new ApiKey("token", "token", "header"));
}
}
//goto src\main\java\com\fly\core\config\WebMvcConfig.java
package com.fly.core.config;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.fly.core.auth.AuthInterceptor;
/**
*
* mvc配置
*
* @author 00fly
* @version [版本号, 2021年4月23日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Configuration
@ConditionalOnWebApplication
public class WebMvcConfig implements WebMvcConfigurer
{
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void configureMessageConverters(final List<HttpMessageConverter<?>> converters)
{
converters.add(stringHttpMessageConverter());
converters.add(mappingJackson2HttpMessageConverter());
}
@Override
public void configureContentNegotiation(final ContentNegotiationConfigurer configurer)
{
configurer.defaultContentType(MediaType.APPLICATION_JSON);
configurer.ignoreUnknownPathExtensions(false);
configurer.favorPathExtension(true);
configurer.favorParameter(false);
final Map<String, MediaType> mediaTypes = new ConcurrentHashMap<>(3);
mediaTypes.put("atom", MediaType.APPLICATION_ATOM_XML);
mediaTypes.put("html", MediaType.TEXT_HTML);
mediaTypes.put("json", MediaType.APPLICATION_JSON);
configurer.mediaTypes(mediaTypes);
}
@Bean
StringHttpMessageConverter stringHttpMessageConverter()
{
return new StringHttpMessageConverter();
}
@Bean
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter()
{
final MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
final List<MediaType> list = new ArrayList<>();
list.add(MediaType.APPLICATION_JSON);
list.add(MediaType.APPLICATION_XML);
list.add(MediaType.TEXT_PLAIN);
list.add(MediaType.TEXT_HTML);
list.add(MediaType.TEXT_XML);
messageConverter.setSupportedMediaTypes(list);
return messageConverter;
}
/**
* 等价于mvc中<mvc:view-controller path="/" view-name="redirect:index" /><br>
* 等价于mvc中<mvc:view-controller path="/index" view-name="index.html" />
*
* @param registry
*/
@Override
public void addViewControllers(final ViewControllerRegistry registry)
{
registry.addViewController("/").setViewName("redirect:index");
// registry.addViewController("/index").setViewName("index.html");
}
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(authInterceptor).addPathPatterns("/rest/file/**", "/file/**");
}
}
//goto src\main\java\com\fly\core\exception\GlobalExceptionHandler.java
package com.fly.core.exception;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.fly.web.entity.JsonResult;
import lombok.extern.slf4j.Slf4j;
/**
* 统一异常处理器
*
* @author 00fly
* @version [版本号, 2018-09-11]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler
{
@ExceptionHandler(value = Exception.class)
public JsonResult<?> handleBadRequest(Exception exception)
{
// JSR303参数校验异常
if (exception instanceof BindException)
{
BindingResult bindingResult = ((BindException)exception).getBindingResult();
if (null != bindingResult && bindingResult.hasErrors())
{
List<String> errMsg = new ArrayList<>();
bindingResult.getFieldErrors().stream().forEach(fieldError -> {
errMsg.add(fieldError.getDefaultMessage());
});
Collections.sort(errMsg);
return JsonResult.error(StringUtils.join(errMsg, ","));
}
}
if (exception instanceof MethodArgumentNotValidException)
{
BindingResult bindingResult = ((MethodArgumentNotValidException)exception).getBindingResult();
if (null != bindingResult && bindingResult.hasErrors())
{
List<String> errMsg = new ArrayList<>();
bindingResult.getFieldErrors().stream().forEach(fieldError -> {
errMsg.add(fieldError.getDefaultMessage());
});
return JsonResult.error(StringUtils.join(errMsg, ","));
}
}
// 其余情况
log.error("Error: handleBadRequest StackTrace : {}", exception);
return JsonResult.error(StringUtils.defaultString(exception.getMessage(), "系统异常,请联系管理员"));
}
}
//goto src\main\java\com\fly\core\exception\ValidateException.java
package com.fly.core.exception;
public class ValidateException extends RuntimeException
{
private static final long serialVersionUID = -939208231165751812L;
public ValidateException()
{
super();
}
public ValidateException(String message)
{
super(message);
}
}
//goto src\main\java\com\fly\core\qr\BufferedImageLuminanceSource.java
package com.fly.core.qr;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import com.google.zxing.LuminanceSource;
public class BufferedImageLuminanceSource extends LuminanceSource
{
private BufferedImage image;
private int left;
private int top;
public BufferedImageLuminanceSource(BufferedImage image)
{
this(image, 0, 0, image.getWidth(), image.getHeight());
}
public BufferedImageLuminanceSource(BufferedImage image, int left, int top, int width, int height)
{
super(width, height);
int sourceWidth = image.getWidth();
int sourceHeight = image.getHeight();
if (left + width > sourceWidth || top + height > sourceHeight)
{
throw new IllegalArgumentException("Crop rectangle does not fit within image data.");
}
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width; x++)
{
if ((image.getRGB(x, y) & 0xFF000000) == 0)
{
image.setRGB(x, y, 0xFFFFFFFF);
}
}
}
this.image = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_BYTE_GRAY);
this.image.getGraphics().drawImage(image, 0, 0, null);
this.left = left;
this.top = top;
}
@Override
public byte[] getRow(int y, byte[] row)
{
if (y < 0 || y >= getHeight())
{
throw new IllegalArgumentException("Requested row is outside the image: " + y);
}
int width = getWidth();
if (row == null || row.length < width)
{
row = new byte[width];
}
image.getRaster().getDataElements(left, top + y, width, 1, row);
return row;
}
@Override
public byte[] getMatrix()
{
int width = getWidth();
int height = getHeight();
int area = width * height;
byte[] matrix = new byte[area];
image.getRaster().getDataElements(left, top, width, height, matrix);
return matrix;
}
@Override
public boolean isCropSupported()
{
return true;
}
@Override
public LuminanceSource crop(int left, int top, int width, int height)
{
return new BufferedImageLuminanceSource(image, this.left + left, this.top + top, width, height);
}
@Override
public boolean isRotateSupported()
{
return true;
}
@Override
public LuminanceSource rotateCounterClockwise()
{
int sourceWidth = image.getWidth();
int sourceHeight = image.getHeight();
AffineTransform transform = new AffineTransform(0.0, -1.0, 1.0, 0.0, 0.0, sourceWidth);
BufferedImage rotatedImage = new BufferedImage(sourceHeight, sourceWidth, BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g = rotatedImage.createGraphics();
g.drawImage(image, transform, null);
g.dispose();
int width = getWidth();
return new BufferedImageLuminanceSource(rotatedImage, top, sourceWidth - (left + width), getHeight(), width);
}
}
//goto src\main\java\com\fly\core\qr\QRCodeUtil.java
package com.fly.core.qr;
import java.awt.BasicStroke;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Shape;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Hashtable;
import javax.imageio.ImageIO;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.Result;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
public class QRCodeUtil
{
/**
* 二维码尺寸
*/
private static final int QRCODE_SIZE = 300;
/**
* LOGO宽度
*/
private static final int WIDTH = 60;
/**
* LOGO高度
*/
private static final int HEIGHT = 60;
/**
* 给定内容、图标生成二维码图片
*
* @param content 內容
* @param imgURL 图标
* @param needCompress 是否压缩尺寸
* @return
* @throws Exception
* @see [类、类#方法、类#成员]
*/
public static BufferedImage createImage(String content, URL imgURL, boolean needCompress)
throws Exception
{
Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8);
hints.put(EncodeHintType.MARGIN, 1);
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE, hints);
int width = bitMatrix.getWidth();
int height = bitMatrix.getHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
if (imgURL == null)
{
return image;
}
// 插入图片
insertImage(image, imgURL, needCompress);
return image;
}
private static void insertImage(BufferedImage source, URL imgURL, boolean needCompress)
throws Exception
{
if (imgURL == null)
{
System.err.println("文件不存在!");
return;
}
Image src = ImageIO.read(imgURL);
int width = src.getWidth(null);
int height = src.getHeight(null);
if (needCompress)
{
// 压缩LOGO
width = Math.min(width, WIDTH);
height = Math.min(height, HEIGHT);
Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = tag.getGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
src = image;
}
// 插入LOGO
Graphics2D graph = source.createGraphics();
int x = (QRCODE_SIZE - width) / 2;
int y = (QRCODE_SIZE - height) / 2;
graph.drawImage(src, x, y, width, height, null);
Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6);
graph.setStroke(new BasicStroke(3f));
graph.draw(shape);
graph.dispose();
}
/**
* 解析二维码图
*
* @param file
* @return
* @throws Exception
* @see [类、类#方法、类#成员]
*/
public static String decode(File file)
throws Exception
{
BufferedImage image = ImageIO.read(file);
if (image == null)
{
return null;
}
BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(image);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
Hashtable<DecodeHintType, String> hints = new Hashtable<>();
hints.put(DecodeHintType.CHARACTER_SET, StandardCharsets.UTF_8.name());
Result result = new MultiFormatReader().decode(bitmap, hints);
return result.getText();
}
/**
* 解析二维码图
*
* @param path
* @return
* @throws Exception
* @see [类、类#方法、类#成员]
*/
public static String decode(String path)
throws Exception
{
return decode(new File(path));
}
}
//goto src\main\java\com\fly\core\utils\TokenUtils.java
package com.fly.core.utils;
import java.util.Date;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DateUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TokenUtils
{
private static String sysToken;
/**
* sysToken有效时间(2小时)
*/
private static Date sysTokenTime;
/**
* 验证token是否合法
*
* @param token
* @return
* @return
*/
public static boolean valide(String token)
{
boolean success = StringUtils.equals(token, getToken());
if (!success)
{
log.info("------ now valid sysToken is: {}", sysToken);
}
return success;
}
/**
* 获取sysToken有效时间
*
* @return
*/
public static String getTokenTime()
{
if (sysTokenTime != null)
{
return DateFormatUtils.format(sysTokenTime, "yyyy-MM-dd HH:mm:ss");
}
return null;
}
/**
* 获取sysToken
*
* @return
* @see [类、类#方法、类#成员]
*/
private static String getToken()
{
Date now = new Date();
if (sysTokenTime == null || now.after(sysTokenTime))
{
sysTokenTime = DateUtils.addHours(now, 2);
sysToken = UUID.randomUUID().toString().replace("-", "");
log.info("------ now valid sysToken is: {}", sysToken);
}
return sysToken;
}
}
//goto src\main\java\com\fly\FilesSendBootApplication.java
package com.fly;
import java.net.InetAddress;
import org.apache.commons.lang3.SystemUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class FilesSendBootApplication
{
@Value("${server.port}")
String port;
public static void main(String[] args)
{
SpringApplication.run(FilesSendBootApplication.class, args);
}
@Bean
@ConditionalOnWebApplication
CommandLineRunner init()
{
return args -> {
if (SystemUtils.IS_OS_WINDOWS)// 防止非windows系统报错,启动失败
{
String ip = InetAddress.getLocalHost().getHostAddress();
String url = "http://" + ip + ":" + port;
Runtime.getRuntime().exec("cmd /c start " + url);
}
};
}
}
//goto src\main\java\com\fly\web\controller\ApiController.java
package com.fly.web.controller;
import java.awt.image.BufferedImage;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.fly.core.qr.QRCodeUtil;
import com.fly.core.utils.TokenUtils;
import com.fly.web.entity.JsonResult;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
@Api(tags = "系统接口")
@RestController
@RequestMapping("/api")
public class ApiController
{
@Autowired
HttpSession httpSession;
@ApiOperationSupport(order = 10)
@PostMapping("/login")
@ApiOperation("登录系统")
public JsonResult<?> login(String token)
{
if (!TokenUtils.valide(token))
{
return JsonResult.error("token empty or valide failed!");
}
httpSession.setAttribute("token", token);
String date = DateFormatUtils.format(System.currentTimeMillis(), "yyyy-MM-dd HH:mm:ss");
return JsonResult.success(date + " login success!");
}
@ApiOperationSupport(order = 20)
@PostMapping("/logout")
@ApiOperation("退出系统")
public JsonResult<?> logout()
{
httpSession.invalidate();
String date = DateFormatUtils.format(System.currentTimeMillis(), "yyyy-MM-dd HH:mm:ss");
return JsonResult.success(date + " logout success!");
}
@ApiOperation("生成二维码")
@ApiImplicitParam(name = "content", value = "二维码文本", required = true, example = "乡愁是一棵没有年轮的树,永不老去")
@PostMapping(value = "/qr/create", produces = MediaType.IMAGE_JPEG_VALUE)
public void index(String content, HttpServletResponse response)
throws Exception
{
if (StringUtils.isNotBlank(content))
{
Resource resource = new ClassPathResource("img/dog.jpg");
URL imgURL = resource.getURL();
BufferedImage image = QRCodeUtil.createImage(content, imgURL, true);
// 输出图象到页面
ImageIO.write(image, "JPEG", response.getOutputStream());
}
}
}
//goto src\main\java\com\fly\web\controller\file\FileController.java
package com.fly.web.controller.file;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import com.fly.core.exception.ValidateException;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
@RequestMapping("/file")
public class FileController
{
@PostMapping("/upload")
public String upload(@RequestParam MultipartFile[] files)
throws IOException
{
if (files == null || files.length == 0)
{
throw new ValidateException("files is null");
}
String date = DateFormatUtils.format(System.currentTimeMillis(), "yyyyMMdd");
String dir = new File("upload").getCanonicalPath() + File.separator + date + File.separator;
new File(dir).mkdirs();
// 保存文件
for (MultipartFile file : files)
{
if (StringUtils.isNotBlank(file.getOriginalFilename()))
{
File newFile = new File(dir + file.getOriginalFilename());
FileCopyUtils.copy(file.getInputStream(), new FileOutputStream(newFile));
log.info("###### file upload to: {}", dir);
}
}
return "redirect:/index";
}
@GetMapping(value = "/down/{index}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void down(@PathVariable int index, HttpServletResponse response)
throws IOException
{
File dir = new File("upload");
List<File> files = FileUtils.listFiles(dir, null, true).stream().filter(f -> f.isFile()).sorted(Comparator.comparing(File::getAbsolutePath)).collect(Collectors.toList());
if (index >= 0 && index < files.size())
{
File file = files.get(index);
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), StandardCharsets.UTF_8.name()));
response.setHeader("Cache-Control", "no-store, no-cache");
FileCopyUtils.copy(new FileInputStream(file), response.getOutputStream());
}
}
@GetMapping(value = "/clear")
public String clear()
throws IOException
{
File dir = new File("upload");
FileUtils.cleanDirectory(dir);
return "redirect:/index";
}
@GetMapping(value = "/delete/{index}")
public String delete(@PathVariable int index)
{
File dir = new File("upload");
List<File> files = FileUtils.listFiles(dir, null, true).stream().filter(f -> f.isFile()).sorted(Comparator.comparing(File::getAbsolutePath)).collect(Collectors.toList());
if (index >= 0 && index < files.size())
{
files.get(index).delete();
}
return "redirect:/index";
}
}
//goto src\main\java\com\fly\web\controller\file\RestFileController.java
package com.fly.web.controller.file;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.http.MediaType;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.fly.core.exception.ValidateException;
import com.fly.web.entity.JsonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Api(tags = "文件上传、下载接口")
@RestController
@RequestMapping("/rest/file")
public class RestFileController
{
@ApiOperation("文件下载, index取值 [0, files.length)")
@ApiImplicitParam(name = "index", value = "文件索引,起始值0", required = true, allowableValues = "0,1,2,3,4,5,6,7,8,9,10")
@GetMapping(value = "/down/{index}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void down(@PathVariable int index, HttpServletResponse response)
throws IOException
{
File dir = new File("upload");
List<File> files = FileUtils.listFiles(dir, null, true).stream().filter(f -> f.isFile()).sorted(Comparator.comparing(File::getAbsolutePath)).collect(Collectors.toList());
if (index >= 0 && index < files.size())
{
File file = files.get(index);
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), StandardCharsets.UTF_8.name()));
response.setHeader("Cache-Control", "no-store, no-cache");
FileCopyUtils.copy(new FileInputStream(file), response.getOutputStream());
}
}
@ApiOperation("文件搜索")
@PostMapping("/list")
public JsonResult<?> list()
{
File dir = new File("upload");
if (!dir.exists() || !dir.isDirectory())
{
return JsonResult.error("文件目录不存在");
}
// 检索文件路径排序
List<String> paths = FileUtils.listFiles(dir, null, true).stream().filter(f -> f.isFile()).map(f -> f.getPath()).sorted().collect(Collectors.toList());
return JsonResult.success(paths);
}
@ApiOperation("文件批量上传处理")
@PostMapping("/upload")
public JsonResult<?> upload(MultipartFile[] files)
throws IOException
{
if (files == null || files.length == 0)
{
throw new ValidateException("文件不能为空");
}
String date = DateFormatUtils.format(System.currentTimeMillis(), "yyyyMMdd");
String dir = new File("upload").getCanonicalPath() + File.separator + date + File.separator;
new File(dir).mkdirs();
// 保存文件
for (MultipartFile file : files)
{
File newFile = new File(dir + file.getOriginalFilename());
FileCopyUtils.copy(file.getInputStream(), new FileOutputStream(newFile));
}
if (SystemUtils.IS_OS_WINDOWS)
{
Runtime.getRuntime().exec("cmd /c start " + dir);
}
else
{
log.info("###### file upload to: {}", dir);
}
return JsonResult.success("文件上传成功,保存目录:" + dir);
}
}
//goto src\main\java\com\fly\web\controller\IndexController.java
package com.fly.web.controller;
import java.io.File;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpSession;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.fly.core.utils.TokenUtils;
@Controller
public class IndexController
{
@Autowired
HttpSession httpSession;
@GetMapping("/index")
public String index(Model model)
{
String token = (String)httpSession.getAttribute("token");
if (TokenUtils.valide(token))
{
model.addAttribute("isLogin", true);
model.addAttribute("sysTokenTime", TokenUtils.getTokenTime());
File dir = new File("upload");
if (dir.exists())
{
List<File> files = FileUtils.listFiles(dir, null, true).stream().filter(f -> f.isFile()).sorted(Comparator.comparing(File::getAbsolutePath)).collect(Collectors.toList());
model.addAttribute("files", files);
}
}
return "index";
}
@PostMapping("/login")
public String login(String token)
{
if (TokenUtils.valide(token))
{
httpSession.setAttribute("token", token);
}
return "redirect:/index";
}
@GetMapping("/logout")
public String logout()
{
httpSession.invalidate();
return "redirect:/index";
}
}
//goto src\main\java\com\fly\web\entity\JsonResult.java
package com.fly.web.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
*
* 结果对象
*
* @author 00fly
* @version [版本号, 2021年5月2日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Data
@ApiModel(description = "Json格式消息体")
public class JsonResult<T>
{
@ApiModelProperty(value = "数据对象")
private T data;
@ApiModelProperty(value = "是否成功", required = true, example = "true")
private boolean success;
@ApiModelProperty(value = "错误码")
private String errorCode;
@ApiModelProperty(value = "提示信息")
private String message;
public JsonResult()
{
super();
}
public static <T> JsonResult<T> success(T data)
{
JsonResult<T> r = new JsonResult<>();
r.setData(data);
r.setSuccess(true);
return r;
}
public static JsonResult<?> success()
{
JsonResult<Object> r = new JsonResult<>();
r.setSuccess(true);
return r;
}
public static JsonResult<Object> error(String code, String msg)
{
JsonResult<Object> r = new JsonResult<>();
r.setSuccess(false);
r.setErrorCode(code);
r.setMessage(msg);
return r;
}
public static JsonResult<Object> error(String msg)
{
return error("500", msg);
}
}
//goto src\main\resources\application-dev.yml
logging:
level:
root: info
//goto src\main\resources\application.yml
server:
port: 8080
servlet:
context-path: /
session:
timeout: 1800
spring:
profiles:
active:
- dev
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
thymeleaf:
cache: false
check-template-location: true
encoding: UTF-8
mode: HTML
prefix: classpath:/templates/
suffix: .html
//goto src\main\resources\static\error\404.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>
404(找不到页面)
</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link href="//cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"/>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"/>
<![endif]-->
</head>
<body>
<div class="wrapper-page">
<div class="ex-page-content text-center">
<h1><span>404</span>!</h1>
<h2>很抱歉,没有找到这个页面!</h2>
<br></br>
<a class="btn btn-purple waves-effect waves-light" href="/"><i class="fa fa-angle-left"></i> 返回首页</a>
</div>
</div>
</body>
</html>
//goto src\main\resources\templates\index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<link href="//cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
<style>
body {
margin: 10;
font-size: 62.5%;
line-height: 1.5;
}
.blue-button {
background: #25A6E1;
padding: 3px 20px;
color: #fff;
font-size: 10px;
border-radius: 2px;
-moz-border-radius: 2px;
-webkit-border-radius: 4px;
border: 1px solid #1A87B9
}
table {
width: 60%;
}
tr:hover {
background-color: #FFFACD;
}
th {
background: SteelBlue;
color: white;
}
td,
th {
border: 1px solid gray;
font-size: 12px;
text-align: left;
padding: 5px 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 200px;
white-space: nowrap;
text-overflow: ellipsis;
text-overflow: ellipsis;
}
</style>
</head>
<title>simple文件共享工具</title>
<body>
<div class="wrapper-page">
<table align="center">
<tr>
<th colspan="4">Navigate</th>
</tr>
<tr>
<td><a href="/index" target="_self">index</a></td>
<td><a href="/404/" target="_self">出错页面</a></td>
<td><a href="/doc.html" target="_blank">doc.html</a></td>
</tr>
</table>
</div>
<h5></h5>
<div class="wrapper-page">
<table align="center">
<tr>
<th colspan="3">功能列表</th>
</tr>
<tr th:if="${isLogin!=true}">
<td colspan="3" style="text-align: center;">
<form action="/login" method="post">
<label>登录系统</label>
<span style="color:red;">token</span>
<input name="token" type="password" size="40" maxlength="40" />
<input type="submit" class="blue-button" value="登 录"/>
</form>
</td>
</tr>
<tr th:if="${isLogin}">
<td colspan="3" style="text-align: right;">
<span style="color:red;" th:text="'token有效期至'+${sysTokenTime}"></span>
<span th:if="${isLogin}"><a href="/logout"> 退出登录 </a>|<a href="/file/clear"> 清空文件 </a></span>
</td>
</tr>
<tr th:if="${isLogin==true}">
<td colspan="3" style="text-align: center;">
<form action="/file/upload" method="post" enctype="multipart/form-data">
<span style="color:red;">批量上传</span>
<input name="files" style="display:inline; width:50%;" type="file" multiple="true" />
<input type="submit" class="blue-button" />
</form>
</td>
</tr>
</table>
</div>
<h3></h3>
<div th:if="${isLogin==true}" class="wrapper-page">
<table align="center">
<tr>
<th colspan="4">文件列表</th>
</tr>
<th:block th:each="file,stat:${files}">
<tr>
<td style="text-align: center;" th:text="${stat.count}"></td>
<td><a th:href="'/file/down/'+${stat.count-1}" target="_blank">
<th:block th:text="${file.path}" />
</a>
</td>
<td style="text-align: center;" th:text="${file.length()/1024}+' KB'"></td>
<td style="text-align: center;"><a th:href="'/file/delete/'+${stat.count-1}">删除</a></td>
</tr>
</th:block>
</table>
</div>
</body>
</html>
三、运行部署
1、jar方式
工程目录执行mvn clean package
,会在target文件下生成jar包,拷贝出来运行即可
java -jar springboot-file-send-1.0.0.jar --server.port=8081&
2、docker方式
docker run -it -d -p 80:8080 registry.cn-shanghai.aliyuncs.com/00fly/springboot-file-send:1.0.0
3、docker-compose方式
上传docker文件到服务器,服务需提前安装docker、docker-compose环境
docker-compose.yml
version: '3'
services:
file-send:
image: registry.cn-shanghai.aliyuncs.com/00fly/springboot-file-send:1.0.0
container_name: file-send
deploy:
resources:
limits:
cpus: '1'
memory: 300M
reservations:
cpus: '0.05'
memory: 200M
ports:
- 80:8080
restart: on-failure
logging:
driver: json-file
options:
max-size: 5m
max-file: '1'
restart.sh
#!/bin/bash
docker-compose down && docker system prune -f && docker-compose --compatibility up -d
registry.cn-shanghai.aliyuncs.com/00fly/springboot-file-send:1.0.0
镜像已经上传到阿里镜像仓库,使用下面的命令直接启动应用,如端口冲突的话修改 - 80:8080
即可。
sh restart.sh
四、优化方向
- 限制docker应用磁盘空间大小
- 添加完善的用户、权限控制
有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!
-over-