Conversation
Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/87c9258c-7253-4653-93c6-c2cd61d95a53 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/87c9258c-7253-4653-93c6-c2cd61d95a53 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/84ab6763-84b1-4133-bb31-c5cca3632fc9 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/84ab6763-84b1-4133-bb31-c5cca3632fc9 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
| const hasControlCharacter = (text: string) => | ||
| [...text].some(character => { | ||
| const code = character.charCodeAt(0); | ||
|
|
||
| return code < 32 || code === 127; | ||
| }); | ||
| const MAX_UPLOAD_FILE_PATH_LENGTH = 512; | ||
|
|
||
| const sanitizePathInput = (inputPath: string) => { | ||
| if (inputPath.length > MAX_UPLOAD_FILE_PATH_LENGTH) | ||
| throw new UnprocessableEntityError(`Invalid file path: ${inputPath}`); | ||
|
|
||
| const normalizedPath = path.normalize(inputPath).replace(/^[\\/]+/g, ''); | ||
|
|
||
| if ( | ||
| !normalizedPath || | ||
| normalizedPath === '.' || | ||
| normalizedPath.startsWith('..') || | ||
| hasControlCharacter(normalizedPath) || | ||
| path.isAbsolute(normalizedPath) | ||
| ) | ||
| throw new UnprocessableEntityError(`Invalid file path: ${inputPath}`); | ||
|
|
||
| return normalizedPath; | ||
| }; | ||
|
|
||
| const resolveSafePath = (basePath: string, inputPath: string) => { | ||
| const normalizedPath = sanitizePathInput(inputPath); | ||
| const targetPath = path.resolve(basePath, normalizedPath); | ||
|
|
||
| if (targetPath !== basePath && !targetPath.startsWith(`${basePath}${path.sep}`)) | ||
| throw new UnprocessableEntityError(`Invalid file path: ${inputPath}`); | ||
|
|
||
| return targetPath; | ||
| }; | ||
|
|
||
| const normalizeRelativePath = (path: string) => sanitizePathInput(path); | ||
|
|
||
| const sanitizeGitError = (message: string, accessToken: string) => | ||
| message | ||
| .replaceAll(accessToken, '***') | ||
| .replaceAll(encodeURIComponent(accessToken), '***') | ||
| .replace(/\/\/[^/@\s]+@/g, '//***@') | ||
| .trim(); |
There was a problem hiding this comment.
这块主要是路径规范化与越界校验,当前保持内置 path + 框架错误类型,避免再引入额外依赖;上传/文件操作已改用 multer + zx(fs/path/os)。相关调整见提交 8d22364。
There was a problem hiding this comment.
这块主要是路径规范化与越界校验,当前保持内置 path + 框架错误类型,避免再引入额外依赖
那就放到工具函数的模块吧,类的模块尽量还是写类本身。
There was a problem hiding this comment.
已把路径校验和 Git 错误脱敏等工具函数移到独立模块 source/gitUploadUtility.ts,控制器里只保留接口流程。见提交 a933bce。
…t review Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/9469069a-ad0f-423f-b2b3-bd4bd9ec6f61 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
…apping Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/9469069a-ad0f-423f-b2b3-bd4bd9ec6f61 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/9469069a-ad0f-423f-b2b3-bd4bd9ec6f61 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/9469069a-ad0f-423f-b2b3-bd4bd9ec6f61 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/9469069a-ad0f-423f-b2b3-bd4bd9ec6f61 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
| const uploadMiddleware = multer({ | ||
| storage: multer.diskStorage({ | ||
| destination: os.tmpdir(), | ||
| filename: (_request, _file, callback) => | ||
| callback( | ||
| null, | ||
| `hop-upload-${Date.now()}-${randomUUID()}` | ||
| ) | ||
| }) | ||
| }); |
There was a problem hiding this comment.
这个库的默认存储行为不是每次上传都有相互隔离的临时文件夹吗?还是我记错了?
There was a problem hiding this comment.
你没记错,multer 在指定 dest 时会用默认磁盘存储并生成临时文件名;我这里已改成 multer({ dest: os.tmpdir() }),不再自定义文件名逻辑。见提交 a933bce。
| const hasControlCharacter = (text: string) => | ||
| [...text].some(character => { | ||
| const code = character.charCodeAt(0); | ||
|
|
||
| return code < 32 || code === 127; | ||
| }); | ||
| const MAX_UPLOAD_FILE_PATH_LENGTH = 512; | ||
|
|
||
| const sanitizePathInput = (inputPath: string) => { | ||
| if (inputPath.length > MAX_UPLOAD_FILE_PATH_LENGTH) | ||
| throw new UnprocessableEntityError(`Invalid file path: ${inputPath}`); | ||
|
|
||
| const normalizedPath = path.normalize(inputPath).replace(/^[\\/]+/g, ''); | ||
|
|
||
| if ( | ||
| !normalizedPath || | ||
| normalizedPath === '.' || | ||
| normalizedPath.startsWith('..') || | ||
| hasControlCharacter(normalizedPath) || | ||
| path.isAbsolute(normalizedPath) | ||
| ) | ||
| throw new UnprocessableEntityError(`Invalid file path: ${inputPath}`); | ||
|
|
||
| return normalizedPath; | ||
| }; | ||
|
|
||
| const resolveSafePath = (basePath: string, inputPath: string) => { | ||
| const normalizedPath = sanitizePathInput(inputPath); | ||
| const targetPath = path.resolve(basePath, normalizedPath); | ||
|
|
||
| if (targetPath !== basePath && !targetPath.startsWith(`${basePath}${path.sep}`)) | ||
| throw new UnprocessableEntityError(`Invalid file path: ${inputPath}`); | ||
|
|
||
| return targetPath; | ||
| }; | ||
|
|
||
| const normalizeRelativePath = (path: string) => sanitizePathInput(path); | ||
|
|
||
| const sanitizeGitError = (message: string, accessToken: string) => | ||
| message | ||
| .replaceAll(accessToken, '***') | ||
| .replaceAll(encodeURIComponent(accessToken), '***') | ||
| .replace(/\/\/[^/@\s]+@/g, '//***@') | ||
| .trim(); |
There was a problem hiding this comment.
这块主要是路径规范化与越界校验,当前保持内置 path + 框架错误类型,避免再引入额外依赖
那就放到工具函数的模块吧,类的模块尽量还是写类本身。
…atest review Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/16ee324b-8aa4-4da5-baf1-3c0866c858e0 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/16ee324b-8aa4-4da5-baf1-3c0866c858e0 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/16ee324b-8aa4-4da5-baf1-3c0866c858e0 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
该 PR 旨在支持前端通过 FormData 批量上传多文件并推送到 Git 仓库:新增 /file/Git/:noProtocolURL(.*) 上传入口;同时扩展 OAuth 凭据模型以保存用于 Git HTTP URL 的第三方 username,并新增按仓库 host 自动解析 OAuth 平台的能力;容器层面补齐 git 运行依赖并引入 zx 以执行上传命令。
Changes:
- 新增 Git 多文件上传接口(multipart + 字段名作为仓库内路径),并在服务端整理文件后执行
xgit upload推送 - OAuthCredential 增加
username字段,并在 OAuth 登录同步时写入(配合 host->platform 解析) - Docker 镜像安装
git;新增依赖zx与@types/multer
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| source/controller/File.ts | 新增 /file/Git/:noProtocolURL(.*) 上传接口、解析文件并调用 xgit upload 推送 |
| source/gitUploadUtility.ts | 新增路径校验/安全拼接与 Git 错误脱敏工具函数 |
| source/model/OAuth.ts | 增加 OAuth 平台 host 映射与 OAuthCredential.username 字段 |
| source/controller/OAuth.ts | OAuth 登录同步逻辑调整:保存 accessToken + username,并更新 profile 处理 |
| package.json | 增加运行时依赖 zx 与开发依赖 @types/multer |
| pnpm-lock.yaml | 锁文件更新以纳入新增依赖 |
| Dockerfile | 基础镜像安装 git 以支持运行期执行 Git 命令 |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
source/controller/OAuth.ts:60
oldProfileincludesnamebutnewProfileintentionally omits it (it’s destructured out). This makesisDeepStrictEqual(oldProfile, newProfile)always false, so every OAuth login will trigger a User save +activityLogService.logUpdate, even when nothing changed. Either includenameinnewProfile(if you still want to sync it) or remove it fromoldProfile/comparison so the equality check is meaningful.
const { token: accessToken, name, ...newProfileData } = profile;
if (!accessToken)
throw new UnprocessableEntityError(
`${platform} user info is missing required field (accessToken)`
);
if (!name)
throw new UnprocessableEntityError(
`${platform} user info is missing required field (username)`
);
const user =
(await this.userStore.findOneBy({ email })) ||
(await sessionService.signUp({ email, password: accessToken }));
const { name: oldName, avatar, languages } = user;
const oldProfile = { name: oldName, avatar, languages: languages?.length ? languages : [] };
const newProfile = {
...newProfileData,
languages: newProfileData.languages?.length ? newProfileData.languages : []
};
if (!isDeepStrictEqual(oldProfile, newProfile)) {
await this.userStore.save(Object.assign(user, newProfile));
await activityLogService.logUpdate(user, 'User', user.id);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @IsString() | ||
| @Column() | ||
| username: string; |
| @Req() { files }: { files?: Express.Multer.File[] }, | ||
| @CurrentUser() user: User, | ||
| @Param('noProtocolURL') noProtocolURL: string, | ||
| @QueryParam('branch') branch = 'main', | ||
| @QueryParam('folder') folder?: string | ||
| ) { |
There was a problem hiding this comment.
On Koa,
@koa/multerattaches parsed uploads to the underlying Node request (ctx.req.files), while@Req()in routing-controllers returns the Koarequestobject. Destructuring{ files }from@Req()is likely to always beundefined, causing this endpoint to reject valid uploads. Prefer@Ctx() ctxand readctx.req.files, or accept the full request object and accessrequest.req.filesexplicitly.
@copilot 我们是不是应该用 Koa 的 @Ctx()?
There was a problem hiding this comment.
是的,这里改用 Koa 的 @Ctx() 更稳妥。我已改为从 @Ctx() { req } 读取 req.files,避免 @Req() 取不到上传文件的问题。见提交 a09f609。此改动仅后端接口逻辑,无 UI 变更截图。
| repositoryURL.username = credential.username; | ||
| repositoryURL.password = credential.accessToken; | ||
|
|
||
| const targetFolder = folder ? normalizeRelativePath(folder) : undefined; | ||
| const tempRoot = await fs.mkdtemp(path.resolve(os.tmpdir(), 'hop-git-upload-')); | ||
| const pendingUploadedPaths = new Set(files.map(({ path }) => path)); | ||
| const originalVerbose = Boolean($.verbose); | ||
|
|
||
| try { | ||
| $.verbose = true; | ||
| for (const file of files) { |
| if (targetFolder) | ||
| await $`npx xgit upload ${tempRoot} ${repositoryURL} ${branch} ${targetFolder}`; | ||
| else | ||
| await $`npx xgit upload ${tempRoot} ${repositoryURL} ${branch}`; |
| @@ -44,4 +64,79 @@ export class FileController { | |||
|
|
|||
| await s3Client.send(command); | |||
| } | |||
|
|
|||
| @Put('/Git/:noProtocolURL(.*)') | |||
| @Authorized() | |||
| @HttpCode(201) | |||
| @UseBefore(uploadMiddleware.any()) | |||
| async uploadFilesToGit( | |||
| export const sanitizePathInput = (inputPath: string) => { | ||
| if (inputPath.length > MAX_UPLOAD_FILE_PATH_LENGTH) | ||
| throw new UnprocessableEntityError(`Invalid file path: ${inputPath}`); | ||
|
|
Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP-service/sessions/8e3fd298-ee08-4c33-9c4d-6b79236b1a51 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
为支持前端以
FormData批量上传代码文件到 Git 仓库,新增了/file/Git/*noProtocolURL上传链路,并补齐了从上传 URL 自动匹配平台凭据所需的数据与映射能力。同时补充容器运行环境依赖,确保服务内可执行 Git 上传命令。
接口能力:新增
/file/Git/:noProtocolURL(.*)PUT 上传入口FileController增加 Git 上传接口,接收 multipart FormData。multer.any()+@Ctx()读取ctx.req.files,并以file.fieldname作为仓库内目标路径。dest: os.tmpdir()),降低大批量上传时的内存占用。source/gitUploadUtility.ts,控制器聚焦接口流程。zx调用npx xgit upload推送到目标仓库。branch(默认main)和可选folder参数。../ 绝对路径穿越、控制字符、超长路径)与错误脱敏。BadRequestError、UnprocessableEntityError、ForbiddenError、InternalServerError)。OAuth 凭据模型:补充第三方用户名
OAuthCredential新增username字段,并改为必填,用于与accessToken组合构建 HTTP Git URL。login,CNB 使用username)。syncProfile参数复用现有 profile 字段,语义调整为:profile.token传accessToken,profile.name传第三方用户名。平台映射:新增平台名与主域名映射能力
OAuthPlatformHostMap与resolveOAuthPlatformByHost()。noProtocolURL的 host 自动解析平台,并查找当前用户对应 OAuth 凭据。运行环境与依赖
git,满足xgit执行依赖。zx(用于执行npx xgit与文件系统/路径操作)。@types/multer(补齐上传文件类型声明)。补充示例(上传调用形态)