独立直链和短链功能

This commit is contained in:
zhaojun
2022-07-28 21:24:38 +08:00
parent 43d8143c74
commit 0f167a304d
4 changed files with 138 additions and 84 deletions

View File

@@ -1,13 +1,18 @@
package im.zhaojun.zfile.admin.model.entity;
import cn.hutool.extra.servlet.ServletUtil;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import im.zhaojun.zfile.common.util.RequestHolder;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpHeaders;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Date;
@@ -19,6 +24,7 @@ import java.util.Date;
@Data
@ApiModel(value="文件下载日志")
@TableName(value = "`download_log`")
@NoArgsConstructor
public class DownloadLog implements Serializable {
private static final long serialVersionUID = 1L;
@@ -62,4 +68,16 @@ public class DownloadLog implements Serializable {
@ApiModelProperty(value="访问 referer")
private String referer;
public DownloadLog(String path, String storageKey, String shortKey) {
this.path = path;
this.storageKey = storageKey;
this.shortKey = shortKey;
this.createTime = new Date();
HttpServletRequest request = RequestHolder.getRequest();
this.ip = ServletUtil.getClientIP(request);
this.referer = request.getHeader(HttpHeaders.REFERER);
this.userAgent = request.getHeader(HttpHeaders.USER_AGENT);
}
}

View File

@@ -1,13 +1,32 @@
package im.zhaojun.zfile.admin.service;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import im.zhaojun.zfile.admin.mapper.ShortLinkMapper;
import im.zhaojun.zfile.admin.model.entity.DownloadLog;
import im.zhaojun.zfile.admin.model.entity.ShortLink;
import im.zhaojun.zfile.common.context.StorageSourceContext;
import im.zhaojun.zfile.common.exception.InvalidStorageSourceException;
import im.zhaojun.zfile.common.exception.file.operator.DownloadFileException;
import im.zhaojun.zfile.common.util.HttpUtil;
import im.zhaojun.zfile.common.util.RequestHolder;
import im.zhaojun.zfile.home.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.home.service.base.AbstractBaseFileService;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.util.EncodingUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
/**
@@ -16,6 +35,7 @@ import java.util.Date;
* @author zhaojun
*/
@Service
@Slf4j
public class ShortLinkService extends ServiceImpl<ShortLinkMapper, ShortLink> implements IService<ShortLink> {
@Resource
@@ -24,6 +44,15 @@ public class ShortLinkService extends ServiceImpl<ShortLinkMapper, ShortLink> im
@Resource
private StorageSourceService storageSourceService;
@Resource
private StorageSourceContext storageSourceContext;
@Resource
private SystemConfigService systemConfigService;
@Resource
private DownloadLogService downloadLogService;
/**
* 根据短链接 key 查询短链接
*
@@ -113,4 +142,77 @@ public class ShortLinkService extends ServiceImpl<ShortLinkMapper, ShortLink> im
return shortLink;
}
/**
* 处理指定存储源的下载请求
*
* @param storageKey
* 存储源 key
*
* @param filePath
* 文件路径
*
* @param shortKey
* 短链接 key
*
* @throws IOException 可能抛出的 IO 异常
*/
public void handlerDownload(String storageKey, String filePath, String shortKey) throws IOException {
HttpServletResponse response = RequestHolder.getResponse();
// 获取存储源 Service
AbstractBaseFileService<?> fileService;
try {
fileService = storageSourceContext.getByKey(storageKey);
} catch (InvalidStorageSourceException e) {
log.error("无效的存储源,存储源 key {}, 文件路径 {}", storageKey, filePath);
response.setHeader(HttpHeaders.CONTENT_TYPE, "text/plain;charset=utf-8");
response.getWriter().write("无效的或初始化失败的存储源, 请联系管理员!");
return;
}
// 获取文件下载链接
String downloadUrl;
try {
downloadUrl = fileService.getDownloadUrl(filePath);
} catch (DownloadFileException e) {
log.error("获取文件下载链接异常 {}. 存储源 ID: {}, 文件路径: {}", e.getMessage(), e.getStorageId(), e.getPathAndName());
response.setHeader(HttpHeaders.CONTENT_TYPE, "text/plain;charset=utf-8");
response.getWriter().write("获取下载链接异常,请联系管理员!");
return;
}
// 判断下载链接是否为空
if (StrUtil.isEmpty(downloadUrl)) {
log.error("获取到文件下载链接为空,存储源 key {}, 文件路径 {}", storageKey, filePath);
response.setHeader(HttpHeaders.CONTENT_TYPE, "text/plain;charset=utf-8");
response.getWriter().write("获取下载链接异常,请联系管理员![2]");
}
// 记录下载日志.
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
Boolean recordDownloadLog = systemConfig.getRecordDownloadLog();
if (BooleanUtil.isTrue(recordDownloadLog)) {
DownloadLog downloadLog = new DownloadLog(filePath, storageKey, shortKey);
downloadLogService.save(downloadLog);
}
// 判断下载链接是否为 m3u8 格式, 如果是则返回 m3u8 内容.
if (StrUtil.equalsIgnoreCase(FileUtil.extName(filePath), "m3u8")) {
String textContent = HttpUtil.getTextContent(downloadUrl);
response.setContentType("application/vnd.apple.mpegurl;charset=utf-8");
OutputStream outputStream = response.getOutputStream();
byte[] textContentBytes = EncodingUtils.getBytes(textContent, CharsetUtil.CHARSET_UTF_8.displayName());
IoUtil.write(outputStream, true, textContentBytes);
}
// 禁止直链被浏览器 302 缓存.
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate, private");
response.setHeader(HttpHeaders.PRAGMA, "no-cache");
response.setHeader(HttpHeaders.EXPIRES, "0");
// 重定向到下载链接.
response.sendRedirect(downloadUrl);
}
}

View File

@@ -6,6 +6,7 @@ import com.github.xiaoymin.knife4j.annotations.ApiSort;
import com.github.xiaoymin.knife4j.annotations.DynamicParameter;
import com.github.xiaoymin.knife4j.annotations.DynamicResponseParameters;
import im.zhaojun.zfile.admin.model.entity.ShortLink;
import im.zhaojun.zfile.admin.model.entity.StorageSource;
import im.zhaojun.zfile.admin.service.ShortLinkService;
import im.zhaojun.zfile.admin.service.StorageSourceService;
import im.zhaojun.zfile.admin.service.SystemConfigService;
@@ -25,6 +26,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.annotation.Resource;
import java.io.IOException;
/**
* 短链接口
@@ -67,7 +69,7 @@ public class ShortLinkController {
Boolean showShortLink = systemConfig.getShowShortLink();
Boolean showPathLink = systemConfig.getShowPathLink();
if ( BooleanUtil.isFalse(showShortLink) && BooleanUtil.isFalse(showPathLink)) {
throw new IllegalDownloadLinkException("当前系统不允许使用短链和短链.");
throw new IllegalDownloadLinkException("当前系统不允许使用短链.");
}
String domain = systemConfig.getDomain();
@@ -91,28 +93,28 @@ public class ShortLinkController {
@ApiOperationSupport(order = 2)
@ApiOperation(value = "跳转短链", notes = "根据短链 key 跳转302 重定向)到对应的直链.")
@ApiImplicitParam(paramType = "path", name = "key", value = "短链 key", required = true)
public String parseShortKey(@PathVariable String key) {
public String parseShortKey(@PathVariable String key) throws IOException {
ShortLink shortLink = shortLinkService.findByKey(key);
if (shortLink == null) {
throw new RuntimeException("此直链不存在或已失效.");
}
// 获取站点域名
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
String domain = systemConfig.getDomain();
// 是否允许生成短链.
// 判断是否允许生成短链.
Boolean showShortLink = systemConfig.getShowShortLink();
if ( BooleanUtil.isFalse(showShortLink)) {
throw new IllegalDownloadLinkException("当前系统不允许使用短链.");
}
String directLinkPrefix = systemConfig.getDirectLinkPrefix();
Integer storageId = shortLink.getStorageId();
String storageKey = storageSourceService.findKeyById(storageId);
String filePath = StringUtils.encodeAllIgnoreSlashes(shortLink.getUrl());
StorageSource storageSource = storageSourceService.findById(storageId);
String storageKey = storageSource.getKey();
String filePath = shortLink.getUrl();
String url = StringUtils.concat(domain, directLinkPrefix, storageKey, filePath);
return "redirect:" + url;
shortLinkService.handlerDownload(storageKey, filePath, shortLink.getShortKey());
return null;
}
}

View File

@@ -1,14 +1,9 @@
package im.zhaojun.zfile.home.filter;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.extra.spring.SpringUtil;
import im.zhaojun.zfile.admin.model.entity.DownloadLog;
import im.zhaojun.zfile.admin.model.entity.ShortLink;
import im.zhaojun.zfile.admin.model.entity.StorageSource;
import im.zhaojun.zfile.admin.service.DownloadLogService;
@@ -18,14 +13,9 @@ import im.zhaojun.zfile.admin.service.StorageSourceService;
import im.zhaojun.zfile.admin.service.SystemConfigService;
import im.zhaojun.zfile.common.constant.ZFileConstant;
import im.zhaojun.zfile.common.context.StorageSourceContext;
import im.zhaojun.zfile.common.exception.InvalidStorageSourceException;
import im.zhaojun.zfile.common.exception.file.operator.DownloadFileException;
import im.zhaojun.zfile.common.util.HttpUtil;
import im.zhaojun.zfile.common.util.StringUtils;
import im.zhaojun.zfile.home.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.home.service.base.AbstractBaseFileService;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.util.EncodingUtils;
import org.springframework.http.HttpHeaders;
import javax.servlet.Filter;
@@ -37,8 +27,6 @@ import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.List;
@@ -109,7 +97,6 @@ public class DownloadLinkFilter implements Filter {
// 获取系统配置的直链前缀
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
String directLinkPrefix = systemConfig.getDirectLinkPrefix();
if (StrUtil.equalsIgnoreCase(currentRequestPrefix, directLinkPrefix)) {
if (BooleanUtil.isFalse(systemConfig.getShowPathLink())) {
@@ -129,27 +116,7 @@ public class DownloadLinkFilter implements Filter {
httpServletResponse.sendRedirect(forbiddenUrl);
return;
}
Boolean recordDownloadLog = systemConfig.getRecordDownloadLog();
if (BooleanUtil.isTrue(recordDownloadLog)) {
DownloadLog downloadLog = new DownloadLog();
downloadLog.setPath(decodeFilePath);
downloadLog.setStorageKey(currentStorageKey);
downloadLog.setCreateTime(new Date());
downloadLog.setIp(ServletUtil.getClientIP(httpServletRequest));
downloadLog.setReferer(httpServletRequest.getHeader(HttpHeaders.REFERER));
downloadLog.setUserAgent(httpServletRequest.getHeader(HttpHeaders.USER_AGENT));
ShortLink shortLink = shortLinkService.findByStorageIdAndUrl(storageId, decodeFilePath);
// 如果没有短链,则生成短链
if (shortLink == null) {
shortLink = shortLinkService.generatorShortLink(storageId, decodeFilePath);
}
downloadLog.setShortKey(shortLink.getShortKey());
downloadLogService.save(downloadLog);
}
handleDownloadLink(httpServletResponse, currentStorageKey, decodeFilePath);
handleDownloadLink(httpServletResponse, storageId, currentStorageKey, decodeFilePath);
return;
}
}
@@ -170,7 +137,7 @@ public class DownloadLinkFilter implements Filter {
* @param filePath
* 文件路径
*/
private void handleDownloadLink(HttpServletResponse response, String storageKey, String filePath) throws IOException {
private void handleDownloadLink(HttpServletResponse response, Integer storageId, String storageKey, String filePath) throws IOException {
StorageSource storageSource = storageSourceService.findByStorageKey(storageKey);
Boolean enable = storageSource.getEnable();
if (!enable) {
@@ -184,48 +151,13 @@ public class DownloadLinkFilter implements Filter {
filePath = "/" + filePath;
}
AbstractBaseFileService<?> fileService;
try {
fileService = storageSourceContext.getByKey(storageKey);
} catch (InvalidStorageSourceException e) {
log.error("无效的存储源,存储源 key {}, 文件路径 {}", storageKey, filePath);
response.setHeader(HttpHeaders.CONTENT_TYPE, "text/plain;charset=utf-8");
response.getWriter().write("无效的或初始化失败的存储源, 请联系管理员!");
return;
ShortLink shortLink = shortLinkService.findByStorageIdAndUrl(storageId, filePath);
// 如果没有短链,则生成短链
if (shortLink == null) {
shortLink = shortLinkService.generatorShortLink(storageId, filePath);
}
String downloadUrl;
try {
downloadUrl = fileService.getDownloadUrl(filePath);
} catch (DownloadFileException e) {
log.error("获取文件下载链接异常 {}. 存储源 ID: {}, 文件路径: {}", e.getMessage(), e.getStorageId(), e.getPathAndName());
response.setHeader(HttpHeaders.CONTENT_TYPE, "text/plain;charset=utf-8");
response.getWriter().write("获取下载链接异常,请联系管理员!");
return;
}
if (StrUtil.isEmpty(downloadUrl)) {
log.error("获取到文件下载链接为空,存储源 key {}, 文件路径 {}", storageKey, filePath);
response.setHeader(HttpHeaders.CONTENT_TYPE, "text/plain;charset=utf-8");
response.getWriter().write("获取下载链接异常,请联系管理员![2]");
return;
}
if (StrUtil.equalsIgnoreCase(FileUtil.extName(filePath), "m3u8")) {
String textContent = HttpUtil.getTextContent(downloadUrl);
response.setContentType("application/vnd.apple.mpegurl;charset=utf-8");
OutputStream outputStream = response.getOutputStream();
byte[] textContentBytes = EncodingUtils.getBytes(textContent, CharsetUtil.CHARSET_UTF_8.displayName());
IoUtil.write(outputStream, true, textContentBytes);
return;
}
// 禁止直链被浏览器 302 缓存.
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate, private");
response.setHeader(HttpHeaders.PRAGMA, "no-cache");
response.setHeader(HttpHeaders.EXPIRES, "0");
response.sendRedirect(downloadUrl);
shortLinkService.handlerDownload(storageKey, filePath, shortLink.getShortKey());
}
}