diff --git a/os/image/rpi.js b/os/image/rpi.js index 45db12722..9552cb346 100644 --- a/os/image/rpi.js +++ b/os/image/rpi.js @@ -64,7 +64,7 @@ export async function setTryBootFlag(bool) { export async function getBootedPartitionNumber() { const { stdout } = - await $`fdtget /sys/firmware/fdt /chosen/bootloader partition` + await $`sudo fdtget /sys/firmware/fdt /chosen/bootloader partition` const n = Number(stdout.trim()) if (!Number.isInteger(n)) throw new Error("Could not get booted partition") return n diff --git a/os/image/rpios.js b/os/image/rpios.js index 54b7962f4..a60f25e45 100644 --- a/os/image/rpios.js +++ b/os/image/rpios.js @@ -3,7 +3,6 @@ import assert from "node:assert" import { basename, join } from "node:path" -import { fileURLToPath } from "node:url" import { access, readFile, readlink } from "node:fs/promises" import { once } from "node:events" diff --git a/os/pkos/README.md b/os/pkos/README.md index 9852f5a43..27ab0f0f4 100644 --- a/os/pkos/README.md +++ b/os/pkos/README.md @@ -44,3 +44,14 @@ sudo NODE_DEBUG=execa ./pkos.js create-bundle /dev/device B [version] ``` This will create a bundle from partitions `FIRMWARE_B` and `ROOT_B` on device `/dev/device`. + +# remote + +`remote` is a CLI to perform a clean install of PlanktoScope OS on the slot of a remote PlanktoScope and generated a bundle. + +It's mostly helpful for CI/CD automation. + +```sh +# On your computer +node remote.js --host=192.168.1.20 --bootname=B --version=2026.0.1-alpha.99 +``` diff --git a/os/pkos/justfile b/os/pkos/justfile index fb3291699..16dfc54e1 100644 --- a/os/pkos/justfile +++ b/os/pkos/justfile @@ -1,5 +1,6 @@ setup: just --justfile ../../justfile install-node npm ci --omit=dev + npm --prefix ../../lib ci --omit=dev npm --prefix ../rauc ci --omit=dev npm --prefix ../image ci --omit=dev diff --git a/os/pkos/package-lock.json b/os/pkos/package-lock.json index 8ffe83b6b..e09724421 100644 --- a/os/pkos/package-lock.json +++ b/os/pkos/package-lock.json @@ -5,7 +5,114 @@ "packages": { "": { "dependencies": { - "ini": "^7.0.0" + "ini": "^7.0.0", + "ora": "^9.4.1", + "shell-escape": "^0.2.0", + "ssh2": "^1.17.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ini": { @@ -16,6 +123,220 @@ "engines": { "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nan": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", + "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", + "license": "MIT", + "optional": true + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", + "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/shell-escape": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz", + "integrity": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/os/pkos/package.json b/os/pkos/package.json index eed92bb97..8f8fcda14 100644 --- a/os/pkos/package.json +++ b/os/pkos/package.json @@ -2,6 +2,9 @@ "private": true, "type": "module", "dependencies": { - "ini": "^7.0.0" + "ini": "^7.0.0", + "ora": "^9.4.1", + "shell-escape": "^0.2.0", + "ssh2": "^1.17.0" } } diff --git a/os/pkos/remote.js b/os/pkos/remote.js new file mode 100755 index 000000000..f2e8f11c2 --- /dev/null +++ b/os/pkos/remote.js @@ -0,0 +1,166 @@ +#!/usr/bin/env node + +import assert from "assert" +import { readFileSync } from "fs" +import { once } from "events" +import os from "os" +import { parseArgs } from "node:util" +import { oraPromise } from "ora" + +import { waitForSSH, exec, downloadFile } from "./ssh.js" +import { join } from "path" +import { basename } from "node:path" +import { setTimeout } from "node:timers/promises" + +let conn +const config = { + username: "pi", +} + +const PARTNS = { + A: "2", + B: "3", +} + +const SLOTS = Object.fromEntries( + Object.entries(PARTNS).map(([key, value]) => [value, key]), +) + +// Kinda duplicate from rpi.js +async function getBootedPartitionNumber() { + const { stdout } = await exec(conn, [ + "sudo", + "fdtget", + "/sys/firmware/fdt", + "/chosen/bootloader", + "partition", + ]) + const n = Number(stdout.trim()) + if (!Number.isInteger(n)) throw new Error("Could not get booted partition") + return n +} + +async function getBootedSlot() { + const booted_partn = await getBootedPartitionNumber() + return SLOTS[booted_partn] +} + +async function useSlot(bootname) { + if ((await getBootedSlot()) === bootname) { + return + } + + function log(err) { + console.error(err) + } + + // ignore error triggered by reboot + conn.on("error", log) + process.on("uncaughtException", log) + + const p1 = once(conn, "close") + const p2 = exec(conn, ["sudo", "reboot", PARTNS[bootname]]) + await Promise.all([p1, p2]) + conn.end() + + process.removeListener("uncaughtException", log) + + conn = await oraPromise(waitForSSH(config), { + text: "SSH waiting for connection", + successText: "SSH reconnected", + failText: "SSH timeout", + }) + + assert.equal(await getBootedSlot(), bootname) +} + +const { values } = parseArgs({ + options: { + host: { + type: "string", + }, + key: { + type: "string", + default: join(os.homedir(), ".ssh/planktoscope"), + }, + bootname: { + type: "string", + }, + version: { + type: "string", + }, + }, +}) + +const { host, key, bootname, version } = values +if (!host || !key || !bootname || !version) { + throw new Error( + `Usage: remote.js --host=192.168.x.x [--key=~/.ssh/key] --bootname=B --version=202x.x.x`, + ) +} + +Object.assign(config, { + host, + privateKey: readFileSync(key), +}) + +conn = await waitForSSH(config) + +const other = bootname === "A" ? "B" : "A" + +// await useSlot(other) + +// await exec(conn, [ +// "sudo", +// "NODE_DEBUG=execa", +// "node", +// "/opt/PlanktoScope/os/pkos/pkos.js", +// "install-rpios", +// "/dev/nvme0n1", +// bootname, +// ]) + +await useSlot(bootname) + +await useSlot(other) + +// await exec(conn, ["sudo", "apt", "update", "-y"]) +// await exec(conn, ["sudo", "apt", "install", "-y", "git", "just"]) +// await exec(conn, [ +// "git", +// "clone", +// "--branch", +// "remote", +// // "--no-progress", +// "https://github.com/fairscope/PlanktoScope.git", +// "/home/pi/repo", +// ]) +// await exec(conn, ["sudo", "mv", "/home/pi/repo", "/opt/PlanktoScope"]) +// await exec(conn, ["sudo", "chown", "-R", "pi:pi", "/opt/PlanktoScope"]) +// await exec(conn, [ +// "just", +// "--justfile", +// "/opt/PlanktoScope/justfile", +// "install-node", +// ]) +// await exec(conn, ["just", "--justfile", "/opt/PlanktoScope/lib/justfile"]) +// await exec(conn, ["just", "--justfile", "/opt/PlanktoScope/os/pkos/justfile"]) +// await exec(conn, ["/opt/PlanktoScope/os/pkos/pkos.js", "prepare"]) + +// await useSlot(other) + +// const { stdout } = await exec(conn, [ +// "sudo", +// "/opt/PlanktoScope/os/pkos/pkos.js", +// "create-bundle", +// "/dev/nvme0n1", +// bootname, +// version, +// ]) + +// const bundle_path = stdout.trim() +// const filename = basename(bundle_path) + +// await downloadFile(conn, bundle_path, filename) + +conn.end() diff --git a/os/pkos/ssh.js b/os/pkos/ssh.js new file mode 100644 index 000000000..a5bb7c9a2 --- /dev/null +++ b/os/pkos/ssh.js @@ -0,0 +1,145 @@ +import shellEscape from "shell-escape" +import { PassThrough } from "stream" +import fs from "fs" +import ora from "ora" + +import { Client } from "ssh2" + +export function waitForSSH( + config, + interval = 5000, + maxTries = 120, + timeout = 5000 * 4, +) { + let tries = 0 + let success = false + + return new Promise((resolve, reject) => { + const attempt = () => { + console.log("attempt", tries) + + let timeout_interval = null + + const conn = new Client() + + let timeout_timeout = setTimeout(() => { + console.log("timeout") + conn.emit("error", new Error("timeout")) + clearTimeout(timeout_timeout) + timeout_timeout = null + }, timeout) + + function onReady() { + console.log("rdy") + success = true + resolve(conn) + cleanup() + } + + function onError(err) { + console.error("onError", err) + conn.destroy() + cleanup() + if (success) return + if (timeout_interval !== null) return + if (timeout_timeout !== null) return + tries++ + if (tries >= maxTries) return reject(new Error("SSH not reachable")) + timeout_interval = setTimeout(attempt, interval) + } + + function cleanup() { + conn.removeListener("ready", onReady) + conn.removeListener("error", onError) + clearTimeout(timeout_interval) + timeout_interval = null + clearTimeout(timeout_timeout) + timeout_timeout = null + } + + conn.on("ready", onReady) + conn.on("error", onError) + conn.connect(config) + } + + attempt() + }) +} + +export function exec(conn, cmd) { + return new Promise((resolve, reject) => { + const c = shellEscape(cmd) + console.log(`> ${c}`) + conn.exec(c, { pty: false }, (err, stream) => { + if (err) return reject(err) + + let stdout = "" + const tee = new PassThrough() + tee.on("data", (chunk) => { + stdout += chunk.toString() + }) + + stream.pipe(tee).pipe(process.stdout) + stream.stderr.pipe(process.stderr) + stream.once("close", (code, signal) => { + if (!code) { + resolve({ stdout }) + } + // console.log(`\nExited with code ${code} and signal ${signal}`) + else if (code !== 0) { + reject(new Error(`\nExited with code ${code} and signal ${signal}`)) + } else { + resolve({ stdout }) + } + }) + }) + }) +} + +function _sftp(conn) { + return new Promise((resolve, reject) => { + conn.sftp((err, sftp) => { + if (err) return reject(err) + resolve(sftp) + }) + }) +} + +export function getFileSize(sftp, path) { + return new Promise((resolve, reject) => { + sftp.stat(path, (err, stats) => { + if (err) return reject(err) + resolve(stats.size) + }) + }) +} + +export function downloadFile(conn, remote, local) { + return new Promise(async (resolve, reject) => { + const spinner = ora().start() + + let downloaded = 0 + + const sftp = await _sftp(conn) + + const size = await getFileSize(sftp, remote) + + const rs = sftp.createReadStream(remote) + const ws = fs.createWriteStream(local) + + rs.on("data", (chunk) => { + downloaded += chunk.length + const percent = ((downloaded / size) * 100).toFixed(1) + spinner.text = `Downloading ${percent}%` + }) + + rs.pipe(ws) + + rs.on("error", reject) + ws.on("error", reject) + ws.on("close", () => { + spinner.succeed() + resolve() + }) + }) +} diff --git a/os/pkos/test.js b/os/pkos/test.js new file mode 100644 index 000000000..dd9a7c2f7 --- /dev/null +++ b/os/pkos/test.js @@ -0,0 +1,16 @@ +import { waitForSSH } from "./ssh.js" +import { readFileSync } from "fs" + +let conn +const config = { + username: "pi", +} + +Object.assign(config, { + host: "192.168.1.22", + privateKey: readFileSync("/home/sonny/.ssh/planktoscope"), +}) + +conn = await waitForSSH(config) + +console.log("ok")