Fix app install file upload (#18593)
remove wrong file path based file selection --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
This commit is contained in:
+50
-57
@@ -2,8 +2,9 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import { join, relative } from 'path';
|
||||
import { resolve } from 'path';
|
||||
|
||||
import { Manifest } from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -24,23 +25,6 @@ import { ApplicationSyncService } from 'src/engine/core-modules/application/appl
|
||||
import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
|
||||
const FILE_FOLDER_MAPPING: Record<string, FileFolder> = {
|
||||
'package.json': FileFolder.Dependencies,
|
||||
'yarn.lock': FileFolder.Dependencies,
|
||||
};
|
||||
|
||||
const FILE_FOLDER_PATTERN_MAPPING: Array<{
|
||||
pattern: RegExp;
|
||||
folder: FileFolder;
|
||||
}> = [
|
||||
{ pattern: /\.function\.mjs$/, folder: FileFolder.BuiltLogicFunction },
|
||||
{
|
||||
pattern: /\.front-component\.mjs$/,
|
||||
folder: FileFolder.BuiltFrontComponent,
|
||||
},
|
||||
{ pattern: /^public\//, folder: FileFolder.PublicAsset },
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationInstallService {
|
||||
private readonly logger = new Logger(ApplicationInstallService.name);
|
||||
@@ -123,6 +107,7 @@ export class ApplicationInstallService {
|
||||
|
||||
await this.writeFilesToStorage(
|
||||
resolvedPackage.extractedDir,
|
||||
resolvedPackage.manifest,
|
||||
universalIdentifier,
|
||||
params.workspaceId,
|
||||
);
|
||||
@@ -155,15 +140,32 @@ export class ApplicationInstallService {
|
||||
|
||||
private async writeFilesToStorage(
|
||||
extractedDir: string,
|
||||
manifest: Manifest,
|
||||
applicationUniversalIdentifier: string,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const files = await this.collectFiles(extractedDir);
|
||||
const filesToWrite = this.buildFileList(manifest);
|
||||
|
||||
for (const filePath of files) {
|
||||
const relativePath = relative(extractedDir, filePath);
|
||||
const fileFolder = this.resolveFileFolder(relativePath);
|
||||
const content = await fs.readFile(filePath);
|
||||
for (const { relativePath, fileFolder } of filesToWrite) {
|
||||
const absolutePath = resolve(extractedDir, relativePath);
|
||||
|
||||
if (!absolutePath.startsWith(extractedDir)) {
|
||||
throw new ApplicationException(
|
||||
`Path traversal detected for file: ${relativePath}`,
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
let content: Buffer;
|
||||
|
||||
try {
|
||||
content = await fs.readFile(absolutePath);
|
||||
} catch {
|
||||
throw new ApplicationException(
|
||||
`File not found in package: ${relativePath}`,
|
||||
ApplicationExceptionCode.PACKAGE_RESOLUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
await this.fileStorageService.writeFile({
|
||||
sourceFile: content,
|
||||
@@ -177,47 +179,38 @@ export class ApplicationInstallService {
|
||||
}
|
||||
}
|
||||
|
||||
private resolveFileFolder(relativePath: string): FileFolder {
|
||||
const exact = FILE_FOLDER_MAPPING[relativePath];
|
||||
private buildFileList(
|
||||
manifest: Manifest,
|
||||
): Array<{ relativePath: string; fileFolder: FileFolder }> {
|
||||
const files: Array<{ relativePath: string; fileFolder: FileFolder }> = [];
|
||||
|
||||
if (isDefined(exact)) {
|
||||
return exact;
|
||||
files.push(
|
||||
{ relativePath: 'package.json', fileFolder: FileFolder.Dependencies },
|
||||
{ relativePath: 'manifest.json', fileFolder: FileFolder.Source },
|
||||
);
|
||||
|
||||
for (const logicFunction of manifest.logicFunctions ?? []) {
|
||||
files.push({
|
||||
relativePath: logicFunction.builtHandlerPath,
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
});
|
||||
}
|
||||
|
||||
for (const { pattern, folder } of FILE_FOLDER_PATTERN_MAPPING) {
|
||||
if (pattern.test(relativePath)) {
|
||||
return folder;
|
||||
}
|
||||
for (const frontComponent of manifest.frontComponents ?? []) {
|
||||
files.push({
|
||||
relativePath: frontComponent.builtComponentPath,
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
});
|
||||
}
|
||||
|
||||
return FileFolder.Source;
|
||||
}
|
||||
|
||||
private async collectFiles(dir: string): Promise<string[]> {
|
||||
const result: string[] = [];
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
|
||||
if (entry.name === 'node_modules' || entry.name === '.yarn') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
const subFiles = await this.collectFiles(fullPath);
|
||||
|
||||
result.push(...subFiles);
|
||||
} else {
|
||||
result.push(fullPath);
|
||||
}
|
||||
for (const publicAsset of manifest.publicAssets ?? []) {
|
||||
files.push({
|
||||
relativePath: publicAsset.filePath,
|
||||
fileFolder: FileFolder.PublicAsset,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
return files;
|
||||
}
|
||||
|
||||
private async ensureApplicationExists(params: {
|
||||
|
||||
+41
-10
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import crypto from 'crypto';
|
||||
import { join } from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -239,7 +240,16 @@ export class LogicFunctionResourceService {
|
||||
workspaceId: string;
|
||||
inMemoryFolderPath: string;
|
||||
}) {
|
||||
await Promise.all([
|
||||
const yarnLockExists = await this.fileStorageService.checkFileExists({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
resourcePath: 'yarn.lock',
|
||||
});
|
||||
|
||||
const promises = [];
|
||||
|
||||
promises.push(
|
||||
this.fileStorageService.downloadFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
@@ -247,14 +257,35 @@ export class LogicFunctionResourceService {
|
||||
resourcePath: 'package.json',
|
||||
localPath: join(inMemoryFolderPath, 'package.json'),
|
||||
}),
|
||||
this.fileStorageService.downloadFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
resourcePath: 'yarn.lock',
|
||||
localPath: join(inMemoryFolderPath, 'yarn.lock'),
|
||||
}),
|
||||
]);
|
||||
);
|
||||
|
||||
if (yarnLockExists) {
|
||||
promises.push(
|
||||
this.fileStorageService.downloadFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
resourcePath: 'yarn.lock',
|
||||
localPath: join(inMemoryFolderPath, 'yarn.lock'),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const yarnLockPath = join(inMemoryFolderPath, 'yarn.lock');
|
||||
|
||||
promises.push(
|
||||
fs.mkdir(dirname(yarnLockPath), { recursive: true }).then(() =>
|
||||
fs.writeFile(
|
||||
yarnLockPath,
|
||||
`# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
`,
|
||||
'utf-8',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
async getBuiltCode({
|
||||
|
||||
Reference in New Issue
Block a user