@abtnode/core 1.8.64-beta-b3d407e0 → 1.8.64-beta-f211f0c5
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/lib/api/team.js +48 -7
- package/lib/blocklet/manager/disk.js +40 -5
- package/lib/blocklet/storage/backup/audit-log.js +27 -0
- package/lib/blocklet/storage/backup/base.js +63 -0
- package/lib/blocklet/storage/backup/blocklet-extras.js +105 -0
- package/lib/blocklet/storage/backup/blocklet.js +53 -0
- package/lib/blocklet/storage/backup/blocklets.js +80 -0
- package/lib/blocklet/storage/backup/data.js +19 -0
- package/lib/blocklet/storage/backup/logs.js +23 -0
- package/lib/blocklet/storage/backup/routing-rule.js +19 -0
- package/lib/blocklet/storage/backup/spaces.js +189 -0
- package/lib/blocklet/storage/restore/base.js +55 -0
- package/lib/blocklet/storage/restore/blocklet-extras.js +84 -0
- package/lib/blocklet/storage/restore/blocklets.js +24 -0
- package/lib/blocklet/storage/restore/spaces.js +137 -0
- package/lib/index.js +4 -1
- package/lib/states/blocklet-extras.js +4 -0
- package/lib/states/blocklet.js +0 -22
- package/lib/util/blocklet.js +23 -2
- package/lib/validators/util.js +1 -0
- package/package.json +30 -27
- package/lib/validators/blocklet.js +0 -5
package/lib/api/team.js
CHANGED
|
@@ -408,6 +408,26 @@ class TeamAPI extends EventEmitter {
|
|
|
408
408
|
|
|
409
409
|
// Invite member
|
|
410
410
|
|
|
411
|
+
/**
|
|
412
|
+
* @type InvitationSession {
|
|
413
|
+
* type: 'invite';
|
|
414
|
+
* role: string;
|
|
415
|
+
* remark: string;
|
|
416
|
+
* expireDate: number;
|
|
417
|
+
* inviter: {
|
|
418
|
+
* did: string;
|
|
419
|
+
* email?: string;
|
|
420
|
+
* fullName?: string;
|
|
421
|
+
* role?: string;
|
|
422
|
+
* };
|
|
423
|
+
* teamDid: string;
|
|
424
|
+
* status: '' | 'success';
|
|
425
|
+
* receiver: {
|
|
426
|
+
* did: string;
|
|
427
|
+
* };
|
|
428
|
+
* }
|
|
429
|
+
* @returns
|
|
430
|
+
*/
|
|
411
431
|
async createMemberInvitation({ teamDid, role, expireTime, remark }, context) {
|
|
412
432
|
await this.teamManager.checkEnablePassportIssuance(teamDid);
|
|
413
433
|
|
|
@@ -484,15 +504,17 @@ class TeamAPI extends EventEmitter {
|
|
|
484
504
|
expireDate: new Date(invitation.expireDate).toString(),
|
|
485
505
|
inviter: invitation.inviter,
|
|
486
506
|
teamDid: invitation.teamDid,
|
|
507
|
+
status: invitation.status,
|
|
508
|
+
receiver: invitation.receiver,
|
|
487
509
|
};
|
|
488
510
|
}
|
|
489
511
|
|
|
490
|
-
async getInvitations({ teamDid }) {
|
|
512
|
+
async getInvitations({ teamDid, filter }) {
|
|
491
513
|
const state = await this.getSessionState(teamDid);
|
|
492
514
|
|
|
493
515
|
const invitations = await state.find({ type: 'invite' });
|
|
494
516
|
|
|
495
|
-
return invitations.map((d) => ({
|
|
517
|
+
return invitations.filter(filter || ((x) => x.status !== 'success')).map((d) => ({
|
|
496
518
|
// eslint-disable-next-line no-underscore-dangle
|
|
497
519
|
inviteId: d._id,
|
|
498
520
|
role: d.role,
|
|
@@ -500,6 +522,8 @@ class TeamAPI extends EventEmitter {
|
|
|
500
522
|
expireDate: new Date(d.expireDate).toString(),
|
|
501
523
|
inviter: d.inviter,
|
|
502
524
|
teamDid: d.teamDid,
|
|
525
|
+
status: d.status,
|
|
526
|
+
receiver: d.receiver,
|
|
503
527
|
}));
|
|
504
528
|
}
|
|
505
529
|
|
|
@@ -516,7 +540,7 @@ class TeamAPI extends EventEmitter {
|
|
|
516
540
|
return true;
|
|
517
541
|
}
|
|
518
542
|
|
|
519
|
-
async
|
|
543
|
+
async checkInvitation({ teamDid, inviteId }) {
|
|
520
544
|
const state = await this.getSessionState(teamDid);
|
|
521
545
|
|
|
522
546
|
const invitation = await state.read(inviteId);
|
|
@@ -538,16 +562,33 @@ class TeamAPI extends EventEmitter {
|
|
|
538
562
|
throw new Error(`Role does not exist: ${role}`);
|
|
539
563
|
}
|
|
540
564
|
|
|
541
|
-
await state.end(inviteId);
|
|
542
|
-
|
|
543
|
-
logger.info('Invitation session completed successfully', { inviteId, role });
|
|
544
|
-
|
|
545
565
|
return {
|
|
546
566
|
role,
|
|
547
567
|
remark,
|
|
548
568
|
};
|
|
549
569
|
}
|
|
550
570
|
|
|
571
|
+
async closeInvitation({ teamDid, inviteId, status, receiver, timeout = 30 * 1000 }) {
|
|
572
|
+
const state = await this.getSessionState(teamDid);
|
|
573
|
+
|
|
574
|
+
const invitation = await state.read(inviteId);
|
|
575
|
+
|
|
576
|
+
if (!invitation) {
|
|
577
|
+
throw new Error(`The invitation does not exist: ${inviteId}`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
await state.update(inviteId, { status, receiver });
|
|
581
|
+
|
|
582
|
+
setTimeout(async () => {
|
|
583
|
+
try {
|
|
584
|
+
logger.info('Invitation session closed', { inviteId });
|
|
585
|
+
await state.end(inviteId);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
logger.error('close invitation failed', { error });
|
|
588
|
+
}
|
|
589
|
+
}, timeout);
|
|
590
|
+
}
|
|
591
|
+
|
|
551
592
|
// Issue Passport
|
|
552
593
|
|
|
553
594
|
async createPassportIssuance({ teamDid, ownerDid, name }) {
|
|
@@ -134,6 +134,8 @@ const handleInstanceInStore = require('../../util/public-to-store');
|
|
|
134
134
|
const { getNFTState, getServerDidDomain } = require('../../util');
|
|
135
135
|
const { BlockletRuntimeMonitor } = require('../../monitor/blocklet-runtime-monitor');
|
|
136
136
|
const getHistoryList = require('../../monitor/get-history-list');
|
|
137
|
+
const { SpacesBackup } = require('../storage/backup/spaces');
|
|
138
|
+
const { SpacesRestore } = require('../storage/restore/spaces');
|
|
137
139
|
const installFromBackup = require('./helper/install-from-backup');
|
|
138
140
|
|
|
139
141
|
const {
|
|
@@ -609,6 +611,35 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
609
611
|
return blocklet;
|
|
610
612
|
}
|
|
611
613
|
|
|
614
|
+
/**
|
|
615
|
+
*
|
|
616
|
+
*
|
|
617
|
+
* @param {import('@abtnode/client').BackupToSpacesParams} input
|
|
618
|
+
* @memberof BlockletManager
|
|
619
|
+
*/
|
|
620
|
+
async backupToSpaces(input) {
|
|
621
|
+
const spacesBackup = new SpacesBackup(input);
|
|
622
|
+
await spacesBackup.backup();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
*
|
|
627
|
+
*
|
|
628
|
+
* @param {import('@abtnode/client').RequestRestoreFromSpacesInput} input
|
|
629
|
+
* @memberof BlockletManager
|
|
630
|
+
*/
|
|
631
|
+
async restoreFromSpaces(input) {
|
|
632
|
+
const spacesRestore = new SpacesRestore(input);
|
|
633
|
+
await spacesRestore.restore();
|
|
634
|
+
|
|
635
|
+
// FIXME: 需要改成队列执行,本次失败,下次还需要重试,页面刷新后也能知道最新的状态
|
|
636
|
+
await this._installFromBackup({
|
|
637
|
+
url: `file://${spacesRestore.blockletRestoreDir}`,
|
|
638
|
+
blockletSecretKey: spacesRestore.blockletWallet.secretKey,
|
|
639
|
+
moveDir: true,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
612
643
|
async restart({ did }, context) {
|
|
613
644
|
logger.info('restart blocklet', { did });
|
|
614
645
|
|
|
@@ -1617,6 +1648,11 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1617
1648
|
* /data
|
|
1618
1649
|
* /blocklet.json
|
|
1619
1650
|
* /blocklet_extras.json
|
|
1651
|
+
* @see Blocklet 端到端备份方案的设计与实现(一期) https://team.arcblock.io/comment/discussions/e62084d5-fafa-489e-91d5-defcd6e93223
|
|
1652
|
+
* @param {{ url: string, blockletSecretKey: string, moveDir: boolean}} [{ url }={}]
|
|
1653
|
+
* @param {Record<string, string>} [context={}]
|
|
1654
|
+
* @return {Promise<any>}
|
|
1655
|
+
* @memberof BlockletManager
|
|
1620
1656
|
*/
|
|
1621
1657
|
async _installFromBackup({ url, blockletSecretKey, moveDir } = {}, context = {}) {
|
|
1622
1658
|
return installFromBackup({ url, blockletSecretKey, moveDir, context, manager: this, states });
|
|
@@ -2090,10 +2126,9 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
2090
2126
|
|
|
2091
2127
|
if (controller?.nftId) {
|
|
2092
2128
|
// sometimes nedb will throw error if use states.blocklet.count({ ['controller.nftId']: controller.nftId })
|
|
2093
|
-
const
|
|
2094
|
-
const count = blocklets.filter((x) => x.controller?.nftId === controller.nftId).length;
|
|
2129
|
+
const installedCount = await states.blockletExtras.count({ 'controller.nftId': controller.nftId });
|
|
2095
2130
|
|
|
2096
|
-
if (
|
|
2131
|
+
if (installedCount >= (controller.appMaxCount || 1)) {
|
|
2097
2132
|
throw new Error(
|
|
2098
2133
|
`You can only install ${controller.appMaxCount} blocklet with this credential: ${controller.nftId}`
|
|
2099
2134
|
);
|
|
@@ -2691,7 +2726,6 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
2691
2726
|
source,
|
|
2692
2727
|
deployedFrom,
|
|
2693
2728
|
children,
|
|
2694
|
-
controller,
|
|
2695
2729
|
});
|
|
2696
2730
|
|
|
2697
2731
|
await validateBlocklet(blocklet);
|
|
@@ -3798,7 +3832,8 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
3798
3832
|
|
|
3799
3833
|
async _createNotification(did, notification) {
|
|
3800
3834
|
try {
|
|
3801
|
-
const
|
|
3835
|
+
const extra = await states.blockletExtras.getMeta(did);
|
|
3836
|
+
const isExternal = !!extra?.controller;
|
|
3802
3837
|
|
|
3803
3838
|
if (isExternal) {
|
|
3804
3839
|
return;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const { removeSync, outputJsonSync } = require('fs-extra');
|
|
2
|
+
const { join } = require('path');
|
|
3
|
+
const states = require('../../../states');
|
|
4
|
+
const { BaseBackup } = require('./base');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
*
|
|
9
|
+
* @class AuditLogBackup
|
|
10
|
+
*/
|
|
11
|
+
class AuditLogBackup extends BaseBackup {
|
|
12
|
+
filename = 'audit-log.json';
|
|
13
|
+
|
|
14
|
+
async export() {
|
|
15
|
+
/**
|
|
16
|
+
* @type {import('@abtnode/client').AuditLog}
|
|
17
|
+
*/
|
|
18
|
+
const auditLogs = await states.auditLog.find({
|
|
19
|
+
scope: this.blocklet.meta.did,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
removeSync(join(this.blockletBackupDir, this.filename));
|
|
23
|
+
outputJsonSync(join(this.blockletBackupDir, this.filename), auditLogs);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { AuditLogBackup };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
class BaseBackup {
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* @type {import('./spaces').SpaceBackupInput}
|
|
5
|
+
* @memberof BaseBackup
|
|
6
|
+
*/
|
|
7
|
+
input;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @description blocklet state 对象
|
|
11
|
+
* @type {import('@abtnode/client').BlockletState}
|
|
12
|
+
* @memberof BaseBackup
|
|
13
|
+
*/
|
|
14
|
+
blocklet;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @description 当前 blocklet 的数据目录
|
|
18
|
+
* @type {string}
|
|
19
|
+
* @memberof BaseBackup
|
|
20
|
+
*/
|
|
21
|
+
blockletBackupDir;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
*
|
|
25
|
+
* @description spaces 的 endpoint
|
|
26
|
+
* @type {import('@ocap/wallet').WalletObject}
|
|
27
|
+
* @memberof BaseBackup
|
|
28
|
+
*/
|
|
29
|
+
blockletWallet;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
*
|
|
33
|
+
* @description server 的数据目录
|
|
34
|
+
* @type {string}
|
|
35
|
+
* @memberof BaseBackup
|
|
36
|
+
*/
|
|
37
|
+
serverDataDir;
|
|
38
|
+
|
|
39
|
+
constructor(input) {
|
|
40
|
+
this.input = input;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
*
|
|
45
|
+
*
|
|
46
|
+
* @param {import('./spaces').SpacesBackup} spacesBackup
|
|
47
|
+
* @memberof BaseBackup
|
|
48
|
+
*/
|
|
49
|
+
ensureParams(spacesBackup) {
|
|
50
|
+
this.blocklet = spacesBackup.blocklet;
|
|
51
|
+
this.serverDataDir = spacesBackup.serverDataDir;
|
|
52
|
+
this.blockletBackupDir = spacesBackup.blockletBackupDir;
|
|
53
|
+
this.blockletWallet = spacesBackup.blockletWallet;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async export() {
|
|
57
|
+
throw new Error('not implemented');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
BaseBackup,
|
|
63
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const { removeSync, outputJsonSync, readFileSync } = require('fs-extra');
|
|
2
|
+
const { isEmpty, cloneDeep } = require('lodash');
|
|
3
|
+
const { join } = require('path');
|
|
4
|
+
const security = require('@abtnode/util/lib/security');
|
|
5
|
+
const { BLOCKLET_CONFIGURABLE_KEY } = require('@blocklet/constant');
|
|
6
|
+
const states = require('../../../states');
|
|
7
|
+
const { BaseBackup } = require('./base');
|
|
8
|
+
|
|
9
|
+
class BlockletExtrasBackup extends BaseBackup {
|
|
10
|
+
filename = 'blocklet-extras.json';
|
|
11
|
+
|
|
12
|
+
async export() {
|
|
13
|
+
const blockletExtra = await this.getBlockletExtra();
|
|
14
|
+
removeSync(join(this.blockletBackupDir, this.filename));
|
|
15
|
+
outputJsonSync(join(this.blockletBackupDir, this.filename), blockletExtra);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
*
|
|
20
|
+
* @description
|
|
21
|
+
* @return {Promise<import('@abtnode/client').BlockletState>}
|
|
22
|
+
* @memberof BlockletExtrasBackup
|
|
23
|
+
*/
|
|
24
|
+
async getBlockletExtra() {
|
|
25
|
+
/**
|
|
26
|
+
* @type {import('@abtnode/client').BlockletState}
|
|
27
|
+
*/
|
|
28
|
+
const blockletExtra = await states.blockletExtras.findOne({
|
|
29
|
+
did: this.blocklet.meta.did,
|
|
30
|
+
});
|
|
31
|
+
if (isEmpty(blockletExtra)) {
|
|
32
|
+
throw new Error('blockletExtra cannot be empty');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return this.cleanDate(blockletExtra);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
*
|
|
40
|
+
* @description 清理数据并加密
|
|
41
|
+
* @param {import('@abtnode/client').BlockletState} blockletExtraInput
|
|
42
|
+
* @return {Promise<void>}
|
|
43
|
+
* @memberof BlockletExtrasBackup
|
|
44
|
+
*/
|
|
45
|
+
async cleanDate(blockletExtraInput) {
|
|
46
|
+
const blockletExtra = cloneDeep(blockletExtraInput);
|
|
47
|
+
|
|
48
|
+
const queue = [blockletExtra];
|
|
49
|
+
while (queue.length) {
|
|
50
|
+
const currentBlockletExtra = queue.pop();
|
|
51
|
+
|
|
52
|
+
// 删除父 blocklet 的某些数据
|
|
53
|
+
if (currentBlockletExtra._id) {
|
|
54
|
+
delete currentBlockletExtra._id;
|
|
55
|
+
delete currentBlockletExtra.createdAt;
|
|
56
|
+
delete currentBlockletExtra.updatedAt;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 加解密
|
|
60
|
+
this.useBlockletEncryptConfigs(currentBlockletExtra.configs);
|
|
61
|
+
|
|
62
|
+
if (currentBlockletExtra?.children) {
|
|
63
|
+
queue.push(...currentBlockletExtra.children);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return blockletExtra;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
*
|
|
72
|
+
* @description 清理数据并加密
|
|
73
|
+
* @param {import('@abtnode/client').ConfigEntry[]} configs
|
|
74
|
+
* @return {void}
|
|
75
|
+
* @memberof BlockletExtrasBackup
|
|
76
|
+
*/
|
|
77
|
+
useBlockletEncryptConfigs(configs) {
|
|
78
|
+
// 准备加解密所需的参数
|
|
79
|
+
// @see: https://github.com/ArcBlock/blocklet-server/blob/f561ba7290285f2e23dccb6d5323eb4d43c3cc3e/core/state/lib/index.js#L59
|
|
80
|
+
const dek = readFileSync(join(this.serverDataDir, '.sock'));
|
|
81
|
+
|
|
82
|
+
for (const config of configs) {
|
|
83
|
+
// 置空 blocklet 的密钥,但是保留其他属性,这很重要
|
|
84
|
+
if (config.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK) {
|
|
85
|
+
config.value = '';
|
|
86
|
+
} else if (config.secure) {
|
|
87
|
+
// secure 为 true 的配置才需要被加密保存上次到 did spaces
|
|
88
|
+
|
|
89
|
+
const encryptByServer = config.value;
|
|
90
|
+
// 先从 blocklet server 解密
|
|
91
|
+
// @see https://github.com/ArcBlock/blocklet-server/blob/f40338168a66893f325464cea79ae54c43f623b1/core/state/lib/blocklet/extras.js#L139
|
|
92
|
+
const decryptByServer = security.decrypt(encryptByServer, this.blocklet.meta.did, dek);
|
|
93
|
+
// 再用 blocklet secret 加密,然后才可以上传到 spaces
|
|
94
|
+
const encryptByBlocklet = security.encrypt(
|
|
95
|
+
decryptByServer,
|
|
96
|
+
this.blockletWallet.address,
|
|
97
|
+
Buffer.from(this.blockletWallet.secretKey)
|
|
98
|
+
);
|
|
99
|
+
config.value = encryptByBlocklet;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { BlockletExtrasBackup };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const { removeSync, outputJsonSync } = require('fs-extra');
|
|
2
|
+
const { cloneDeep } = require('lodash');
|
|
3
|
+
const { join } = require('path');
|
|
4
|
+
const { BaseBackup } = require('./base');
|
|
5
|
+
|
|
6
|
+
class BlockletBackup extends BaseBackup {
|
|
7
|
+
filename = 'blocklet.json';
|
|
8
|
+
|
|
9
|
+
async export() {
|
|
10
|
+
const blocklet = await this.cleanData();
|
|
11
|
+
removeSync(join(this.blockletBackupDir, this.filename));
|
|
12
|
+
outputJsonSync(join(this.blockletBackupDir, this.filename), blocklet);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @description 清理数据
|
|
17
|
+
* @see blocklet.db 中需要删除哪些字段呢? https://github.com/ArcBlock/blocklet-server/issues/6120#issuecomment-1383798348
|
|
18
|
+
* @return {Promise<void>}
|
|
19
|
+
* @memberof BlockletBackup
|
|
20
|
+
*/
|
|
21
|
+
async cleanData() {
|
|
22
|
+
const originalBlocklet = cloneDeep(this.blocklet);
|
|
23
|
+
|
|
24
|
+
/** @type {import('@abtnode/client').ComponentState[]} */
|
|
25
|
+
const queue = [originalBlocklet];
|
|
26
|
+
|
|
27
|
+
// 广度优先遍历
|
|
28
|
+
while (queue.length) {
|
|
29
|
+
const currentBlocklet = queue.pop();
|
|
30
|
+
|
|
31
|
+
// 父组件才需要删除的属性
|
|
32
|
+
if (currentBlocklet._id) {
|
|
33
|
+
delete currentBlocklet._id;
|
|
34
|
+
delete currentBlocklet.createdAt;
|
|
35
|
+
delete currentBlocklet.startedAt;
|
|
36
|
+
delete currentBlocklet.installedAt;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 子组件和父组件都需要删除的属性
|
|
40
|
+
delete currentBlocklet.status;
|
|
41
|
+
delete currentBlocklet.ports;
|
|
42
|
+
delete currentBlocklet.environments;
|
|
43
|
+
|
|
44
|
+
if (currentBlocklet.children) {
|
|
45
|
+
queue.push(...currentBlocklet.children);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return originalBlocklet;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { BlockletBackup };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const { removeSync, existsSync, ensureDirSync, createWriteStream } = require('fs-extra');
|
|
2
|
+
const { join, dirname } = require('path');
|
|
3
|
+
const archiver = require('archiver');
|
|
4
|
+
const { BaseBackup } = require('./base');
|
|
5
|
+
|
|
6
|
+
class BlockletsBackup extends BaseBackup {
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
* @memberof BlockletsBackup
|
|
11
|
+
*/
|
|
12
|
+
async export() {
|
|
13
|
+
const blockletMetas = this.getBlockletMetas(this.blocklet);
|
|
14
|
+
const serverBlockletsDir = join(this.serverDataDir, 'blocklets');
|
|
15
|
+
|
|
16
|
+
const dirs = [];
|
|
17
|
+
for (const blockletMeta of blockletMetas) {
|
|
18
|
+
const sourceDir = join(serverBlockletsDir, blockletMeta.name, blockletMeta.version);
|
|
19
|
+
const destDir = join(blockletMeta.name, blockletMeta.version);
|
|
20
|
+
dirs.push({ sourceDir, destDir });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await this.dirsToZip(dirs, join(this.blockletBackupDir, 'blocklets.zip'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
*
|
|
28
|
+
*
|
|
29
|
+
* @param {import('@abtnode/client').BlockletState} blocklet
|
|
30
|
+
* @returns {Array<{name: string, version: string}>}
|
|
31
|
+
* @memberof BlockletsBackup
|
|
32
|
+
*/
|
|
33
|
+
getBlockletMetas(blocklet) {
|
|
34
|
+
if (!blocklet?.meta?.bundleName || !blocklet?.meta?.version) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const metas = [];
|
|
39
|
+
metas.push({
|
|
40
|
+
name: blocklet.meta.bundleName,
|
|
41
|
+
version: blocklet.meta.version,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
for (const child of blocklet.children) {
|
|
45
|
+
metas.push(...this.getBlockletMetas(child));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return metas;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {Array<{sourceDir: string, destDir: string}>} dirs: /some/folder/to/compress
|
|
53
|
+
* @param {String} zipPath: /path/to/created.zip
|
|
54
|
+
* @returns {Promise}
|
|
55
|
+
* @memberof BlockletsBackup
|
|
56
|
+
*/
|
|
57
|
+
async dirsToZip(dirs, zipPath) {
|
|
58
|
+
ensureDirSync(dirname(zipPath));
|
|
59
|
+
if (existsSync(zipPath)) {
|
|
60
|
+
removeSync(zipPath);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
64
|
+
const stream = createWriteStream(zipPath);
|
|
65
|
+
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
archive.on('error', (err) => reject(err));
|
|
68
|
+
stream.on('close', () => resolve());
|
|
69
|
+
|
|
70
|
+
for (const dir of dirs) {
|
|
71
|
+
archive.directory(dir.sourceDir, dir.destDir);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
archive.pipe(stream);
|
|
75
|
+
archive.finalize();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { BlockletsBackup };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { copySync } = require('fs-extra');
|
|
2
|
+
const { join } = require('path');
|
|
3
|
+
const { BaseBackup } = require('./base');
|
|
4
|
+
|
|
5
|
+
class DataBackup extends BaseBackup {
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
* @memberof BlockletsBackup
|
|
10
|
+
*/
|
|
11
|
+
async export() {
|
|
12
|
+
const blockletDataDir = join(this.serverDataDir, 'data', this.blocklet.meta.name);
|
|
13
|
+
const blockletBackupDataDir = join(this.blockletBackupDir, 'data');
|
|
14
|
+
|
|
15
|
+
copySync(blockletDataDir, blockletBackupDataDir, { overwrite: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { DataBackup };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { ensureDirSync, copySync, statSync } = require('fs-extra');
|
|
2
|
+
const { join } = require('path');
|
|
3
|
+
const { BaseBackup } = require('./base');
|
|
4
|
+
|
|
5
|
+
class LogsBackup extends BaseBackup {
|
|
6
|
+
async export() {
|
|
7
|
+
const sourceLogsDir = join(this.serverDataDir, 'logs', this.blocklet.meta.name);
|
|
8
|
+
ensureDirSync(sourceLogsDir);
|
|
9
|
+
|
|
10
|
+
const targetLogsDir = join(this.blockletBackupDir, 'logs');
|
|
11
|
+
|
|
12
|
+
copySync(sourceLogsDir, targetLogsDir, {
|
|
13
|
+
overwrite: true,
|
|
14
|
+
filter: (src) => {
|
|
15
|
+
return !statSync(src).isSymbolicLink();
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
LogsBackup,
|
|
23
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { removeSync, outputJsonSync } = require('fs-extra');
|
|
2
|
+
const { join } = require('path');
|
|
3
|
+
const states = require('../../../states');
|
|
4
|
+
const { BaseBackup } = require('./base');
|
|
5
|
+
|
|
6
|
+
class RoutingRuleBackup extends BaseBackup {
|
|
7
|
+
filename = 'routing_rule.json';
|
|
8
|
+
|
|
9
|
+
async export() {
|
|
10
|
+
const routingRule = await states.site.findOne({
|
|
11
|
+
domain: `${this.blocklet.meta.did}.blocklet-domain-group`,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
removeSync(join(this.blockletBackupDir, this.filename));
|
|
15
|
+
outputJsonSync(join(this.blockletBackupDir, this.filename), routingRule);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { RoutingRuleBackup };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{
|
|
3
|
+
* did?: string
|
|
4
|
+
* }} SpaceBackupInput
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { isValid } = require('@arcblock/did');
|
|
8
|
+
const { BLOCKLET_CONFIGURABLE_KEY } = require('@blocklet/constant');
|
|
9
|
+
const { getBlockletInfo } = require('@blocklet/meta');
|
|
10
|
+
const { SpaceClient, SyncFolderPushCommand } = require('@did-space/client');
|
|
11
|
+
const { ensureDirSync, existsSync, rmdirSync, removeSync } = require('fs-extra');
|
|
12
|
+
const { isEmpty } = require('lodash');
|
|
13
|
+
const { join } = require('path');
|
|
14
|
+
const states = require('../../../states');
|
|
15
|
+
const { AuditLogBackup } = require('./audit-log');
|
|
16
|
+
const { BlockletBackup } = require('./blocklet');
|
|
17
|
+
const { BlockletExtrasBackup } = require('./blocklet-extras');
|
|
18
|
+
const { BlockletsBackup } = require('./blocklets');
|
|
19
|
+
const { DataBackup } = require('./data');
|
|
20
|
+
const { LogsBackup } = require('./logs');
|
|
21
|
+
const { RoutingRuleBackup } = require('./routing-rule');
|
|
22
|
+
|
|
23
|
+
class SpacesBackup {
|
|
24
|
+
/**
|
|
25
|
+
*
|
|
26
|
+
* @type {SpaceBackupInput}
|
|
27
|
+
* @memberof SpacesBackup
|
|
28
|
+
*/
|
|
29
|
+
input;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @description blocklet state 对象
|
|
33
|
+
* @type {import('@abtnode/client').BlockletState}
|
|
34
|
+
* @memberof SpacesBackup
|
|
35
|
+
*/
|
|
36
|
+
blocklet;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @description 当前 blocklet 的数据目录
|
|
40
|
+
* @type {string}
|
|
41
|
+
* @memberof SpacesBackup
|
|
42
|
+
*/
|
|
43
|
+
blockletBackupDir;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
*
|
|
47
|
+
* @description server 的数据目录
|
|
48
|
+
* @type {string}
|
|
49
|
+
* @memberof SpacesBackup
|
|
50
|
+
*/
|
|
51
|
+
serverDataDir;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
*
|
|
55
|
+
* @description spaces 的 endpoint
|
|
56
|
+
* @type {string}
|
|
57
|
+
* @memberof SpacesBackup
|
|
58
|
+
*/
|
|
59
|
+
spacesEndpoint;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
*
|
|
63
|
+
* @description spaces 的 endpoint
|
|
64
|
+
* @type {import('@ocap/wallet').WalletObject}
|
|
65
|
+
* @memberof SpacesBackup
|
|
66
|
+
*/
|
|
67
|
+
blockletWallet;
|
|
68
|
+
|
|
69
|
+
storages;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
*
|
|
73
|
+
* @param {SpaceBackupInput} input
|
|
74
|
+
* @memberof SpacesBackup
|
|
75
|
+
*/
|
|
76
|
+
constructor(input) {
|
|
77
|
+
this.verify(input);
|
|
78
|
+
this.input = input;
|
|
79
|
+
this.storages = [
|
|
80
|
+
new AuditLogBackup(this.input),
|
|
81
|
+
new LogsBackup(this.input),
|
|
82
|
+
new BlockletBackup(this.input),
|
|
83
|
+
new BlockletsBackup(this.input),
|
|
84
|
+
new BlockletExtrasBackup(this.input),
|
|
85
|
+
new RoutingRuleBackup(this.input),
|
|
86
|
+
new DataBackup(this.input),
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {SpaceBackupInput} input
|
|
92
|
+
* @returns {void}
|
|
93
|
+
* @memberof SpacesBackup
|
|
94
|
+
*/
|
|
95
|
+
verify(input) {
|
|
96
|
+
if (isEmpty(input?.did) || !isValid(input?.did)) {
|
|
97
|
+
throw new Error(`input.did(${input?.did}) is not a valid did`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
*
|
|
103
|
+
* @returns {Promise<void>}
|
|
104
|
+
* @memberof SpacesBackup
|
|
105
|
+
*/
|
|
106
|
+
async backup() {
|
|
107
|
+
await this.initialize();
|
|
108
|
+
await this.export();
|
|
109
|
+
await this.syncToSpaces();
|
|
110
|
+
await this.destroy();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async initialize() {
|
|
114
|
+
this.blocklet = await states.blocklet.findOne({
|
|
115
|
+
appDid: this.input.did,
|
|
116
|
+
});
|
|
117
|
+
if (isEmpty(this.blocklet)) {
|
|
118
|
+
throw new Error('blocklet cannot be empty');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.serverDataDir = process.env.ABT_NODE_DATA_DIR;
|
|
122
|
+
|
|
123
|
+
this.blockletBackupDir = join(process.env.ABT_NODE_DATA_DIR, 'tmp/backup', this.blocklet.meta.did);
|
|
124
|
+
if (existsSync(this.blockletBackupDir)) {
|
|
125
|
+
rmdirSync(this.blockletBackupDir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
ensureDirSync(this.blockletBackupDir);
|
|
128
|
+
|
|
129
|
+
this.spacesEndpoint = this.blocklet.environments.find(
|
|
130
|
+
(e) => e.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPACE_ENDPOINT
|
|
131
|
+
)?.value;
|
|
132
|
+
if (isEmpty(this.spacesEndpoint)) {
|
|
133
|
+
throw new Error('spacesEndpoint cannot be empty');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.blockletWallet = await this.getBlockletWallet();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async export() {
|
|
140
|
+
await Promise.all(
|
|
141
|
+
this.storages.map((storage) => {
|
|
142
|
+
storage.ensureParams(this);
|
|
143
|
+
return storage.export();
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async syncToSpaces() {
|
|
149
|
+
const wallet = await this.getBlockletWallet();
|
|
150
|
+
const spaceClient = new SpaceClient({
|
|
151
|
+
endpoint: this.spacesEndpoint,
|
|
152
|
+
wallet,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// FIXME: 在 Spaces 里面预览出 blocklet 的样式,需要规划一个好的数据结构
|
|
156
|
+
const { errorCount } = await spaceClient.send(
|
|
157
|
+
new SyncFolderPushCommand({
|
|
158
|
+
source: join(this.blockletBackupDir, '/'),
|
|
159
|
+
target: join('.did-objects', this.blocklet.appDid),
|
|
160
|
+
debug: true,
|
|
161
|
+
retryCount: 3,
|
|
162
|
+
filter: (object) => {
|
|
163
|
+
return object.name !== '.DS_Store';
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (errorCount !== 0) {
|
|
169
|
+
throw new Error(`Sync to spaces encountered ${errorCount} error`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async getBlockletWallet() {
|
|
174
|
+
const blockletInfo = await states.blocklet.getBlocklet(this.input.did);
|
|
175
|
+
const nodeInfo = await states.node.read();
|
|
176
|
+
const { wallet } = getBlockletInfo(blockletInfo, nodeInfo.sk);
|
|
177
|
+
return wallet;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async destroy() {
|
|
181
|
+
if (existsSync(this.blockletBackupDir)) {
|
|
182
|
+
removeSync(this.blockletBackupDir);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
SpacesBackup,
|
|
189
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
class BaseRestore {
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* @type {import('./spaces').SpaceRestoreInput}
|
|
5
|
+
* @memberof BaseRestore
|
|
6
|
+
*/
|
|
7
|
+
input;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @description 当前 blocklet 的数据目录
|
|
11
|
+
* @type {string}
|
|
12
|
+
* @memberof BaseRestore
|
|
13
|
+
*/
|
|
14
|
+
blockletRestoreDir;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* @description spaces 的 endpoint
|
|
19
|
+
* @type {import('@ocap/wallet').WalletObject}
|
|
20
|
+
* @memberof BaseRestore
|
|
21
|
+
*/
|
|
22
|
+
blockletWallet;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
*
|
|
26
|
+
* @description server 的数据目录
|
|
27
|
+
* @type {string}
|
|
28
|
+
* @memberof BaseRestore
|
|
29
|
+
*/
|
|
30
|
+
serverDataDir;
|
|
31
|
+
|
|
32
|
+
constructor(input) {
|
|
33
|
+
this.input = input;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
*
|
|
38
|
+
*
|
|
39
|
+
* @param {import('./spaces').SpacesRestore} spaces
|
|
40
|
+
* @memberof BaseRestore
|
|
41
|
+
*/
|
|
42
|
+
ensureParams(spaces) {
|
|
43
|
+
this.blockletRestoreDir = spaces.blockletRestoreDir;
|
|
44
|
+
this.blockletWallet = spaces.blockletWallet;
|
|
45
|
+
this.serverDataDir = spaces.serverDataDir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async import() {
|
|
49
|
+
throw new Error('not implemented');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
BaseRestore,
|
|
55
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const { removeSync, outputJsonSync, readJSONSync } = require('fs-extra');
|
|
2
|
+
const { cloneDeep } = require('lodash');
|
|
3
|
+
const { join } = require('path');
|
|
4
|
+
const security = require('@abtnode/util/lib/security');
|
|
5
|
+
const { BaseRestore } = require('./base');
|
|
6
|
+
|
|
7
|
+
class BlockletExtrasRestore extends BaseRestore {
|
|
8
|
+
filename = 'blocklet-extras.json';
|
|
9
|
+
|
|
10
|
+
async import() {
|
|
11
|
+
const blockletExtra = await this.getBlockletExtra();
|
|
12
|
+
removeSync(join(this.blockletRestoreDir, this.filename));
|
|
13
|
+
outputJsonSync(join(this.blockletRestoreDir, this.filename), blockletExtra);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* @description
|
|
19
|
+
* @return {Promise<import('@abtnode/client').BlockletState>}
|
|
20
|
+
* @memberof BlockletExtrasRestore
|
|
21
|
+
*/
|
|
22
|
+
async getBlockletExtra() {
|
|
23
|
+
/**
|
|
24
|
+
* @type {import('@abtnode/client').BlockletState}
|
|
25
|
+
*/
|
|
26
|
+
const blockletExtra = readJSONSync(join(this.blockletRestoreDir, this.filename));
|
|
27
|
+
|
|
28
|
+
return this.cleanData(blockletExtra);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
*
|
|
33
|
+
* @description 清理数据并加密
|
|
34
|
+
* @param {import('@abtnode/client').BlockletState} blockletExtraInput
|
|
35
|
+
* @return {Promise<void>}
|
|
36
|
+
* @memberof BlockletExtrasRestore
|
|
37
|
+
*/
|
|
38
|
+
async cleanData(blockletExtraInput) {
|
|
39
|
+
const blockletExtra = cloneDeep(blockletExtraInput);
|
|
40
|
+
|
|
41
|
+
const queue = [blockletExtra];
|
|
42
|
+
while (queue.length) {
|
|
43
|
+
const currentBlockletExtra = queue.pop();
|
|
44
|
+
|
|
45
|
+
// 加解密
|
|
46
|
+
this.useBlockletDecryptConfigs(currentBlockletExtra.configs);
|
|
47
|
+
|
|
48
|
+
if (currentBlockletExtra?.children) {
|
|
49
|
+
queue.push(...currentBlockletExtra.children);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return blockletExtra;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
*
|
|
58
|
+
* @description 清理数据并加密
|
|
59
|
+
* @param {import('@abtnode/client').ConfigEntry[]} configs
|
|
60
|
+
* @return {void}
|
|
61
|
+
* @memberof BlockletExtrasRestore
|
|
62
|
+
*/
|
|
63
|
+
useBlockletDecryptConfigs(configs) {
|
|
64
|
+
for (const config of configs) {
|
|
65
|
+
// secure 为 true 的配置才需要被加密保存上次到 did spaces
|
|
66
|
+
if (config.secure) {
|
|
67
|
+
const encryptByBlocklet = config.value;
|
|
68
|
+
|
|
69
|
+
// 再用 blocklet secret 加密,然后才可以上传到 spaces
|
|
70
|
+
const decryptByBlocklet = security.decrypt(
|
|
71
|
+
encryptByBlocklet,
|
|
72
|
+
this.blockletWallet.address,
|
|
73
|
+
Buffer.from(this.blockletWallet.secretKey)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
config.value = decryptByBlocklet;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return configs;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { BlockletExtrasRestore };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { existsSync, removeSync } = require('fs-extra');
|
|
2
|
+
const { join } = require('path');
|
|
3
|
+
const StreamZip = require('node-stream-zip');
|
|
4
|
+
const { BaseRestore } = require('./base');
|
|
5
|
+
|
|
6
|
+
class BlockletsRestore extends BaseRestore {
|
|
7
|
+
filename = 'blocklets.zip';
|
|
8
|
+
|
|
9
|
+
async import() {
|
|
10
|
+
const blockletZipPath = join(this.blockletRestoreDir, this.filename);
|
|
11
|
+
|
|
12
|
+
if (!existsSync(blockletZipPath)) {
|
|
13
|
+
throw new Error(`file not found: ${blockletZipPath}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line new-cap
|
|
17
|
+
const zip = new StreamZip.async({ file: blockletZipPath });
|
|
18
|
+
await zip.extract(null, join(this.blockletRestoreDir, 'blocklets'));
|
|
19
|
+
await zip.close();
|
|
20
|
+
removeSync(blockletZipPath);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { BlockletsRestore };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{
|
|
3
|
+
* endpoint: string;
|
|
4
|
+
* blockletSecretKey: string;
|
|
5
|
+
* }} SpaceRestoreInput
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { SpaceClient, SyncFolderPullCommand } = require('@did-space/client');
|
|
9
|
+
const { types } = require('@ocap/mcrypto');
|
|
10
|
+
const { fromSecretKey, WalletType } = require('@ocap/wallet');
|
|
11
|
+
const { ensureDirSync, existsSync, rmdirSync } = require('fs-extra');
|
|
12
|
+
const { join } = require('path');
|
|
13
|
+
const validUrl = require('valid-url');
|
|
14
|
+
const { BlockletExtrasRestore } = require('./blocklet-extras');
|
|
15
|
+
const { BlockletsRestore } = require('./blocklets');
|
|
16
|
+
|
|
17
|
+
class SpacesRestore {
|
|
18
|
+
/**
|
|
19
|
+
*
|
|
20
|
+
* @type {SpaceRestoreInput}
|
|
21
|
+
* @memberof SpacesRestore
|
|
22
|
+
*/
|
|
23
|
+
input;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @description 当前 blocklet 的数据目录
|
|
27
|
+
* @type {string}
|
|
28
|
+
* @memberof SpacesRestore
|
|
29
|
+
*/
|
|
30
|
+
blockletRestoreDir;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
*
|
|
34
|
+
* @description server 的数据目录
|
|
35
|
+
* @type {string}
|
|
36
|
+
* @memberof SpacesRestore
|
|
37
|
+
*/
|
|
38
|
+
serverDataDir;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
*
|
|
42
|
+
* @description spaces 的 endpoint
|
|
43
|
+
* @type {import('@ocap/wallet').WalletObject}
|
|
44
|
+
* @memberof SpacesRestore
|
|
45
|
+
*/
|
|
46
|
+
blockletWallet;
|
|
47
|
+
|
|
48
|
+
storages;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
*
|
|
52
|
+
* @param {SpaceRestoreInput} input
|
|
53
|
+
* @memberof SpacesRestore
|
|
54
|
+
*/
|
|
55
|
+
constructor(input) {
|
|
56
|
+
this.verify(input);
|
|
57
|
+
this.input = input;
|
|
58
|
+
this.storages = [new BlockletExtrasRestore(this.input), new BlockletsRestore(this.input)];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
*
|
|
63
|
+
* @param {SpaceRestoreInput} input
|
|
64
|
+
* @returns {void}
|
|
65
|
+
* @memberof SpacesRestore
|
|
66
|
+
*/
|
|
67
|
+
verify(input) {
|
|
68
|
+
if (!validUrl.isWebUri(input.endpoint)) {
|
|
69
|
+
throw new Error(`endpoint(${input.endpoint}) must be a WebUri`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async initialize() {
|
|
74
|
+
this.serverDataDir = process.env.ABT_NODE_DATA_DIR;
|
|
75
|
+
this.blockletWallet = await this.getBlockletWallet();
|
|
76
|
+
|
|
77
|
+
this.blockletRestoreDir = join(process.env.ABT_NODE_DATA_DIR, 'tmp/restore', this.blockletWallet.address);
|
|
78
|
+
if (existsSync(this.blockletRestoreDir)) {
|
|
79
|
+
rmdirSync(this.blockletRestoreDir, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
ensureDirSync(this.blockletRestoreDir);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
*
|
|
86
|
+
* @returns {Promise<void>}
|
|
87
|
+
* @memberof SpacesRestore
|
|
88
|
+
*/
|
|
89
|
+
async restore() {
|
|
90
|
+
await this.initialize();
|
|
91
|
+
await this.syncFromSpaces();
|
|
92
|
+
await this.import();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async getBlockletWallet() {
|
|
96
|
+
// @FIXME: blocklet 钱包类型如何得知呢?
|
|
97
|
+
const wallet = fromSecretKey(this.input.blockletSecretKey, WalletType({ role: types.RoleType.ROLE_APPLICATION }));
|
|
98
|
+
|
|
99
|
+
return wallet;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async syncFromSpaces() {
|
|
103
|
+
const { endpoint } = this.input;
|
|
104
|
+
const wallet = await this.getBlockletWallet();
|
|
105
|
+
|
|
106
|
+
const spaceClient = new SpaceClient({
|
|
107
|
+
endpoint,
|
|
108
|
+
wallet,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const { errorCount } = await spaceClient.send(
|
|
112
|
+
new SyncFolderPullCommand({
|
|
113
|
+
source: join('.did-objects', this.blockletWallet.address, '/'),
|
|
114
|
+
target: this.blockletRestoreDir,
|
|
115
|
+
debug: true,
|
|
116
|
+
retryCount: 3,
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (errorCount !== 0) {
|
|
121
|
+
throw new Error(`Sync from spaces encountered ${errorCount} error`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async import() {
|
|
126
|
+
await Promise.all(
|
|
127
|
+
this.storages.map((storage) => {
|
|
128
|
+
storage.ensureParams(this);
|
|
129
|
+
return storage.import();
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
SpacesRestore,
|
|
137
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -216,6 +216,8 @@ function ABTNode(options) {
|
|
|
216
216
|
updateWhoCanAccess: blockletManager.updateWhoCanAccess.bind(blockletManager),
|
|
217
217
|
updateComponentTitle: blockletManager.updateComponentTitle.bind(blockletManager),
|
|
218
218
|
updateComponentMountPoint: blockletManager.updateComponentMountPoint.bind(blockletManager),
|
|
219
|
+
backupToSpaces: blockletManager.backupToSpaces.bind(blockletManager),
|
|
220
|
+
restoreFromSpaces: blockletManager.restoreFromSpaces.bind(blockletManager),
|
|
219
221
|
|
|
220
222
|
// For diagnose purpose
|
|
221
223
|
syncBlockletStatus: blockletManager.status.bind(blockletManager),
|
|
@@ -259,8 +261,9 @@ function ABTNode(options) {
|
|
|
259
261
|
createTransferInvitation: teamAPI.createTransferInvitation.bind(teamAPI),
|
|
260
262
|
getInvitation: teamAPI.getInvitation.bind(teamAPI),
|
|
261
263
|
getInvitations: teamAPI.getInvitations.bind(teamAPI),
|
|
262
|
-
|
|
264
|
+
checkInvitation: teamAPI.checkInvitation.bind(teamAPI),
|
|
263
265
|
deleteInvitation: teamAPI.deleteInvitation.bind(teamAPI),
|
|
266
|
+
closeInvitation: teamAPI.closeInvitation.bind(teamAPI),
|
|
264
267
|
|
|
265
268
|
// Account
|
|
266
269
|
getUsers: teamAPI.getUsers.bind(teamAPI),
|
|
@@ -199,6 +199,10 @@ class BlockletExtrasState extends BaseState {
|
|
|
199
199
|
return super.update({ did }, { $set: entity }, { upsert: true });
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
async getMeta(did) {
|
|
203
|
+
return super.findOne({ did }, { did: 1, controller: 1, meta: 1 });
|
|
204
|
+
}
|
|
205
|
+
|
|
202
206
|
async updateExpireInfo({ did, expiredAt } = {}) {
|
|
203
207
|
const entity = { did, expiredAt };
|
|
204
208
|
await validateExpiredInfo(entity);
|
package/lib/states/blocklet.js
CHANGED
|
@@ -23,7 +23,6 @@ const logger = require('@abtnode/logger')('state-blocklet');
|
|
|
23
23
|
const BaseState = require('./base');
|
|
24
24
|
const { checkDuplicateComponents, ensureMeta } = require('../util/blocklet');
|
|
25
25
|
const { validateBlockletMeta } = require('../util');
|
|
26
|
-
const { validateBlockletController } = require('../validators/blocklet');
|
|
27
26
|
|
|
28
27
|
const lock = new Lock('blocklet-port-assign-lock');
|
|
29
28
|
|
|
@@ -122,21 +121,6 @@ class BlockletState extends BaseState {
|
|
|
122
121
|
});
|
|
123
122
|
}
|
|
124
123
|
|
|
125
|
-
async isExternalBlocklet(did) {
|
|
126
|
-
if (!did) {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const exist = await this.findOne({
|
|
131
|
-
$or: [{ 'meta.did': did }, { appDid: did }],
|
|
132
|
-
controller: {
|
|
133
|
-
$exists: true,
|
|
134
|
-
},
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
return !!exist;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
124
|
async getBlockletStatus(did) {
|
|
141
125
|
return new Promise((resolve, reject) => {
|
|
142
126
|
if (!did) {
|
|
@@ -212,7 +196,6 @@ class BlockletState extends BaseState {
|
|
|
212
196
|
deployedFrom = '',
|
|
213
197
|
mode = BLOCKLET_MODES.PRODUCTION,
|
|
214
198
|
children: rawChildren = [],
|
|
215
|
-
controller,
|
|
216
199
|
} = {}) {
|
|
217
200
|
return this.getBlocklet(meta.did).then(
|
|
218
201
|
(exist) =>
|
|
@@ -251,13 +234,8 @@ class BlockletState extends BaseState {
|
|
|
251
234
|
ports,
|
|
252
235
|
environments: [],
|
|
253
236
|
children,
|
|
254
|
-
controller,
|
|
255
237
|
};
|
|
256
238
|
|
|
257
|
-
if (controller) {
|
|
258
|
-
data.controller = await validateBlockletController(controller);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
239
|
// add to db
|
|
262
240
|
this.insert(data, (err, doc) => {
|
|
263
241
|
lock.release();
|
package/lib/util/blocklet.js
CHANGED
|
@@ -88,6 +88,7 @@ const {
|
|
|
88
88
|
findInterfacePortByName,
|
|
89
89
|
validateBlockletMeta,
|
|
90
90
|
prettyURL,
|
|
91
|
+
getNFTState,
|
|
91
92
|
} = require('./index');
|
|
92
93
|
|
|
93
94
|
const getComponentConfig = (meta) => meta.components || meta.children;
|
|
@@ -1392,6 +1393,11 @@ const getBlocklet = async ({
|
|
|
1392
1393
|
blocklet.enablePassportIssuance = get(settings, 'enablePassportIssuance', true);
|
|
1393
1394
|
blocklet.settings = settings || {};
|
|
1394
1395
|
|
|
1396
|
+
const extrasMeta = await states.blockletExtras.getMeta(blocklet.meta.did);
|
|
1397
|
+
if (extrasMeta) {
|
|
1398
|
+
blocklet.controller = extrasMeta.controller;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1395
1401
|
blocklet.settings.storeList = blocklet.settings.storeList || [];
|
|
1396
1402
|
|
|
1397
1403
|
if (!blocklet.settings.storeList.find((x) => x.url === BLOCKLET_STORE.url)) {
|
|
@@ -1491,7 +1497,11 @@ const getConfigFromPreferences = (blocklet) => {
|
|
|
1491
1497
|
|
|
1492
1498
|
const consumeServerlessNFT = async ({ nftId, nodeInfo, blocklet }) => {
|
|
1493
1499
|
try {
|
|
1494
|
-
const
|
|
1500
|
+
const state = await getNFTState(blocklet.controller.chainHost, nftId);
|
|
1501
|
+
if (!state) {
|
|
1502
|
+
throw new Error(`get nft state failed, chainHost: ${blocklet.controller.chainHost}, nftId: ${nftId}`);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1495
1505
|
const type = WalletType({
|
|
1496
1506
|
role: types.RoleType.ROLE_APPLICATION,
|
|
1497
1507
|
pk: types.KeyType.ED25519,
|
|
@@ -1502,7 +1512,8 @@ const consumeServerlessNFT = async ({ nftId, nodeInfo, blocklet }) => {
|
|
|
1502
1512
|
|
|
1503
1513
|
const body = { nftId, appURL };
|
|
1504
1514
|
|
|
1505
|
-
const {
|
|
1515
|
+
const { launcherUrl } = state.data.value;
|
|
1516
|
+
const { data } = await axios.post(joinURL(launcherUrl, '/api/serverless/consume'), body, {
|
|
1506
1517
|
headers: {
|
|
1507
1518
|
'x-sig': toBase58(wallet.sign(stableStringify(body))),
|
|
1508
1519
|
},
|
|
@@ -1618,6 +1629,16 @@ const validateAppConfig = async (config, blockletDid, states) => {
|
|
|
1618
1629
|
throw new Error(`${x.key}(${x.value}) is not a valid http address`);
|
|
1619
1630
|
}
|
|
1620
1631
|
}
|
|
1632
|
+
|
|
1633
|
+
if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPACES_URL) {
|
|
1634
|
+
if (isEmpty(x.value)) {
|
|
1635
|
+
throw new Error(`${x.key} can not be empty`);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (!isUrl(x.value)) {
|
|
1639
|
+
throw new Error(`${x.key}(${x.value}) is not a valid http address`);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1621
1642
|
};
|
|
1622
1643
|
|
|
1623
1644
|
const checkDuplicateAppSk = async ({ sk, did, states }) => {
|
package/lib/validators/util.js
CHANGED
|
@@ -13,6 +13,7 @@ const getMultipleLangParams = (context) => ({
|
|
|
13
13
|
const blockletController = Joi.object({
|
|
14
14
|
nftId: Joi.DID().required(),
|
|
15
15
|
nftOwner: Joi.DID().required(),
|
|
16
|
+
chainHost: Joi.string().uri().required(),
|
|
16
17
|
appMaxCount: Joi.number().required().min(1),
|
|
17
18
|
}).options({ stripUnknown: true });
|
|
18
19
|
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.8.64-beta-
|
|
6
|
+
"version": "1.8.64-beta-f211f0c5",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -19,32 +19,33 @@
|
|
|
19
19
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@abtnode/auth": "1.8.64-beta-
|
|
23
|
-
"@abtnode/certificate-manager": "1.8.64-beta-
|
|
24
|
-
"@abtnode/constant": "1.8.64-beta-
|
|
25
|
-
"@abtnode/cron": "1.8.64-beta-
|
|
26
|
-
"@abtnode/db": "1.8.64-beta-
|
|
27
|
-
"@abtnode/logger": "1.8.64-beta-
|
|
28
|
-
"@abtnode/queue": "1.8.64-beta-
|
|
29
|
-
"@abtnode/rbac": "1.8.64-beta-
|
|
30
|
-
"@abtnode/router-provider": "1.8.64-beta-
|
|
31
|
-
"@abtnode/static-server": "1.8.64-beta-
|
|
32
|
-
"@abtnode/timemachine": "1.8.64-beta-
|
|
33
|
-
"@abtnode/util": "1.8.64-beta-
|
|
34
|
-
"@arcblock/did": "1.18.
|
|
22
|
+
"@abtnode/auth": "1.8.64-beta-f211f0c5",
|
|
23
|
+
"@abtnode/certificate-manager": "1.8.64-beta-f211f0c5",
|
|
24
|
+
"@abtnode/constant": "1.8.64-beta-f211f0c5",
|
|
25
|
+
"@abtnode/cron": "1.8.64-beta-f211f0c5",
|
|
26
|
+
"@abtnode/db": "1.8.64-beta-f211f0c5",
|
|
27
|
+
"@abtnode/logger": "1.8.64-beta-f211f0c5",
|
|
28
|
+
"@abtnode/queue": "1.8.64-beta-f211f0c5",
|
|
29
|
+
"@abtnode/rbac": "1.8.64-beta-f211f0c5",
|
|
30
|
+
"@abtnode/router-provider": "1.8.64-beta-f211f0c5",
|
|
31
|
+
"@abtnode/static-server": "1.8.64-beta-f211f0c5",
|
|
32
|
+
"@abtnode/timemachine": "1.8.64-beta-f211f0c5",
|
|
33
|
+
"@abtnode/util": "1.8.64-beta-f211f0c5",
|
|
34
|
+
"@arcblock/did": "1.18.37",
|
|
35
35
|
"@arcblock/did-motif": "^1.1.10",
|
|
36
|
-
"@arcblock/did-util": "1.18.
|
|
37
|
-
"@arcblock/event-hub": "1.18.
|
|
38
|
-
"@arcblock/jwt": "^1.18.
|
|
36
|
+
"@arcblock/did-util": "1.18.37",
|
|
37
|
+
"@arcblock/event-hub": "1.18.37",
|
|
38
|
+
"@arcblock/jwt": "^1.18.37",
|
|
39
39
|
"@arcblock/pm2-events": "^0.0.5",
|
|
40
|
-
"@arcblock/vc": "1.18.
|
|
41
|
-
"@blocklet/constant": "1.8.64-beta-
|
|
42
|
-
"@blocklet/meta": "1.8.64-beta-
|
|
43
|
-
"@blocklet/sdk": "1.8.64-beta-
|
|
40
|
+
"@arcblock/vc": "1.18.37",
|
|
41
|
+
"@blocklet/constant": "1.8.64-beta-f211f0c5",
|
|
42
|
+
"@blocklet/meta": "1.8.64-beta-f211f0c5",
|
|
43
|
+
"@blocklet/sdk": "1.8.64-beta-f211f0c5",
|
|
44
|
+
"@did-space/client": "^0.1.66",
|
|
44
45
|
"@fidm/x509": "^1.2.1",
|
|
45
|
-
"@ocap/mcrypto": "1.18.
|
|
46
|
-
"@ocap/util": "1.18.
|
|
47
|
-
"@ocap/wallet": "1.18.
|
|
46
|
+
"@ocap/mcrypto": "1.18.37",
|
|
47
|
+
"@ocap/util": "1.18.37",
|
|
48
|
+
"@ocap/wallet": "1.18.37",
|
|
48
49
|
"@slack/webhook": "^5.0.4",
|
|
49
50
|
"archiver": "^5.3.1",
|
|
50
51
|
"axios": "^0.27.2",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"detect-port": "^1.5.1",
|
|
56
57
|
"escape-string-regexp": "^4.0.0",
|
|
57
58
|
"flat": "^5.0.2",
|
|
58
|
-
"fs-extra": "^
|
|
59
|
+
"fs-extra": "^11.1.0",
|
|
59
60
|
"get-port": "^5.1.1",
|
|
60
61
|
"is-base64": "^1.1.0",
|
|
61
62
|
"is-ip": "^3.1.0",
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
"js-yaml": "^4.1.0",
|
|
65
66
|
"lodash": "^4.17.21",
|
|
66
67
|
"lru-cache": "^6.0.0",
|
|
68
|
+
"node-stream-zip": "^1.15.0",
|
|
67
69
|
"p-limit": "^3.1.0",
|
|
68
70
|
"pm2": "^5.2.0",
|
|
69
71
|
"semver": "^7.3.8",
|
|
@@ -77,7 +79,8 @@
|
|
|
77
79
|
"ua-parser-js": "^1.0.2",
|
|
78
80
|
"unzipper": "^0.10.11",
|
|
79
81
|
"url-join": "^4.0.1",
|
|
80
|
-
"uuid": "7.0.3"
|
|
82
|
+
"uuid": "7.0.3",
|
|
83
|
+
"valid-url": "^1.0.9"
|
|
81
84
|
},
|
|
82
85
|
"devDependencies": {
|
|
83
86
|
"compression": "^1.7.4",
|
|
@@ -85,5 +88,5 @@
|
|
|
85
88
|
"express": "^4.18.2",
|
|
86
89
|
"jest": "^27.5.1"
|
|
87
90
|
},
|
|
88
|
-
"gitHead": "
|
|
91
|
+
"gitHead": "37ba774e398433766d5ef5d89970438e40d0aeac"
|
|
89
92
|
}
|