@cloudbase/manager-node 4.2.0 → 4.2.1

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +0 -4
  2. package/lib/constant.js +1 -5
  3. package/lib/env/index.js +209 -10
  4. package/lib/function/index.js +5 -3
  5. package/lib/storage/index.js +3 -19
  6. package/lib/utils/cloud-api-request.js +0 -7
  7. package/lib/utils/http-request.js +3 -3
  8. package/package.json +3 -4
  9. package/src/access/index.ts +168 -0
  10. package/src/access/types.ts +55 -0
  11. package/src/billing/index.ts +43 -0
  12. package/src/cam/index.ts +106 -0
  13. package/src/cloudBaseRun/index.ts +40 -0
  14. package/src/cloudBaseRun/types.ts +24 -0
  15. package/src/common/index.ts +54 -0
  16. package/src/constant.ts +56 -0
  17. package/src/context.ts +18 -0
  18. package/src/database/index.ts +369 -0
  19. package/src/debug.ts +34 -0
  20. package/src/env/index.ts +614 -0
  21. package/src/environment.ts +156 -0
  22. package/src/environmentManager.ts +50 -0
  23. package/src/error.ts +27 -0
  24. package/src/function/index.ts +1362 -0
  25. package/src/function/packer.ts +164 -0
  26. package/src/function/types.ts +164 -0
  27. package/src/hosting/index.ts +698 -0
  28. package/src/index.ts +127 -0
  29. package/src/interfaces/base.interface.ts +8 -0
  30. package/src/interfaces/billing.interface.ts +21 -0
  31. package/src/interfaces/cam.interface.ts +28 -0
  32. package/src/interfaces/flexdb.interface.ts +104 -0
  33. package/src/interfaces/function.interface.ts +75 -0
  34. package/src/interfaces/index.ts +7 -0
  35. package/src/interfaces/storage.interface.ts +29 -0
  36. package/src/interfaces/tcb.interface.ts +636 -0
  37. package/src/storage/index.ts +1281 -0
  38. package/src/third/index.ts +24 -0
  39. package/src/user/index.ts +174 -0
  40. package/src/user/types.ts +21 -0
  41. package/src/utils/auth.ts +112 -0
  42. package/src/utils/cloud-api-request.ts +252 -0
  43. package/src/utils/cloudbase-request.ts +109 -0
  44. package/src/utils/envLazy.ts +15 -0
  45. package/src/utils/fs.ts +57 -0
  46. package/src/utils/http-request.ts +37 -0
  47. package/src/utils/index.ts +103 -0
  48. package/src/utils/parallel.ts +82 -0
  49. package/src/utils/uuid.ts +14 -0
  50. package/types/constant.d.ts +0 -7
  51. package/types/env/index.d.ts +17 -0
  52. package/types/function/index.d.ts +2 -1
  53. package/lib/utils/runenv.js +0 -8
  54. package/types/utils/runenv.d.ts +0 -1
@@ -0,0 +1,698 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import makeDir from 'make-dir'
4
+ import { CloudBaseError } from '../error'
5
+ import { Environment } from '../environment'
6
+ import {
7
+ CloudService,
8
+ preLazy,
9
+ checkReadable,
10
+ isDirectory,
11
+ checkFullAccess,
12
+ fetchStream
13
+ } from '../utils'
14
+ import { IListFileInfo } from '../interfaces'
15
+ import { AsyncTaskParallelController } from '../utils/parallel'
16
+
17
+ const envDomainCache = new Map()
18
+
19
+ export interface IProgressData {
20
+ loaded: number // 已经上传的部分 字节
21
+ total: number // 整个文件的大小 字节
22
+ speed: number // 文件上传速度 字节/秒
23
+ percent: number // 百分比 小数 0 - 1
24
+ }
25
+
26
+ export type OnProgress = (progressData: IProgressData) => void
27
+ export type OnFileFinish = (error: Error, res: any, fileData: any) => void
28
+
29
+ export interface IHostingFileOptions {
30
+ localPath: string
31
+ cloudPath?: string
32
+ // 上传文件并发数量
33
+ parallel?: number
34
+ files?: {
35
+ localPath: string
36
+ cloudPath: string
37
+ }[]
38
+ onProgress?: OnProgress
39
+ onFileFinish?: OnFileFinish
40
+ ignore?: string | string[]
41
+ // 重试次数
42
+ retryCount?: number
43
+ // 重试时间间隔(毫秒)
44
+ retryInterval?: number
45
+ }
46
+
47
+ export interface IHostingFilesOptions {
48
+ // 上传文件并发数量
49
+ localPath?: string
50
+ cloudPath?: string
51
+ parallel?: number
52
+ files: {
53
+ localPath: string
54
+ cloudPath: string
55
+ }[]
56
+ onProgress?: OnProgress
57
+ onFileFinish?: OnFileFinish
58
+ ignore?: string | string[]
59
+ // 重试次数
60
+ retryCount?: number
61
+ // 重试时间间隔(毫秒)
62
+ retryInterval?: number
63
+ }
64
+
65
+ export type IHostingOptions = IHostingFileOptions | IHostingFilesOptions
66
+
67
+ export interface IHostingCloudOptions {
68
+ cloudPath: string
69
+ isDir: boolean
70
+ }
71
+
72
+ export interface IBindDomainOptions {
73
+ domain: string
74
+ certId: string
75
+ }
76
+
77
+ export interface ICheckSourceOptions {
78
+ domains: string[]
79
+ }
80
+
81
+ export interface ITcbOrigin {
82
+ Master: string // 主站
83
+ Slave: string // 副站
84
+ }
85
+
86
+ export interface IIpFilter {
87
+ Switch: string // IP 黑白名单配置开关
88
+ FilterType?: string // IP 黑白名单类型
89
+ Filters?: string[] // IP 黑白名单列表
90
+ }
91
+
92
+ export interface IIpFreqLimit {
93
+ Switch: string // IP 限频配置开关
94
+ Qps?: number // 设置每秒请求数限制
95
+ }
96
+
97
+ export interface ITcbAuthentication {
98
+ Switch: string // on/off 开启或关闭
99
+ SecretKey: string // 用户计算签名的秘钥
100
+ SignParam?: string // url串中签名的字段名
101
+ TimeParam?: string // url串中时间的字段名
102
+ ExpireTime?: string // 开启时必填过期时间
103
+ }
104
+
105
+ export interface ITcbCache {
106
+ RuleType: string // 规则类型
107
+ RuleValue: string // 规则内容
108
+ CacheTtl: number // 缓存时间
109
+ }
110
+
111
+ export interface ITcbRefererRule {
112
+ RefererType: string // referer 配置类型
113
+ Referers: string[] // referer 内容列表列表
114
+ AllowEmpty: boolean // 是否允许空 referer
115
+ }
116
+
117
+ export interface ITcbReferer {
118
+ Switch: string // 黑白名单配置开关
119
+ RefererRules?: ITcbRefererRule[]
120
+ }
121
+
122
+ export interface ITcbDomainConfig {
123
+ // Origin?: ITcbOrigin // 源域名
124
+ // CosPrivateAccess?: string //是否开始cos私有读
125
+ // Authentication?: ITcbAuthentication // 防盗链设置
126
+ Cache?: ITcbCache[] // 缓存策略
127
+ IpFilter?: IIpFilter // IP 黑白名单配置
128
+ IpFreqLimit?: IIpFreqLimit // IP 限频配置
129
+ Refer?: ITcbReferer
130
+ }
131
+
132
+ export interface IModifyOptions {
133
+ domain: string // 域名
134
+ domainId: number // 域名ID
135
+ domainConfig: ITcbDomainConfig // 域名配置
136
+ }
137
+
138
+ export interface IDeleteDomainOptions {
139
+ domain: string
140
+ }
141
+
142
+ export interface IDomainInfo {
143
+ Domain: string
144
+ DomainId: number
145
+ Status: 'process' | 'online' | 'offline'
146
+ DomainConfig: Record<string, string>
147
+ CName: String
148
+ }
149
+
150
+ const HostingStatusMap = {
151
+ init: '初始化中',
152
+ process: '处理中',
153
+ online: '上线',
154
+ destroying: '销毁中',
155
+ offline: '下线',
156
+ create_fail: '初始化失败', // eslint-disable-line
157
+ destroy_fail: '销毁失败' // eslint-disable-line
158
+ }
159
+
160
+ export interface IHostingInfo {
161
+ EnvId: string
162
+ CdnDomain: string
163
+ Bucket: string
164
+ Regoin: string
165
+ Status: string
166
+ MaxDomain: number
167
+ Id: number
168
+ PolicyId: number
169
+ }
170
+
171
+ export interface IRoutingRules {
172
+ keyPrefixEquals?: string // 前缀匹配
173
+ httpErrorCodeReturnedEquals?: string // 错误码
174
+ replaceKeyWith?: string // 替换内容
175
+ replaceKeyPrefixWith?: string // condition设置为KeyPrefixEquals 前缀匹配时可设置
176
+ }
177
+
178
+ export interface IBucketWebsiteOptiosn {
179
+ indexDocument: string
180
+ errorDocument?: string
181
+ routingRules?: Array<IRoutingRules>
182
+ }
183
+
184
+ export interface IFindOptions {
185
+ prefix?: string
186
+ marker?: string
187
+ maxKeys?: number
188
+ }
189
+
190
+ export class HostingService {
191
+ private environment: Environment
192
+ private tcbService: CloudService
193
+ private cdnService: CloudService
194
+
195
+ constructor(environment: Environment) {
196
+ this.environment = environment
197
+ this.tcbService = new CloudService(environment.cloudBaseContext, 'tcb', '2018-06-08')
198
+ this.cdnService = new CloudService(environment.cloudBaseContext, 'cdn', '2018-06-06')
199
+ }
200
+
201
+ /**
202
+ * 获取 hosting 信息
203
+ */
204
+ @preLazy()
205
+ async getInfo(): Promise<IHostingInfo[]> {
206
+ const { envId } = this.getHostingConfig()
207
+ const { Data } = await this.tcbService.request('DescribeStaticStore', {
208
+ EnvId: envId
209
+ })
210
+
211
+ return Data
212
+ }
213
+
214
+ /**
215
+ * 开启 hosting 服务,异步任务
216
+ */
217
+ @preLazy()
218
+ async enableService() {
219
+ const { envId } = this.getHostingConfig()
220
+
221
+ const hostings = await this.getInfo()
222
+ // hosting 服务已开启
223
+ if (hostings?.length) {
224
+ const website = hostings[0]
225
+ // offline 状态的服务可重新开启
226
+ if (website.Status !== 'offline') {
227
+ throw new CloudBaseError('静态网站服务已开启,请勿重复操作!')
228
+ }
229
+ }
230
+
231
+ const res = await this.tcbService.request<{
232
+ RequestId: string
233
+ Result: string
234
+ }>('CreateStaticStore', {
235
+ EnvId: envId
236
+ })
237
+
238
+ const code = res.Result === 'succ' ? 0 : -1
239
+
240
+ return {
241
+ code,
242
+ requestId: res.RequestId
243
+ }
244
+ }
245
+
246
+ async findFiles(options: IFindOptions): Promise<any> {
247
+ const hosting = await this.checkStatus()
248
+ const { Bucket, Regoin } = hosting
249
+ const { maxKeys, marker, prefix } = options
250
+
251
+ const storageService = await this.environment.getStorageService()
252
+ const res = await storageService.getBucket({
253
+ bucket: Bucket,
254
+ region: Regoin,
255
+ maxKeys,
256
+ marker,
257
+ prefix
258
+ })
259
+ return res
260
+ }
261
+
262
+ /**
263
+ * 展示文件列表
264
+ */
265
+ @preLazy()
266
+ async listFiles(): Promise<IListFileInfo[]> {
267
+ const hosting = await this.checkStatus()
268
+ const { Bucket, Regoin } = hosting
269
+ const storageService = await this.environment.getStorageService()
270
+
271
+ const list = await storageService.walkCloudDirCustom({
272
+ prefix: '',
273
+ bucket: Bucket,
274
+ region: Regoin
275
+ })
276
+
277
+ return list
278
+ }
279
+
280
+ /**
281
+ * 销毁静态托管服务
282
+ */
283
+ @preLazy()
284
+ async destroyService() {
285
+ const { envId } = this.getHostingConfig()
286
+
287
+ const files = await this.listFiles()
288
+
289
+ if (files?.length) {
290
+ throw new CloudBaseError('静态网站文件不为空,无法销毁!', {
291
+ code: 'INVALID_OPERATION'
292
+ })
293
+ }
294
+
295
+ const hostings = await this.getInfo()
296
+
297
+ if (!hostings || !hostings.length) {
298
+ throw new CloudBaseError('静态网站服务未开启!', {
299
+ code: 'INVALID_OPERATION'
300
+ })
301
+ }
302
+
303
+ const website = hostings[0]
304
+
305
+ // destroy_fail 状态可重试
306
+ if (website.Status !== 'online' && website.Status !== 'destroy_fail') {
307
+ throw new CloudBaseError(
308
+ `静态网站服务【${HostingStatusMap[website.Status]}】,无法进行此操作!`,
309
+ {
310
+ code: 'INVALID_OPERATION'
311
+ }
312
+ )
313
+ }
314
+
315
+ const res = await this.tcbService.request<{
316
+ RequestId: string
317
+ Result: string
318
+ }>('DestroyStaticStore', {
319
+ EnvId: envId
320
+ })
321
+
322
+ const code = res.Result === 'succ' ? 0 : -1
323
+ return {
324
+ code,
325
+ requestId: res.RequestId
326
+ }
327
+ }
328
+
329
+ /**
330
+ * 支持上传单个文件,文件夹,或多个文件
331
+ * @param options
332
+ */
333
+ @preLazy()
334
+ async uploadFiles(options: IHostingOptions) {
335
+ const {
336
+ localPath,
337
+ cloudPath,
338
+ files = [],
339
+ onProgress,
340
+ onFileFinish,
341
+ parallel = 20,
342
+ ignore,
343
+ retryCount,
344
+ retryInterval
345
+ } = options
346
+
347
+ const hosting = await this.checkStatus()
348
+ const { Bucket, Regoin } = hosting
349
+ const storageService = await this.environment.getStorageService()
350
+
351
+ let uploadFiles = Array.isArray(files) ? files : []
352
+
353
+ // localPath 存在,上传文件夹/文件
354
+ if (localPath) {
355
+ const resolvePath = path.resolve(localPath)
356
+ // 检查路径是否存在
357
+ checkReadable(resolvePath, true)
358
+
359
+ if (isDirectory(resolvePath)) {
360
+ return storageService.uploadDirectoryCustom({
361
+ localPath: resolvePath,
362
+ cloudPath,
363
+ bucket: Bucket,
364
+ region: Regoin,
365
+ onProgress,
366
+ onFileFinish,
367
+ fileId: false,
368
+ ignore,
369
+ retryCount,
370
+ retryInterval,
371
+ })
372
+ } else {
373
+ // 文件上传统一通过批量上传接口
374
+ const assignCloudPath = cloudPath || path.parse(resolvePath).base
375
+ uploadFiles.push({
376
+ localPath: resolvePath,
377
+ cloudPath: assignCloudPath
378
+ })
379
+ }
380
+ }
381
+
382
+ // 文件上传统一通过批量上传接口
383
+ return storageService.uploadFilesCustom({
384
+ ignore,
385
+ parallel,
386
+ onProgress,
387
+ onFileFinish,
388
+ bucket: Bucket,
389
+ region: Regoin,
390
+ files: uploadFiles,
391
+ fileId: false,
392
+ retryCount,
393
+ retryInterval,
394
+ })
395
+ }
396
+
397
+ /**
398
+ * 删除文件或文件夹
399
+ * @param options
400
+ */
401
+ @preLazy()
402
+ async deleteFiles(options: IHostingCloudOptions) {
403
+ const { cloudPath, isDir } = options
404
+ const hosting = await this.checkStatus()
405
+ const { Bucket, Regoin } = hosting
406
+ const storageService = await this.environment.getStorageService()
407
+
408
+ if (isDir) {
409
+ return storageService.deleteDirectoryCustom({
410
+ cloudPath,
411
+ bucket: Bucket,
412
+ region: Regoin
413
+ })
414
+ } else {
415
+ try {
416
+ await storageService.deleteFileCustom([cloudPath], Bucket, Regoin)
417
+ return {
418
+ Deleted: [{ Key: cloudPath }],
419
+ Error: []
420
+ }
421
+ } catch (e) {
422
+ return {
423
+ Deleted: [],
424
+ Error: [e]
425
+ }
426
+ }
427
+ }
428
+ }
429
+
430
+ /**
431
+ * 下载文件
432
+ * @param {string} cloudPath 云端文件路径
433
+ * @param {string} localPath 文件本地存储路径,文件需指定文件名称
434
+ * @returns {Promise<NodeJS.ReadableStream>}
435
+ */
436
+ @preLazy()
437
+ public async downloadFile(options: {
438
+ cloudPath: string
439
+ localPath?: string
440
+ }): Promise<NodeJS.ReadableStream | string> {
441
+ const { cloudPath, localPath } = options
442
+ const resolveLocalPath = path.resolve(localPath)
443
+ const fileDir = path.dirname(localPath)
444
+
445
+ checkFullAccess(fileDir, true)
446
+
447
+ const envConfig = this.environment.lazyEnvironmentConfig
448
+ const cacheHosting: IHostingInfo & { cacheTime: number } = envDomainCache.get(
449
+ envConfig.EnvId
450
+ )
451
+
452
+ let CdnDomain
453
+ // 2 分钟有效
454
+ if (cacheHosting?.cacheTime && Number(cacheHosting?.cacheTime) + 120000 < Date.now()) {
455
+ console.log('cache')
456
+ CdnDomain = cacheHosting.CdnDomain
457
+ } else {
458
+ const hosting = await this.checkStatus()
459
+ CdnDomain = hosting.CdnDomain
460
+ envDomainCache.set(envConfig.EnvId, {
461
+ ...hosting,
462
+ cacheTime: Date.now()
463
+ })
464
+ }
465
+
466
+ const url = new URL(cloudPath, `https://${CdnDomain}`).toString()
467
+
468
+ const { proxy } = await this.environment.getAuthConfig()
469
+ const res = await fetchStream(url, {}, proxy)
470
+
471
+ // localPath 不存在时,返回 ReadableStream
472
+ if (!localPath) {
473
+ return res.body
474
+ }
475
+ const dest = fs.createWriteStream(resolveLocalPath)
476
+ res.body.pipe(dest)
477
+
478
+ // 写完成后返回
479
+ return new Promise(resolve => {
480
+ dest.on('close', () => {
481
+ // 返回文件地址
482
+ resolve(resolveLocalPath)
483
+ })
484
+ })
485
+ }
486
+
487
+ /**
488
+ * 下载文件夹
489
+ * @param {string} cloudPath 云端文件路径
490
+ * @param {string} localPath 本地文件夹存储路径
491
+ * @returns {Promise<(NodeJS.ReadableStream | string)[]>}
492
+ */
493
+ @preLazy()
494
+ public async downloadDirectory(options: {
495
+ cloudPath: string
496
+ localPath?: string
497
+ }): Promise<void> {
498
+ const { cloudPath, localPath } = options
499
+ const resolveLocalPath = path.resolve(localPath)
500
+
501
+ const hosting = await this.checkStatus()
502
+ const { Bucket, Regoin } = hosting
503
+
504
+ const cloudDirectoryKey = this.getCloudKey(cloudPath)
505
+ const storageService = await this.environment.getStorageService()
506
+ const files = await storageService.walkCloudDirCustom({
507
+ prefix: cloudDirectoryKey,
508
+ bucket: Bucket,
509
+ region: Regoin
510
+ })
511
+
512
+ const tasks = files.map(file => async () => {
513
+ const fileRelativePath = file.Key.replace(cloudDirectoryKey, '')
514
+ // 空路径和文件夹跳过
515
+ if (!fileRelativePath || /\/$/g.test(fileRelativePath)) {
516
+ return
517
+ }
518
+ const localFilePath = path.join(resolveLocalPath, fileRelativePath)
519
+ // 创建文件的父文件夹
520
+ const fileDir = path.dirname(localFilePath)
521
+ await makeDir(fileDir)
522
+ return this.downloadFile({
523
+ cloudPath: file.Key,
524
+ localPath: localFilePath
525
+ })
526
+ })
527
+
528
+ // 下载请求
529
+ const creatingDirController = new AsyncTaskParallelController(20, 50)
530
+ creatingDirController.loadTasks(tasks)
531
+ await creatingDirController.run()
532
+ }
533
+
534
+ // 遍历文件
535
+ @preLazy()
536
+ async walkLocalDir(envId: string, dir: string) {
537
+ const storageService = await this.environment.getStorageService()
538
+ return storageService.walkLocalDir(dir)
539
+ }
540
+
541
+ /**
542
+ * 绑定自定义域名
543
+ * @param {IBindDomainOptions} options
544
+ * @returns
545
+ * @memberof HostingService
546
+ */
547
+ @preLazy()
548
+ async CreateHostingDomain(options: IBindDomainOptions) {
549
+ const { envId } = this.getHostingConfig()
550
+ const { certId, domain } = options
551
+ const res = await this.tcbService.request('CreateHostingDomain', {
552
+ EnvId: envId,
553
+ Domain: domain,
554
+ CertId: certId
555
+ })
556
+
557
+ return res
558
+ }
559
+
560
+ /**
561
+ * 删除托管域名
562
+ *
563
+ * @param {IBindDomainOptions} options
564
+ * @returns
565
+ * @memberof HostingService
566
+ */
567
+ @preLazy()
568
+ async deleteHostingDomain(options: IDeleteDomainOptions) {
569
+ const { envId } = this.getHostingConfig()
570
+ const { domain } = options
571
+ return this.tcbService.request<{ RequestId: string }>('DeleteHostingDomain', {
572
+ EnvId: envId,
573
+ Domain: domain
574
+ })
575
+ }
576
+
577
+ /**
578
+ * 查询域名状态信息
579
+ * @param options
580
+ */
581
+ async tcbCheckResource(options: ICheckSourceOptions) {
582
+ return this.cdnService.request<{
583
+ Domains: IDomainInfo[]
584
+ RecordCount: number
585
+ RequestId: string
586
+ }>('TcbCheckResource', {
587
+ Domains: options.domains
588
+ })
589
+ }
590
+
591
+ /**
592
+ * 域名配置变更
593
+ * @param options
594
+ */
595
+ async tcbModifyAttribute(options: IModifyOptions) {
596
+ const { domain, domainId, domainConfig } = options
597
+
598
+ const res = await this.cdnService.request('TcbModifyAttribute', {
599
+ Domain: domain,
600
+ DomainId: domainId,
601
+ DomainConfig: domainConfig
602
+ })
603
+ return res
604
+ }
605
+
606
+ /**
607
+ * 查询静态网站配置
608
+ * @memberof HostingService
609
+ */
610
+ async getWebsiteConfig() {
611
+ const hosting = await this.checkStatus()
612
+ const { Bucket, Regoin } = hosting
613
+ const storageService = await this.environment.getStorageService()
614
+ const res = await storageService.getWebsiteConfig({ bucket: Bucket, region: Regoin })
615
+ return res
616
+ }
617
+
618
+ /**
619
+ * 配置静态网站文档
620
+ * @param options
621
+ */
622
+ async setWebsiteDocument(options: IBucketWebsiteOptiosn) {
623
+ const { indexDocument, errorDocument, routingRules } = options
624
+ const hosting = await this.checkStatus()
625
+ const { Bucket, Regoin } = hosting
626
+
627
+ const storageService = await this.environment.getStorageService()
628
+ const res = await storageService.putBucketWebsite({
629
+ bucket: Bucket,
630
+ region: Regoin,
631
+ indexDocument,
632
+ errorDocument,
633
+ routingRules
634
+ })
635
+ return res
636
+ }
637
+
638
+ /**
639
+ * 检查 hosting 服务状态
640
+ */
641
+ @preLazy()
642
+ private async checkStatus() {
643
+ const hostings = await this.getInfo()
644
+
645
+ if (!hostings || !hostings.length) {
646
+ throw new CloudBaseError(
647
+ `您还没有开启静态网站服务,请先到云开发控制台开启静态网站服务!`,
648
+ {
649
+ code: 'INVALID_OPERATION'
650
+ }
651
+ )
652
+ }
653
+
654
+ const website = hostings[0]
655
+
656
+ if (website.Status !== 'online') {
657
+ throw new CloudBaseError(
658
+ `静态网站服务【${HostingStatusMap[website.Status]}】,无法进行此操作!`,
659
+ {
660
+ code: 'INVALID_OPERATION'
661
+ }
662
+ )
663
+ }
664
+
665
+ return website
666
+ }
667
+
668
+ /**
669
+ * 获取配置
670
+ */
671
+ private getHostingConfig() {
672
+ const envConfig = this.environment.lazyEnvironmentConfig
673
+ const appId = envConfig.Storages[0]?.AppId
674
+ const { proxy } = this.environment.cloudBaseContext
675
+
676
+ return {
677
+ appId,
678
+ proxy,
679
+ envId: envConfig.EnvId
680
+ }
681
+ }
682
+
683
+ /**
684
+ * 将 cloudPath 转换成 cloudPath/ 形式
685
+ */
686
+ private getCloudKey(cloudPath: string): string {
687
+ if (!cloudPath) {
688
+ return ''
689
+ }
690
+
691
+ // 单个 / 转换成根目录
692
+ if (cloudPath === '/') {
693
+ return ''
694
+ }
695
+
696
+ return cloudPath[cloudPath.length - 1] === '/' ? cloudPath : `${cloudPath}/`
697
+ }
698
+ }