springboot 简易文件共享工具

一、运行界面

http://124.71.129.204:8080/

1、登录

在这里插入图片描述
后台查看日志,获取token值
在这里插入图片描述

2、二维码使用

如果在手机上登录的话,为方便复制token可按如下步骤操作:

电脑网页端生成二维码

1.拷贝token
2.打开doc.html-》系统接口-》生成二维码-》调试-》输入token-》点击发送
在这里插入图片描述
在这里插入图片描述

手机端(如UC浏览器、微信等)扫描二维码,复制识别内容

3、展示

在这里插入图片描述
批量上传文件或者点击链接下载
在这里插入图片描述

二、源码传送

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 --name file-send -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-

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值