diff --git a/docs/login-command-plan.md b/docs/login-command-plan.md new file mode 100644 index 0000000..3a8c452 --- /dev/null +++ b/docs/login-command-plan.md @@ -0,0 +1,89 @@ +# LionWebsite 一键登录指令方案(/login) + +状态:待用户确认,未实施。 +关联待办:`todos/open-td-20260921-a1b2c3-lionwebsite-login-tg.md`。 + +## 1. 现状 + +- 个人面板入口是 `https://personal.lionwebsite.xyz/index`(桌面)与 `/mobile`(移动)。 + nginx 把 `/` 代理到后端 `/personal/`,把 `/index`、`/mobile` 映射到静态入口文件。 +- 面板没有登录页。`PrivateMain` 与 `PrivateMainForMobile` 的 `src/store/index.js` + 把 `authCode: "alone"` 写死,所有请求都带 `?AuthCode=alone`。 +- 后端 `PersonalInterceptor` 对 `/personal/**`、`/remote/**` 直接比较字面量 `alone`; + `AdaptorFilter` 也用它决定移动端跳转。`/remote/**` 目前没有对应控制器,属历史遗留。 +- `alone` 不在 `User` 表里(表内是 big lion / pubraseer / temp / au283602 / 0619 / + liondown / bigcat)。它是独立的固定字面量:不轮换、不区分人,且随 JS 产物公开。 +- 存储节点另有两处写死 `alone`:`storageNode` 的 `CustomUtil.java`(向 `/message2me` + 推送)与 `MultiThreadedHTTPServer.java`(本机 HTTP 鉴权)。 +- 机器人(QQ `LionQQBot` + Telegram `PersonalHub`,主机 vm103)目前没有 `/login`。 + +## 2. 目标 + +1. 机器人在 QQ 与 Telegram 都能执行 `/login`,返回一条可点击的个人面板登录地址。 +2. 地址自带短期凭证,点开即登录,不需要手输授权码。 +3. 长期固定的 `alone` 不再出现在前端产物与聊天消息里。 +4. 凭证可过期、可吊销,登录行为在后端留痕。 + +## 3. 推荐方案:短期签名链接 + 会话 Cookie + +### 3.1 后端(`lionwebsite-backend`,主机 us9929) + +1. 新增登录端点。nginx 无需改动:`location /` 已把请求改写成 `/personal/...`, + 因此 `https://personal.lionwebsite.xyz/login?t=...` 会落到后端 `/personal/login`。 + - `GET /login?t=`:校验票据 → 下发 HttpOnly Cookie(`personal_session`, + `SameSite=Lax`、`Secure`,有效期建议 30 天)→ 302 跳 `/index`。 + - 票据格式建议 `v1.<签发时间戳>.`,有效期 5 分钟。 +2. `PersonalInterceptor` 的放行条件改为「有效会话 Cookie **或** 合法 `AuthCode`」。 + 过渡期保留 `AuthCode`,避免影响下载器前端与存储节点推送。 +3. `InterceptorConfiguration` 必须排除 `/personal/login`,否则登录端点会被自己拦住。 +4. 共享密钥通过 `application.yaml` 的环境变量覆盖位注入(如 `personal.login-secret`), + 密钥值不进仓库、不进日志。 +5. 可选 `GET /login/logout` 清除 Cookie。 + +选签名票据而不是「后端签发一次性 token」的理由:机器人与后端之间不需要新增网络调用 +和状态存储,双方共享一个密钥即可。代价是票据在 5 分钟窗口内可重放,对私聊场景可接受; +若要求真正一次性,改为后端 mint 端点 + 内存 token 表。 + +### 3.2 机器人(QQ + Telegram,主机 vm103) + +- QQ:在 LionQQBot 插件里新增 `/login`(别名 `/登录`),复用既有命令注册与主人校验。 +- Telegram:`sync_commands` 菜单增加 `/login`,`telegram.py` 派发到同名处理函数。 +- 处理函数:用共享密钥生成票据,回复 + `https://personal.lionwebsite.xyz/login?t=`,并附一句有效期提示。 +- 密钥存 PersonalHub 秘密库,共享盘只保留 `secret://` 引用。 +- 按现行约定,新增命令必须同时覆盖 QQ 与 Telegram,帮助菜单同步更新。 + +### 3.3 前端(`PrivateMain` 桌面 / `PrivateMainForMobile` 移动) + +- 删除 `authCode: "alone"` 常量与 `?AuthCode=` 查询串,改为依赖同源 Cookie + (axios 同源请求默认携带 Cookie)。 +- 未登录或被拒时显示提示页「登录已过期,请在机器人里发送 /login」,不要静默失败。 +- 下载器前端(`lionwebsite-frontend-desktop` / `-mobile`)使用每个用户自己的授权码, + 本次不动。 + +### 3.4 收尾:退役 `alone` + +- `PersonalInterceptor`、`AdaptorFilter` 不再比较字面量;移动端跳转改为原样透传查询串。 +- 存储节点两处 `alone` 换成配置项,属 storageNode 仓库,单独一次发布。 +- 轮换后确认无调用方仍依赖旧字面量。 + +## 4. 更小的备选 + +- 方案 B(最小改动):前端增加「从 URL 读取 `AuthCode` 并记住」的逻辑,机器人 `/login` + 直接回 `https://personal.lionwebsite.xyz/index?AuthCode=alone`。半天内可上线,但长期 + 密钥仍会进入聊天记录、浏览器历史与 nginx 日志,也没有解决 `alone` 写死的问题。 +- 方案 C(只治理配置):把 `alone` 从代码搬到配置并轮换,机器人从配置读取。安全提升有限。 + +## 5. 需要确认的三点 + +1. 采用推荐方案(Cookie 会话),还是先上备选 B? +2. 登录链接只允许主人使用,还是允许机器人给其他授权用户分别签发(对应 `User` 表账号)? +3. 会话有效期:建议 30 天滑动过期,可调整。 + +## 6. 实施顺序 + +1. 后端票据签发与 Cookie 校验、拦截器放行,含单元测试。 +2. 前端去掉 `alone`,补未登录提示。 +3. 机器人 QQ 与 Telegram 双向 `/login`。 +4. 存储节点两处 `alone` 改为配置。 +5. 退役字面量 `alone`。 diff --git a/llm_readme.txt b/llm_readme.txt index 9474372..57ef353 100644 --- a/llm_readme.txt +++ b/llm_readme.txt @@ -6,8 +6,8 @@ ────────────────────────────────────────────────────────────────────────────── 名称: LionWebsite 技术栈: Spring Boot 4.1.1, Java 21 字节码(生产 JDK 25), Maven, SQLite, MyBatis 4, Netty 4.2 -定位: 个人/私有 Web 应用,兼具 E-Hentai 画廊下载管理、个人文件服务、 - 代理订阅管理等功能。 +定位: 个人/私有 Web 应用,兼具 E-Hentai 画廊下载管理、代理订阅 + 管理等功能。 运行端口: 8888 数据库: 双 SQLite 数据库 — LionWebsite.db (主库) + cache.db (缓存库) 构建目标: 当前以 JVM/JAR 运行;保留 GraalVM Native Image 配置但尚未在 JDK 25 完成原生验证 @@ -29,8 +29,8 @@ src/main/java/com/lion/lionwebsite/ ├── Controller/ │ ├── GalleryManageController.java 画廊任务 CRUD、收藏、图片在线缓存 /GalleryManage │ ├── QueryController.java E-Hentai 搜索代理 /query -│ ├── PublicController.java 根路由、IP、订阅、文件分享、验证 /、/ip、/sub/、/GetFile/、/validate -│ ├── PersonalController.java 个人文件管理 /personal/ +│ ├── PublicController.java 根路由、IP、订阅、验证 /、/ip、/sub/、/validate +│ ├── PersonalController.java 个人管理 (订阅更新/最后更新时间/IP/留言) /personal/ │ ├── SubController.java 订阅绑定管理 /personal/subBind/ │ └── UserController.java 用户管理 /personal/user │ @@ -41,8 +41,8 @@ src/main/java/com/lion/lionwebsite/ │ ├── PushService.java Telegram Bot 通知 (admin 告警) │ ├── QueryService.java E-Hentai 搜索 + 缩略图代理缓存 (转 AVIF) │ ├── LocalServiceImpl.java 定时任务 (连接检测/额度重置/Cookie验证/订阅更新/缩略图清理) -│ ├── PublicServiceImpl.java IP 记录、分享码文件获取、授权码修改 -│ ├── PersonalServiceImpl.java 文件管理 (浏览/上传/下载/分享/压缩/删除/TAR打包) +│ ├── PublicServiceImpl.java IP 记录、授权码修改、身份查询 +│ ├── PersonalServiceImpl.java 订阅最后更新时间、家里 IP、留言转发 │ ├── SubService.java 代理订阅绑定/重置/查询/更新记录 │ ├── CollectService.java 画廊收藏/取消收藏 │ └── UserServiceImpl.java 用户 CRUD + 授权码管理 @@ -52,7 +52,6 @@ src/main/java/com/lion/lionwebsite/ │ │ ├── GalleryMapper.java gallery 表 CRUD │ │ ├── UserMapper.java user 表 CRUD │ │ ├── CollectMapper.java collect 表 CRUD -│ │ ├── ShareFileMapper.java ShareFile 表 CRUD │ │ ├── CustomConfigurationMapper.java 配置键值对读写 │ │ └── SubMapper.java 订阅绑定 & 更新记录 │ └── cache/ 缓存库 Mapper @@ -66,7 +65,6 @@ src/main/java/com/lion/lionwebsite/ │ ├── GidToKey.java 画廊 GID → MPV Key 映射 │ ├── ImageKeyCache.java 图片 key 缓存 (gid, page, imgkey) │ ├── CustomConfiguration.java 配置键常量定义 -│ ├── ShareFile.java 文件分享 (ShareCode, FilePath, ExpireTime) │ ├── SubBind.java 订阅绑定 (key, user) │ ├── SubUpdateRecord.java 订阅更新记录 (ip, UA, time, location) │ └── PageNameCache.java 页面名缓存 (gid, page, pageName) @@ -115,13 +113,11 @@ src/main/java/com/lion/lionwebsite/ - 支持图片在线预览: 缓存 MPV key → 按需下载单页 → 转为 AVIF 格式 - 画廊收藏/取消收藏 -2. 个人文件服务 - - 文件浏览器: 按路径浏览文件/文件夹,显示大小、分享状态 - - 上传: MultipartFile 上传到指定路径 - - 下载: 支持 HTTP Range 断点续传 - - 分享: 生成 8 位随机分享码,设置过期时间,可延长/取消 - - 压缩: 异步 TAR 打包目录 - - 删除文件/文件夹 +2. 个人管理服务 + - 手动触发订阅更新,返回最近更新时间与已记录的家庭 IP + - 管理员留言经 PushService 转发到 Telegram + + (文件浏览/上传/下载/分享/压缩/删除已在 td-20260921-a1b2c3 中整体下线) 3. E-Hentai 搜索代理 - 代理搜索 exhentai.org,返回格式化结果 (含缩略图 URL) @@ -141,7 +137,6 @@ src/main/java/com/lion/lionwebsite/ - 每 30 分钟: 检测存储节点连接,断开则自动重连 - 每周一 4:00: 重置每周下载额度 - 每天 0:00: 验证 E-Hentai Cookie 有效性 -- 每天 4:00: 清理过期分享码 - 每 24 小时: 更新代理订阅配置文件 - 每周一 4:00: 清理缩略图缓存 (保留最近 10000 个) @@ -151,13 +146,12 @@ src/main/java/com/lion/lionwebsite/ - /personal 和 /remote 路径限 AuthCode="alone" 用户 - HumanInterceptor 拒绝无 User-Agent 请求 - AdaptorFilter 记录所有请求日志 (IP/路径/UA/时间) -- 分享码失败黑名单 (连续失败则屏蔽) 六、依赖 ────────────────────────────────────────────────────────────────────────────── spring-boot-starter-webmvc, spring-boot-starter-websocket, mybatis-spring-boot-starter 4.1 jsoup (HTML 解析), hutool-all (工具集), sqlite-jdbc (数据库) -httpclient5 (HTTP 请求), commons-compress (TAR 打包) +httpclient5 (HTTP 请求) commons-io, commons-lang3, netty-all (TCP 通信) java-telegram-bot-api (Telegram Bot), graalvm native-maven-plugin (AOT) diff --git a/src/main/java/com/lion/lionwebsite/Configuration/CustomBean.java b/src/main/java/com/lion/lionwebsite/Configuration/CustomBean.java index ad85b30..c227574 100644 --- a/src/main/java/com/lion/lionwebsite/Configuration/CustomBean.java +++ b/src/main/java/com/lion/lionwebsite/Configuration/CustomBean.java @@ -22,7 +22,7 @@ import java.util.Random; @Configuration @RegisterReflectionForBinding(classes = {CustomConfiguration.class, GidToKey.class, ImageKeyCache.class, GalleryForQuery.class, Gallery.class, GalleryTask.class, HikariConfig.class, - PageNameCache.class, ShareFile.class, User.class, + PageNameCache.class, User.class, SendResponse.class, Message.class, com.pengrad.telegrambot.model.User.class, Chat.class, MessageEntity.class, AbstractMethodError.class, DeleteGalleryMessage.class, DownloadPostMessage.class, DownloadStatusMessage.class, diff --git a/src/main/java/com/lion/lionwebsite/Controller/PersonalController.java b/src/main/java/com/lion/lionwebsite/Controller/PersonalController.java index 0d977a4..930b216 100644 --- a/src/main/java/com/lion/lionwebsite/Controller/PersonalController.java +++ b/src/main/java/com/lion/lionwebsite/Controller/PersonalController.java @@ -3,7 +3,6 @@ package com.lion.lionwebsite.Controller; import com.lion.lionwebsite.Service.LocalService; import com.lion.lionwebsite.Service.PersonalService; import com.lion.lionwebsite.Util.Response; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -11,7 +10,6 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @@ -31,46 +29,6 @@ public class PersonalController { resp.sendRedirect("/index"); } - @GetMapping("/files") - public String file(String path){ - return personalService.getFiles(path); - } - - @PostMapping("/uploadFile") - public String uploadFile(String path, String fileName, MultipartFile file){ - return personalService.uploadFile(path, fileName, file); - } - - @GetMapping("/private/**") - public void getFile(HttpServletRequest request, HttpServletResponse response, String path){ - personalService.download(request, response, path); - } - - @PostMapping("/share") - public String shareFile(String path, Integer expireHour) { - return personalService.shareFile(path, expireHour); - } - - @PostMapping("/compress") - public String compress(String path){ - return personalService.compress(path); - } - - @PostMapping("/delete") - public String deleteFile(String path){ - return personalService.deleteFile(path); - } - - @PostMapping("/extendShareTime") - public String extendShareTime(String path, Integer extendHour) { - return personalService.extendShareTime(path, extendHour); - } - - @PostMapping("/cancelShare") - public String cancelShare(String path){ - return personalService.cancelShare(path); - } - @PostMapping("/updateSub") public String updateSub() throws IOException { Response response = Response.generateResponse(); diff --git a/src/main/java/com/lion/lionwebsite/Controller/PublicController.java b/src/main/java/com/lion/lionwebsite/Controller/PublicController.java index 0996edb..fd2b722 100644 --- a/src/main/java/com/lion/lionwebsite/Controller/PublicController.java +++ b/src/main/java/com/lion/lionwebsite/Controller/PublicController.java @@ -16,16 +16,12 @@ import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.LinkedList; -import java.util.List; @RestController @Slf4j @RequiredArgsConstructor public class PublicController { - final List black_share_codes = new LinkedList<>(); - final PublicService publicService; final RemoteService remoteService; @@ -60,24 +56,6 @@ public class PublicController { subService.updateSub(response, request, client, key); } - @GetMapping("/GetFile/{path}") - public void getFile(HttpServletRequest request, HttpServletResponse response, String ShareCode, @PathVariable("path") String path) throws IOException { - synchronized (black_share_codes) { - if (black_share_codes.contains(ShareCode)) - return; - } - - log.info("ShareCode:{}", ShareCode); - log.info("Path:{}", path); - boolean result = publicService.GetFile(request, response, ShareCode); - - if(!result) - black_share_codes.add(ShareCode); - - if(black_share_codes.size() > 100) - black_share_codes.removeFirst(); - } - @PostMapping("/validate") public String validate(String AuthCode){ Response response = Response.generateResponse(); diff --git a/src/main/java/com/lion/lionwebsite/Dao/normal/ShareFileMapper.java b/src/main/java/com/lion/lionwebsite/Dao/normal/ShareFileMapper.java deleted file mode 100644 index 34cc354..0000000 --- a/src/main/java/com/lion/lionwebsite/Dao/normal/ShareFileMapper.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.lion.lionwebsite.Dao.normal; - -import com.lion.lionwebsite.Domain.ShareFile; -import org.apache.ibatis.annotations.*; - -import java.util.ArrayList; -import java.util.Date; - -@Mapper -public interface ShareFileMapper { - @Insert("insert into ShareFile (ShareCode, FilePath, ExpireTime) values (#{ShareCode}, #{FilePath}, #{ExpireTime})") - void insertShareFile(@Param("ShareCode")String ShareCode, @Param("FilePath")String FilePath, @Param("ExpireTime") Date ExpireTime); - - @Select("select * from ShareFile where ShareCode=#{ShareCode}") - ShareFile selectShareFileByShareCode(String ShareCode); - - @Select("select * from ShareFile where FilePath=#{FilePath}") - ShareFile selectShareFileByFilePath(String FilePath); - - @Select("select * from ShareFile where FilePath like '%' || #{FilePath} || '%'") - ArrayList selectShareFilesByFilePath(String FilePath); - - @Delete("delete from ShareFile where ShareCode=#{ShareCode}") - void deleteShareFile(String ShareCode); - - @Select("select * from ShareFile") - ShareFile[] selectAllShareFile(); - - @Update("update ShareFile set ExpireTime=#{ExpireTime}, ShareCode=#{ShareCode} where FilePath=#{FilePath}") - void updateShareFile(ShareFile ShareFile); -} diff --git a/src/main/java/com/lion/lionwebsite/Domain/ShareFile.java b/src/main/java/com/lion/lionwebsite/Domain/ShareFile.java deleted file mode 100644 index 7408e0d..0000000 --- a/src/main/java/com/lion/lionwebsite/Domain/ShareFile.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.lion.lionwebsite.Domain; - -import lombok.Data; - -import java.util.Date; - -@Data -public class ShareFile { - String ShareCode; - String FilePath; - Date ExpireTime; -} diff --git a/src/main/java/com/lion/lionwebsite/Service/GalleryManageService.java b/src/main/java/com/lion/lionwebsite/Service/GalleryManageService.java index 314d017..98b329d 100644 --- a/src/main/java/com/lion/lionwebsite/Service/GalleryManageService.java +++ b/src/main/java/com/lion/lionwebsite/Service/GalleryManageService.java @@ -42,8 +42,6 @@ public class GalleryManageService { final UserMapper userMapper; - final ShareFileMapper shareFileMapper; - final ImageCacheMapper imageCacheMapper; final RemoteService remoteService; diff --git a/src/main/java/com/lion/lionwebsite/Service/LocalService.java b/src/main/java/com/lion/lionwebsite/Service/LocalService.java index b11cb35..0c00b9d 100644 --- a/src/main/java/com/lion/lionwebsite/Service/LocalService.java +++ b/src/main/java/com/lion/lionwebsite/Service/LocalService.java @@ -2,9 +2,7 @@ package com.lion.lionwebsite.Service; import com.lion.lionwebsite.Dao.normal.CustomConfigurationMapper; import com.lion.lionwebsite.Dao.normal.GalleryMapper; -import com.lion.lionwebsite.Dao.normal.ShareFileMapper; import com.lion.lionwebsite.Domain.CustomConfiguration; -import com.lion.lionwebsite.Domain.ShareFile; import com.lion.lionwebsite.Util.CustomUtil; import com.lion.lionwebsite.Util.GalleryUtil; import lombok.Data; @@ -45,8 +43,6 @@ public class LocalService{ final CustomConfigurationMapper configurationMapper; - final ShareFileMapper shareFileMapper; - final GalleryMapper galleryMapper; final PushService pushService; @@ -150,23 +146,6 @@ public class LocalService{ return temp; } - /** - * 每天四点,清理过期分享码 - */ - @Scheduled(cron = "0 0 4 * * *") - public void checkShareCode(){ - ShareFile[] shareFiles = shareFileMapper.selectAllShareFile(); - Calendar now; - Calendar expireTime; - for(ShareFile shareFile: shareFiles){ - now = Calendar.getInstance(); - expireTime = Calendar.getInstance(); - expireTime.setTime(shareFile.getExpireTime()); - if(now.after(expireTime)) - shareFileMapper.deleteShareFile(shareFile.getShareCode()); - } - } - /** * 每周,查看缩略图数量,按照访问时间排序,删除超出1w的部分 */ diff --git a/src/main/java/com/lion/lionwebsite/Service/PersonalService.java b/src/main/java/com/lion/lionwebsite/Service/PersonalService.java index afcd378..9690360 100644 --- a/src/main/java/com/lion/lionwebsite/Service/PersonalService.java +++ b/src/main/java/com/lion/lionwebsite/Service/PersonalService.java @@ -1,43 +1,17 @@ package com.lion.lionwebsite.Service; -import cn.hutool.core.io.FileUtil; -import cn.hutool.core.util.RandomUtil; import com.lion.lionwebsite.Dao.normal.CustomConfigurationMapper; -import com.lion.lionwebsite.Dao.normal.ShareFileMapper; -import com.lion.lionwebsite.Dao.normal.UserMapper; import com.lion.lionwebsite.Domain.CustomConfiguration; -import com.lion.lionwebsite.Domain.ShareFile; -import com.lion.lionwebsite.Interceptor.TaskHandlerInterceptor; -import com.lion.lionwebsite.Util.CustomUtil; -import com.lion.lionwebsite.Util.FileDownload; import com.lion.lionwebsite.Util.Response; import tools.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; -import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; -import org.apache.tomcat.util.http.fileupload.IOUtils; import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.nio.file.*; -import java.nio.file.attribute.BasicFileAttributes; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.HashMap; +import java.util.Map; @Service @@ -46,221 +20,8 @@ import java.util.concurrent.Executors; public class PersonalService{ final CustomConfigurationMapper configurationMapper; - final UserMapper userMapper; - - final ShareFileMapper shareFileMapper; - - final TaskHandlerInterceptor taskHandlerInterceptor; - - String StoragePath = "/storage/"; - - DateTimeFormatter dateTimeFormatter = CustomUtil.dateTimeFormatter(); - - ExecutorService compressThreadPool = Executors.newFixedThreadPool(1); - final PushService pushService; - /** - * 获取文件列表,同时带上分享码以及过期时间 - * @param path 路径 - * @return 文件列表 - */ - public String getFiles(String path) { - Response response = Response.generateResponse(); - path = URLDecoder.decode(path, StandardCharsets.UTF_8); - File root_file = new File(StoragePath + path); - Calendar now = Calendar.getInstance(); - Calendar expireTime = Calendar.getInstance(); - - //如果目标路径是文件夹 - if(root_file.isDirectory()) { - ArrayList> fileMaps = new ArrayList<>(); - File[] originalFiles = root_file.listFiles(); - - //如果文件夹里的文件不为空 - if (originalFiles != null) { - originalFiles = Arrays.stream(originalFiles).sorted(Comparator.comparing(File::getName)).toArray(File[]::new); - ArrayList shareFiles = shareFileMapper.selectShareFilesByFilePath(root_file.getAbsolutePath()); - - //遍历文件,放入文件信息以及查询对应的分享码 - for (File file : originalFiles) { - Map fileMap = new LinkedHashMap<>(); - fileMap.put("name", file.getName()); - fileMap.put("path", file.getAbsolutePath()); - - if (file.isDirectory()) { - fileMap.put("type", "FOLDER"); - } else if (file.isFile()) { - fileMap.put("type", "FILE"); //处理文件大小单位 - String fileSize = CustomUtil.fileSizeToString(file.length()); - fileMap.put("size", fileSize); - - Iterator iterator = shareFiles.iterator(); - while(iterator.hasNext()){ - ShareFile shareFile = iterator.next(); - if(shareFile.getFilePath().equals(file.getAbsolutePath())){ - expireTime.setTime(shareFile.getExpireTime()); - if(now.after(expireTime)){ - shareFileMapper.deleteShareFile(shareFile.getShareCode()); - } - else { - fileMap.put("shareCode", shareFile.getShareCode()); - fileMap.put("expireTime", dateTimeFormatter.format(shareFile.getExpireTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime())); - } - iterator.remove(); - break; - } - } - } - fileMaps.add(fileMap); - } - response.success(new ObjectMapper().valueToTree(fileMaps).toString()); - } - else - // listFiles() 只在 I/O 出错时返回 null(可读空目录返回长度为 0 的数组), - // 报「文件夹为空」会掩盖权限/磁盘问题,这里如实提示。 - response.failure("读取文件夹失败"); - } - return response.toJSONString(); - } - - /** - * 下载文件 - * @param request 请求对象 - * @param response 响应对象 - * @param path 目标路径 - */ - public void download(HttpServletRequest request, HttpServletResponse response, String path){ - path = URLDecoder.decode(path, StandardCharsets.UTF_8); - File file = new File(path); - if(file.exists()) - FileDownload.export(request, response, path); - - else - try{ - response.getWriter().print("404 NOT FOUND"); - }catch (IOException e){ - log.warn("输出404失败", e); - } - } - - /** - * 上传文件 - * @param path 目标路径 - * @param fileName 文件名称 - * @param file 文件对象 - * @return 上传结果 - */ - public String uploadFile(String path, String fileName, MultipartFile file) { - Response response = Response.generateResponse(); - log.info("上传文件:{}, 目标路径:{}", fileName, path); - - if(path == null || fileName == null || file == null){ - response.failure("参数不完整"); - return response.toJSONString(); - } - - File directory = new File(StoragePath + path); - if(directory.isDirectory()){ - File targetFile = new File(StoragePath + path, fileName); - - if(targetFile.exists()) - response.failure("目标文件已存在"); - else - try { - file.transferTo(Path.of(StoragePath + path, fileName)); - response.success("上传成功"); - } catch (IOException e) { - response.failure("上传失败"); - log.error("上传失败: {}", fileName, e); - } - } - else - response.failure("该路径不存在或者不是文件夹"); - - return response.toJSONString(); - } - - /** - * 创建分享码 - * @param path 目标路径 - * @param expireHour 过期时间 - * @return 如果成功则是分享码以及过期时间,失败则是失败原因 - */ - public String shareFile(String path, Integer expireHour) { - Response response = Response.generateResponse(); - path = URLDecoder.decode(path, StandardCharsets.UTF_8); - Map jsonObject = new HashMap<>(); - File file = new File(path); - if(file.isFile()){ //如果是文件,则生成分享码,调用此接口时不需要考虑分享已分享文件以及分享已过期文件 - String ShareCode; - ShareCode = RandomUtil.randomString(8); - Calendar expireTime = Calendar.getInstance(); - - expireTime.add(Calendar.HOUR, expireHour); - shareFileMapper.insertShareFile(ShareCode, path, expireTime.getTime()); - jsonObject.put("shareCode", ShareCode); - jsonObject.put("expireTime", dateTimeFormatter.format(expireTime.getTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime())); - response.success(new ObjectMapper().valueToTree(jsonObject).toString()); - } - else - response.failure("此路径为文件夹或不存在"); - - return response.toJSONString(); - } - - /** - * 延长分享时间 - * @param path 目标文件路径 - * @param extendHour 延长小时数 - * @return 延长结果 - */ - public String extendShareTime(String path, Integer extendHour) { - Response response = Response.generateResponse(); - path = URLDecoder.decode(path, StandardCharsets.UTF_8); - Map data = new LinkedHashMap<>(); - ShareFile shareFile = shareFileMapper.selectShareFileByFilePath(path); - - if(shareFile != null){ - Calendar calendar = Calendar.getInstance(); - calendar.setTime(shareFile.getExpireTime()); - - calendar.add(Calendar.HOUR, extendHour); - shareFile.setExpireTime(calendar.getTime()); - shareFileMapper.updateShareFile(shareFile); - data.put("expireTime", dateTimeFormatter.format(shareFile.getExpireTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime())); - data.put("path", shareFile.getFilePath()); - - response.success(new ObjectMapper().valueToTree(data).toString()); - } - else{ - response.failure("该文件未被分享"); - } - - return response.toJSONString(); - } - - /** - * 取消分享 - * @param path 目标路径 - * @return 取消结果 - */ - public String cancelShare(String path) { - Response response = Response.generateResponse(); - path = URLDecoder.decode(path, StandardCharsets.UTF_8); - ShareFile shareFile = shareFileMapper.selectShareFileByFilePath(path); - - if(shareFile != null){ - shareFileMapper.deleteShareFile(shareFile.getShareCode()); - response.success("取消分享成功"); - } - else{ - response.failure("该文件未被分享"); - } - - return response.toJSONString(); - } - /** * 获取订阅文件上次更新时间 * @return 订阅文件上次更新时间 @@ -289,90 +50,6 @@ public class PersonalService{ return response.toJSONString(); } - /** - * 打包文件 - * @param path 目标路径 - * @return 响应提交成功,因为该方法为异步执行,未完成时后辍为undone - */ - public String compress(String path) { - Response response = Response.generateResponse(); - path = URLDecoder.decode(path, StandardCharsets.UTF_8); - File file = new File(path); - String finalPath = path; - - if(!file.isDirectory() || !file.exists()){ - response.failure("选中的路径不是文件夹"); - return response.toJSONString(); - } - - compressThreadPool.submit(() -> { - Path temporary = Paths.get(finalPath + ".tar***undone"); - try { - writeTar(Paths.get(finalPath), temporary); - // writeTar closes the archive (including its trailer) before publication. - try { - Files.move(temporary, Paths.get(finalPath + ".tar"), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); - } catch (AtomicMoveNotSupportedException e) { - Files.move(temporary, Paths.get(finalPath + ".tar"), StandardCopyOption.REPLACE_EXISTING); - } - log.info("打包成功: {}.tar", finalPath); - } catch (IOException e) { - log.error("打包失败", e); - } finally { - try { Files.deleteIfExists(temporary); } - catch (IOException e) { log.warn("清理打包临时文件失败", e); } - } - }); - response.success("加入队列成功"); - return response.toJSONString(); - } - - static void writeTar(Path directory, Path output) throws IOException { - try (OutputStream stream = new BufferedOutputStream(Files.newOutputStream(output)); - TarArchiveOutputStream archive = new TarArchiveOutputStream(stream)) { - archive.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU); - Files.walkFileTree(directory, new SimpleFileVisitor<>() { - @Override public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) throws IOException { - if (!path.equals(directory)) { - archive.putArchiveEntry(new TarArchiveEntry(path.toFile(), directory.relativize(path).toString())); - archive.closeArchiveEntry(); - } - return FileVisitResult.CONTINUE; - } - @Override public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { - archive.putArchiveEntry(new TarArchiveEntry(path.toFile(), directory.relativize(path).toString())); - try (InputStream input = Files.newInputStream(path)) { - IOUtils.copy(input, archive); - } - archive.closeArchiveEntry(); - return FileVisitResult.CONTINUE; - } - }); - } - } - - /** - * 删除文件 - * @param path 目标路径 - * @return 删除结果 - */ - public String deleteFile(String path) { - Response response = Response.generateResponse(); - path = URLDecoder.decode(path, StandardCharsets.UTF_8); - File file = new File(path); - - // hutool 的 FileUtil.del 对不存在的目标返回 true(幂等语义), - // 直接据此报成功会让「路径写错/文件已不存在」也回「删除成功」,前端误以为已删除。 - if (!file.exists()) - response.failure("文件不存在"); - else if (FileUtil.del(file)) - response.success("删除成功"); - else - response.failure("删除失败"); - - return response.toJSONString(); - } - public String message2me(String message){ Response response = Response.generateResponse(); pushService.sendToMe(message); diff --git a/src/main/java/com/lion/lionwebsite/Service/PublicService.java b/src/main/java/com/lion/lionwebsite/Service/PublicService.java index 508a09a..eb561e1 100644 --- a/src/main/java/com/lion/lionwebsite/Service/PublicService.java +++ b/src/main/java/com/lion/lionwebsite/Service/PublicService.java @@ -2,35 +2,23 @@ package com.lion.lionwebsite.Service; import com.lion.lionwebsite.Dao.normal.CustomConfigurationMapper; -import com.lion.lionwebsite.Dao.normal.ShareFileMapper; import com.lion.lionwebsite.Dao.normal.UserMapper; import com.lion.lionwebsite.Domain.CustomConfiguration; -import com.lion.lionwebsite.Domain.ShareFile; import com.lion.lionwebsite.Domain.User; import com.lion.lionwebsite.Interceptor.TaskHandlerInterceptor; import com.lion.lionwebsite.Util.CustomUtil; -import com.lion.lionwebsite.Util.FileDownload; import com.lion.lionwebsite.Util.Response; import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Calendar; - @Service @RequiredArgsConstructor public class PublicService { final CustomConfigurationMapper configurationMapper; - final ShareFileMapper shareFileMapper; - final UserMapper userMapper; final TaskHandlerInterceptor taskHandlerInterceptor; @@ -44,41 +32,6 @@ public class PublicService { configurationMapper.updateConfiguration(CustomConfiguration.LAST_UPDATE_IP_ADDRESS_TIME, CustomUtil.now()); } - /** - * 通过分享码获取文件 - * @param httpRequest 请求对象 - * @param httpResponse 响应对象 - * @param ShareCode 分享码 - * @throws IOException 响应时的异常 - */ - public boolean GetFile(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String ShareCode) throws IOException { - Response response = Response.generateResponse(); - - //参数为空的情况 - if(ShareCode == null) { - response.failure("ShareCode invalid"); - } else { - ShareFile shareFile = shareFileMapper.selectShareFileByShareCode(ShareCode); - Calendar ExpireTime = Calendar.getInstance(); - Calendar now = Calendar.getInstance(); - - if (shareFile != null) { - ExpireTime.setTime(shareFile.getExpireTime()); - if (ExpireTime.after(now) && new File(shareFile.getFilePath()).isFile()) { - FileDownload.export(httpRequest, httpResponse, shareFile.getFilePath()); - return true; - } else { - shareFileMapper.deleteShareFile(shareFile.getShareCode()); - response.failure("ShareCode is expired or File is not exist"); - } - } else - response.failure("ShareCode is not exist or expired"); - } - - httpResponse.getOutputStream().write(response.toJSONString().getBytes(StandardCharsets.UTF_8)); - return false; - } - /** * 修改授权码,如果能够执行此方法,则授权码一定存在 * @param AuthCode 原来的授权码 diff --git a/src/test/java/com/lion/lionwebsite/Controller/PersonalControllerTest.java b/src/test/java/com/lion/lionwebsite/Controller/PersonalControllerTest.java index b516e7e..1135025 100644 --- a/src/test/java/com/lion/lionwebsite/Controller/PersonalControllerTest.java +++ b/src/test/java/com/lion/lionwebsite/Controller/PersonalControllerTest.java @@ -2,24 +2,20 @@ package com.lion.lionwebsite.Controller; import com.lion.lionwebsite.Service.LocalService; import com.lion.lionwebsite.Service.PersonalService; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** * /personal 的 HTTP 契约。 - * 这些接口能做到浏览/上传/删除文件,权限由 PersonalInterceptor 另外把关 - * (见 PersonalInterceptorTest),此处只验证参数如何传到服务层。 + * 文件管理与分享接口下线后,这里只剩订阅更新时间、家里 IP 与留言转发; + * 权限由 PersonalInterceptor 另外把关(见 PersonalInterceptorTest)。 */ class PersonalControllerTest { @@ -43,57 +39,8 @@ class PersonalControllerTest { .andExpect(redirectedUrl("/index")); } - @Test - void fileListPassesPathThrough() throws Exception { - when(personalService.getFiles("/docs")).thenReturn("{\"result\":\"success\"}"); - - mockMvc.perform(get("/personal/files").param("path", "/docs")) - .andExpect(status().isOk()); - - verify(personalService).getFiles("/docs"); - } - - /** 上传要同时带上目标路径、文件名与文件体。 */ - @Test - void uploadPassesPathFileNameAndContent() throws Exception { - var file = new MockMultipartFile("file", "a.txt", "text/plain", "hi".getBytes()); - - mockMvc.perform(multipart("/personal/uploadFile") - .file(file) - .param("path", "/docs") - .param("fileName", "a.txt")) - .andExpect(status().isOk()); - - verify(personalService).uploadFile(eq("/docs"), eq("a.txt"), any()); - } - - /** 下载走通配路径,path 参数与 request/response 都要透传。 */ - @Test - void downloadForwardsWildcardPath() throws Exception { - mockMvc.perform(get("/personal/private/deep/nested/file.txt").param("path", "/deep/nested/file.txt")) - .andExpect(status().isOk()); - - verify(personalService).download(any(HttpServletRequest.class), any(HttpServletResponse.class), - eq("/deep/nested/file.txt")); - } - @Test void simpleOperationsDelegateWithTheirParameters() throws Exception { - mockMvc.perform(post("/personal/share").param("path", "/a.txt").param("expireHour", "24")); - verify(personalService).shareFile("/a.txt", 24); - - mockMvc.perform(post("/personal/compress").param("path", "/dir")); - verify(personalService).compress("/dir"); - - mockMvc.perform(post("/personal/delete").param("path", "/a.txt")); - verify(personalService).deleteFile("/a.txt"); - - mockMvc.perform(post("/personal/extendShareTime").param("path", "/a.txt").param("extendHour", "2")); - verify(personalService).extendShareTime("/a.txt", 2); - - mockMvc.perform(post("/personal/cancelShare").param("path", "/a.txt")); - verify(personalService).cancelShare("/a.txt"); - mockMvc.perform(get("/personal/lastUpdate")); verify(personalService).lastUpdate(); diff --git a/src/test/java/com/lion/lionwebsite/Controller/PublicControllerTest.java b/src/test/java/com/lion/lionwebsite/Controller/PublicControllerTest.java index 077ec2f..7df503a 100644 --- a/src/test/java/com/lion/lionwebsite/Controller/PublicControllerTest.java +++ b/src/test/java/com/lion/lionwebsite/Controller/PublicControllerTest.java @@ -151,59 +151,4 @@ class PublicControllerTest { any(HttpServletRequest.class), any(HttpServletResponse.class)); } - // ---------- GetFile 与失败分享码黑名单 ---------- - - @Test - void getFileForwardsToService() throws Exception { - when(publicService.GetFile(any(), any(), eq("goodcode"))).thenReturn(true); - - mockMvc.perform(get("/GetFile/a.txt").param("ShareCode", "goodcode")) - .andExpect(status().isOk()); - - verify(publicService).GetFile(any(), any(), eq("goodcode")); - } - - /** - * 失败的分享码会被加入黑名单,后续相同请求直接短路返回, - * 不再反复查库/探测文件(防爆破)。这是该接口唯一的限流手段。 - */ - @Test - void getFileBlacklistsFailingShareCode() throws Exception { - when(publicService.GetFile(any(), any(), eq("badcode"))).thenReturn(false); - - mockMvc.perform(get("/GetFile/a.txt").param("ShareCode", "badcode")); - mockMvc.perform(get("/GetFile/a.txt").param("ShareCode", "badcode")); - mockMvc.perform(get("/GetFile/a.txt").param("ShareCode", "badcode")); - - verify(publicService, times(1)).GetFile(any(), any(), eq("badcode")); - } - - /** 不同分享码各自独立计数,一个坏码不应影响好码。 */ - @Test - void blacklistIsPerShareCode() throws Exception { - when(publicService.GetFile(any(), any(), eq("bad-1"))).thenReturn(false); - when(publicService.GetFile(any(), any(), eq("good-2"))).thenReturn(true); - - mockMvc.perform(get("/GetFile/a.txt").param("ShareCode", "bad-1")); - mockMvc.perform(get("/GetFile/a.txt").param("ShareCode", "good-2")); - - verify(publicService).GetFile(any(), any(), eq("bad-1")); - verify(publicService).GetFile(any(), any(), eq("good-2")); - } - - /** 黑名单上限 100:超过后最旧的条目被淘汰,不会无界增长。 */ - @Test - void blacklistIsCappedAtHundredEntries() throws Exception { - when(publicService.GetFile(any(), any(), anyString())).thenReturn(false); - - for (int i = 0; i < 105; i++) - mockMvc.perform(get("/GetFile/a.txt").param("ShareCode", "code-" + i)); - - // 105 个不同坏码各触发一次服务调用;最早那批已被挤出,容量保持有界 - verify(publicService, times(105)).GetFile(any(), any(), anyString()); - - // 已被淘汰的 code-0 再次请求会重新走一次服务层(说明它确实被移出了黑名单) - mockMvc.perform(get("/GetFile/a.txt").param("ShareCode", "code-0")); - verify(publicService, times(106)).GetFile(any(), any(), anyString()); - } } diff --git a/src/test/java/com/lion/lionwebsite/Service/GalleryManageServiceTest.java b/src/test/java/com/lion/lionwebsite/Service/GalleryManageServiceTest.java index 6b182da..ad8321d 100644 --- a/src/test/java/com/lion/lionwebsite/Service/GalleryManageServiceTest.java +++ b/src/test/java/com/lion/lionwebsite/Service/GalleryManageServiceTest.java @@ -35,7 +35,7 @@ class GalleryManageServiceTest { remote = mock(RemoteService.class); push = mock(PushService.class); service = new GalleryManageService(galleries, collectMapper, - configurationMapper, users, mock(ShareFileMapper.class), + configurationMapper, users, mock(ImageCacheMapper.class), remote, push); User user = new User(); @@ -59,7 +59,7 @@ class GalleryManageServiceTest { ImageCacheMapper imageCacheMapper = mock(ImageCacheMapper.class); when(imageCacheMapper.selectKeyByGid(anyString())).thenReturn(null); // 缓存未命中 GalleryManageService svc = new GalleryManageService(galleries, collectMapper, - configurationMapper, users, mock(ShareFileMapper.class), + configurationMapper, users, imageCacheMapper, remote, push); // imagelist 行以分号结尾——即线上真实页面格式 @@ -94,7 +94,7 @@ class GalleryManageServiceTest { ImageCacheMapper imageCacheMapper = mock(ImageCacheMapper.class); when(imageCacheMapper.selectKeyByGid(anyString())).thenReturn(null); GalleryManageService svc = new GalleryManageService(galleries, collectMapper, - configurationMapper, users, mock(ShareFileMapper.class), + configurationMapper, users, imageCacheMapper, remote, push); String brokenPage = "