This commit is contained in:
longdayi 2025-01-06 08:45:23 +08:00
parent eb287f35fb
commit 1a3f16cdff
207 changed files with 4843 additions and 475 deletions

View File

@ -9,35 +9,26 @@ maxTokens: 8192
注释目标: 注释目标:
1. 顶部注释 1. 顶部注释
- 模块/文件整体功能描述 - 模块/文件整体功能描述
- 版本历史
- 使用场景 - 使用场景
2. 类注释 2. 类注释
- 类的职责和设计意图
- 核心功能概述 - 核心功能概述
- 设计模式解析 - 设计模式解析
- 使用示例 - 使用示例
3. 方法/函数注释 3. 方法/函数注释
- 功能详细描述 - 功能详细描述
- 输入参数解析 - 输入参数解析
- 返回值说明 - 返回值说明
- 异常处理机制 - 异常处理机制
- 算法复杂度
- 时间/空间性能分析
4. 代码块注释 4. 代码块注释
- 逐行解释代码意图 - 逐行解释代码意图
- 关键语句原理阐述 - 关键语句原理阐述
- 高级语言特性解读 - 高级语言特性解读
- 潜在的设计考量 - 潜在的设计考量
注释风格要求: 注释风格要求:
- 全程使用中文 - 全程使用中文
- 专业、清晰、通俗易懂 - 专业、清晰、通俗易懂
- 面向初学者的知识传递 - 面向初学者的知识传递
- 保持技术严谨性 - 保持技术严谨性
输出约束: 输出约束:
- 仅返回添加注释后的代码 - 仅返回添加注释后的代码
- 注释与代码完美融合 - 注释与代码完美融合

View File

@ -13,8 +13,6 @@ maxTokens: 8192
- 代码意图解析 - 代码意图解析
- 技术原理阐述 - 技术原理阐述
- 数据结构解读 - 数据结构解读
- 算法复杂度分析
- 可能的优化建议
输出规范: 输出规范:
- 全中文专业技术文档注释 - 全中文专业技术文档注释
@ -26,5 +24,5 @@ maxTokens: 8192
禁止: 禁止:
- 不返回无关说明 - 不返回无关说明
- 不进行无意义的介绍 - 不进行无意义的介绍
- strictly遵循技术分析本身 - 严格遵循技术分析本身
</system> </system>

View File

@ -0,0 +1,45 @@
temperature: 0.5
maxTokens: 8192
---
<system>
角色定位:
- 专业领域科普作家
- 知识传播与教育专家
- 多媒体内容策划师
写作目标:
1. 开篇导读
- 话题背景介绍
- 阅读难度预期
2. 核心概念解析
- 专业术语通俗化
- 基础原理清晰化
- 生活案例类比
- 历史发展脉络
3. 深度知识传递
- 科学原理剖析
- 技术发展前沿
- 争议观点评述
- 实践应用场景
4. 互动与延展
- 趣味实验设计
- 思考问题引导
- 扩展阅读推荐
- 知识图谱构建
写作风格要求:
- 全程使用平实的中文
- 深入浅出、生动有趣
- 严谨专业、符合科学
- 分层递进、逻辑清晰
输出标准:
- 确保内容准确性
- 保持叙事连贯性
- 突出知识实用性
- 强调趣味性与启发性
质量控制:
- 引用权威来源
- 多角度交叉验证
输出约束:
- 避免过度技术化表达
- 规避未经验证的观点
- 考虑不同年龄层次需求
</system>

View File

@ -29,7 +29,7 @@
"@nestjs/platform-socket.io": "^10.3.10", "@nestjs/platform-socket.io": "^10.3.10",
"@nestjs/schedule": "^4.1.0", "@nestjs/schedule": "^4.1.0",
"@nestjs/websockets": "^10.3.10", "@nestjs/websockets": "^10.3.10",
"@nicestack/common": "workspace:*", "@nice/common": "workspace:*",
"@trpc/server": "11.0.0-rc.456", "@trpc/server": "11.0.0-rc.456",
"@tus/file-store": "^1.5.1", "@tus/file-store": "^1.5.1",
"@tus/s3-store": "^1.6.2", "@tus/s3-store": "^1.6.2",

View File

@ -1,6 +1,6 @@
import { Controller, Headers, Post, Body, UseGuards, Get, Req, HttpException, HttpStatus, BadRequestException, InternalServerErrorException, NotFoundException, UnauthorizedException, Logger } from '@nestjs/common'; import { Controller, Headers, Post, Body, UseGuards, Get, Req, HttpException, HttpStatus, BadRequestException, InternalServerErrorException, NotFoundException, UnauthorizedException, Logger } from '@nestjs/common';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { AuthSchema, JwtPayload } from '@nicestack/common'; import { AuthSchema, JwtPayload } from '@nice/common';
import { AuthGuard } from './auth.guard'; import { AuthGuard } from './auth.guard';
import { UserProfileService } from './utils'; import { UserProfileService } from './utils';
import { z } from 'zod'; import { z } from 'zod';
@ -54,8 +54,10 @@ export class AuthController {
throw error; throw error;
} }
} }
@UseGuards(AuthGuard)
@Get('user-profile') @Get('user-profile')
async getUserProfile(@Req() request: Request) { async getUserProfile(@Req() request: Request) {
const payload: JwtPayload = (request as any).user; const payload: JwtPayload = (request as any).user;
const { staff } = await UserProfileService.instance.getUserProfileById(payload.sub); const { staff } = await UserProfileService.instance.getUserProfileById(payload.sub);
return staff return staff

View File

@ -7,7 +7,7 @@ import {
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { env } from '@server/env'; import { env } from '@server/env';
import { JwtPayload } from '@nicestack/common'; import { JwtPayload } from '@nice/common';
import { extractTokenFromHeader } from './utils'; import { extractTokenFromHeader } from './utils';
@Injectable() @Injectable()
@ -16,7 +16,6 @@ export class AuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const token = extractTokenFromHeader(request); const token = extractTokenFromHeader(request);
if (!token) { if (!token) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
@ -27,9 +26,6 @@ export class AuthGuard implements CanActivate {
secret: env.JWT_SECRET secret: env.JWT_SECRET
} }
); );
// 💡 We're assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload; request['user'] = payload;
} catch { } catch {
throw new UnauthorizedException(); throw new UnauthorizedException();

View File

@ -11,7 +11,7 @@ import {
db, db,
AuthSchema, AuthSchema,
JwtPayload, JwtPayload,
} from '@nicestack/common'; } from '@nice/common';
import * as argon2 from 'argon2'; import * as argon2 from 'argon2';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { redis } from '@server/utils/redis/redis.service'; import { redis } from '@server/utils/redis/redis.service';

View File

@ -5,7 +5,7 @@ import {
JwtPayload, JwtPayload,
RolePerms, RolePerms,
ObjectType, ObjectType,
} from '@nicestack/common'; } from '@nice/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { env } from '@server/env'; import { env } from '@server/env';
import { redis } from '@server/utils/redis/redis.service'; import { redis } from '@server/utils/redis/redis.service';
@ -22,8 +22,7 @@ interface TokenVerifyResult {
error?: string; error?: string;
} }
export function extractTokenFromHeader(request: Request): string | undefined { export function extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = extractTokenFromAuthorization(request.headers.authorization) return extractTokenFromAuthorization(request.headers.authorization)
return type === 'Bearer' ? token : undefined;
} }
export function extractTokenFromAuthorization(authorization: string): string | undefined { export function extractTokenFromAuthorization(authorization: string): string | undefined {
const [type, token] = authorization?.split(' ') ?? []; const [type, token] = authorization?.split(' ') ?? [];

View File

@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { AppConfigService } from './app-config.service'; import { AppConfigService } from './app-config.service';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
import { Prisma } from '@nicestack/common'; import { Prisma } from '@nice/common';
import { RealtimeServer } from '@server/socket/realtime/realtime.server'; import { RealtimeServer } from '@server/socket/realtime/realtime.server';
const AppConfigUncheckedCreateInputSchema: ZodType<Prisma.AppConfigUncheckedCreateInput> = z.any() const AppConfigUncheckedCreateInputSchema: ZodType<Prisma.AppConfigUncheckedCreateInput> = z.any()
const AppConfigUpdateArgsSchema: ZodType<Prisma.AppConfigUpdateArgs> = z.any() const AppConfigUpdateArgsSchema: ZodType<Prisma.AppConfigUpdateArgs> = z.any()

View File

@ -3,7 +3,7 @@ import {
db, db,
ObjectType, ObjectType,
Prisma, Prisma,
} from '@nicestack/common'; } from '@nice/common';
import { BaseService } from '../base/base.service'; import { BaseService } from '../base/base.service';

View File

@ -1,4 +1,4 @@
import { db, Prisma, PrismaClient } from '@nicestack/common'; import { db, Prisma, PrismaClient } from '@nice/common';
import { import {
Operations, Operations,
DelegateArgs, DelegateArgs,

View File

@ -1,4 +1,4 @@
import { Prisma, PrismaClient } from '@nicestack/common'; import { Prisma, PrismaClient } from '@nice/common';
import { BaseService } from "./base.service"; import { BaseService } from "./base.service";
import { DataArgs, DelegateArgs, DelegateFuncs, DelegateReturnTypes, UpdateOrderArgs } from "./base.type"; import { DataArgs, DelegateArgs, DelegateFuncs, DelegateReturnTypes, UpdateOrderArgs } from "./base.type";

View File

@ -1,4 +1,4 @@
import { db, Prisma, PrismaClient } from "@nicestack/common"; import { db, Prisma, PrismaClient } from "@nice/common";
export type Operations = export type Operations =
| 'aggregate' | 'aggregate'

View File

@ -1,4 +1,4 @@
import { UserProfile, RowModelRequest, RowRequestSchema } from "@nicestack/common"; import { UserProfile, RowModelRequest, RowRequestSchema } from "@nice/common";
import { RowModelService } from "./row-model.service"; import { RowModelService } from "./row-model.service";
import { isFieldCondition, LogicalCondition, SQLBuilder } from "./sql-builder"; import { isFieldCondition, LogicalCondition, SQLBuilder } from "./sql-builder";
import EventBus from "@server/utils/event-bus"; import EventBus from "@server/utils/event-bus";

View File

@ -1,5 +1,5 @@
import { Logger } from "@nestjs/common"; import { Logger } from "@nestjs/common";
import { UserProfile, db, RowModelRequest } from "@nicestack/common"; import { UserProfile, db, RowModelRequest } from "@nice/common";
import { LogicalCondition, OperatorType, SQLBuilder } from './sql-builder'; import { LogicalCondition, OperatorType, SQLBuilder } from './sql-builder';
export interface GetRowOptions { export interface GetRowOptions {
id?: string; id?: string;

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { Prisma, UpdateOrderSchema } from '@nicestack/common'; import { Prisma, UpdateOrderSchema } from '@nice/common';
import { CourseService } from './course.service'; import { CourseService } from './course.service';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
const CourseCreateArgsSchema: ZodType<Prisma.CourseCreateArgs> = z.any() const CourseCreateArgsSchema: ZodType<Prisma.CourseCreateArgs> = z.any()

View File

@ -6,7 +6,7 @@ import {
ObjectType, ObjectType,
Prisma, Prisma,
InstructorRole, InstructorRole,
} from '@nicestack/common'; } from '@nice/common';
@Injectable() @Injectable()
export class CourseService extends BaseService<Prisma.CourseDelegate> { export class CourseService extends BaseService<Prisma.CourseDelegate> {
constructor() { constructor() {

View File

@ -1,4 +1,4 @@
import { db, EnrollmentStatus, PostType } from "@nicestack/common"; import { db, EnrollmentStatus, PostType } from "@nice/common";
// 更新课程评价统计 // 更新课程评价统计
export async function updateCourseReviewStats(courseId: string) { export async function updateCourseReviewStats(courseId: string) {

View File

@ -2,7 +2,7 @@ import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { DepartmentService } from './department.service'; import { DepartmentService } from './department.service';
import { AuthGuard } from '@server/auth/auth.guard'; import { AuthGuard } from '@server/auth/auth.guard';
import { db } from '@nicestack/common'; import { db } from '@nice/common';
@Controller('dept') @Controller('dept')
export class DepartmentController { export class DepartmentController {

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { DepartmentService } from './department.service'; // assuming it's in the same directory import { DepartmentService } from './department.service'; // assuming it's in the same directory
import { DepartmentMethodSchema, Prisma, UpdateOrderSchema } from '@nicestack/common'; import { DepartmentMethodSchema, Prisma, UpdateOrderSchema } from '@nice/common';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
import { DepartmentRowService } from './department.row.service'; import { DepartmentRowService } from './department.row.service';

View File

@ -8,7 +8,7 @@ import {
DepartmentMethodSchema, DepartmentMethodSchema,
ObjectType, ObjectType,
UserProfile, UserProfile,
} from '@nicestack/common'; } from '@nice/common';
import { date, z } from 'zod'; import { date, z } from 'zod';
import { RowCacheService } from '../base/row-cache.service'; import { RowCacheService } from '../base/row-cache.service';
import { isFieldCondition } from '../base/sql-builder'; import { isFieldCondition } from '../base/sql-builder';

View File

@ -6,7 +6,7 @@ import {
getUniqueItems, getUniqueItems,
ObjectType, ObjectType,
Prisma, Prisma,
} from '@nicestack/common'; } from '@nice/common';
import { BaseTreeService } from '../base/base.tree.service'; import { BaseTreeService } from '../base/base.tree.service';
import { z } from 'zod'; import { z } from 'zod';
import { mapToDeptSimpleTree, getStaffsByDeptIds } from './utils'; import { mapToDeptSimpleTree, getStaffsByDeptIds } from './utils';

View File

@ -1,4 +1,4 @@
import { UserProfile, db, DeptSimpleTreeNode, TreeDataNode } from "@nicestack/common"; import { UserProfile, db, DeptSimpleTreeNode, TreeDataNode } from "@nice/common";
/** /**
* DeptSimpleTreeNode结构 * DeptSimpleTreeNode结构

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { Prisma, UpdateOrderSchema } from '@nicestack/common'; import { Prisma, UpdateOrderSchema } from '@nice/common';
import { EnrollmentService } from './enrollment.service'; import { EnrollmentService } from './enrollment.service';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
import { EnrollSchema, UnenrollSchema } from './enroll.schema'; import { EnrollSchema, UnenrollSchema } from './enroll.schema';

View File

@ -6,7 +6,7 @@ import {
ObjectType, ObjectType,
Prisma, Prisma,
EnrollmentStatus EnrollmentStatus
} from '@nicestack/common'; } from '@nice/common';
import { z } from 'zod'; import { z } from 'zod';
import { EnrollSchema, UnenrollSchema } from './enroll.schema'; import { EnrollSchema, UnenrollSchema } from './enroll.schema';
import EventBus, { CrudOperation } from '@server/utils/event-bus'; import EventBus, { CrudOperation } from '@server/utils/event-bus';

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { Prisma, UpdateOrderSchema } from '@nicestack/common'; import { Prisma, UpdateOrderSchema } from '@nice/common';
import { LectureService } from './lecture.service'; import { LectureService } from './lecture.service';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
const LectureCreateArgsSchema: ZodType<Prisma.LectureCreateArgs> = z.any() const LectureCreateArgsSchema: ZodType<Prisma.LectureCreateArgs> = z.any()

View File

@ -5,7 +5,7 @@ import {
db, db,
ObjectType, ObjectType,
Prisma Prisma
} from '@nicestack/common'; } from '@nice/common';
import EventBus, { CrudOperation } from '@server/utils/event-bus'; import EventBus, { CrudOperation } from '@server/utils/event-bus';
@Injectable() @Injectable()

View File

@ -1,4 +1,4 @@
import { db, Lecture } from "@nicestack/common" import { db, Lecture } from "@nice/common"
export async function updateSectionLectureStats(sectionId: string) { export async function updateSectionLectureStats(sectionId: string) {
const sectionStats = await db.lecture.aggregate({ const sectionStats = await db.lecture.aggregate({

View File

@ -2,7 +2,7 @@ import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { MessageService } from './message.service'; import { MessageService } from './message.service';
import { AuthGuard } from '@server/auth/auth.guard'; import { AuthGuard } from '@server/auth/auth.guard';
import { db, VisitType } from '@nicestack/common'; import { db, VisitType } from '@nice/common';
@Controller('message') @Controller('message')
export class MessageController { export class MessageController {

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { MessageService } from './message.service'; import { MessageService } from './message.service';
import { Prisma } from '@nicestack/common'; import { Prisma } from '@nice/common';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
const MessageUncheckedCreateInputSchema: ZodType<Prisma.MessageUncheckedCreateInput> = z.any() const MessageUncheckedCreateInputSchema: ZodType<Prisma.MessageUncheckedCreateInput> = z.any()
const MessageWhereInputSchema: ZodType<Prisma.MessageWhereInput> = z.any() const MessageWhereInputSchema: ZodType<Prisma.MessageWhereInput> = z.any()

View File

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { UserProfile, db, Prisma, VisitType, ObjectType } from '@nicestack/common'; import { UserProfile, db, Prisma, VisitType, ObjectType } from '@nice/common';
import { BaseService } from '../base/base.service'; import { BaseService } from '../base/base.service';
import EventBus, { CrudOperation } from '@server/utils/event-bus'; import EventBus, { CrudOperation } from '@server/utils/event-bus';
import { setMessageRelation } from './utils'; import { setMessageRelation } from './utils';

View File

@ -1,4 +1,4 @@
import { Message, UserProfile, VisitType, db } from "@nicestack/common" import { Message, UserProfile, VisitType, db } from "@nice/common"
export async function setMessageRelation( export async function setMessageRelation(
data: Message, data: Message,
staff?: UserProfile, staff?: UserProfile,

View File

@ -2,7 +2,7 @@ import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { PostService } from './post.service'; import { PostService } from './post.service';
import { AuthGuard } from '@server/auth/auth.guard'; import { AuthGuard } from '@server/auth/auth.guard';
import { db } from '@nicestack/common'; import { db } from '@nice/common';
@Controller('post') @Controller('post')
export class PostController { export class PostController {

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { Prisma } from '@nicestack/common'; import { Prisma } from '@nice/common';
import { PostService } from './post.service'; import { PostService } from './post.service';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
const PostCreateArgsSchema: ZodType<Prisma.PostCreateArgs> = z.any(); const PostCreateArgsSchema: ZodType<Prisma.PostCreateArgs> = z.any();

View File

@ -9,7 +9,7 @@ import {
RolePerms, RolePerms,
ResPerm, ResPerm,
ObjectType, ObjectType,
} from '@nicestack/common'; } from '@nice/common';
import { MessageService } from '../message/message.service'; import { MessageService } from '../message/message.service';
import { BaseService } from '../base/base.service'; import { BaseService } from '../base/base.service';
import { DepartmentService } from '../department/department.service'; import { DepartmentService } from '../department/department.service';

View File

@ -1,4 +1,4 @@
import { db, Post, PostType, UserProfile, VisitType } from "@nicestack/common"; import { db, Post, PostType, UserProfile, VisitType } from "@nice/common";
export async function setPostRelation(params: { data: Post, staff?: UserProfile }) { export async function setPostRelation(params: { data: Post, staff?: UserProfile }) {
const { data, staff } = params const { data, staff } = params

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { Prisma, UpdateOrderSchema } from '@nicestack/common'; import { Prisma, UpdateOrderSchema } from '@nice/common';
import { RoleService } from './role.service'; import { RoleService } from './role.service';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
const RoleCreateArgsSchema: ZodType<Prisma.RoleCreateArgs> = z.any() const RoleCreateArgsSchema: ZodType<Prisma.RoleCreateArgs> = z.any()

View File

@ -1,4 +1,4 @@
import { db, ObjectType, RowModelRequest, RowRequestSchema, UserProfile } from "@nicestack/common"; import { db, ObjectType, RowModelRequest, RowRequestSchema, UserProfile } from "@nice/common";
import { RowCacheService } from "../base/row-cache.service"; import { RowCacheService } from "../base/row-cache.service";
import { isFieldCondition, LogicalCondition } from "../base/sql-builder"; import { isFieldCondition, LogicalCondition } from "../base/sql-builder";
import { z } from "zod"; import { z } from "zod";

View File

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { db, RoleMethodSchema, ObjectType, Prisma } from "@nicestack/common"; import { db, RoleMethodSchema, ObjectType, Prisma } from "@nice/common";
import { BaseService } from '../base/base.service'; import { BaseService } from '../base/base.service';
@Injectable() @Injectable()
export class RoleService extends BaseService<Prisma.RoleDelegate> { export class RoleService extends BaseService<Prisma.RoleDelegate> {

View File

@ -3,7 +3,7 @@ import { TrpcService } from '@server/trpc/trpc.service';
import { import {
ObjectType, ObjectType,
RoleMapMethodSchema, RoleMapMethodSchema,
} from '@nicestack/common'; } from '@nice/common';
import { RoleMapService } from './rolemap.service'; import { RoleMapService } from './rolemap.service';
@Injectable() @Injectable()

View File

@ -6,7 +6,7 @@ import {
Prisma, Prisma,
RowModelRequest, RowModelRequest,
UserProfile, UserProfile,
} from '@nicestack/common'; } from '@nice/common';
import { DepartmentService } from '@server/models/department/department.service'; import { DepartmentService } from '@server/models/department/department.service';
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import { RowModelService } from '../base/row-model.service'; import { RowModelService } from '../base/row-model.service';

View File

@ -1,6 +1,6 @@
import { PrismaClient, Resource } from '@prisma/client' import { PrismaClient, Resource } from '@prisma/client'
import { ProcessResult, ResourceProcessor } from '../types' import { ProcessResult, ResourceProcessor } from '../types'
import { db, ResourceProcessStatus } from '@nicestack/common' import { db, ResourceProcessStatus } from '@nice/common'
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';

View File

@ -1,5 +1,5 @@
import { BaseMetadata, FileMetadata, ResourceProcessor } from "../types"; import { BaseMetadata, FileMetadata, ResourceProcessor } from "../types";
import { Resource, db, ResourceProcessStatus } from "@nicestack/common"; import { Resource, db, ResourceProcessStatus } from "@nice/common";
import { extname } from "path"; import { extname } from "path";
import mime from "mime"; import mime from "mime";
import { calculateFileHash, getUploadFilePath } from "@server/utils/file"; import { calculateFileHash, getUploadFilePath } from "@server/utils/file";

View File

@ -1,7 +1,7 @@
import path from "path"; import path from "path";
import sharp from 'sharp'; import sharp from 'sharp';
import { FileMetadata, ImageMetadata, ResourceProcessor } from "../types"; import { FileMetadata, ImageMetadata, ResourceProcessor } from "../types";
import { Resource, ResourceProcessStatus, db } from "@nicestack/common"; import { Resource, ResourceProcessStatus, db } from "@nice/common";
import { getUploadFilePath } from "@server/utils/file"; import { getUploadFilePath } from "@server/utils/file";
import { Logger } from "@nestjs/common"; import { Logger } from "@nestjs/common";
import { promises as fsPromises } from 'fs'; import { promises as fsPromises } from 'fs';

View File

@ -1,6 +1,6 @@
// import ffmpeg from 'fluent-ffmpeg'; // import ffmpeg from 'fluent-ffmpeg';
// import { ResourceProcessor } from '../types'; // import { ResourceProcessor } from '../types';
// import { Resource } from '@nicestack/common'; // import { Resource } from '@nice/common';
// export class VideoProcessor implements ResourceProcessor { // export class VideoProcessor implements ResourceProcessor {
// async process(resource: Resource): Promise<Resource> { // async process(resource: Resource): Promise<Resource> {

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { Prisma, UpdateOrderSchema } from '@nicestack/common'; import { Prisma, UpdateOrderSchema } from '@nice/common';
import { ResourceService } from './resource.service'; import { ResourceService } from './resource.service';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
const ResourceCreateArgsSchema: ZodType<Prisma.ResourceCreateArgs> = z.any() const ResourceCreateArgsSchema: ZodType<Prisma.ResourceCreateArgs> = z.any()

View File

@ -6,8 +6,7 @@ import {
ObjectType, ObjectType,
Prisma, Prisma,
Resource, Resource,
} from '@nicestack/common'; } from '@nice/common';
import { createHash } from 'crypto';
@Injectable() @Injectable()
export class ResourceService extends BaseService<Prisma.ResourceDelegate> { export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
@ -31,8 +30,4 @@ export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
}, },
}); });
} }
async calculateFileHash(buffer: Buffer): Promise<string> {
return createHash('sha256').update(buffer).digest('hex');
}
} }

View File

@ -1,4 +1,4 @@
import { Resource } from "@nicestack/common"; import { Resource } from "@nice/common";
export interface ResourceProcessor { export interface ResourceProcessor {
process(resource: Resource): Promise<any> process(resource: Resource): Promise<any>

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { Prisma, UpdateOrderSchema } from '@nicestack/common'; import { Prisma, UpdateOrderSchema } from '@nice/common';
import { SectionService } from './section.service'; import { SectionService } from './section.service';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
const SectionCreateArgsSchema: ZodType<Prisma.SectionCreateArgs> = z.any() const SectionCreateArgsSchema: ZodType<Prisma.SectionCreateArgs> = z.any()

View File

@ -6,7 +6,7 @@ import {
ObjectType, ObjectType,
Prisma, Prisma,
} from '@nicestack/common'; } from '@nice/common';
@Injectable() @Injectable()
export class SectionService extends BaseService<Prisma.SectionDelegate> { export class SectionService extends BaseService<Prisma.SectionDelegate> {
constructor() { constructor() {

View File

@ -2,7 +2,7 @@ import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { StaffService } from './staff.service'; import { StaffService } from './staff.service';
import { AuthGuard } from '@server/auth/auth.guard'; import { AuthGuard } from '@server/auth/auth.guard';
import { db } from '@nicestack/common'; import { db } from '@nice/common';
@Controller('staff') @Controller('staff')
export class StaffController { export class StaffController {

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { StaffService } from './staff.service'; // Adjust the import path as necessary import { StaffService } from './staff.service'; // Adjust the import path as necessary
import { StaffMethodSchema, Prisma, UpdateOrderSchema } from '@nicestack/common'; import { StaffMethodSchema, Prisma, UpdateOrderSchema } from '@nice/common';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
import { StaffRowService } from './staff.row.service'; import { StaffRowService } from './staff.row.service';
const StaffCreateArgsSchema: ZodType<Prisma.StaffCreateArgs> = z.any(); const StaffCreateArgsSchema: ZodType<Prisma.StaffCreateArgs> = z.any();

View File

@ -8,7 +8,7 @@ import {
ResPerm, ResPerm,
Staff, Staff,
RowModelRequest, RowModelRequest,
} from '@nicestack/common'; } from '@nice/common';
import { DepartmentService } from '../department/department.service'; import { DepartmentService } from '../department/department.service';
import { RowCacheService } from '../base/row-cache.service'; import { RowCacheService } from '../base/row-cache.service';
import { z } from 'zod'; import { z } from 'zod';

View File

@ -5,7 +5,7 @@ import {
ObjectType, ObjectType,
UserProfile, UserProfile,
Prisma, Prisma,
} from '@nicestack/common'; } from '@nice/common';
import { DepartmentService } from '../department/department.service'; import { DepartmentService } from '../department/department.service';
import { z } from 'zod'; import { z } from 'zod';
import { BaseService } from '../base/base.service'; import { BaseService } from '../base/base.service';

View File

@ -2,7 +2,7 @@ import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { TaxonomyService } from './taxonomy.service'; import { TaxonomyService } from './taxonomy.service';
import { AuthGuard } from '@server/auth/auth.guard'; import { AuthGuard } from '@server/auth/auth.guard';
import { db } from '@nicestack/common'; import { db } from '@nice/common';
@Controller('tax') @Controller('tax')
export class TaxonomyController { export class TaxonomyController {

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { TaxonomyService } from './taxonomy.service'; import { TaxonomyService } from './taxonomy.service';
import { TaxonomyMethodSchema } from '@nicestack/common'; import { TaxonomyMethodSchema } from '@nice/common';
@Injectable() @Injectable()
export class TaxonomyRouter { export class TaxonomyRouter {

View File

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { db, TaxonomyMethodSchema, Prisma } from '@nicestack/common'; import { db, TaxonomyMethodSchema, Prisma } from '@nice/common';
import { redis } from '@server/utils/redis/redis.service'; import { redis } from '@server/utils/redis/redis.service';
import { deleteByPattern } from '@server/utils/redis/utils'; import { deleteByPattern } from '@server/utils/redis/utils';
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';

View File

@ -2,7 +2,7 @@ import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { TermService } from './term.service'; import { TermService } from './term.service';
import { AuthGuard } from '@server/auth/auth.guard'; import { AuthGuard } from '@server/auth/auth.guard';
import { db } from '@nicestack/common'; import { db } from '@nice/common';
@Controller('term') @Controller('term')
export class TermController { export class TermController {

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { TermService } from './term.service'; // Adjust the import path as necessary import { TermService } from './term.service'; // Adjust the import path as necessary
import { Prisma, TermMethodSchema, UpdateOrderSchema } from '@nicestack/common'; import { Prisma, TermMethodSchema, UpdateOrderSchema } from '@nice/common';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
import { TermRowService } from './term.row.service'; import { TermRowService } from './term.row.service';
const TermCreateArgsSchema: ZodType<Prisma.TermCreateArgs> = z.any(); const TermCreateArgsSchema: ZodType<Prisma.TermCreateArgs> = z.any();

View File

@ -4,7 +4,7 @@ import {
RowModelRequest, RowModelRequest,
TermMethodSchema, TermMethodSchema,
UserProfile, UserProfile,
} from '@nicestack/common'; } from '@nice/common';
import { date, z } from 'zod'; import { date, z } from 'zod';
import { RowCacheService } from '../base/row-cache.service'; import { RowCacheService } from '../base/row-cache.service';
import { isFieldCondition } from '../base/sql-builder'; import { isFieldCondition } from '../base/sql-builder';

View File

@ -13,7 +13,7 @@ import {
TaxonomySlug, TaxonomySlug,
ObjectType, ObjectType,
TermAncestry, TermAncestry,
} from '@nicestack/common'; } from '@nice/common';
import { z } from 'zod'; import { z } from 'zod';
import { BaseTreeService } from '../base/base.tree.service'; import { BaseTreeService } from '../base/base.tree.service';
import EventBus, { CrudOperation } from '@server/utils/event-bus'; import EventBus, { CrudOperation } from '@server/utils/event-bus';

View File

@ -1,4 +1,4 @@
import { TreeDataNode } from '@nicestack/common'; import { TreeDataNode } from '@nice/common';
export function formatToTermTreeData(term: any): TreeDataNode { export function formatToTermTreeData(term: any): TreeDataNode {
return { return {

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TransformService } from './transform.service'; import { TransformService } from './transform.service';
import { TransformMethodSchema} from '@nicestack/common'; import { TransformMethodSchema} from '@nice/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
@Injectable() @Injectable()
export class TransformRouter { export class TransformRouter {

View File

@ -5,7 +5,7 @@ import {
db, db,
Prisma, Prisma,
Staff, Staff,
} from '@nicestack/common'; } from '@nice/common';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import * as argon2 from 'argon2'; import * as argon2 from 'argon2';
import { TaxonomyService } from '@server/models/taxonomy/taxonomy.service'; import { TaxonomyService } from '@server/models/taxonomy/taxonomy.service';

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { Prisma } from '@nicestack/common'; import { Prisma } from '@nice/common';
import { VisitService } from './visit.service'; import { VisitService } from './visit.service';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';

View File

@ -6,7 +6,7 @@ import {
ObjectType, ObjectType,
Prisma, Prisma,
VisitType, VisitType,
} from '@nicestack/common'; } from '@nice/common';
import EventBus from '@server/utils/event-bus'; import EventBus from '@server/utils/event-bus';
@Injectable() @Injectable()
export class VisitService extends BaseService<Prisma.VisitDelegate> { export class VisitService extends BaseService<Prisma.VisitDelegate> {

View File

@ -2,7 +2,7 @@ import { InjectQueue } from "@nestjs/bullmq";
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import EventBus from "@server/utils/event-bus"; import EventBus from "@server/utils/event-bus";
import { Queue } from "bullmq"; import { Queue } from "bullmq";
import { ObjectType } from "@nicestack/common"; import { ObjectType } from "@nice/common";
import { QueueJobType } from "../types"; import { QueueJobType } from "../types";
@Injectable() @Injectable()
export class StatsService { export class StatsService {

View File

@ -5,7 +5,7 @@ import { ResourceProcessingPipeline } from '@server/models/resource/pipe/resourc
import { GeneralProcessor } from '@server/models/resource/processor/GeneralProcessor'; import { GeneralProcessor } from '@server/models/resource/processor/GeneralProcessor';
import { ImageProcessor } from '@server/models/resource/processor/ImageProcessor'; import { ImageProcessor } from '@server/models/resource/processor/ImageProcessor';
import superjson from 'superjson-cjs'; import superjson from 'superjson-cjs';
import { Resource } from '@nicestack/common'; import { Resource } from '@nice/common';
const logger = new Logger('FileProcessorWorker'); const logger = new Logger('FileProcessorWorker');
const pipeline = new ResourceProcessingPipeline() const pipeline = new ResourceProcessingPipeline()
.addProcessor(new GeneralProcessor()) .addProcessor(new GeneralProcessor())

View File

@ -4,7 +4,7 @@ import {
updateCourseLectureStats, updateCourseLectureStats,
updateSectionLectureStats updateSectionLectureStats
} from '@server/models/lecture/utils'; } from '@server/models/lecture/utils';
import { ObjectType } from '@nicestack/common'; import { ObjectType } from '@nice/common';
import { import {
updateCourseEnrollmentStats, updateCourseEnrollmentStats,
updateCourseReviewStats updateCourseReviewStats

View File

@ -2,7 +2,7 @@
import { WebSocketServer, WebSocket } from "ws"; import { WebSocketServer, WebSocket } from "ws";
import { Logger } from "@nestjs/common"; import { Logger } from "@nestjs/common";
import { WebSocketServerConfig, WSClient, WebSocketType } from "../types"; import { WebSocketServerConfig, WSClient, WebSocketType } from "../types";
import { SocketMessage } from '@nicestack/common'; import { SocketMessage } from '@nice/common';
const DEFAULT_CONFIG: WebSocketServerConfig = { const DEFAULT_CONFIG: WebSocketServerConfig = {
pingInterval: 30000, pingInterval: 30000,

View File

@ -1,5 +1,5 @@
import { readSyncMessage } from '@nicestack/common'; import { readSyncMessage } from '@nice/common';
import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates, writeSyncStep1, writeUpdate } from '@nicestack/common'; import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates, writeSyncStep1, writeUpdate } from '@nice/common';
import * as encoding from 'lib0/encoding'; import * as encoding from 'lib0/encoding';
import * as decoding from 'lib0/decoding'; import * as decoding from 'lib0/decoding';
import * as Y from "yjs" import * as Y from "yjs"
@ -7,7 +7,7 @@ import { debounce } from 'lodash';
import { getPersistence, setPersistence } from './persistence'; import { getPersistence, setPersistence } from './persistence';
import { callbackHandler, isCallbackSet } from './callback'; import { callbackHandler, isCallbackSet } from './callback';
import { WebSocket } from "ws"; import { WebSocket } from "ws";
import { YMessageType } from '@nicestack/common'; import { YMessageType } from '@nice/common';
import { WSClient } from '../types'; import { WSClient } from '../types';
export const docs = new Map<string, WSSharedDoc>(); export const docs = new Map<string, WSSharedDoc>();
export const CALLBACK_DEBOUNCE_WAIT = parseInt(process.env.CALLBACK_DEBOUNCE_WAIT || '2000'); export const CALLBACK_DEBOUNCE_WAIT = parseInt(process.env.CALLBACK_DEBOUNCE_WAIT || '2000');

View File

@ -2,7 +2,7 @@ import { Injectable } from "@nestjs/common";
import { WebSocketType, WSClient } from "../types"; import { WebSocketType, WSClient } from "../types";
import { BaseWebSocketServer } from "../base/base-websocket-server"; import { BaseWebSocketServer } from "../base/base-websocket-server";
import { encoding } from "lib0"; import { encoding } from "lib0";
import { YMessageType, writeSyncStep1, encodeAwarenessUpdate } from "@nicestack/common"; import { YMessageType, writeSyncStep1, encodeAwarenessUpdate } from "@nice/common";
import { getYDoc, closeConn, WSSharedDoc, messageListener, send } from "./ws-shared-doc"; import { getYDoc, closeConn, WSSharedDoc, messageListener, send } from "./ws-shared-doc";
@Injectable() @Injectable()
export class YjsServer extends BaseWebSocketServer { export class YjsServer extends BaseWebSocketServer {

View File

@ -2,7 +2,7 @@ import { Injectable, OnModuleInit } from "@nestjs/common";
import { WebSocketType } from "../types"; import { WebSocketType } from "../types";
import { BaseWebSocketServer } from "../base/base-websocket-server"; import { BaseWebSocketServer } from "../base/base-websocket-server";
import EventBus, { CrudOperation } from "@server/utils/event-bus"; import EventBus, { CrudOperation } from "@server/utils/event-bus";
import { ObjectType, SocketMsgType, MessageDto, PostDto, PostType } from "@nicestack/common"; import { ObjectType, SocketMsgType, MessageDto, PostDto, PostType } from "@nice/common";
@Injectable() @Injectable()
export class RealtimeServer extends BaseWebSocketServer implements OnModuleInit { export class RealtimeServer extends BaseWebSocketServer implements OnModuleInit {
onModuleInit() { onModuleInit() {

View File

@ -10,8 +10,7 @@ import {
Staff, Staff,
TaxonomySlug, TaxonomySlug,
Term, Term,
} from '@nicestack/common'; } from '@nice/common';
import * as argon2 from 'argon2';
import EventBus from '@server/utils/event-bus'; import EventBus from '@server/utils/event-bus';
import { import {
@ -19,9 +18,7 @@ import {
DevDataCounts, DevDataCounts,
getCounts, getCounts,
} from './utils'; } from './utils';
import { StaffService } from '@server/models/staff/staff.service'; import { StaffService } from '@server/models/staff/staff.service';
import { uuidv4 } from 'lib0/random';
@Injectable() @Injectable()
export class GenDevService { export class GenDevService {
private readonly logger = new Logger(GenDevService.name); private readonly logger = new Logger(GenDevService.name);

View File

@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { db, InitAppConfigs, InitRoles, InitTaxonomies, ObjectType } from "@nicestack/common"; import { db, InitAppConfigs, InitRoles, InitTaxonomies, ObjectType } from "@nice/common";
import { AuthService } from '@server/auth/auth.service'; import { AuthService } from '@server/auth/auth.service';
import { MinioService } from '@server/utils/minio/minio.service'; import { MinioService } from '@server/utils/minio/minio.service';
import { AppConfigService } from '@server/models/app-config/app-config.service'; import { AppConfigService } from '@server/models/app-config/app-config.service';

View File

@ -1,4 +1,4 @@
import { db, getRandomElement, getRandomIntInRange, getRandomTimeInterval, } from '@nicestack/common'; import { db, getRandomElement, getRandomIntInRange, getRandomTimeInterval, } from '@nice/common';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
export interface DevDataCounts { export interface DevDataCounts {
deptCount: number; deptCount: number;

View File

@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { initTRPC, TRPCError } from '@trpc/server'; import { initTRPC, TRPCError } from '@trpc/server';
import superjson from 'superjson-cjs'; import superjson from 'superjson-cjs';
import * as trpcExpress from '@trpc/server/adapters/express'; import * as trpcExpress from '@trpc/server/adapters/express';
import { db, JwtPayload, UserProfile, RolePerms } from '@nicestack/common'; import { db, JwtPayload, UserProfile, RolePerms } from '@nice/common';
import { CreateWSSContextFnOptions } from '@trpc/server/adapters/ws'; import { CreateWSSContextFnOptions } from '@trpc/server/adapters/ws';
import { UserProfileService } from '@server/auth/utils'; import { UserProfileService } from '@server/auth/utils';
type Context = Awaited<ReturnType<TrpcService['createExpressContext']>>; type Context = Awaited<ReturnType<TrpcService['createExpressContext']>>;

View File

@ -0,0 +1,135 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { Injectable, Logger } from '@nestjs/common';
import { ChunkDto } from '@nice/common';
@Injectable()
export class ChunkManager {
private readonly logger = new Logger(ChunkManager.name);
private readonly CHUNK_PROCESSING_CONCURRENCY = 3;
private readonly uploadDir: string;
private readonly tempDir: string;
constructor() {
this.uploadDir = process.env.UPLOAD_DIR || path.join(process.cwd(), 'uploads');
this.tempDir = path.join(this.uploadDir, 'temp');
this.logger.localInstance.setLogLevels(["log", "debug"])
this.logger.log(`Initialized ChunkManager with uploadDir: ${this.uploadDir}, tempDir: ${this.tempDir}`);
}
private validateChunk(chunk: ChunkDto, file: Express.Multer.File): void {
this.logger.debug(`Validating chunk: identifier=${chunk?.identifier}, chunkNumber=${chunk?.chunkNumber}, bufferLength=${file?.buffer?.length}`);
if (!chunk?.identifier || !chunk.chunkNumber || !file?.buffer?.length) {
this.logger.warn('Invalid chunk metadata or buffer');
throw new Error('Invalid chunk metadata or buffer');
}
}
async save(chunk: ChunkDto, file: Express.Multer.File): Promise<void> {
this.logger.log(`Saving chunk: identifier=${chunk.identifier}, chunkNumber=${chunk.chunkNumber}`);
this.validateChunk(chunk, file);
const chunkDir = path.join(this.tempDir, chunk.identifier);
const chunkPath = path.join(chunkDir, `${chunk.chunkNumber}`);
try {
this.logger.debug(`Creating chunk directory: ${chunkDir}`);
await fs.mkdir(chunkDir, { recursive: true, mode: 0o755 });
this.logger.debug(`Writing chunk file: ${chunkPath}`);
await fs.writeFile(chunkPath, file.buffer, { mode: 0o644 });
this.logger.log(`Chunk saved successfully: ${chunkPath}`);
} catch (error) {
this.logger.error(`Chunk save failed: ${error instanceof Error ? error.message : error}`);
throw error;
}
}
async getChunks(identifier: string): Promise<number[]> {
this.logger.log(`Retrieving chunks for identifier: ${identifier}`);
if (!identifier) {
this.logger.warn('No identifier provided');
return [];
}
try {
const chunkPath = path.join(this.tempDir, identifier);
this.logger.debug(`Reading directory: ${chunkPath}`);
const files = await fs.readdir(chunkPath);
const chunks = files
.map(Number)
.filter(num => !isNaN(num) && num > 0)
.sort((a, b) => a - b);
this.logger.log(`Found chunks: ${chunks.join(', ')}`);
return chunks;
} catch (error) {
this.logger.warn(`Chunk retrieval failed: ${error}`);
return [];
}
}
async merge(chunkDir: string, finalPath: string, totalChunks: number): Promise<void> {
this.logger.log(`Merging chunks: chunkDir=${chunkDir}, finalPath=${finalPath}, totalChunks=${totalChunks}`);
if (!chunkDir || !finalPath || totalChunks <= 0) {
this.logger.warn('Invalid merge parameters');
throw new Error('Invalid merge parameters');
}
try {
this.logger.debug(`Opening final file: ${finalPath}`);
const fileHandle = await fs.open(finalPath, 'w');
try {
for (let i = 0; i < totalChunks; i += this.CHUNK_PROCESSING_CONCURRENCY) {
const batch = Array.from(
{ length: Math.min(this.CHUNK_PROCESSING_CONCURRENCY, totalChunks - i) },
(_, j) => i + j + 1
);
this.logger.debug(`Processing batch: ${batch.join(', ')}`);
const chunkBuffers = await Promise.all(
batch.map(chunkNumber => {
const chunkPath = path.join(chunkDir, `${chunkNumber}`);
this.logger.debug(`Reading chunk: ${chunkPath}`);
return fs.readFile(chunkPath);
})
);
for (const chunkBuffer of chunkBuffers) {
await fileHandle.write(chunkBuffer, 0, chunkBuffer.length);
}
}
} finally {
this.logger.debug(`Closing file handle: ${finalPath}`);
await fileHandle.close();
}
this.logger.log(`Merge completed successfully: ${finalPath}`);
} catch (error) {
this.logger.error(`Merge failed: ${error}`);
throw error;
}
}
async cleanup(identifier: string): Promise<void> {
this.logger.log(`Cleaning up chunks for identifier: ${identifier}`);
if (!identifier) {
this.logger.warn('No identifier provided for cleanup');
return;
}
try {
const cleanupPath = path.join(this.tempDir, identifier);
this.logger.debug(`Removing directory: ${cleanupPath}`);
await fs.rm(cleanupPath, { recursive: true, force: true });
this.logger.log(`Cleanup completed for: ${identifier}`);
} catch (err) {
if (err instanceof Error && err.name !== 'ENOENT') {
this.logger.error(`Cleanup failed: ${err.message}`);
}
}
}
}

View File

@ -0,0 +1,52 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import * as fs from 'fs/promises';
import * as path from 'path';
import { UploadService } from './upload.service';
@Injectable()
export class CleanService {
private readonly logger = new Logger(CleanService.name);
private readonly tempDir: string;
private readonly TEMP_FILE_EXPIRATION_TIME = 24 * 60 * 60 * 1000; // 24 hours
constructor(private uploadService: UploadService) {
this.tempDir = process.env.UPLOAD_TEMP_DIR || path.join(process.cwd(), 'uploads', 'temp');
}
@Cron(CronExpression.EVERY_6_HOURS)
async cleanExpiredTemporaryFiles(): Promise<void> {
try {
const files = await fs.readdir(this.tempDir);
for (const identifier of files) {
const chunkDir = path.join(this.tempDir, identifier);
try {
const stat = await fs.stat(chunkDir);
// 检查目录是否超过过期时间
const isExpired = (Date.now() - stat.mtime.getTime()) > this.TEMP_FILE_EXPIRATION_TIME;
// 检查上传是否不在进行中
const status = this.uploadService.checkUploadStatusInfo(identifier);
const isNotInProgress = !status ||
status.status === 'completed' ||
status.status === 'error' ||
status.status === 'paused';
if (isExpired && isNotInProgress) {
await fs.rm(chunkDir, { recursive: true, force: true });
this.logger.log(`Cleaned up expired temporary files for identifier: ${identifier}`);
this.uploadService.deleteUploadStatusInfo(identifier);
}
} catch (statError) {
// 处理可能的文件系统错误
this.logger.error(`Error processing directory ${identifier}: ${statError}`);
}
}
} catch (error) {
this.logger.error(`Error during temporary file cleanup: ${error instanceof Error ? error.message : error}`);
}
}
}

View File

@ -1,32 +1,24 @@
export interface HookRequest { export interface UploadCompleteEvent {
Type: 'post-finish' | 'pre-create'; identifier: string;
Event: { filename: string;
Upload: { size: number;
ID?: string; hash: string;
MetaData?: { integrityVerified: boolean;
filename?: string
};
Size?: number;
Storage?: {
Path: string
Type: string
};
};
};
} }
export interface HookResponse {
RejectUpload?: boolean; export type UploadEvent = {
HTTPResponse?: { uploadStart: { identifier: string; filename: string; totalSize: number, resuming?: boolean };
StatusCode?: number; uploadComplete: UploadCompleteEvent
Body?: string; uploadError: { identifier: string; error: string, filename: string };
Headers?: Record<string, string>;
};
ChangeFileInfo?: {
ID?: string;
MetaData?: Record<string, string>;
};
} }
export interface FileHandle { export interface UploadLock {
filename: string clientId: string;
path: string timestamp: number;
} }
// 添加重试机制,处理临时网络问题
// 实现定期清理过期的临时文件
// 添加文件完整性校验
// 实现上传进度持久化,支持服务重启后恢复
// 添加并发限制,防止系统资源耗尽
// 实现文件去重功能,避免重复上传
// 添加日志记录和监控机制

View File

@ -0,0 +1,88 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule'; // 注意这里是 @Cron
import { UploadLock } from './types';
import { UploadLockInfo } from '@nice/common';
@Injectable()
export class UploadLockService {
private readonly logger = new Logger(UploadLockService.name);
private readonly uploadLocks: Map<string, UploadLock> = new Map();
private readonly MAX_UPLOAD_DURATION = 2 * 60 * 60 * 1000; // 2 hours
acquireLock(identifier: string, clientId: string): boolean {
if (!identifier || !clientId) {
this.logger.warn('Invalid lock parameters');
return false;
}
const now = Date.now();
const existingLock = this.uploadLocks.get(identifier);
// Check if lock exists and is not expired
if (existingLock) {
const lockDuration = now - existingLock.timestamp;
// More robust lock conflict check
if (
existingLock.clientId !== clientId &&
lockDuration < this.MAX_UPLOAD_DURATION
) {
this.logger.warn(`Upload conflict: File ${identifier} is locked by another client`);
return false;
}
}
// Acquire or update the lock
this.uploadLocks.set(identifier, { clientId, timestamp: now });
return true;
}
releaseLock(identifier: string, clientId: string): boolean {
if (!identifier || !clientId) {
this.logger.warn('Invalid unlock parameters');
return false;
}
const currentLock = this.uploadLocks.get(identifier);
// Only allow release by the current lock owner
if (currentLock?.clientId === clientId) {
this.uploadLocks.delete(identifier);
return true;
}
this.logger.warn(`Unauthorized lock release attempt for ${identifier}`);
return false;
}
checkLock(identifier: string): UploadLockInfo {
const now = Date.now();
const lockInfo = this.uploadLocks.get(identifier);
// Check if lock exists and is not expired
if (lockInfo && now - lockInfo.timestamp < this.MAX_UPLOAD_DURATION) {
return {
isLocked: true,
lockedBy: lockInfo.clientId
};
}
return { isLocked: false };
}
@Cron(CronExpression.EVERY_HOUR)
cleanupExpiredLocks(): number {
const now = Date.now();
let removedLocksCount = 0;
for (const [identifier, lock] of this.uploadLocks.entries()) {
if (now - lock.timestamp > this.MAX_UPLOAD_DURATION) {
this.uploadLocks.delete(identifier);
removedLocksCount++;
}
}
if (removedLocksCount > 0) {
this.logger.log(`Cleaned up ${removedLocksCount} expired locks`);
}
return removedLocksCount;
}
}

View File

@ -1,20 +1,54 @@
import { Controller, Logger, Post, Body, Res } from "@nestjs/common"; import {
import { Response } from "express"; Controller,
import { HookRequest, HookResponse } from "./types"; Post,
import { UploadService } from "./upload.service"; UseInterceptors,
UploadedFile,
Body,
Param,
Get,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { UploadService } from './upload.service';
import { ChunkDto } from '@nice/common';
@Controller("upload") @Controller('upload')
export class UploadController { export class UploadController {
private readonly logger = new Logger(UploadController.name);
constructor(private readonly uploadService: UploadService) { } constructor(private readonly uploadService: UploadService) { }
@Post("hook")
async handleTusHook(@Body() hookRequest: HookRequest, @Res() res: Response) { @Post('chunk')
try { @UseInterceptors(FileInterceptor('file'))
const hookResponse = await this.uploadService.handleTusHook(hookRequest); async uploadChunk(
return res.status(200).json(hookResponse); @Body('chunk') chunkString: string, // 改为接收字符串
} catch (error: any) { @UploadedFile() file: Express.Multer.File,
this.logger.error(`Error handling hook: ${error.message}`); @Body('clientId') clientId: string
return res.status(500).json({ error: 'Internal server error' }); ) {
} const chunk = JSON.parse(chunkString); // 解析字符串为对象
await this.uploadService.uploadChunk(chunk, file, clientId);
return { message: 'Chunk uploaded successfully' };
}
@Get('status/:identifier')
checkUploadStatusInfo(@Param('identifier') identifier: string) {
const status = this.uploadService.checkUploadStatusInfo(identifier);
return status || { message: 'No upload status found' };
}
@Post('pause/:identifier')
pauseUpload(
@Param('identifier') identifier: string,
@Body('clientId') clientId: string
) {
this.uploadService.pauseUpload(identifier, clientId);
return { message: 'Upload paused successfully' };
}
@Post('resume/:identifier')
async resumeUpload(
@Param('identifier') identifier: string,
@Body('clientId') clientId: string
) {
const resumed = this.uploadService.resumeUpload(identifier, clientId);
if (!resumed) {
throw new Error('Unable to resume upload');
}
return { message: 'Upload resumed successfully' };
} }
} }

View File

@ -2,6 +2,9 @@ import { Module } from '@nestjs/common';
import { UploadController } from './upload.controller'; import { UploadController } from './upload.controller';
import { UploadService } from './upload.service'; import { UploadService } from './upload.service';
import { BullModule } from '@nestjs/bullmq'; import { BullModule } from '@nestjs/bullmq';
import { UploadLockService } from './upload-lock.service';
import { ChunkManager } from './chunk.manager';
@Module({ @Module({
imports: [ imports: [
BullModule.registerQueue({ BullModule.registerQueue({
@ -9,6 +12,6 @@ import { BullModule } from '@nestjs/bullmq';
}), }),
], ],
controllers: [UploadController], controllers: [UploadController],
providers: [UploadService], providers: [UploadService, UploadLockService, ChunkManager],
}) })
export class UploadModule { } export class UploadModule { }

View File

@ -1,109 +1,285 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Queue, Job } from 'bullmq'; // 添加 Job 导入 import { ChunkDto, UploadStatusInfo, UploadProgress, UploadStatusInfoDto } from '@nice/common';
import { InjectQueue } from '@nestjs/bullmq'; import * as fs from 'fs/promises';
import { db, Resource, ResourceType } from '@nicestack/common'; import * as path from 'path';
import { HookRequest, HookResponse } from './types'; import mitt from 'mitt';
import dayjs from 'dayjs'; import { ChunkManager } from './chunk.manager';
import { getFilenameWithoutExt } from '@server/utils/file'; import { UploadLockService } from './upload-lock.service';
import { QueueJobType } from '@server/queue/types'; import { calculateFileHash } from '@server/utils/file';
import { nanoid } from 'nanoid'; import { UploadEvent } from './types';
import { slugify } from 'transliteration';
import path from 'path';
import fs from 'node:fs';
@Injectable() @Injectable()
export class UploadService { export class UploadService {
private readonly logger = new Logger(UploadService.name); private readonly logger = new Logger(UploadService.name);
private readonly uploadDir: string;
private readonly tempDir: string;
private readonly fileStatuses: Map<string, UploadStatusInfo> = new Map();
private readonly emitter = mitt<UploadEvent>();
// Performance Optimization: Configurable upload parameters
private MAX_CONCURRENT_UPLOADS = 5; // Configurable concurrent uploads
private MAX_CHUNK_SIZE = 10 * 1024 * 1024; // 10MB max chunk size
private UPLOAD_TIMEOUT = 30 * 60 * 1000; // 30 minutes timeout
constructor( constructor(
@InjectQueue('file-queue') private fileQueue: Queue private chunkManager: ChunkManager,
) { } private uploadLockService: UploadLockService
) {
async handlePreCreateHook(hookRequest: HookRequest): Promise<HookResponse> { // Validate upload directory configuration
const hookResponse: HookResponse = { this.uploadDir = this.validateUploadDirectory();
HTTPResponse: { this.tempDir = path.join(this.uploadDir, 'temp');
Headers: {} this.initDirectories().catch(error => {
this.logger.error(`Failed to initialize upload directories: ${error.message}`);
process.exit(1);
});
this.configureUploadLimits();
} }
}; private validateUploadDirectory(): string {
const metaData = hookRequest.Event.Upload.MetaData; const uploadDir = process.env.UPLOAD_DIR;
const isValid = metaData && 'filename' in metaData; if (!uploadDir) {
if (!isValid) { throw new Error('UPLOAD_DIR environment variable is not set');
hookResponse.RejectUpload = true;
hookResponse.HTTPResponse.StatusCode = 400;
hookResponse.HTTPResponse.Body = 'no filename provided';
hookResponse.HTTPResponse.Headers['X-Some-Header'] = 'yes';
} else {
const timestamp = dayjs().format('YYMMDDHHmmss');
const originalName = metaData.filename;
const extension = path.extname(originalName); // 获取文件扩展名
// 清理并转换文件名(不包含扩展名)
const cleanName = slugify(getFilenameWithoutExt(originalName), {
lowercase: true,
separator: '-',
trim: true
})
.replace(/[^\w-]/g, '')
.replace(/-+/g, '-')
.substring(0, 32);
const uniqueId = nanoid(6);
const fileId = `${timestamp}_${cleanName}_${uniqueId}`;
fs.mkdirSync(path.join(process.env.UPLOAD_DIR, fileId), { recursive: true, mode: 0o755 })
// fs.chownSync(path.join(process.env.UPLOAD_DIR, fileId), 100999, 100999);
// await fs.promises.mkdir(path.join(process.env.UPLOAD_DIR, fileId), { recursive: true });
hookResponse.ChangeFileInfo = {
ID: `${fileId}/${fileId}${extension}`,
MetaData: {
filename: originalName,
normalized_name: cleanName,
folder: fileId
} }
}; return uploadDir;
}
return hookResponse;
} }
async handlePostFinishHook(hookRequest: HookRequest) { private handleUploadError(identifier: string, error: unknown): void {
const { ID, Size, Storage, MetaData } = hookRequest.Event.Upload; const status = this.fileStatuses.get(identifier);
const filename = MetaData?.filename; const errorMessage = error instanceof Error ? error.message : String(error);
if (status) {
const resource = await db.resource.create({ status.status = 'error';
data: { status.error = errorMessage;
filename,
fileId: ID,
title: getFilenameWithoutExt(filename), // 使用没有扩展名的标题
metadata: MetaData || {}
} }
}) this.logger.error(`Upload error for ${identifier}: ${errorMessage}`);
await this.addToProcessorPipe(resource) this.emitter.emit('uploadError', {
this.logger.log(`Upload ${ID} (${Size} bytes) is finished.`); identifier,
error: errorMessage,
filename: status?.filename
});
// Safe cleanup of temporary files
this.chunkManager.cleanup(identifier).catch(cleanupError =>
this.logger.error(`Cleanup failed for ${identifier}: ${cleanupError}`)
);
} }
async handleTusHook(hookRequest: HookRequest): Promise<HookResponse> { // Improved directory initialization with better error handling
const hookResponse: HookResponse = { private async initDirectories(): Promise<void> {
HTTPResponse: {
Headers: {}
}
};
try { try {
if (hookRequest.Type === 'pre-create') { await fs.mkdir(this.uploadDir, { recursive: true });
return this.handlePreCreateHook(hookRequest); await fs.mkdir(this.tempDir, { recursive: true });
} } catch (error) {
if (hookRequest.Type === 'post-finish') { this.logger.error(`Directory initialization failed: ${error}`);
this.handlePostFinishHook(hookRequest);
}
return hookResponse;
} catch (error: any) {
this.logger.error(`Error handling hook: ${error.message}`);
throw error; throw error;
} }
} }
async addToProcessorPipe(resource: Resource): Promise<Job> { // 修改返回类型为 Job private configureUploadLimits(): void {
const job = await this.fileQueue.add(QueueJobType.FILE_PROCESS, { const maxUploads = parseInt(process.env.MAX_CONCURRENT_UPLOADS || '5', 10);
resource, const maxChunkSize = parseInt(process.env.MAX_CHUNK_SIZE || '10485760', 10);
timestamp: Date.now() this.MAX_CONCURRENT_UPLOADS = maxUploads;
}, { this.MAX_CHUNK_SIZE = maxChunkSize;
attempts: 3, }
removeOnComplete: true, // Enhanced input validation
jobId: resource.id async uploadChunk(chunk: ChunkDto, file: Express.Multer.File, clientId: string): Promise<void> {
// Validate chunk size
if (chunk.currentChunkSize > this.MAX_CHUNK_SIZE) {
throw new Error(`Chunk size exceeds maximum limit of ${this.MAX_CHUNK_SIZE} bytes`);
}
// Rate limiting and concurrent upload control
await this.controlConcurrentUploads();
const { identifier } = chunk;
const lockAcquired = this.uploadLockService.acquireLock(identifier, clientId);
if (!lockAcquired) {
throw new Error('Concurrent upload limit reached');
}
try {
// Add timeout mechanism
const uploadPromise = this.processChunkUpload(chunk, file);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Upload timeout')), this.UPLOAD_TIMEOUT)
);
await Promise.race([uploadPromise, timeoutPromise]);
} catch (error) {
this.handleUploadError(identifier, error);
} finally {
this.uploadLockService.releaseLock(identifier, clientId);
}
}
private async controlConcurrentUploads(): Promise<void> {
const activeUploads = Array.from(this.fileStatuses.values())
.filter(status => status.status === 'uploading').length;
if (activeUploads >= this.MAX_CONCURRENT_UPLOADS) {
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait and retry
await this.controlConcurrentUploads();
}
}
private async processChunkUpload(chunk: ChunkDto, file: Express.Multer.File): Promise<void> {
const { identifier } = chunk;
if (!this.fileStatuses.has(identifier)) {
await this.initUploadStatusInfo(chunk);
}
const status = this.fileStatuses.get(identifier);
if (!status) {
throw new Error('File status initialization failed');
}
if (status.chunks.has(chunk.chunkNumber)) {
return;
}
await this.chunkManager.save(chunk, file);
this.updateProgress(chunk);
if (this.isUploadComplete(identifier)) {
await this.finalizeUpload(chunk);
}
}
private async initUploadStatusInfo(chunk: ChunkDto): Promise<void> {
const { identifier, filename, totalSize } = chunk;
// 获取已经上传的chunks
const uploadedChunks = await this.chunkManager.getChunks(identifier);
const uploadedSize = uploadedChunks.length * chunk.currentChunkSize;
this.emitter.emit('uploadStart', {
identifier,
filename,
totalSize,
resuming: uploadedChunks.length > 0
}); });
return job;
this.fileStatuses.set(identifier, {
identifier,
filename,
totalSize,
uploadedSize,
status: 'uploading',
chunks: new Set(uploadedChunks), // 初始化已上传的chunks
startTime: Date.now(),
lastUpdateTime: Date.now()
});
}
private updateProgress(chunk: ChunkDto): void {
const status = this.fileStatuses.get(chunk.identifier);
if (!status) return;
// Use more efficient progress calculation
const newUploadedSize = chunk.chunkNumber * chunk.currentChunkSize;
const progressPercentage = Math.min(
Math.round((newUploadedSize / status.totalSize) * 100),
100
);
status.chunks.add(chunk.chunkNumber);
status.uploadedSize = newUploadedSize;
status.lastUpdateTime = Date.now();
const progress: UploadProgress = {
identifier: chunk.identifier,
percentage: progressPercentage,
uploadedSize: newUploadedSize,
totalSize: status.totalSize,
speed: this.calculateSpeed(status),
remainingTime: this.calculateRemainingTime(status)
};
status.progress = progress
}
private calculateRemainingTime(status: UploadStatusInfo): number {
const speed = this.calculateSpeed(status);
if (speed === 0) return 0;
const remainingBytes = status.totalSize - status.uploadedSize;
return Math.ceil(remainingBytes / speed); // Returns seconds remaining
}
private calculateSpeed(status: UploadStatusInfo): number {
const duration = (status.lastUpdateTime - status.startTime) / 1000; // in seconds
return duration > 0 ? Math.round(status.uploadedSize / duration) : 0; // bytes per second
}
private async finalizeUpload(chunk: ChunkDto): Promise<void> {
const { identifier, filename, totalChunks, checksum } = chunk;
const chunkDir = path.join(this.tempDir, identifier);
const finalPath = path.join(this.uploadDir, filename);
try {
await this.chunkManager.merge(chunkDir, finalPath, totalChunks);
// Calculate file hash
const calculatedHash = await calculateFileHash(finalPath);
// Verify file integrity
if (checksum && calculatedHash !== checksum) {
throw new Error('File integrity check failed: Checksums do not match');
}
await this.chunkManager.cleanup(identifier);
const status = this.fileStatuses.get(identifier);
if (status) {
status.status = 'completed';
status.hash = calculatedHash;
}
this.emitter.emit('uploadComplete', {
identifier,
filename,
size: status.totalSize,
hash: calculatedHash,
integrityVerified: !checksum || calculatedHash === checksum
});
} catch (error) {
this.handleUploadError(identifier, error);
}
}
private isUploadComplete(identifier: string): boolean {
const status = this.fileStatuses.get(identifier);
if (!status) return false;
return status.uploadedSize === status.totalSize;
}
deleteUploadStatusInfo(identifier: string): boolean {
// Check if the file status exists
const status = this.fileStatuses.get(identifier);
if (!status) {
// If the status doesn't exist, return false
return false;
}
// Check if the upload is still in progress
if (status.status === 'uploading') {
this.logger.warn(`Attempting to delete file status for ongoing upload: ${identifier}`);
return false;
}
// Remove the file status from the map
const deleted = this.fileStatuses.delete(identifier);
if (deleted) {
this.logger.log(`File status deleted for identifier: ${identifier}`);
}
return deleted;
}
checkUploadStatusInfo(identifier: string): UploadStatusInfoDto {
const lockInfo = this.uploadLockService.checkLock(identifier);
const statusInfo = {
...lockInfo,
...this.fileStatuses.get(identifier)
};
return statusInfo || null
}
pauseUpload(identifier: string, clientId: string): void {
const status = this.fileStatuses.get(identifier);
if (status) {
status.status = 'paused';
this.uploadLockService.releaseLock(identifier, clientId);
}
}
resumeUpload(identifier: string, clientId: string): boolean {
const status = this.fileStatuses.get(identifier);
if (status) {
// Try to reacquire the lock
const lockAcquired = this.uploadLockService.acquireLock(identifier, clientId);
if (lockAcquired) {
status.status = 'uploading';
return true;
}
}
return false;
}
onUploadEvent<K extends keyof UploadEvent>(
event: K,
handler: (data: UploadEvent[K]) => void
): () => void {
this.emitter.on(event, handler);
return () => this.emitter.off(event, handler);
} }
} }

View File

@ -1,5 +1,5 @@
import mitt from 'mitt'; import mitt from 'mitt';
import { ObjectType, UserProfile, MessageDto } from '@nicestack/common'; import { ObjectType, UserProfile, MessageDto } from '@nice/common';
export enum CrudOperation { export enum CrudOperation {
CREATED, CREATED,
UPDATED, UPDATED,

View File

@ -1,7 +1,7 @@
import { createReadStream } from "fs"; import { createReadStream } from "fs";
import { createInterface } from "readline"; import { createInterface } from "readline";
import { db } from '@nicestack/common'; import { db } from '@nice/common';
import * as tus from "tus-js-client"; import * as tus from "tus-js-client";
import ExcelJS from 'exceljs'; import ExcelJS from 'exceljs';

View File

@ -0,0 +1,18 @@
import { PipeTransform, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) { }
transform(value: unknown) {
try {
const result = this.schema.parse(value);
return result;
} catch (error: any) {
throw new BadRequestException('Validation failed', {
cause: error,
description: error.errors
});
}
}
}

View File

@ -27,9 +27,9 @@
"@floating-ui/react": "^0.26.25", "@floating-ui/react": "^0.26.25",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@nicestack/client": "workspace:^", "@nice/client": "workspace:^",
"@nicestack/common": "workspace:^", "@nice/common": "workspace:^",
"@nicestack/iconer": "workspace:^", "@nice/iconer": "workspace:^",
"@tanstack/query-async-storage-persister": "^5.51.9", "@tanstack/query-async-storage-persister": "^5.51.9",
"@tanstack/react-query": "^5.51.21", "@tanstack/react-query": "^5.51.21",
"@tanstack/react-query-persist-client": "^5.51.9", "@tanstack/react-query-persist-client": "^5.51.9",
@ -58,7 +58,8 @@
"superjson": "^2.2.1", "superjson": "^2.2.1",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"yjs": "^13.6.20", "yjs": "^13.6.20",
"zod": "^3.23.8" "zod": "^3.23.8",
"uuid": "^10.0.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.9.0", "@eslint/js": "^9.9.0",

View File

@ -2,7 +2,7 @@ import {
AppConfigSlug, AppConfigSlug,
BaseSetting, BaseSetting,
RolePerms, RolePerms,
} from "@nicestack/common"; } from "@nice/common";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { import {
Button, Button,
@ -11,12 +11,12 @@ import {
message, message,
theme, theme,
} from "antd"; } from "antd";
import { useAppConfig } from "@nicestack/client"; import { useAppConfig } from "@nice/client";
import { useAuth } from "@web/src/providers/auth-provider"; import { useAuth } from "@web/src/providers/auth-provider";
import FixedHeader from "@web/src/components/layout/fix-header"; import FixedHeader from "@web/src/components/layout/fix-header";
import { useForm } from "antd/es/form/Form"; import { useForm } from "antd/es/form/Form";
import { api } from "@nicestack/client" import { api } from "@nice/client"
import { MainLayoutContext } from "../layout"; import { MainLayoutContext } from "../layout";
export default function BaseSettingPage() { export default function BaseSettingPage() {

View File

@ -10,10 +10,10 @@ import { theme } from "antd";
import ResizableSidebar from "@web/src/components/layout/resizable-sidebar"; import ResizableSidebar from "@web/src/components/layout/resizable-sidebar";
import SidebarContent from "@web/src/components/layout/sidebar-content"; import SidebarContent from "@web/src/components/layout/sidebar-content";
import UserHeader from "@web/src/components/layout/user-header"; import UserHeader from "@web/src/components/layout/user-header";
import { Icon } from "@nicestack/iconer"; import { Icon } from "@nice/iconer";
import { env } from "@web/src/env"; import { env } from "@web/src/env";
import RoundedClip from "@web/src/components/svg/rounded-clip"; import RoundedClip from "@web/src/components/svg/rounded-clip";
import {useTerm} from "@nicestack/client" import {useTerm} from "@nice/client"
export const MainLayoutContext = createContext<{ export const MainLayoutContext = createContext<{
pageWidth?: number; pageWidth?: number;

View File

@ -1,7 +1,7 @@
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
import { CourseList } from "@web/src/components/models/course/list/course-list"; import { CourseList } from "@web/src/components/models/course/list/course-list";
import { Button } from "@web/src/components/presentation/element/Button"; import { Button } from "@web/src/components/presentation/element/Button";
import { api } from "@nicestack/client"; import { api } from "@nice/client";
import { useState } from "react"; import { useState } from "react";
import { CourseCard } from "@web/src/components/models/course/card/CourseCard"; import { CourseCard } from "@web/src/components/models/course/card/CourseCard";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";

View File

@ -1,5 +1,5 @@
import { CourseList } from "@web/src/components/models/course/list/course-list"; import { CourseList } from "@web/src/components/models/course/list/course-list";
import { api } from "@nicestack/client"; import { api } from "@nice/client";
import { useState } from "react"; import { useState } from "react";
import { CourseCard } from "@web/src/components/models/course/card/CourseCard"; import { CourseCard } from "@web/src/components/models/course/card/CourseCard";
import { useAuth } from "@web/src/providers/auth-provider"; import { useAuth } from "@web/src/providers/auth-provider";

View File

@ -1,79 +1,82 @@
import React, { useCallback, useState } from 'react'; import React, { useState } from 'react';
import * as tus from 'tus-js-client'; import { useUpload } from '@nice/client'; // Assuming the previous hook is in this file
const UploadTest: React.FC = () => { const FileUploadComponent: React.FC = () => {
const [progress, setProgress] = useState<number>(0); const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [uploadStatus, setUploadStatus] = useState<string>('');
const handleFileSelect = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { const {
const file = event.target.files?.[0]; upload,
if (!file) return; pauseUpload,
resumeUpload,
// 创建 tus upload 实例 progress,
const upload = new tus.Upload(file, { errors
endpoint: 'http://localhost:8080/files', // 替换成你的 NestJS 服务器地址 } = useUpload({
retryDelays: [0, 3000, 5000], // Optional configuration
metadata: { baseUrl: "http://localhost:3000/upload",
filename: file.name, onProgress: (progressInfo) => {
filetype: file.type console.log('Upload progress:', progressInfo);
}, },
onError: (error) => { onError: (error) => {
console.error('上传失败:', error); console.error('Upload error:', error);
setUploadStatus('上传失败');
},
onProgress: (bytesUploaded, bytesTotal) => {
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
setProgress(Number(percentage));
setUploadStatus('上传中...');
},
onSuccess: () => {
setUploadStatus('上传成功!');
console.log('上传完成:', upload.url);
} }
}); });
// 开始上传 const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
upload.start(); if (event.target.files) {
}, []); setSelectedFiles(Array.from(event.target.files));
}
};
const handleUpload = async () => {
try {
await upload(selectedFiles);
alert('Upload completed successfully!');
} catch (error) {
console.error('Upload failed:', error);
}
};
const renderProgressBar = (fileName: string) => {
const fileProgress = progress[fileName];
if (!fileProgress) return null;
return ( return (
<div style={{ padding: '20px' }}>
<h2></h2>
<input
type="file"
onChange={handleFileSelect}
style={{ marginBottom: '20px' }}
/>
{progress > 0 && (
<div> <div>
<div>: {progress}%</div> <p>{fileName}</p>
<div style={{ <progress
width: '300px', value={fileProgress.percentage}
height: '20px', max="100"
border: '1px solid #ccc', />
marginTop: '10px' <span>{fileProgress.percentage.toFixed(2)}%</span>
}}> {errors[fileName] && (
<div style={{ <p style={{ color: 'red' }}>
width: `${progress}%`, Error: {errors[fileName].message}
height: '100%', </p>
backgroundColor: '#4CAF50',
transition: 'width 0.3s'
}} />
</div>
</div>
)}
{uploadStatus && (
<div style={{ marginTop: '10px' }}>
: {uploadStatus}
</div>
)} )}
</div> </div>
); );
}; };
return (
<div>
<input
type="file"
multiple
onChange={handleFileSelect}
/>
<button
onClick={handleUpload}
disabled={selectedFiles.length === 0}
>
Upload Files
</button>
export default function HomePage() { <div>
return <UploadTest></UploadTest> <h3>Upload Progress</h3>
} {selectedFiles.map(file => renderProgressBar(file.name))}
</div>
</div>
);
};
export default FileUploadComponent;

View File

@ -4,8 +4,8 @@ import React, { ReactNode, useEffect, useState, useRef, CSSProperties } from "re
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import Breadcrumb from "../layout/breadcrumb"; import Breadcrumb from "../layout/breadcrumb";
import * as Y from "yjs"; import * as Y from "yjs";
import { stringToColor, YWsProvider } from "@nicestack/common"; import { stringToColor, YWsProvider } from "@nice/common";
import { lightenColor } from "@nicestack/client" import { lightenColor } from "@nice/client"
import { useLocalSettings } from "@web/src/hooks/useLocalSetting"; import { useLocalSettings } from "@web/src/hooks/useLocalSetting";
interface FixedHeaderProps { interface FixedHeaderProps {
children?: ReactNode; children?: ReactNode;

View File

@ -1,4 +1,4 @@
import { NavItem } from '@nicestack/client'; import { NavItem } from '@nice/client';
import { import {
HomeIcon, HomeIcon,
BookOpenIcon, BookOpenIcon,

View File

@ -1,6 +1,6 @@
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { NavItem } from '@nicestack/client'; import { NavItem } from '@nice/client';
interface SidebarProps { interface SidebarProps {
navItems: Array<NavItem>; navItems: Array<NavItem>;

View File

@ -1,9 +1,9 @@
import { Avatar, Divider, Dropdown, theme } from "antd"; import { Avatar, Divider, Dropdown, theme } from "antd";
import { Icon } from "@nicestack/iconer"; import { Icon } from "@nice/iconer";
import CollapsibleSection from "../presentation/collapse-section"; import CollapsibleSection from "../presentation/collapse-section";
import { useAuth } from "@web/src/providers/auth-provider"; import { useAuth } from "@web/src/providers/auth-provider";
import { RolePerms } from "@nicestack/common"; import { RolePerms } from "@nice/common";
export default function SidebarContent() { export default function SidebarContent() {
const { logout, user, isAuthenticated, hasSomePermissions } = useAuth(); const { logout, user, isAuthenticated, hasSomePermissions } = useAuth();

View File

@ -1,6 +1,6 @@
import { Avatar, Button, Dropdown, theme } from "antd"; import { Avatar, Button, Dropdown, theme } from "antd";
import { useAuth } from "@web/src/providers/auth-provider"; import { useAuth } from "@web/src/providers/auth-provider";
import { Icon } from "@nicestack/iconer"; import { Icon } from "@nice/iconer";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export default function UserHeader() { export default function UserHeader() {

View File

@ -1,4 +1,4 @@
import { CourseDto } from '@nicestack/common'; import { CourseDto } from '@nice/common';
import { Card } from '@web/src/components/presentation/container/Card'; import { Card } from '@web/src/components/presentation/container/Card';
import { CourseHeader } from './CourseHeader'; import { CourseHeader } from './CourseHeader';
import { CourseStats } from './CourseStats'; import { CourseStats } from './CourseStats';

View File

@ -1,5 +1,5 @@
import { CheckCircleIcon } from '@heroicons/react/24/outline' import { CheckCircleIcon } from '@heroicons/react/24/outline'
import { Course } from "@nicestack/common" import { Course } from "@nice/common"
interface CourseDetailProps { interface CourseDetailProps {
course: Course course: Course
} }

Some files were not shown because too many files have changed in this diff Show More