@abtnode/core 1.16.0-beta-7a7d5d97 → 1.16.0-beta-b741bcb3
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 +10 -4
- package/lib/blocklet/manager/disk.js +88 -38
- package/lib/event.js +1 -0
- package/lib/states/blocklet-extras.js +4 -0
- package/lib/states/user.js +22 -1
- package/lib/util/blocklet.js +9 -4
- package/package.json +17 -17
package/lib/api/team.js
CHANGED
|
@@ -136,7 +136,7 @@ class TeamAPI extends EventEmitter {
|
|
|
136
136
|
return doc;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
async getUsers({ teamDid, query, paging: inputPaging, sort, dids }) {
|
|
139
|
+
async getUsers({ teamDid, query, paging: inputPaging, sort, dids, sourceIds }) {
|
|
140
140
|
const state = await this.getUserState(teamDid);
|
|
141
141
|
|
|
142
142
|
if (inputPaging?.pageSize > MAX_USER_PAGE_SIZE) {
|
|
@@ -158,6 +158,14 @@ class TeamAPI extends EventEmitter {
|
|
|
158
158
|
pageCount: 1,
|
|
159
159
|
page: 1,
|
|
160
160
|
};
|
|
161
|
+
} else if (sourceIds) {
|
|
162
|
+
list = await state.getUsersBySourceIds({ query, sourceIds });
|
|
163
|
+
paging = {
|
|
164
|
+
total: list.length,
|
|
165
|
+
pageSize: sourceIds.length,
|
|
166
|
+
pageCount: 1,
|
|
167
|
+
page: 1,
|
|
168
|
+
};
|
|
161
169
|
} else {
|
|
162
170
|
const doc = await state.getUsers({ query, sort, paging: { pageSize: 20, ...inputPaging } });
|
|
163
171
|
list = doc.list;
|
|
@@ -185,8 +193,7 @@ class TeamAPI extends EventEmitter {
|
|
|
185
193
|
'locale',
|
|
186
194
|
// oauth relate fields
|
|
187
195
|
'source',
|
|
188
|
-
'
|
|
189
|
-
'connectedAccounts',
|
|
196
|
+
'extraConfigs',
|
|
190
197
|
])
|
|
191
198
|
// eslint-disable-next-line function-paren-newline
|
|
192
199
|
),
|
|
@@ -647,7 +654,6 @@ class TeamAPI extends EventEmitter {
|
|
|
647
654
|
}),
|
|
648
655
|
ownerProfile: {
|
|
649
656
|
...currentUser,
|
|
650
|
-
// avatar: await parseUserAvatar(currentUser.avatar, { dataDir: blocklet.env.dataDir }),
|
|
651
657
|
},
|
|
652
658
|
preferredColor: passportColor,
|
|
653
659
|
});
|
|
@@ -548,21 +548,29 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
548
548
|
this.emit(BlockletEvents.statusChange, blocklet);
|
|
549
549
|
}
|
|
550
550
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
551
|
+
try {
|
|
552
|
+
const nodeEnvironments = await states.node.getEnvironments();
|
|
553
|
+
await stopBlockletProcess(blocklet, {
|
|
554
|
+
preStop: (b, { ancestors }) =>
|
|
555
|
+
hooks.preStop(b.env.processId, {
|
|
556
|
+
appDir: b.env.appDir,
|
|
557
|
+
hooks: Object.assign(b.meta.hooks || {}, b.meta.scripts || {}),
|
|
558
|
+
env: getRuntimeEnvironments(b, nodeEnvironments, ancestors),
|
|
559
|
+
did, // root blocklet did
|
|
560
|
+
notification: states.notification,
|
|
561
|
+
context,
|
|
562
|
+
exitOnError: false,
|
|
563
|
+
silent,
|
|
564
|
+
}),
|
|
565
|
+
});
|
|
566
|
+
} catch (error) {
|
|
567
|
+
logger.error('Failed to stop blocklet', { error, did });
|
|
568
|
+
if (updateStatus) {
|
|
569
|
+
const res = await states.blocklet.setBlockletStatus(did, BlockletStatus.error);
|
|
570
|
+
this.emit(BlockletEvents.statusChange, res);
|
|
571
|
+
}
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
566
574
|
|
|
567
575
|
logger.info('blocklet stopped successfully', { processId, did });
|
|
568
576
|
|
|
@@ -1007,16 +1015,17 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1007
1015
|
const [rootDid, ...childDids] = dids;
|
|
1008
1016
|
logger.info('config blocklet', { dids });
|
|
1009
1017
|
|
|
1018
|
+
const ancestors = [];
|
|
1010
1019
|
let blocklet = await this.getBlocklet(rootDid);
|
|
1011
1020
|
for (const childDid of childDids) {
|
|
1021
|
+
ancestors.push(blocklet);
|
|
1012
1022
|
blocklet = blocklet.children.find((x) => x.meta.did === childDid);
|
|
1013
1023
|
if (!blocklet) {
|
|
1014
1024
|
throw new Error('Child blocklet does not exist', { dids });
|
|
1015
1025
|
}
|
|
1016
1026
|
}
|
|
1017
1027
|
|
|
1018
|
-
|
|
1019
|
-
const nodeEnvironments = await states.node.getEnvironments();
|
|
1028
|
+
const configObj = {};
|
|
1020
1029
|
for (const x of newConfigs) {
|
|
1021
1030
|
if (x.custom === true) {
|
|
1022
1031
|
// custom key
|
|
@@ -1036,21 +1045,25 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1036
1045
|
}
|
|
1037
1046
|
}
|
|
1038
1047
|
|
|
1039
|
-
|
|
1048
|
+
configObj[x.key] = x.value;
|
|
1040
1049
|
}
|
|
1041
1050
|
|
|
1051
|
+
// run hook
|
|
1042
1052
|
if (!skipHook) {
|
|
1053
|
+
const nodeEnvironments = await states.node.getEnvironments();
|
|
1043
1054
|
// FIXME: we should also call preConfig for child blocklets
|
|
1044
1055
|
await hooks.preConfig(blocklet.env.processId, {
|
|
1045
1056
|
appDir: blocklet.env.appDir,
|
|
1046
1057
|
hooks: Object.assign(blocklet.meta.hooks || {}, blocklet.meta.scripts || {}),
|
|
1047
1058
|
exitOnError: true,
|
|
1048
|
-
env: { ...getRuntimeEnvironments(blocklet, nodeEnvironments), ...
|
|
1059
|
+
env: { ...getRuntimeEnvironments(blocklet, nodeEnvironments, ancestors), ...configObj },
|
|
1049
1060
|
did,
|
|
1050
1061
|
context,
|
|
1051
1062
|
});
|
|
1052
1063
|
}
|
|
1053
1064
|
|
|
1065
|
+
Object.assign(blocklet.configObj, configObj);
|
|
1066
|
+
|
|
1054
1067
|
const willAppSkChange = isRotatingAppSk(newConfigs, blocklet.configs, blocklet.externalSk);
|
|
1055
1068
|
const willAppDidChange = isRotatingAppDid(newConfigs, blocklet.configs, blocklet.externalSk);
|
|
1056
1069
|
|
|
@@ -1404,6 +1417,7 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1404
1417
|
{
|
|
1405
1418
|
name: 'clean-expired-blocklet-data',
|
|
1406
1419
|
time: '0 */20 0 * * *', // 每天凌晨 0 点的每 20 分钟
|
|
1420
|
+
options: { runOnInit: false },
|
|
1407
1421
|
fn: () => this._cleanExpiredBlockletData(),
|
|
1408
1422
|
},
|
|
1409
1423
|
{
|
|
@@ -1720,6 +1734,7 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1720
1734
|
}
|
|
1721
1735
|
|
|
1722
1736
|
// update state to db
|
|
1737
|
+
await states.blockletExtras.updateByDid(did, { appDid: blocklet.appDid });
|
|
1723
1738
|
return states.blocklet.updateBlocklet(did, blocklet);
|
|
1724
1739
|
}
|
|
1725
1740
|
|
|
@@ -1851,24 +1866,46 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1851
1866
|
|
|
1852
1867
|
const children = component ? await this._getChildrenForInstallation(component) : [];
|
|
1853
1868
|
|
|
1854
|
-
|
|
1869
|
+
// FIXME @linchen
|
|
1870
|
+
// 当应用本身是容器时, 下载这个容器, 因为容器可能除 blocklet.yml 额外的文件
|
|
1871
|
+
// 本身就是容器的应用, 在容器中添加额外文件可能不是合理的做法
|
|
1872
|
+
// 容器只在安装时有效, 安装后容器无法升级
|
|
1873
|
+
const containerSourceUrl =
|
|
1874
|
+
component && component.meta.group === BlockletGroup.gateway && component.meta.dist && component.bundleSource?.url;
|
|
1875
|
+
|
|
1876
|
+
if (containerSourceUrl) {
|
|
1877
|
+
meta.bundleDid = component.meta.did;
|
|
1878
|
+
meta.bundleName = component.meta.name;
|
|
1879
|
+
meta.version = component.meta.version;
|
|
1880
|
+
meta.dist = component.meta.dist;
|
|
1881
|
+
meta.logo = component.meta.logo;
|
|
1882
|
+
} else if (children[0]?.meta?.logo) {
|
|
1855
1883
|
meta.logo = children[0].meta.logo;
|
|
1856
1884
|
}
|
|
1857
1885
|
|
|
1858
1886
|
await validateBlocklet({ meta, children });
|
|
1859
1887
|
|
|
1860
1888
|
// fake install bundle
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1889
|
+
if (!containerSourceUrl) {
|
|
1890
|
+
const bundleDir = getBundleDir(this.installDir, meta);
|
|
1891
|
+
fs.mkdirSync(bundleDir, { recursive: true });
|
|
1892
|
+
updateMetaFile(path.join(bundleDir, BLOCKLET_META_FILE), meta);
|
|
1893
|
+
}
|
|
1864
1894
|
|
|
1865
1895
|
// add blocklet to db
|
|
1866
|
-
const
|
|
1896
|
+
const params = {
|
|
1867
1897
|
meta,
|
|
1868
1898
|
source: BlockletSource.custom,
|
|
1869
1899
|
children,
|
|
1870
1900
|
mode,
|
|
1871
|
-
}
|
|
1901
|
+
};
|
|
1902
|
+
|
|
1903
|
+
if (containerSourceUrl) {
|
|
1904
|
+
params.source = BlockletSource.url;
|
|
1905
|
+
params.deployedFrom = containerSourceUrl;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
const blocklet = await states.blocklet.addBlocklet(params);
|
|
1872
1909
|
|
|
1873
1910
|
return blocklet;
|
|
1874
1911
|
}
|
|
@@ -1906,7 +1943,7 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1906
1943
|
logger.info('blocklet installed', { source, did: meta.did });
|
|
1907
1944
|
|
|
1908
1945
|
// logo
|
|
1909
|
-
if (blocklet.children[0]?.meta?.logo) {
|
|
1946
|
+
if (blocklet.source === BlockletSource.custom && blocklet.children[0]?.meta?.logo) {
|
|
1910
1947
|
const fileName = blocklet.children[0].meta.logo;
|
|
1911
1948
|
const src = path.join(getBundleDir(this.installDir, blocklet.children[0].meta), fileName);
|
|
1912
1949
|
const dist = path.join(getBundleDir(this.installDir, blocklet.meta), fileName);
|
|
@@ -2388,7 +2425,7 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
2388
2425
|
controller: {
|
|
2389
2426
|
$exists: true,
|
|
2390
2427
|
},
|
|
2391
|
-
|
|
2428
|
+
expiredAt: {
|
|
2392
2429
|
$exists: false,
|
|
2393
2430
|
},
|
|
2394
2431
|
},
|
|
@@ -2406,11 +2443,21 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
2406
2443
|
nftId: data.controller.nftId,
|
|
2407
2444
|
});
|
|
2408
2445
|
|
|
2409
|
-
await
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2446
|
+
const blocklet = await states.blocklet.getBlocklet(data.meta.did);
|
|
2447
|
+
if (blocklet) {
|
|
2448
|
+
// 预防 Blocklet 已经删除,但是 blocklet extra data 没有清理的场景
|
|
2449
|
+
await this._backupToDisk({ blocklet });
|
|
2450
|
+
logger.info('backed up the expired blocklet', {
|
|
2451
|
+
blockletDid: data.meta.did,
|
|
2452
|
+
nftId: data.controller.nftId,
|
|
2453
|
+
});
|
|
2454
|
+
|
|
2455
|
+
await this.delete({ did: data.meta.did, keepData: true, keepConfigs: true, keepLogsDir: true });
|
|
2456
|
+
logger.info('the expired blocklet already deleted', {
|
|
2457
|
+
blockletDid: data.meta.did,
|
|
2458
|
+
nftId: data.controller.nftId,
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2414
2461
|
|
|
2415
2462
|
const expiredAt = getNftExpirationDate(assetState);
|
|
2416
2463
|
await states.blockletExtras.updateExpireInfo({ did: data.meta.did, expiredAt });
|
|
@@ -2435,7 +2482,7 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
2435
2482
|
|
|
2436
2483
|
logger.info('check expired external blocklet end');
|
|
2437
2484
|
} catch (error) {
|
|
2438
|
-
logger.
|
|
2485
|
+
logger.error('check expired external blocklet failed', { error });
|
|
2439
2486
|
}
|
|
2440
2487
|
}
|
|
2441
2488
|
|
|
@@ -2448,16 +2495,19 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
2448
2495
|
return;
|
|
2449
2496
|
}
|
|
2450
2497
|
|
|
2451
|
-
const tasks = blockletExtras.map(async ({
|
|
2452
|
-
|
|
2453
|
-
|
|
2498
|
+
const tasks = blockletExtras.map(async ({ appDid, meta }) => {
|
|
2499
|
+
await this._cleanBlockletData({ blocklet: { meta }, keepData: false, keepLogsDir: false, keepConfigs: false });
|
|
2500
|
+
logger.info(`clean blocklet ${meta.did} extra data`);
|
|
2501
|
+
|
|
2502
|
+
await removeBackup(this.dataDirs.data, appDid);
|
|
2503
|
+
logger.info(`removed blocklet ${meta.did} backup`);
|
|
2454
2504
|
|
|
2455
2505
|
this.emit(BlockletEvents.dataCleaned, {
|
|
2456
|
-
blocklet,
|
|
2506
|
+
blocklet: { meta },
|
|
2457
2507
|
keepRouting: false,
|
|
2458
2508
|
});
|
|
2459
2509
|
|
|
2460
|
-
logger.info(`cleaned expired blocklet blocklet ${did} data`);
|
|
2510
|
+
logger.info(`cleaned expired blocklet blocklet ${meta.did} data`);
|
|
2461
2511
|
});
|
|
2462
2512
|
|
|
2463
2513
|
await Promise.all(tasks);
|
package/lib/event.js
CHANGED
package/lib/states/user.js
CHANGED
|
@@ -193,7 +193,7 @@ class User extends BaseState {
|
|
|
193
193
|
|
|
194
194
|
// 根据 derivedDid 查询 user 信息(wallet 账户绑定 oauth 账户后,会有该字段)
|
|
195
195
|
if (derivedDid) {
|
|
196
|
-
queryParam['derivedAccount.did'] = derivedDid;
|
|
196
|
+
queryParam['extraConfigs.derivedAccount.did'] = derivedDid;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
const sortParam = pickBy(sort, (x) => !isNullOrUndefined(x));
|
|
@@ -232,6 +232,27 @@ class User extends BaseState {
|
|
|
232
232
|
return this.find(queryParam);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/**
|
|
236
|
+
* get user list by sourceId list
|
|
237
|
+
* @param {string[]} sourceIds user sourceId list
|
|
238
|
+
* @returns {BlockletUser[]}
|
|
239
|
+
*/
|
|
240
|
+
async getUsersBySourceIds({ sourceIds, query } = {}) {
|
|
241
|
+
const { approved } = query || {};
|
|
242
|
+
const sourceIdList = sourceIds || [];
|
|
243
|
+
|
|
244
|
+
const queryParam = {
|
|
245
|
+
'extraConfigs.sourceId': { $in: sourceIdList },
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
if (!isNullOrUndefined(approved)) {
|
|
249
|
+
queryParam.approved = !!approved;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// get data
|
|
253
|
+
return this.find(queryParam);
|
|
254
|
+
}
|
|
255
|
+
|
|
235
256
|
/**
|
|
236
257
|
* get user by did
|
|
237
258
|
* @param {string} did user's did
|
package/lib/util/blocklet.js
CHANGED
|
@@ -460,7 +460,10 @@ const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors) => {
|
|
|
460
460
|
};
|
|
461
461
|
|
|
462
462
|
const isUsefulError = (err) =>
|
|
463
|
-
err &&
|
|
463
|
+
err &&
|
|
464
|
+
err.message !== 'process or namespace not found' &&
|
|
465
|
+
!/id unknown/.test(err.message) &&
|
|
466
|
+
!/^Process \d+ not found$/.test(err.message);
|
|
464
467
|
|
|
465
468
|
const getHealthyCheckTimeout = (blocklet, { checkHealthImmediately } = {}) => {
|
|
466
469
|
let minConsecutiveTime = 5000;
|
|
@@ -611,6 +614,10 @@ const stopBlockletProcess = async (blocklet, { preStop = noop, skippedProcessIds
|
|
|
611
614
|
await forEachBlocklet(
|
|
612
615
|
blocklet,
|
|
613
616
|
async (b, { ancestors }) => {
|
|
617
|
+
if (b.meta?.group === BlockletGroup.gateway) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
614
621
|
if (skippedProcessIds.includes(b.env.processId)) {
|
|
615
622
|
logger.info(`skip stop process ${b.env.processId}`);
|
|
616
623
|
return;
|
|
@@ -806,9 +813,7 @@ const parseComponents = async (component, context = {}) => {
|
|
|
806
813
|
try {
|
|
807
814
|
rawMeta = await getBlockletMetaFromUrls(urls, { logger });
|
|
808
815
|
} catch (error) {
|
|
809
|
-
throw new Error(
|
|
810
|
-
`Failed get component meta. Component: ${rawMeta.title || rawMeta.did}, reason: ${error.message}`
|
|
811
|
-
);
|
|
816
|
+
throw new Error(`Failed get component meta. Component: ${urls.join(', ')}, reason: ${error.message}`);
|
|
812
817
|
}
|
|
813
818
|
|
|
814
819
|
if (rawMeta.group === BlockletGroup.gateway) {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.16.0-beta-
|
|
6
|
+
"version": "1.16.0-beta-b741bcb3",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -19,18 +19,18 @@
|
|
|
19
19
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@abtnode/auth": "1.16.0-beta-
|
|
23
|
-
"@abtnode/certificate-manager": "1.16.0-beta-
|
|
24
|
-
"@abtnode/constant": "1.16.0-beta-
|
|
25
|
-
"@abtnode/cron": "1.16.0-beta-
|
|
26
|
-
"@abtnode/db": "1.16.0-beta-
|
|
27
|
-
"@abtnode/logger": "1.16.0-beta-
|
|
28
|
-
"@abtnode/queue": "1.16.0-beta-
|
|
29
|
-
"@abtnode/rbac": "1.16.0-beta-
|
|
30
|
-
"@abtnode/router-provider": "1.16.0-beta-
|
|
31
|
-
"@abtnode/static-server": "1.16.0-beta-
|
|
32
|
-
"@abtnode/timemachine": "1.16.0-beta-
|
|
33
|
-
"@abtnode/util": "1.16.0-beta-
|
|
22
|
+
"@abtnode/auth": "1.16.0-beta-b741bcb3",
|
|
23
|
+
"@abtnode/certificate-manager": "1.16.0-beta-b741bcb3",
|
|
24
|
+
"@abtnode/constant": "1.16.0-beta-b741bcb3",
|
|
25
|
+
"@abtnode/cron": "1.16.0-beta-b741bcb3",
|
|
26
|
+
"@abtnode/db": "1.16.0-beta-b741bcb3",
|
|
27
|
+
"@abtnode/logger": "1.16.0-beta-b741bcb3",
|
|
28
|
+
"@abtnode/queue": "1.16.0-beta-b741bcb3",
|
|
29
|
+
"@abtnode/rbac": "1.16.0-beta-b741bcb3",
|
|
30
|
+
"@abtnode/router-provider": "1.16.0-beta-b741bcb3",
|
|
31
|
+
"@abtnode/static-server": "1.16.0-beta-b741bcb3",
|
|
32
|
+
"@abtnode/timemachine": "1.16.0-beta-b741bcb3",
|
|
33
|
+
"@abtnode/util": "1.16.0-beta-b741bcb3",
|
|
34
34
|
"@arcblock/did": "1.18.64",
|
|
35
35
|
"@arcblock/did-motif": "^1.1.10",
|
|
36
36
|
"@arcblock/did-util": "1.18.64",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"@arcblock/jwt": "^1.18.64",
|
|
39
39
|
"@arcblock/pm2-events": "^0.0.5",
|
|
40
40
|
"@arcblock/vc": "1.18.64",
|
|
41
|
-
"@blocklet/constant": "1.16.0-beta-
|
|
42
|
-
"@blocklet/meta": "1.16.0-beta-
|
|
43
|
-
"@blocklet/sdk": "1.16.0-beta-
|
|
41
|
+
"@blocklet/constant": "1.16.0-beta-b741bcb3",
|
|
42
|
+
"@blocklet/meta": "1.16.0-beta-b741bcb3",
|
|
43
|
+
"@blocklet/sdk": "1.16.0-beta-b741bcb3",
|
|
44
44
|
"@did-space/client": "^0.2.45",
|
|
45
45
|
"@fidm/x509": "^1.2.1",
|
|
46
46
|
"@ocap/client": "1.18.64",
|
|
@@ -91,5 +91,5 @@
|
|
|
91
91
|
"express": "^4.18.2",
|
|
92
92
|
"jest": "^27.5.1"
|
|
93
93
|
},
|
|
94
|
-
"gitHead": "
|
|
94
|
+
"gitHead": "f41666daf2c499566ff2f8e76a2de4fa6e1122ba"
|
|
95
95
|
}
|