completed basic backup and restore

Basic backup and restore, will need to to do extensive testing but it should work

Reworked websocket class to allow for updating rows of data instead of sending a new row for an updated state

Cleaned up more stats that used jf_all_user_activity
This commit is contained in:
Thegan Govender
2023-04-24 18:44:44 +02:00
parent 03ab7fc2ff
commit 0b5b4b6e19
9 changed files with 167 additions and 100 deletions

View File

@@ -27,7 +27,28 @@ function createWebSocketServer() {
// define a separate method that sends a message to all connected clients
function sendMessageToClients(message) {
messages.push(message);
if(message.key)
{
const findMessage = messages.filter(item => item.hasOwnProperty('key')).find(item => item.key === message.key);
if(findMessage)
{
messages.filter(item => item.hasOwnProperty('key')).forEach(item => {
if (item.key === message.key) {
item.Message = message.Message;
}
});
}else{
messages.push(message);
}
}else{
messages.push(message);
}
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(messages));

View File

@@ -187,7 +187,7 @@ router.get("/getHistory", async (req, res) => {
const { rows } = await db.query(
`SELECT * FROM jf_all_playback_activity order by "ActivityDateInserted" desc`
`SELECT * FROM jf_playback_activity order by "ActivityDateInserted" desc`
);
const groupedResults = {};

File diff suppressed because one or more lines are too long

View File

@@ -6,9 +6,6 @@ const moment = require('moment');
const wss = require("./WebsocketHandler");
var messages=[];
const router = Router();
// Database connection parameters
@@ -21,6 +18,7 @@ const postgresDatabase = process.env.POSTGRES_DATABASE || 'jfstat';
// Tables to back up
const tables = ['jf_libraries', 'jf_library_items', 'jf_library_seasons','jf_library_episodes','jf_users','jf_playback_activity','jf_playback_reporting_plugin_data','jf_item_info'];
// Backup function
async function backup() {
const pool = new Pool({
@@ -32,10 +30,15 @@ async function backup() {
});
// Get data from each table and append it to the backup file
try{
let now = moment();
const backupPath = `./backup-data/backup_${now.format('yyyy-MM-DD HH-mm-ss')}.json`;
const stream = fs.createWriteStream(backupPath, { flags: 'a' });
const backup_data=[];
wss.clearMessages();
wss.sendMessageToClients({ color: "yellow", Message: "Begin Backup "+backupPath });
for (let table of tables) {
@@ -43,104 +46,110 @@ async function backup() {
const { rows } = await pool.query(query);
console.log(`Reading ${rows.length} rows for table ${table}`);
wss.sendMessageToClients({color: "dodgerblue",Message: `Saving ${rows.length} rows for table ${table}`});
wss.sendMessageToClients({color: "dodgerblue",Message: `Saving ${rows.length} rows for table ${table}`});
backup_data.push({[table]:rows});
// stream.write(JSON.stringify(backup_data));
backup_data.push({[table]:rows});
}
wss.sendMessageToClients({ color: "lawngreen", Message: "Backup Complete" });
stream.write(JSON.stringify(backup_data));
stream.end();
await stream.write(JSON.stringify(backup_data));
stream.end();
wss.sendMessageToClients({ color: "lawngreen", Message: "Backup Complete" });
}catch(error)
{
console.log(error);
wss.sendMessageToClients({ color: "red", Message: "Backup Failed: "+error });
}
await pool.end();
}
// Restore function
async function restore() {
// const pool = new Pool({
// user: 'postgres',
// password: 'mypassword',
// host: postgresIp,
// port: 25432,
// database: postgresDatabase
// });
let user='postgres';
let password='mypassword';
let host=postgresIp;
let port=25432;
let database= postgresDatabase;
const client = new Pool({
host,
port,
database,
user,
password
});
const backupPath = './backup-data/backup.json';
let jsonData;
await fs.readFile(backupPath, 'utf8', async (err, data) => {
function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) {
console.error(err);
reject(err);
return;
}
jsonData = await JSON.parse(data);
const json = JSON.parse(data);
resolve(json);
});
});
}
console.log(jsonData);
async function restore(file) {
wss.clearMessages();
wss.sendMessageToClients({ color: "yellow", Message: "Restoring from Backup: "+file });
const pool = new Pool({
user: postgresUser,
password: postgresPassword,
host: postgresIp,
port: postgresPort,
database: postgresDatabase
});
const backupPath = file;
let jsonData;
try {
// Use await to wait for the Promise to resolve
jsonData = await readFile(backupPath);
} catch (err) {
console.error(err);
}
// console.log(jsonData);
if(!jsonData)
{
console.log('No Data');
return;
}
jsonData.forEach((library) => {
for(let table of jsonData)
{
const data = Object.values(table)[0];
const tableName=Object.keys(table)[0];
console.log(library);
});
for(let index in data)
{
// await client.connect();
wss.sendMessageToClients({ color: "dodgerblue",key:tableName ,Message: `Restoring ${tableName} ${(((index)/(data.length-1))*100).toFixed(2)}%`});
const keysWithQuotes = Object.keys(data[index]).map(key => `"${key}"`);
const keyString = keysWithQuotes.join(", ");
const valuesWithQuotes = Object.values(data[index]).map(col => {
if (col === null) {
return 'NULL';
} else if (typeof col === 'string') {
return `'${col.replace(/'/g, "''")}'`;
}else if (typeof col === 'object') {
return `'${JSON.stringify(col).replace(/'/g, "''")}'`;
} else {
return `'${col}'`;
}
});
const valueString = valuesWithQuotes.join(", ");
const query=`INSERT INTO ${tableName} (${keyString}) VALUES(${valueString}) ON CONFLICT DO NOTHING`;
const { rows } = await pool.query( query );
}
}
await pool.end();
// let tableStarted = false;
// let tableColumns = '';
// let tableValues = '';
// let tableName = '';
// for await (const line of rl) {
// if (!tableStarted && line.startsWith('COPY ')) {
// tableName = line.match(/COPY (.*) \(/)[1];
// tableStarted = true;
// tableColumns = '';
// tableValues = '';
// } else if (tableStarted && line.startsWith('\.')) {
// const insertStatement = `INSERT INTO ${tableName} (${tableColumns}) VALUES ${tableValues};`;
// await client.query(insertStatement);
// tableStarted = false;
// } else if (tableStarted && tableColumns === '') {
// tableColumns = line.replace(/\(/g, '').replace(/\)/g, '').replace(/"/g, '').trim();
// tableValues = '';
// } else if (tableStarted) {
// const values = line.replace(/\(/g, '').replace(/\)/g, '').split('\t').map(value => {
// if (value === '') {
// return null;
// }
// if (!isNaN(parseFloat(value))) {
// return parseFloat(value);
// }
// return value.replace(/'/g, "''");
// });
// const rowValues = `(${values.join(',')})`;
// tableValues = `${tableValues}${tableValues === '' ? '' : ','}${rowValues}`;
// }
// }
// await client.end();
}
// Route handler for backup endpoint
@@ -154,13 +163,16 @@ router.get('/backup', async (req, res) => {
}
});
router.get('/restore', async (req, res) => {
router.get('/restore/:filename', async (req, res) => {
try {
await restore();
res.send('Backup completed successfully');
const filePath = path.join(__dirname, backupfolder, req.params.filename);
await restore(filePath);
wss.sendMessageToClients({ color: "lawngreen", Message: `Restoring Complete` });
res.send('Restore completed successfully');
} catch (error) {
console.error(error);
res.status(500).send('Backup failed');
wss.sendMessageToClients({ color: "red", Message: error });
res.status(500).send('Restore failed');
}
});
@@ -174,7 +186,8 @@ router.get('/restore', async (req, res) => {
if (err) {
res.status(500).send('Unable to read directory');
} else {
const fileData = files.map(file => {
const fileData = files.filter(file => file.endsWith('.json'))
.map(file => {
const filePath = path.join(directoryPath, file);
const stats = fs.statSync(filePath);
return {

View File

@@ -170,7 +170,7 @@ router.get("/getPlaybackActivity", async (req, res) => {
router.get("/getAllUserActivity", async (req, res) => {
try {
const { rows } = await db.query("SELECT * FROM jf_all_user_activity");
const { rows } = await db.query("SELECT * FROM jf_user_activity");
res.send(rows);
} catch (error) {
res.send(error);

View File

@@ -66,7 +66,21 @@ function ActivityTable(props) {
}
}));
}
function formatTotalWatchTime(seconds) {
const hours = Math.floor(seconds / 3600); // 1 hour = 3600 seconds
const minutes = Math.floor((seconds % 3600) / 60); // 1 minute = 60 seconds
let formattedTime='';
if(hours)
{
formattedTime+=`${hours} hours`;
}
if(minutes)
{
formattedTime+=` ${minutes} minutes`;
}
return formattedTime ;
}
@@ -78,6 +92,7 @@ function ActivityTable(props) {
<div onClick={() => handleSort("UserName")}>User</div>
<div onClick={() => handleSort("NowPlayingItemName")}>Title </div>
<div onClick={() => handleSort("ActivityDateInserted")}>Date</div>
<div onClick={() => handleSort("PlaybackDuration")}>Playback Duration</div>
<div onClick={() => handleSort("results")}>Total Plays</div>
</div>
@@ -88,6 +103,7 @@ function ActivityTable(props) {
<div><Link to={`/users/${item.UserId}`}>{item.UserName}</Link></div>
<div><Link to={`/item/${item.EpisodeId || item.NowPlayingItemId}`}>{!item.SeriesName ? item.NowPlayingItemName : item.SeriesName+' - '+ item.NowPlayingItemName}</Link></div>
<div>{Intl.DateTimeFormat('en-UK', options).format(new Date(item.ActivityDateInserted))}</div>
<div>{formatTotalWatchTime(item.PlaybackDuration) || '0 sec'}</div>
<div>{item.results.length+1}</div>
</div>
<div className={`sub-table ${item.isCollapsed ? 'collapsed' : ''}`}>
@@ -98,6 +114,7 @@ function ActivityTable(props) {
<div><Link to={`/item/${sub_item.EpisodeId || sub_item.NowPlayingItemId}`}>{!sub_item.SeriesName ? sub_item.NowPlayingItemName : sub_item.SeriesName+' - '+ sub_item.NowPlayingItemName}</Link></div>
<div>{Intl.DateTimeFormat('en-UK', options).format(new Date(sub_item.ActivityDateInserted))}</div>
<div></div>
<div>1</div>
</div>
))}
</div>

View File

@@ -21,16 +21,6 @@ const TerminalComponent = () => {
}
}, []);
// function to handle scrolling to the last message
const scrollToBottom = () => {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
// scroll to the last message whenever messages change
useEffect(() => {
scrollToBottom();
}, [messages]);
return (
<div className='my-4'>
<div className="console-container">

View File

@@ -1,6 +1,6 @@
import React, { useState,useEffect } from "react";
import axios from "axios";
import Button from "react-bootstrap/Button";
import { DropdownButton, Dropdown, Button } from 'react-bootstrap';
import Alert from "react-bootstrap/Alert";
@@ -59,6 +59,25 @@ export default function BackupFiles() {
});
}
async function restoreBackup(filename) {
const url=`/data/restore/${filename}`;
axios
.get(url, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
.then((response) => {
setshowAlert({visible:true,title:'Success',type:'success',message:response.data});
})
.catch((error) => {
setshowAlert({visible:true,title:'Error',type:'danger',message:error.response.data});
});
}
async function deleteBackup(filename) {
const url=`/data/files/${filename}`;
axios
@@ -138,7 +157,6 @@ export default function BackupFiles() {
<th>Date Created</th>
<th>Size</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
@@ -148,8 +166,15 @@ export default function BackupFiles() {
<td>{file.name}</td>
<td>{Intl.DateTimeFormat('en-UK', options).format(new Date(file.datecreated))}</td>
<td>{formatFileSize(file.size)}</td>
<td ><Button type="button" onClick={()=>downloadBackup(file.name)} >Download</Button></td>
<td ><Button type="button" className="btn-danger" onClick={()=>deleteBackup(file.name)} >Delete</Button></td>
<td>
<DropdownButton title="Actions" variant="outline-primary">
<Dropdown.Item as="button" variant="primary" onClick={()=>downloadBackup(file.name)}>Download</Dropdown.Item>
<Dropdown.Item as="button" variant="warning" onClick={()=>restoreBackup(file.name)}>Restore</Dropdown.Item>
<Dropdown.Divider ></Dropdown.Divider>
<Dropdown.Item as="button" variant="danger" onClick={()=>deleteBackup(file.name)}>Delete</Dropdown.Item>
</DropdownButton>
</td>
</tr>
))}
{files.length===0 ? <tr><td colSpan="5" style={{ textAlign: "center", fontStyle: "italic" ,color:"gray"}}>No Backups Found</td></tr> :''}

View File

@@ -70,7 +70,7 @@ div a
.collapsed {
transition: all 0.3s ease;
opacity: 100;
max-height: 500px;
max-height: min-content;
}
.sub-row{