本章目的是讲解图片服务器及其运用的使用,主讲解个别要点,细节没有全列出来,如果是未搭建过相关服务的小白白,建议先去参考下其他健全的讲解内容,你所希望的搭建一套完整体系流程,本篇可能不太适合。

事先安装docker和docker-compose这里就不说了,

我们希望在nginx下能直接拉取图片,所以ftp的上传的映射路径要和nginx的映射路径一起,方便取图

直接展示拉取配置如下:

vsftpd:
  restart: always
  image: fauria/vsftpd
  volumes:
    - /root/docker-data/vsftpd:/home/vsftpd
  ports:
    - "20:20"
    - "21:21"
    - "21100-21110:21100-21110"
  environment:
    FTP_USER: XXX
    FTP_PASS: XXX
    PASV_MIN_PORT: 21100
    PASV_MAX_PORT: 21110

nginx:
  restart: always
  image: jwilder/nginx-proxy:latest
  ports:
    - "80:80"
    - "443:443"
  volumes:
    - /etc/localtime:/etc/localtime:ro
    - /etc/timezone:/etc/timezone:ro
    - /var/run/docker.sock:/tmp/docker.sock:ro
    - /root/docker-data/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    - /root/docker-data/nginx/site-enabled:/etc/nginx/site-enabled
    - /root/docker-data/vsftpd/XXX:/usr/local/images:ro

以上镜像配置是个人配置,镜像的挂载请根据个人情况修改配置。

注:/root/docker-data/vsftpd/XXX ,XXX是ftp镜像里面配置的登陆用户名。下面会说到为什么用这个。

镜像拉取后,使用启动命令

docker-compose up -d

然后在服务器测试下链接

ftp xxx.xxx.xxx.xxx

出现 bash: ftp: command not found 是因为linux还没安装ftp服务,运行如下命令就行

yum install -y ftp

再次链接后,会要求输入用户名和密码,正确后会显示 Login successful. 算是搭建完成。

xftp链接docker容器 docker ftp服务器_ftp

现在开始外网访问测试下,如果是自己搭建的虚拟机,记得要关闭防火墙或是开放对应的访问端口,不然远程链接不上。

我这里使用的是阿里云的ESC,需要到安全组配置端口,端口范围就是我们的20,21和浮动端口21100-21110。

xftp链接docker容器 docker ftp服务器_xftp链接docker容器_02

我是配置了多个端口限制,如果服务太多又想全部链接的话,直接创建端口范围从 1024/65535 可一次搞定,但感觉不安全。 

xftp链接docker容器 docker ftp服务器_xftp链接docker容器_03

这里使用网页版登陆刷新下

xftp链接docker容器 docker ftp服务器_docker_04

表示配置访问成功。

注:现在看到的这个文件目录,虽然是“/” , 但在服务器上存在的位置如下

容器的位置:/home/vsftpd/xxx

宿主主机的位置:/root/docker-data/vsftpd/xxx

xxx是登录用户名,镜像挂载配置,ftp默认把登录用户所在的目录设为主目录

然后我们再配置nginx.conf,添加如下配置

location /images/ {
            root  /usr/local;
            autoindex_exact_size off;
            autoindex_localtime on;
            charset utf-8;
        }

参数说明:
(1)  root  /usr/local;: 添加图片目录映射,映射目录为/usr/local/images/
(2)  autoindex on;:在Nginx下默认是不允许列出整个目录的。如需此功能,将该项设置为on
(3)  autoindex_exact_size off;:默认为on,显示出文件的确切大小,单位是bytes,改为off后,显示出文件的大概大小,单位是kB或者MB或者GB
(4)  autoindex_localtime on;:默认为off,显示的文件时间为GMT时间, 注意:改为on后,显示的文件时间为文件的服务器时间
(5)  charset utf-8,gbk;:设置编码(防止中文乱码),可以设置对全局生效或者部分路径生效

因为ftp上次的图片文件映射到宿主机,随后又挂载到nginx内,所以当你访问http://XXX.XXX.XXX.XX/images/123.jpg时,等同于访问 /usr/local/images/123.jpg

重启动nginx镜像,让服务生效

docker restart nginx

图片服务器就搭好了。

接下来就是用代码上传下我们的图片,先建立一个基础的springboot服务,这里也直接跳过。

引入主要依赖:

<!-- 图片缩放 -->
    <dependency>
        <groupId>net.coobird</groupId>
        <artifactId>thumbnailator</artifactId>
        <version>0.4.8</version>
    </dependency>
    <!-- net  -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-io</artifactId>
        <version>1.3.2</version>
    </dependency>
    <!-- ftp  -->
    <dependency>
        <groupId>org.apache.camel</groupId>
        <artifactId>camel-ftp</artifactId>
        <version>2.24.2</version>
    </dependency>

编写两个工具类:一个图片工具类,一个FTP工具类

/**
 * 图片工具类
 * @author leopard
 *
 */
public class ImageUtil {

	/**
	 * 简单上传图片,不做任何处理
	 * 
	 * @param imagePath  目标文件夹路径
	 * @param imageName 图片名称
	 * @param inputStream 输出流
	 * @return
	 */
	public static ImageResult uploadImage(String imagePath, String imageName, InputStream inputStream) {

		BufferedInputStream inBuff = null;
		BufferedOutputStream outBuff = null;
		if (!"/".equals(imagePath.substring(imagePath.length() - 1, imagePath.length()))
				&& !"\\".equals(imagePath.substring(imagePath.length() - 1, imagePath.length()))) {
			imagePath += "/";
		}
		File realFile = new File(imagePath);
		// 不存在文件夹则创建
		if (!realFile.exists())
			realFile.mkdirs();
		try {
			// 新建文件输入流并对它进行缓冲
			inBuff = new BufferedInputStream(inputStream);
			// 新建文件输出流并对它进行缓冲
			outBuff = new BufferedOutputStream(new FileOutputStream(imagePath + "/" + imageName));
			// 缓冲数组
			byte[] b = new byte[1024 * 5];
			int len;
			while ((len = inBuff.read(b)) != -1) {
				outBuff.write(b, 0, len);
			}
			// 刷新此缓冲的输出流
			outBuff.flush();
		} catch (FileNotFoundException a) {
			a.printStackTrace();
			return ImageResult.error("目标文件夹找不到");
		} catch (IOException e) {
			e.printStackTrace();
			return ImageResult.error("操作流异常!");
		} finally {
			// 关闭流
			try {
				if (inBuff != null)
					inBuff.close();
				if (outBuff != null)
					outBuff.close();
			} catch (IOException c) {
				c.printStackTrace();
			}
		}
		StringBuffer result = new StringBuffer(imagePath);
		result.append(imageName);
		return ImageResult.success(result.toString(), null);
	}

	/**
	 * 图片名生成
	 */
	public static String genImageName() {

		// 取当前时间的长整形值包含毫秒
		long millis = System.currentTimeMillis();
		// 加上三位随机数
		int end3 = MathUtil.getNumRandom(3);
		// 如果不足三位前面补0
		String str = millis + String.format("%03d", end3);

		return str;
	}

	/**
	 * 保存图片并且生成缩略图
	 * 
	 * @param imagePath 目标文件夹路径
	 * @param imageName 图片名称
	 * @param inputStream  输出流
	 * @param fileSize  文件大小
	 * @param width 期望宽度
	 * @param height  期望高度
	 * @return
	 */
	public static ImageResult uploadFileAndCreateThumbnail(String imagePath, String imageName, InputStream inputStream,
			long fileSize, int width, int height) {

		if (width == 0 || height == 0) {
			return ImageResult.error("请设置缩略图宽高!");
		}

		// 保存源文件
		ImageResult uploadImage = uploadImage(imagePath, imageName, inputStream);
		if (!SystemCodeAndMsg.SUCCESS.code().equals(uploadImage.getCode())) {
			return uploadImage;
		}

		/*** 缩略图begin */
		// 源图片路径
		StringBuffer imagePathBuffer = new StringBuffer(imagePath);
		// 如果最后不存在斜杆,则自行添加
		if (!"/".equals(imagePath.substring(imagePath.length() - 1, imagePath.length()))
				&& !"\\".equals(imagePath.substring(imagePath.length() - 1, imagePath.length()))) {
			imagePathBuffer.append("/");
		}
		String filePathName = imagePathBuffer.append(imageName).toString();
		// 目标图片(移除后缀 并改名 xxx.jpg
		// ->xxxx-small)不含后缀路径,目的在下面的outputFormat(jpg),转化为固定的jpg格式文件
		StringBuffer thumbnailFilePathName = new StringBuffer(filePathName.substring(0, filePathName.lastIndexOf(".")))
				.append("-small");
		thumbnailFilePathName = thumbnailFilePathName.append(".jpg");
		String disposeUrl = thumbnailFilePathName.toString();
		try {
			Builder<File> builderFile = Thumbnails.of(filePathName);
			// 压缩200KB
			if (fileSize > 200 * 1024) {
				builderFile.outputQuality((200 * 1024f) / fileSize);
			}
			// 按自定义大小比例缩放
			builderFile.forceSize(width, height).toFile(disposeUrl);

		} catch (Exception e1) {
			return ImageResult.error(e1.getMessage());
		}
		/*** 缩略图end */
		return ImageResult.success(uploadImage.getUrl(), disposeUrl);
	}

}
/**
 * ftp工具类
 * 
 * @author leopard
 *
 */
public class FtpUtil {

	/**
	 * Description: 向FTP服务器上传文件
	 * 
	 * @param host FTP服务器ip
	 * @param port  FTP服务器端口
	 * @param username  FTP登录账号
	 * @param password FTP登录密码
	 * @param basePath FTP服务器基础目录,/images --> /home/vsftpd/${FTP登录账号 }/images
	 * @param filePath  FTP服务器文件存放路径。例如分日期存放:/2018/05/28。文件的路径为basePath+filePath
	 * @param filename 上传到FTP服务器上的文件名
	 * @param input 输入流
	 * @return 
	 */
	public static FtpResult uploadFileToImage(String host, int port, String username, String password, String basePath,
			String filePath, String filename, InputStream inputStream, long fileSize, int width, int height) {

		String imageTempPath = ClassUtils.getDefaultClassLoader().getResource("").getPath() + "/imageTemp";
		ImageResult uploadFileAndCreateThumbnail = ImageUtil.uploadFileAndCreateThumbnail(imageTempPath, filename,
				inputStream, fileSize, width, height);
		if (!SystemCodeAndMsg.SUCCESS.code().equals(uploadFileAndCreateThumbnail.getCode())) {
			return new FtpResult(uploadFileAndCreateThumbnail);
		}

		FTPClient ftp = new FTPClient();
		String imagePath = basePath + filePath;
		StringBuffer initUrl = new StringBuffer(imagePath); // 原文件上传路径
		StringBuffer disposeUrl = new StringBuffer(imagePath); // 处理后文件上传路径
		try {
			int reply;
			ftp.connect(host, port);// 连接FTP服务器
			// 如果采用默认端口,可以使用ftp.connect(host)的方式直接连接FTP服务器
			ftp.login(username, password);// 登录
			reply = ftp.getReplyCode();
			if (!FTPReply.isPositiveCompletion(reply)) {
				ftp.disconnect();
				return FtpResult.error("ftp远程登录失败!");
			}
			// 切换到上传目录--changeWorkingDirectory()方法类似linux的cd命令
			if (!ftp.changeWorkingDirectory(imagePath)) {
				// 如果目录不存在创建目录
				String[] dirs = imagePath.split("/");
				String tempPath = "/";
				for (String dir : dirs) {
					if (null == dir || "".equals(dir)) {
						continue;
					}
					tempPath += "/" + dir;
					if (!ftp.changeWorkingDirectory(tempPath)) {
						//makeDirectory()方法类似linux的mkdir命令
						if (!ftp.makeDirectory(tempPath)) {
							return FtpResult.error("ftp远程创建文件夹失败!");
						} else {
							ftp.changeWorkingDirectory(tempPath);
						}
					}
				}
			}
			// 设置为被动模式
			ftp.enterLocalPassiveMode();
			// 设置上传文件的类型为二进制类型
			ftp.setFileType(FTP.BINARY_FILE_TYPE);

			String initImageFileUrl = uploadFileAndCreateThumbnail.getUrl();
			String disposeImageFileUrl = uploadFileAndCreateThumbnail.getDisposeUrl();
			// 上传原文件
			if (StringUtils.isNotBlank(initImageFileUrl)) {
				File initImageFile = new File(initImageFileUrl);
				if (!ftp.storeFile(filename, new FileInputStream(initImageFile))) {
					return FtpResult.error("ftp远程上传原文件失败!");
				}
				initUrl.append(filename);
				// initImageFile.delete();//所在进程运行中,删除不了
			}
			// 上传压缩文件
			if (StringUtils.isNotBlank(disposeImageFileUrl)) {
				File disposeImageFile = new File(disposeImageFileUrl);
				String disposeFileName = disposeImageFileUrl.substring(disposeImageFileUrl.lastIndexOf("/") + 1,
						disposeImageFileUrl.length());
				if (!ftp.storeFile(disposeFileName, new FileInputStream(disposeImageFile))) {
					return FtpResult.error("ftp远程上传压缩文件失败!");
				}
				disposeUrl.append(disposeFileName);
				// disposeImageFile.delete();//所在进程运行中,删除不了
			}

			inputStream.close();
			ftp.logout();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (ftp.isConnected()) {
				try {
					ftp.disconnect();
				} catch (IOException ioe) {
				}
			}
		}
		return FtpResult.success(initUrl.toString(), disposeUrl.toString());
	}

}

FTP工具类问题检测,一般都看reply的数值,200<= reply

如:一开始,工具类里得局部参数tempPath不是从“ / ”,而是从“/temp”,开始,那拼接出入的“/images”,就变成“/temp/images”,

makeDirectory() 方法也无法一次性创建两层目录。进入源码或是打印出来,可以看到reply 的数组为550.

注:本章主要是运用搭建图片服务器和上传图片为主,所以FTP工具类被个人改动为主为图片服务,如要上传其他类型文件,可自行修改。

测试代码块:

@RequestMapping(value = "/savePic", method = RequestMethod.POST)
	@ResponseBody
	public JSONObject sendSimple(MultipartFile uploadFile) {

		JSONObject jsonObject = new JSONObject();
		jsonObject.put("code", SystemCodeAndMsg.WEB_SUCCESS.code());
		jsonObject.put("msg", SystemCodeAndMsg.WEB_SUCCESS.msg());


		// 判断上传图片是否为空
		if (null == uploadFile || uploadFile.isEmpty()) {
			jsonObject.put("msg", "文件不能为空");
			return jsonObject;
		}
		if (uploadFile.getSize() >= 5 * 1024 * 1024) {
			jsonObject.put("msg", "文件不能大于5M");
			return jsonObject;
		}

		// 取文件扩展名
		String ext = FilenameUtils.getExtension(uploadFile.getOriginalFilename());
		String imageName = ImageUtil.genImageName() + "." + ext;
		
		// 文件在服务器的存放路径,应该使用日期分隔的目录结构
		DateTime dateTime = new DateTime();
		String filePath = dateTime.toString("yyyy/MM/dd");

		String IMAGE_BASE_URL = "H:";
		String imagePath = IMAGE_BASE_URL + "/images/" + filePath;
		try {
			//本地图片上传方式
//			ImageResult uploadFileAndCreateThumbnail = ImageUtil.uploadFileAndCreateThumbnail(imagePath, imageName, 
//					uploadFile.getInputStream(), uploadFile.getSize(), 400, 400);
//			jsonObject.put("msg", uploadFileAndCreateThumbnail.getUrl());
			//远程FTP图片服务器上传
			FtpResult uploadFile2 = FtpUtil.uploadFileToImage("xxx.leopardxxx.com", 21, "xxx", "xxx", "/images/",
					filePath, imageName, uploadFile.getInputStream() ,uploadFile.getSize() , 400 , 400);
			jsonObject.put("msg", uploadFile2.getDisposeUrl());
		} catch (Exception e) {
			e.printStackTrace();
			return jsonObject;
		}
		// 返回结果,生成一个可以访问到图片的url返回
		return jsonObject;
	}

文章内的 FtpResult 和  ImageResult 两个只是回调参数封装,还有表单功能,这里不做展示。