mirror of
https://github.com/koush/scrypted.git
synced 2026-02-08 08:19:56 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a640758d1 | ||
|
|
9be913af26 | ||
|
|
da17bee516 | ||
|
|
48d9790051 | ||
|
|
c43014348d | ||
|
|
cec3a592ba | ||
|
|
c446ddcdf4 | ||
|
|
72f79ea8ef | ||
|
|
41988699d0 | ||
|
|
5151c520d4 | ||
|
|
e1abe717fa |
@@ -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"
|
||||
|
||||
2
packages/cli/.vscode/launch.json
vendored
2
packages/cli/.vscode/launch.json
vendored
@@ -21,7 +21,7 @@
|
||||
],
|
||||
"preLaunchTask": "npm: build",
|
||||
"args": [
|
||||
"shell",
|
||||
"login",
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
|
||||
4
packages/cli/package-lock.json
generated
4
packages/cli/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.10",
|
||||
"description": "",
|
||||
"main": "./dist/packages/cli/src/main.js",
|
||||
"bin": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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/**/*"
|
||||
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = "/";
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Submodule plugins/sample-cameraprovider updated: bfcc0b8df6...51bbc2be20
4
sdk/package-lock.json
generated
4
sdk/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"description": "",
|
||||
"main": "dist/src/index.js",
|
||||
"exports": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
4
sdk/types/package-lock.json
generated
4
sdk/types/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"author": "",
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.88.0",
|
||||
"version": "0.89.0",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
63
server/src/services/backup.ts
Normal file
63
server/src/services/backup.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user