Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrity check #158

Merged
merged 20 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/mops-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ on:
jobs:
test:
strategy:
max-parallel: 2
max-parallel: 3
matrix:
moc-version: [0.9.3, latest]
moc-version: [0.10.0]
mops-version: [./cli, ic-mops@latest, ic-mops@0.28.2]
node-version: [16, 20]

Expand Down
121 changes: 102 additions & 19 deletions backend/main/main-canister.mo
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {DAY} "mo:time-consts";
import {ic} "mo:ic";
import Map "mo:map/Map";
import Backup "mo:backup";
import Sha256 "mo:sha2/Sha256";
import HttpTypes "mo:http-types";

import Utils "../utils";
Expand Down Expand Up @@ -73,6 +74,7 @@ actor {
var packageConfigs = TrieMap.TrieMap<PackageId, PackageConfigV2>(Text.equal, Text.hash);
var packagePublications = TrieMap.TrieMap<PackageId, PackagePublication>(Text.equal, Text.hash);
var fileIdsByPackage = TrieMap.TrieMap<PackageId, [FileId]>(Text.equal, Text.hash);
var hashByFileId = TrieMap.TrieMap<FileId, Blob>(Text.equal, Text.hash);
var packageFileStats = TrieMap.TrieMap<PackageId, PackageFileStats>(Text.equal, Text.hash);
var packageTestStats = TrieMap.TrieMap<PackageId, TestStats>(Text.equal, Text.hash);
var packageNotes = TrieMap.TrieMap<PackageId, Text>(Text.equal, Text.hash);
Expand Down Expand Up @@ -102,6 +104,7 @@ actor {
let publishingPackageFileStats = TrieMap.TrieMap<PublishingId, PackageFileStats>(Text.equal, Text.hash);
let publishingTestStats = TrieMap.TrieMap<PublishingId, TestStats>(Text.equal, Text.hash);
let publishingNotes = TrieMap.TrieMap<PublishingId, Text>(Text.equal, Text.hash);
let publishingFileHashers = TrieMap.TrieMap<FileId, Sha256.Digest>(Text.equal, Text.hash);

// PRIVATE
func _getHighestVersion(name : PackageName) : ?PackageVersion {
Expand Down Expand Up @@ -421,6 +424,10 @@ actor {
case (_) {};
};

// add temp hasher
let hasher = Sha256.Digest(#sha256);
publishingFileHashers.put(fileId, hasher);

// upload first chunk
if (chunkCount != 0) {
let uploadRes = await storageManager.uploadChunk(publishing.storage, fileId, 0, firstChunk);
Expand All @@ -430,6 +437,9 @@ actor {
};
case (_) {};
};

// compute hash of the first chunk
hasher.writeBlob(firstChunk);
};

// file stats
Expand Down Expand Up @@ -499,6 +509,9 @@ actor {
return pkgSizeRes;
};

let ?hasher = publishingFileHashers.get(fileId) else return #err("Hasher not found");
hasher.writeBlob(chunk);

#ok;
};

Expand Down Expand Up @@ -567,15 +580,25 @@ actor {
file.id;
});

let publicFileIds = Array.filter(fileIds, func(fileId : Text.Text) : Bool {
not Text.endsWith(fileId, #text("docs.tgz"));
});

fileIdsByPackage.put(packageId, publicFileIds);

// store file hashes
for (fileId in publicFileIds.vals()) {
let ?hasher = publishingFileHashers.get(fileId) else return #err("Hasher not found");
hashByFileId.put(fileId, hasher.sum());
publishingFileHashers.delete(fileId);
};

// finish uploads
let res = await storageManager.finishUploads(publishing.storage, fileIds);
if (Result.isErr(res)) {
return res;
};

fileIdsByPackage.put(packageId, Array.filter(fileIds, func(fileId : Text.Text) : Bool {
not Text.endsWith(fileId, #text("docs.tgz"));
}));

_updateHighestConfig(publishing.config);

let versions = Option.get(packageVersions.get(publishing.config.name), []);
Expand Down Expand Up @@ -724,6 +747,26 @@ actor {
Buffer.toArray(buf);
};

public shared ({caller}) func computeHashesForExistingFiles() : async () {
assert(Utils.isAdmin(caller));

for ((packageId, fileIds) in fileIdsByPackage.entries()) {
let ?publication = packagePublications.get(packageId) else Debug.trap("Package publication '" # packageId # "' not found");
let storage = actor(Principal.toText(publication.storage)) : Storage.Storage;

for (fileId in fileIds.vals()) {
let #ok(fileMeta) = await storage.getFileMeta(fileId) else Debug.trap("File meta '" # fileId # "' not found");

let hasher = Sha256.Digest(#sha256);
for (i in Iter.range(0, fileMeta.chunkCount - 1)) {
let #ok(chunk) = await storage.downloadChunk(fileId, i) else Debug.trap("File chunk '" # fileId # "' not found");
hasher.writeBlob(chunk);
};
hashByFileId.put(fileId, hasher.sum());
};
};
};

func _computeDepsStatus(name : PackageName, version : PackageVersion) : DepsStatus {
let packageId = name # "@" # version;
let ?config = packageConfigs.get(packageId) else Debug.trap("Package '" # packageId # "' not found");
Expand Down Expand Up @@ -855,7 +898,8 @@ actor {
case ("0.14.3") [("base", "0.9.3")];
case ("0.14.4") [("base", "0.9.3")];
case ("0.15.0") [("base", "0.9.7")];
case ("0.15.1") [("base", "0.9.7")];
case ("0.15.1") [("base", "0.9.8")];
case ("0.15.2") [("base", "0.10.1")];
case (_) {
switch (_getHighestVersion("base")) {
case (?ver) [("base", ver)];
Expand Down Expand Up @@ -933,6 +977,35 @@ actor {
Result.fromOption(fileIdsByPackage.get(packageId), "Package '" # packageId # "' not found");
};

func _getFileHashes(packageId : PackageId) : Result.Result<[(FileId, Blob)], Err> {
let ?fileIds = fileIdsByPackage.get(packageId) else return #err("Package '" # packageId # "' not found");
let buf = Buffer.Buffer<(FileId, Blob)>(fileIds.size());
for (fileId in fileIds.vals()) {
let ?hash = hashByFileId.get(fileId) else return #err("File hash not found for " # fileId);
buf.add((fileId, hash));
};
#ok(Buffer.toArray(buf));
};

public shared ({caller}) func getFileHashes(name : PackageName, version : PackageVersion) : async Result.Result<[(FileId, Blob)], Err> {
let packageId = name # "@" # version;
_getFileHashes(packageId);
};

public shared ({caller}) func getFileHashesByPackageIds(packageIds : [PackageId]) : async [(PackageId, [(FileId, Blob)])] {
let buf = Buffer.Buffer<(PackageId, [(FileId, Blob)])>(packageIds.size());

for (packageId in packageIds.vals()) {
let hashes = switch (_getFileHashes(packageId)) {
case (#ok(hashes)) hashes;
case (#err(_)) [];
};
buf.add((packageId, hashes));
};

Buffer.toArray(buf);
};

func _notifyInstall(name : PackageName, version : PackageVersion, downloader : Principal) {
let packageId = name # "@" # version;

Expand Down Expand Up @@ -1361,13 +1434,14 @@ actor {
let backupManager = Backup.BackupManager(backupState);

type BackupChunk = {
#v3 : {
#v4 : {
#packagePublications : [(PackageId, PackagePublication)];
#packageVersions : [(PackageName, [PackageVersion])];
#packageOwners : [(PackageName, Principal)];
#packageConfigs : [(PackageId, PackageConfigV2)];
#highestConfigs : [(PackageName, PackageConfigV2)];
#fileIdsByPackage : [(PackageId, [FileId])];
#hashByFileId : [(FileId, Blob)];
#packageTestStats : [(PackageId, TestStats)];
#packageNotes : [(PackageId, Text)];
#downloadLog : DownloadLog.Stable;
Expand All @@ -1387,19 +1461,20 @@ actor {
};

func _backup() : async () {
let backup = backupManager.NewBackup("v3");
let backup = backupManager.NewBackup("v4");
await backup.startBackup();
await backup.uploadChunk(to_candid(#v3(#packagePublications(Iter.toArray(packagePublications.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v3(#packageVersions(Iter.toArray(packageVersions.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v3(#packageOwners(Iter.toArray(packageOwners.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v3(#fileIdsByPackage(Iter.toArray(fileIdsByPackage.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v3(#packageTestStats(Iter.toArray(packageTestStats.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v3(#packageNotes(Iter.toArray(packageNotes.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v3(#downloadLog(downloadLog.toStable())) : BackupChunk));
await backup.uploadChunk(to_candid(#v3(#storageManager(storageManager.toStable())) : BackupChunk));
await backup.uploadChunk(to_candid(#v3(#users(users.toStable())) : BackupChunk));
await backup.uploadChunk(to_candid(#v3(#highestConfigs(Iter.toArray(highestConfigs.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v3(#packageConfigs(Iter.toArray(packageConfigs.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v4(#packagePublications(Iter.toArray(packagePublications.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v4(#packageVersions(Iter.toArray(packageVersions.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v4(#packageOwners(Iter.toArray(packageOwners.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v4(#fileIdsByPackage(Iter.toArray(fileIdsByPackage.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v4(#hashByFileId(Iter.toArray(hashByFileId.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v4(#packageTestStats(Iter.toArray(packageTestStats.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v4(#packageNotes(Iter.toArray(packageNotes.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v4(#downloadLog(downloadLog.toStable())) : BackupChunk));
await backup.uploadChunk(to_candid(#v4(#storageManager(storageManager.toStable())) : BackupChunk));
await backup.uploadChunk(to_candid(#v4(#users(users.toStable())) : BackupChunk));
await backup.uploadChunk(to_candid(#v4(#highestConfigs(Iter.toArray(highestConfigs.entries()))) : BackupChunk));
await backup.uploadChunk(to_candid(#v4(#packageConfigs(Iter.toArray(packageConfigs.entries()))) : BackupChunk));
await backup.finishBackup();
};

Expand All @@ -1409,7 +1484,7 @@ actor {
assert(Utils.isAdmin(caller));

await backupManager.restore(backupId, func(blob : Blob) {
let ?#v3(chunk) : ?BackupChunk = from_candid(blob) else Debug.trap("Failed to restore chunk");
let ?#v4(chunk) : ?BackupChunk = from_candid(blob) else Debug.trap("Failed to restore chunk");

switch (chunk) {
case (#packagePublications(packagePublicationsStable)) {
Expand All @@ -1424,6 +1499,9 @@ actor {
case (#fileIdsByPackage(fileIdsByPackageStable)) {
fileIdsByPackage := TrieMap.fromEntries<PackageId, [FileId]>(fileIdsByPackageStable.vals(), Text.equal, Text.hash);
};
case (#hashByFileId(hashByFileIdStable)) {
hashByFileId := TrieMap.fromEntries<FileId, Blob>(hashByFileIdStable.vals(), Text.equal, Text.hash);
};
case (#packageTestStats(packageTestStatsStable)) {
packageTestStats := TrieMap.fromEntries<PackageId, TestStats>(packageTestStatsStable.vals(), Text.equal, Text.hash);
};
Expand Down Expand Up @@ -1459,6 +1537,7 @@ actor {
stable var highestConfigsStableV2 : [(PackageName, PackageConfigV2)] = [];

stable var fileIdsByPackageStable : [(PackageId, [FileId])] = [];
stable var hashByFileIdStable : [(FileId, Blob)] = [];
stable var packageFileStatsStable : [(PackageId, PackageFileStats)] = [];
stable var packageTestStatsStable : [(PackageId, TestStats)] = [];
stable var packageNotesStable : [(PackageId, Text)] = [];
Expand All @@ -1472,6 +1551,7 @@ actor {
packageVersionsStable := Iter.toArray(packageVersions.entries());
packageOwnersStable := Iter.toArray(packageOwners.entries());
fileIdsByPackageStable := Iter.toArray(fileIdsByPackage.entries());
hashByFileIdStable := Iter.toArray(hashByFileId.entries());
packageFileStatsStable := Iter.toArray(packageFileStats.entries());
packageTestStatsStable := Iter.toArray(packageTestStats.entries());
packageNotesStable := Iter.toArray(packageNotes.entries());
Expand All @@ -1496,6 +1576,9 @@ actor {
fileIdsByPackage := TrieMap.fromEntries<PackageId, [FileId]>(fileIdsByPackageStable.vals(), Text.equal, Text.hash);
fileIdsByPackageStable := [];

hashByFileId := TrieMap.fromEntries<FileId, Blob>(hashByFileIdStable.vals(), Text.equal, Text.hash);
hashByFileIdStable := [];

packageFileStats := TrieMap.fromEntries<PackageId, PackageFileStats>(packageFileStatsStable.vals(), Text.equal, Text.hash);
packageFileStatsStable := [];

Expand Down
1 change: 1 addition & 0 deletions cli/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
save-exact = true
13 changes: 9 additions & 4 deletions cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ program
.description('Install the package and save it to mops.toml')
.option('--dev')
.option('--verbose')
.addOption(new Option('--lockfile <lockfile>', 'Lockfile action').choices(['save', 'ignore']))
.action(async (pkg, options) => {
if (!checkConfigFile()) {
process.exit(1);
Expand All @@ -64,6 +65,7 @@ program
.option('--dev', 'Remove from dev-dependencies instead of dependencies')
.option('--verbose', 'Show more information')
.option('--dry-run', 'Do not actually remove anything')
.addOption(new Option('--lockfile <lockfile>', 'Lockfile action').choices(['save', 'ignore']))
.action(async (pkg, options) => {
if (!checkConfigFile()) {
process.exit(1);
Expand All @@ -77,6 +79,7 @@ program
.alias('i')
.description('Install all dependencies specified in mops.toml')
.option('--verbose')
.addOption(new Option('--lockfile <lockfile>', 'Lockfile action').choices(['save', 'check', 'ignore']))
.action(async (pkg, options) => {
if (!checkConfigFile()) {
process.exit(1);
Expand Down Expand Up @@ -313,8 +316,9 @@ program
program
.command('sync')
.description('Add missing packages and remove unused packages')
.action(async () => {
await sync();
.addOption(new Option('--lockfile <lockfile>', 'Lockfile action').choices(['save', 'ignore']))
.action(async (options) => {
await sync(options);
});

// outdated
Expand All @@ -329,8 +333,9 @@ program
program
.command('update [pkg]')
.description('Update dependencies specified in mops.toml')
.action(async (pkg) => {
await update(pkg);
.addOption(new Option('--lockfile <lockfile>', 'Lockfile action').choices(['save', 'ignore']))
.action(async (pkg, options) => {
await update(pkg, options);
});

// transfer-ownership
Expand Down
11 changes: 10 additions & 1 deletion cli/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ import {checkConfigFile, getGithubCommit, getHighestVersion, parseGithubURL, rea
import {installFromGithub} from '../vessel.js';
import {install} from './install.js';
import {notifyInstalls} from '../notify-installs.js';
import {checkIntegrity} from '../integrity.js';

export async function add(name: string, {verbose = false, dev = false} = {}) {
type AddOptions = {
verbose?: boolean;
dev?: boolean;
lockfile?: 'save' | 'ignore';
};

export async function add(name: string, {verbose = false, dev = false, lockfile}: AddOptions = {}) {
if (!checkConfigFile()) {
return;
}
Expand Down Expand Up @@ -103,4 +110,6 @@ export async function add(name: string, {verbose = false, dev = false} = {}) {

logUpdate.clear();
console.log(chalk.green('Package installed ') + `${pkgDetails.name} = "${pkgDetails.repo || pkgDetails.path || pkgDetails.version}"`);

await checkIntegrity(lockfile);
}
11 changes: 10 additions & 1 deletion cli/commands/install-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ import {checkConfigFile, readConfig} from '../mops.js';
import {install} from './install.js';
import {installFromGithub} from '../vessel.js';
import {notifyInstalls} from '../notify-installs.js';
import {checkIntegrity} from '../integrity.js';

export async function installAll({verbose = false, silent = false} = {}) {
type InstallAllOptions = {
verbose?: boolean;
silent?: boolean;
lockfile?: 'save' | 'check' | 'ignore';
}

export async function installAll({verbose = false, silent = false, lockfile}: InstallAllOptions = {}) {
if (!checkConfigFile()) {
return;
}
Expand Down Expand Up @@ -35,4 +42,6 @@ export async function installAll({verbose = false, silent = false} = {}) {
logUpdate.clear();
console.log(chalk.green('All packages installed'));
}

await checkIntegrity(lockfile);
}
6 changes: 3 additions & 3 deletions cli/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ export async function install(pkg: string, version = '', {verbose = false, silen
installedDeps = {...installedDeps, [pkg]: version};
}

if (ok) {
return installedDeps;
if (!ok) {
return false;
}
return false;
return installedDeps;
}
Loading