Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions __tests__/ut/commands/invoke_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import logger from '../../../src/logger';
import { IInputs } from '../../../src/interface';
import fs from 'fs';

// Mock @serverless-devs/downloads module to prevent import errors
jest.mock('@serverless-devs/downloads', () => ({
__esModule: true,
default: jest.fn(),
}));

// Mock dependencies
jest.mock('../../../src/resources/fc', () => {
return {
Expand Down
6 changes: 6 additions & 0 deletions __tests__/ut/utils/utils_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import log from '../../../src/logger';
import { execSync } from 'child_process';
log._set(console);

// Mock @serverless-devs/downloads module to prevent import errors
jest.mock('@serverless-devs/downloads', () => ({
__esModule: true,
default: jest.fn(),
}));

describe('isAuto', () => {
test('should return true if config is string "AUTO" or "auto"', () => {
expect(isAuto('AUTO')).toBe(true);
Expand Down
72 changes: 22 additions & 50 deletions src/subCommands/deploy/impl/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import _ from 'lodash';
import { diffConvertYaml } from '@serverless-devs/diff';
import inquirer from 'inquirer';
import fs from 'fs';
import os from 'os';
import assert from 'assert';
import path from 'path';
import { yellow } from 'chalk';
Expand All @@ -19,10 +18,15 @@ import FC, { GetApiType } from '../../../resources/fc';
import VPC_NAS from '../../../resources/vpc-nas';
import Base from './base';
import { ICredentials } from '@serverless-devs/component-interface';
import { calculateCRC64, getFileSize, parseAutoConfig, checkFcDir } from '../../../utils';
import {
calculateCRC64,
getFileSize,
parseAutoConfig,
checkFcDir,
_downloadFromUrl,
} from '../../../utils';
import OSS from '../../../resources/oss';
import { setNodeModulesBinPermissions } from '../../../resources/fc/impl/utils';
import downloads from '@serverless-devs/downloads';

type IType = 'code' | 'config' | boolean;
interface IOpts {
Expand Down Expand Up @@ -280,11 +284,11 @@ export default class Service extends Base {
}

let zipPath: string;
let downloadedTempDir = '';
let downloadedTempFile = '';
// 处理不同类型的 codeUri
if (codeUri.startsWith('http://') || codeUri.startsWith('https://')) {
zipPath = await this._downloadFromUrl(codeUri);
downloadedTempDir = path.dirname(zipPath);
zipPath = await _downloadFromUrl(codeUri);
downloadedTempFile = zipPath;
} else {
zipPath = path.isAbsolute(codeUri) ? codeUri : path.join(this.inputs.baseDir, codeUri);
}
Expand Down Expand Up @@ -321,6 +325,14 @@ export default class Service extends Base {
logger.debug(
yellow(`skip uploadCode because code is no changed, codeChecksum=${crc64Value}`),
);
if (downloadedTempFile) {
try {
logger.debug(`Removing temp download dir: ${downloadedTempFile}`);
fs.rmSync(downloadedTempFile, { recursive: true, force: true });
} catch (ex) {
logger.debug(`Unable to remove temp download dir: ${downloadedTempFile}`);
}
}
return false;
} else {
logger.debug(`\x1b[33mcodeChecksum from ${this.codeChecksum} to ${crc64Value}\x1b[0m`);
Expand All @@ -338,58 +350,18 @@ export default class Service extends Base {
}
}

if (downloadedTempDir) {
if (downloadedTempFile) {
try {
logger.debug(`Removing temp download dir: ${downloadedTempDir}`);
fs.rmSync(downloadedTempDir, { recursive: true, force: true });
logger.debug(`Removing temp download dir: ${downloadedTempFile}`);
fs.rmSync(downloadedTempFile, { recursive: true, force: true });
} catch (ex) {
logger.debug(`Unable to remove temp download dir: ${downloadedTempDir}`);
logger.debug(`Unable to remove temp download dir: ${downloadedTempFile}`);
}
}

return true;
}

/**
* 从URL下载文件到本地临时目录
*/
private async _downloadFromUrl(url: string): Promise<string> {
logger.info(`Downloading code from URL: ${url}`);

// 创建临时目录
const tempDir = path.join(os.tmpdir(), 'fc_code_download');
let downloadPath: string;

try {
// 从URL获取文件名
const urlPath = new URL(url).pathname;
const parsedPathName = path.parse(urlPath).name;
const filename = path.basename(urlPath) || `downloaded_code_${Date.now()}`;
downloadPath = path.join(tempDir, filename);

await downloads(url, {
dest: tempDir,
filename: parsedPathName,
extract: false,
});

logger.debug(`Downloaded file to: ${downloadPath}`);

// 返回下载文件路径,由主流程决定是否需要压缩
return downloadPath;
} catch (error) {
// 如果下载失败,清理临时目录
try {
fs.rmSync(tempDir, { recursive: true, force: true });
logger.debug(`Cleaned up temporary directory after error: ${tempDir}`);
} catch (cleanupError) {
logger.debug(`Failed to clean up temporary directory: ${cleanupError.message}`);
}

throw new Error(`Failed to download code from URL: ${error.message}`);
}
}

/**
* 生成 auto 资源,非 FC 资源,主要指 vpc、nas、log、role(oss mount 挂载点才有)
*/
Expand Down
32 changes: 31 additions & 1 deletion src/subCommands/layer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
calculateCRC64,
getFileSize,
getUserAgent,
_downloadFromUrl,
} from '../../utils';
import chalk from 'chalk';

Expand All @@ -28,6 +29,7 @@ export default class Layer {
layerName: string,
compatibleRuntimeList: string[],
description: string,
downloadedTempFile = '',
): Promise<any> {
let zipPath = toZipDir;
let generateZipFilePath = '';
Expand All @@ -53,6 +55,14 @@ export default class Layer {
`Skip uploadCode because code is no changed, codeChecksum=${crc64Value}; Laster layerArn=${latestLayer.layerVersionArn}`,
),
);
if (downloadedTempFile) {
try {
logger.debug(`Removing temp download dir: ${downloadedTempFile}`);
fs.rmSync(downloadedTempFile, { recursive: true, force: true });
} catch (ex) {
logger.debug(`Unable to remove temp download dir: ${downloadedTempFile}`);
}
}
return latestLayer;
}
}
Expand All @@ -76,6 +86,16 @@ export default class Layer {
logger.debug(`Unable to remove zip file: ${zipPath}`);
}
}

if (downloadedTempFile) {
try {
logger.debug(`Removing temp download file: ${downloadedTempFile}`);
fs.rmSync(downloadedTempFile, { recursive: true, force: true });
} catch (ex) {
logger.debug(`Unable to remove temp download file: ${downloadedTempFile}`);
}
}

console.log(JSON.stringify(result));
return result;
}
Expand Down Expand Up @@ -220,7 +240,16 @@ export default class Layer {
);
}

const toZipDir: string = path.isAbsolute(codeUri) ? codeUri : path.join(this.baseDir, codeUri);
let toZipDir: string;
let downloadedTempFile = '';
// 处理不同类型的 codeUri
if (codeUri.startsWith('http://') || codeUri.startsWith('https://')) {
toZipDir = await _downloadFromUrl(codeUri);
downloadedTempFile = toZipDir;
} else {
toZipDir = path.isAbsolute(codeUri) ? codeUri : path.join(this.baseDir, codeUri);
}

const compatibleRuntimeList = compatibleRuntime.split(',');
return Layer.safe_publish_layer(
this.fcSdk,
Expand All @@ -229,6 +258,7 @@ export default class Layer {
layerName,
compatibleRuntimeList,
this.opts.description || '',
downloadedTempFile,
);
}

Expand Down
45 changes: 43 additions & 2 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import Table from 'tty-table';
import * as crc64 from 'crc64-ecma182.js';
import { promisify } from 'util';
import * as fs from 'fs';
import os from 'os';
import logger from '../logger';
import { execSync } from 'child_process';
import axios from 'axios';
import { FC_API_ERROR_CODE, isInvalidArgument } from '../resources/fc/error-code';
import path from 'path';
import downloads from '@serverless-devs/downloads';

export { default as verify } from './verify';
export { default as runCommand } from './run-command';
Expand Down Expand Up @@ -261,8 +264,8 @@ export function getUserAgent(userAgent: string, command: string) {
/**
* 验证并规范化路径
*/
export function checkFcDir(path: string, paramName = 'path'): string {
const normalizedPath = path.trim();
export function checkFcDir(inputPath: string, paramName = 'path'): string {
const normalizedPath = inputPath.trim();

if (!normalizedPath.startsWith('/')) {
throw new Error(`${paramName} does not start with '/'`);
Expand Down Expand Up @@ -308,3 +311,41 @@ export function checkFcDir(path: string, paramName = 'path'): string {

return normalizedPath;
}

/**
* 从URL下载文件到本地临时目录
*/
export async function _downloadFromUrl(url: string): Promise<string> {
// 创建临时目录
const tempDir = path.join(os.tmpdir(), 'fc_code_download');
let downloadPath: string;

try {
// 从URL获取文件名
const urlPath = new URL(url).pathname;
const parsedPathName = path.parse(urlPath).name;
const filename = path.basename(urlPath) || `downloaded_code_${Date.now()}`;
downloadPath = path.join(tempDir, filename);

await downloads(url, {
dest: tempDir,
filename: parsedPathName,
extract: false,
Comment on lines +326 to +333
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

git ls-files | head -20

Repository: devsapp/fc3

Length of output: 468


🏁 Script executed:

fd -t f "index.ts" | grep -E "(utils|layer|deploy)" | head -20

Repository: devsapp/fc3

Length of output: 211


🏁 Script executed:

cat -n src/utils/index.ts | sed -n '320,340p'

Repository: devsapp/fc3

Length of output: 786


🏁 Script executed:

cat -n src/utils/index.ts | sed -n '300,350p'

Repository: devsapp/fc3

Length of output: 1906


🏁 Script executed:

cat -n src/subCommands/layer/index.ts | sed -n '240,255p'

Repository: devsapp/fc3

Length of output: 653


🏁 Script executed:

cat -n src/subCommands/deploy/impl/function.ts | sed -n '283,297p'

Repository: devsapp/fc3

Length of output: 647


🏁 Script executed:

cat package.json | grep -A 5 -B 5 "downloads"

Repository: devsapp/fc3

Length of output: 562


🏁 Script executed:

rg "from.*downloads|require.*downloads" -A 2 -B 2

Repository: devsapp/fc3

Length of output: 2179


🏁 Script executed:

rg "downloadPath" src/utils/index.ts -B 3 -A 3

Repository: devsapp/fc3

Length of output: 724


🏁 Script executed:

#!/bin/bash

# Check the main file structure around the lines mentioned
echo "=== src/utils/index.ts lines 320-350 ==="
head -350 src/utils/index.ts | tail -35 | cat -n

echo ""
echo "=== Package.json to find downloads version ==="
grep -A 10 '"dependencies"' package.json | grep downloads

echo ""
echo "=== Check layer/index.ts around line 247 ==="
sed -n '240,255p' src/subCommands/layer/index.ts | cat -n

echo ""
echo "=== Check function.ts around line 290 ==="
sed -n '283,300p' src/subCommands/deploy/impl/function.ts | cat -n

Repository: devsapp/fc3

Length of output: 2878


🌐 Web query:

@serverless-devs/downloads v0.0.7 filename option behavior documentation

💡 Result:

In @serverless-devs/downloads v0.0.7, the options.filename parameter controls the name of the file written to disk (“文件保存的名称” / file save name). It’s optional, and defaults to demo.zip. If you don’t set it, the downloaded content will be saved using that default name (in the directory indicated by options.dest, if provided). [1]

Source(s):

  • [1] Socket.dev package README for @serverless-devs/downloads (v0.0.7): options table (filename, default demo.zip).

Use the filename option with the full filename including extension.

Line 326 strips the extension from the filename (path.parse(...).name), but Line 332 passes this stripped name to the downloads() function. This creates a mismatch: downloadPath (Line 328) is calculated with the full filename including extension, but the actual downloaded file is saved without the extension. The function then returns a path that doesn't match the actual file location. This causes downstream issues in src/subCommands/layer/index.ts (line 247) and src/subCommands/deploy/impl/function.ts (line 290) when processing the returned path.

Suggested fix
-    const parsedPathName = path.parse(urlPath).name;
-    const filename = path.basename(urlPath) || `downloaded_code_${Date.now()}`;
+    const filename = path.basename(urlPath) || `downloaded_code_${Date.now()}.zip`;
     downloadPath = path.join(tempDir, filename);

     await downloads(url, {
       dest: tempDir,
-      filename: parsedPathName,
+      filename,
       extract: false,
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const parsedPathName = path.parse(urlPath).name;
const filename = path.basename(urlPath) || `downloaded_code_${Date.now()}`;
downloadPath = path.join(tempDir, filename);
await downloads(url, {
dest: tempDir,
filename: parsedPathName,
extract: false,
const filename = path.basename(urlPath) || `downloaded_code_${Date.now()}.zip`;
downloadPath = path.join(tempDir, filename);
await downloads(url, {
dest: tempDir,
filename,
extract: false,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/index.ts` around lines 326 - 333, The download filename sent to
downloads() is missing the extension (parsedPathName uses path.parse(...).name)
causing downloadPath to mismatch the actual file; fix by using the full basename
(use path.parse(...).base or path.basename(urlPath)) when constructing the name
passed to downloads() and ensure downloadPath is built using that same full
filename variable (update parsedPathName/filename usage so downloads(url, {
filename: fullFilename }) and downloadPath = path.join(tempDir, fullFilename)
are consistent).

});

logger.debug(`Downloaded file to: ${downloadPath}`);

// 返回下载文件路径,由主流程决定是否需要压缩
return downloadPath;
} catch (error) {
// 如果下载失败,清理临时目录
try {
fs.rmSync(tempDir, { recursive: true, force: true });
logger.debug(`Cleaned up temporary directory after error: ${tempDir}`);
} catch (cleanupError) {
logger.debug(`Failed to clean up temporary directory: ${cleanupError.message}`);
}

throw new Error(`Failed to download code from URL: ${error.message}`);
}
}
Loading