mirror of
https://github.com/koush/scrypted.git
synced 2026-02-05 23:22:13 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59e1391fae | ||
|
|
e0a6e66e8a | ||
|
|
fa7071b335 | ||
|
|
c28e60d875 | ||
|
|
62cbb88207 | ||
|
|
4b6a858f2b | ||
|
|
97a254b5d2 | ||
|
|
0ada6286e7 | ||
|
|
9f12e6dd6e | ||
|
|
604798e845 | ||
|
|
d912266de1 | ||
|
|
f5a32489d7 | ||
|
|
135ad8e3a8 | ||
|
|
3c4021c66b | ||
|
|
669ab17772 | ||
|
|
1860d7d8ea | ||
|
|
fa266e9dd1 | ||
|
|
4e2f3bf2c7 | ||
|
|
146e27fd13 | ||
|
|
e4bb50375f | ||
|
|
9686315c02 | ||
|
|
520895f3aa | ||
|
|
ddffc49bcf | ||
|
|
a07f52445d | ||
|
|
5e7b203f11 | ||
|
|
d752298960 | ||
|
|
5253f29831 | ||
|
|
58d674746d | ||
|
|
8a640758d1 | ||
|
|
9be913af26 | ||
|
|
da17bee516 | ||
|
|
48d9790051 | ||
|
|
c43014348d | ||
|
|
cec3a592ba | ||
|
|
c446ddcdf4 | ||
|
|
72f79ea8ef | ||
|
|
41988699d0 | ||
|
|
5151c520d4 | ||
|
|
e1abe717fa | ||
|
|
c7a9ca06be | ||
|
|
9827f15f5f | ||
|
|
d245a722e2 |
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "18-jammy-full.s6-v0.85.0"
|
||||
version: "18-jammy-full.s6-v0.91.6"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
|
||||
@@ -26,6 +26,10 @@ then
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Stopping local service if it is running..."
|
||||
systemctl stop scrypted.service 2> /dev/null
|
||||
systemctl disable scrypted.service 2> /dev/null
|
||||
|
||||
USER_HOME=$(eval echo ~$SERVICE_USER)
|
||||
SCRYPTED_HOME=$USER_HOME/.scrypted
|
||||
mkdir -p $SCRYPTED_HOME
|
||||
@@ -48,21 +52,12 @@ echo "Created $DOCKER_COMPOSE_YML"
|
||||
curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/docker-compose.yml | sed s/SET_THIS_TO_SOME_RANDOM_TEXT/"$(echo $RANDOM | md5sum | head -c 32)"/g > $DOCKER_COMPOSE_YML
|
||||
if [ -d /dev/dri ]
|
||||
then
|
||||
sed -i 's/'#' - \/dev\/dri/- \/dev\/dri/g' $DOCKER_COMPOSE_YML
|
||||
sed -i 's/'#' "\/dev\/dri/"\/dev\/dri/g' $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
|
||||
echo "Setting permissions on $SCRYPTED_HOME"
|
||||
chown -R $SERVICE_USER $SCRYPTED_HOME
|
||||
|
||||
echo "Optional:"
|
||||
readyn "Edit docker-compose.yml to add external storage for Scrypted NVR?"
|
||||
|
||||
if [ "$yn" == "y" ]
|
||||
then
|
||||
apt install nano
|
||||
nano $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
|
||||
set +e
|
||||
|
||||
echo "docker compose down"
|
||||
@@ -85,3 +80,6 @@ echo "Scrypted is now running at: https://localhost:10443/"
|
||||
echo "Note that it is https and that you'll be asked to approve/ignore the website certificate."
|
||||
echo
|
||||
echo
|
||||
echo "Optional:"
|
||||
echo "Scrypted NVR Recording storage directory can be configured with an additional script:"
|
||||
echo "https://docs.scrypted.app/scrypted-nvr/installation.html#docker-volume"
|
||||
|
||||
140
install/docker/setup-scrypted-nvr-volume.sh
Normal file
140
install/docker/setup-scrypted-nvr-volume.sh
Normal file
@@ -0,0 +1,140 @@
|
||||
|
||||
if [ -z "$SERVICE_USER" ]
|
||||
then
|
||||
echo "Scrypted SERVICE_USER environment variable was not specified. NVR Storage can not be configured."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$USER" != "root" ]
|
||||
then
|
||||
echo "$USER"
|
||||
echo "This script must be run as sudo or root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USER_HOME=$(eval echo ~$SERVICE_USER)
|
||||
SCRYPTED_HOME=$USER_HOME/.scrypted
|
||||
DOCKER_COMPOSE_YML=$SCRYPTED_HOME/docker-compose.yml
|
||||
|
||||
if [ ! -f "$DOCKER_COMPOSE_YML" ]
|
||||
then
|
||||
echo "$DOCKER_COMPOSE_YML not found. Install Scrypted first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NVR_MOUNT_LINE=$(cat "$DOCKER_COMPOSE_YML" | grep :/nvr)
|
||||
if [ -z "$NVR_MOUNT_LINE" ]
|
||||
then
|
||||
echo "Unexpected contents in $DOCKER_COMPOSE_YML. Rerun the Scrypted docker compose installer."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function backup() {
|
||||
BACKUP_FILE="$1".scrypted-bak
|
||||
if [ ! -f "$BACKUP_FILE" ]
|
||||
then
|
||||
cp "$1" "$BACKUP_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
backup "$DOCKER_COMPOSE_YML"
|
||||
|
||||
function readyn() {
|
||||
while true; do
|
||||
read -p "$1 (y/n) " yn
|
||||
case $yn in
|
||||
[Yy]* ) break;;
|
||||
[Nn]* ) break;;
|
||||
* ) echo "Please answer yes or no. (y/n)";;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
lsblk
|
||||
echo ""
|
||||
echo "Please run the script with an existing mount path or the 'disk' device to format (e.g. sdx)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function stopscrypted() {
|
||||
cd "$SCRYPTED_HOME"
|
||||
echo ""
|
||||
echo "Stopping the Scrypted container. If there are any errors during disk setup, Scrypted will need to be manually restarted with:"
|
||||
echo "cd $SCRYPTED_HOME && docker compose up -d"
|
||||
echo ""
|
||||
docker compose down
|
||||
}
|
||||
|
||||
BLOCK_DEVICE="/dev/$1"
|
||||
if [ -b "$BLOCK_DEVICE" ]
|
||||
then
|
||||
readyn "Format $BLOCK_DEVICE?"
|
||||
if [ "$yn" == "n" ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
stopscrypted
|
||||
|
||||
umount "$BLOCK_DEVICE"1 2> /dev/null
|
||||
umount "$BLOCK_DEVICE"2 2> /dev/null
|
||||
umount /mnt/scrypted-nvr 2> /dev/null
|
||||
|
||||
set -e
|
||||
parted "$BLOCK_DEVICE" --script mklabel gpt
|
||||
parted -a optimal "$BLOCK_DEVICE" mkpart scrypted-nvr "0%" "100%"
|
||||
set +e
|
||||
|
||||
sync
|
||||
mkfs -F -t ext4 "$BLOCK_DEVICE"1
|
||||
sync
|
||||
|
||||
# parse/evaluate blkid line as env vars
|
||||
for attr in $(blkid | grep "$BLOCK_DEVICE")
|
||||
do
|
||||
e=$(echo $attr | grep =)
|
||||
if [ ! -z "$e" ]
|
||||
then
|
||||
export "$e"
|
||||
fi
|
||||
done
|
||||
if [ -z "$UUID" ]
|
||||
then
|
||||
echo "Error parsing disk UUID."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "UUID: $UUID"
|
||||
set -e
|
||||
backup "/etc/fstab"
|
||||
grep -v "scrypted-nvr" /etc/fstab > /tmp/fstab && cp /tmp/fstab /etc/fstab
|
||||
# ensure newline
|
||||
sed -i -e '$a\' /etc/fstab
|
||||
mkdir -p /mnt/scrypted-nvr
|
||||
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults 0 0" >> /etc/fstab
|
||||
mount -a
|
||||
set +e
|
||||
|
||||
DIR="/mnt/scrypted-nvr"
|
||||
else
|
||||
if [ ! -d "$1" ]
|
||||
then
|
||||
echo "$1 is not a valid directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
stopscrypted
|
||||
|
||||
DIR="$1"
|
||||
fi
|
||||
|
||||
ESCAPED_DIR=$(echo "$DIR" | sed s/\\//\\\\\\//g)
|
||||
|
||||
set -e
|
||||
sed -i s/'^.*:\/nvr'/" - $ESCAPED_DIR:\/nvr"/ "$DOCKER_COMPOSE_YML"
|
||||
sed -i s/'^.*SCRYPTED_NVR_VOLUME.*$'/" - SCRYPTED_NVR_VOLUME=\/nvr"/ "$DOCKER_COMPOSE_YML"
|
||||
set +e
|
||||
|
||||
cd "$SCRYPTED_HOME" && docker compose up -d
|
||||
@@ -89,6 +89,13 @@ USER_HOME=$(eval echo ~$SERVICE_USER)
|
||||
echo "Setting permissions on $USER_HOME/.scrypted"
|
||||
chown -R $SERVICE_USER $USER_HOME/.scrypted
|
||||
|
||||
echo "Stopping docker service if it exists..."
|
||||
cd $USER_HOME/.scrypted
|
||||
echo "docker compose down"
|
||||
sudo -u $SERVICE_USER docker compose down 2> /dev/null
|
||||
echo "docker compose rm -rf"
|
||||
sudo -u $SERVICE_USER docker rm -f /scrypted /scrypted-watchtower 2> /dev/null
|
||||
|
||||
echo "Installing Scrypted..."
|
||||
RUN sudo -u $SERVICE_USER npx -y scrypted@latest install-server
|
||||
|
||||
|
||||
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.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.3",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
4
plugins/core/ui/package-lock.json
generated
4
plugins/core/ui/package-lock.json
generated
@@ -121,7 +121,7 @@
|
||||
},
|
||||
"../../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"../../../sdk/types": {
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/rimraf": "^3.0.2",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -33,17 +33,56 @@ export default {
|
||||
const termSvcRaw = this.$scrypted.systemManager.getDeviceByName("@scrypted/core");
|
||||
const termSvc = await termSvcRaw.getDevice("terminalservice");
|
||||
const termSvcDirect = await this.$scrypted.connectRPCObject(termSvc);
|
||||
const queue = createAsyncQueue();
|
||||
const dataQueue = createAsyncQueue();
|
||||
const ctrlQueue = createAsyncQueue();
|
||||
|
||||
queue.enqueue(JSON.stringify({ interactive: true }));
|
||||
queue.enqueue(JSON.stringify({ dim: { cols: term.cols, rows: term.rows } }));
|
||||
ctrlQueue.enqueue({ interactive: true });
|
||||
ctrlQueue.enqueue({ dim: { cols: term.cols, rows: term.rows } });
|
||||
|
||||
term.onData(data => queue.enqueue(Buffer.from(data, 'utf8')));
|
||||
term.onBinary(data => queue.enqueue(Buffer.from(data, 'binary')));
|
||||
term.onResize(dim => queue.enqueue(JSON.stringify({ dim })));
|
||||
let bufferedLength = 0;
|
||||
const MAX_BUFFERED_LENGTH = 64000;
|
||||
async function dataQueueEnqueue(data) {
|
||||
bufferedLength += data.length;
|
||||
const promise = dataQueue.enqueue(data).then(() => bufferedLength -= data.length);
|
||||
if (bufferedLength >= MAX_BUFFERED_LENGTH) {
|
||||
term.setOption("disableStdin", true);
|
||||
await promise;
|
||||
if (bufferedLength < MAX_BUFFERED_LENGTH)
|
||||
term.setOption("disableStdin", false);
|
||||
}
|
||||
}
|
||||
|
||||
const localGenerator = queue.queue;
|
||||
const remoteGenerator = await termSvcDirect.connectStream(localGenerator);
|
||||
term.onData(data => dataQueueEnqueue(Buffer.from(data, 'utf8')));
|
||||
term.onBinary(data => dataQueueEnqueue(Buffer.from(data, 'binary')));
|
||||
term.onResize(dim => {
|
||||
ctrlQueue.enqueue({ dim });
|
||||
ctrlQueue.enqueue(Buffer.alloc(0));
|
||||
});
|
||||
|
||||
async function* localGenerator() {
|
||||
while (true) {
|
||||
const ctrlBuffers = ctrlQueue.clear();
|
||||
if (ctrlBuffers.length) {
|
||||
for (const ctrl of ctrlBuffers) {
|
||||
yield JSON.stringify(ctrl);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const dataBuffers = dataQueue.clear();
|
||||
if (dataBuffers.length === 0) {
|
||||
const buf = await dataQueue.dequeue();
|
||||
if (buf.length)
|
||||
yield buf;
|
||||
continue;
|
||||
}
|
||||
|
||||
const concat = Buffer.concat(dataBuffers);
|
||||
if (concat.length)
|
||||
yield concat;
|
||||
}
|
||||
}
|
||||
const remoteGenerator = await termSvcDirect.connectStream(localGenerator());
|
||||
|
||||
for await (const message of remoteGenerator) {
|
||||
if (!message) {
|
||||
|
||||
4
plugins/objectdetector/package-lock.json
generated
4
plugins/objectdetector/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -514,10 +514,14 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (!o.boundingBox)
|
||||
continue;
|
||||
|
||||
o.zones = []
|
||||
const box = normalizeBox(o.boundingBox, detection.inputDimensions);
|
||||
|
||||
let included: boolean;
|
||||
// need a way to explicitly include package zone.
|
||||
if (o.zones)
|
||||
included = true;
|
||||
else
|
||||
o.zones = [];
|
||||
for (const [zone, zoneValue] of Object.entries(this.zones)) {
|
||||
if (zoneValue.length < 3) {
|
||||
// this.console.warn(zone, 'Zone is unconfigured, skipping.');
|
||||
|
||||
4
plugins/openvino/package-lock.json
generated
4
plugins/openvino/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.48",
|
||||
"version": "0.1.51",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.48",
|
||||
"version": "0.1.51",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.48"
|
||||
"version": "0.1.51"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
openvino==2023.2.0
|
||||
openvino==2023.3.0
|
||||
|
||||
# pillow-simd is available on x64 linux
|
||||
# pillow-simd confirmed not building with arm64 linux or apple silicon
|
||||
|
||||
4
plugins/prebuffer-mixin/package-lock.json
generated
4
plugins/prebuffer-mixin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.11",
|
||||
"version": "0.10.12",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.11",
|
||||
"version": "0.10.12",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.11",
|
||||
"version": "0.10.12",
|
||||
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
import path from 'path'
|
||||
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
|
||||
import { getDebugModeH264EncoderArgs, getH264EncoderArgs } from '@scrypted/common/src/ffmpeg-hardware-acceleration';
|
||||
import { addVideoFilterArguments } from '@scrypted/common/src/ffmpeg-helpers';
|
||||
@@ -971,6 +971,7 @@ class PrebufferSession {
|
||||
|
||||
const clientPromise = await listenSingleRtspClient({
|
||||
hostname,
|
||||
pathToken: path.join(crypto.randomBytes(8).toString('hex'), this.mixin.id),
|
||||
createServer: duplex => {
|
||||
sdp = addTrackControls(sdp);
|
||||
server = new FileRtspServer(duplex, sdp);
|
||||
|
||||
Submodule plugins/sample-cameraprovider updated: bfcc0b8df6...51bbc2be20
191
plugins/synology-ss/package-lock.json
generated
191
plugins/synology-ss/package-lock.json
generated
@@ -1,30 +1,30 @@
|
||||
{
|
||||
"name": "@scrypted/synology-ss",
|
||||
"version": "0.0.16",
|
||||
"version": "0.0.17",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/synology-ss",
|
||||
"version": "0.0.16",
|
||||
"version": "0.0.17",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"axios": "^0.24.0"
|
||||
"axios": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.6.1"
|
||||
"@types/node": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.86",
|
||||
"version": "0.3.5",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
@@ -59,23 +59,52 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz",
|
||||
"integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==",
|
||||
"dev": true
|
||||
"version": "18.19.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.10.tgz",
|
||||
"integrity": "sha512-IZD8kAM02AW1HRDTPOlz3npFava678pr8Ie9Vp8uRhBROXAv8MXT2pCnGZZAKYdromsNQLHQcfWQ6EOatVLtqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
|
||||
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
"follow-redirects": "^1.15.4",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -90,6 +119,49 @@
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -100,7 +172,7 @@
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
@@ -118,23 +190,80 @@
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "16.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz",
|
||||
"integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==",
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||
"version": "18.19.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.10.tgz",
|
||||
"integrity": "sha512-IZD8kAM02AW1HRDTPOlz3npFava678pr8Ie9Vp8uRhBROXAv8MXT2pCnGZZAKYdromsNQLHQcfWQ6EOatVLtqA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
|
||||
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.4",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"requires": {
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/synology-ss",
|
||||
"version": "0.0.16",
|
||||
"version": "0.0.17",
|
||||
"description": "A Synology Surveillance Station plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -36,10 +36,10 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.24.0"
|
||||
"axios": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.6.1"
|
||||
"@types/node": "^18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,11 +164,12 @@ export class SynologyApiClient {
|
||||
const response = await this.client.get<SynologyApiResponse<T>>(url ?? await this.getApiPath(params.api), { params });
|
||||
|
||||
if (!response.data?.success) {
|
||||
if (response.data?.error?.code) {
|
||||
const errorCode = response.data?.error?.code;
|
||||
if (errorCode) {
|
||||
const errorCodeLookup = { ...errorCodeDescriptions, ...extraErrorCodes }
|
||||
throw new Error(`${errorCodeLookup[response.data.error.code]} (error code ${response.data.error.code})`)
|
||||
throw new SynologyApiError(`${errorCodeLookup[errorCode]} (error code ${errorCode})`, errorCode)
|
||||
} else {
|
||||
throw new Error(`Synology API call failed with status code ${response.status}`);
|
||||
throw new SynologyApiError(`Synology API call failed with status code ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +187,17 @@ export interface SynologyApiInfo {
|
||||
maxVersion: number;
|
||||
}
|
||||
|
||||
export interface SynologyApiError {
|
||||
export class SynologyApiError extends Error {
|
||||
code?: string;
|
||||
constructor(message: string, code?: string) {
|
||||
super(message);
|
||||
|
||||
this.name = 'SynologyApiError';
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SynologyApiErrorObject {
|
||||
code: string;
|
||||
}
|
||||
|
||||
@@ -198,7 +209,7 @@ interface SynologyApiRequestParams {
|
||||
|
||||
interface SynologyApiResponse<T> {
|
||||
data?: T;
|
||||
error?: SynologyApiError;
|
||||
error?: SynologyApiErrorObject;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import sdk, { Camera, Device, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, MediaObject, MediaStreamOptions, MediaStreamUrl, MotionSensor, PictureOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera } from "@scrypted/sdk";
|
||||
import { createInstanceableProviderPlugin, enableInstanceableProviderMode, isInstanceableProviderModeEnabled } from '../../../common/src/provider-plugin';
|
||||
import { SynologyApiClient, SynologyCamera, SynologyCameraStream } from "./api/synology-api-client";
|
||||
import { SynologyApiClient, SynologyApiError, SynologyCamera, SynologyCameraStream } from "./api/synology-api-client";
|
||||
|
||||
const { deviceManager } = sdk;
|
||||
|
||||
@@ -162,10 +162,11 @@ class SynologyCameraDevice extends ScryptedDeviceBase implements Camera, HttpReq
|
||||
}
|
||||
|
||||
class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings, DeviceProvider {
|
||||
private cameras: SynologyCamera[];
|
||||
private cameras: SynologyCamera[] = [];
|
||||
private cameraDevices: Map<string, SynologyCameraDevice> = new Map();
|
||||
api: SynologyApiClient;
|
||||
private startup: Promise<void>;
|
||||
private discovering: boolean;
|
||||
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
@@ -177,66 +178,23 @@ class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings
|
||||
}
|
||||
|
||||
public async discoverDevices(duration: number): Promise<void> {
|
||||
const url = this.getSetting('url');
|
||||
const username = this.getSetting('username');
|
||||
const password = this.getSetting('password');
|
||||
const otpCode = this.getSetting('otpCode');
|
||||
const mfaDeviceId = this.getSetting('mfaDeviceId');
|
||||
if (this.discovering) return;
|
||||
this.discovering = true;
|
||||
|
||||
this.log.clearAlerts();
|
||||
|
||||
if (!url) {
|
||||
this.log.a('Must provide URL.');
|
||||
return
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
this.log.a('Must provide username.');
|
||||
return
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
this.log.a('Must provide password.');
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.api || url !== this.api.url) {
|
||||
this.api = new SynologyApiClient(url);
|
||||
}
|
||||
this.console.info(`Fetching list of cameras from Synology server...`);
|
||||
|
||||
try {
|
||||
const newMfaDeviceId = await this.api.login(username, password, otpCode ? parseInt(otpCode) : undefined, !!otpCode, 'Scrypted', mfaDeviceId);
|
||||
|
||||
// If a OTP was present, store the device ID to allow us to skip the OTP requirement next login.
|
||||
if (otpCode) {
|
||||
this.storage.setItem('mfaDeviceId', newMfaDeviceId);
|
||||
if (!await this.tryLogin()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.log.a(`login error: ${e}`);
|
||||
this.console.error('login error', e);
|
||||
|
||||
// Clear device ID upon login failure, since it's likely useless now
|
||||
this.storage.removeItem('mfaDeviceId');
|
||||
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
// Clear the OTP setting if provided since it's a temporary code
|
||||
if (otpCode) {
|
||||
this.storage.removeItem('otpCode');
|
||||
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.cameras = await this.api.listCameras();
|
||||
|
||||
if (!this.cameras) {
|
||||
this.console.error('Cameras failed to load. Retrying in 10 seconds.');
|
||||
setTimeout(() => {
|
||||
this.discoverDevices(0);
|
||||
}, 100000);
|
||||
}, 10000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -285,6 +243,8 @@ class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings
|
||||
catch (e) {
|
||||
this.log.a(`device discovery error: ${e}`);
|
||||
this.console.error('device discovery error', e);
|
||||
} finally {
|
||||
this.discovering = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +313,81 @@ class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings
|
||||
return;
|
||||
}
|
||||
this.storage.setItem(key, value.toString());
|
||||
this.discoverDevices(0);
|
||||
|
||||
// Delaying discover in case user updated multiple settings, so that it doesn't run until all have been set
|
||||
setTimeout(() => this.discoverDevices(0), 200);
|
||||
}
|
||||
|
||||
private async tryLogin(): Promise<boolean> {
|
||||
this.console.info('Logging into Synology...');
|
||||
|
||||
const url = this.getSetting('url');
|
||||
const username = this.getSetting('username');
|
||||
const password = this.getSetting('password');
|
||||
const otpCode = this.getSetting('otpCode');
|
||||
const mfaDeviceId = this.getSetting('mfaDeviceId');
|
||||
|
||||
this.log.clearAlerts();
|
||||
|
||||
if (!url) {
|
||||
this.log.a('Must provide URL.');
|
||||
return
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
this.log.a('Must provide username.');
|
||||
return
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
this.log.a('Must provide password.');
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.api || url !== this.api.url) {
|
||||
this.api = new SynologyApiClient(url);
|
||||
}
|
||||
|
||||
let successful = false;
|
||||
for (let attempt=1; attempt<=3; attempt++) {
|
||||
try {
|
||||
const newMfaDeviceId = await this.api.login(username, password, otpCode ? parseInt(otpCode) : undefined, !!otpCode, 'Scrypted', mfaDeviceId);
|
||||
|
||||
// If a OTP was present, store the device ID to allow us to skip the OTP requirement next login.
|
||||
if (otpCode) {
|
||||
this.storage.setItem('mfaDeviceId', newMfaDeviceId);
|
||||
}
|
||||
|
||||
successful = true;
|
||||
}
|
||||
catch (e) {
|
||||
this.log.a(`login error on attempt ${attempt}: ${e}`);
|
||||
this.console.error(`login error on attempt ${attempt}`, e);
|
||||
|
||||
if (e instanceof SynologyApiError) {
|
||||
break;
|
||||
} else {
|
||||
// Retry on failures that aren't Synology-specific, such as timeouts
|
||||
await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Clear the OTP setting if provided since it's a temporary code
|
||||
if (otpCode) {
|
||||
this.storage.removeItem('otpCode');
|
||||
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (successful) {
|
||||
this.console.info(`Successfully logged into Synology`);
|
||||
} else {
|
||||
this.console.info(`Failed to log into Synology`);
|
||||
}
|
||||
|
||||
return successful;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
4
plugins/tensorflow-lite/package-lock.json
generated
4
plugins/tensorflow-lite/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/tensorflow-lite",
|
||||
"version": "0.1.45",
|
||||
"version": "0.1.46",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/tensorflow-lite",
|
||||
"version": "0.1.45",
|
||||
"version": "0.1.46",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -53,5 +53,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.45"
|
||||
"version": "0.1.46"
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class TensorFlowLitePlugin(
|
||||
nonlocal model
|
||||
|
||||
if defaultModel:
|
||||
model = "yolov8n_full_integer_quant_320"
|
||||
model = "efficientdet_lite0_320_ptq"
|
||||
self.yolo = "yolo" in model
|
||||
self.yolov8 = "yolov8" in model
|
||||
|
||||
|
||||
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.87.0",
|
||||
"version": "0.91.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.87.0",
|
||||
"version": "0.91.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.87.0",
|
||||
"version": "0.92.0",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
@@ -62,7 +62,7 @@
|
||||
"build": "tsc --outDir dist",
|
||||
"postbuild": "node test/check-build-output.js",
|
||||
"beta": "npm publish --tag beta",
|
||||
"postbeta": "npm version minor && git add package.json && npm run build && git commit -m postbeta",
|
||||
"postbeta": "npm version patch && git add package.json && npm run build && git commit -m postbeta",
|
||||
"release": "npm publish",
|
||||
"prepublishOnly": "npm run build",
|
||||
"postrelease": "git tag v$npm_package_version && git push origin v$npm_package_version && npm version minor && git add package.json && git commit -m postrelease",
|
||||
|
||||
@@ -7,9 +7,15 @@ export function getScryptedVolume() {
|
||||
return volumeDir;
|
||||
}
|
||||
|
||||
export function getPluginVolume(pluginId: string) {
|
||||
export function getPluginsVolume() {
|
||||
const volume = getScryptedVolume();
|
||||
const pluginVolume = path.join(volume, 'plugins', pluginId);
|
||||
const pluginsVolume = path.join(volume, 'plugins');
|
||||
return pluginsVolume;
|
||||
}
|
||||
|
||||
export function getPluginVolume(pluginId: string) {
|
||||
const volume = getPluginsVolume();
|
||||
const pluginVolume = path.join(volume, pluginId);
|
||||
return pluginVolume;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,7 @@ export class NodeForkWorker extends ChildProcessWorker {
|
||||
|
||||
this.worker = child_process.fork(mainFilename, ['child', this.pluginId], {
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
||||
env: Object.assign({}, process.env, env, {
|
||||
NODE_PATH: path.join(getPluginNodePath(this.pluginId), 'node_modules'),
|
||||
}),
|
||||
env: Object.assign({}, process.env, env),
|
||||
serialization: 'advanced',
|
||||
execArgv,
|
||||
});
|
||||
|
||||
@@ -16,9 +16,7 @@ export class NodeThreadWorker extends EventEmitter implements RuntimeWorker {
|
||||
|
||||
this.worker = new worker_threads.Worker(mainFilename, {
|
||||
argv: ['child-thread', this.pluginId],
|
||||
env: Object.assign({}, process.env, env, {
|
||||
NODE_PATH: path.join(getPluginNodePath(this.pluginId), 'node_modules'),
|
||||
}),
|
||||
env: Object.assign({}, process.env, env),
|
||||
});
|
||||
|
||||
this.worker.on('exit', () => {
|
||||
|
||||
@@ -53,10 +53,12 @@ export class PythonRuntimeWorker extends ChildProcessWorker {
|
||||
const pluginPythonVersion = options.packageJson.scrypted.pythonVersion?.[os.platform()]?.[os.arch()] || options.packageJson.scrypted.pythonVersion?.default;
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
pythonPath ||= 'py.exe';
|
||||
const windowsPythonVersion = pluginPythonVersion || process.env.SCRYPTED_WINDOWS_PYTHON_VERSION;
|
||||
if (windowsPythonVersion)
|
||||
args.unshift(windowsPythonVersion)
|
||||
if (!pythonPath) {
|
||||
pythonPath = 'py.exe';
|
||||
const windowsPythonVersion = pluginPythonVersion || process.env.SCRYPTED_WINDOWS_PYTHON_VERSION;
|
||||
if (windowsPythonVersion)
|
||||
args.unshift(windowsPythonVersion)
|
||||
}
|
||||
}
|
||||
else if (pluginPythonVersion) {
|
||||
pythonPath = `python${pluginPythonVersion}`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import v8 from 'v8';
|
||||
import vm from 'vm';
|
||||
import dns from 'dns';
|
||||
import process from 'process';
|
||||
import semver from 'semver';
|
||||
import { RPCResultError, startPeriodicGarbageCollection } from './rpc';
|
||||
import v8 from 'v8';
|
||||
import vm from 'vm';
|
||||
import { PluginError } from './plugin/plugin-error';
|
||||
import { RPCResultError, startPeriodicGarbageCollection } from './rpc';
|
||||
import type { Runtime } from './scrypted-server-main';
|
||||
|
||||
export function isChildProcess() {
|
||||
@@ -26,7 +27,7 @@ function start(mainFilename: string, options?: {
|
||||
// This causes issues with clients that are on "IPv6" networks that are
|
||||
// actually busted and fail to connect to npm's IPv6 address.
|
||||
// The workaround is to favor IPv4.
|
||||
process.env['NODE_OPTIONS'] = '--dns-result-order=ipv4first';
|
||||
dns.setDefaultResultOrder('ipv4first');
|
||||
|
||||
startPeriodicGarbageCollection();
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { startPluginRemote } from "./plugin/plugin-remote-worker";
|
||||
import { RpcMessage } from "./rpc";
|
||||
import worker_threads from "worker_threads";
|
||||
import v8 from 'v8';
|
||||
import net from 'net';
|
||||
import v8 from 'v8';
|
||||
import worker_threads from "worker_threads";
|
||||
import { getPluginNodePath } from "./plugin/plugin-npm-dependencies";
|
||||
import { startPluginRemote } from "./plugin/plugin-remote-worker";
|
||||
import { SidebandSocketSerializer } from "./plugin/socket-serializer";
|
||||
import { RpcMessage } from "./rpc";
|
||||
|
||||
function start(mainFilename: string) {
|
||||
const pluginId = process.argv[3];
|
||||
console.log('starting plugin', pluginId);
|
||||
module.paths.push(getPluginNodePath(pluginId));
|
||||
|
||||
if (process.argv[2] === 'child-thread') {
|
||||
const peer = startPluginRemote(mainFilename, process.argv[3], (message, reject) => {
|
||||
try {
|
||||
|
||||
@@ -113,7 +113,6 @@ app.use(bodyParser.raw({ type: 'application/zip', limit: 100000000 }) as any)
|
||||
|
||||
async function start(mainFilename: string, options?: {
|
||||
onRuntimeCreated?: (runtime: ScryptedRuntime) => Promise<void>,
|
||||
restart?: () => void,
|
||||
}) {
|
||||
const volumeDir = getScryptedVolume();
|
||||
await fs.promises.mkdir(volumeDir, {
|
||||
@@ -150,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');
|
||||
@@ -340,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({
|
||||
@@ -354,62 +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.",
|
||||
});
|
||||
}
|
||||
|
||||
if (options?.restart)
|
||||
options.restart();
|
||||
else
|
||||
process.exit();
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
68
server/src/services/backup.ts
Normal file
68
server/src/services/backup.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import Level from '../level';
|
||||
import { sleep } from '../sleep';
|
||||
import { getPluginsVolume, getScryptedVolume } from '../plugin/plugin-volume';
|
||||
import AdmZip from 'adm-zip';
|
||||
import { ScryptedRuntime } from '../runtime';
|
||||
import { getPluginNodePath } from '../plugin/plugin-npm-dependencies';
|
||||
|
||||
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();
|
||||
|
||||
// nuke the existing database path
|
||||
await fs.promises.rm(dbPath, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
|
||||
// nuke all the plugins and associated files downloaded by thhem.
|
||||
// first run after restore will reinstall everything.
|
||||
await fs.promises.rm(getPluginsVolume(), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
|
||||
zip.extractAllTo(dbPath, true);
|
||||
this.runtime.serviceControl.restart();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user