From fcd50a25699a74dc5b34bd840d507d002bd7d8a9 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Mon, 15 Sep 2025 18:08:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:oplist=E8=8E=B7=E5=8F=96token=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 8 ++ src/core/openlist/openlist.service.ts | 144 ++++++++++++++++++++++++- src/core/openlist/openlist.utils.ts | 150 +++++++++++++++++++++----- 4 files changed, 277 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 09a7d1a..c8e5f2a 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@nestjs/websockets": "^11.1.6", "axios": "1.11.0", "ioredis": "^5.6.1", + "moment": "^2.30.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "simple-git": "^3.28.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c50913..83ebc11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: ioredis: specifier: ^5.6.1 version: 5.6.1 + moment: + specifier: ^2.30.1 + version: 2.30.1 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -2780,6 +2783,9 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -6743,6 +6749,8 @@ snapshots: dependencies: minimist: 1.2.8 + moment@2.30.1: {} + ms@2.1.3: {} multer@2.0.2: diff --git a/src/core/openlist/openlist.service.ts b/src/core/openlist/openlist.service.ts index 61da63e..20a2659 100644 --- a/src/core/openlist/openlist.service.ts +++ b/src/core/openlist/openlist.service.ts @@ -2,15 +2,157 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { AppConfigService } from '../../config/config.service'; import { DirectoryList, FileInfo, UserInfo } from './openlist.types'; import { OpenListUtils } from './openlist.utils'; +import * as moment from 'moment'; @Injectable() export class OpenListService { private readonly logger = new Logger(OpenListService.name); + private token: string | undefined; + private tokenExpireTime: moment.Moment | undefined; constructor( @Inject(AppConfigService) private readonly configService: AppConfigService, ) { - OpenListUtils.init(configService); + this.initialize().then(); + } + + /** + * 服务初始化 + */ + private async initialize() { + const apiBaseUrl = this.configService.get('OPENLIST_API_BASE_URL'); + const username = this.configService.get('OPENLIST_API_BASE_USERNAME'); + const password = this.configService.get('OPENLIST_API_BASE_PASSWORD'); + + OpenListUtils.init(this.configService); + if (username && password) { + this.token = await this.fetchToken(username, password); + this.tokenExpireTime = moment().add(48, 'hours'); + this.logger.log(`OpenList服务初始化成功: ${apiBaseUrl}`); + } else { + this.logger.error( + `OpenList服务初始化失败,请检查是否填写.env处的用户名和密码..`, + ); + } + } + + /** + * 获取OpenList的JWT Token,如果Token已过期,则重新获取 + * @param username 用户名 + * @param password 密码 + * @returns JWT Token + */ + private async fetchToken( + username: string, + password: string, + ): Promise { + if ( + this.token && + this.tokenExpireTime && + moment().isBefore(this.tokenExpireTime) + ) { + return this.token; + } + try { + const newToken = await OpenListUtils.getToken(username, password); + this.tokenExpireTime = moment().add(48, 'hours'); //过期时间 + return newToken; + } catch (error) { + this.logger.error('获取Token失败:', error); + throw new Error('获取Token失败'); + } + } + + /** + * 获取当前用户信息 + * @returns 用户信息 + */ + public async getUserInfo(): Promise { + try { + const token = await this.fetchToken( + this.configService.get('OPENLIST_API_BASE_USERNAME'), + this.configService.get('OPENLIST_API_BASE_PASSWORD'), + ); + return await OpenListUtils.getUserInfo(token); + } catch (error) { + this.logger.error('获取用户信息失败:', error); + throw new Error('获取用户信息失败'); + } + } + + /** + * 列出目录下的所有文件 + * @param path 目录路径 + * @returns 目录下的文件列表 + */ + public async listFiles(path: string): Promise { + try { + const token = await this.fetchToken( + this.configService.get('OPENLIST_API_BASE_USERNAME'), + this.configService.get('OPENLIST_API_BASE_PASSWORD'), + ); + return await OpenListUtils.listDirectory(token, path); + } catch (error) { + this.logger.error('列出目录失败:', error); + throw new Error('列出目录失败'); + } + } + + /** + * 获取文件信息 + * @param filePath 文件路径 + * @returns 文件信息 + */ + public async getFileInfo(filePath: string): Promise { + try { + const token = await this.fetchToken( + this.configService.get('OPENLIST_API_BASE_USERNAME'), + this.configService.get('OPENLIST_API_BASE_PASSWORD'), + ); + return await OpenListUtils.getFileInfo(token, filePath); + } catch (error) { + this.logger.error('获取文件信息失败:', error); + throw new Error('获取文件信息失败'); + } + } + + /** + * 下载文件 + * @param filePath 文件路径 + * @param downloadPath 本地下载路径 + */ + public async downloadFile( + filePath: string, + downloadPath: string, + ): Promise { + try { + const token = await this.fetchToken( + this.configService.get('OPENLIST_API_BASE_USERNAME'), + this.configService.get('OPENLIST_API_BASE_PASSWORD'), + ); + await OpenListUtils.downloadFile(token, filePath, downloadPath); + } catch (error) { + this.logger.error('下载文件失败:', error); + throw new Error('下载文件失败'); + } + } + + /** + * 上传文件 + * @param filePath 上传路径 + * @param file 文件 + */ + public async uploadFile(filePath: string, file: any): Promise { + try { + const token = await this.fetchToken( + this.configService.get('OPENLIST_API_BASE_USERNAME'), + this.configService.get('OPENLIST_API_BASE_PASSWORD'), + ); + await OpenListUtils.uploadFile(token, filePath, file); + } catch (error) { + this.logger.error('上传文件失败:', error); + throw new Error('上传文件失败'); + } } } diff --git a/src/core/openlist/openlist.utils.ts b/src/core/openlist/openlist.utils.ts index a429a48..4a69977 100644 --- a/src/core/openlist/openlist.utils.ts +++ b/src/core/openlist/openlist.utils.ts @@ -2,6 +2,7 @@ import axios from 'axios'; import { AppConfigService } from '../../config/config.service'; import { Inject, Logger } from '@nestjs/common'; import * as crypto from 'crypto'; +import * as fs from 'fs'; export class OpenListUtils { private static readonly logger = new Logger(OpenListUtils.name); @@ -9,6 +10,7 @@ export class OpenListUtils { static init(@Inject(AppConfigService) configService: AppConfigService) { this.apiBaseUrl = configService.get('OPENLIST_API_BASE_URL'); + this.logger.log('OpenListUtils初始化..'); } /** @@ -18,20 +20,25 @@ export class OpenListUtils { * @returns token */ static async getToken(username: string, password: string): Promise { - const url = `${this.apiBaseUrl}/auth/token`; - const hashedPassword = this.hashPassword(password); + const url = `${this.apiBaseUrl}/api/auth/login`; try { const response = await axios.post(url, { - username, - password: hashedPassword, + username: username, + password: password, }); - const token = response.data.token; - this.logger.log(`获取 Token 成功: ${token}`); - return token; + this.logger.debug(response); + if (response.data.data.token) { + const token: string = response.data.data.token; + this.logger.log(`获取Token成功: ${token}`); + return token; + } else { + this.logger.error(`获取Token失败: ${response.data.data.message}`); + return 'null'; + } } catch (error) { - this.logger.error('获取 Token 失败', error); - throw new Error('获取 Token 失败'); + this.logger.error('获取Token失败..', error); + throw new Error('获取Token失败..'); } } @@ -47,11 +54,11 @@ export class OpenListUtils { const response = await axios.get(url, { headers: { Authorization: `Bearer ${token}` }, }); - this.logger.log('获取用户信息成功'); + this.logger.log('获取用户信息成功..'); return response.data; } catch (error) { - this.logger.error('获取用户信息失败', error); - throw new Error('获取用户信息失败'); + this.logger.error('获取用户信息失败..', error); + throw new Error('获取用户信息失败..'); } } @@ -69,11 +76,11 @@ export class OpenListUtils { params: { path }, headers: { Authorization: `Bearer ${token}` }, }); - this.logger.log('列出目录成功'); + this.logger.log('列出目录成功..'); return response.data; } catch (error) { - this.logger.error('列出目录失败', error); - throw new Error('列出目录失败'); + this.logger.error('列出目录失败..', error); + throw new Error('列出目录失败..'); } } @@ -91,11 +98,11 @@ export class OpenListUtils { params: { path: filePath }, headers: { Authorization: `Bearer ${token}` }, }); - this.logger.log('获取文件信息成功'); + this.logger.log('获取文件信息成功..'); return response.data; } catch (error) { - this.logger.error('获取文件信息失败', error); - throw new Error('获取文件信息失败'); + this.logger.error('获取文件信息失败..', error); + throw new Error('获取文件信息失败..'); } } @@ -124,17 +131,110 @@ export class OpenListUtils { this.logger.log(`文件重命名成功: ${oldPath} => ${newPath}`); return response.data; } catch (error) { - this.logger.error('文件重命名失败', error); - throw new Error('文件重命名失败'); + this.logger.error('文件重命名失败..', error); + throw new Error('文件重命名失败..'); } } /** - * 为密码生成 sha256 hash - * @param password 密码 - * @returns hashed 密码 + * 下载文件 + * @param token 用户 Token + * @param filePath 文件路径 + * @param downloadPath 本地下载路径 */ - private static hashPassword(password: string): string { - return crypto.createHash('sha256').update(password).digest('hex'); + static async downloadFile( + token: string, + filePath: string, + downloadPath: string, + ): Promise { + const url = `${this.apiBaseUrl}/fs/download`; + + try { + const response = await axios.get(url, { + params: { path: filePath }, + headers: { Authorization: `Bearer ${token}` }, + responseType: 'stream', + }); + const writer = fs.createWriteStream(downloadPath); + response.data.pipe(writer); + + writer.on('finish', () => { + this.logger.log(`文件下载成功: ${downloadPath}`); + }); + + writer.on('error', (error) => { + this.logger.error('下载文件失败', error); + throw new Error('下载文件失败'); + }); + } catch (error) { + this.logger.error('下载文件失败..', error); + throw new Error('下载文件失败..'); + } + } + + /** + * 上传文件 + * @param token 用户 Token + * @param filePath 上传文件的路径 + * @param file 上传文件的内容 + * @returns 上传结果 + */ + static async uploadFile( + token: string, + filePath: string, + file: any, + ): Promise { + const url = `${this.apiBaseUrl}/fs/upload`; + + const formData = new FormData(); + formData.append('file', file); + formData.append('path', filePath); + + try { + const response = await axios.post(url, formData, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'multipart/form-data', + }, + }); + + this.logger.log(`文件上传成功: ${filePath}`); + return response.data; + } catch (error) { + this.logger.error('上传文件失败..', error); + throw new Error('上传文件失败..'); + } + } + + /** + * 获取某个目录下的所有文件 + * @param token 用户 Token + * @param directoryPath 目录路径 + * @returns 文件列表 + */ + static async listFilesInDirectory( + token: string, + directoryPath: string, + ): Promise { + const directoryInfo = await this.listDirectory(token, directoryPath); + return directoryInfo.filter( + (item: { is_directory: any }) => !item.is_directory, + ); + } + + /** + * 检查文件更新 + * @param token 用户 Token + * @param filePath 文件路径 + * @param lastModified 上次改动时间 + * @returns 文件是否更新 + */ + static async checkFileUpdate( + token: string, + filePath: string, + lastModified: string, + ): Promise { + const fileInfo = await this.getFileInfo(token, filePath); + return fileInfo.modified_at !== lastModified; } }