Compare commits

..

11 Commits

Author SHA1 Message Date
Koushik Dutta
8a640758d1 server/cli: fix login issues 2024-01-22 09:11:02 -08:00
Koushik Dutta
9be913af26 sample-cameraprovider: update 2024-01-21 15:39:48 -08:00
Koushik Dutta
da17bee516 sdk: publish 2024-01-21 15:38:28 -08:00
Koushik Dutta
48d9790051 cli: fix https://github.com/koush/scrypted/issues/1277 2024-01-21 14:52:10 -08:00
Koushik Dutta
c43014348d sdk: prevent unnecessary JSON exceptions 2024-01-21 14:42:44 -08:00
Koushik Dutta
cec3a592ba client: add missing dependency 2024-01-21 14:42:24 -08:00
Koushik Dutta
c446ddcdf4 Merge branch 'main' of github.com:koush/scrypted 2024-01-21 12:45:24 -08:00
Koushik Dutta
72f79ea8ef core: fix certificate login error, fix backup/restore in ha 2024-01-21 12:45:19 -08:00
Brett Jia
41988699d0 server: expose backup as a service (#1275)
* server: expose backup as a service

* move restore into new backup service
2024-01-20 21:37:56 -08:00
Koushik Dutta
5151c520d4 Update config.yaml 2024-01-18 19:21:11 -08:00
Koushik Dutta
e1abe717fa postrelease 2024-01-18 13:30:02 -08:00
21 changed files with 139 additions and 125 deletions

View File

@@ -1,6 +1,6 @@
# Home Assistant Addon Configuration
name: Scrypted
version: "18-jammy-full.s6-v0.85.0"
version: "18-jammy-full.s6-v0.88.0"
slug: scrypted
description: Scrypted is a high performance home video integration and automation platform
url: "https://github.com/koush/scrypted"

View File

@@ -21,7 +21,7 @@
],
"preLaunchTask": "npm: build",
"args": [
"shell",
"login",
],
"sourceMaps": true,
"resolveSourceMapLocations": [

View File

@@ -1,12 +1,12 @@
{
"name": "scrypted",
"version": "1.3.6",
"version": "1.3.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scrypted",
"version": "1.3.6",
"version": "1.3.10",
"license": "ISC",
"dependencies": {
"@scrypted/client": "^1.3.3",

View File

@@ -1,6 +1,6 @@
{
"name": "scrypted",
"version": "1.3.6",
"version": "1.3.10",
"description": "",
"main": "./dist/packages/cli/src/main.js",
"bin": {

View File

@@ -4,18 +4,13 @@ import { connectScryptedClient } from '@scrypted/client';
import { FFmpegInput, ScryptedMimeTypes } from '@scrypted/types';
import child_process from 'child_process';
import fs from 'fs';
import https from 'https';
import path from 'path';
import readline from 'readline-sync';
import semver from 'semver';
import { authHttpFetch } from '../../../common/src/http-auth-fetch';
import { httpFetch } from '../../../server/src/fetch/http-fetch';
import { installServe, serveMain } from './service';
import { connectShell } from './shell';
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
if (!semver.gte(process.version, '16.0.0')) {
throw new Error('"node" version out of date. Please update node to v16 or higher.')
}
@@ -45,6 +40,12 @@ interface LoginFile {
[host: string]: Login;
}
function basicAuthHeaders(username: string, password: string) {
const headers = new Headers();
headers.set('Authorization', `Basic ${Buffer.from(username + ":" + password).toString('base64')}`);
return headers;
}
async function doLogin(host: string) {
host = toIpAndPort(host);
@@ -54,16 +55,15 @@ async function doLogin(host: string) {
});
const url = `https://${host}/login`;
const response = await authHttpFetch({
const response = await httpFetch({
method: 'GET',
credential: {
username,
password,
},
headers: basicAuthHeaders(username, password),
url,
rejectUnauthorized: false,
responseType: 'json',
});
if (response.body.error)
throw new Error(response.body.error);
fs.mkdirSync(scryptedHome, {
recursive: true,
@@ -81,13 +81,10 @@ async function doLogin(host: string) {
login[host] = response.body;
fs.writeFileSync(loginPath, JSON.stringify(login));
return login;
return login[host];
}
async function getOrDoLogin(host: string): Promise<{
username: string,
token: string,
}> {
async function getOrDoLogin(host: string): Promise<Login> {
let login: LoginFile;
try {
login = JSON.parse(fs.readFileSync(loginPath).toString());
@@ -96,11 +93,12 @@ async function getOrDoLogin(host: string): Promise<{
if (!login[host].username || !login[host].token)
throw new Error();
return login[host];
}
catch (e) {
login = await doLogin(host);
return doLogin(host);
}
return login[host];
}
async function runCommand() {
@@ -150,8 +148,8 @@ async function main() {
}
else if (process.argv[2] === 'login') {
const ip = process.argv[3] || '127.0.0.1';
const token = await doLogin(ip);
console.log('login successful. token:', token);
const login = await doLogin(ip);
console.log('login successful. token:', login.token);
}
else if (process.argv[2] === 'command') {
const { sdk, pendingResult } = await runCommand();
@@ -203,16 +201,15 @@ async function main() {
const login = await getOrDoLogin(ip);
const url = `https://${ip}/web/component/script/install/${pkg}`;
const response = await authHttpFetch({
const response = await httpFetch({
method: 'POST',
credential: {
username: login.username,
password: login.token,
},
headers: basicAuthHeaders(login.username, login.token),
url,
rejectUnauthorized: false,
responseType: 'json',
});
if (response.body.error)
throw new Error(response.body.error);
console.log('install successful. id:', response.body.id);
}
@@ -249,7 +246,6 @@ async function main() {
console.log(' npx scrypted install @scrypted/rtsp');
console.log(' npx scrypted install @scrypted/rtsp/0.0.51');
console.log(' npx scrypted install @scrypted/rtsp/0.0.51 192.168.2.100');
process.exit(1);
}
}

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "Node16",
"target": "esnext",
"noImplicitAny": true,
"outDir": "./dist",
@@ -8,7 +8,7 @@
"sourceMap": true,
"inlineSources": true,
"declaration": true,
"resolveJsonModule": true,
"moduleResolution": "Node16",
},
"include": [
"src/**/*"

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/core",
"version": "0.3.1",
"version": "0.3.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/core",
"version": "0.3.1",
"version": "0.3.2",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/core",
"version": "0.3.1",
"version": "0.3.2",
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -137,7 +137,7 @@ export default {
// cert may need to be reaccepted? Server is down? Go to the
// server root to force the network error to bypass the PWA cache.
if (
e.toString().includes("Network Error") &&
(e.toString().includes("Network Error") || e.toString().includes("Load failed")) &&
window.location.href.startsWith("https:")
) {
window.location = "/";

View File

@@ -21,27 +21,18 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
v-if="!canUpdate"
small
text
href="https://github.com/koush/scrypted#installation"
>More Information</v-btn
>
<v-btn v-if="!canUpdate" small text href="https://github.com/koush/scrypted#installation">More
Information</v-btn>
<v-dialog v-else v-model="updateAndRestart" width="500">
<template v-slot:activator="{ on }">
<v-btn small text color="red" v-on="on"
>Update and Restart Scrypted</v-btn
>
<v-btn small text color="red" v-on="on">Update and Restart Scrypted</v-btn>
</template>
<v-card color="red" dark>
<v-card-title primary-title>Restart Scrypted</v-card-title>
<v-card-text
>Are you sure you want to restart the Scrypted
service?</v-card-text
>
<v-card-text>Are you sure you want to restart the Scrypted
service?</v-card-text>
<v-card-text>{{ restartStatus }}</v-card-text>
<v-divider></v-divider>
@@ -67,13 +58,11 @@
</v-card>
<v-card class="mt-2" v-if="showRestart">
<v-toolbar
><v-toolbar-title>Server Management</v-toolbar-title></v-toolbar
>
<v-toolbar><v-toolbar-title>Server Management</v-toolbar-title></v-toolbar>
<v-card-actions>
<v-btn text href="/web/component/backup" color="info">Backup</v-btn>
<v-btn text :href="backupUrl" color="info">Backup</v-btn>
<v-btn text color="info" @click="restoreClick">Restore</v-btn>
<input type="file" ref="restoreFile" style="display: none;" @change="restore"/>
<input type="file" ref="restoreFile" style="display: none;" @change="restore" />
<v-spacer></v-spacer>
<v-dialog v-model="restart" width="500">
<template v-slot:activator="{ on }">
@@ -83,10 +72,8 @@
<v-card color="red" dark>
<v-card-title primary-title>Restart Scrypted</v-card-title>
<v-card-text
>Are you sure you want to restart the Scrypted
service?</v-card-text
>
<v-card-text>Are you sure you want to restart the Scrypted
service?</v-card-text>
<v-card-text>{{ restartStatus }}</v-card-text>
<v-divider></v-divider>
@@ -106,7 +93,8 @@
<script>
import { checkServerUpdate } from "../plugin/plugin";
import Settings from "../../interfaces/Settings.vue"
import {createSystemSettingsDevice} from './system-settings';
import { createSystemSettingsDevice } from './system-settings';
import { combineBaseUrl, getCurrentBaseUrl } from "../../../../../../packages/client/src";
export default {
components: {
@@ -124,6 +112,12 @@ export default {
showRestart: false,
};
},
computed: {
backupUrl() {
const baseUrl = getCurrentBaseUrl();
return combineBaseUrl(baseUrl, 'web/component/backup');
},
},
mounted() {
this.loadEnv();
this.checkUpdateAvailable();
@@ -140,7 +134,9 @@ export default {
return;
console.log(file);
const fileBlob = new Blob([file]);
await fetch('/web/component/restore', {
const baseUrl = getCurrentBaseUrl();
const restoreUrl = combineBaseUrl(baseUrl, 'web/component/restore');
await fetch(restoreUrl, {
method: 'POST',
body: fileBlob,
});

4
sdk/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/sdk",
"version": "0.3.4",
"version": "0.3.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/sdk",
"version": "0.3.4",
"version": "0.3.5",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/sdk",
"version": "0.3.4",
"version": "0.3.5",
"description": "",
"main": "dist/src/index.js",
"exports": {

View File

@@ -19,6 +19,8 @@ function parseValue(value: string, setting: StorageSetting, readDefaultValue: ()
return parseInt(value) || readDefaultValue() || 0;
}
if (type === 'array') {
if (!value)
return readDefaultValue() || [];
try {
return JSON.parse(value);
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/types",
"version": "0.3.4",
"version": "0.3.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/types",
"version": "0.3.4",
"version": "0.3.5",
"license": "ISC",
"devDependencies": {
"@types/rimraf": "^3.0.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/types",
"version": "0.3.4",
"version": "0.3.5",
"description": "",
"main": "dist/index.js",
"author": "",

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/server",
"version": "0.88.0",
"version": "0.89.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/server",
"version": "0.88.0",
"version": "0.89.0",
"license": "ISC",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/server",
"version": "0.88.0",
"version": "0.89.0",
"description": "",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",

View File

@@ -46,6 +46,7 @@ import { getNpmPackageInfo, PluginComponent } from './services/plugin';
import { ServiceControl } from './services/service-control';
import { UsersService } from './services/users';
import { getState, ScryptedStateManager, setState } from './state';
import { Backup } from './services/backup';
interface DeviceProxyPair {
handler: PluginDeviceProxyHandler;
@@ -102,6 +103,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
addressSettings = new AddressSettings(this);
usersService = new UsersService(this);
info = new Info();
backup = new Backup(this);
pluginHosts = new Map<string, RuntimeHost>();
constructor(public mainFilename: string, public datastore: Level, insecure: http.Server, secure: https.Server, app: express.Application) {
@@ -443,6 +445,8 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
return this.addressSettings;
case "users":
return this.usersService;
case 'backup':
return this.backup;
}
}

View File

@@ -149,6 +149,10 @@ async function start(mainFilename: string, options?: {
realm: 'Scrypted',
}, async (username, password, callback) => {
const user = await db.tryGet(ScryptedUser, username);
if (!user) {
callback(false);
return;
}
const salted = user.salt + password;
const hash = crypto.createHash('sha256');
@@ -339,13 +343,10 @@ async function start(mainFilename: string, options?: {
app.post('/web/component/restore', async (req, res) => {
const buffers: Buffer[] = [];
let zip: AdmZip;
req.on('data', b => buffers.push(b));
try {
await once(req, 'end');
zip = new AdmZip(Buffer.concat(buffers));
if (!zip.test())
throw new Error('backup zip test failed.');
await scrypted.backup.restore(Buffer.concat(buffers))
}
catch (e) {
res.send({
@@ -353,59 +354,11 @@ async function start(mainFilename: string, options?: {
});
return;
}
try {
scrypted.kill();
await sleep(5000);
await db.close();
await fs.promises.rm(volumeDir, {
recursive: true,
force: true,
});
await fs.promises.mkdir(volumeDir, {
recursive: true
});
zip.extractAllTo(dbPath, true);
res.send({
success: true,
});
}
catch (e) {
res.send({
error: "Error during restore.",
});
}
scrypted.serviceControl.restart();
});
app.get('/web/component/backup', async (req, res) => {
try {
const backupDbPath = path.join(volumeDir, 'backup.db');
await fs.promises.rm(backupDbPath, {
recursive: true,
force: true,
});
const backupDb = new Level(backupDbPath);
await backupDb.open();
for await (const [key, value] of db.iterator()) {
await backupDb.put(key, value);
}
await backupDb.close();
const backupZip = path.join(volumeDir, 'backup.zip');
await fs.promises.rm(backupZip, {
recursive: true,
force: true,
});
const zip = new AdmZip();
await zip.addLocalFolderPromise(backupDbPath, {});
const zipBuffer = await zip.toBufferPromise();
const zipBuffer = await scrypted.backup.createBackup();
// the file is a normal zip file, but an extension is added to prevent safari, etc, from unzipping it automatically.
res.header('Content-Disposition', 'attachment; filename="scrypted.zip.backup"')
res.send(zipBuffer);

View File

@@ -0,0 +1,63 @@
import fs from 'fs';
import path from 'path';
import Level from '../level';
import { sleep } from '../sleep';
import { getScryptedVolume } from '../plugin/plugin-volume';
import AdmZip from 'adm-zip';
import { ScryptedRuntime } from '../runtime';
export class Backup {
constructor(public runtime: ScryptedRuntime) {}
async createBackup(): Promise<Buffer> {
const volumeDir = getScryptedVolume();
const backupDbPath = path.join(volumeDir, 'backup.db');
await fs.promises.rm(backupDbPath, {
recursive: true,
force: true,
});
const backupDb = new Level(backupDbPath);
await backupDb.open();
for await (const [key, value] of this.runtime.datastore.iterator()) {
await backupDb.put(key, value);
}
await backupDb.close();
const backupZip = path.join(volumeDir, 'backup.zip');
await fs.promises.rm(backupZip, {
recursive: true,
force: true,
});
const zip = new AdmZip();
await zip.addLocalFolderPromise(backupDbPath, {});
return zip.toBufferPromise();
}
async restore(b: Buffer): Promise<void> {
const volumeDir = getScryptedVolume();
const dbPath = path.join(volumeDir, 'scrypted.db');
const zip = new AdmZip(b);
if (!zip.test())
throw new Error('backup zip test failed.');
this.runtime.kill();
await sleep(5000);
await this.runtime.datastore.close();
await fs.promises.rm(volumeDir, {
recursive: true,
force: true,
});
await fs.promises.mkdir(volumeDir, {
recursive: true
});
zip.extractAllTo(dbPath, true);
this.runtime.serviceControl.restart();
}
}