mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-11 03:41:42 +00:00
Merge branch 'master' into mf/rssInboundManager
This commit is contained in:
commit
ad79f71dc1
105 changed files with 3306 additions and 928 deletions
|
|
@ -1,3 +1,6 @@
|
|||
const child_process = require('child_process')
|
||||
const { promisify } = require('util')
|
||||
const exec = promisify(child_process.exec)
|
||||
const path = require('path')
|
||||
const which = require('../libs/which')
|
||||
const fs = require('../libs/fsExtra')
|
||||
|
|
@ -8,67 +11,143 @@ const fileUtils = require('../utils/fileUtils')
|
|||
class BinaryManager {
|
||||
|
||||
defaultRequiredBinaries = [
|
||||
{ name: 'ffmpeg', envVariable: 'FFMPEG_PATH' },
|
||||
{ name: 'ffprobe', envVariable: 'FFPROBE_PATH' }
|
||||
{ name: 'ffmpeg', envVariable: 'FFMPEG_PATH', validVersions: ['5.1', '6'] },
|
||||
{ name: 'ffprobe', envVariable: 'FFPROBE_PATH', validVersions: ['5.1', '6'] }
|
||||
]
|
||||
|
||||
constructor(requiredBinaries = this.defaultRequiredBinaries) {
|
||||
this.requiredBinaries = requiredBinaries
|
||||
this.mainInstallPath = process.pkg ? path.dirname(process.execPath) : global.appRoot
|
||||
this.altInstallPath = global.ConfigPath
|
||||
this.initialized = false
|
||||
this.exec = exec
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.initialized) return
|
||||
const missingBinaries = await this.findRequiredBinaries()
|
||||
if (missingBinaries.length == 0) return
|
||||
await this.removeOldBinaries(missingBinaries)
|
||||
await this.install(missingBinaries)
|
||||
const missingBinariesAfterInstall = await this.findRequiredBinaries()
|
||||
if (missingBinariesAfterInstall.length != 0) {
|
||||
if (missingBinariesAfterInstall.length) {
|
||||
Logger.error(`[BinaryManager] Failed to find or install required binaries: ${missingBinariesAfterInstall.join(', ')}`)
|
||||
process.exit(1)
|
||||
}
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove old/invalid binaries in main or alt install path
|
||||
*
|
||||
* @param {string[]} binaryNames
|
||||
*/
|
||||
async removeOldBinaries(binaryNames) {
|
||||
for (const binaryName of binaryNames) {
|
||||
const executable = this.getExecutableFileName(binaryName)
|
||||
const mainInstallPath = path.join(this.mainInstallPath, executable)
|
||||
if (await fs.pathExists(mainInstallPath)) {
|
||||
Logger.debug(`[BinaryManager] Removing old binary: ${mainInstallPath}`)
|
||||
await fs.remove(mainInstallPath)
|
||||
}
|
||||
const altInstallPath = path.join(this.altInstallPath, executable)
|
||||
if (await fs.pathExists(altInstallPath)) {
|
||||
Logger.debug(`[BinaryManager] Removing old binary: ${altInstallPath}`)
|
||||
await fs.remove(altInstallPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find required binaries and return array of binary names that are missing
|
||||
*
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async findRequiredBinaries() {
|
||||
const missingBinaries = []
|
||||
for (const binary of this.requiredBinaries) {
|
||||
const binaryPath = await this.findBinary(binary.name, binary.envVariable)
|
||||
const binaryPath = await this.findBinary(binary.name, binary.envVariable, binary.validVersions)
|
||||
if (binaryPath) {
|
||||
Logger.info(`[BinaryManager] Found ${binary.name} at ${binaryPath}`)
|
||||
Logger.info(`[BinaryManager] Found valid binary ${binary.name} at ${binaryPath}`)
|
||||
if (process.env[binary.envVariable] !== binaryPath) {
|
||||
Logger.info(`[BinaryManager] Updating process.env.${binary.envVariable}`)
|
||||
process.env[binary.envVariable] = binaryPath
|
||||
}
|
||||
} else {
|
||||
Logger.info(`[BinaryManager] ${binary.name} not found`)
|
||||
Logger.info(`[BinaryManager] ${binary.name} not found or version too old`)
|
||||
missingBinaries.push(binary.name)
|
||||
}
|
||||
}
|
||||
return missingBinaries
|
||||
}
|
||||
|
||||
async findBinary(name, envVariable) {
|
||||
const executable = name + (process.platform == 'win32' ? '.exe' : '')
|
||||
/**
|
||||
* Find absolute path for binary
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {string} envVariable
|
||||
* @param {string[]} [validVersions]
|
||||
* @returns {Promise<string>} Path to binary
|
||||
*/
|
||||
async findBinary(name, envVariable, validVersions = []) {
|
||||
const executable = this.getExecutableFileName(name)
|
||||
// 1. check path specified in environment variable
|
||||
const defaultPath = process.env[envVariable]
|
||||
if (defaultPath && await fs.pathExists(defaultPath)) return defaultPath
|
||||
if (await this.isBinaryGood(defaultPath, validVersions)) return defaultPath
|
||||
// 2. find the first instance of the binary in the PATH environment variable
|
||||
const whichPath = which.sync(executable, { nothrow: true })
|
||||
if (whichPath) return whichPath
|
||||
if (await this.isBinaryGood(whichPath, validVersions)) return whichPath
|
||||
// 3. check main install path (binary root dir)
|
||||
const mainInstallPath = path.join(this.mainInstallPath, executable)
|
||||
if (await fs.pathExists(mainInstallPath)) return mainInstallPath
|
||||
if (await this.isBinaryGood(mainInstallPath, validVersions)) return mainInstallPath
|
||||
// 4. check alt install path (/config)
|
||||
const altInstallPath = path.join(this.altInstallPath, executable)
|
||||
if (await fs.pathExists(altInstallPath)) return altInstallPath
|
||||
if (await this.isBinaryGood(altInstallPath, validVersions)) return altInstallPath
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check binary path exists and optionally check version is valid
|
||||
*
|
||||
* @param {string} binaryPath
|
||||
* @param {string[]} [validVersions]
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async isBinaryGood(binaryPath, validVersions = []) {
|
||||
if (!binaryPath || !await fs.pathExists(binaryPath)) return false
|
||||
if (!validVersions.length) return true
|
||||
try {
|
||||
const { stdout } = await this.exec('"' + binaryPath + '"' + ' -version')
|
||||
const version = stdout.match(/version\s([\d\.]+)/)?.[1]
|
||||
if (!version) return false
|
||||
return validVersions.some(validVersion => version.startsWith(validVersion))
|
||||
} catch (err) {
|
||||
Logger.error(`[BinaryManager] Failed to check version of ${binaryPath}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string[]} binaries
|
||||
*/
|
||||
async install(binaries) {
|
||||
if (binaries.length == 0) return
|
||||
if (!binaries.length) return
|
||||
Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`)
|
||||
let destination = await fileUtils.isWritable(this.mainInstallPath) ? this.mainInstallPath : this.altInstallPath
|
||||
await ffbinaries.downloadBinaries(binaries, { destination })
|
||||
await ffbinaries.downloadBinaries(binaries, { destination, version: '6.1', force: true })
|
||||
Logger.info(`[BinaryManager] Binaries installed to ${destination}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Append .exe to binary name for Windows
|
||||
*
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
getExecutableFileName(name) {
|
||||
return name + (process.platform == 'win32' ? '.exe' : '')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BinaryManager
|
||||
|
|
@ -1,19 +1,34 @@
|
|||
const Path = require('path')
|
||||
const fs = require('../libs/fsExtra')
|
||||
|
||||
const Logger = require('../Logger')
|
||||
const DailyLog = require('../objects/DailyLog')
|
||||
|
||||
const Logger = require('../Logger')
|
||||
const { LogLevel } = require('../utils/constants')
|
||||
|
||||
const TAG = '[LogManager]'
|
||||
|
||||
/**
|
||||
* @typedef LogObject
|
||||
* @property {string} timestamp
|
||||
* @property {string} source
|
||||
* @property {string} message
|
||||
* @property {string} levelName
|
||||
* @property {number} level
|
||||
*/
|
||||
|
||||
class LogManager {
|
||||
constructor() {
|
||||
this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily')
|
||||
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
|
||||
|
||||
/** @type {DailyLog} */
|
||||
this.currentDailyLog = null
|
||||
|
||||
/** @type {LogObject[]} */
|
||||
this.dailyLogBuffer = []
|
||||
|
||||
/** @type {string[]} */
|
||||
this.dailyLogFiles = []
|
||||
}
|
||||
|
||||
|
|
@ -26,12 +41,12 @@ class LogManager {
|
|||
await fs.ensureDir(this.ScanLogPath)
|
||||
}
|
||||
|
||||
async ensureScanLogDir() {
|
||||
if (!(await fs.pathExists(this.ScanLogPath))) {
|
||||
await fs.mkdir(this.ScanLogPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Ensure log directories exist
|
||||
* 2. Load daily log files
|
||||
* 3. Remove old daily log files
|
||||
* 4. Create/set current daily log file
|
||||
*/
|
||||
async init() {
|
||||
await this.ensureLogDirs()
|
||||
|
||||
|
|
@ -46,11 +61,11 @@ class LogManager {
|
|||
}
|
||||
}
|
||||
|
||||
// set current daily log file or create if does not exist
|
||||
const currentDailyLogFilename = DailyLog.getCurrentDailyLogFilename()
|
||||
Logger.info(TAG, `Init current daily log filename: ${currentDailyLogFilename}`)
|
||||
|
||||
this.currentDailyLog = new DailyLog()
|
||||
this.currentDailyLog.setData({ dailyLogDirPath: this.DailyLogPath })
|
||||
this.currentDailyLog = new DailyLog(this.DailyLogPath)
|
||||
|
||||
if (this.dailyLogFiles.includes(currentDailyLogFilename)) {
|
||||
Logger.debug(TAG, `Daily log file already exists - set in Logger`)
|
||||
|
|
@ -59,7 +74,7 @@ class LogManager {
|
|||
this.dailyLogFiles.push(this.currentDailyLog.filename)
|
||||
}
|
||||
|
||||
// Log buffered Logs
|
||||
// Log buffered daily logs
|
||||
if (this.dailyLogBuffer.length) {
|
||||
this.dailyLogBuffer.forEach((logObj) => {
|
||||
this.currentDailyLog.appendLog(logObj)
|
||||
|
|
@ -68,9 +83,12 @@ class LogManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all daily log filenames in /metadata/logs/daily
|
||||
*/
|
||||
async scanLogFiles() {
|
||||
const dailyFiles = await fs.readdir(this.DailyLogPath)
|
||||
if (dailyFiles && dailyFiles.length) {
|
||||
if (dailyFiles?.length) {
|
||||
dailyFiles.forEach((logFile) => {
|
||||
if (Path.extname(logFile) === '.txt') {
|
||||
Logger.debug('Daily Log file found', logFile)
|
||||
|
|
@ -83,30 +101,38 @@ class LogManager {
|
|||
this.dailyLogFiles.sort()
|
||||
}
|
||||
|
||||
async removeOldestLog() {
|
||||
if (!this.dailyLogFiles.length) return
|
||||
const oldestLog = this.dailyLogFiles[0]
|
||||
return this.removeLogFile(oldestLog)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} filename
|
||||
*/
|
||||
async removeLogFile(filename) {
|
||||
const fullPath = Path.join(this.DailyLogPath, filename)
|
||||
const exists = await fs.pathExists(fullPath)
|
||||
if (!exists) {
|
||||
Logger.error(TAG, 'Invalid log dne ' + fullPath)
|
||||
this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf.filename !== filename)
|
||||
this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf !== filename)
|
||||
} else {
|
||||
try {
|
||||
await fs.unlink(fullPath)
|
||||
Logger.info(TAG, 'Removed daily log: ' + filename)
|
||||
this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf.filename !== filename)
|
||||
this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf !== filename)
|
||||
} catch (error) {
|
||||
Logger.error(TAG, 'Failed to unlink log file ' + fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logToFile(logObj) {
|
||||
/**
|
||||
*
|
||||
* @param {LogObject} logObj
|
||||
*/
|
||||
async logToFile(logObj) {
|
||||
// Fatal crashes get logged to a separate file
|
||||
if (logObj.level === LogLevel.FATAL) {
|
||||
await this.logCrashToFile(logObj)
|
||||
}
|
||||
|
||||
// Buffer when logging before daily logs have been initialized
|
||||
if (!this.currentDailyLog) {
|
||||
this.dailyLogBuffer.push(logObj)
|
||||
return
|
||||
|
|
@ -114,25 +140,39 @@ class LogManager {
|
|||
|
||||
// Check log rolls to next day
|
||||
if (this.currentDailyLog.id !== DailyLog.getCurrentDateString()) {
|
||||
const newDailyLog = new DailyLog()
|
||||
newDailyLog.setData({ dailyLogDirPath: this.DailyLogPath })
|
||||
this.currentDailyLog = newDailyLog
|
||||
this.currentDailyLog = new DailyLog(this.DailyLogPath)
|
||||
if (this.dailyLogFiles.length > this.loggerDailyLogsToKeep) {
|
||||
this.removeOldestLog()
|
||||
// Remove oldest log
|
||||
this.removeLogFile(this.dailyLogFiles[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Append log line to log file
|
||||
this.currentDailyLog.appendLog(logObj)
|
||||
return this.currentDailyLog.appendLog(logObj)
|
||||
}
|
||||
|
||||
socketRequestDailyLogs(socket) {
|
||||
if (!this.currentDailyLog) {
|
||||
return
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {LogObject} logObj
|
||||
*/
|
||||
async logCrashToFile(logObj) {
|
||||
const line = JSON.stringify(logObj) + '\n'
|
||||
|
||||
const lastLogs = this.currentDailyLog.logs.slice(-5000)
|
||||
socket.emit('daily_logs', lastLogs)
|
||||
const logsDir = Path.join(global.MetadataPath, 'logs')
|
||||
await fs.ensureDir(logsDir)
|
||||
const crashLogPath = Path.join(logsDir, 'crash_logs.txt')
|
||||
return fs.writeFile(crashLogPath, line, { flag: "a+" }).catch((error) => {
|
||||
console.log('[LogManager] Appended crash log', error)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Most recent 5000 daily logs
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getMostRecentCurrentDailyLogs() {
|
||||
return this.currentDailyLog?.logs.slice(-5000) || ''
|
||||
}
|
||||
}
|
||||
module.exports = LogManager
|
||||
Loading…
Add table
Add a link
Reference in a new issue