首页> 搜索结果页
"七牛云存储 accesskey" 检索
共 41 条结果
SpringBoot---SpringBoot整合七牛云上传图片
准备工作1.注册并实名认证七牛云账号不进行实名认证将不能创建空间,审核最多需要三个工作日,但通常实名认证过后1~2个小时就能收到认证成功的信息。2.创建空间3.获取几个重要信息AK 和 SK空间名称也就是创建空间时自己去的名字临时域名代码1.yml配置oss: qiniu: domain: qtxxxxxxxx.hn-xxx.xxxxx.com # 访问域名(默认使用七牛云测试域名) accessKey: Gn0uwxxxxxxxxxxxxxxxxxxxxy3GEVmZqR58ed # 公钥 刚才的AK secretKey: hs-ScVOxxxxxxxxxxxo0yG33uHm8_NkmnKy # 私钥 刚才的SK bucketName: officxxxxxxxxxxicture #存储空间名称配置类package studio.banner.officialwebsite.config; import lombok.Data; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** * @Author: Re * @Date: 2021/5/15 20:48 */ @Data @Component public class QiNiuYunConfig { /** * 七牛域名domain */ @Value("${oss.qiniu.domain}") private String qiniuDomain; /** * 七牛ACCESS_KEY */ @Value("${oss.qiniu.accessKey}") private String qiniuAccessKey; /** * 七牛SECRET_KEY */ @Value("${oss.qiniu.secretKey}") private String qiniuSecretKey; /** * 七牛空间名 */ @Value("${oss.qiniu.bucketName}") private String qiniuBucketName; }2.Service接口package studio.banner.officialwebsite.service; import java.io.FileInputStream; /** * @Author: Re * @Date: 2021/5/15 22:42 */ public interface IQiNiuYunService { /** * 上传照片 * @return * @param file * @param path */ String updatePhoto(String path, FileInputStream file); }3.Service实现package studio.banner.officialwebsite.service.Impl; import com.google.gson.Gson; import com.qiniu.common.QiniuException; import com.qiniu.http.Response; import com.qiniu.storage.Configuration; import com.qiniu.storage.Region; import com.qiniu.storage.UploadManager; import com.qiniu.storage.model.DefaultPutRet; import com.qiniu.util.Auth; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import studio.banner.officialwebsite.config.QiNiuYunConfig; import studio.banner.officialwebsite.service.IQiNiuYunService; import java.io.FileInputStream; /** * @Author: Re * @Date: 2021/5/15 22:43 */ @Service public class QiNiuYunServiceImpl implements IQiNiuYunService { protected static Logger logger = LoggerFactory.getLogger(QiNiuYunServiceImpl.class); @Autowired QiNiuYunConfig qiNiuYunConfig; @Override public String updatePhoto(String key, FileInputStream file) { /** * 构造一个带指定Region对象的配置类 */ Configuration cfg = new Configuration(Region.region2()); /** * 其他参数参考类注释 */ UploadManager uploadManager = new UploadManager(cfg); /** * 生成上传凭证,然后准备上传 */ logger.info("密钥信息"+qiNiuYunConfig.getQiniuBucketName()+qiNiuYunConfig.getQiniuAccessKey()+qiNiuYunConfig.getQiniuSecretKey()); Auth auth = Auth.create(qiNiuYunConfig.getQiniuAccessKey(), qiNiuYunConfig.getQiniuSecretKey()); String upToken = auth.uploadToken(qiNiuYunConfig.getQiniuBucketName()); try { Response response = uploadManager.put(file, key, upToken,null,null); //解析上传成功的结果 DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class); logger.info(putRet.key); logger.info(putRet.hash); } catch (QiniuException ex) { Response r = ex.response; logger.error(r.toString()); try { logger.error(r.bodyString()); } catch (QiniuException e) { r = e.response; logger.error(r.toString()); } } return "http://"+qiNiuYunConfig.getQiniuDomain()+"/"+key; } }4.Controller层package studio.banner.officialwebsite.controller.background; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import studio.banner.officialwebsite.service.IQiNiuYunService; import java.io.FileInputStream; import java.io.IOException; import java.util.UUID; /** * @Author: Re * @Date: 2021/5/16 7:55 */ @RestController @Api(tags = "上传图片接口",value = "UploadPhotoController") public class UploadPhotoController { @Autowired protected IQiNiuYunService qiNiuYunService; @PostMapping("/upload") @ApiOperation(value = "上传图片",notes = "上传图片不能为空",httpMethod = "POST") public String upload(@RequestPart MultipartFile file) { // 获取文件名 String fileName = file.getOriginalFilename(); // 生成随机的图片名 String imgName = UUID.randomUUID() + "-" +fileName; if (!file.isEmpty()) { FileInputStream inputStream = null; try { inputStream = (FileInputStream) file.getInputStream(); String path = qiNiuYunService.updatePhoto(imgName,inputStream); System.out.print("七牛云返回的图片链接:" + path); return path; } catch (IOException e) { e.printStackTrace(); } return "上传失败"; } return "上传失败"; } }Swagger测试响应体为复制链接进入文章参考:https://www.cnblogs.com/code-duck/p/13406348.html七牛云JAVASDK
文章
2023-02-23
SpringBoot学习——七牛云上传删除图片
导入依赖 <!-- 七牛云 --> <dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>[7.5.0, 7.5.99]</version> </dependency> <!--Gosn依赖--> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.6</version> </dependency> application.yml 配置# 七牛云 oss: qiniu: domain: 填入你的访问域名 # 访问域名(默认使用七牛云测试域名) accessKey: 填入你的公钥 # 公钥 secretKey: 填入你的私钥 # 私钥 bucketName: 填入你的存储空间名 #存储空间名称 QiNiuYunConfig.java@Configuration public class QiNiuYunConfig { /** * 七牛域名domain */ @Value("${oss.qiniu.domain}") private String qiniuDomain; /** * 七牛ACCESS_KEY */ @Value("${oss.qiniu.accessKey}") private String qiniuAccessKey; /** * 七牛SECRET_KEY */ @Value("${oss.qiniu.secretKey}") private String qiniuSecretKey; /** * 七牛空间名 */ @Value("${oss.qiniu.bucketName}") private String qiniuBucketName; protected static Logger logger = LoggerFactory.getLogger(QiNiuYunConfig.class); public String uploadPhoto(String filename, FileInputStream file) { // 构造一个带指定Region对象的配置类,注意后面的Region个地区不一样的 Configuration cfg = new Configuration(Region.region2()); cfg.useHttpsDomains = false; UploadManager uploadManager = new UploadManager(cfg); // 生成上传凭证,然后准备上传 Auth auth = Auth.create(qiniuAccessKey, qiniuSecretKey); String upToken = auth.uploadToken(qiniuBucketName); try { Response response = uploadManager.put(file, filename, upToken, null, null); // 解析上传成功的结果 DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class); // 这个returnPath是获得到的外链地址,通过这个地址可以直接打开图片 return "http://"+qiniuDomain+"/"+putRet.key; } catch (QiniuException ex) { Response r = ex.response; logger.error(r.toString()); System.err.println(); try { logger.error(r.bodyString()); } catch (QiniuException ex2) { r = ex2.response; logger.error(r.toString()); } } return ""; } public boolean deletePhoto(String fileName) { Configuration configuration = new Configuration(Region.region2()); Auth auth = Auth.create(qiniuAccessKey, qiniuSecretKey); BucketManager bucketManager = new BucketManager(auth, configuration); try { if (fileName != null) { bucketManager.delete(qiniuBucketName, fileName); return true; } } catch (QiniuException ex) { //如果遇到异常,说明删除失败 logger.error(String.valueOf(ex.code())); logger.error(ex.response.toString()); } return false; } } 使用案例:七牛云返回的路径格式:  http:// 访问域名 / 图片名称注:  这里的RespBean是自定义的响应类 @Autowired protected static final Logger logger = LoggerFactory.getLogger(QrCodeController.class); private QiNiuYunConfig qiNiuYunConfig; @Autowired private IQrCodeService iQrCodeService; @PostMapping("qrCode/insert") @ApiOperation("上传二维码图片") @ApiImplicitParams({ @ApiImplicitParam(name = "photoName", value = "图片名称", dataTypeClass = String.class) }) public RespBean insert(@RequestParam String photoName, @RequestPart("file") MultipartFile file) { QrCode qrCode = new QrCode(); qrCode.setPhotoName(photoName); // 获取文件名 //String fileName = file.getOriginalFilename(); // 生成随机的图片名 (这个案例中没使用) //String photoName = UUID.randomUUID() + "-" +fileName; if (!file.isEmpty()) { try { FileInputStream inputStream = (FileInputStream) file.getInputStream(); // 这里接收 七牛云返回的 图片地址 String path = iQiNiuYunService.uploadPhoto(photoName, inputStream); qrCode.setPhotoAddress(path); // 将二维码实体qrcode 插入到mysql数据库中 if (iQrCodeService.insert(qrCode)) { logger.info("上传成功!"); return RespBean.ok("上传成功!"); } } catch (IOException e) { e.printStackTrace(); } } return RespBean.error("上传失败!"); } @DeleteMapping("qrCode/delete") @ApiOperation("根据照片名删除图片") @ApiImplicitParam(name = "photoName", value = "图片名称",dataTypeClass = String.class) public RespBean delete(@RequestParam String photoName){ if (iQiNiuYunService.deletePhoto(photoName)&&iQrCodeService.delete(photoName)){ return RespBean.ok("删除成功"); } return RespBean.ok("删除失败,未找到该图片!"); } 相关说明:关于 Region 对象和机房的关系如下:机房    Region华东    Region.region0() , Region.huadong()华北    Region.region1() , Region.huabei()华南    Region.region2() , Region.huanan()北美    Region.regionNa0() , Region.beimei()
文章
Java
2023-02-14
图片存储方案介绍---七牛云存储
1. 图片存储方案1.1 介绍在实际开发中,我们会有很多处理不同功能的服务器。例如:应用服务器:负责部署我们的应用数据库服务器:运行我们的数据库文件服务器:负责存储用户上传文件的服务器分服务器处理的目的是让服务器各司其职,从而提高我们项目的运行效率。常见的图片存储方案:方案一:使用nginx搭建图片服务器方案二:使用开源的分布式文件存储系统,例如Fastdfs、HDFS等方案三:使用云存储,例如阿里云、七牛云等1.2 七牛云存储七牛云(隶属于上海七牛信息技术有限公司)是国内领先的以视觉智能和数据智能为核心的企业级云计算服务商,同时也是国内知名智能视频云服务商,累计为 70 多万家企业提供服务,覆盖了国内80%网民。围绕富媒体场景推出了对象存储、融合 CDN 加速、容器云、大数据平台、深度学习平台等产品、并提供一站式智能视频云解决方案。为各行业及应用提供可持续发展的智能视频云生态,帮助企业快速上云,创造更广阔的商业价值。官网:https://www.qiniu.com/通过七牛云官网介绍我们可以知道其提供了多种服务,我们主要使用的是七牛云提供的对象存储服务来存储图片。1.2.1 注册、登录要使用七牛云的服务,首先需要注册成为会员。地址:https://portal.qiniu.com/signup注册完成后就可以使用刚刚注册的邮箱和密码登录到七牛云:登录成功后点击页面右上角管理控制台:注意:登录成功后还需要进行实名认证才能进行相关操作。1.2.2 新建存储空间要进行图片存储,我们需要在七牛云管理控制台新建存储空间。点击管理控制台首页对象存储下的立即添加按钮,页面跳转到新建存储空间页面:可以创建多个存储空间,各个存储空间是相互独立的。1.2.3 查看存储空间信息存储空间创建后,会在左侧的存储空间列表菜单中展示创建的存储空间名称,点击存储空间名称可以查看当前存储空间的相关信息1.2.4 开发者中心可以通过七牛云提供的开发者中心学习如何操作七牛云服务,地址:https://developer.qiniu.com/点击对象存储,跳转到对象存储开发页面,地址:https://developer.qiniu.com/kodo七牛云提供了多种方式操作对象存储服务,本项目采用Java SDK方式,地址:https://developer.qiniu.com/kodo/sdk/1239/java使用Java SDK操作七牛云需要导入如下maven坐标:<dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>7.2.0</version> </dependency> 1.2.5 鉴权Java SDK的所有的功能,都需要合法的授权。授权凭证的签算需要七牛账号下的一对有效的Access Key和Secret Key,这对密钥可以在七牛云管理控制台的个人中心(https://portal.qiniu.com/user/key)获得,如下图:1.2.6 Java SDK操作七牛云本章节我们就需要使用七牛云提供的Java SDK完成图片上传和删除,我们可以参考官方提供的例子//构造一个带指定Zone对象的配置类 Configuration cfg = new Configuration(Zone.zone0()); //...其他参数参考类注释 UploadManager uploadManager = new UploadManager(cfg); //...生成上传凭证,然后准备上传 String accessKey = "your access key"; String secretKey = "your secret key"; String bucket = "your bucket name"; //如果是Windows情况下,格式是 D:\\qiniu\\test.png String localFilePath = "/home/qiniu/test.png"; //默认不指定key的情况下,以文件内容的hash值作为文件名 String key = null; Auth auth = Auth.create(accessKey, secretKey); String upToken = auth.uploadToken(bucket); try { Response response = uploadManager.put(localFilePath, key, upToken); //解析上传成功的结果 DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class); System.out.println(putRet.key); System.out.println(putRet.hash); } catch (QiniuException ex) { Response r = ex.response; System.err.println(r.toString()); try { System.err.println(r.bodyString()); } catch (QiniuException ex2) { //ignore } } //构造一个带指定Zone对象的配置类 Configuration cfg = new Configuration(Zone.zone0()); //...其他参数参考类注释 String accessKey = "your access key"; String secretKey = "your secret key"; String bucket = "your bucket name"; String key = "your file key"; Auth auth = Auth.create(accessKey, secretKey); BucketManager bucketManager = new BucketManager(auth, cfg); try { bucketManager.delete(bucket, key); } catch (QiniuException ex) { //如果遇到异常,说明删除失败 System.err.println(ex.code()); System.err.println(ex.response.toString()); } 1.2.7 封装工具类为了方便操作七牛云存储服务,我们可以将官方提供的案例简单改造成一个工具类,在我们的项目中直接使用此工具类来操作就可以:package com.itheima.utils; import com.google.gson.Gson; import com.qiniu.common.QiniuException; import com.qiniu.common.Zone; import com.qiniu.http.Response; import com.qiniu.storage.BucketManager; import com.qiniu.storage.Configuration; import com.qiniu.storage.UploadManager; import com.qiniu.storage.model.DefaultPutRet; import com.qiniu.util.Auth; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; /** * 七牛云工具类 */ public class QiniuUtils { public static String accessKey = "dulF9Wze9bxujtuRvu3yyYb9JX1Sp23jzd3tO708"; public static String secretKey = "vZkhW7iot3uWwcWz9vXfbaP4JepdWADFDHVLMZOe"; public static String bucket = "qiniutest"; public static void upload2Qiniu(String filePath,String fileName){ //构造一个带指定Zone对象的配置类 Configuration cfg = new Configuration(Zone.zone0()); UploadManager uploadManager = new UploadManager(cfg); Auth auth = Auth.create(accessKey, secretKey); String upToken = auth.uploadToken(bucket); try { Response response = uploadManager.put(filePath, fileName, upToken); //解析上传成功的结果 DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class); } catch (QiniuException ex) { Response r = ex.response; try { System.err.println(r.bodyString()); } catch (QiniuException ex2) { //ignore } } } //上传文件 public static void upload2Qiniu(byte[] bytes, String fileName){ //构造一个带指定Zone对象的配置类 Configuration cfg = new Configuration(Zone.zone0()); //...其他参数参考类注释 UploadManager uploadManager = new UploadManager(cfg); //默认不指定key的情况下,以文件内容的hash值作为文件名 String key = fileName; Auth auth = Auth.create(accessKey, secretKey); String upToken = auth.uploadToken(bucket); try { Response response = uploadManager.put(bytes, key, upToken); //解析上传成功的结果 DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class); System.out.println(putRet.key); System.out.println(putRet.hash); } catch (QiniuException ex) { Response r = ex.response; System.err.println(r.toString()); try { System.err.println(r.bodyString()); } catch (QiniuException ex2) { //ignore } } } //删除文件 public static void deleteFileFromQiniu(String fileName){ //构造一个带指定Zone对象的配置类 Configuration cfg = new Configuration(Zone.zone0()); String key = fileName; Auth auth = Auth.create(accessKey, secretKey); BucketManager bucketManager = new BucketManager(auth, cfg); try { bucketManager.delete(bucket, key); } catch (QiniuException ex) { //如果遇到异常,说明删除失败 System.err.println(ex.code()); System.err.println(ex.response.toString()); } } }
文章
存储  ·  Java  ·  应用服务中间件  ·  开发工具  ·  数据库  ·  Maven  ·  对象存储  ·  nginx  ·  开发者  ·  容器
2022-12-11
乾坤大挪移,如何将同步阻塞(sync)三方库包转换为异步非阻塞(async)模式?Python3.10实现。
众所周知,异步并发编程可以帮助程序更好地处理阻塞操作,比如网络 IO 操作或文件 IO 操作,避免因等待这些操作完成而导致程序卡住的情况。云存储文件传输场景正好包含网络 IO 操作和文件 IO 操作,比如业内相对著名的七牛云存储,官方sdk的默认阻塞传输模式虽然差强人意,但未免有些循规蹈矩,不够锐意创新。在全球同性交友网站Github上找了一圈,也没有找到异步版本,那么本次我们来自己动手将同步阻塞版本改造为异步非阻塞版本,并上传至Python官方库。异步改造首先参见七牛云官方接口文档:https://developer.qiniu.com/kodo,新建qiniu\_async.py文件:# @Author:Liu Yue (v3u.cn) # @Software:Vscode # @Time:2022/12/30 import base64 import hmac import time from hashlib import sha1 import json import httpx import aiofiles class Qiniu: def __init__(self, access_key, secret_key): """初始化""" self.__checkKey(access_key, secret_key) self.__access_key = access_key self.__secret_key = secret_key.encode('utf-8') def get_access_key(self): return self.__access_key def get_secret_key(self): return self.__secret_key def __token(self, data): hashed = hmac.new(self.__secret_key,data.encode('utf-8'), sha1) return self.urlsafe_base64_encode(hashed.digest()) def token(self, data): return '{0}:{1}'.format(self.__access_key, self.__token(data)) def token_with_data(self, data): data = self.urlsafe_base64_encode(data) return '{0}:{1}:{2}'.format( self.__access_key, self.__token(data), data) def urlsafe_base64_encode(self,data): if isinstance(data, str): data = data.encode('utf-8') ret = base64.urlsafe_b64encode(data) data = ret.decode('utf-8') return data @staticmethod def __checkKey(access_key, secret_key): if not (access_key and secret_key): raise ValueError('invalid key') def upload_token( self, bucket, key=None, expires=3600, policy=None, strict_policy=True): """生成上传凭证 Args: bucket: 上传的空间名 key: 上传的文件名,默认为空 expires: 上传凭证的过期时间,默认为3600s policy: 上传策略,默认为空 Returns: 上传凭证 """ if bucket is None or bucket == '': raise ValueError('invalid bucket name') scope = bucket if key is not None: scope = '{0}:{1}'.format(bucket, key) args = dict( scope=scope, deadline=int(time.time()) + expires, ) return self.__upload_token(args) @staticmethod def up_token_decode(up_token): up_token_list = up_token.split(':') ak = up_token_list[0] sign = base64.urlsafe_b64decode(up_token_list[1]) decode_policy = base64.urlsafe_b64decode(up_token_list[2]) decode_policy = decode_policy.decode('utf-8') dict_policy = json.loads(decode_policy) return ak, sign, dict_policy def __upload_token(self, policy): data = json.dumps(policy, separators=(',', ':')) return self.token_with_data(data) @staticmethod def __copy_policy(policy, to, strict_policy): for k, v in policy.items(): if (not strict_policy) or k in _policy_fields: to[k] = v这里有两个很关键的异步非阻塞三方库,分别是httpx和aiofiles,对应处理网络IO和文件IO阻塞问题:pip3 install httpx pip3 install aiofiles随后按照文档流程通过加密方法获取文件上传token,这里无须进行异步改造,因为并不涉及IO操作:q = Qiniu(access_key,access_secret) token = q.upload_token("空间名称") print(token)程序返回:➜ mydemo git:(master) ✗ /opt/homebrew/bin/python3.10 "/Users/liuyue/wodfan/work/mydemo/src/test.py" q06bq54Ps5JLfZyP8Ax-qvByMBdu8AoIVJpMco2m:8RjIo9a4CxHM3009DwjbMxDzlU8=:eyJzY29wZSI6ImFkLWgyMTEyIiwiZGVhZGxpbmUiOjE2NzIzNjg2NTd9接着添加文件流推送方法,先看官方原版逻辑:def put_data( up_token, key, data, params=None, mime_type='application/octet-stream', check_crc=False, progress_handler=None, fname=None, hostscache_dir=None, metadata=None): """上传二进制流到七牛 Args: up_token: 上传凭证 key: 上传文件名 data: 上传二进制流 params: 自定义变量,规格参考 https://developer.qiniu.com/kodo/manual/vars#xvar mime_type: 上传数据的mimeType check_crc: 是否校验crc32 progress_handler: 上传进度 hostscache_dir: host请求 缓存文件保存位置 metadata: 元数据 Returns: 一个dict变量,类似 {"hash": "<Hash string>", "key": "<Key string>"} 一个ResponseInfo对象 """ final_data = b'' if hasattr(data, 'read'): while True: tmp_data = data.read(config._BLOCK_SIZE) if len(tmp_data) == 0: break else: final_data += tmp_data else: final_data = data crc = crc32(final_data) return _form_put(up_token, key, final_data, params, mime_type, crc, hostscache_dir, progress_handler, fname, metadata=metadata) def _form_put(up_token, key, data, params, mime_type, crc, hostscache_dir=None, progress_handler=None, file_name=None, modify_time=None, keep_last_modified=False, metadata=None): fields = {} if params: for k, v in params.items(): fields[k] = str(v) if crc: fields['crc32'] = crc if key is not None: fields['key'] = key fields['token'] = up_token if config.get_default('default_zone').up_host: url = config.get_default('default_zone').up_host else: url = config.get_default('default_zone').get_up_host_by_token(up_token, hostscache_dir) # name = key if key else file_name fname = file_name if not fname or not fname.strip(): fname = 'file_name' # last modify time if modify_time and keep_last_modified: fields['x-qn-meta-!Last-Modified'] = rfc_from_timestamp(modify_time) if metadata: for k, v in metadata.items(): if k.startswith('x-qn-meta-'): fields[k] = str(v) r, info = http._post_file(url, data=fields, files={'file': (fname, data, mime_type)}) if r is None and info.need_retry(): if info.connect_failed: if config.get_default('default_zone').up_host_backup: url = config.get_default('default_zone').up_host_backup else: url = config.get_default('default_zone').get_up_host_backup_by_token(up_token, hostscache_dir) if hasattr(data, 'read') is False: pass elif hasattr(data, 'seek') and (not hasattr(data, 'seekable') or data.seekable()): data.seek(0) else: return r, info r, info = http._post_file(url, data=fields, files={'file': (fname, data, mime_type)}) return r, info这里官方使用两个方法,先试用put\_data方法将字符串转换为二进制文件流,随后调用\_form\_put进行同步上传操作,这里\_form\_put这个私有方法是可复用的,既兼容文件流也兼容文件实体,写法上非常值得我们借鉴,弄明白了官方原版的流程后,让我们撰写文件流传输的异步版本:# 上传文件流 async def upload_data(self,up_token, key,data,url="http://up-z1.qiniup.com",params=None,mime_type='application/octet-stream',file_name=None,metadata=None): data.encode('utf-8') fields = {} if params: for k, v in params.items(): fields[k] = str(v) if key is not None: fields['key'] = key fields['token'] = up_token fname = file_name if not fname or not fname.strip(): fname = 'file_name' async with httpx.AsyncClient() as client: # 调用异步使用await关键字 res = await client.post(url,data=fields,files={'file': (fname,data,mime_type)}) print(res.text)这里我们声明异步方法upload\_data,通过encode直接转换文件流,并使用异步httpx.AsyncClient()对象将文件流推送到官网接口地址:up-z1.qiniup.com 随后进行测试:import asyncio q = qiniu_async.Qiniu("accesskey","accesssecret") token = q.upload_token("空间名称") #文件流上传 asyncio.run(q.upload_data(token,"3343.txt","123测试"))程序返回:➜ mydemo git:(master) ✗ /opt/homebrew/bin/python3.10 "/Users/liuyue/wodfan/work/mydemo/src/test.py" {"hash":"FtnQXAXft5AsOH1mrmXGaRzSt-95","key":"33434.txt"}接口会返回文件流的hash编码,没有问题。 接着查看文件上传流程:def put_file(up_token, key, file_path, params=None, mime_type='application/octet-stream', check_crc=False, progress_handler=None, upload_progress_recorder=None, keep_last_modified=False, hostscache_dir=None, part_size=None, version=None, bucket_name=None, metadata=None): """上传文件到七牛 Args: up_token: 上传凭证 key: 上传文件名 file_path: 上传文件的路径 params: 自定义变量,规格参考 https://developer.qiniu.com/kodo/manual/vars#xvar mime_type: 上传数据的mimeType check_crc: 是否校验crc32 progress_handler: 上传进度 upload_progress_recorder: 记录上传进度,用于断点续传 hostscache_dir: host请求 缓存文件保存位置 version: 分片上传版本 目前支持v1/v2版本 默认v1 part_size: 分片上传v2必传字段 默认大小为4MB 分片大小范围为1 MB - 1 GB bucket_name: 分片上传v2字段 空间名称 metadata: 元数据信息 Returns: 一个dict变量,类似 {"hash": "<Hash string>", "key": "<Key string>"} 一个ResponseInfo对象 """ ret = {} size = os.stat(file_path).st_size with open(file_path, 'rb') as input_stream: file_name = os.path.basename(file_path) modify_time = int(os.path.getmtime(file_path)) if size > config.get_default('default_upload_threshold'): ret, info = put_stream(up_token, key, input_stream, file_name, size, hostscache_dir, params, mime_type, progress_handler, upload_progress_recorder=upload_progress_recorder, modify_time=modify_time, keep_last_modified=keep_last_modified, part_size=part_size, version=version, bucket_name=bucket_name, metadata=metadata) else: crc = file_crc32(file_path) ret, info = _form_put(up_token, key, input_stream, params, mime_type, crc, hostscache_dir, progress_handler, file_name, modify_time=modify_time, keep_last_modified=keep_last_modified, metadata=metadata) return ret, info这里官方使用的是标准库上下文管理器同步读取文件,改写为异步方法:# 上传文件实体 async def upload_file(self,up_token,key,path,url="http://up-z1.qiniup.com",params=None,mime_type='application/octet-stream',file_name=None,metadata=None): async with aiofiles.open(path, mode='rb') as f: contents = await f.read() fields = {} if params: for k, v in params.items(): fields[k] = str(v) if key is not None: fields['key'] = key fields['token'] = up_token fname = file_name if not fname or not fname.strip(): fname = 'file_name' async with httpx.AsyncClient() as client: # 调用异步使用await关键字 res = await client.post(url,data=fields,files={'file': (fname,contents,mime_type)}) print(res.text)通过aiofiles异步读取文件后,在通过httpx.AsyncClient()进行异步传输。 需要注意的是,这里默认传输到up-z1.qiniup.com接口,如果是不同区域的云存储服务器,需要更改url参数的值,具体服务器接口列表请参照官网文档。 至此,文件流和文件异步传输就改造好了。上传至Python官方库为了方便广大七牛云用户使用异步传输版本库,可以将qiniu-async上传到Python官方库,首先注册成为Python官方库的开发者:pypi.org/ 随后在项目根目录下新建setup.py文件:import setuptools import pathlib here = pathlib.Path(__file__).parent.resolve() long_description = (here / "README.md").read_text(encoding="utf-8") setuptools.setup( name="qiniu-async", version="1.0.1", author="LiuYue", author_email="zcxey2911@gmail.com", description="qiniu_async python library", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/qiniu-async", packages=setuptools.find_packages(), license="Apache 2.0", classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3 :: Only", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], keywords="qiniu, qiniu_async, async", py_modules=[ 'qiniu_async' ], install_requires=["aiofiles","httpx"], )这是安装文件,主要为了声明该模块的名称、作者、版本以及依赖库。 随后本地打包文件:python3 setup.py sdist程序会根据setup.py文件生成压缩包:➜ qiniu_async tree . ├── README.md ├── dist │ └── qiniu-async-1.0.1.tar.gz ├── https: │ └── github.com │ └── zcxey2911 │ └── qiniu-async.git ├── qiniu_async.egg-info │ ├── PKG-INFO │ ├── SOURCES.txt │ ├── dependency_links.txt │ ├── requires.txt │ └── top_level.txt ├── qiniu_async.py └── setup.py接着安装twine库, 准备提交Python官网:pip3 install twine随后在根目录运行命令提交:twine upload dist/*在官网进行查看:https://pypi.org/project/qiniu-async/ 随后本地就可以直接通过pip命令句进行安装了:pip install qiniu-async -i https://pypi.org/simple非常方便。结语云端存储,异步加持,猛虎添翼,未敢拥钵独飨,除了通过pip安装qiniu-async库,也奉上Github项目地址:https://github.com/zcxey2911/qiniu-async ,与众乡亲同飨。
文章
存储  ·  开发工具  ·  数据安全/隐私保护  ·  开发者  ·  Python
2023-02-21
AI画家第四弹——利用Flask发布风格迁移API
上篇文章介绍了python web开发中经常使用到的一个框架flask,如果有遗忘的,可以点此回顾AI画家第三弹——毕业设计大杀器之Flask,本文的主要任务就是完成上篇文章末尾的要求,利用Flask发布你自己的风格迁移API。本文源码可在微信公众号「01二进制」后台回复「风格迁移API」获得需求分析我们知道软件工程的第一步就是需求分析,放在这里就是要知道我们需要实现的功能是什么样的。我画了一张简陋的图来描述这次的需求:真的是很简陋的一张图啊,其实理解起来很容易,就是用户上传一张图片,Flask获取到这张图片,调用风格迁移的模型,然后生成结果图,在传递回前端即可。环境准备既然明白了需求,那么接下来要做的事情自然是环境搭建了,老样子,这里我们仍然使用Pipenv来创建虚拟环境,如何搭建pipenv环境我就不说了,在微信公众号「01二进制」后台回复「风格迁移API」获得源码之后直接在终端输入pipenv install即可。开始hello world我们首先在项目根目录创建一个main.py的文件作为整个项目的启动文件,上文我们说过,为了简化大型应用并为扩展提供集中的注册入口,我们并不会将所有的视图函数直接写在main.py,而是采用蓝图的方式分模块开发,因此我们需要在项目根目录新建app/文件夹,在其中的__init__.py中编写如下代码:from flask import Flask def create_app(): app = Flask(__name__) return app这样我们就可以通过在main.py中编写如下代码实现一个hello world应用了。from app import create_app app = create_app() @app.route('/') def hello(): return 'hello,world' if __name__ == '__main__': app.run(port=8080, debug=True)启动main.py即可发现项目启动了,在浏览器输入localhost:8080即可看到hello,world字样。蓝图编写我们肯定是不能满足于小小的hello world的,既然说到了模块化开发,那怎么个模块法?这里每个人的想法都是不一样的,其实也没有一个统一的标准,这里我就说下我自己的分级方法吧。stylize是项目根路径app是项目app/api是项目的api部分,一个项目肯定不只有api,还可能会有web等页面内容app/api/v1表明该api的版本是v1,当然日后也有可能会有v2、v3等等app/api/v1/img表明这里存放的都是和img有关的apiapp/api/v1/img/stylize.py表明这个文件存放的是风格迁移的视图函数认识完结构的划分之后,就来编写我们的蓝图吧。首先我们需要在app/api/v1/img/__init__.py中编写如下代码:from flask import Blueprint # 定义一个蓝图 img = Blueprint('img', __name__) from app.api.v1.img import stylize # 这段代码用来测试该接口是否可用 @img.route('/') def say_hello(): return '这里是图片处理类的接口'这样我们就定义了一个叫做img的蓝图,然后我们在app/api/v1/__init__.py中编写如下代码:from flask import Blueprint # 定义一个蓝图 v1 = Blueprint('v1', __name__) from app.api.v1.img import img这样我们就实现了v1蓝图的编写。那这样是不是就可以使用蓝图了呢?当然不是,我们还需要在app中配置这个蓝图,把蓝图加载到app中,否则flask是无法识别蓝图的。加载的方法也很简单,我们在app/__init__.py文件中添加一个函数:def register_blueprint(app): from app.api.v1 import v1 from app.api.v1.img import img app.register_blueprint(v1, url_prefix='/api/v1') app.register_blueprint(img, url_prefix='/api/v1/img')将这两个蓝图注册到app中,其中url_prefix这个参数用来标注路由的。有人可能不清楚,这里举个例子你就懂了。我们启动这个项目之后,在浏览器输入的是localhost:8080,如果加了url_prefix这个参数之后,我们访问img下的视图函数时就需要把路径改为localhost:8080/api/v1/img/了。然后在create_app()函数中调用这个方法即可,main.py的代码如下:from flask import Flask def create_app(): app = Flask(__name__) register_blueprint(app) return app def register_blueprint(app): from app.api.v1 import v1 from app.api.v1.img import img app.register_blueprint(v1, url_prefix='/api/v1') app.register_blueprint(img, url_prefix='/api/v1/img') if __name__ == '__main__': create_app()接下来我们测试下这个蓝图是否真的注册成功了,我们启动该项目,并打开Postman输入:localhost:8080/api/v1/img,我们可以看到如下信息:说明我们的蓝图已经注册编写风格迁移工具类既然蓝图都已经编写好了,那么视图函数的编写也就非常简单了,因此这里我们先把风格迁移的工具类写好,最后再编写视图函数。上上篇文章我们介绍了图像风格迁移,记不清的可以看这篇文章AI绘画第二弹——图像风格迁移,这篇文章介绍了最传统的图像风格迁移,想要生成一张图片的速度非常非常慢,肯定是没办法作为实际使用的,因此这篇文章所采用的生成风格迁移的图片的方法并不是这篇文章,而是基于李飞飞等人的一篇论文《Perceptual Losses forReal-Time Style Transfer and Super-Resolution》所实现的快速图像风格迁移。这里只介绍如何拿训练好的模型去运用,有兴趣的自己下载这篇文章去研究。我们新建一个文件夹:app/utils/stylize,这个文件夹中包含了风格迁移的工具类,项目结构如下:其中output为最终的生成文件夹、src存放风格迁移的文件(可以不用管)、evaluate.py是之前用于评估模型性能的文件(其实也就是生成图片的文件,不用管+1)、trained_model文件夹存放了我们已经训练好的模型、temp存放的是我们从前端上传的图片,create_stylize_photo.py包含的是我们对外提供的风格迁移工具类。因此这整个文件夹我们只需要关注create_stylize_photo.py这一个文件就可以了,其他的下载源码之后自己看就可以了。压缩图片# 压缩图片 def compress_image(): im = Image.open(content_image) if content_image.endswith(".png"): im = im.convert('P') im.save(content_image, optimize=True)选择图片风格即所需要使用的模型style_list = ['la_muse', 'rain_princess', 'scream', 'udnie', 'wave', 'wreck'] style = style_list[int(image_style)] # 模型的 checkpoint 的位置 check_point_dir = trained_models_path + style + '.ckpt'执行生成图片的操作# 最终生成的图片路径 result_image = path + '/output/' + 'output.jpg' # 执行生成图片的操作 ffwd_to_img(content_image, result_image, check_point_dir)执行完上述步骤后,我们就可以在utils/stylize/output中看到已经生成的风格迁移图片了。利用七牛云存储结果图片由于服务器带宽限制,我们最后返回给前端的结果肯定不能是我们自己服务器的url(毕竟学生机的带宽只有1M),所以这里我建议使用七牛云存储的功能将生成的结果保存到七牛云上,然后返回一个url即可。# 将生成的图片上传到七牛云 # 传入filename和filepath,返回图片的URL def upload_pic_to_qiniu(filename, filepath): from app.secure import QINIU_AK from app.secure import QINIU_SK access_key = QINIU_AK secret_key = QINIU_SK q = Auth(access_key, secret_key) # 要上传的空间 bucket_name = 'ytools' # 生成上传 Token,可以指定过期时间等 token = q.upload_token(bucket_name, filename, 3600) ret, info = put_file(token, filename, filepath) return BASE_URL + ret['key']然后我们在create_stylize_photo.py中加一句存储的代码即可。img_url = upload_pic_to_qiniu(filename, result_image)编写风格迁移API现在我们通过风格迁移工具类已经可以实现输入一张原始图片返回生成图片的URL的功能,现在我们来将目光聚焦到风格迁移API的编写上。定义路由@img.route('/stylize/create', methods=['POST']) def create_style_changed_img():接收参数我们定义一个函数create_style_changed_img(),方法采用POST方式,我们需要接受前端发来的两个参数,分别是img和typeimg = request.files.get('img') type = request.form.get('type')生成图片及URL然后我们将接收到的文件保存到之前新建的temp文件加中,然后调用工具类的方法返回图片的urlimg.save(path + '/temp/' + 'temp.jpg') img_url = change_style(int(type))定义返回格式作为一个好的api,我们肯定不只能返回一张图片的url就可以了,我们还需要记录下生成的时间,因此我们在代码执行的开始和结束的时候分别添加一段代码:start = datetime.datetime.now() end = datetime.datetime.now()然后我们再定义返回格式status = 200 msg = '图片生成成功' info = [ { 'img_url': img_url, 'created_time': get_date_now(), 'finish_time': (end - start).seconds } ]然后将结果返回res_json = Res(status, msg, info) return jsonify(res_json.__dict__)这里的Res是我定义的一个返回的实体信息类,长得是下面这样class Res: status = 200 msg = '' info = [] def __init__(self, status, msg, info): self.status = status self.msg = msg self.info = info最后我们调用flask的jsonify方法,就可以返回json结果。测试到这里我们就已经完成了一个风格迁移API的编写,接下来我们测试下我们的API吧,首先先启动项目,然后打开Postman,将请求方法改为post,添加两个参数img和type,如下:选择图片的时候,图片的质量尽量不要太大,否则可能会出现卡死的情况最后的返回结果如下:{ "info": [ { "created_time": "2019-05-15 15:17:25", "finish_time": 2, "img_url": "https://user-gold-cdn.xitu.io/2019/5/15/16aba682a31521a7?w=640&h=360&f=jpeg&s=39700" } ], "msg": "图片生成成功", "status": 200 }访问该URL即可看到如下图片(感觉还蛮好看的):至此我们已经实现了利用Flask发布一个风格迁移API了。总结本文源码可在微信公众号「01二进制」后台回复「风格迁移API」获得最后总结下,在这篇文章中介绍了如何利用Flask发布一个风格迁移API,其中我们介绍了应该如何利用蓝图进行模块化开发,并给出了我自己认为的比较好的分层方法,同时利用七牛云存储为我们的服务器减压,最后利用postman请求该API完成测试。
文章
存储  ·  人工智能  ·  JSON  ·  前端开发  ·  API  ·  数据格式  ·  Python
2023-01-16
Python3.7+jieba(结巴分词)配合Wordcloud2.js来构造网站标签云(关键词集合)
其实很早以前就想搞一套完备的标签云架构了,迫于没有时间(其实就是懒),一直就没有弄出来完整的代码,说到底标签对于网站来说还是很重要的,它能够对一件事物产生标志性描述,通常都会采用相关性很强的关键字,这样不仅便于检索和分类,同时对网站的内链体系也是有促进作用的。最近疫情的关系一直在家里呆着,闲暇时和一些学生聊天的时候,人家问:你说你一直在写博客,那你到底在写一些什么内容的文章呢?我竟然一时语塞,于是搞出来下面这种的标签云,下回被问同样的问题时,就可以展示一下了。和传统的在线博客标签云最大的区别在于,这些标签并不是我手动打上去的,因为时间有限,每写一篇文章就自己提取很多关键字出来,还得挨个入库,这件事想想就很痛苦,于是写脚本自动提取关键字,再综合所有文章的标题得出。这里用到的技术点就是基于python3.7的结巴分词中的提取关键词,首先进行安装pip3 install jieba结巴分词基于TF-IDF关键词提取算法 TF-IDF是关键词提取最基本、最简单易懂的方法。判断一个词再一篇文章中是否重要,一个最容易想到的衡量指标就是词频,重要的词往往在文章中出现的频率也非常高;但另一方面,不是出现次数越多的词就一定重要,因为有些词在各种文章中都频繁出现(例如:我们),那它的重要性肯定不如哪些只在某篇文章中频繁出现的词重要性强。从统计学的角度,就是给予那些不常见的词以较大的权重,而减少常见词的权重,最终得分较高的词语即为关键词。与此同时,结巴分词还可以帮你过滤那些无意义的虚词,类似「的、地、得、着、了、过」这种代码如下:import jieba.analyse data = "其实很早以前就想搞一套完备的标签云架构了,迫于没有时间(其实就是懒),一直就没有弄出来完整的代码,说到底标签对于网站来说还是很重要的,它能够对一件事物产生标志性描述,通常都会采用相关性很强的关键字,这样不仅便于检索和分类,同时对网站的内链体系也是有促进作用的。最近疫情的关系一直在家里呆着,闲暇时和一些学生聊天的时候,人家问:你说你一直在写博客,那你到底在写一些什么内容的文章呢?我竟然一时语塞,于是搞出来下面这种的标签云,下回被问同样的问题时,就可以展示一下了。" for keyword, weight in jieba.analyse.extract_tags(data, withWeight=True): print('%s %s' % (keyword, weight))默认会直接提取前20个关键词,按照权重倒序:标签 0.36316568234921054 一直 0.17986207627776318 网站 0.17220419499 内链 0.15729957240657894 弄出来 0.13730186512105264 语塞 0.13539157551710526 其实 0.13493691317526316 下回 0.1301755850886842 很早以前 0.12859925351223683 关键字 0.1277766172361842 检索 0.1236956313375 闲暇 0.1223093087630263 标志性 0.12002044945868422 迫于 0.11836056412552631 相关性 0.11816706218618422 架构 0.11760306607526315 促进作用 0.11620754539157895 说到底 0.11464857692289475 博客 0.11084492236894737 聊天 0.11041170151776317看起来还是相当靠谱。如果你需要修改关键词数量,可以指定topK参数,输入几个就返回几个那么只要在文章提交时加入上面的脚本,就可以每一次都自动生成关键词了,当然了,关键词的存储结构设计也是一个难题,有时间会探讨一下。有了文章和关键词的关联关系,那么就剩下前端的展示,这里推荐一下台湾同胞写的一个控件:https://github.com/timdream/wordcloud2.js效果还是非常赞的,该插件作者谦逊的说自己”可能“是最好的标签云插件,不过我个人认为可能这两个字完全可以去掉了,它就是最好的。那么wordcloud2.js使用起来也非常简单,导入js文件后,按照官方文档使用即可,这里附上代码<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Demo</title> </head> <style> #container{ width: 1000px; margin: 100px auto; border: 1px solid #ccc; } </style> <body> <h1>一般</h1> <div id="container" style="height: 500px;"></div> <!-- <h1>无数据</h1> <div id="container1" style="height: 400px;"></div> --> <script src="./wordcloud2.min.js"></script> <script> var wordFreqData = [['使用', 28, 114], ['Mac', 17, 107], ['Python', 15, 1], ['实现', 13, 121], ['python3.7', 13, 157], ['Django2.0', 12, 283], ['利用',11, 202], ['阿里', 10, 187], ['开发', 10, 230], ['关于', 10, 51], ['文件', 9, 215], ['基于', 9, 199], ['Centos7.6', 9, 455], ['配置', 9, 141], ['部署', 8, 323], ['Docker', 8, 479], ['系统', 8, 112], ['vue', 8, 425], ['异步', 8, 162], ['服务', 8, 356], ['安装', 7, 102], ['nginx', 7, 195], ['微信', 7, 707], ['程序', 7, 711], ['进行', 7, 381], ['问题', 7, 52], ['上传', 6, 171], ['mysql', 6, 86], ['以及', 6, 446], ['一个', 6, 14], ['响应', 5, 54], ['compose', 5, 685], ['python3', 5, 414], ['Django', 5, 219], ['搭建', 5, 514], ['性能', 5, 9], ['功能', 5, 119], ['环境', 5, 231], ['OS', 4, 123], ['遇到', 4, 4], ['检测', 4, 41], ['配合', 4, 200], ['redis', 4, 290], ['网站', 4, 324], ['页面', 4, 46], ['记录', 4, 27], ['Tornado', 4, 588], ['测试', 4, 377], ['接口', 4, 399], ['操作', 4, 31], ['js', 4, 207], ['前端', 4, 432], ['应用', 4, 78], ['centos', 4, 100], ['解决方案', 3, 35], ['登录', 3, 361], ['js2.6', 3, 464], ['项目', 3, 552], ['Flask', 3, 252], ['商城', 3, 341], ['实时', 3, 342], ['视频', 3, 471], ['面试', 3, 626], ['api', 3, 884], ['设计', 3, 58], ['打造', 3, 198], ['切换', 3, 155], ['分离', 3, 561], ['最新', 3, 64], ['如何', 3, 130], ['爬虫', 3, 45], ['结合', 3, 371], ['框架', 3, 26], ['自己', 3, 204], ['支付宝', 3, 397], ['布局', 3, 70], ['博客', 3, 186], ['模式', 3, 529], ['并且', 3, 190], ['支付', 3, 401], ['代码', 3, 15], ['Go', 2, 192], ['Linux', 2, 438], ['Lang', 2, 193], ['集成', 2, 410], ['Windows', 2, 439], ['ffmpeg', 2, 470],['全文检索', 2, 727], ['Redisearch', 2, 728], ['变量', 2, 39], ['celery', 2, 315], ['添加', 2, 343], ['Centos7', 2, 413], ['模块', 2, 21], ['各种', 2, 84], ['静态', 2, 197], ['跨域', 2, 284], ['读写', 2, 558], ['前后', 2, 700], ['自定义', 2, 794], ['存储', 2, 888],['Home', 2, 288], ['服务器', 2, 321], ['统一', 2, 701], ['github', 2, 23], ['Brew', 2, 289], ['FastDfs', 2, 513], ['管理', 2, 704], ['中文', 2, 87], ['版本', 2, 156], ['mpvue', 2, 706], ['方案', 2, 65], ['属于', 2, 201], ['文档', 2, 222], ['Tornado5.1', 2, 158], ['https', 2, 184], ['下载', 2, 223], ['方法', 2, 10], ['移动', 2, 69], ['引擎', 2, 92], ['不要', 2, 113], ['h5', 2, 329], ['并发', 2, 374], ['一下', 2, 28], ['Selenium', 2, 139], ['Rabbitmq3.7', 2, 160], ['插件', 2, 205], ['通过', 2, 226], ['Mongodb', 2, 297], ['删除', 2, 332], ['新版', 2, 398], ['excel', 2, 30], ['美多', 2, 335], ['集群', 2, 767], ['用来', 2, 13], ['适应', 2, 74], ['总结', 2, 303], ['压力', 2, 379], ['播放', 2, 490], ['上面', 2, 542], ['可用', 2, 770], ['技巧', 2, 98], ['js2.0', 2, 945], ['Scrapy', 2, 144], ['过程', 2, 191], ['http', 2, 209], ['推送', 2, 339], ['Supervisor', 2, 497], ['工具', 1, 16], ['之子', 1, 53], ['FLOAT', 1, 79], ['python2.7', 1, 101], ['Operation', 1, 145], ['快捷键', 1, 166], ['压缩', 1, 211], ['图形化', 1, 233], ['中文翻译', 1, 251], ['password', 1, 276], ['Score', 1, 307], ['客服', 1, 340], ['vscode', 1, 383], ['2019.04', 1, 406], ['监控', 1, 499], ['tree', 1, 599], ['localStorage', 1, 624], ['格式', 1, 649], ['个人', 1, 678], ['这样', 1, 725], ['mpvue1.0', 1, 780], ['双机', 1, 825], ['浏览器', 1, 857], ['加上', 1, 881], ['截说', 1, 919], ['云云', 1, 950], ['验证码', 1, 996], ['检查', 1, 17], ['显示', 1, 36], ['坍塌', 1, 80], ['预装', 1,122], ['permitted', 1, 146], ['五年', 1, 169], ['请求', 1, 212], ['git', 1, 234], ['YES', 1, 277], ['大名鼎鼎', 1, 308], ['密码', 1,362], ['中为', 1, 384], ['各类', 1, 501], ['master', 1, 555], ['前缀', 1, 600], ['弄清楚', 1, 625], ['电子书', 1, 650], ['协程', 1, 789], ['keepalived', 1, 828], ['从无到有', 1, 858], ['七牛云', 1, 883], ['简短', 1, 921], ['sublime3', 1, 957], ['百度', 1, 998], ['MySQLdb', 1, 19], ['成员', 1, 37], ['数目', 1, 55], ['原理', 1, 82], ['Anaconda', 1, 103], ['shell', 1, 148], ['Hugo', 1, 194], ['效率', 1, 213], ['界面', 1, 235], ['手册', 1, 253], ['进入', 1, 282], ['制度', 1, 309], ['记住', 1, 363], ['误报', 1, 386], ['Centos6', 1, 412], ['pip', 1, 440], ['uwsgi', 1, 506], ['slave', 1, 556], ['索引', 1, 601], ['10g', 1, 654], ['反向', 1, 696], ['原生', 1, 792],['热备', 1, 829], ['调试', 1, 859], ['阐述', 1, 922], ['win10', 1, 958], ['模拟', 1, 1001], ['import', 1, 2], ['autocommit', 1, 20],['元素', 1, 57], ['居中', 1, 83], ['东西', 1, 104], ['10.11', 1, 124], ['脚本', 1, 149], ['千万', 1, 173], ['推荐', 1, 236], ['虚拟环境', 1, 256], ['常用', 1, 364], ['专属', 1, 387], ['缓存', 1, 441], ['针对', 1, 472], ['负载', 1, 508], ['主从', 1, 557], ['原则', 1, 602], ['场景', 1, 627], ['50g', 1, 655], ['代理', 1, 697], ['交互', 1, 736], ['阻塞', 1, 793], ['上用', 1, 836], ['谷歌', 1, 860], ['切分', 1, 886], ['事务', 1, 923], ['vue2.0', 1, 960], ['镜像', 1, 1003], ['惨案', 1, 3], ['函数', 1, 40], ['发现', 1, 105], ['El', 1, 125], ['知识', 1, 151], ['涉及', 1, 174], ['减少', 1, 216], ['双撇号', 1, 238], ['之中', 1, 258], ['那些', 1, 317], ['通信', 1, 344], ['文本编辑', 1, 366], ['语法', 1, 388], ['节约', 1, 442], ['一些', 1, 475], ['均衡', 1, 510], ['联合', 1, 603], ['彻底', 1, 628],['1t', 1, 656], ['高性能', 1, 737], ['抓包', 1, 837], ['半小时', 1, 861], ['拍云', 1, 925], ['注册码', 1, 961], ['下载速度', 1, 1004], ['15', 1, 22], ['笔记', 1, 59], ['乱码', 1, 85], ['Capitan', 1, 126], ['共存', 1, 154], ['业务', 1, 175], ['提高', 1, 218], ['关键字', 1, 239], ['SQLAlchemy', 1, 261], ['主动', 1, 346], ['KindEditor', 1, 368], ['提醒', 1, 390], ['神坑', 1, 415], ['用以', 1, 444], ['直播', 1, 478], ['分布式文件系统', 1, 512], ['数据库', 1, 559], ['单元测试', 1, 608], ['区别', 1, 629], ['花式', 1, 657], ['虚假', 1, 743], ['CSS3', 1, 795], ['Charles', 1, 838], ['一款', 1, 862], ['扫码', 1, 893], ['cdn', 1, 926], ['破解', 1, 962], ['Homebrew',1, 1007], ['遍历', 1, 6], ['是否', 1, 42], ['适配', 1, 62], ['默认', 1, 109], ['升级', 1, 128], ['公司', 1, 177], ['保留', 1, 240], ['基础', 1, 264], ['win', 1, 348], ['4.1', 1, 369], ['防止', 1, 391], ['处理', 1, 416], ['空间', 1, 445], ['mock', 1, 609], ['架构图', 1, 631], ['读取', 1, 658], ['真实', 1, 744], ['暗黑', 1, 796], ['抓取', 1, 840], ['扩展', 1, 863], ['第三方', 1, 896], ['全网', 1, 927], ['2020', 1, 971], ['换成', 1, 1009], ['字典', 1, 7], ['py', 1, 24], ['存在', 1, 44], ['倒霉', 1, 110], ['dict', 1, 179], ['xlwt', 1, 220], ['对于', 1, 242], ['优酷', 1, 265], ['Process', 1, 350], ['11', 1, 370], ['2019', 1, 394], ['kindeditor4.11', 1, 419], ['rtmp', 1, 481], ['谈谈', 1, 566], ['cProfile', 1, 610], ['搞清楚', 1, 633], ['Authentication', 1, 664], ['容器', 1, 749], ['属性', 1,798], ['水印', 1, 841], ['关联', 1, 867], ['Picture', 1, 898], ['评测', 1, 928], ['攻略', 1, 972], ['国内', 1, 1011], ['对比', 1, 8], ['受欢迎', 1, 25], ['终端', 1, 88], ['最好', 1, 111], ['pytest', 1, 133], ['key', 1, 180], ['情况', 1, 243], ['大误', 1, 266], ['守护', 1, 291], ['协议', 1, 325], ['Worker', 1, 351], ['私钥', 1, 395], ['解决', 1, 423], ['3.7', 1, 448], ['推流', 1, 483], ['Hbuilder', 1, 524], ['优化', 1, 567], ['轻量', 1, 613], ['绘制', 1, 635], ['Basic', 1, 665], ['flex', 1, 754], ['celery4.1', 1, 804], ['高清', 1, 842], ['尝试', 1, 868], ['画中画', 1, 899], ['免费', 1, 929], ['机器人', 1, 974], ['增加', 1, 1012], ['转载', 1, 68], ['查看', 1, 89], ['setuptools', 1, 134], ['出现', 1, 244], ['几年', 1, 267], ['进程', 1, 293], ['Uploadify3.0', 1, 328], ['exited', 1, 352], ['ApacheBench', 1, 373], ['公钥', 1, 396], ['Alipay', 1, 449], ['Video', 1, 485], ['app', 1, 525], ['具体', 1, 570], ['Hexo', 1, 614],['怎样', 1, 636], ['Auth', 1, 666], ['弹性', 1, 757], ['封装', 1, 807], ['软件', 1, 844], ['公众', 1, 869], ['技术', 1, 907], ['加速', 1, 930], ['递归', 1, 978], ['禁止', 1, 47], ['wget', 1, 135], ['Celery3.1', 1, 159], ['解析', 1, 185], ['方式', 1, 225], ['弹指一挥间', 1, 245], ['Access', 1, 270], ['设置', 1, 294], ['exitcode', 1, 353], ['resource', 1, 427], ['refund', 1, 450], ['播放器', 1, 486], ['混合', 1, 528], ['Siege', 1, 573], ['高逼格', 1, 615], ['到底', 1, 637], ['Oauth2', 1, 667], ['9012', 1, 715], ['Sentinel', 1, 765], ['架构', 1, 808], ['手机', 1, 846], ['消息', 1, 874], ['opencv4.1', 1, 910], ['长期', 1, 932], ['层级', 1, 983], ['装逼', 1, 11], ['控件', 1, 48], ['sql', 1, 95], ['宴席', 1, 246], ['denied', 1, 271], ['duplicate', 1, 354], ['秒杀', 1, 375], ['绑定', 1, 429], ['退款', 1, 452], ['挂载', 1, 487], ['简易', 1, 617], ['支撑', 1, 638], ['jwt', 1, 668], ['Thrift', 1, 716], ['哨兵', 1, 766], ['面试题', 1, 811], ['体验版', 1, 847], ['live2d', 1, 875], ['人脸识别', 1, 911], ['Apache', 1, 936], ['组件', 1, 984], ['利器', 1, 12], ['选择器', 1, 49], ['异同', 1, 72], ['排序', 1, 96], ['vim', 1, 115], ['Webdriver', 1, 140], ['16', 1, 161], ['grunt', 1, 206], ['rails', 1, 228], ['不散', 1, 247], ['user', 1, 272], ['常用命令', 1, 302], ['下面', 1, 355], ['双向', 1, 431], ['分发', 1, 489], ['分布式', 1, 538], ['MindMaster', 1, 593], ['建立', 1, 618], ['互转', 1, 641], ['认证', 1, 669], ['RPC', 1, 717], ['Weui', 1, 813], ['openId', 1, 849], ['博君', 1, 876], ['人脸', 1, 912], ['httpd', 1, 937], ['无限', 1, 985], ['checkbox', 1, 50], ['查询', 1, 97], ['自动', 1, 118], ['迁移', 1, 188], ['感恩', 1, 248], ['root', 1, 273], ['websocket', 1, 337], ['生成', 1, 400], ['nginx1.16', 1, 456], ['导图', 1, 594], ['session', 1, 621], ['word', 1, 643], ['进化', 1, 670], ['拯救', 1, 720], ['题库', 1, 814], ['获取', 1, 850], ['挂件', 1,877], ['模型', 1, 913], ['上将', 1, 940], ['分类', 1, 986], ['中文字体', 1, 33], ['rem', 1, 75], ['报错', 1, 142], ['队列', 1, 163],['css', 1, 208], ['六年', 1, 249], ['localhost', 1, 274], ['等级分', 1, 304], ['聊天室', 1, 338], ['Iterm', 1, 359], ['机制', 1, 434], ['uwsgi2.0', 1, 457], ['网络', 1, 492], ['Jenkins', 1, 545], ['脑图', 1, 595], ['cookie', 1, 622], ['pdf', 1, 644], ['用户', 1, 671], ['传统', 1, 722], ['Motor', 1, 773], ['正式', 1, 819], ['不到', 1, 852], ['一晒', 1, 878], ['训练', 1, 914], ['结构', 1, 988], ['绘图', 1, 34], ['完成', 1, 120], ['任务', 1, 164], ['版本控制', 1, 232], ['没有', 1, 250], ['using', 1, 275], ['Elo', 1, 306], ['账号', 1, 360], ['语法错误', 1, 382], ['最新版', 1, 403], ['安装包', 1, 436], ['18', 1, 458], ['自动化', 1, 549], ['思维', 1, 596], ['sessionStorage', 1, 623], ['兼容', 1, 648], ['Pelican', 1, 673], ['需要', 1, 724], ['提高效率', 1, 777], ['发布', 1, 822], ['Chrome', 1, 856], ['动态', 1, 879], ['特征', 1, 915], ['七牛', 1, 948], ['ai', 1, 993]]; var canvas = document.getElementById('container'); var options = eval({ "list": wordFreqData, "gridSize": 9, // 密集程度 数字越小越密集 "word":'v3u', //"shape" : "pentagon", drawOutOfBound: false, fontWeight:700, maxRotation: 40 * Math.PI / 180, minRotation: -40 * Math.PI / 180, drawMask: false, "weightFactor": 1, "color": 'random-light', // 字体颜色 'random-dark' 或者 'random-light' "backgroundColor": 'black', // 背景颜色 "rotateRatio": 1, // 字体倾斜(旋转)概率,1代表总是倾斜(旋转) click: function(item) { alert(item[0] + ': ' + item[2]); } }); //生成 WordCloud(canvas, options); </script> </body> </html>
文章
存储  ·  自然语言处理  ·  算法  ·  前端开发  ·  JavaScript
2023-01-23
传智健康项目中相关知识点介绍(如图片存储,发送短信,定时调度,统计报表...)(一)
1. 图片存储方案1.1 介绍在实际开发中,我们会有很多处理不同功能的服务器。例如:应用服务器:负责部署我们的应用数据库服务器:运行我们的数据库文件服务器:负责存储用户上传文件的服务器分服务器处理的目的是让服务器各司其职,从而提高我们项目的运行效率。常见的图片存储方案:方案一:使用nginx搭建图片服务器方案二:使用开源的分布式文件存储系统,例如Fastdfs、HDFS等方案三:使用云存储,例如阿里云、七牛云等1.2 七牛云存储七牛云(隶属于上海七牛信息技术有限公司)是国内领先的以视觉智能和数据智能为核心的企业级云计算服务商,同时也是国内知名智能视频云服务商,累计为 70 多万家企业提供服务,覆盖了国内80%网民。围绕富媒体场景推出了对象存储、融合 CDN 加速、容器云、大数据平台、深度学习平台等产品、并提供一站式智能视频云解决方案。为各行业及应用提供可持续发展的智能视频云生态,帮助企业快速上云,创造更广阔的商业价值。官网:https://www.qiniu.com/通过七牛云官网介绍我们可以知道其提供了多种服务,我们主要使用的是七牛云提供的对象存储服务来存储图片。1.2.1 注册、登录要使用七牛云的服务,首先需要注册成为会员。地址:https://portal.qiniu.com/signup注册完成后就可以使用刚刚注册的邮箱和密码登录到七牛云:登录成功后点击页面右上角管理控制台:注意:登录成功后还需要进行实名认证才能进行相关操作。1.2.2 新建存储空间要进行图片存储,我们需要在七牛云管理控制台新建存储空间。点击管理控制台首页对象存储下的立即添加按钮,页面跳转到新建存储空间页面:可以创建多个存储空间,各个存储空间是相互独立的。1.2.3 查看存储空间信息存储空间创建后,会在左侧的存储空间列表菜单中展示创建的存储空间名称,点击存储空间名称可以查看当前存储空间的相关信息1.2.4 开发者中心可以通过七牛云提供的开发者中心学习如何操作七牛云服务,地址:https://developer.qiniu.com/点击对象存储,跳转到对象存储开发页面,地址:https://developer.qiniu.com/kodo七牛云提供了多种方式操作对象存储服务,本项目采用Java SDK方式,地址:https://developer.qiniu.com/kodo/sdk/1239/java使用Java SDK操作七牛云需要导入如下maven坐标:<dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>7.2.0</version> </dependency> 1.2.5 鉴权Java SDK的所有的功能,都需要合法的授权。授权凭证的签算需要七牛账号下的一对有效的Access Key和Secret Key,这对密钥可以在七牛云管理控制台的个人中心(https://portal.qiniu.com/user/key)获得,如下图:1.2.6 Java SDK操作七牛云本章节我们就需要使用七牛云提供的Java SDK完成图片上传和删除,我们可以参考官方提供的例子//构造一个带指定Zone对象的配置类 Configuration cfg = new Configuration(Zone.zone0()); //...其他参数参考类注释 UploadManager uploadManager = new UploadManager(cfg); //...生成上传凭证,然后准备上传 String accessKey = "your access key"; String secretKey = "your secret key"; String bucket = "your bucket name"; //如果是Windows情况下,格式是 D:\\qiniu\\test.png String localFilePath = "/home/qiniu/test.png"; //默认不指定key的情况下,以文件内容的hash值作为文件名 String key = null; Auth auth = Auth.create(accessKey, secretKey); String upToken = auth.uploadToken(bucket); try { Response response = uploadManager.put(localFilePath, key, upToken); //解析上传成功的结果 DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class); System.out.println(putRet.key); System.out.println(putRet.hash); } catch (QiniuException ex) { Response r = ex.response; System.err.println(r.toString()); try { System.err.println(r.bodyString()); } catch (QiniuException ex2) { //ignore } } //构造一个带指定Zone对象的配置类 Configuration cfg = new Configuration(Zone.zone0()); //...其他参数参考类注释 String accessKey = "your access key"; String secretKey = "your secret key"; String bucket = "your bucket name"; String key = "your file key"; Auth auth = Auth.create(accessKey, secretKey); BucketManager bucketManager = new BucketManager(auth, cfg); try { bucketManager.delete(bucket, key); } catch (QiniuException ex) { //如果遇到异常,说明删除失败 System.err.println(ex.code()); System.err.println(ex.response.toString()); } 1.2.7 封装工具类为了方便操作七牛云存储服务,我们可以将官方提供的案例简单改造成一个工具类,在我们的项目中直接使用此工具类来操作就可以:package com.itheima.utils; import com.google.gson.Gson; import com.qiniu.common.QiniuException; import com.qiniu.common.Zone; import com.qiniu.http.Response; import com.qiniu.storage.BucketManager; import com.qiniu.storage.Configuration; import com.qiniu.storage.UploadManager; import com.qiniu.storage.model.DefaultPutRet; import com.qiniu.util.Auth; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; /** * 七牛云工具类 */ public class QiniuUtils { public static String accessKey = "dulF9Wze9bxujtuRvu3yyYb9JX1Sp23jzd3tO708"; public static String secretKey = "vZkhW7iot3uWwcWz9vXfbaP4JepdWADFDHVLMZOe"; public static String bucket = "qiniutest"; public static void upload2Qiniu(String filePath,String fileName){ //构造一个带指定Zone对象的配置类 Configuration cfg = new Configuration(Zone.zone0()); UploadManager uploadManager = new UploadManager(cfg); Auth auth = Auth.create(accessKey, secretKey); String upToken = auth.uploadToken(bucket); try { Response response = uploadManager.put(filePath, fileName, upToken); //解析上传成功的结果 DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class); } catch (QiniuException ex) { Response r = ex.response; try { System.err.println(r.bodyString()); } catch (QiniuException ex2) { //ignore } } } //上传文件 public static void upload2Qiniu(byte[] bytes, String fileName){ //构造一个带指定Zone对象的配置类 Configuration cfg = new Configuration(Zone.zone0()); //...其他参数参考类注释 UploadManager uploadManager = new UploadManager(cfg); //默认不指定key的情况下,以文件内容的hash值作为文件名 String key = fileName; Auth auth = Auth.create(accessKey, secretKey); String upToken = auth.uploadToken(bucket); try { Response response = uploadManager.put(bytes, key, upToken); //解析上传成功的结果 DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class); System.out.println(putRet.key); System.out.println(putRet.hash); } catch (QiniuException ex) { Response r = ex.response; System.err.println(r.toString()); try { System.err.println(r.bodyString()); } catch (QiniuException ex2) { //ignore } } } //删除文件 public static void deleteFileFromQiniu(String fileName){ //构造一个带指定Zone对象的配置类 Configuration cfg = new Configuration(Zone.zone0()); String key = fileName; Auth auth = Auth.create(accessKey, secretKey); BucketManager bucketManager = new BucketManager(auth, cfg); try { bucketManager.delete(bucket, key); } catch (QiniuException ex) { //如果遇到异常,说明删除失败 System.err.println(ex.code()); System.err.println(ex.response.toString()); } } } 2. 短信发送2.1 短信服务介绍目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信)对接,我们只需要注册成为会员并且按照提供的开发文档进行调用就可以发送短信。需要说明的是这些短信服务都是收费的服务。本项目短信发送我们选择的是阿里云提供的短信服务。短信服务(Short Message Service)是阿里云为用户提供的一种通信服务的能力,支持快速发送短信验证码、短信通知等。 三网合一专属通道,与工信部携号转网平台实时互联。电信级运维保障,实时监控自动切换,到达率高达99%。短信服务API提供短信发送、发送状态查询、短信批量发送等能力,在短信服务控制台上添加签名、模板并通过审核之后,可以调用短信服务API完成短信发送等操作。2.2 注册阿里云账号阿里云官网:https://www.aliyun.com/点击官网首页免费注册跳转到如下注册页面:2.3 设置短信签名注册成功后,点击登录按钮进行登录。登录后进入短信服务管理页面,选择国内消息菜单:点击添加签名按钮:目前个人用户只能申请适用场景为验证码的签名2.4 设置短信模板在国内消息菜单页面中,点击模板管理标签页:点击添加模板按钮:2.5 设置access keys在发送短信时需要进行身份认证,只有认证通过才能发送短信。本小节就是要设置用于发送短信时进行身份认证的key和密钥。鼠标放在页面右上角当前用户头像上,会出现下拉菜单:点击accesskeys:点击开始使用子用户AccessKey按钮:创建成功,其中AccessKeyID为访问短信服务时使用的ID,AccessKeySecret为密钥。可以在用户详情页面下禁用刚刚创建的AccessKey:可以设置每日和每月短信发送上限:由于短信服务是收费服务,所以还需要进行充值才能发送短信:
文章
存储  ·  Java  ·  BI  ·  应用服务中间件  ·  API  ·  调度  ·  开发工具  ·  数据库  ·  对象存储  ·  开发者
2022-12-11
开源测试平台横向测评系列『流马』篇:流马部署
前言我是从今年5月份第一次接触流马这个平台。第一次听到这个名字的时候,就觉得挺有趣的,猜测其名字应该是取自诸葛亮的“木牛流马”,后来和作者证实了一下,确实如此。当初诸葛亮发明木牛流马是为了提高运输效率,而流马测试平台是为了提高测试效率,可以说这个名字取得“恰到好处”。本文一万两千字左右,我写了好多天,可能是我耗时最久的一篇文章。其实写文章不是最难的,难的是边学习、边摸索、边踩坑、边解决问题、边写文章记录、边总结。所以写得还算是比较用心的,整体来说也比较详细。读起来可能会有点长,大家可以先关注收藏、后期有时间、空下来了再照着文章内容仔细研究。内容大致分为以下四个部分:【简介篇】项目概述:技术栈、工作原理项目功能简介:功能特点【部署篇】部署规划依赖环境部署(JDK、MySQ、NGINX、Git、NodeJS、Python3)代码打包:克隆项目、前端代码打包、后端代码打包项目部署:前端部署、后端部署、执行引擎部署【简介篇】以下项目概述及功能介绍内容来自官网及GitHub项目介绍一、项目概述流马是一款低代码自动化测试平台,旨在采用最简单的架构统一支持API/WebUI/AppUI的自动化测试。平台采用低代码设计模式,将传统测试脚本以配置化实现,从而让代码能力稍弱的用户快速上手自动化测试。同时平台也支持通过简单的代码编写实现自定义组件,使用户可以灵活实现自己的需求。项目分为平台端和引擎端,采用分布式执行设计,可以将测试执行的节点(即引擎)注册在任意环境的任意一台机器上,从而突破资源及网络限制。同时,通过将引擎启动在本地PC上,方便用户快速调试测试用例,实时查看执行过程,带来传统脚本编写一致的便捷。官网:http://www.liumatest.cn/代码地址:https://github.com/Chras-fu/Liuma-platform部署文档:https://docs.qq.com/doc/p/c989fa8bf467eca1a1e0fa59b32ceab017407168使用手册:https://docs.qq.com/doc/p/1e36932d41b40df896c1627a004068df9a28fc3f平台技术栈:前端VUE+ElementUI,后台Java+SpringBoot,测试引擎Python。二、功能介绍1.API测试支持单接口测试和链路测试。 支持接口统一管理,支持swagger导入。 支持一键生成字段校验的接口健壮性用例。 支持全局变量、关联、断言、内置函数、自定义函数。 支持前后置脚本、失败继续、超时时间、等待/条件/循环等逻辑控制器。 支持环境与用例解耦,多种方式匹配域名,让一套用例可以在多个环境上执行。 2.WebUI测试支持关键字驱动,零代码编写用例。 支持UI元素统一管理,Excel模板批量导入。 支持自定义关键字,封装公共的操作步骤,提升用例可读性。 支持本地引擎执行,实时查看执行过程。 支持与API用例在同一用例集合顺序执行。 3.AppUI测试(1.1版本上线)支持WebUI同等用例编写和执行能力支持安卓和苹果系统支持持真机管理、投屏和在线操作 支持控件元素在线获取,一键保存元素 支持实时查看执行过程【部署篇】一、部署说明1.部署说明官方部署文档地址:https://docs.qq.com/doc/p/c989fa8bf467eca1a1e0fa59b32ceab017407168,共提供了两种部署方式,一种是容器部署,一种是常规部署。容器部署的好处是简单、快捷,常规部署方式的好处是相对于容器来说、出现问题容易排查,缺点是步骤较为繁琐。两种方式各有优劣,根据自己的喜好自由选择即可。本文采用的是常规部署方式。2.部署规划机器/系统部署环境说明192.168.1.123,CentOS7JDK8MySQL8NginxCentOS7内网服务器:用于运行后台Java服务+前端Nginx用于代理转发MySQL为后台存储数据库192.168.1.131,Windows10GitJDK8MavenIDEA编辑器NodeJS个人Windows10办公电脑:Git用于克隆前后台以及引擎代码代码到本地IDEA编辑器用于修改配置文件Maven自动下载依赖包、打包程序NodeJS编译打包前端程序192.168.1.188,Windows7Python3SeleniumChromeChromeDriver同一内网下的其他Windows主机:Python3为执行引擎环境Selenium为Web自动化测试工具Chrome谷歌浏览器ChromeDriver谷歌浏览器驱动程序关于执行引擎机,也可以继续使用个人办公电脑作为执行引擎机,考虑到个人电脑经常会关机重启,就需要来回启动执行引擎,比较麻烦,所以我就选了一台本地Windows主机。当然也可以部署在Linux系统上,不过对于UI自动化测试而言,没有可视化的界面展示,调试起来相对麻烦一些。二、依赖环境部署1.安装Java1.8CentOS服务器和个人Windows电脑分别需要安装JDK。CentOS下,推荐脚本部署方式:安装脚本下载:https://share.weiyun.com/6JMLvSyKJDK包下载地址:https://share.weiyun.com/mKDxXd1xsource jdk_install.sh # 通过source命令安装,省去配置环境变量步骤2.安装mysql1)安装mysqlCentOS下安装,本次通过docker进行快速安装,如服务器或其他内网机器已安装mysql,直连即可,可以忽略此步。注意要使用8.0+版本的mysql,我用的是5.7.33版本,启动时候就会报错不支持。docker run -d --restart always --name mysql -e MYSQL_ROOT_PASSWORD=123456 -p 3307:3306 mysql:8.0.282)登录mysql进入mysql容器docker exec -it mymysql sh连接mysqlmysql -uroot -p # 登录mysql,根据提示输入密码1234562)创建数据库进入mysql命令行执行:mysql> create database liuma character set utf8 collate utf8_general_ci;3.安装nginxCentOS下安装,推荐脚本部署方式nginx安装脚本下载地址:https://share.weiyun.com/HLuVRTO2nginx安装包下载地址:https://share.weiyun.com/uhffdijl将其下载下来,上传到服务器,执行以下命令安装:source nginx_install.sh4.安装gitWindows下安装,用于拉取项目代码,下载后双击、按照提示一步步安装。下载地址:https://share.weiyun.com/NJBlZGmE5.安装node.jsWindows下安装,用于安装前端依赖、打包编译。下载地址:https://share.weiyun.com/2PpWyXkz,下载下来后双击安装即可。更换淘宝镜像源1.临时更换npm --registry https://registry.npm.taobao.org install node-sass(要安装的模块)2.永久更换npm config set registry https://registry.npm.taobao.org npm config get registry # 查看是否更换成功3.通过cnpm使用npm install -g cnpm --registry=https://registry.npm.taobao.org6.安装python3CentOS或Windows下安装均可,执行机选用哪个系统就安装在哪个系统下。如果是Linux系统,可以参考之前的文章《Linux下一键安装Python3&更改镜像源&虚拟环境管理技巧》,如果是Windows系统,则在Windows系统下安装python3.三、代码打包1.克隆项目代码git clone https://github.com/Chras-fu/Liuma-platform.git  # 克隆平台代码git clone https://github.com/Chras-fu/Liuma-engine.git  # 克隆引擎代码平台代码目录:LiuMa-backend:后台代码LiuMa-frontent:前端代码2.打包前端代码进入前端文件目录,安装相关依赖并执行构建npm install # 安装相关依赖 npm run build # 构建出现"Build complete."提示代表构建成功:构建成功后,目录下会生成dist文件目录,可以将其打包成.zip格式并上传至服务器,然后再解压3.打包后端代码1)安装依赖包用IDEA打开liuma-platform/LiuMa-backend,使用maven安装依赖2)修改配置① 数据库配置文件位置:liuma-platform/LiuMa-backend/src/main/resources/application.properties,配置如下所示:username:数据库用户名;password:数据库密码;url:连接数据库的URL地址;# database spring.datasource.password=123456 spring.datasource.username=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:3307/liuma?serverTimezone=UTC&characterEncoding=utf-8注意事项:项目部署的服务器与数据库处于同一主机时可使用127.0.0.1,如不在同一主机下,需改为该主机的IP地址;3307为前面docker部署mysql时映射的端口号,按照你自己的数据库端口号配置即可;② 邮件配置文件位置:liuma-platform/LiuMa-backend/src/main/resources/application.properties,配置如下所示:# aliyun 阿里云 # 阿里云邮件key,改成你自己的 aliyun.email.accessKey = xxxxxxx # 阿里云邮件secret,改成你自己的 aliyun.email.accessSecret = xxxxxxx # 发送人邮箱地址,改成你自己的 aliyun.email.runnerSenderAddress = xxxxxxx aliyun.email.runnerSenderName = 执行通知机器人③ 七牛云配置文件位置:liuma-platform/LiuMa-backend/src/main/resources/application.properties,配置如下所示:# qiniuyun 七牛云 # 七牛云ak,改成你自己的 qiniu.cloud.ak = xxxxxxx # 七牛云sk,改成你自己的 qiniu.cloud.sk = xxxxxxx # 七牛云空间名,改成你自己的 qiniu.cloud.bucket = xxxxxxx # 七牛云加速域名,改成你自己的 qiniu.cloud.downloadUrl = xxxxxxx qiniu.cloud.uploadUrl = xxxxxxx3)maven打包提示“BUILD SUCCESS”即表示打包成功,目录下会多出一个LiuMa-1.0.3.jar的jar包(我目前拉的最新代码,打包出来的是1.0.3,还有个LiuMa-1.0.0.jar包是两个月前打包的)四、项目部署1.平台部署1)上传打包后的前端文件将前端打包后的文件夹dist上传到:nginx安装目录/usr/local/nginx/html/下2)上传打包后的后端文件可以在/home目录下新建一个文件夹liuma,用于存放前面打包的jar包文件:LiuMa-1.0.3.jar,执行命令,后台启动服务:nohup java -jar LiuMa-1.0.3.jar > logs.log 2>&1 &启动后,可以查看logs.log日志文件,看看是否启动成功,以及是否成功连接到数据库,连接成功后自动创建相关数据表:3)配置Nginx① 新建nginx_liuma.conf在nginx的/usr/local/nginx/conf目录下新建nginx_liuma.conf,nginx用于配置server的代理转发,详细配置如下:#user nobody; worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 8888; server_name 192.168.1.122; location / { index index.html index.htm; root /usr/local/nginx/html/dist; } location /autotest { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Origin ""; } location /openapi { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Origin ""; } access_log /usr/local/nginx/html/wwwlogs/access.log; } }② 测试配置nginx -t -c /usr/local/nginx/conf/nginx_liuma.conf测试过程中可能会出现失败提示"nginx: [emerg] open() "/usr/local/nginx/html/wwwlogs/access.log" failed (2: No such file or directory)",原因是html目录下没有wwwlogs/access.log这个文件路径,直接新建一个wwwlogs目录和access.log文件即可。③ 指定配置文件启动nginxnginx -c /usr/local/nginx/conf/nginx_liuma.conf4)验证部署情况访问前端:http://192.168.1.122:8888/,管理员账号:LMadmin,密码:Liuma@123456登录后的页面如下所示:5)问题排查因为流马是前后端分离项目,所以前端能访问并不代表后端也是正常的。如果登录遇到502,则是后台服务器没启起来,多半是数据库的问题,可以通过以下方式排查:数据库版本,建议用8.x版本,5.7版本不支持查看后端配置文件application.properties中数据库配置是否正确可以通过前面提到的logs.log日志查看,也可以查看数据库是否自动创建相关数据表检查数据库端口及Nginx反向代理的端口在防火墙中是否放开另外,如果前端页面访问不了,很可能是防火墙没开放权限,需要在防火墙中放开上述配置文件中配置的8888端口:firewall-cmd --permanent --add-port=8888/tcp firewall-cmd --reload如果是前后端服务是部署在云服务器,需要在安全组中放开8888端口。2.测试引擎部署测试引擎可以理解为接口测试和UI自动化测试的运行环境。测试引擎可以选择部署在Linux系统,也可以选择使用个人Windows电脑,最好处于同一局域网下。当然如果服务端是部署在云服务上,有公网IP地址,Windows是个人办公电脑也可以,只要引擎电脑能要连上部署后端服务的那台服务器就行。以下是引擎部署过程:1)上传代码前面已经通过“git clone https://github.com/Chras-fu/Liuma-engine.git”克隆好了引擎代码,直接上传到对应的服务器即可,比如我选用的是Windows作为引擎,那么直接拷贝到Windows即可。2)安装依赖前面依赖环境部署已经安装好了Python3,创建并激活了虚拟环境,下面直接进入项目所在目录liuma-engine,安装依赖即可。pip install -r requirements.txt3)下载Chrome驱动对照引擎机的Chrome浏览器版本,下载对应驱动,存放在/browser目录下4)添加引擎① 注册引擎流马平台环境中心-引擎管理-注册引擎,输入引擎名称,名称任意,自己能识别即可,如:engine-192.168.1.188,确认后,会弹出一个提示框,复制里面的引擎code和引擎秘钥,后面会用到。② 配置引擎服务器编辑liuma-engine/config目录下的配置文件config.ini,进行配置,几个重要配置如下:url:后台服务所在的URL;engine-code:前面注册引擎成功后,提示框中的引擎code;engine-secret:前面注册引擎成功后,提示框中的引擎秘钥;options:如果引擎机是Linux系统,则改为headless,即无头模式,如果是Windows则保持默认的normal;path:如果引擎机是Linux系统,则改为chromedriver,如果是Windows则保持默认的chromedriver.exe;token:后面注册成功后会自动生成,可以不用管;[Platform] url = http://192.168.1.122:8888 enable-proxy = false enable-stderr = true [Engine] engine-code = 9d4358f0c8a34a2ab7e4c297949149fa engine-secret = 86a2f8f8bd654e03a60da9f19cdec017 [Header] content-type = application/json;charset=utf-8 token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjI2Mjk4OTQsImVuZ2luZVNlY3JldCI6IjVhM2ZhYzdlYzJiODQ4NDlhYzJhNjY4ZGRlNjRhMDk0IiwiaWF0IjoxNjYyMDI1MDk0LCJlbmdpbmVJZCI6ImU3ZWMzOTllZDdkZDRkMmFiZWIxNGNiNTE2NGNiNGVkIn0._LVsCKIauFxV4IKAUNgYS1lVbD5twO_2E39QCOufKH8 [WebDriver] options = normal path = chromedriver.exe [PlatformProxy] url = http://0.0.0.0:80 username = **** password = ****5)启动引擎python startup.py6)验证引擎是否添加成功引擎启动成功后,即可在流马平台环境中心-引擎管理中查看引擎在线情况。一台服务器可以启动多个引擎,一般默认有四个系统引擎,启动后互不影响。以上就是流马的介绍篇和部署篇,由于字数和篇幅限制,就介绍到这里。下一篇文章将介绍流马《使用篇》和《总结篇》。
文章
前端开发  ·  关系型数据库  ·  MySQL  ·  Java  ·  测试技术  ·  应用服务中间件  ·  Linux  ·  数据库  ·  nginx  ·  Windows
2022-11-01
Springboot 一行代码实现文件上传 20个平台!少写代码到极致
大家好,我是小富~技术交流,公众号:程序员小富又是做好人好事的一天,有个小可爱私下问我有没有好用的springboot文件上传工具,这不巧了嘛,正好我私藏了一个好东西,顺便给小伙伴们也分享一下,demo地址放在文末了。文件上传在平常不过的一个功能,做后端开发的基本都会接触到,虽然不难可着实有点繁琐。数据流的开闭、读取还容易出错,尤其是在对接一些OSS对象存储平台,一个平台一堆SDK代码看起来乱糟糟的。下边给我大家推荐一个工具Spring File Storage,上传文件只要些许配置一行代码搞定,开发效率杠杠的,一起看看是不是有这么流批!官网:https://spring-file-storage.xuyanwu.cnSpring File Storage工具几乎整合了市面上所有的OSS对象存储平台,包括本地、FTP、SFTP、WebDAV、阿里云OSS、华为云OBS、七牛云Kodo、腾讯云COS、百度云 BOS、又拍云USS、MinIO、京东云 OSS、网易数帆 NOS等其它兼容 S3 协议的平台,只要在springboot中通过极简的方式就可以实现文件存储。简单配置下边以本地和Aliyun OSS上传为例,pom.xml中引入必要的spring-file-storage.jar,注意: 如果要上传文件到OSS平台,需要引入对应平台的SDK包。<!-- spring-file-storage 必须要引入 --> <dependency> <groupId>cn.xuyanwu</groupId> <artifactId>spring-file-storage</artifactId> <version>0.5.0</version> </dependency> <!-- 阿里云oss --> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.10.2</version> </dependency>application.yml文件中配置些基础信息。enable-storage:只有状态开启才会被识别到default-platform:默认的上传平台domain:生成的文件url中访问的域名base-path:存储地址thumbnail-suffix:缩略图后缀要是上传OSS对象存储平台,将aliyun oss提供的变量配置到相应的模块上即可。spring: #文件存储配置(本地、oss) file-storage: default-platform: local-1 thumbnail-suffix: ".min.jpg" #缩略图后缀 local: - platform: local-1 # 存储平台标识 enable-storage: true #是否开启本存储(只能选一种) enable-access: true #启用访问(线上请使用 Nginx 配置,效率更高) domain: "http://127.0.0.1:2222" #访问域名,注意后面要和path-patterns保持一致,“/”结尾 base-path: /tmp/Pictures/ # 存储地址 path-patterns: /** #访问路径 aliyun-oss: - platform: aliyun-oss enable-storage: true access-key: xxxx secret-key: xxxx end-point: xxx bucket-name: firebook domain: http://fire100.top base-path: #云平台文件路径springboot启动类中增加注解@EnableFileStorage,显式的开启文件上传功能,到这就可以用了@EnableFileStorage // 文件上传工具 @SpringBootApplication public class SpringbootFileStorageApplication { public static void main(String[] args) { SpringApplication.run(SpringbootFileStorageApplication.class, args); } }上传文件接下来在业务类中引入FileStorageService服务,如下只要一行代码就可以完成文件上传,是不是So easy,下载也是如法炮制。@RestController public class FileController { @Autowired private FileStorageService fileStorageService; /** * 公众号:程序员小富 * 上传文件 */ @PostMapping(value = {"/upload"}) public Object upload(MultipartFile file) { FileInfo upload = fileStorageService.of(file).upload(); return upload; } }我们用postman测试上传一张图片,看到图片已经成功传到了/tmp/Pictures目录下,返回结果中包含了完整的访问文件的URL路径。不仅如此spring-file-storage还支持多种文件形式,URI、URL、String、byte[]、InputStream、MultipartFile,使开发更加灵活。文件上传功能,更多时候我们都是在上传图片,那就会有动态裁剪图片、生成缩略图的需求,这些 spring-file-storage 都可以很容易实现。/** * 公众号:程序员小富 * 上传图片裁剪大小并生成一张缩略图 */ @PostMapping("/uploadThumbnail") public FileInfo uploadThumbnail(MultipartFile file) { return fileStorageService.of(file) .image(img -> img.size(1000,1000)) //将图片大小调整到 1000*1000 .thumbnail(th -> th.size(200,200)) //再生成一张 200*200 的缩略图 .upload(); }而且我们还可以动态选择上传平台,配置文件中将所有平台开启,在实际使用中自由的选择。/** * 公众号:程序员小富 * 上传文件到指定存储平台,成功返回文件信息 */ @PostMapping("/upload-platform") public FileInfo uploadPlatform(MultipartFile file) { return fileStorageService.of(file) .setPlatform("aliyun-oss") //使用指定的存储平台 .upload(); }下载文件下载文件也同样的简单,可以直接根据文件url或者文件流下载。/** * 公众号:程序员小富 * 下载文件 */ @PostMapping("/download") public void download(MultipartFile file) { // 获取文件信息 FileInfo fileInfo = fileStorageService.getFileInfoByUrl("http://file.abc.com/test/a.jpg"); // 下载到文件 fileStorageService.download(fileInfo).file("C:\\a.jpg"); // 直接通过文件信息中的 url 下载,省去手动查询文件信息记录的过程 fileStorageService.download("http://file.abc.com/test/a.jpg").file("C:\\a.jpg"); // 下载缩略图 fileStorageService.downloadTh(fileInfo).file("C:\\th.jpg"); }提供了监听下载进度的功能,可以清晰明了的掌握文件的下载情况。// 下载文件 显示进度 fileStorageService.download(fileInfo).setProgressMonitor(new ProgressListener() { @Override public void start() { System.out.println("下载开始"); } @Override public void progress(long progressSize,long allSize) { System.out.println("已下载 " + progressSize + " 总大小" + allSize); } @Override public void finish() { System.out.println("下载结束"); } }).file("C:\\a.jpg");文件存在、删除我们还可以根据文件的URL地址来判断文件是否存在、以及删除文件。//直接通过文件信息中的 url 删除,省去手动查询文件信息记录的过程 fileStorageService.delete("http://file.abc.com/test/a.jpg");//直接通过文件信息中的 url 判断文件是否存在,省去手动查询文件信息记录的过程 boolean exists2 = fileStorageService.exists("http://file.abc.com/test/a.jpg");切面工具还提供了每种操作的切面,可以在每个动作的前后进行干预,比如打日志或者玩点花活,实现FileStorageAspect类重写对应动作的xxxAround方法。** * 使用切面打印文件上传和删除的日志 */ @Slf4j @Component public class LogFileStorageAspect implements FileStorageAspect { /** * 上传,成功返回文件信息,失败返回 null */ @Override public FileInfo uploadAround(UploadAspectChain chain, FileInfo fileInfo, UploadPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder) { log.info("上传文件 before -> {}",fileInfo); fileInfo = chain.next(fileInfo,pre,fileStorage,fileRecorder); log.info("上传文件 after -> {}",fileInfo); return fileInfo; } }demo案例地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-file-storage总结用了这个工具确实极大的减少了上传文件所带来的代码量,提升了开发效率,使用过程中暂未发现有什么坑,好东西就是要大家分享,如果符合你的需求,犹豫什么用起来吧。技术交流,公众号:程序员小富
文章
存储  ·  Java  ·  程序员  ·  开发工具  ·  文件存储  ·  对象存储  ·  Spring
2022-10-25
SpringCloud微服务实战——搭建企业级开发框架(二十九):集成对象存储服务MinIO+七牛云+阿里云+腾讯云
微服务应用中图片、文件等存储区别于单体应用,单体应用可以放到本地读写磁盘文件,微服务应用必需用到分布式存储,将图片、文件等存储到服务稳定的分布式存储服务器。目前,很多云服务商提供了存储的云服务,比如阿里云OSS、腾讯云COS、七牛云对象存储Kodo、百度云对象存储BOS等等、还有开源对象存储服务器,比如FastDFS、MinIO等。  如果我们的框架只支持一种存储服务,那么在后期扩展或者修改时会有局限性,所以,这里希望能够定义一个抽象接口,想使用哪种服务就实现哪种服务,在配置多个服务时,调用的存储时可以进行选择。在这里云服务选择七牛云,开源服务选择MinIO进行集成,如果需要其他服务可以自行扩展。  首先,在框架搭建前,我们先准备环境,这里以MinIO和七牛云为例,MinIO的安装十分简单,我们这里选择Linux安装包的方式来安装,具体方式参考:http://docs.minio.org.cn/docs/,七牛云只需要到官网注册并实名认证即可获得10G免费存储容量https://www.qiniu.com/。一、基础底层库实现1、在GitEgg-Platform中新建gitegg-platform-dfs (dfs: Distributed File System分布式文件系统)子工程用于定义对象存储服务的抽象接口,新建IDfsBaseService用于定义文件上传下载常用接口/** * 分布式文件存储操作接口定义 * 为了保留系统操作记录,原则上不允许上传文件物理删除,修改等操作。 * 业务操作的修改删除文件,只是关联关系的修改,重新上传文件后并与业务关联即可。 */ public interface IDfsBaseService { /** * 获取简单上传凭证 * @param bucket * @return */ String uploadToken(String bucket); /** * 获取覆盖上传凭证 * @param bucket * @return */ String uploadToken(String bucket, String key); /** * 创建 bucket * @param bucket */ void createBucket(String bucket); /** * 通过流上传文件,指定文件名 * @param inputStream * @param fileName * @return */ GitEggDfsFile uploadFile(InputStream inputStream, String fileName); /** * 通过流上传文件,指定文件名和bucket * @param inputStream * @param bucket * @param fileName * @return */ GitEggDfsFile uploadFile(InputStream inputStream, String bucket, String fileName); /** * 通过文件名获取文件访问链接 * @param fileName * @return */ String getFileUrl(String fileName); /** * 通过文件名和bucket获取文件访问链接 * @param fileName * @param bucket * @return */ String getFileUrl(String bucket, String fileName); /** * 通过文件名和bucket获取文件访问链接,设置有效期 * @param bucket * @param fileName * @param duration * @param unit * @return */ String getFileUrl(String bucket, String fileName, int duration, TimeUnit unit); /** * 通过文件名以流的形式下载一个对象 * @param fileName * @return */ OutputStream getFileObject(String fileName, OutputStream outputStream); /** * 通过文件名和bucket以流的形式下载一个对象 * @param fileName * @param bucket * @return */ OutputStream getFileObject(String bucket, String fileName, OutputStream outputStream); /** * 根据文件名删除文件 * @param fileName */ String removeFile(String fileName); /** * 根据文件名删除指定bucket下的文件 * @param bucket * @param fileName */ String removeFile(String bucket, String fileName); /** * 根据文件名列表批量删除文件 * @param fileNames */ String removeFiles(List<String> fileNames); /** * 根据文件名列表批量删除bucket下的文件 * @param bucket * @param fileNames */ String removeFiles(String bucket, List<String> fileNames); }2、在GitEgg-Platform中新建gitegg-platform-dfs-minio子工程,新建MinioDfsServiceImpl和MinioDfsProperties用于实现IDfsBaseService文件上传下载接口@Data @Component @ConfigurationProperties(prefix = "dfs.minio") public class MinioDfsProperties { /** * AccessKey */ private String accessKey; /** * SecretKey */ private String secretKey; /** * 区域,需要在MinIO配置服务器的物理位置,默认是us-east-1(美国东区1),这也是亚马逊S3的默认区域。 */ private String region; /** * Bucket */ private String bucket; /** * 公开还是私有 */ private Integer accessControl; /** * 上传服务器域名地址 */ private String uploadUrl; /** * 文件请求地址前缀 */ private String accessUrlPrefix; /** * 上传文件夹前缀 */ private String uploadDirPrefix; }@Slf4j @AllArgsConstructor public class MinioDfsServiceImpl implements IDfsBaseService { private final MinioClient minioClient; private final MinioDfsProperties minioDfsProperties; @Override public String uploadToken(String bucket) { return null; } @Override public String uploadToken(String bucket, String key) { return null; } @Override public void createBucket(String bucket) { BucketExistsArgs bea = BucketExistsArgs.builder().bucket(bucket).build(); try { if (!minioClient.bucketExists(bea)) { MakeBucketArgs mba = MakeBucketArgs.builder().bucket(bucket).build(); minioClient.makeBucket(mba); } } catch (ErrorResponseException e) { e.printStackTrace(); } catch (InsufficientDataException e) { e.printStackTrace(); } catch (InternalException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (InvalidResponseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (ServerException e) { e.printStackTrace(); } catch (XmlParserException e) { e.printStackTrace(); } } @Override public GitEggDfsFile uploadFile(InputStream inputStream, String fileName) { return this.uploadFile(inputStream, minioDfsProperties.getBucket(), fileName); } @Override public GitEggDfsFile uploadFile(InputStream inputStream, String bucket, String fileName) { GitEggDfsFile dfsFile = new GitEggDfsFile(); try { dfsFile.setBucket(bucket); dfsFile.setBucketDomain(minioDfsProperties.getUploadUrl()); dfsFile.setFileUrl(minioDfsProperties.getAccessUrlPrefix()); dfsFile.setEncodedFileName(fileName); minioClient.putObject(PutObjectArgs.builder() .bucket(bucket) .stream(inputStream, -1, 5*1024*1024) .object(fileName) .build()); } catch (ErrorResponseException e) { e.printStackTrace(); } catch (InsufficientDataException e) { e.printStackTrace(); } catch (InternalException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (InvalidResponseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (ServerException e) { e.printStackTrace(); } catch (XmlParserException e) { e.printStackTrace(); } return dfsFile; } @Override public String getFileUrl(String fileName) { return this.getFileUrl(minioDfsProperties.getBucket(), fileName); } @Override public String getFileUrl(String bucket, String fileName) { return this.getFileUrl(bucket, fileName, DfsConstants.DFS_FILE_DURATION, DfsConstants.DFS_FILE_DURATION_UNIT); } @Override public String getFileUrl(String bucket, String fileName, int duration, TimeUnit unit) { String url = null; try { url = minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucket) .object(fileName) .expiry(duration, unit) .build()); } catch (ErrorResponseException e) { e.printStackTrace(); } catch (InsufficientDataException e) { e.printStackTrace(); } catch (InternalException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (InvalidResponseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (XmlParserException e) { e.printStackTrace(); } catch (ServerException e) { e.printStackTrace(); } return url; } @Override public OutputStream getFileObject(String fileName, OutputStream outputStream) { return this.getFileObject(minioDfsProperties.getBucket(), fileName, outputStream); } @Override public OutputStream getFileObject(String bucket, String fileName, OutputStream outputStream) { BufferedInputStream bis = null; InputStream stream = null; try { stream = minioClient.getObject( GetObjectArgs.builder() .bucket(bucket) .object(fileName) .build()); bis = new BufferedInputStream(stream); IOUtils.copy(bis, outputStream); } catch (ErrorResponseException e) { e.printStackTrace(); } catch (InsufficientDataException e) { e.printStackTrace(); } catch (InternalException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (InvalidResponseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (ServerException e) { e.printStackTrace(); } catch (XmlParserException e) { e.printStackTrace(); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { e.printStackTrace(); } } if (bis != null) { try { bis.close(); } catch (IOException e) { e.printStackTrace(); } } } return outputStream; } @Override public String removeFile(String fileName) { return this.removeFile(minioDfsProperties.getBucket(), fileName); } @Override public String removeFile(String bucket, String fileName) { return this.removeFiles(bucket, Collections.singletonList(fileName)); } @Override public String removeFiles(List<String> fileNames) { return this.removeFiles(minioDfsProperties.getBucket(), fileNames); } @Override public String removeFiles(String bucket, List<String> fileNames) { List<DeleteObject> deleteObject = new ArrayList<>(); if (!CollectionUtils.isEmpty(fileNames)) { fileNames.stream().forEach(item -> { deleteObject.add(new DeleteObject(item)); }); } Iterable<Result<DeleteError>> result = minioClient.removeObjects(RemoveObjectsArgs.builder() .bucket(bucket) .objects(deleteObject) .build()); try { return JsonUtils.objToJsonIgnoreNull(result); } catch (Exception e) { e.printStackTrace(); } return null; } }3、在GitEgg-Platform中新建gitegg-platform-dfs-qiniu子工程,新建QiNiuDfsServiceImpl和QiNiuDfsProperties用于实现IDfsBaseService文件上传下载接口@Data @Component @ConfigurationProperties(prefix = "dfs.qiniu") public class QiNiuDfsProperties { /** * AccessKey */ private String accessKey; /** * SecretKey */ private String secretKey; /** * 七牛云机房 */ private String region; /** * Bucket 存储块 */ private String bucket; /** * 公开还是私有 */ private Integer accessControl; /** * 上传服务器域名地址 */ private String uploadUrl; /** * 文件请求地址前缀 */ private String accessUrlPrefix; /** * 上传文件夹前缀 */ private String uploadDirPrefix; }@Slf4j @AllArgsConstructor public class QiNiuDfsServiceImpl implements IDfsBaseService { private final Auth auth; private final UploadManager uploadManager; private final BucketManager bucketManager; private final QiNiuDfsProperties qiNiuDfsProperties; /** * * @param bucket * @return */ @Override public String uploadToken(String bucket) { Auth auth = Auth.create(qiNiuDfsProperties.getAccessKey(), qiNiuDfsProperties.getSecretKey()); String upToken = auth.uploadToken(bucket); return upToken; } /** * * @param bucket * @param key * @return */ @Override public String uploadToken(String bucket, String key) { Auth auth = Auth.create(qiNiuDfsProperties.getAccessKey(), qiNiuDfsProperties.getSecretKey()); String upToken = auth.uploadToken(bucket, key); return upToken; } @Override public void createBucket(String bucket) { try { String[] buckets = bucketManager.buckets(); if (!ArrayUtil.contains(buckets, bucket)) { bucketManager.createBucket(bucket, qiNiuDfsProperties.getRegion()); } } catch (QiniuException e) { e.printStackTrace(); } } /** * * @param inputStream * @param fileName * @return */ @Override public GitEggDfsFile uploadFile(InputStream inputStream, String fileName) { return this.uploadFile(inputStream, qiNiuDfsProperties.getBucket(), fileName); } /** * * @param inputStream * @param bucket * @param fileName * @return */ @Override public GitEggDfsFile uploadFile(InputStream inputStream, String bucket, String fileName) { GitEggDfsFile dfsFile = null; //默认不指定key的情况下,以文件内容的hash值作为文件名 String key = null; if (!StringUtils.isEmpty(fileName)) { key = fileName; } try { String upToken = auth.uploadToken(bucket); Response response = uploadManager.put(inputStream, key, upToken,null, null); //解析上传成功的结果 dfsFile = JsonUtils.jsonToPojo(response.bodyString(), GitEggDfsFile.class); if (dfsFile != null) { dfsFile.setBucket(bucket); dfsFile.setBucketDomain(qiNiuDfsProperties.getUploadUrl()); dfsFile.setFileUrl(qiNiuDfsProperties.getAccessUrlPrefix()); dfsFile.setEncodedFileName(fileName); } } catch (QiniuException ex) { Response r = ex.response; log.error(r.toString()); try { log.error(r.bodyString()); } catch (QiniuException ex2) { log.error(ex2.toString()); } } catch (Exception e) { log.error(e.toString()); } return dfsFile; } @Override public String getFileUrl(String fileName) { return this.getFileUrl(qiNiuDfsProperties.getBucket(), fileName); } @Override public String getFileUrl(String bucket, String fileName) { return this.getFileUrl(bucket, fileName, DfsConstants.DFS_FILE_DURATION, DfsConstants.DFS_FILE_DURATION_UNIT); } @Override public String getFileUrl(String bucket, String fileName, int duration, TimeUnit unit) { String finalUrl = null; try { Integer accessControl = qiNiuDfsProperties.getAccessControl(); if (accessControl != null && DfsConstants.DFS_FILE_PRIVATE == accessControl.intValue()) { String encodedFileName = URLEncoder.encode(fileName, "utf-8").replace("+", "%20"); String publicUrl = String.format("%s/%s", qiNiuDfsProperties.getAccessUrlPrefix(), encodedFileName); String accessKey = qiNiuDfsProperties.getAccessKey(); String secretKey = qiNiuDfsProperties.getSecretKey(); Auth auth = Auth.create(accessKey, secretKey); long expireInSeconds = unit.toSeconds(duration); finalUrl = auth.privateDownloadUrl(publicUrl, expireInSeconds); } else { finalUrl = String.format("%s/%s", qiNiuDfsProperties.getAccessUrlPrefix(), fileName); } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return finalUrl; } @Override public OutputStream getFileObject(String fileName, OutputStream outputStream) { return this.getFileObject(qiNiuDfsProperties.getBucket(), fileName, outputStream); } @Override public OutputStream getFileObject(String bucket, String fileName, OutputStream outputStream) { URL url = null; HttpURLConnection conn = null; BufferedInputStream bis = null; try { String path = this.getFileUrl(bucket, fileName, DfsConstants.DFS_FILE_DURATION, DfsConstants.DFS_FILE_DURATION_UNIT); url = new URL(path); conn = (HttpURLConnection)url.openConnection(); //设置超时间 conn.setConnectTimeout(DfsConstants.DOWNLOAD_TIMEOUT); //防止屏蔽程序抓取而返回403错误 conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)"); conn.connect(); //得到输入流 bis = new BufferedInputStream(conn.getInputStream()); IOUtils.copy(bis, outputStream); } catch (Exception e) { log.error("读取网络文件异常:" + fileName); } finally { conn.disconnect(); if (bis != null) { try { bis.close(); } catch (IOException e) { e.printStackTrace(); } } } return outputStream; } /** * * @param fileName * @return */ @Override public String removeFile(String fileName) { return this.removeFile( qiNiuDfsProperties.getBucket(), fileName); } /** * * @param bucket * @param fileName * @return */ @Override public String removeFile(String bucket, String fileName) { String resultStr = null; try { Response response = bucketManager.delete(bucket, fileName); resultStr = JsonUtils.objToJson(response); } catch (QiniuException e) { Response r = e.response; log.error(r.toString()); try { log.error(r.bodyString()); } catch (QiniuException ex2) { log.error(ex2.toString()); } } catch (Exception e) { log.error(e.toString()); } return resultStr; } /** * * @param fileNames * @return */ @Override public String removeFiles(List<String> fileNames) { return this.removeFiles(qiNiuDfsProperties.getBucket(), fileNames); } /** * * @param bucket * @param fileNames * @return */ @Override public String removeFiles(String bucket, List<String> fileNames) { String resultStr = null; try { if (!CollectionUtils.isEmpty(fileNames) && fileNames.size() > GitEggConstant.Number.THOUSAND) { throw new BusinessException("单次批量请求的文件数量不得超过1000"); } BucketManager.BatchOperations batchOperations = new BucketManager.BatchOperations(); batchOperations.addDeleteOp(bucket, (String [])fileNames.toArray()); Response response = bucketManager.batch(batchOperations); BatchStatus[] batchStatusList = response.jsonToObject(BatchStatus[].class); resultStr = JsonUtils.objToJson(batchStatusList); } catch (QiniuException ex) { log.error(ex.response.toString()); } catch (Exception e) { log.error(e.toString()); } return resultStr; } }4、在GitEgg-Platform中新建gitegg-platform-dfs-starter子工程,用于集成所有文件上传下载子工程,方便业务统一引入所有实现<?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"> <parent> <artifactId>GitEgg-Platform</artifactId> <groupId>com.gitegg.platform</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>gitegg-platform-dfs-starter</artifactId> <name>${project.artifactId}</name> <packaging>jar</packaging> <dependencies> <!-- gitegg 分布式文件自定义扩展-minio --> <dependency> <groupId>com.gitegg.platform</groupId> <artifactId>gitegg-platform-dfs-minio</artifactId> </dependency> <!-- gitegg 分布式文件自定义扩展-七牛云 --> <dependency> <groupId>com.gitegg.platform</groupId> <artifactId>gitegg-platform-dfs-qiniu</artifactId> </dependency> </dependencies> </project>5、gitegg-platform-bom中添加文件存储相关依赖<!-- gitegg 分布式文件自定义扩展 --> <dependency> <groupId>com.gitegg.platform</groupId> <artifactId>gitegg-platform-dfs</artifactId> <version>${gitegg.project.version}</version> </dependency> <!-- gitegg 分布式文件自定义扩展-minio --> <dependency> <groupId>com.gitegg.platform</groupId> <artifactId>gitegg-platform-dfs-minio</artifactId> <version>${gitegg.project.version}</version> </dependency> <!-- gitegg 分布式文件自定义扩展-七牛云 --> <dependency> <groupId>com.gitegg.platform</groupId> <artifactId>gitegg-platform-dfs-qiniu</artifactId> <version>${gitegg.project.version}</version> </dependency> <!-- gitegg 分布式文件自定义扩展-starter --> <dependency> <groupId>com.gitegg.platform</groupId> <artifactId>gitegg-platform-dfs-starter</artifactId> <version>${gitegg.project.version}</version> </dependency> <!-- minio文件存储服务 https://mvnrepository.com/artifact/io.minio/minio --> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>${dfs.minio.version}</version> </dependency> <!--七牛云文件存储服务--> <dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>${dfs.qiniu.version}</version> </dependency>二、业务功能实现分布式文件存储功能作为系统扩展功能放在gitegg-service-extension工程中,首先需要分为几个模块:文件服务器的基本配置模块文件的上传、下载记录模块(下载只记录私有文件,对于公共可访问的文件不需要记录)前端访问下载实现1、新建文件服务器配置表,用于存放文件服务器相关配置,定义好表结构,使用代码生成工具生成增删改查代码。CREATE TABLE `t_sys_dfs` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户id', `dfs_type` bigint(20) NULL DEFAULT NULL COMMENT '分布式存储分类', `dfs_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分布式存储编号', `access_url_prefix` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件访问地址前缀', `upload_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分布式存储上传地址', `bucket` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '空间名称', `app_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用ID', `region` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '区域', `access_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'accessKey', `secret_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'secretKey', `dfs_default` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否默认存储 0否,1是', `dfs_status` tinyint(2) NOT NULL DEFAULT 1 COMMENT '状态 0禁用,1 启用', `access_control` tinyint(2) NOT NULL DEFAULT 0 COMMENT '访问控制 0私有,1公开', `comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `creator` bigint(20) NULL DEFAULT NULL COMMENT '创建者', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者', `del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '1:删除 0:不删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分布式存储配置表' ROW_FORMAT = DYNAMIC;2、新建DfsQiniuFactory和DfsMinioFactory接口实现工厂类,用于根据当前用户的选择,实例化需要的接口实现类/** * 七牛云上传服务接口工厂类 */ public class DfsQiniuFactory { public static IDfsBaseService getDfsBaseService(DfsDTO dfsDTO) { Auth auth = Auth.create(dfsDTO.getAccessKey(), dfsDTO.getSecretKey()); Configuration cfg = new Configuration(Region.autoRegion()); UploadManager uploadManager = new UploadManager(cfg); BucketManager bucketManager = new BucketManager(auth, cfg); QiNiuDfsProperties qiNiuDfsProperties = new QiNiuDfsProperties(); qiNiuDfsProperties.setAccessKey(dfsDTO.getAccessKey()); qiNiuDfsProperties.setSecretKey(dfsDTO.getSecretKey()); qiNiuDfsProperties.setRegion(dfsDTO.getRegion()); qiNiuDfsProperties.setBucket(dfsDTO.getBucket()); qiNiuDfsProperties.setUploadUrl(dfsDTO.getUploadUrl()); qiNiuDfsProperties.setAccessUrlPrefix(dfsDTO.getAccessUrlPrefix()); qiNiuDfsProperties.setAccessControl(dfsDTO.getAccessControl()); return new QiNiuDfsServiceImpl(auth, uploadManager, bucketManager, qiNiuDfsProperties); } }/** * MINIO上传服务接口工厂类 */ public class DfsMinioFactory { public static IDfsBaseService getDfsBaseService(DfsDTO dfsDTO) { MinioClient minioClient = MinioClient.builder() .endpoint(dfsDTO.getUploadUrl()) .credentials(dfsDTO.getAccessKey(), dfsDTO.getSecretKey()).build();; MinioDfsProperties minioDfsProperties = new MinioDfsProperties(); minioDfsProperties.setAccessKey(dfsDTO.getAccessKey()); minioDfsProperties.setSecretKey(dfsDTO.getSecretKey()); minioDfsProperties.setRegion(dfsDTO.getRegion()); minioDfsProperties.setBucket(dfsDTO.getBucket()); minioDfsProperties.setUploadUrl(dfsDTO.getUploadUrl()); minioDfsProperties.setAccessUrlPrefix(dfsDTO.getAccessUrlPrefix()); minioDfsProperties.setAccessControl(dfsDTO.getAccessControl()); return new MinioDfsServiceImpl(minioClient, minioDfsProperties); } }3、新建DfsFactory工厂类,添加@Component使用容器管理该类(默认单例),用于根据系统用户配置,生成及缓存对应的上传下载接口实现/** * DfsFactory工厂类,根据系统用户配置,生成及缓存对应的上传下载接口实现 */ @Component public class DfsFactory { /** * DfsService 缓存 */ private final static Map<Long, IDfsBaseService> dfsBaseServiceMap = new ConcurrentHashMap<>(); /** * 获取 DfsService * * @param dfsDTO 分布式存储配置 * @return dfsService */ public IDfsBaseService getDfsBaseService(DfsDTO dfsDTO) { //根据dfsId获取对应的分布式存储服务接口,dfsId是唯一的,每个租户有其自有的dfsId Long dfsId = dfsDTO.getId(); IDfsBaseService dfsBaseService = dfsBaseServiceMap.get(dfsId); if (null == dfsBaseService) { Class cls = null; try { cls = Class.forName(DfsFactoryClassEnum.getValue(String.valueOf(dfsDTO.getDfsType()))); Method staticMethod = cls.getDeclaredMethod(DfsConstants.DFS_SERVICE_FUNCTION, DfsDTO.class); dfsBaseService = (IDfsBaseService) staticMethod.invoke(cls, dfsDTO); dfsBaseServiceMap.put(dfsId, dfsBaseService); } catch (ClassNotFoundException | NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } return dfsBaseService; } }4、新建枚举类DfsFactoryClassEnum,用于DfsFactory 工厂类通过反射实例化对应文件服务器的接口实现类/** * @ClassName: DfsFactoryClassEnum * @Description: 分布式存储工厂类枚举 ,因dfs表存的是数据字典表的id,这里省一次数据库查询,所以就用数据字典的id * @author GitEgg * @date 2020年09月19日 下午11:49:45 */ public enum DfsFactoryClassEnum { /** * MINIO MINIO */ MINIO("2", "com.gitegg.service.extension.dfs.factory.DfsMinioFactory"), /** * 七牛云Kodo QINIUYUN_KODO */ QI_NIU("3", "com.gitegg.service.extension.dfs.factory.DfsQiniuFactory"), /** * 阿里云OSS ALIYUN_OSS */ ALI_YUN("4", "com.gitegg.service.extension.dfs.factory.DfsAliyunFactory"), /** * 腾讯云COS TENCENT_COS */ TENCENT("5", "com.gitegg.service.extension.dfs.factory.DfsTencentFactory"); public String code; public String value; DfsFactoryClassEnum(String code, String value) { this.code = code; this.value = value; } public static String getValue(String code) { DfsFactoryClassEnum[] smsFactoryClassEnums = values(); for (DfsFactoryClassEnum smsFactoryClassEnum : smsFactoryClassEnums) { if (smsFactoryClassEnum.getCode().equals(code)) { return smsFactoryClassEnum.getValue(); } } return null; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } }5、新建IGitEggDfsService接口,用于定义业务需要的文件上传下载接口/** * 业务文件上传下载接口实现 * */ public interface IGitEggDfsService { /** * 获取文件上传的 token * @param dfsCode * @return */ String uploadToken(String dfsCode); /** * 上传文件 * * @param dfsCode * @param file * @return */ GitEggDfsFile uploadFile(String dfsCode, MultipartFile file); /** * 获取文件访问链接 * @param dfsCode * @param fileName * @return */ String getFileUrl(String dfsCode, String fileName); /** * 下载文件 * @param dfsCode * @param fileName * @return */ OutputStream downloadFile(String dfsCode, String fileName, OutputStream outputStream); }6、新建IGitEggDfsService接口实现类GitEggDfsServiceImpl,用于实现业务需要的文件上传下载接口@Slf4j @Service @RequiredArgsConstructor(onConstructor_ = @Autowired) public class GitEggDfsServiceImpl implements IGitEggDfsService { private final DfsFactory dfsFactory; private final IDfsService dfsService; private final IDfsFileService dfsFileService; @Override public String uploadToken(String dfsCode) { QueryDfsDTO queryDfsDTO = new QueryDfsDTO(); queryDfsDTO.setDfsCode(dfsCode); DfsDTO dfsDTO = dfsService.queryDfs(queryDfsDTO); IDfsBaseService dfsBaseService = dfsFactory.getDfsBaseService(dfsDTO); String token = dfsBaseService.uploadToken(dfsDTO.getBucket()); return token; } @Override public GitEggDfsFile uploadFile(String dfsCode, MultipartFile file) { QueryDfsDTO queryDfsDTO = new QueryDfsDTO(); DfsDTO dfsDTO = null; // 如果上传时没有选择存储方式,那么取默认存储方式 if(StringUtils.isEmpty(dfsCode)) { queryDfsDTO.setDfsDefault(GitEggConstant.ENABLE); } else { queryDfsDTO.setDfsCode(dfsCode); } GitEggDfsFile gitEggDfsFile = null; DfsFile dfsFile = new DfsFile(); try { dfsDTO = dfsService.queryDfs(queryDfsDTO); IDfsBaseService dfsFileService = dfsFactory.getDfsBaseService(dfsDTO); //获取文件名 String originalName = file.getOriginalFilename(); //获取文件后缀 String extension = FilenameUtils.getExtension(originalName); String hash = Etag.stream(file.getInputStream(), file.getSize()); String fileName = hash + "." + extension; // 保存文件上传记录 dfsFile.setDfsId(dfsDTO.getId()); dfsFile.setOriginalName(originalName); dfsFile.setFileName(fileName); dfsFile.setFileExtension(extension); dfsFile.setFileSize(file.getSize()); dfsFile.setFileStatus(GitEggConstant.ENABLE); //执行文件上传操作 gitEggDfsFile = dfsFileService.uploadFile(file.getInputStream(), fileName); if (gitEggDfsFile != null) { gitEggDfsFile.setFileName(originalName); gitEggDfsFile.setKey(hash); gitEggDfsFile.setHash(hash); gitEggDfsFile.setFileSize(file.getSize()); } dfsFile.setAccessUrl(gitEggDfsFile.getFileUrl()); } catch (IOException e) { log.error("文件上传失败:{}", e); dfsFile.setFileStatus(GitEggConstant.DISABLE); dfsFile.setComments(String.valueOf(e)); } finally { dfsFileService.save(dfsFile); } return gitEggDfsFile; } @Override public String getFileUrl(String dfsCode, String fileName) { String fileUrl = null; QueryDfsDTO queryDfsDTO = new QueryDfsDTO(); DfsDTO dfsDTO = null; // 如果上传时没有选择存储方式,那么取默认存储方式 if(StringUtils.isEmpty(dfsCode)) { queryDfsDTO.setDfsDefault(GitEggConstant.ENABLE); } else { queryDfsDTO.setDfsCode(dfsCode); } try { dfsDTO = dfsService.queryDfs(queryDfsDTO); IDfsBaseService dfsFileService = dfsFactory.getDfsBaseService(dfsDTO); fileUrl = dfsFileService.getFileUrl(fileName); } catch (Exception e) { log.error("文件上传失败:{}", e); } return fileUrl; } @Override public OutputStream downloadFile(String dfsCode, String fileName, OutputStream outputStream) { QueryDfsDTO queryDfsDTO = new QueryDfsDTO(); DfsDTO dfsDTO = null; // 如果上传时没有选择存储方式,那么取默认存储方式 if(StringUtils.isEmpty(dfsCode)) { queryDfsDTO.setDfsDefault(GitEggConstant.ENABLE); } else { queryDfsDTO.setDfsCode(dfsCode); } try { dfsDTO = dfsService.queryDfs(queryDfsDTO); IDfsBaseService dfsFileService = dfsFactory.getDfsBaseService(dfsDTO); outputStream = dfsFileService.getFileObject(fileName, outputStream); } catch (Exception e) { log.error("文件上传失败:{}", e); } return outputStream; } }7、新建GitEggDfsController用于文件上传下载通用访问控制器@RestController @RequestMapping("/extension") @RequiredArgsConstructor(onConstructor_ = @Autowired) @Api(value = "GitEggDfsController|文件上传前端控制器") @RefreshScope public class GitEggDfsController { private final IGitEggDfsService gitEggDfsService; /** * 上传文件 * @param uploadFile * @param dfsCode * @return */ @PostMapping("/upload/file") public Result<?> uploadFile(@RequestParam("uploadFile") MultipartFile[] uploadFile, String dfsCode) { GitEggDfsFile gitEggDfsFile = null; if (ArrayUtils.isNotEmpty(uploadFile)) { for (MultipartFile file : uploadFile) { gitEggDfsFile = gitEggDfsService.uploadFile(dfsCode, file); } } return Result.data(gitEggDfsFile); } /** * 通过文件名获取文件访问链接 */ @GetMapping("/get/file/url") @ApiOperation(value = "查询分布式存储配置表详情") public Result<?> query(String dfsCode, String fileName) { String fileUrl = gitEggDfsService.getFileUrl(dfsCode, fileName); return Result.data(fileUrl); } /** * 通过文件名以文件流的方式下载文件 */ @GetMapping("/get/file/download") public void downloadFile(HttpServletResponse response,HttpServletRequest request,String dfsCode, String fileName) { if (fileName != null) { response.setCharacterEncoding(request.getCharacterEncoding()); response.setContentType("application/octet-stream"); response.addHeader("Content-Disposition", "attachment;fileName=" + fileName); OutputStream os = null; try { os = response.getOutputStream(); os = gitEggDfsService.downloadFile(dfsCode, fileName, os); os.flush(); os.close(); } catch (Exception e) { e.printStackTrace(); } finally { if (os != null) { try { os.close(); } catch (IOException e) { e.printStackTrace(); } } } } } }8、前端上传下载实现,注意的是:axios请求下载文件流时,需要设置 responseType: 'blob'上传handleUploadTest (row) { this.fileList = [] this.uploading = false this.uploadForm.dfsType = row.dfsType this.uploadForm.dfsCode = row.dfsCode this.uploadForm.uploadFile = null this.dialogTestUploadVisible = true }, handleRemove (file) { const index = this.fileList.indexOf(file) const newFileList = this.fileList.slice() newFileList.splice(index, 1) this.fileList = newFileList }, beforeUpload (file) { this.fileList = [...this.fileList, file] return false }, handleUpload () { this.uploadedFileName = '' const { fileList } = this const formData = new FormData() formData.append('dfsCode', this.uploadForm.dfsCode) fileList.forEach(file => { formData.append('uploadFile', file) }) this.uploading = true dfsUpload(formData).then(() => { this.fileList = [] this.uploading = false this.$message.success('上传成功') }).catch(err => { console.log('uploading', err) this.$message.error('上传失败') }) }下载getFileUrl (row) { this.listLoading = true this.fileDownload.dfsCode = row.dfsCode this.fileDownload.fileName = row.fileName dfsGetFileUrl(this.fileDownload).then(response => { window.open(response.data) this.listLoading = false }) }, downLoadFile (row) { this.listLoading = true this.fileDownload.dfsCode = row.dfsCode this.fileDownload.fileName = row.fileName this.fileDownload.responseType = 'blob' dfsDownloadFileUrl(this.fileDownload).then(response => { const blob = new Blob([response.data]) var fileName = row.originalName const elink = document.createElement('a') elink.download = fileName elink.style.display = 'none' elink.href = URL.createObjectURL(blob) document.body.appendChild(elink) elink.click() URL.revokeObjectURL(elink.href) document.body.removeChild(elink) this.listLoading = false }) }前端接口import request from '@/utils/request' export function dfsUpload (formData) { return request({ url: '/gitegg-service-extension/extension/upload/file', method: 'post', data: formData }) } export function dfsGetFileUrl (query) { return request({ url: '/gitegg-service-extension/extension/get/file/url', method: 'get', params: query }) } export function dfsDownloadFileUrl (query) { return request({ url: '/gitegg-service-extension/extension/get/file/download', method: 'get', responseType: 'blob', params: query }) }三、功能测试界面1、批量上传上传界面2、文件流下载及获取文件地址文件流下载及获取文件地址备注1、防止文件名重复,这里文件名统一采用七牛云的hash算法,可以防止文件重复,在界面需要展示的文件名,则存储到数据库一个文件名字段进行展示。所有的上传文件都留有记录。
文章
存储  ·  缓存  ·  开发框架  ·  前端开发  ·  算法  ·  测试技术  ·  文件存储  ·  对象存储  ·  微服务  ·  容器
2022-05-19
跳转至:
【阿里云】云大使推广计划
909 人关注 | 7 讨论 | 505 内容
+ 订阅
  • 2022年 | 十月云大使返佣规则
  • 一分钟教你怎么拿到阿里云的数字藏品#阿里云数字藏品合集
  • 2023年 | 四月云大使返佣规则
查看更多 >
开发与运维
5785 人关注 | 133442 讨论 | 319435 内容
+ 订阅
  • 什么证书对信息通信技术行业的人最有用?
  • 什么是受 DRM 保护的内容?
  • 基于duffing振子的微弱信号检测附matlab代码
查看更多 >
云计算
21833 人关注 | 59801 讨论 | 58184 内容
+ 订阅
  • 什么证书对信息通信技术行业的人最有用?
  • Serverless云开发简介及使用步骤
  • Yii2如何进行性能优化?底层原理是什么?
查看更多 >
人工智能
2874 人关注 | 12394 讨论 | 102639 内容
+ 订阅
  • 基于duffing振子的微弱信号检测附matlab代码
  • 计算均匀线阵的3dB波束宽度matlab代码
  • 基于MATLAB模拟圆周阵列天线
查看更多 >
数据库
252945 人关注 | 52317 讨论 | 99236 内容
+ 订阅
  • 基于duffing振子的微弱信号检测附matlab代码
  • Yii2如何进行性能优化?底层原理是什么?
  • Yii2如何进行缓存优化?底层原理是什么?
查看更多 >