本地存储
本篇文章介绍一个实例,演示如何将文件存储到本地文件系统。通过配置和编写相应的代码,实现文件的上传、存储及访问功能。
目录
实现本地存储
配置文件
在 app.properties
文件中配置服务器端口、文件上传大小限制、静态资源位置、数据库连接信息以及文件访问前缀 URL。
server.port=8123
http.multipart.max-request-size=73741824
http.multipart.max-file-size=73741824
server.resources.static-locations=pages
DATABASE_DSN=postgresql://postgres:123456@192.168.3.9:5432/static_file
file_prefix_url=http://localhost:8123
配置说明:
server.port=8123
:指定服务器监听的端口号为 8123。http.multipart.max-request-size=73741824
:设置 HTTP 请求的最大大小为 73,741,824 字节(约 70 MB)。http.multipart.max-file-size=73741824
:设置单个文件上传的最大大小为 73,741,824 字节(约 70 MB)。server.resources.static-locations=pages
:指定静态资源的存储位置为项目根目录下的pages
文件夹。这样,存储在pages
目录中的文件可以通过 URL 直接访问。DATABASE_DSN=postgresql://postgres:123456@192.168.3.9:5432/static_file
:配置数据库连接字符串,连接到 PostgreSQL 数据库。file_prefix_url=http://localhost:8123
:配置文件访问的前缀 URL,用于生成文件的访问链接。
配置类
配置类负责初始化数据库连接池、ActiveRecord 插件以及注册文件处理器的路由。
数据库配置类 DbConfig
package com.litongjava.file.config;
import com.jfinal.template.Engine;
import com.jfinal.template.source.ClassPathSourceFactory;
import com.litongjava.annotation.AConfiguration;
import com.litongjava.annotation.Initialization;
import com.litongjava.db.activerecord.ActiveRecordPlugin;
import com.litongjava.db.activerecord.OrderedFieldContainerFactory;
import com.litongjava.db.activerecord.dialect.PostgreSqlDialect;
import com.litongjava.db.hikaricp.HikariCpPlugin;
import com.litongjava.model.dsn.JdbcInfo;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.utils.dsn.DbDSNParser;
import com.litongjava.tio.utils.environment.EnvUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@AConfiguration
public class DbConfig {
@Initialization
public void config() {
// 获取数据库连接信息
String dsn = EnvUtils.getStr("DATABASE_DSN");
JdbcInfo jdbc = new DbDSNParser().parse(dsn);
// 初始化 HikariCP 数据库连接池
HikariCpPlugin hikariCpPlugin = new HikariCpPlugin(jdbc.getUrl(), jdbc.getUser(), jdbc.getPswd());
hikariCpPlugin.start();
// 初始化 ActiveRecordPlugin
ActiveRecordPlugin arp = new ActiveRecordPlugin(hikariCpPlugin);
// 开发环境下启用开发模式
if (EnvUtils.isDev()) {
arp.setDevMode(true);
}
// 是否展示 SQL
boolean showSql = EnvUtils.getBoolean("jdbc.showSql", false);
log.info("show sql:{}", showSql);
arp.setShowSql(showSql);
arp.setDialect(new PostgreSqlDialect());
arp.setContainerFactory(new OrderedFieldContainerFactory());
// 配置模板引擎
Engine engine = arp.getEngine();
engine.setSourceFactory(new ClassPathSourceFactory());
engine.setCompressorOn(' ');
engine.setCompressorOn('\n');
// 启动 ActiveRecordPlugin
arp.start();
// 在应用销毁时停止数据库连接池
TioBootServer.me().addDestroyMethod(() -> {
arp.stop();
hikariCpPlugin.stop();
});
}
}
关键点说明:
- 数据库连接初始化:通过
DATABASE_DSN
获取数据库连接信息,并使用HikariCpPlugin
初始化数据库连接池。 - ActiveRecordPlugin 配置:设置开发模式、SQL 显示选项、方言(PostgreSQL)、容器工厂等,并启动插件。
- 模板引擎配置:配置 JFinal 模板引擎的源工厂和压缩选项。
- 资源回收:注册应用销毁时的回调方法,确保数据库连接池和 ActiveRecordPlugin 正确关闭,释放资源。
处理器配置类 HandlerConfiguration
package com.litongjava.file.config;
import com.litongjava.annotation.AConfiguration;
import com.litongjava.annotation.Initialization;
import com.litongjava.file.handler.SystemFileHandler;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.http.server.router.HttpRequestRouter;
@AConfiguration
public class HandlerConfiguration {
@Initialization
public void config() {
HttpRequestRouter router = TioBootServer.me().getRequestRouter();
if (router == null) {
return;
}
// 获取文件处理器实例并注册路由
SystemFileHandler fileHandler = Aop.get(SystemFileHandler.class);
router.add("/api/system/file/upload", fileHandler::upload);
router.add("/api/system/file/url", fileHandler::getUrl);
}
}
关键点说明:
- 路由注册:通过
HttpRequestRouter
注册文件上传和获取 URL 的 API 路径,分别对应upload
和getUrl
方法。 - 依赖注入:使用 AOP 获取
SystemFileHandler
实例,确保处理器能够被正确管理和注入。
业务层
业务层负责处理文件的上传、存储和查询逻辑。
package com.litongjava.file.service;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import com.litongjava.db.activerecord.Record;
import com.litongjava.file.dao.SystemUploadFileDao;
import com.litongjava.file.model.UploadResultVo;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.tio.http.common.UploadFile;
import com.litongjava.tio.utils.crypto.Md5Utils;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.litongjava.tio.utils.hutool.FileUtil;
import com.litongjava.tio.utils.hutool.FilenameUtils;
import com.litongjava.tio.utils.snowflake.SnowflakeIdUtils;
public class SystemFileService {
/**
* 上传文件并存储到本地文件系统
*
* @param uploadFile 上传的文件
* @param bucketName 存储桶名称
* @param category 文件分类
* @return UploadResultVo 文件上传结果
*/
public UploadResultVo upload(UploadFile uploadFile, String bucketName, String category) {
if (uploadFile != null) {
byte[] fileData = uploadFile.getData();
String digestHex = Md5Utils.digestHex(fileData);
SystemUploadFileDao systemUploadFileDao = Aop.get(SystemUploadFileDao.class);
Record record = systemUploadFileDao.getFileBasicInfoByMd5(bucketName, digestHex);
// 如果文件已存在,返回已有文件信息
if (record != null) {
Long id = record.getLong("id");
String filename = record.getStr("filename");
String targetName = record.getStr("target_name");
String url = getUrl(bucketName, targetName);
return new UploadResultVo(id, filename, url, digestHex);
}
// 生成新的文件名和路径
String originFilename = uploadFile.getName();
String suffix = FilenameUtils.getSuffix(originFilename);
long id = SnowflakeIdUtils.id();
String filename = id + "." + suffix;
Path path = Paths.get("pages", bucketName, category);
// 创建目录(如果不存在)
try {
Files.createDirectories(path);
} catch (IOException e) {
e.printStackTrace();
return null;
}
// 完整文件路径
Path filePath = path.resolve(filename);
File file = filePath.toFile();
// 将文件数据写入指定路径
FileUtil.writeBytes(fileData, file);
String targetName = category + "/" + filename;
String url = getUrl(bucketName, targetName);
systemUploadFileDao.save(id, digestHex, originFilename, fileData.length, "local", bucketName, targetName);
return new UploadResultVo(id, originFilename, url, digestHex);
}
return null;
}
/**
* 根据存储桶和目标名称生成文件访问 URL
*
* @param bucketName 存储桶名称
* @param targetName 目标名称
* @return 文件访问 URL
*/
public String getUrl(String bucketName, String targetName) {
String prefixUrl = EnvUtils.getStr("file_prefix_url");
return prefixUrl + "/" + bucketName + "/" + targetName;
}
/**
* 根据文件 ID 获取文件 URL
*
* @param id 文件 ID
* @return UploadResultVo 文件上传结果
*/
public UploadResultVo getUrlById(Long id) {
SystemUploadFileDao systemUploadFileDao = Aop.get(SystemUploadFileDao.class);
Record record = systemUploadFileDao.getFileBasicInfoById(id);
String md5 = record.getStr("md5");
String filename = record.getStr("filename");
String bucketName = record.getStr("bucket_name");
String targetName = record.getStr("target_name");
String url = getUrl(bucketName, targetName);
return new UploadResultVo(id, filename, url, md5);
}
/**
* 根据文件 MD5 获取文件 URL
*
* @param bucketName 存储桶名称
* @param md5 文件 MD5 值
* @return UploadResultVo 文件上传结果
*/
public UploadResultVo getUrlByMd5(String bucketName, String md5) {
SystemUploadFileDao systemUploadFileDao = Aop.get(SystemUploadFileDao.class);
Record record = systemUploadFileDao.getFileBasicInfoByMd5(bucketName, md5);
Long id = record.getLong("id");
String filename = record.getStr("filename");
String targetName = record.getStr("target_name");
String url = getUrl(bucketName, targetName);
return new UploadResultVo(id, filename, url, md5);
}
}
功能说明:
文件上传:
- MD5 校验:通过文件数据生成 MD5 值,检查文件是否已存在,以避免重复上传。
- 文件存储:生成唯一文件名,创建存储路径(
pages/{bucketName}/{category}
),将文件数据写入本地文件系统。 - 数据库记录:将文件的基本信息(如 ID、MD5、文件名、存储路径等)保存到数据库中。
URL 生成:根据存储桶名称和目标文件名生成文件的访问 URL。
文件查询:
- 按 ID 查询:根据文件 ID 获取文件的 URL 和其他信息。
- 按 MD5 查询:根据文件的 MD5 值和存储桶名称获取文件的 URL 和其他信息。
接入层
接入层负责处理 HTTP 请求,调用业务层的相关方法,并返回响应结果。
package com.litongjava.file.handler;
import com.jfinal.kit.StrKit;
import com.litongjava.file.model.UploadResultVo;
import com.litongjava.file.service.SystemFileService;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.model.body.RespBodyVo;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.common.UploadFile;
import com.litongjava.tio.http.server.model.HttpCors;
import com.litongjava.tio.http.server.util.CORSUtils;
import com.litongjava.tio.http.server.util.Resps;
public class SystemFileHandler {
/**
* 处理文件上传请求
*
* @param request HTTP 请求
* @return HttpResponse 响应
*/
public HttpResponse upload(HttpRequest request) {
HttpResponse httpResponse = TioRequestContext.getResponse();
CORSUtils.enableCORS(httpResponse, new HttpCors());
UploadFile uploadFile = request.getUploadFile("file");
String bucket = request.getParam("bucket");
String category = request.getParam("category");
if (uploadFile == null) {
return httpResponse.fail(RespBodyVo.fail("请求体中未找到文件"));
}
if (bucket == null) {
return httpResponse.fail(RespBodyVo.fail("存储桶名称不能为空"));
}
if (category == null) {
return httpResponse.fail(RespBodyVo.fail("文件分类不能为空"));
}
SystemFileService systemFileService = Aop.get(SystemFileService.class);
UploadResultVo vo = systemFileService.upload(uploadFile, bucket, category);
return httpResponse.setJson(vo);
}
/**
* 处理获取文件 URL 的请求
*
* @param request HTTP 请求
* @return HttpResponse 响应
* @throws Exception 异常
*/
public HttpResponse getUrl(HttpRequest request) throws Exception {
HttpResponse httpResponse = TioRequestContext.getResponse();
CORSUtils.enableCORS(httpResponse, new HttpCors());
SystemFileService systemFileService = Aop.get(SystemFileService.class);
RespBodyVo respBodyVo = null;
Long id = request.getLong("id");
String md5 = request.getParam("md5");
String bucket = request.getParam("bucket");
if (id != null && id > 0) {
// 根据 ID 获取文件信息
UploadResultVo uploadResultVo = systemFileService.getUrlById(id);
if (uploadResultVo == null) {
respBodyVo = RespBodyVo.fail();
} else {
respBodyVo = RespBodyVo.ok(uploadResultVo);
}
} else if (StrKit.notBlank(md5)) {
// 根据 MD5 获取文件信息
UploadResultVo uploadResultVo = systemFileService.getUrlByMd5(bucket, md5);
if (uploadResultVo == null) {
respBodyVo = RespBodyVo.fail();
} else {
respBodyVo = RespBodyVo.ok(uploadResultVo);
}
} else {
respBodyVo = RespBodyVo.fail("id 或 md5 不能为空");
}
return Resps.json(httpResponse, respBodyVo);
}
}
功能说明:
- 文件上传接口 (
/api/system/file/upload
):- CORS 支持:启用跨域资源共享(CORS)。
- 参数校验:检查请求中是否包含文件、存储桶名称和文件分类。
- 调用业务层:调用
SystemFileService
的upload
方法处理文件上传。 - 响应结果:返回文件上传结果的 JSON 数据。
- 获取文件 URL 接口 (
/api/system/file/url
):- CORS 支持:启用跨域资源共享(CORS)。
- 参数校验:根据请求参数
id
或md5
获取文件信息。 - 调用业务层:调用
SystemFileService
的getUrlById
或getUrlByMd5
方法获取文件的 URL。 - 响应结果:返回文件信息的 JSON 数据。
文件上传文档
1. 客户端生成文件的 MD5 值
在文件上传之前,客户端需要生成文件的 MD5 值。该值用于识别文件是否已经上传,以避免重复上传。
2. 检查文件是否已上传
通过以下 API 接口,客户端可以根据文件的 MD5 值查询文件是否已经存在于系统中:
- 请求方式:
GET
- URL:
http://localhost:8123/api/system/file/url?md5=76b503588b76c3236f5741a053f1e6bb&bucket=channel_001
示例响应:
{
"data": {
"id": "439681354499067904",
"md5": "76b503588b76c3236f5741a053f1e6bb",
"filename": "200-dpi.png",
"url": "http://localhost:8123/channel_001/images/439681354499067904.png"
},
"ok": true,
"msg": null,
"code": 1
}
- 说明: 如果文件已存在,响应会包含文件的
id
、url
、filename
等信息,客户端即可使用该文件的链接,无需重新上传。
3. 上传文件
如果文件不存在,客户端需执行上传操作,将文件发送至服务器。
- 请求方式:
POST
- URL:
http://localhost:8123/api/system/file/upload
请求参数:
--form 'file=@"C:\\Users\\Administrator\\Pictures\\200-dpi.png"' \
--form 'bucket="channel_001"' \
--form 'category="images"'
示例响应:
{
"id": "439679344561782784",
"url": "http://localhost:8123/channel_001/images/439679344561782784.png",
"filename": "200-dpi.png",
"md5": "76b503588b76c3236f5741a053f1e6bb"
}
- 说明: 成功上传后,系统返回该文件的
id
、url
、filename
及 MD5 值等信息。
4. 将文件信息入库
文件上传成功后,客户端应将以下信息存入数据库,以便于后续管理和查询:
id
: 文件的唯一标识符md5
: 文件的 MD5 值bucket
: 文件所在的存储分区(例如channel_001
)category
: 文件的分类(例如images
)filename
: 文件名
完整流程总结
- 生成文件 MD5 值 - 客户端生成文件的 MD5 值。
- 检查文件是否存在 - 调用检查接口,通过 MD5 值判断文件是否已上传。
- 上传文件 - 若文件不存在,则调用上传接口上传文件。
- 文件信息入库 - 上传成功后,客户端将文件的详细信息入库。
关键配置说明
server.resources.static-locations=pages
该配置项指定了服务器静态资源的存储位置为项目根目录下的 pages
文件夹。
作用与优势:
静态资源服务:通过配置
server.resources.static-locations=pages
,服务器能够自动将pages
目录下的文件作为静态资源进行服务。这样,存储在pages
目录中的文件(如图片、CSS、JavaScript 文件等)可以通过 URL 直接访问,无需额外的处理。组织结构清晰:将静态文件统一存储在
pages
目录下,有助于项目结构的清晰与管理。简化访问路径:用户可以通过简单的 URL 直接访问静态资源,例如
http://localhost:8123/channel_001/images/439679344561782784.png
。
注意事项:
- 确保
pages
目录具有适当的读写权限,服务器进程能够访问和修改该目录中的文件。 - 根据实际需求,可以调整
static-locations
的值,以适应不同的项目结构和部署环境。
Path path = Paths.get("pages", bucketName, category);
该代码行用于生成文件存储的路径,确保文件按照存储桶名称和分类进行组织。
作用与优势:
动态路径生成:通过
Paths.get("pages", bucketName, category)
,根据传入的bucketName
和category
动态生成文件存储路径。例如,对于bucketName
为channel_001
,category
为images
,生成的路径为pages/channel_001/images
。组织有序:将文件按照存储桶和分类进行组织,有助于文件的管理和检索。不同存储桶可以代表不同的业务模块或用户群体,分类则可以进一步细分文件类型或用途。
避免命名冲突:通过将文件存储在不同的目录下,可以有效避免文件名冲突。例如,不同存储桶下可以存在相同文件名的文件,而不会互相覆盖。
注意事项:
- 目录创建:在上传文件前,需确保目标目录存在。代码中已使用
Files.createDirectories(path)
创建必要的目录结构,如果目录已存在,该方法不会抛出异常。 - 路径安全:确保
bucketName
和category
的值经过验证,避免路径遍历等安全风险。
总结
通过以上步骤,您可以在 Tio-boot 应用中成功实现本地文件存储功能。这不仅包括文件的上传和存储,还涵盖了文件的查询和访问。关键配置如 server.resources.static-locations=pages
确保了静态资源的正确服务,而 Path path = Paths.get("pages", bucketName, category);
则提供了灵活且有序的文件存储路径生成机制。
建议:
- 安全性:确保文件上传功能具备必要的安全措施,如文件类型验证、大小限制、防止恶意文件上传等。
- 性能优化:对于大文件或高并发上传场景,考虑使用异步处理或分片上传技术,以提升系统性能和用户体验。
- 备份与恢复:定期备份存储的文件,确保在意外情况下能够快速恢复数据。
- 监控与日志:监控文件上传和访问的日志,及时发现和处理异常情况,保障系统的稳定运行。
通过合理的配置和优化,本地存储功能将为您的 Tio-boot 应用提供强大的文件管理支持,提升系统的整体功能性和用户体验。