文章目录
- 1. 根据下载链接下载文件
- 2. 根据下载链接下载文件
- 1. 根据token获取存放在redis中的fileId
- 2. 根据fileId获得文件信息
- 3. 确定用户拥有操作这个fileId的权限
- 4. 获取文件在服务器的存储路径
- 5. 如果用户指定了下载时的文件名则处理文件名
- 6. 验证文件路径是否存在
- 7. 下载文件
- 8. 更新文件操作时间
系列文章:
文件 - 01 上传附件到服务器并保存信息到MySQL
文件 - 02 上传临时文件到服务器
文件 - 03 根据文件id获取下载链接
整体思路:
附件表:
CREATE TABLE `t_upload_file` (`id` int(11) NOT NULL AUTO_INCREMENT,`fileId` varchar(50) NOT NULL COMMENT '文件id',`fileName` varchar(50) NOT NULL COMMENT '文件名',`fileType` varchar(100) NOT NULL COMMENT '文件类型',`fileSize` int(10) NOT NULL COMMENT '文件大小bytes',`filePathSuffix` varchar(60) NOT NULL COMMENT '文件存储路径',`fileUploader` varchar(32) DEFAULT NULL,`uploadTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '文件上传时间',`lastUsedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '文件最近使用时间',`neverExpire` tinyint(1) NOT NULL DEFAULT '1' COMMENT '文件是否永不过期,当为1时,则忽略expireTime',`expireTime` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '文件过期时间',PRIMARY KEY (`id`),UNIQUE KEY `fileId` (`fileId`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
1. 根据下载链接下载文件
@RestController
@ResponseBody
@ResponseResult
@Slf4j
@RequestMapping("/ngsoc/PORTAL/api/v1")
@Api(tags = "File")
public class FileController {@ApiOperation(value = "根据下载链接下载文件", notes = "根据输入的token查询redis,如果存在/尚未过期,则下载文件", httpMethod = "GET")@RequestMapping(value = "/files/fetch/{token}", method = RequestMethod.GET)public ResponseEntity<StreamingResponseBody> downloadFileByToken(@ApiParam(value = "下载链接token", required = true) @PathVariable("token") String downloadToken,@ApiParam(value = "用户输入的文件名", required = false) @RequestParam(value = "inputFileName", required = false, defaultValue = "") String inputFileName) throws UploadDownloadFileException {return fileService.downloadFileByToken(downloadToken, inputFileName);}
}
2. 根据下载链接下载文件
public interface IFileService {/*** 根据下载链接下载文件** @param downloadToken 下载链接token* @param inputFileName 用户传入的文件名* @return* @throws UploadDownloadFileException*/ResponseEntity<StreamingResponseBody> downloadFileByToken(String downloadToken, String inputFileName) throws UploadDownloadFileException;
}
@Controller
@Slf4j
public class FileServiceImpl implements IFileService {@Autowiredprivate IFileMapper fileDao;@Autowiredprivate FileConfig fileConfig;@Autowiredprivate RedisTemplate<String, String> stringRedisTemplate;@Setter(onMethod_ = @Autowired)private FileIdAuthorityServiceImpl fileIdAuthorityServiceImpl;/*** 根据token值 下载文件** @param downloadToken 返回的下载链接中的token* @return 下载文件*/@Overridepublic ResponseEntity<StreamingResponseBody> downloadFileByToken(String downloadToken, String inputFileName) throws UploadDownloadFileException {// 根据token获取存放在redis中的fileIdString fileId = this.stringRedisTemplate.opsForValue().get(downloadToken);if (StringUtils.isEmpty(fileId)) {log.error("Download link expired.");throw new UploadDownloadFileException(I18nUtils.i18n("exception.file.redis.expire"));}return downloadFileById(fileId, inputFileName);}/*** 下載文件* @param fileId 文件id* @param inputFileName 用户传入的文件名* @return* @throws UploadDownloadFileException*/@Overridepublic ResponseEntity<StreamingResponseBody> downloadFileById(String fileId, String inputFileName)throws UploadDownloadFileException {// 根据fileId --> path , name --> 更新文件最近使用时间 --> returnFileInfo fileInfo = fileDao.getFileNameTypePathByFileId(fileId);fileIdAuthorityServiceImpl.assertThisUserHaveAuthority(fileInfo);if (ObjectUtils.isEmpty(fileInfo)) {throw new UploadDownloadFileException(I18nUtils.i18n("exception.file.mysql.absent"));}String storePath = getStorePath(fileInfo);// 如果用户指定了下载时的文件名String fileName;if (StringUtils.isEmpty(inputFileName)) {fileName = fileInfo.getFileName();} else {fileName = inputFileName.concat(getDotSuffix(fileInfo.getFileName()));}// 下载时,验证文件路径是否存在if (!FileUtil.exist(storePath)) {log.error("File not exist.");throw new UploadDownloadFileException(I18nUtils.i18n("exception.file.download"));}// 下载的时候 直接下载,不用验证类型 调用downloadFile即可ResponseEntity<StreamingResponseBody> responseBodyResponseEntity = downloadFile(storePath, fileName);// 更新 mysql中文件的最后过期时间fileDao.updateLastUsedTime(fileId, new Timestamp(System.currentTimeMillis()));return responseBodyResponseEntity;}}
1. 根据token获取存放在redis中的fileId
String fileId = this.stringRedisTemplate.opsForValue().get(downloadToken);
2. 根据fileId获得文件信息
@Mapper
@Repository
public interface IFileMapper {/*** 根据id获得文件名, 在文件下载时需要文件的初始名展示给用户** @param fileId 文件id* @return*/FileInfo getFileNameTypePathByFileId(@Param("fileId") String fileId);
}
<select id="getFileNameTypePathByFileId" parameterType="String"resultType="portal.model.entity.FileInfo">select fileName, fileType, filePathSuffix from t_upload_filewhere fileId = #{fileId} and <include refid="unexpiredFilter"/>
</select><!-- 大于号 >-->
<!-- 永不过期 neverExpire = 1,或者未过期的,即过期时间 >= 当前时间 -->
<sql id="unexpiredFilter">(neverExpire = 1 or expireTime >= current_timestamp())
</sql>
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "文件信息", description = "文件基本信息表")
public class FileInfo {@ApiModelProperty(value = "文件Id", required = true, example = "b70c68b5833f41b6a8f506698541b7141604318375398")private String fileId;@ApiModelProperty(value = "文件名", required = true, example = "xxx.png")private String fileName;@ApiModelProperty(value = "文件类型", required = true, example = "image/png")private String fileType;@ApiModelProperty(value = "文件大小bytes", required = true, example = "1211")private Long fileSize;@ApiModelProperty(value = "文件存储路径后缀", required = true, example = "as\\da\\6d3a8c3629fb48cea1c22c5e74b4801e")private String filePathSuffix;@ApiModelProperty(value = "文件上传者", required = true, example = "1211")private String fileUploader;@ApiModelProperty(value = "文件是否永不过期", required = true, example = "1")private Integer neverExpire;@ApiModelProperty(value = "文件过期时间", example = "2020-11-03 17:20:00")private Timestamp expireTime;@ApiModelProperty(value = "文件信息上传时间", required = true, example = "2020-11-03 17:12:58")private Timestamp uploadTime;@ApiModelProperty(value = "文件信息更新时间", required = true, example = "2020-11-03 17:13:14")private Timestamp lastUsedTime;/*** 是否所有人都可以获取&操作该文件** 如果为null,则视为与true等同处理*/@ApiModelProperty(value = "是否所有人都可以获取&操作该文件", required = true, example = "1")private Integer share;
}
3. 确定用户拥有操作这个fileId的权限
public interface FileIdAuthorityService {/*** 确定用户拥有操作这个fileId的权限* <p>* 否则报错** @param fileInfo fileInfo* @return FileInfo* @throws FileIdAuthorityException 当用户无权限操作该文件时*/@NullableFileInfo assertThisUserHaveAuthority(@Nullable FileInfo fileInfo);
}
@Slf4j
@Service
public class FileIdAuthorityServiceImpl implements FileIdAuthorityService {@Setter(onMethod_ = @Autowired)private IFileMapper fileDao;@Nullable@Overridepublic FileInfo assertThisUserHaveAuthority(@Nullable FileInfo fileInfo) {if (fileInfo == null) {return null;}String userName = SpringSecurityUtil.getCurrentUserNameFromSpringSecurity();return this.assertThisUserHaveAuthority(fileInfo, userName);}@Nullable@Overridepublic FileInfo assertThisUserHaveAuthority(@Nullable FileInfo fileInfo, @Nullable String userName) {log.debug("assertThisUserHaveAuthority : fileInfo : {} , userName : {}", fileInfo, userName);if (fileInfo == null) {return null;}if (fileInfo.getShare() == null || fileInfo.getShare() == FileInfoShareEnum.SHARE_TO_ALL.getValue()) {return fileInfo;}if (StringUtils.equalsAny(userName,"transfer",fileInfo.getFileUploader())) {// 按照当前云soc权限设计,凡是API调用都是transfer账户return fileInfo;}throw new FileIdAuthorityException(fileInfo.getFileId(),userName);}
}
从SpringSecurity获取当前用户名称
/*** 用于从SpringSecurity中获取当前用户的用户名*/
public class SpringSecurityUtil {/*** 静态工具类,禁止创建实例*/private SpringSecurityUtil() {}/*** 从SpringSecurity获取当前用户名称** @return 当前用户名称*/@Nullablepublic static String getCurrentUserNameFromSpringSecurity() {return Optional.ofNullable(UserInfoShareHolder.getUserInfo()).map(UserInfo::getName).orElse(null);}
}
/*** 用户信息共享的ThreadLocal类*/
public class UserInfoShareHolder {private static final ThreadLocal<UserInfo> USER_INFO_THREAD_LOCAL = new TransmittableThreadLocal<>();/*** 存储用户信息** @param userInfo 用户信息* @return*/public static void setUserInfo(UserInfo userInfo) {USER_INFO_THREAD_LOCAL.set(userInfo);}/*** 获取用户相关信息** @return*/public static UserInfo getUserInfo() {return USER_INFO_THREAD_LOCAL.get();}/*** 清除ThreadLocal信息*/public static void remove() {USER_INFO_THREAD_LOCAL.remove();}
}
4. 获取文件在服务器的存储路径
String storePath = getStorePath(fileInfo);
// /opt/ngsoc/data/local/upload/12/29/6dd633cdfba7489f8ecc1054366ce274
private String getStorePath(FileInfo fileInfo) {return Paths.get(fileConfig.getSavePath(), fileInfo.getFilePathSuffix()).toString();
}
5. 如果用户指定了下载时的文件名则处理文件名
String fileName;
if (StringUtils.isEmpty(inputFileName)) {fileName = fileInfo.getFileName();
} else {fileName = inputFileName.concat(getDotSuffix(fileInfo.getFileName()));
}
private String getDotSuffix(String fileName) {return fileName.substring(fileName.lastIndexOf("."));
}
6. 验证文件路径是否存在
// 下载时,验证文件路径是否存在
if (!FileUtil.exist(storePath)) {log.error("File not exist.");throw new UploadDownloadFileException(I18nUtils.i18n("exception.file.download"));
}
7. 下载文件
ResponseEntity<StreamingResponseBody> responseBodyResponseEntity = downloadFile(storePath, fileName);
@Override
public ResponseEntity<StreamingResponseBody> downloadFile(String filePath, String fileName) {HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);headers.setContentDisposition(ContentDisposition.parse("attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)));StreamingResponseBody streamingResponseBody = outputStream -> {Files.copy(Paths.get(filePath), outputStream);};return new ResponseEntity<>(streamingResponseBody, headers, HttpStatus.OK);
}
8. 更新文件操作时间
fileDao.updateLastUsedTime(fileId, new Timestamp(System.currentTimeMillis()));
@Mapper
@Repository
public interface IFileMapper {/*** 文件被下载时 修改文件的最后使用时间** @param lastUsedTime 当前时间* @param fileId 文件id*/void updateLastUsedTime(@Param("fileId") String fileId, @Param("lastUsedTime") Timestamp lastUsedTime);
}
<update id="updateLastUsedTime">update t_upload_file set lastUsedTime = #{lastUsedTime}where fileId = #{fileId} and <include refid="unexpiredFilter"/>
</update><!-- 大于号 >-->
<!-- 永不过期 neverExpire = 1,或者未过期的,即过期时间 >= 当前时间 -->
<sql id="unexpiredFilter">(neverExpire = 1 or expireTime >= current_timestamp())
</sql>