Skip to content

Commit eab5bef

Browse files
feat: download and upload snapshot offline (#382)
* feat: download snapshot offline * feat: download chunks * feat: download all memories * feat: print * feat: logs * feat: skip size zero * feat: use heap and stable * feat: download chunk store * feat: log * feat: more chunks * feat: rename and integrate for all modules * feat: smaller limit for batching * feat: add command to help * feat: init upload * feat: write stream to a single file * feat: assert size and write metadata once the process is complete * feat: read metadata * feat: upload snapshot * feat: check dir before loading snapshot and fix passing down args * fix: always a new snapshot id * feat: cleanup * chore: rename constant * refactor: extract and move types and schemas * refactor: move hash utils * chore: fmt and lint * docs: incorrect referenced lib
1 parent f98d008 commit eab5bef

19 files changed

Lines changed: 1193 additions & 46 deletions

package-lock.json

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@dfinity/agent": "^3.2.6",
2727
"@dfinity/auth-client": "^3.2.6",
2828
"@dfinity/candid": "^3.2.6",
29-
"@dfinity/ic-management": "^7.0.1",
29+
"@dfinity/ic-management": "^7.0.2-beta-2025-09-30",
3030
"@dfinity/identity": "^3.2.6",
3131
"@dfinity/principal": "^3.2.6",
3232
"@dfinity/zod-schemas": "^2.1.0",

src/api/ic.api.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import {ICManagementCanister} from '@dfinity/ic-management';
22
import type {
33
list_canister_snapshots_result,
4-
snapshot_id
4+
read_canister_snapshot_data_response,
5+
snapshot_id,
6+
upload_canister_snapshot_metadata_response
57
} from '@dfinity/ic-management/dist/candid/ic-management';
8+
import {
9+
type ReadCanisterSnapshotMetadataParams,
10+
type SnapshotParams,
11+
type UploadCanisterSnapshotDataParams,
12+
type UploadCanisterSnapshotMetadataParams
13+
} from '@dfinity/ic-management/dist/types/types/snapshot.params';
14+
import type {ReadCanisterSnapshotMetadataResponse} from '@dfinity/ic-management/dist/types/types/snapshot.responses';
615
import type {Principal} from '@dfinity/principal';
716
import {initAgent} from './agent.api';
817

@@ -76,3 +85,51 @@ export const deleteCanisterSnapshot = async (params: {
7685

7786
await deleteCanisterSnapshot(params);
7887
};
88+
89+
export const readCanisterSnapshotMetadata = async (
90+
params: SnapshotParams
91+
): Promise<ReadCanisterSnapshotMetadataResponse> => {
92+
const agent = await initAgent();
93+
94+
const {readCanisterSnapshotMetadata} = ICManagementCanister.create({
95+
agent
96+
});
97+
98+
return await readCanisterSnapshotMetadata(params);
99+
};
100+
101+
export const readCanisterSnapshotData = async (
102+
params: ReadCanisterSnapshotMetadataParams
103+
): Promise<read_canister_snapshot_data_response> => {
104+
const agent = await initAgent();
105+
106+
const {readCanisterSnapshotData} = ICManagementCanister.create({
107+
agent
108+
});
109+
110+
return await readCanisterSnapshotData(params);
111+
};
112+
113+
export const uploadCanisterSnapshotMetadata = async (
114+
params: UploadCanisterSnapshotMetadataParams
115+
): Promise<upload_canister_snapshot_metadata_response> => {
116+
const agent = await initAgent();
117+
118+
const {uploadCanisterSnapshotMetadata} = ICManagementCanister.create({
119+
agent
120+
});
121+
122+
return await uploadCanisterSnapshotMetadata(params);
123+
};
124+
125+
export const uploadCanisterSnapshotData = async (
126+
params: UploadCanisterSnapshotDataParams
127+
): Promise<void> => {
128+
const agent = await initAgent();
129+
130+
const {uploadCanisterSnapshotData} = ICManagementCanister.create({
131+
agent
132+
});
133+
134+
await uploadCanisterSnapshotData(params);
135+
};

src/commands/snapshot.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
import {nextArg} from '@junobuild/cli-tools';
22
import {red} from 'kleur';
33
import {logHelpSnapshot} from '../help/snapshot.help';
4+
import {logHelpSnapshotUpload} from '../help/snapshot.upload.help';
45
import {
56
createSnapshotMissionControl,
67
deleteSnapshotMissionControl,
7-
restoreSnapshotMissionControl
8+
downloadSnapshotMissionControl,
9+
restoreSnapshotMissionControl,
10+
uploadSnapshotMissionControl
811
} from '../services/modules/snapshot/snapshot.mission-control.services';
912
import {
1013
createSnapshotOrbiter,
1114
deleteSnapshotOrbiter,
12-
restoreSnapshotOrbiter
15+
downloadSnapshotOrbiter,
16+
restoreSnapshotOrbiter,
17+
uploadSnapshotOrbiter
1318
} from '../services/modules/snapshot/snapshot.orbiter.services';
1419
import {
1520
createSnapshotSatellite,
1621
deleteSnapshotSatellite,
17-
restoreSnapshotSatellite
22+
downloadSnapshotSatellite,
23+
restoreSnapshotSatellite,
24+
uploadSnapshotSatellite
1825
} from '../services/modules/snapshot/snapshot.satellite.services';
1926

2027
export const snapshot = async (args?: string[]) => {
@@ -45,6 +52,22 @@ export const snapshot = async (args?: string[]) => {
4552
orbiterFn: deleteSnapshotOrbiter
4653
});
4754
break;
55+
case 'download':
56+
await executeSnapshotFn({
57+
args,
58+
satelliteFn: downloadSnapshotSatellite,
59+
missionControlFn: downloadSnapshotMissionControl,
60+
orbiterFn: downloadSnapshotOrbiter
61+
});
62+
break;
63+
case 'upload':
64+
await executeSnapshotFn({
65+
args,
66+
satelliteFn: uploadSnapshotSatellite,
67+
missionControlFn: uploadSnapshotMissionControl,
68+
orbiterFn: uploadSnapshotOrbiter
69+
});
70+
break;
4871
default:
4972
console.log(red('Unknown subcommand.'));
5073
logHelpSnapshot(args);
@@ -58,27 +81,39 @@ const executeSnapshotFn = async ({
5881
orbiterFn
5982
}: {
6083
args?: string[];
61-
satelliteFn: () => Promise<void>;
62-
missionControlFn: () => Promise<void>;
63-
orbiterFn: () => Promise<void>;
84+
satelliteFn: (args?: string[]) => Promise<void>;
85+
missionControlFn: (args?: string[]) => Promise<void>;
86+
orbiterFn: (args?: string[]) => Promise<void>;
6487
}) => {
6588
const target = nextArg({args, option: '-t'}) ?? nextArg({args, option: '--target'});
6689

6790
switch (target) {
6891
case 's':
6992
case 'satellite':
70-
await satelliteFn();
93+
await satelliteFn(args);
7194
break;
7295
case 'm':
7396
case 'mission-control':
74-
await missionControlFn();
97+
await missionControlFn(args);
7598
break;
7699
case 'o':
77100
case 'orbiter':
78-
await orbiterFn();
101+
await orbiterFn(args);
79102
break;
80103
default:
81104
console.log(red('Unknown target.'));
82105
logHelpSnapshot(args);
83106
}
84107
};
108+
109+
export const helpSnapshot = (args?: string[]) => {
110+
const [subCommand] = args ?? [];
111+
112+
switch (subCommand) {
113+
case 'upload':
114+
logHelpSnapshotUpload(args);
115+
break;
116+
default:
117+
logHelpSnapshot(args);
118+
}
119+
};

src/constants/help.constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export const CHANGES_LIST_DESCRIPTION = 'List all submitted or applied changes.'
4848
export const CHANGES_APPLY_DESCRIPTION = 'Apply a submitted change.';
4949
export const CHANGES_REJECT_DESCRIPTION = 'Reject a change.';
5050

51+
export const SNAPSHOT_UPLOAD_DESCRIPTION = 'Upload a snapshot from offline files.';
52+
5153
export const OPTION_KEEP_STAGED = `${yellow('-k, --keep-staged')} Keep staged assets in memory after applying the change.`;
5254
export const OPTION_HASH = `${yellow('--hash')} The expected hash of all included changes (for verification).`;
5355
export const OPTION_HELP = `${yellow('-h, --help')} Output usage information.`;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {join} from 'node:path';
2+
3+
export const SNAPSHOTS_PATH = join(process.cwd(), '.snapshots');
4+
5+
// https://forum.dfinity.org/t/canister-snapshot-up-download/57397?u=peterparker
6+
// Same value as INSTALL_MAX_CHUNK_SIZE in @junobuild/admin
7+
export const SNAPSHOT_MAX_CHUNK_SIZE = 1_000_000n;

src/help/snapshot.help.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import {cyan, green, magenta, yellow} from 'kleur';
2-
import {OPTIONS_ENV, OPTION_HELP, SNAPSHOT_DESCRIPTION} from '../constants/help.constants';
2+
import {
3+
OPTIONS_ENV,
4+
OPTION_HELP,
5+
SNAPSHOT_DESCRIPTION,
6+
SNAPSHOT_UPLOAD_DESCRIPTION
7+
} from '../constants/help.constants';
38
import {helpOutput} from './common.help';
49
import {TITLE} from './help';
510
import {TARGET_OPTION_NOTE, targetOption} from './target.help';
@@ -8,8 +13,10 @@ const usage = `Usage: ${green('juno')} ${cyan('snapshot')} ${magenta('<subcomman
813
914
Subcommands:
1015
${magenta('create')} Create a snapshot of your current state.
11-
${magenta('restore')} Restore a previously created snapshot.
1216
${magenta('delete')} Delete an existing snapshot.
17+
${magenta('download')} Download a snapshot to offline files.
18+
${magenta('upload')} ${SNAPSHOT_UPLOAD_DESCRIPTION}
19+
${magenta('restore')} Restore a previously created snapshot.
1320
1421
Options:
1522
${targetOption('snapshot')}

src/help/snapshot.upload.help.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {cyan, green, magenta, yellow} from 'kleur';
2+
import {OPTION_HELP, OPTIONS_ENV, SNAPSHOT_UPLOAD_DESCRIPTION} from '../constants/help.constants';
3+
import {helpOutput} from './common.help';
4+
import {TITLE} from './help';
5+
6+
const usage = `Usage: ${green('juno')} ${cyan('snapshot')} ${magenta('upload')} ${yellow('[options]')}
7+
8+
Options:
9+
${yellow('--dir')} Path to the snapshot directory that contains the metadata.json and chunks.
10+
${OPTIONS_ENV}
11+
${OPTION_HELP}`;
12+
13+
const doc = `${SNAPSHOT_UPLOAD_DESCRIPTION}
14+
15+
\`\`\`
16+
${usage}
17+
\`\`\`
18+
`;
19+
20+
const help = `${TITLE}
21+
22+
${SNAPSHOT_UPLOAD_DESCRIPTION}
23+
24+
${usage}
25+
`;
26+
27+
export const logHelpSnapshotUpload = (args?: string[]) => {
28+
console.log(helpOutput(args) === 'doc' ? doc : help);
29+
};

src/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {functions, helpFunctions} from './commands/functions';
1212
import {helpHosting, hosting} from './commands/hosting';
1313
import {open} from './commands/open';
1414
import {helpRun, run as runCmd} from './commands/run';
15-
import {snapshot} from './commands/snapshot';
15+
import {helpSnapshot, snapshot} from './commands/snapshot';
1616
import {startStop} from './commands/start-stop';
1717
import {status} from './commands/status';
1818
import {upgrade} from './commands/upgrade';
@@ -22,7 +22,6 @@ import {help} from './help/help';
2222
import {logHelpLogin} from './help/login.help';
2323
import {logHelpLogout} from './help/logout.help';
2424
import {logHelpOpen} from './help/open.help';
25-
import {logHelpSnapshot} from './help/snapshot.help';
2625
import {logHelpStart} from './help/start.help';
2726
import {logHelpStatus} from './help/status.help';
2827
import {logHelpStop} from './help/stop.help';
@@ -94,7 +93,7 @@ export const run = async () => {
9493
helpFunctions(args);
9594
break;
9695
case 'snapshot':
97-
logHelpSnapshot(args);
96+
helpSnapshot(args);
9897
break;
9998
case 'init':
10099
helpInit(args);

src/schema/snapshot.schema.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as z from 'zod';
2+
3+
const Uint8ArrayLike = z.instanceof(Uint8Array) as z.ZodType<Uint8Array>;
4+
5+
// A Zod schema for the ic-management ReadCanisterSnapshotMetadataResponse type
6+
export const ReadCanisterSnapshotMetadataResponseSchema = z.strictObject({
7+
globals: z.array(
8+
z.union([
9+
z.object({f32: z.number()}),
10+
z.object({f64: z.number()}),
11+
z.object({i32: z.number()}),
12+
z.object({i64: z.bigint()}),
13+
z.object({v128: z.bigint()})
14+
])
15+
),
16+
canisterVersion: z.bigint(),
17+
source: z.union([
18+
z.object({metadataUpload: z.unknown()}),
19+
z.object({takenFromCanister: z.unknown()})
20+
]),
21+
certifiedData: z.union([Uint8ArrayLike, z.array(z.number())]),
22+
globalTimer: z.union([z.object({active: z.bigint()}), z.object({inactive: z.null()})]).optional(),
23+
onLowWasmMemoryHookStatus: z
24+
.union([
25+
z.object({conditionNotSatisfied: z.null()}),
26+
z.object({executed: z.null()}),
27+
z.object({ready: z.null()})
28+
])
29+
.optional(),
30+
wasmModuleSize: z.bigint(),
31+
stableMemorySize: z.bigint(),
32+
wasmChunkStore: z.array(
33+
z.object({
34+
hash: z.union([Uint8ArrayLike, z.array(z.number())])
35+
})
36+
),
37+
takenAtTimestamp: z.bigint(),
38+
wasmMemorySize: z.bigint()
39+
});
40+
41+
export const SnapshotFilenameSchema = z.enum([
42+
'wasm-code.bin',
43+
'heap.bin',
44+
'stable.bin',
45+
'chunks-store.bin'
46+
]);
47+
48+
export const SnapshotFileSchema = z.strictObject({
49+
filename: SnapshotFilenameSchema,
50+
size: z.bigint(),
51+
hash: z.hash('sha256')
52+
});
53+
54+
const SnapshotDataSchema = z.strictObject({
55+
wasmModule: SnapshotFileSchema.nullable(),
56+
wasmMemory: SnapshotFileSchema.nullable(),
57+
stableMemory: SnapshotFileSchema.nullable(),
58+
wasmChunkStore: SnapshotFileSchema.nullable()
59+
});
60+
61+
export const SnapshotMetadataSchema = z.strictObject({
62+
snapshotId: z.string(),
63+
data: SnapshotDataSchema,
64+
metadata: ReadCanisterSnapshotMetadataResponseSchema
65+
});

0 commit comments

Comments
 (0)