TODO: updated all quota / expire / delete drive functionality including recalculating device used storage in worker.js

also fixed some bugs, some performance improvements, removed drive count limit and added a "MOTD" field rendered on the login page.
pull/4/head
Florian Brede 2021-05-17 03:53:25 +02:00
parent a2f7c65cea
commit e000d7a7a6
3 changed files with 211 additions and 33 deletions

View File

@ -19,12 +19,13 @@ var config = {
baseDriveDownloadPathMapping: '/realdata', // path mapping of above download url for expressjs, prefix with "/"
storagePath: 'realdata/', // relative or absolute ( "/..." for absolute path )
deviceStorageQuotaMb: 200000,
deviceDriveQuota: 1000,
deviceDriveExpirationDays: 30,
cabanaUrl: 'http://192.168.1.165:3001/'
cabanaUrl: 'http://192.168.1.165:3001/',
welcomeMessage: `<><><><><><><><><><><><><><><><><><><><><><><br>2021 RetroPilot`
};
module.exports = config;

View File

@ -23,11 +23,16 @@ const sendmail = require('sendmail')();
const htmlspecialchars = require('htmlspecialchars');
const dirTree = require("directory-tree");
const execSync = require('child_process').execSync;
const adapter = new FileSync(config.databaseFile);
const db = low(adapter);
const ALL = 1E8;
var totalStorageUsed=null; // global variable that is regularly updated in the background to track the total used storage
log4js.configure({
appenders: { logfile: { type: "file", filename: "server.log" }, out: { type: "console"} },
@ -192,6 +197,13 @@ function getAuthenticatedAccount(req) {
return account.value();
}
function updateTotalStorageUsed() {
var verifiedPath = mkDirByPathSync(config.storagePath, {isRelativeToScript: (config.storagePath.indexOf("/")===0 ? false : true)});
if (verifiedPath!==null) {
totalStorageUsed = execSync("du -hs "+verifiedPath+" | awk -F'\t' '{print $1;}'").toString();
}
setTimeout(function() {updateTotalStorageUsed();}, 120000); // update the used storage each 120 seconds
}
// CREATE OUR SERVER EXPRESS APP
@ -271,7 +283,7 @@ app.put('/backend/post_upload', bodyParser.raw({ inflate: true, limit: '100000kb
// DRIVE & BOOT/CRASH LOG FILE UPLOAD URL REQUEST
app.get('/v1.3/:dongleId/upload_url/', (req, res) => {
var path = req.query.path; // todo: validate filename
var path = req.query.path;
logger.info("HTTP.UPLOAD_URL called for "+req.params.dongleId+" and file "+path+": "+JSON.stringify(req.headers));
var device = db.get('devices').find({ dongle_id: req.params.dongleId});
@ -425,7 +437,7 @@ app.post('/v2/pilotauth/', bodyParser.urlencoded({ extended: true }), (req, res)
var device = db.get('devices').find({dongle_id: dongleId}).value();
if (!device) {
var resultingDevice = db.get('devices')
.push({ dongle_id: dongleId, account_id: 0, imei: imei1, serial: serial, device_type: 'freon', public_key: public_key, created: Date.now(), last_ping: Date.now()})
.push({ dongle_id: dongleId, account_id: 0, imei: imei1, serial: serial, device_type: 'freon', public_key: public_key, created: Date.now(), last_ping: Date.now(), storage_used: 0})
.write();
var device = db.get('devices').find({dongle_id: dongleId}).value();
@ -529,12 +541,6 @@ app.get('/useradmin', (req, res) => {
return;
}
var verifiedPath = mkDirByPathSync(config.storagePath, {isRelativeToScript: (config.storagePath.indexOf("/")===0 ? false : true)});
if (verifiedPath!==null) {
const execSync = require('child_process').execSync;
bytes = execSync("du -hs "+verifiedPath+" | awk -F'\t' '{print $1;}'").toString();
}
res.status(200);
res.send('<html style="font-family: monospace"><h2>Welcome To The RetroPilot Server Dashboard!</h2>'+
`<br><br>
@ -548,8 +554,7 @@ app.get('/useradmin', (req, res) => {
'Accounts: '+db.get('accounts').size().value()+' | '+
'Devices: '+db.get('devices').size().value()+' | '+
'Drives: '+db.get('drives').size().value()+' | '+
'Storage Used: '+(verifiedPath!==null ? bytes : '--')+'</html>');
'Storage Used: '+(totalStorageUsed!==null ? totalStorageUsed : '--')+'<br><br>'+config.welcomeMessage+'</html>');
}),
@ -673,7 +678,7 @@ app.get('/useradmin/overview', (req, res) => {
return;
}
const devices = db.get('devices').filter({account_id: account.id}).sortBy('dongle_id').take(1000).value();
const devices = db.get('devices').filter({account_id: account.id}).sortBy('dongle_id').take(ALL).value();
var response = '<html style="font-family: monospace"><h2>Welcome To The RetroPilot Server Dashboard!</h2>'+
@ -683,11 +688,11 @@ app.get('/useradmin/overview', (req, res) => {
<b>Created:</b> `+formatDate(account.created)+`<br><br>
<b>Devices:</b><br>
<table border=1 cellpadding=2 cellspacing=2>
<tr><th>dongle_id</th><th>device_type</th><th>created</th><th>last_ping</th></tr>
<tr><th>dongle_id</th><th>device_type</th><th>created</th><th>last_ping</th><th>storage_used</th></tr>
`;
for (var i in devices) {
response+='<tr><td><a href="/useradmin/device/'+devices[i].dongle_id+'">'+devices[i].dongle_id+'</a></td><td>'+devices[i].device_type+'</td><td>'+formatDate(devices[i].created)+'</td><td>'+formatDate(devices[i].last_ping)+'</td></tr>';
response+='<tr><td><a href="/useradmin/device/'+devices[i].dongle_id+'">'+devices[i].dongle_id+'</a></td><td>'+devices[i].device_type+'</td><td>'+formatDate(devices[i].created)+'</td><td>'+formatDate(devices[i].last_ping)+'</td><td>'+devices[i].storage_used+' MB</td></tr>';
}
response+=`</table>
<br>
@ -771,7 +776,7 @@ app.get('/useradmin/device/:dongleId', (req, res) => {
}
device = device.value();
const drives = db.get('drives').filter({dongle_id: device.dongle_id, is_deleted: false}).sortBy('created').take(1000).value();
const drives = db.get('drives').filter({dongle_id: device.dongle_id, is_deleted: false}).sortBy('created').take(ALL).value();
var dongleIdHash = crypto.createHmac('sha256', config.applicationSalt).update(device.dongle_id).digest('hex');
@ -812,8 +817,8 @@ app.get('/useradmin/device/:dongleId', (req, res) => {
<b>Last Ping:</b> `+formatDate(device.last_ping)+`<br>
<b>Public Key:</b><br><span style="font-size: 0.8em">`+device.public_key.replace(/\r?\n|\r/g, "<br>")+`</span>
<br>
<b>QuotaDrives:</b> `+drives.length+` / `+config.deviceDriveQuota+`<br>
<b>Quota Storage:</b> `+(0)+` MB / `+config.deviceStorageQuotaMb+` MB<br>
<b>Stored Drives:</b> `+drives.length+`<br>
<b>Quota Storage:</b> `+device.storage_used+` MB / `+config.deviceStorageQuotaMb+` MB<br>
<br>
`;
@ -953,6 +958,7 @@ app.get('/useradmin/drive/:dongleId/:driveIdentifier', (req, res) => {
var directorySegments={};
for (var i in directoryTree.children) {
// skip any non-directory entries (for example m3u8 file in the drive directory)
if (directoryTree.children[i].type!='directory') continue;
@ -982,9 +988,25 @@ app.get('/useradmin/drive/:dongleId/:driveIdentifier', (req, res) => {
isStalled=drive_segment.is_stalled;
}
response+='<tr><td>'+segment+'</td><td>'+qcamera+'</td><td>'+qlog+'</td><td>'+fcamera+'</td><td>'+rlog+'</td><td>'+dcamera+'</td><td>'+isProcessed+'</td><td>'+isStalled+'</td></tr>';
directorySegments["seg-"+segment] = '<tr><td>'+segment+'</td><td>'+qcamera+'</td><td>'+qlog+'</td><td>'+fcamera+'</td><td>'+rlog+'</td><td>'+dcamera+'</td><td>'+isProcessed+'</td><td>'+isStalled+'</td></tr>';
}
var qcamera = '--';
var fcamera = '--';
var dcamera = '--';
var qlog = '--';
var rlog = '--';
var isProcessed='?';
var isStalled='?';
for (var i=0; i<=drive.max_segment; i++) {
if (directorySegments["seg-"+i]==undefined) {
response+='<tr><td>'+i+'</td><td>'+qcamera+'</td><td>'+qlog+'</td><td>'+fcamera+'</td><td>'+rlog+'</td><td>'+dcamera+'</td><td>'+isProcessed+'</td><td>'+isStalled+'</td></tr>';
}
else
response+=directorySegments["seg-"+i];
}
response+=`</table>
<br><br>
<hr/>
@ -1019,11 +1041,12 @@ app.post('*', (req, res) => {
lockfile.lock('retropilot_server.lock', { realpath: false, stale: 30000, update: 2000 })
lockfile.lock('retropilot_server.lock'+Math.random(), { realpath: false, stale: 30000, update: 2000 })
.then((release) => {
console.log("STARTING SERVER...");
initializeDatabase();
initializeStorage();
updateTotalStorageUsed();
var privateKey = fs.readFileSync(config.sslKey, 'utf8');
var certificate = fs.readFileSync(config.sslCrt, 'utf8');

176
worker.js
View File

@ -24,6 +24,7 @@ const htmlspecialchars = require('htmlspecialchars');
const dirTree = require("directory-tree");
const { resolve } = require('path');
const execSync = require('child_process').execSync;
const Reader = require('@commaai/log_reader');
@ -34,6 +35,10 @@ const { exception } = require('console');
const adapter = new FileSync(config.databaseFile);
const db = low(adapter);
const ALL = 1E8;
var lastCleaningTime=0;
var startTime=Date.now();
log4js.configure({
appenders: { logfile: { type: "file", filename: "worker.log" }, out: { type: "console"} },
@ -183,12 +188,25 @@ function moveUploadedFile(buffer, directory, filename) {
};
function deleteFolderRecursive(directoryPath) {
if (fs.existsSync(directoryPath)) {
fs.readdirSync(directoryPath).forEach((file, index) => {
const curPath = path.join(directoryPath, file);
if (fs.lstatSync(curPath).isDirectory()) {
deleteFolderRecursive(curPath);
} else {
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(directoryPath);
}
};
var segmentProcessQueue=[];
var segmentProcessPosition=0;
var affectedDrives={};
var affectedDevices={};
var rlog_lastTs=0;
var rlog_prevLat=-1000;
@ -293,7 +311,7 @@ function updateSegments() {
segmentProcessPosition=0;
affectedDrives={};
drive_segments = db.get('drive_segments').filter({upload_complete: false, is_stalled: false}).sortBy('created').take(10000).value();
drive_segments = db.get('drive_segments').filter({upload_complete: false, is_stalled: false}).sortBy('created').take(ALL).value();
for (var t=0; t<drive_segments.length; t++) {
var segment = drive_segments[t];
@ -345,6 +363,23 @@ function updateSegments() {
}
function updateDevices() {
// go through all affected devices (with deleted or updated drives) and update them (storage_used)
logger.info("updateDevices - affected drives: "+JSON.stringify(affectedDevices));
for (const [key, value] of Object.entries(affectedDevices)) {
var dongleId = key;
var device = db.get('devices').find({dongle_id: dongleId});
if (!device.value()) continue;
var dongleIdHash = crypto.createHmac('sha256', config.applicationSalt).update(device.value().dongle_id).digest('hex');
var devicePath=config.storagePath+device.value().dongle_id+"/"+dongleIdHash;
var deviceQuotaMb = Math.round(parseInt(execSync("du -s "+devicePath+" | awk -F'\t' '{print $1;}'").toString())/1024);
logger.info("updateDevices device "+dongleId+" has an updated storage_used of: "+deviceQuotaMb+" MB");
device.assign({storage_used: deviceQuotaMb}).write();
}
affectedDevices=[];
}
function updateDrives() {
// go through all affected drives and update them / complete and/or build m3u8
@ -366,7 +401,7 @@ function updateDrives() {
var totalDurationSeconds=0;
var playlistSegmentStrings='';
drive_segments = db.get('drive_segments').filter({drive_identifier: driveIdentifier, dongle_id: dongleId}).sortBy('created').take(10000).value();
drive_segments = db.get('drive_segments').filter({drive_identifier: driveIdentifier, dongle_id: dongleId}).sortBy('created').take(ALL).value();
for (var t=0; t<drive_segments.length; t++) {
if (!drive_segments[t].upload_complete) uploadComplete=false;
if (!drive_segments[t].is_processed) isProcessed=false;
@ -382,7 +417,6 @@ function updateDrives() {
var updates = {distance_meters: Math.round(totalDistanceMeters), duration: totalDurationSeconds, upload_complete : uploadComplete, is_processed : isProcessed};
if (uploadComplete) {
updates['filesize'] = 0;
const execSync = require('child_process').execSync;
try {
var dongleIdHash = crypto.createHmac('sha256', config.applicationSalt).update(dongleId).digest('hex');
var driveIdentifierHash = crypto.createHmac('sha256', config.applicationSalt).update(driveIdentifier).digest('hex');
@ -393,6 +427,8 @@ function updateDrives() {
logger.info("updateDrives drive "+dongleId+" "+driveIdentifier+" uploadComplete: "+JSON.stringify(updates));
drive.assign(updates).write();
affectedDevices[dongleId]=true;
if (isProcessed) {
// create the playlist file m3u8 for cabana
var playlist = `#EXTM3U\n`+
@ -407,16 +443,130 @@ function updateDrives() {
}
}
updateDevices();
setTimeout(function() {mainWorkerLoop();}, 0);
}
function deleteExpiredDrives() {
// TODO: implement
var expirationTs = Date.now()-config.deviceDriveExpirationDays*24*3600*1000;
var expiredDrives = db.get('drives').filter({is_preserved: false, is_deleted: false}).orderBy('created', 'asc').take(ALL).value();
for (var t=0; t<expiredDrives.length; t++) {
if (expiredDrives[t].created>expirationTs) {
break; // the drives are queried ordered by date, so break at the first newer one
}
var drive = db.get('drives').find({ identifier: expiredDrives[t].identifier, dongle_id: expiredDrives[t].dongle_id});
if (!drive.value()) continue;
logger.info("deleteExpiredDrives drive "+expiredDrives[t].dongle_id+" "+expiredDrives[t].identifier+" is older than "+config.deviceDriveExpirationDays+" days, set is_deleted=true");
drive.assign({is_deleted: true}).write();
}
}
function removeDeletedDrivesPhysically() {
// TODO: implement
var expiredDrives = db.get('drives').filter({is_deleted: true}).orderBy('created', 'asc').take(ALL).value();
for (var t=0; t<expiredDrives.length; t++) {
logger.info("removeDeletedDrivesPhysically drive "+expiredDrives[t].dongle_id+" "+expiredDrives[t].identifier+" is deleted, remove physical files and clean database");
var drive = db.get('drives').find({ identifier: expiredDrives[t].identifier, dongle_id: expiredDrives[t].dongle_id});
if (!drive.value()) continue;
var dongleIdHash = crypto.createHmac('sha256', config.applicationSalt).update(expiredDrives[t].dongle_id).digest('hex');
var driveIdentifierHash = crypto.createHmac('sha256', config.applicationSalt).update(expiredDrives[t].identifier).digest('hex');
const drivePath = config.storagePath+expiredDrives[t].dongle_id+"/"+dongleIdHash+"/"+driveIdentifierHash+"";
logger.info("removeDeletedDrivesPhysically drive "+expiredDrives[t].dongle_id+" "+expiredDrives[t].identifier+" storage path is "+drivePath);
try {
deleteFolderRecursive(drivePath, { recursive: true });
db.get('drives').remove({ identifier: expiredDrives[t].identifier, dongle_id: expiredDrives[t].dongle_id}).write();
affectedDevices[expiredDrives[t].dongle_id]=true;
} catch (exception) {
logger.error(exception);
}
}
}
function deleteOverQuotaDrives() {
var devices = db.get('devices').filter({}).orderBy('storage_used', 'desc').take(ALL).value();
for (var t=0; t<devices.length; t++) {
if (devices[t].storage_used>config.deviceStorageQuotaMb) {
var foundDriveToDelete=false;
var allDrives = db.get('drives').filter({dongle_id: devices[t].dongle_id, is_preserved: false, is_deleted: false}).orderBy('created', 'asc').take(1).value();
for (var i=0; i<allDrives.length; i++) {
logger.info("deleteExpiredDrives drive "+allDrives[i].dongle_id+" "+allDrives[i].identifier+" (normal) is deleted for over-quota");
var drive = db.get('drives').find({ identifier: allDrives[i].identifier, dongle_id: allDrives[i].dongle_id});
if (!drive.value()) continue;
drive.assign({is_deleted: true}).write();
foundDriveToDelete=true;
break;
}
if (!foundDriveToDelete) {
var allDrives = db.get('drives').filter({dongle_id: devices[t].dongle_id, is_preserved: true, is_deleted: false}).orderBy('created', 'asc').take(1).value();
for (var i=0; i<allDrives.length; i++) {
logger.info("deleteOverQuotaDrives drive "+allDrives[i].dongle_id+" "+allDrives[i].identifier+" (preserved!) is deleted for over-quota");
var drive = db.get('drives').find({ identifier: allDrives[i].identifier, dongle_id: allDrives[i].dongle_id});
if (!drive.value()) continue;
drive.assign({is_deleted: true}).write();
foundDriveToDelete=true;
break;
}
}
}
}
}
function deleteBootAndCrashLogs() {
var devices = db.get('devices').filter({}).take(ALL).value();
for (var t=0; t<devices.length; t++) {
var device = devices[t];
var dongleIdHash = crypto.createHmac('sha256', config.applicationSalt).update(device.dongle_id).digest('hex');
const bootlogDirectoryTree = dirTree(config.storagePath+device.dongle_id+"/"+dongleIdHash+"/boot/", {attributes:['size']});
var bootlogFiles = [];
if (bootlogDirectoryTree!=undefined) {
for (var i=0; i<bootlogDirectoryTree.children.length; i++) {
var timeSplit = bootlogDirectoryTree.children[i].name.replace('boot-', '').replace('crash-', '').replace('\.bz2', '').split('--');
var timeString = timeSplit[0]+' '+timeSplit[1].replace('-',':');
bootlogFiles.push({'name': bootlogDirectoryTree.children[i].name, 'size': bootlogDirectoryTree.children[i].size, 'date': Date.parse(timeString), 'path' : bootlogDirectoryTree.children[i].path});
}
bootlogFiles.sort((a,b) => (a.date < b.date) ? 1 : -1);
for (var c=5; c<bootlogFiles.length; c++) {
logger.info("deleteBootAndCrashLogs deleting boot log "+bootlogFiles[c]['path']+"");
try {
fs.unlinkSync(bootlogFiles[c]['path']);
affectedDevices[device.dongle_id]=true;
} catch (exception) {
logger.error(exception);
}
}
}
const crashlogDirectoryTree = dirTree(config.storagePath+device.dongle_id+"/"+dongleIdHash+"/crash/", {attributes:['size']});
var crashlogFiles = [];
if (crashlogDirectoryTree!=undefined) {
for (var i=0; i<crashlogDirectoryTree.children.length; i++) {
var timeSplit = crashlogDirectoryTree.children[i].name.replace('boot-', '').replace('crash-', '').replace('\.bz2', '').split('--');
var timeString = timeSplit[0]+' '+timeSplit[1].replace('-',':');
crashlogFiles.push({'name': crashlogDirectoryTree.children[i].name, 'size': crashlogDirectoryTree.children[i].size, 'date': Date.parse(timeString)});
}
crashlogFiles.sort((a,b) => (a.date < b.date) ? 1 : -1);
for (var c=5; c<crashlogFiles.length; c++) {
logger.info("deleteBootAndCrashLogs deleting crash log "+crashlogFiles[c]['path']+"");
try {
fs.unlinkSync(crashlogFiles[c]['path']);
affectedDevices[device.dongle_id]=true;
} catch (exception) {
logger.error(exception);
}
}
}
}
}
@ -426,17 +576,21 @@ function mainWorkerLoop() {
process.exit();
}
deleteExpiredDrives();
removeDeletedDrivesPhysically();
if (Date.now()-lastCleaningTime>20*3600*1000) {
deleteBootAndCrashLogs();
deleteExpiredDrives();
deleteOverQuotaDrives();
removeDeletedDrivesPhysically();
lastCleaningTime=Date.now();
}
setTimeout(function() {updateSegments();}, 5000);
}
startTime=Date.now();
lockfile.lock('retropilot_worker.lock', { realpath: false, stale: 30000, update: 2000 })
.then((release) => {
logger.info("STARTING WORKER...");