From 7bfc4a0dee68653f0ebed364466d0ec0091e5308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E9=AD=8F=E6=B4=AA?= Date: Wed, 4 Mar 2026 13:16:46 +0800 Subject: [PATCH] fix: layer publish support code is http url --- __tests__/ut/commands/invoke_test.ts | 6 +++ __tests__/ut/utils/utils_test.ts | 6 +++ src/subCommands/deploy/impl/function.ts | 72 ++++++++----------------- src/subCommands/layer/index.ts | 32 ++++++++++- src/utils/index.ts | 45 +++++++++++++++- 5 files changed, 108 insertions(+), 53 deletions(-) diff --git a/__tests__/ut/commands/invoke_test.ts b/__tests__/ut/commands/invoke_test.ts index 13e00293..3eaa9f72 100644 --- a/__tests__/ut/commands/invoke_test.ts +++ b/__tests__/ut/commands/invoke_test.ts @@ -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 { diff --git a/__tests__/ut/utils/utils_test.ts b/__tests__/ut/utils/utils_test.ts index fbf26200..7a819fde 100644 --- a/__tests__/ut/utils/utils_test.ts +++ b/__tests__/ut/utils/utils_test.ts @@ -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); diff --git a/src/subCommands/deploy/impl/function.ts b/src/subCommands/deploy/impl/function.ts index 67ca76a4..340bc285 100644 --- a/src/subCommands/deploy/impl/function.ts +++ b/src/subCommands/deploy/impl/function.ts @@ -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'; @@ -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 { @@ -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); } @@ -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`); @@ -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 { - 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 挂载点才有) */ diff --git a/src/subCommands/layer/index.ts b/src/subCommands/layer/index.ts index 3372bee9..853ecaac 100644 --- a/src/subCommands/layer/index.ts +++ b/src/subCommands/layer/index.ts @@ -15,6 +15,7 @@ import { calculateCRC64, getFileSize, getUserAgent, + _downloadFromUrl, } from '../../utils'; import chalk from 'chalk'; @@ -28,6 +29,7 @@ export default class Layer { layerName: string, compatibleRuntimeList: string[], description: string, + downloadedTempFile = '', ): Promise { let zipPath = toZipDir; let generateZipFilePath = ''; @@ -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; } } @@ -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; } @@ -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, @@ -229,6 +258,7 @@ export default class Layer { layerName, compatibleRuntimeList, this.opts.description || '', + downloadedTempFile, ); } diff --git a/src/utils/index.ts b/src/utils/index.ts index ddce954c..bbf59102 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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'; @@ -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 '/'`); @@ -308,3 +311,41 @@ export function checkFcDir(path: string, paramName = 'path'): string { return normalizedPath; } + +/** + * 从URL下载文件到本地临时目录 + */ +export async function _downloadFromUrl(url: string): Promise { + // 创建临时目录 + 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}`); + } +}