基于Java Web实现一个轻量级的文件服务器

概述

​ 本文是在我由于缺乏IM工具时候需要传软件给别人的时候萌生出来的一个想法,就是希望在没有合适的工具的情况下也能实现将本地文件分享给别人,因此做了这么一件“有趣”的事情。
​ 先来看下实现的效果吧:


1561258028118.png

点击文件可以直接下载,点击文件夹可以进入文件夹查看该文件夹中的文件列表:


1560990559376.png

​ 并且为了方便多人维护这个文件服务器,实现资源的共享,开发了身份验证+多文件上传的功能:


1561259432651.png

​ 这时候只要将网址中的“localhost”替换为部署的服务器IP就能实现简易的轻量级文件服务器。

实现步骤

​ 主要针对java web新手,下面将给出主要的详细实现步骤:

  • 新建一个java web项目
    我这里以springboot项目为例,可以在https://start.spring.io/网站上快速构建一个springboot项目,填入项目相关信息,Dependencies中搜索并添加“Web”、“Freemarker”、“security”依赖即可;点击确定后下载初始化的项目到本地,导入到你的IDE中,如Eclipse、IDEA等。

  • springboot启动文件DemoApplication

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

​ 该文件是springboot启动的入口类,新建springboot项目时会自动生成。

  • 依赖配置文件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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.yuhuan</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

  • 新建配置文件application.yml

在项目的src\main\resources目录下新建application.yml文件(或者application.xml),本文以yaml文件为例:

server:
  port: 8090
spring:
  servlet:
    multipart:
      enabled: true
      file-size-threshold: 0
      max-file-size: 4096MB
      max-request-size: 8192MB
  freemarker:
    request-context-attribute: req
    suffix: .html
    content-type: text/html
    enabled: true
    cache: false
    template-loader-path: classpath:/templates/
    charset: UTF-8
#配置共享文件夹的路径
share:
  path: D:\\java soft
#配置上传文件的账户  
system:
  user:
    name: admin
    pwd: admin@123

其中,share.path配置的就是你要共享的本地文件夹的路径。

  • 编写前台页面fileList.html
<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title>共享文件列表</title>
    <script src="/static/js/jquery-3.4.1.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        function uploadFile(){
            var files = $("#files")[0].files;
            console.log(files);
            if(undefined == files || files.length == 0) {
                alert('请选择上传文件!');
                return false;
            }

            var formData = new FormData();
            for(var i = 0; i < files.length; i++){
                formData.append("files", files[i]);
            }

            formData.append("currentPath", $('#currentPath').val());
            $.ajax({
                type: 'post',
                url: '/file/upload',
                data: formData,
                contentType: false,
                processData: false,
                success:function(res){
                    console.log(res);
                    if(res["code"]=="200"){
                        //refresh
                        window.location.reload();
                        alert(res.msg);
                    }else if(res["code"] == 500){
                        console.log(res);
                        alert(res.msg);
                    }
                    else{
                        alert('此操作需要权限,请先验证身份!');
                        window.location = "/login";
                    }
                }
            });
        }
    </script>
</head>
    <input type="hidden" id="currentPath" name="currentPath" value="${currentPath}"/>
    <#if root??>
        <div style="font-size:18px;width:200px;text-align:left;"><a href="${root!''}"><img src="/static/img/home.png" width="20" height="20"/>返回根目录</a></div></br>
    </#if>
    <#if parent??>
        <div style="font-size:18px;width:200px;text-align:left;"><a href="${parent!''}"><img src="/static/img/back.png" width="20" height="20"/>返回上级目录</a></div></br>
    </#if>
    <#list files as file>
        <#if file.isDirectory()>
            <img src="/static/img/package.png" width="20" height="20"/>
        <#else>
            <img src="/static/img/file.png" width="20" height="20"/>
        </#if>
        <a href="${file.url}">${file.name}</a></br>
    </#list>
    <hr/>
    <div>
        <input type="file" id="files" name="files" multiple="multiple" />
        <input type="button" id="uploadBtn" onclick="uploadFile()" value="上传"/>
    </div>
</body>
</html>
  • 下载图片资源,并保存在src\main\resources\static\img文件夹下

back.png
file.png
home.png
package.png

  • 新建包和mvc配置文件

在src\main\java中新建包:com.yuhuan.demo,再在这个文件夹底下新建五个包:common、config、controller、entity、service,看下项目的目录结构(忽略mapper及mapping):


1561258287641.png
  • 在config包下新建WebMvcConfig类

这个配置文件主要是作用是建立网络请求地址url和服务器文件夹的映射关系:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
        registry.addResourceHandler(GlobalConstants.DOWNLOAD_URL_PREFIX + "/**")
                .addResourceLocations("file:"+ GlobalConstants.sharePath+"/");
    }
}
  • 在common包下新建GlobalConstants类

这个类主要是提供读取配置文件属性值及全局变量使用:

@Configuration
public class GlobalConstants {

    //共享文件夹路径
    public static String sharePath;
    public static final String BROWSE_URL_PREFIX = "/file/list";
    public static final String DOWNLOAD_URL_PREFIX = "/file/download";
    public static final String UPLOAD_URL_PREFIX = "/file/upload";

    public static String systemUserName;
    public static String systemUserPwd;

    @Value("${system.user.name}")
    private void setSystemUserName(String systemUserName) {
        GlobalConstants.systemUserName = systemUserName;
    }
    @Value("${system.user.pwd}")
    private void setSystemUserPwd(String systemUserPwd) {
        GlobalConstants.systemUserPwd = systemUserPwd;
    }

    @Value("${share.path}")
    private void setSharePath(String sharePath) {
        GlobalConstants.sharePath = sharePath;
    }


    public static String convertUrl(String originPath){
        String strs[] = originPath.split(sharePath);
        if(strs.length > 1) {
            return BROWSE_URL_PREFIX + "?path=" + originPath.split(sharePath)[1].replaceAll("\\\\", "/");
        }
        return BROWSE_URL_PREFIX;
    }
}
  • 在common包下新建ResultData类

    该类用于统一返回给前端的数据格式,在本项目中作用不大:

public class ResultData <T> {


    private String msg;
    private Integer code;
    private T data;

    public static <T> ResultData setResultCode(ResultCode resultCode){
        return new ResultData()
                .setCode(resultCode.getCode())
                .setMsg(resultCode.getMsg());
    }

    public static ResultData ok(){
        return setResultCode(ResultCode.OK);
    }

    public static <T> ResultData failed(){
        return setResultCode(ResultCode.FAILED);
    }

    public String getMsg() {
        return msg;
    }

    public ResultData setMsg(String msg) {
        this.msg = msg;
        return this;
    }

    public Integer getCode() {
        return code;
    }

    public ResultData setCode(Integer code) {
        this.code = code;
        return this;
    }

    public T getData() {
        return data;
    }

    public ResultData setData(T data) {
        this.data = data;
        return this;
    }

    enum ResultCode{
        OK(200, "请求成功!"),
        FAILED(500, "请求失败!");

        private Integer code;
        private String msg;

        ResultCode(Integer code, String msg){
            this.code = code;
            this.msg = msg;
        }

        public Integer getCode() {
            return code;
        }

        public void setCode(Integer code) {
            this.code = code;
        }

        public String getMsg() {
            return msg;
        }

        public void setMsg(String msg) {
            this.msg = msg;
        }
    }
}
  • 在entity包下新建CustomerFile类
public class CustomerFile {
    private String name;
    private String url;
    private boolean isDirectory;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public boolean isDirectory() {
        return isDirectory;
    }

    public void isDirectory(boolean directory) {
        isDirectory = directory;
    }

}

  • 在controller包下新建FileSystemController类
@Controller
@RequestMapping("/file")
public class FileSystemController {

    @Autowired
    private FileService fileService;

    @RequestMapping("/list")
    public String browseFile(Model model, @RequestParam(name="path", required = false,
            defaultValue = "") String path){
        File file;
        if(("").equals(path)){
            file = new File(GlobalConstants.sharePath);
        }else {
            file = new File(GlobalConstants.sharePath + path);
            model.addAttribute("parent", GlobalConstants.convertUrl(file.getParent()));
            model.addAttribute("root", GlobalConstants.BROWSE_URL_PREFIX);
        }
        model.addAttribute("currentPath", GlobalConstants.sharePath + path);
        model.addAttribute("files", fileService.getFileList(file));
        return "fileList";
    }

    @RequestMapping("/upload")
    @ResponseBody
    public ResultData uploadFile(@RequestParam("files") MultipartFile[] files, @RequestParam(name=
            "currentPath", required = false, defaultValue = "") String currentPath){
        String msg;
        try {
            msg = fileService.uploadFiles(files, currentPath);
        } catch (IOException e) {
            e.printStackTrace();
            return ResultData.failed().setMsg("上传失败!");
        }
        return ResultData.ok().setMsg(msg);
    }

}
  • 在service包下新建FileService接口

FileService

public interface FileService {
    List<CustomerFile> getFileList(File file);

    String uploadFiles(MultipartFile[] files, String currentPath) throws IOException;
}
  • 在service包下新建impl子包,并在impl包下创建FileServiceImpl类
@Service
public class FileServiceImpl implements FileService {
    @Override
    public List<CustomerFile> getFileList(File file) {
        List<File> files = new ArrayList<>();
        if(file.isDirectory()){
            File[] subFiles = file.listFiles();
            files = Arrays.asList(subFiles);
        }
        sortFiles(files);
        List<CustomerFile> customerFiles = files.stream().map(f->{
            CustomerFile customerFile;
            String url;
            if(f.isDirectory()){
                url = GlobalConstants.convertUrl(f.getPath());
            }else{
                url = f.getPath().replaceAll(GlobalConstants.sharePath, GlobalConstants.DOWNLOAD_URL_PREFIX);
            }

            customerFile = new CustomerFile();
            customerFile.setName(f.getName());
            customerFile.setUrl(url);
            customerFile.isDirectory(f.isDirectory());
            return customerFile;
        }).collect(Collectors.toList());
        return customerFiles;
    }

    @Override
    public String uploadFiles(MultipartFile[] files, String currentPath) throws IOException {
        if(null == files || files.length == 0){
            return "请选择上传文件!";
        }
        File destFile;
        for(MultipartFile file : files){
            destFile = new File(currentPath+File.separator+file.getOriginalFilename());
            //此处很重要,避免别人覆盖已有的本地文件
            if(destFile.exists()){
                return String.format("文件:%s已存在,不能上传,请联系管理员删除后再上传!", file.getOriginalFilename());
            }
            file.transferTo(destFile);
        }
        return "上传成功!";
    }

    /**
    *对文件进行排序,使得在展示文件列表时,文件夹始终在前面
    **/
    private void sortFiles(List<File> files) {
        Collections.sort(files, (f1, f2) -> {
            if (f1.isDirectory()) {
                return -1;
            }else if (f2.isDirectory()) {
                return 1;
            }
            return 0; //相等为0
        });
    }

}

部署使用

​ 至此,我们可以来编译并启动这个项目了,可以借助IDEA、Eclipse等IDE来编译启动,直接运行DemoApplication类即可;也可以编译项目后用java -jar命令启动生成的jar包。

​ 启动后,用浏览器访问:http://localhost:8090/file/list即可看到你配置的共享文件夹的文件列表了,点击文件可以下载,点击文件夹可以进入该文件夹继续浏览文件。

​ 上传文件时会要求用户首先登陆,验证成功后才能上传文件,避免恶意文件的上传导致磁盘空间吃紧!同时,为了避免上传文件时将本地已有文件覆盖(有可能是恶意的),在上传时要验证上传的文件不能将本地文件覆盖!

总结

​ 本文主要是临时萌发的一个想法并把它用熟悉的编程语言实现了,个人觉得还比较有趣。这个轻量级的文件服务器包含了共享文件夹位置的配置、文件浏览与下载功能、用户验证与文件上传等功能,对于想学习java web的同学来说也起到了抛砖引玉的作用!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,188评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,464评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,562评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,893评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,917评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,708评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,430评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,342评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,801评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,976评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,115评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,804评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,458评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,008评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,135评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,365评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,055评论 2 355

推荐阅读更多精彩内容