ai-yuca 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +25 -0
- package/CONFIG_UPLOAD.md +154 -0
- package/CONTRIBUTING.md +58 -0
- package/INSTALLATION.md +192 -0
- package/README.md +80 -0
- package/bin/cli.js +85 -0
- package/bin/cli.ts +302 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +297 -0
- package/dist/package.json +51 -0
- package/dist/src/config.d.ts +56 -0
- package/dist/src/config.js +101 -0
- package/dist/src/download.d.ts +30 -0
- package/dist/src/download.js +214 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +126 -0
- package/dist/src/types/analyze.d.ts +33 -0
- package/dist/src/types/analyze.js +5 -0
- package/dist/src/types/download.d.ts +60 -0
- package/dist/src/types/download.js +2 -0
- package/dist/src/types/index.d.ts +8 -0
- package/dist/src/types/index.js +28 -0
- package/dist/src/types/upload.d.ts +89 -0
- package/dist/src/types/upload.js +2 -0
- package/dist/src/upload.d.ts +24 -0
- package/dist/src/upload.js +252 -0
- package/dist/src/uploadWithConfig.d.ts +34 -0
- package/dist/src/uploadWithConfig.js +82 -0
- package/dist/src/utils/compression.d.ts +16 -0
- package/dist/src/utils/compression.js +85 -0
- package/dist/test/compression.test.d.ts +1 -0
- package/dist/test/compression.test.js +109 -0
- package/dist/test/download.test.d.ts +1 -0
- package/dist/test/download.test.js +168 -0
- package/dist/test/index.test.d.ts +1 -0
- package/dist/test/index.test.js +33 -0
- package/dist/test/upload.test.d.ts +1 -0
- package/dist/test/upload.test.js +140 -0
- package/docs/usage.md +223 -0
- package/examples/sample.txt +7 -0
- package/examples/upload-example.js +53 -0
- package/out/test.txt +1 -0
- package/package.json +51 -0
- package/src/config.ts +104 -0
- package/src/download.ts +216 -0
- package/src/index.js +88 -0
- package/src/index.ts +98 -0
- package/src/types/analyze.ts +37 -0
- package/src/types/download.ts +67 -0
- package/src/types/index.ts +16 -0
- package/src/types/upload.ts +97 -0
- package/src/upload.js +197 -0
- package/src/upload.ts +254 -0
- package/src/uploadWithConfig.ts +122 -0
- package/src/utils/compression.ts +61 -0
- package/test/compression.test.ts +88 -0
- package/test/download.test.ts +162 -0
- package/test/index.test.js +38 -0
- package/test/index.test.ts +39 -0
- package/test/upload.test.ts +131 -0
- package/tsconfig.json +17 -0
- package/vs.config.json +42 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 基于配置文件的GCP上传功能模块
|
|
3
|
+
*/
|
|
4
|
+
import { Storage } from '@google-cloud/storage';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { StorageClientOptions, UploadFilesResult } from './types/upload';
|
|
7
|
+
import { uploadFiles } from './upload';
|
|
8
|
+
import { loadConfig, getBucketName, getUploadDestination, getUploadSourcePath, VSConfig } from './config';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 基于配置文件的上传选项
|
|
12
|
+
*/
|
|
13
|
+
export interface UploadWithConfigOptions {
|
|
14
|
+
/** 存储客户端配置选项 */
|
|
15
|
+
storageClientOptions?: Partial<StorageClientOptions>;
|
|
16
|
+
/** 配置文件路径,默认为项目根目录下的vs.config.json */
|
|
17
|
+
configPath?: string;
|
|
18
|
+
/** 是否递归上传子目录,默认为true */
|
|
19
|
+
recursive?: boolean;
|
|
20
|
+
/** 是否启用压缩,默认为true */
|
|
21
|
+
enableCompression?: boolean;
|
|
22
|
+
/** 自定义源路径,如果提供则覆盖配置文件中的uploadPath */
|
|
23
|
+
customSourcePath?: string;
|
|
24
|
+
/** 自定义目标路径,如果提供则覆盖配置文件中的目标路径 */
|
|
25
|
+
customDestination?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 创建存储客户端
|
|
30
|
+
* @param options - 客户端配置选项
|
|
31
|
+
* @returns GCP存储客户端实例
|
|
32
|
+
*/
|
|
33
|
+
function createStorageClientFromConfig(options: Partial<StorageClientOptions> = {}): Storage {
|
|
34
|
+
const { keyFilename, projectId, credentials } = options;
|
|
35
|
+
|
|
36
|
+
// 如果提供了keyFilename,使用密钥文件认证
|
|
37
|
+
if (keyFilename) {
|
|
38
|
+
return new Storage({
|
|
39
|
+
keyFilename
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 如果提供了credentials,使用凭证对象认证
|
|
44
|
+
if (credentials) {
|
|
45
|
+
return new Storage({
|
|
46
|
+
projectId,
|
|
47
|
+
credentials
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 使用应用默认凭证(ADC)进行免密认证
|
|
52
|
+
return new Storage();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 基于配置文件批量上传文件
|
|
57
|
+
* @param options - 上传选项
|
|
58
|
+
* @returns 上传结果
|
|
59
|
+
*/
|
|
60
|
+
export async function uploadFilesWithConfig(options: UploadWithConfigOptions = {}): Promise<UploadFilesResult> {
|
|
61
|
+
const {
|
|
62
|
+
storageClientOptions = {},
|
|
63
|
+
configPath,
|
|
64
|
+
recursive = true,
|
|
65
|
+
enableCompression = true,
|
|
66
|
+
customSourcePath,
|
|
67
|
+
customDestination
|
|
68
|
+
} = options;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// 加载配置文件
|
|
72
|
+
const config: VSConfig = loadConfig(configPath);
|
|
73
|
+
|
|
74
|
+
// 创建存储客户端
|
|
75
|
+
const storageClient = createStorageClientFromConfig(storageClientOptions);
|
|
76
|
+
|
|
77
|
+
// 获取配置信息
|
|
78
|
+
const bucketName = getBucketName(config);
|
|
79
|
+
const sourcePath = customSourcePath || getUploadSourcePath(config);
|
|
80
|
+
const destination = customDestination || getUploadDestination(config);
|
|
81
|
+
|
|
82
|
+
console.log(`开始上传文件...`);
|
|
83
|
+
console.log(`源路径: ${sourcePath}`);
|
|
84
|
+
console.log(`目标桶: ${bucketName}`);
|
|
85
|
+
console.log(`目标路径: ${destination}`);
|
|
86
|
+
|
|
87
|
+
// 执行批量上传
|
|
88
|
+
const result = await uploadFiles({
|
|
89
|
+
bucketName,
|
|
90
|
+
sourcePath,
|
|
91
|
+
destination,
|
|
92
|
+
storageClient,
|
|
93
|
+
recursive,
|
|
94
|
+
enableCompression
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
console.log(`上传完成! 成功: ${result.success.length}, 失败: ${result.failed.length}`);
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(`基于配置文件的上传失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 获取配置信息(用于调试或显示)
|
|
107
|
+
* @param configPath - 配置文件路径
|
|
108
|
+
* @returns 配置信息摘要
|
|
109
|
+
*/
|
|
110
|
+
export function getConfigSummary(configPath?: string): {
|
|
111
|
+
bucketName: string;
|
|
112
|
+
sourcePath: string;
|
|
113
|
+
destination: string;
|
|
114
|
+
} {
|
|
115
|
+
const config = loadConfig(configPath);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
bucketName: getBucketName(config),
|
|
119
|
+
sourcePath: getUploadSourcePath(config),
|
|
120
|
+
destination: getUploadDestination(config)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件压缩工具模块
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { createReadStream, createWriteStream } from 'fs';
|
|
6
|
+
import * as zlib from 'zlib';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as util from 'util';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import { pipeline as stream } from 'stream';
|
|
11
|
+
|
|
12
|
+
const pipeline = util.promisify(stream);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 需要进行GZIP压缩的文件扩展名
|
|
16
|
+
*/
|
|
17
|
+
export const COMPRESSIBLE_EXTENSIONS = [
|
|
18
|
+
'.js',
|
|
19
|
+
'.css',
|
|
20
|
+
'.json',
|
|
21
|
+
'.html',
|
|
22
|
+
'.woff'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 检查文件是否需要压缩
|
|
27
|
+
* @param filePath - 文件路径
|
|
28
|
+
* @returns 是否需要压缩
|
|
29
|
+
*/
|
|
30
|
+
export function shouldCompressFile(filePath: string): boolean {
|
|
31
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
32
|
+
return COMPRESSIBLE_EXTENSIONS.includes(ext);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 使用GZIP压缩文件
|
|
37
|
+
* @param inputPath - 输入文件路径
|
|
38
|
+
* @returns 压缩后的临时文件路径
|
|
39
|
+
*/
|
|
40
|
+
export async function compressFile(inputPath: string): Promise<string> {
|
|
41
|
+
// 创建临时文件路径
|
|
42
|
+
const tempDir = os.tmpdir();
|
|
43
|
+
const fileName = path.basename(inputPath);
|
|
44
|
+
const outputPath = path.join(tempDir, `${fileName}.gz`);
|
|
45
|
+
|
|
46
|
+
// 创建读取流、GZIP压缩流和写入流
|
|
47
|
+
const gzip = zlib.createGzip();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// 使用pipeline进行流处理
|
|
51
|
+
await pipeline(
|
|
52
|
+
createReadStream(inputPath),
|
|
53
|
+
gzip,
|
|
54
|
+
createWriteStream(outputPath)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return outputPath;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new Error(`压缩文件失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import * as sinon from 'sinon';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { shouldCompressFile, compressFile, COMPRESSIBLE_EXTENSIONS } from '../src/utils/compression';
|
|
7
|
+
|
|
8
|
+
describe('压缩工具测试', function() {
|
|
9
|
+
describe('shouldCompressFile', function() {
|
|
10
|
+
it('应该正确识别可压缩的文件类型', function() {
|
|
11
|
+
// 测试所有可压缩的扩展名
|
|
12
|
+
COMPRESSIBLE_EXTENSIONS.forEach(ext => {
|
|
13
|
+
const filePath = `/path/to/file${ext}`;
|
|
14
|
+
expect(shouldCompressFile(filePath)).to.be.true;
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('应该正确识别不可压缩的文件类型', function() {
|
|
19
|
+
const nonCompressibleExtensions = [
|
|
20
|
+
'.jpg', '.png', '.gif', '.mp4', '.pdf', '.txt'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
nonCompressibleExtensions.forEach(ext => {
|
|
24
|
+
const filePath = `/path/to/file${ext}`;
|
|
25
|
+
expect(shouldCompressFile(filePath)).to.be.false;
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('应该处理大写扩展名', function() {
|
|
30
|
+
expect(shouldCompressFile('/path/to/file.JS')).to.be.true;
|
|
31
|
+
expect(shouldCompressFile('/path/to/file.CSS')).to.be.true;
|
|
32
|
+
expect(shouldCompressFile('/path/to/file.JSON')).to.be.true;
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('compressFile', function() {
|
|
37
|
+
let tempDir: string;
|
|
38
|
+
let testFilePath: string;
|
|
39
|
+
|
|
40
|
+
beforeEach(function() {
|
|
41
|
+
// 创建临时目录和测试文件
|
|
42
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'compression-test-'));
|
|
43
|
+
testFilePath = path.join(tempDir, 'test.js');
|
|
44
|
+
fs.writeFileSync(testFilePath, 'console.log("Hello, World!");');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(function() {
|
|
48
|
+
// 清理临时文件和目录
|
|
49
|
+
if (fs.existsSync(tempDir)) {
|
|
50
|
+
const files = fs.readdirSync(tempDir);
|
|
51
|
+
files.forEach(file => {
|
|
52
|
+
fs.unlinkSync(path.join(tempDir, file));
|
|
53
|
+
});
|
|
54
|
+
fs.rmdirSync(tempDir);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('应该成功压缩文件并返回压缩后的文件路径', async function() {
|
|
59
|
+
const compressedFilePath = await compressFile(testFilePath);
|
|
60
|
+
|
|
61
|
+
// 验证压缩文件存在
|
|
62
|
+
expect(fs.existsSync(compressedFilePath)).to.be.true;
|
|
63
|
+
|
|
64
|
+
// 验证压缩文件大小小于原文件
|
|
65
|
+
const originalSize = fs.statSync(testFilePath).size;
|
|
66
|
+
const compressedSize = fs.statSync(compressedFilePath).size;
|
|
67
|
+
|
|
68
|
+
// 清理压缩文件
|
|
69
|
+
fs.unlinkSync(compressedFilePath);
|
|
70
|
+
|
|
71
|
+
// 由于GZIP头部信息,对于非常小的文件,压缩后可能会更大
|
|
72
|
+
// 但对于实际应用中的JS/CSS文件,压缩效果会很明显
|
|
73
|
+
// 这里我们只验证压缩文件已创建
|
|
74
|
+
expect(compressedFilePath.endsWith('.gz')).to.be.true;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('应该在文件不存在时抛出错误', async function() {
|
|
78
|
+
const nonExistentFile = path.join(tempDir, 'non-existent.js');
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await compressFile(nonExistentFile);
|
|
82
|
+
expect.fail('应该抛出错误');
|
|
83
|
+
} catch (error) {
|
|
84
|
+
expect(error).to.exist;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import * as sinon from 'sinon';
|
|
3
|
+
import { Storage } from '@google-cloud/storage';
|
|
4
|
+
import { createStorageClient, downloadFile, downloadFiles, ensureDirectoryExists } from '../src/download';
|
|
5
|
+
import { DownloadSuccessResult, DownloadFailedResult } from '../src/types/download';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as util from 'util';
|
|
9
|
+
|
|
10
|
+
describe('下载功能测试', function() {
|
|
11
|
+
let storageStub: sinon.SinonStubbedInstance<Storage>;
|
|
12
|
+
let fsExistsStub: sinon.SinonStub;
|
|
13
|
+
let fsMkdirStub: sinon.SinonStub;
|
|
14
|
+
let bucketStub: any;
|
|
15
|
+
let fileStub: any;
|
|
16
|
+
|
|
17
|
+
beforeEach(function() {
|
|
18
|
+
// 创建存储客户端的存根
|
|
19
|
+
storageStub = sinon.createStubInstance(Storage);
|
|
20
|
+
|
|
21
|
+
// 创建文件系统存根
|
|
22
|
+
fsExistsStub = sinon.stub(fs, 'existsSync');
|
|
23
|
+
fsMkdirStub = sinon.stub().resolves();
|
|
24
|
+
sinon.stub(util, 'promisify').returns(fsMkdirStub);
|
|
25
|
+
|
|
26
|
+
// 创建存储桶和文件存根
|
|
27
|
+
bucketStub = {
|
|
28
|
+
file: sinon.stub(),
|
|
29
|
+
getFiles: sinon.stub()
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
fileStub = {
|
|
33
|
+
exists: sinon.stub(),
|
|
34
|
+
download: sinon.stub(),
|
|
35
|
+
getMetadata: sinon.stub()
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// 设置存根行为
|
|
39
|
+
storageStub.bucket.returns(bucketStub as any);
|
|
40
|
+
bucketStub.file.returns(fileStub);
|
|
41
|
+
fileStub.exists.resolves([true]);
|
|
42
|
+
fileStub.download.resolves([{}]);
|
|
43
|
+
fileStub.getMetadata.resolves([{ name: 'test.txt' }]);
|
|
44
|
+
|
|
45
|
+
// 默认目录存在
|
|
46
|
+
fsExistsStub.returns(true);
|
|
47
|
+
|
|
48
|
+
// 设置getFiles的默认行为
|
|
49
|
+
bucketStub.getFiles.resolves([[
|
|
50
|
+
{ name: 'test.txt' },
|
|
51
|
+
{ name: 'folder/test2.txt' }
|
|
52
|
+
], null, { prefixes: ['folder/'] }]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(function() {
|
|
56
|
+
sinon.restore();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('createStorageClient()', function() {
|
|
60
|
+
it('应该使用应用默认凭证创建存储客户端', function() {
|
|
61
|
+
const storageClient = createStorageClient();
|
|
62
|
+
expect(storageClient).to.be.instanceOf(Storage);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('应该使用密钥文件创建存储客户端', function() {
|
|
66
|
+
const storageClient = createStorageClient({ keyFilename: 'key.json' });
|
|
67
|
+
expect(storageClient).to.be.instanceOf(Storage);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('应该使用凭证对象创建存储客户端', function() {
|
|
71
|
+
const storageClient = createStorageClient({
|
|
72
|
+
projectId: 'test-project',
|
|
73
|
+
credentials: {
|
|
74
|
+
client_email: 'test@example.com',
|
|
75
|
+
private_key: 'private-key'
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
expect(storageClient).to.be.instanceOf(Storage);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('ensureDirectoryExists()', function() {
|
|
83
|
+
it('应该在目录不存在时创建目录', async function() {
|
|
84
|
+
fsExistsStub.returns(false);
|
|
85
|
+
|
|
86
|
+
await ensureDirectoryExists('/test/dir');
|
|
87
|
+
|
|
88
|
+
expect(fsExistsStub.calledWith('/test/dir')).to.be.true;
|
|
89
|
+
expect(fsMkdirStub.calledWith('/test/dir', { recursive: true })).to.be.true;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('应该在目录已存在时不创建目录', async function() {
|
|
93
|
+
fsExistsStub.returns(true);
|
|
94
|
+
|
|
95
|
+
await ensureDirectoryExists('/test/dir');
|
|
96
|
+
|
|
97
|
+
expect(fsExistsStub.calledWith('/test/dir')).to.be.true;
|
|
98
|
+
expect(fsMkdirStub.called).to.be.false;
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('downloadFile()', function() {
|
|
103
|
+
it('应该成功下载文件', async function() {
|
|
104
|
+
const result = await downloadFile({
|
|
105
|
+
bucketName: 'test-bucket',
|
|
106
|
+
sourcePath: 'test.txt',
|
|
107
|
+
destinationPath: '/test/test.txt',
|
|
108
|
+
storageClient: storageStub as any
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(result.success).to.be.true;
|
|
112
|
+
expect((result as DownloadSuccessResult).file).to.equal('test.txt');
|
|
113
|
+
expect((result as DownloadSuccessResult).localPath).to.equal('/test/test.txt');
|
|
114
|
+
expect(storageStub.bucket.calledWith('test-bucket')).to.be.true;
|
|
115
|
+
expect(bucketStub.file.calledWith('test.txt')).to.be.true;
|
|
116
|
+
expect(fileStub.download.calledWith({ destination: '/test/test.txt' })).to.be.true;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('应该在文件不存在时返回失败结果', async function() {
|
|
120
|
+
fileStub.exists.resolves([false]);
|
|
121
|
+
|
|
122
|
+
const result = await downloadFile({
|
|
123
|
+
bucketName: 'test-bucket',
|
|
124
|
+
sourcePath: 'nonexistent.txt',
|
|
125
|
+
destinationPath: '/test/nonexistent.txt',
|
|
126
|
+
storageClient: storageStub as any
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(result.success).to.be.false;
|
|
130
|
+
expect((result as DownloadFailedResult).file).to.equal('nonexistent.txt');
|
|
131
|
+
expect((result as DownloadFailedResult).error).to.include('文件不存在');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('downloadFiles()', function() {
|
|
136
|
+
it('应该成功下载多个文件', async function() {
|
|
137
|
+
const result = await downloadFiles({
|
|
138
|
+
bucketName: 'test-bucket',
|
|
139
|
+
sourcePath: '',
|
|
140
|
+
destinationPath: '/test',
|
|
141
|
+
storageClient: storageStub as any
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(result.success.length).to.equal(2);
|
|
145
|
+
expect(result.failed.length).to.equal(0);
|
|
146
|
+
expect(bucketStub.getFiles.called).to.be.true;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('应该在递归模式下下载子目录', async function() {
|
|
150
|
+
const result = await downloadFiles({
|
|
151
|
+
bucketName: 'test-bucket',
|
|
152
|
+
sourcePath: '',
|
|
153
|
+
destinationPath: '/test',
|
|
154
|
+
storageClient: storageStub as any,
|
|
155
|
+
recursive: true
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.success.length).to.equal(2);
|
|
159
|
+
expect(bucketStub.getFiles.called).to.be.true;
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { expect } = require('chai');
|
|
2
|
+
const { analyzeContent } = require('../src/index');
|
|
3
|
+
|
|
4
|
+
describe('AI-Yuca 核心功能测试', function() {
|
|
5
|
+
describe('analyzeContent()', function() {
|
|
6
|
+
it('应该正确计算文本统计信息', function() {
|
|
7
|
+
const testText = 'Hello world!\nThis is a test.\nAI-Yuca is awesome.';
|
|
8
|
+
const result = analyzeContent(testText);
|
|
9
|
+
|
|
10
|
+
expect(result).to.have.property('statistics');
|
|
11
|
+
expect(result.statistics).to.have.property('charCount').that.equals(testText.length);
|
|
12
|
+
expect(result.statistics).to.have.property('wordCount').that.equals(9);
|
|
13
|
+
expect(result.statistics).to.have.property('lineCount').that.equals(3);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('应该提取关键词', function() {
|
|
17
|
+
const testText = 'The quick brown fox jumps over the lazy dog. The fox is quick and brown.';
|
|
18
|
+
const result = analyzeContent(testText);
|
|
19
|
+
|
|
20
|
+
expect(result).to.have.property('keywords');
|
|
21
|
+
expect(result.keywords).to.be.an('array');
|
|
22
|
+
|
|
23
|
+
// 检查是否包含预期的关键词
|
|
24
|
+
const keywords = result.keywords.map(k => k.word);
|
|
25
|
+
expect(keywords).to.include('quick');
|
|
26
|
+
expect(keywords).to.include('brown');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('应该处理空文本', function() {
|
|
30
|
+
const result = analyzeContent('');
|
|
31
|
+
|
|
32
|
+
expect(result.statistics.charCount).to.equal(0);
|
|
33
|
+
expect(result.statistics.wordCount).to.equal(0);
|
|
34
|
+
expect(result.statistics.lineCount).to.equal(1); // 空字符串也算一行
|
|
35
|
+
expect(result.keywords).to.be.an('array').that.is.empty;
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { analyzeContent } from '../src/index';
|
|
3
|
+
import { AnalysisResult } from '../src/types/analyze';
|
|
4
|
+
|
|
5
|
+
describe('AI-Yuca 核心功能测试', function() {
|
|
6
|
+
describe('analyzeContent()', function() {
|
|
7
|
+
it('应该正确计算文本统计信息', function() {
|
|
8
|
+
const testText = 'Hello world!\nThis is a test.\nAI-Yuca is awesome.';
|
|
9
|
+
const result: AnalysisResult = analyzeContent(testText);
|
|
10
|
+
|
|
11
|
+
expect(result).to.have.property('statistics');
|
|
12
|
+
expect(result.statistics).to.have.property('charCount').that.equals(testText.length);
|
|
13
|
+
expect(result.statistics).to.have.property('wordCount').that.equals(9);
|
|
14
|
+
expect(result.statistics).to.have.property('lineCount').that.equals(3);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('应该提取关键词', function() {
|
|
18
|
+
const testText = 'The quick brown fox jumps over the lazy dog. The fox is quick and brown.';
|
|
19
|
+
const result: AnalysisResult = analyzeContent(testText);
|
|
20
|
+
|
|
21
|
+
expect(result).to.have.property('keywords');
|
|
22
|
+
expect(result.keywords).to.be.an('array');
|
|
23
|
+
|
|
24
|
+
// 检查是否包含预期的关键词
|
|
25
|
+
const keywords = result.keywords.map((k: { word: string; count: number }) => k.word);
|
|
26
|
+
expect(keywords).to.include('quick');
|
|
27
|
+
expect(keywords).to.include('brown');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('应该处理空文本', function() {
|
|
31
|
+
const result: AnalysisResult = analyzeContent('');
|
|
32
|
+
|
|
33
|
+
expect(result.statistics.charCount).to.equal(0);
|
|
34
|
+
expect(result.statistics.wordCount).to.equal(0);
|
|
35
|
+
expect(result.statistics.lineCount).to.equal(1); // 空字符串也算一行
|
|
36
|
+
expect(result.keywords).to.be.an('array').that.is.empty;
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import * as sinon from 'sinon';
|
|
3
|
+
import { Storage } from '@google-cloud/storage';
|
|
4
|
+
import { createStorageClient, uploadFile, uploadFiles } from '../src/upload';
|
|
5
|
+
import { UploadSuccessResult, UploadFailedResult } from '../src/types/upload';
|
|
6
|
+
import { shouldCompressFile, compressFile } from '../src/utils/compression';
|
|
7
|
+
|
|
8
|
+
// 在测试中使用sinon替代jest进行模拟
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
|
|
12
|
+
describe('上传功能测试', function() {
|
|
13
|
+
let storageStub: sinon.SinonStubbedInstance<Storage>;
|
|
14
|
+
let fsExistsStub: sinon.SinonStub;
|
|
15
|
+
let bucketStub: any;
|
|
16
|
+
|
|
17
|
+
beforeEach(function() {
|
|
18
|
+
// 创建存储客户端的存根
|
|
19
|
+
storageStub = sinon.createStubInstance(Storage);
|
|
20
|
+
|
|
21
|
+
// 创建文件系统存根
|
|
22
|
+
fsExistsStub = sinon.stub(fs, 'existsSync');
|
|
23
|
+
|
|
24
|
+
// 创建存储桶存根
|
|
25
|
+
bucketStub = {
|
|
26
|
+
upload: sinon.stub().resolves([{
|
|
27
|
+
getMetadata: sinon.stub().resolves([{
|
|
28
|
+
name: 'test-file.txt',
|
|
29
|
+
size: '1024',
|
|
30
|
+
contentType: 'text/plain',
|
|
31
|
+
timeCreated: '2023-01-01T00:00:00.000Z'
|
|
32
|
+
}])
|
|
33
|
+
}])
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
storageStub.bucket.returns(bucketStub as any);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(function() {
|
|
40
|
+
sinon.restore();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('createStorageClient()', function() {
|
|
44
|
+
it('应该使用应用默认凭证创建存储客户端', function() {
|
|
45
|
+
const client = createStorageClient({});
|
|
46
|
+
expect(client).to.be.an.instanceof(Storage);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('应该使用密钥文件创建存储客户端', function() {
|
|
50
|
+
const client = createStorageClient({ keyFilename: 'test-key.json' });
|
|
51
|
+
expect(client).to.be.an.instanceof(Storage);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('应该使用凭证对象创建存储客户端', function() {
|
|
55
|
+
const client = createStorageClient({
|
|
56
|
+
credentials: { client_email: 'test@example.com', private_key: 'test-key' }
|
|
57
|
+
});
|
|
58
|
+
expect(client).to.be.an.instanceof(Storage);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('uploadFile()', function() {
|
|
63
|
+
beforeEach(function() {
|
|
64
|
+
// 模拟压缩函数
|
|
65
|
+
(shouldCompressFile as any) = sinon.stub().returns(false);
|
|
66
|
+
(compressFile as any) = sinon.stub().resolves('test-file.txt.gz');
|
|
67
|
+
|
|
68
|
+
// 模拟文件系统
|
|
69
|
+
fsExistsStub.withArgs('test-file.txt.gz').returns(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('应该在文件不存在时返回失败结果', async function() {
|
|
73
|
+
fsExistsStub.withArgs('non-existent-file.txt').returns(false);
|
|
74
|
+
|
|
75
|
+
const result = await uploadFile({
|
|
76
|
+
bucketName: 'test-bucket',
|
|
77
|
+
filePath: 'non-existent-file.txt',
|
|
78
|
+
storageClient: storageStub as unknown as Storage
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(result.success).to.be.false;
|
|
82
|
+
expect(result).to.have.property('error').that.includes('文件不存在');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('应该成功上传文件(不压缩)', async function() {
|
|
86
|
+
fsExistsStub.withArgs('test-file.txt').returns(true);
|
|
87
|
+
|
|
88
|
+
const result = await uploadFile({
|
|
89
|
+
bucketName: 'test-bucket',
|
|
90
|
+
filePath: 'test-file.txt',
|
|
91
|
+
storageClient: storageStub as unknown as Storage
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(result.success).to.be.true;
|
|
95
|
+
expect(result).to.have.property('file').that.equals('test-file.txt');
|
|
96
|
+
expect(result).to.have.property('url').that.includes('test-bucket/test-file.txt');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('应该成功上传压缩文件', async function() {
|
|
100
|
+
fsExistsStub.withArgs('test-file.js').returns(true);
|
|
101
|
+
(shouldCompressFile as any).returns(true);
|
|
102
|
+
|
|
103
|
+
const result = await uploadFile({
|
|
104
|
+
bucketName: 'test-bucket',
|
|
105
|
+
filePath: 'test-file.js',
|
|
106
|
+
storageClient: storageStub as unknown as Storage,
|
|
107
|
+
enableCompression: true
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect((shouldCompressFile as any).calledWith('test-file.js')).to.be.true;
|
|
111
|
+
expect((compressFile as any).calledWith('test-file.js')).to.be.true;
|
|
112
|
+
expect(result.success).to.be.true;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('应该在禁用压缩时不压缩文件', async function() {
|
|
116
|
+
fsExistsStub.withArgs('test-file.js').returns(true);
|
|
117
|
+
(shouldCompressFile as any).returns(true);
|
|
118
|
+
|
|
119
|
+
const result = await uploadFile({
|
|
120
|
+
bucketName: 'test-bucket',
|
|
121
|
+
filePath: 'test-file.js',
|
|
122
|
+
storageClient: storageStub as unknown as Storage,
|
|
123
|
+
enableCompression: false
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// 不应该调用压缩函数
|
|
127
|
+
expect((compressFile as any).called).to.be.false;
|
|
128
|
+
expect(result.success).to.be.true;
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2018",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"lib": ["ES2018"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*", "bin/**/*", "test/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|