mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-11-13 12:23:34 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edb1f7efc9 | ||
|
|
92517f473b | ||
|
|
9715c7a8f7 | ||
|
|
95cb5bad27 | ||
|
|
5cb461f9b5 | ||
|
|
16aa80ed5c | ||
|
|
743d915220 | ||
|
|
34715558df | ||
|
|
2a0507ffc6 | ||
|
|
cff295fdc9 | ||
|
|
6b60beff6e | ||
|
|
db433ce076 | ||
|
|
5edd20dfa3 | ||
|
|
3563d49424 | ||
|
|
06935b78b5 | ||
|
|
4aee64da07 | ||
|
|
02cf2da4fa | ||
|
|
366320bf94 | ||
|
|
a2cba8dd05 | ||
|
|
86bc18487b | ||
|
|
9e18cb41e9 | ||
|
|
273693e8ad | ||
|
|
3f9afe57bc | ||
|
|
ac63b0c7fc | ||
|
|
94400acdcd |
No files matched your search
@@ -1,40 +1,31 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
|
||||
{
|
||||
"name": "Audiobookshelf",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
// Update 'VARIANT' to pick a Node version: 18, 16, 14.
|
||||
// Append -bullseye or -buster to pin to an OS version.
|
||||
// Use -bullseye variants on local arm64/Apple Silicon.
|
||||
"args": {
|
||||
"VARIANT": "20"
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
"source=abs-server-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
|
||||
"source=abs-client-node_modules,target=${containerWorkspaceFolder}/client/node_modules,type=volume"
|
||||
],
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [
|
||||
3000,
|
||||
3333
|
||||
],
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "sh .devcontainer/post-create.sh",
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"octref.vetur"
|
||||
]
|
||||
}
|
||||
}
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
"name": "Audiobookshelf",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
// Update 'VARIANT' to pick a Node version: 18, 16, 14.
|
||||
// Append -bullseye or -buster to pin to an OS version.
|
||||
// Use -bullseye variants on local arm64/Apple Silicon.
|
||||
"args": {
|
||||
"VARIANT": "24"
|
||||
}
|
||||
},
|
||||
"mounts": ["source=abs-server-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume", "source=abs-client-node_modules,target=${containerWorkspaceFolder}/client/node_modules,type=volume"],
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [3000, 3333],
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "sh .devcontainer/post-create.sh",
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": ["dbaeumer.vscode-eslint", "octref.vetur"]
|
||||
}
|
||||
}
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
@@ -11,6 +11,8 @@ ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=always
|
||||
User=audiobookshelf
|
||||
Group=audiobookshelf
|
||||
Environment=HOME=/var/cache/audiobookshelf
|
||||
CacheDirectory=audiobookshelf
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+6
-1
@@ -35,7 +35,6 @@ echo "--------------------"
|
||||
mkdir -p dist
|
||||
rm -rf dist/debian
|
||||
cp -R build/debian dist/debian
|
||||
chmod -R 775 dist/debian
|
||||
|
||||
controlfile="Package: audiobookshelf
|
||||
Version: $VERSION
|
||||
@@ -48,6 +47,12 @@ Description: $DESCRIPTION"
|
||||
|
||||
echo "$controlfile" > dist/debian/DEBIAN/control;
|
||||
|
||||
# Package contents: directories 0755, files 0644 (including the systemd unit).
|
||||
# Maintainer scripts must stay executable so dpkg can run them.
|
||||
find dist/debian -type d -exec chmod 755 {} +
|
||||
find dist/debian -type f -exec chmod 644 {} +
|
||||
chmod 755 dist/debian/DEBIAN/preinst dist/debian/DEBIAN/postinst dist/debian/DEBIAN/prerm
|
||||
|
||||
# Package debian
|
||||
pkg --config package.json -t node24-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf dist-server/index.js
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.37.0",
|
||||
"version": "2.37.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.37.0",
|
||||
"version": "2.37.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.37.0",
|
||||
"version": "2.37.2",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
|
||||
+12
-9
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"ButtonAdd": "Lisa",
|
||||
"ButtonAddApiKey": "Lisa API võti",
|
||||
"ButtonAddChapters": "Lisa peatükid",
|
||||
"ButtonAddDevice": "Lisa seade",
|
||||
"ButtonAddLibrary": "Lisa raamatukogu",
|
||||
@@ -53,7 +54,7 @@
|
||||
"ButtonOpenFeed": "Ava voog",
|
||||
"ButtonOpenManager": "Ava haldur",
|
||||
"ButtonPause": "Paus",
|
||||
"ButtonPlay": "Play",
|
||||
"ButtonPlay": "Mängi",
|
||||
"ButtonPlayAll": "Mängi kõik",
|
||||
"ButtonPlaying": "Mängib",
|
||||
"ButtonPlaylists": "Playlist",
|
||||
@@ -159,7 +160,7 @@
|
||||
"HeaderPlaylistItems": "Playlisti esemed",
|
||||
"HeaderPodcastsToAdd": "Lisatavad podcastid",
|
||||
"HeaderPreviewCover": "Eelvaate kaas",
|
||||
"HeaderRSSFeedGeneral": "RSS-i üksikasjad",
|
||||
"HeaderRSSFeedGeneral": "RSS detailid",
|
||||
"HeaderRSSFeedIsOpen": "RSS-voog on avatud",
|
||||
"HeaderRSSFeeds": "RSS-vooged",
|
||||
"HeaderRemoveEpisode": "Eemalda episood",
|
||||
@@ -177,7 +178,7 @@
|
||||
"HeaderSleepTimer": "Unetaimer",
|
||||
"HeaderStatsLargestItems": "Suurimad esemed",
|
||||
"HeaderStatsLongestItems": "Kõige pikemad esemed (tunnid)",
|
||||
"HeaderStatsMinutesListeningChart": "Kuulamise minutid (viimased 7 päeva)",
|
||||
"HeaderStatsMinutesListeningChart": "Kuulatud minutid (viimased 7 päeva)",
|
||||
"HeaderStatsRecentSessions": "Hiljutised sessioonid",
|
||||
"HeaderStatsTop10Authors": "Top 10 autorit",
|
||||
"HeaderStatsTop5Genres": "Top 5 žanrit",
|
||||
@@ -245,7 +246,7 @@
|
||||
"LabelConfirmPassword": "Kinnita parool",
|
||||
"LabelContinueListening": "Jätka kuulamist",
|
||||
"LabelContinueReading": "Jätka lugemist",
|
||||
"LabelContinueSeries": "Jätka seeriat",
|
||||
"LabelContinueSeries": "Jäta sarja",
|
||||
"LabelCover": "Ümbris",
|
||||
"LabelCoverImageURL": "Ümbrise pildi URL",
|
||||
"LabelCreatedAt": "Loodud",
|
||||
@@ -325,9 +326,10 @@
|
||||
"LabelIntervalEveryDay": "Iga päev",
|
||||
"LabelIntervalEveryHour": "Iga tunni tagant",
|
||||
"LabelInvert": "Pööra ümber",
|
||||
"LabelIpAddress": "IP-aadress",
|
||||
"LabelItem": "Kirje",
|
||||
"LabelLanguage": "Keel",
|
||||
"LabelLanguageDefaultServer": "Vaikeserveri keel",
|
||||
"LabelLanguageDefaultServer": "Serveri vaikekeel",
|
||||
"LabelLastBookAdded": "Viimati lisatud raamat",
|
||||
"LabelLastBookUpdated": "Viimati uuendatud raamat",
|
||||
"LabelLastSeen": "Viimati nähtud",
|
||||
@@ -368,8 +370,8 @@
|
||||
"LabelNarrators": "Jutustajad",
|
||||
"LabelNew": "Uus",
|
||||
"LabelNewPassword": "Uus parool",
|
||||
"LabelNewestAuthors": "Uuemad autorid",
|
||||
"LabelNewestEpisodes": "Uuemad episoodid",
|
||||
"LabelNewestAuthors": "Uusimad autorid",
|
||||
"LabelNewestEpisodes": "Uusimad episoodid",
|
||||
"LabelNextBackupDate": "Järgmine varukoopia kuupäev",
|
||||
"LabelNextScheduledRun": "Järgmine ajakava järgmine",
|
||||
"LabelNoEpisodesSelected": "Episoodid pole valitud",
|
||||
@@ -458,8 +460,8 @@
|
||||
"LabelSettingsFindCoversHelp": "Kui teie heliraamatul pole sisseehitatud ümbrist ega ümbrise pilti kaustas, proovib skanner leida ümbrist.<br>Märkus: see pikendab skaneerimisaega",
|
||||
"LabelSettingsHideSingleBookSeries": "Peida üksikute raamatute seeriad",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Ühe raamatuga seeriaid peidetakse seeria lehelt ja avalehe riiulitelt.",
|
||||
"LabelSettingsHomePageBookshelfView": "Avaleht kasutage raamatukoguvaadet",
|
||||
"LabelSettingsLibraryBookshelfView": "Raamatukogu kasutamiseks kasutage raamatukoguvaadet",
|
||||
"LabelSettingsHomePageBookshelfView": "Avalehel kasuta raamaturiiulivaadet",
|
||||
"LabelSettingsLibraryBookshelfView": "Raamatukogus kasuta raamaturiiulivaadet",
|
||||
"LabelSettingsParseSubtitles": "Lugege subtiitreid",
|
||||
"LabelSettingsParseSubtitlesHelp": "Eraldage subtiitrid heliraamatu kaustade nimedest.<br>Subtiitrid peavad olema eraldatud kasutades \" - \".<br>Näiteks: \"Raamatu pealkiri - Siin on alapealkiri\" alapealkiri on \"Siin on alapealkiri\"",
|
||||
"LabelSettingsPreferMatchedMetadata": "Eelista sobitatud metaandmeid",
|
||||
@@ -565,6 +567,7 @@
|
||||
"MessageBookshelfNoCollectionsHelp": "Kollektsioonid on avalikud. Kõik kasutajad kellel on olemas ligipääs raamatukogule saavad neid näha.",
|
||||
"MessageBookshelfNoRSSFeeds": "Ühtegi RSS-i voogu pole avatud",
|
||||
"MessageBookshelfNoResultsForFilter": "Filtrile \"{0}: {1}\" pole tulemusi",
|
||||
"MessageBookshelfNoResultsForQuery": "Päringul ei ole tulemusi",
|
||||
"MessageBookshelfNoSeries": "Teil pole ühtegi seeriat",
|
||||
"MessageChapterEndIsAfter": "Peatüki lõpp on pärast teie heliraamatu lõppu",
|
||||
"MessageChapterErrorFirstNotZero": "Esimene peatükk peab algama 0-st",
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
"LabelComplete": "Baigta",
|
||||
"LabelConfirmPassword": "Patvirtinkite slaptažodį",
|
||||
"LabelContinueListening": "Klausytis toliau",
|
||||
"LabelContinueReading": "Tęsti skaitymą",
|
||||
"LabelContinueReading": "Skaityti toliau",
|
||||
"LabelContinueSeries": "Tęsti seriją",
|
||||
"LabelCover": "Viršelis",
|
||||
"LabelCoverImageURL": "Viršelio paveikslėlio URL",
|
||||
@@ -395,7 +395,7 @@
|
||||
"LabelRead": "Skaityta",
|
||||
"LabelReadAgain": "Skaityti dar kartą",
|
||||
"LabelReadEbookWithoutProgress": "Skaityti e-knygą be pažangos saugojimo",
|
||||
"LabelRecentSeries": "Naujausiais įkeltos serijos",
|
||||
"LabelRecentSeries": "Naujausios serijos",
|
||||
"LabelRecentlyAdded": "Naujausiai įkelta",
|
||||
"LabelRecommended": "Rekomenduojama",
|
||||
"LabelRegion": "Regionas",
|
||||
|
||||
@@ -1113,7 +1113,7 @@
|
||||
"ToastProgressIsNotBeingSynced": "O progresso não está sendo sincronizado, reinicie a reprodução",
|
||||
"ToastProviderCreatedFailed": "Falha ao adicionar o provedor",
|
||||
"ToastProviderCreatedSuccess": "Novo provedor adicionado",
|
||||
"ToastProviderNameAndUrlRequired": "Nome e URL obrigatórios",
|
||||
"ToastProviderNameAndUrlRequired": "Nome e Url obrigatórios",
|
||||
"ToastProviderRemoveSuccess": "Fornecedor removido",
|
||||
"ToastRSSFeedCloseFailed": "Falha ao fechar feed RSS",
|
||||
"ToastRSSFeedCloseSuccess": "Feed RSS fechado",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.37.0",
|
||||
"version": "2.37.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.37.0",
|
||||
"version": "2.37.2",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.37.0",
|
||||
"version": "2.37.2",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "dist-server/index.js",
|
||||
|
||||
+13
-5
@@ -216,14 +216,15 @@ class Auth {
|
||||
* @returns {Object|null} - Returns error object if validation fails, null if successful
|
||||
*/
|
||||
paramsToCookies(req, res, authMethod = 'local') {
|
||||
const TWO_MINUTES = 120000 // 2 minutes in milliseconds
|
||||
// Cookie max age in milliseconds
|
||||
const TEN_MINUTES = 10 * 60 * 1000
|
||||
const callback = req.query.redirect_uri || req.query.callback
|
||||
|
||||
// Additional handling for non-API based authMethod
|
||||
if (!this.isAuthMethodAPIBased(authMethod)) {
|
||||
// Store 'auth_state' if present in the request
|
||||
if (req.query.state) {
|
||||
res.cookie('auth_state', req.query.state, { maxAge: TWO_MINUTES, httpOnly: true })
|
||||
res.cookie('auth_state', req.query.state, { maxAge: TEN_MINUTES, httpOnly: true })
|
||||
}
|
||||
|
||||
// Validate and store the callback URL
|
||||
@@ -239,7 +240,7 @@ class Auth {
|
||||
return { error: 'Invalid callback URL - must be same-origin' }
|
||||
}
|
||||
|
||||
res.cookie('auth_cb', callback, { maxAge: TWO_MINUTES, httpOnly: true })
|
||||
res.cookie('auth_cb', callback, { maxAge: TEN_MINUTES, httpOnly: true })
|
||||
}
|
||||
|
||||
// Store the authentication method for long
|
||||
@@ -273,7 +274,8 @@ class Auth {
|
||||
// TODO: Temporarily continue sending the old token as setToken
|
||||
res.redirect(302, `${req.cookies.auth_cb}?setToken=${userResponse.user.token}&accessToken=${userResponse.user.accessToken}${stateQuery}`)
|
||||
} else {
|
||||
res.status(400).send('No callback or already expired')
|
||||
Logger.error('[Auth] No callback or already expired')
|
||||
res.redirect(`/login?error=${encodeURIComponent('No callback or already expired')}&autoLaunch=0`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,7 +387,13 @@ class Auth {
|
||||
const sessionKey = this.oidcAuthStrategy.getStrategy()._key
|
||||
|
||||
if (!req.session[sessionKey]) {
|
||||
return res.status(400).send('No session')
|
||||
// Mobile clients send code_verifier and expect a status, not the web login page
|
||||
const isMobile = req.cookies.auth_method === 'openid-mobile' || !!req.query.code_verifier
|
||||
Logger.error('[Auth] /auth/openid/callback route: No session')
|
||||
if (isMobile) {
|
||||
return res.status(400).send('No session')
|
||||
}
|
||||
return res.redirect(`/login?error=${encodeURIComponent('No sign-in session')}&autoLaunch=0`)
|
||||
}
|
||||
|
||||
// If the client sends us a code_verifier, we will tell passport to use this to send this in the token request
|
||||
|
||||
+15
-3
@@ -420,9 +420,21 @@ class Database {
|
||||
*/
|
||||
async createRootUser(username, pash, auth) {
|
||||
if (!this.sequelize) return false
|
||||
await this.userModel.createRootUser(username, pash, auth)
|
||||
this.hasRootUser = true
|
||||
return true
|
||||
const transaction = await this.sequelize.transaction()
|
||||
try {
|
||||
const rootCount = await this.userModel.count({ where: { type: 'root' }, transaction })
|
||||
if (rootCount > 0) {
|
||||
await transaction.rollback()
|
||||
return false
|
||||
}
|
||||
await this.userModel.createRootUser(username, pash, auth, transaction)
|
||||
await transaction.commit()
|
||||
this.hasRootUser = true
|
||||
return true
|
||||
} catch (error) {
|
||||
await transaction.rollback()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
updateServerSettings() {
|
||||
|
||||
+22
-2
@@ -46,6 +46,22 @@ const passport = require('passport')
|
||||
const expressSession = require('express-session')
|
||||
const MemoryStore = require('./libs/memorystore')
|
||||
|
||||
/**
|
||||
* Normalize to '' (root) or '/segment' with no trailing slash.
|
||||
* Matches audiobookshelf-client-react `src/lib/basePath.ts`.
|
||||
* @param {string|null|undefined} basePath
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeBasePath(basePath) {
|
||||
if (!basePath) return ''
|
||||
|
||||
const trimmed = basePath.trim()
|
||||
if (!trimmed || trimmed === '/') return ''
|
||||
|
||||
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`
|
||||
return withLeadingSlash.endsWith('/') ? withLeadingSlash.slice(0, -1) : withLeadingSlash
|
||||
}
|
||||
|
||||
/**
|
||||
* `next()` for a custom server is a NextCustomServer wrapper. After prepare(),
|
||||
* `.server` is another NextServer wrapper; the Node server with `nextConfig` is
|
||||
@@ -66,7 +82,7 @@ class Server {
|
||||
global.isWin = process.platform === 'win32'
|
||||
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
|
||||
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
|
||||
global.RouterBasePath = ROUTER_BASE_PATH
|
||||
global.RouterBasePath = normalizeBasePath(ROUTER_BASE_PATH)
|
||||
global.XAccel = process.env.USE_X_ACCEL
|
||||
global.AllowCors = process.env.ALLOW_CORS === '1'
|
||||
|
||||
@@ -472,7 +488,11 @@ class Server {
|
||||
const rootUsername = newRoot.username || 'root'
|
||||
const rootPash = newRoot.password ? await this.auth.localAuthStrategy.hashPassword(newRoot.password) : ''
|
||||
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
|
||||
await Database.createRootUser(rootUsername, rootPash, this.auth)
|
||||
const created = await Database.createRootUser(rootUsername, rootPash, this.auth)
|
||||
if (!created) {
|
||||
Logger.error(`[Server] attempt to init server when server already has a root user`)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { LRUCache } = require('lru-cache')
|
||||
const { Request, Response } = require('express')
|
||||
const passport = require('passport')
|
||||
const OpenIDClient = require('openid-client')
|
||||
@@ -15,7 +16,10 @@ class OidcAuthStrategy {
|
||||
this.strategy = null
|
||||
this.client = null
|
||||
// Map of openId sessions indexed by oauth2 state-variable
|
||||
this.openIdAuthSession = new Map()
|
||||
this.openIdAuthSession = new LRUCache({
|
||||
ttl: 10 * 60 * 1000,
|
||||
ttlAutopurge: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,7 +44,7 @@ class MiscController {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const files = Object.values(req.files)
|
||||
const files = Object.values(req.files).flat()
|
||||
let { title, author, series, folder: folderId, library: libraryId } = req.body
|
||||
// Validate request body
|
||||
if (!libraryId || !folderId || typeof libraryId !== 'string' || typeof folderId !== 'string' || !title || typeof title !== 'string') {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const fs = require('graceful-fs')
|
||||
const path = require('path')
|
||||
const { pipeline } = require('stream')
|
||||
const mkdirs = require('../mkdirs').mkdirs
|
||||
const pathExists = require('../path-exists').pathExists
|
||||
const utimesMillis = require('../util/utimes').utimesMillis
|
||||
@@ -98,10 +99,20 @@ function mayCopyFile (srcStat, src, dest, opts, cb) {
|
||||
}
|
||||
|
||||
function copyFile (srcStat, src, dest, opts, cb) {
|
||||
fs.copyFile(src, dest, err => {
|
||||
const finish = err => {
|
||||
if (err) return cb(err)
|
||||
if (opts.preserveTimestamps) return handleTimestampsAndMode(srcStat.mode, src, dest, cb)
|
||||
return setDestMode(dest, srcStat.mode, cb)
|
||||
}
|
||||
|
||||
fs.copyFile(src, dest, err => {
|
||||
if (err?.code !== 'EPERM' || !srcStat.isFile()) return finish(err)
|
||||
|
||||
// Stream the fallback so database copies do not require a full-file buffer.
|
||||
pipeline(fs.createReadStream(src, { highWaterMark: 1024 * 1024 }), fs.createWriteStream(dest, { highWaterMark: 1024 * 1024 }), streamError => {
|
||||
if (streamError) return fs.unlink(dest, () => cb(streamError))
|
||||
finish()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ function doRename (src, dest, overwrite, isChangingCase, cb) {
|
||||
function rename (src, dest, overwrite, cb) {
|
||||
fs.rename(src, dest, err => {
|
||||
if (!err) return cb()
|
||||
if (err.code !== 'EXDEV') return cb(err)
|
||||
if (err.code !== 'EXDEV' && err.code !== 'EPERM') return cb(err)
|
||||
return moveAcrossDevice(src, dest, overwrite, cb)
|
||||
})
|
||||
}
|
||||
|
||||
+15
-3
@@ -87,6 +87,18 @@ class Feed extends Model {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} slug
|
||||
* @param {string|null|undefined} coverPath
|
||||
* @param {Date|null|undefined} entityUpdatedAt
|
||||
* @returns {string}
|
||||
*/
|
||||
static getFeedImageURL(slug, coverPath, entityUpdatedAt) {
|
||||
if (!coverPath) return '/Logo.png'
|
||||
const cacheBuster = entityUpdatedAt != null ? `?ts=${entityUpdatedAt.valueOf()}` : ''
|
||||
return `/feed/${slug}/cover${Path.extname(coverPath)}${cacheBuster}`
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} userId
|
||||
@@ -119,7 +131,7 @@ class Feed extends Model {
|
||||
entityUpdatedAt,
|
||||
serverAddress,
|
||||
feedURL: `/feed/${slug}`,
|
||||
imageURL: media.coverPath ? `/feed/${slug}/cover${Path.extname(media.coverPath)}` : `/Logo.png`,
|
||||
imageURL: Feed.getFeedImageURL(slug, media.coverPath, entityUpdatedAt),
|
||||
siteURL: `/item/${libraryItem.id}`,
|
||||
title: media.title,
|
||||
description: media.description,
|
||||
@@ -212,7 +224,7 @@ class Feed extends Model {
|
||||
entityUpdatedAt,
|
||||
serverAddress,
|
||||
feedURL: `/feed/${slug}`,
|
||||
imageURL: firstBookWithCover?.coverPath ? `/feed/${slug}/cover${Path.extname(firstBookWithCover.coverPath)}` : `/Logo.png`,
|
||||
imageURL: Feed.getFeedImageURL(slug, firstBookWithCover?.coverPath, entityUpdatedAt),
|
||||
siteURL: `/collection/${collectionExpanded.id}`,
|
||||
title: collectionExpanded.name,
|
||||
description: collectionExpanded.description || '',
|
||||
@@ -301,7 +313,7 @@ class Feed extends Model {
|
||||
entityUpdatedAt,
|
||||
serverAddress,
|
||||
feedURL: `/feed/${slug}`,
|
||||
imageURL: firstBookWithCover?.coverPath ? `/feed/${slug}/cover${Path.extname(firstBookWithCover.coverPath)}` : `/Logo.png`,
|
||||
imageURL: Feed.getFeedImageURL(slug, firstBookWithCover?.coverPath, entityUpdatedAt),
|
||||
siteURL: `/library/${booksWithTracks[0].libraryItem.libraryId}/series/${seriesExpanded.id}`,
|
||||
title: seriesExpanded.name,
|
||||
description: seriesExpanded.description || '',
|
||||
|
||||
@@ -189,7 +189,7 @@ class User extends Model {
|
||||
* @param {import('../Auth')} auth
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
static async createRootUser(username, pash, auth) {
|
||||
static async createRootUser(username, pash, auth, transaction = null) {
|
||||
const userId = uuidv4()
|
||||
|
||||
const token = auth.generateAccessToken({ id: userId, username })
|
||||
@@ -207,7 +207,7 @@ class User extends Model {
|
||||
seriesHideFromContinueListening: []
|
||||
}
|
||||
}
|
||||
return this.create(newUser)
|
||||
return this.create(newUser, { transaction })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -160,7 +160,7 @@ class Audible {
|
||||
timeout
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res?.data?.products) return null
|
||||
if (!res?.data?.products) return []
|
||||
return Promise.all(res.data.products.map((result) => this.asinSearch(result.asin, region, timeout)))
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -75,3 +75,26 @@ describe('OidcAuthStrategy - isValidWebCallbackUrl', () => {
|
||||
expect(strategy.isValidWebCallbackUrl('http://books.example.com/login', req)).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('OidcAuthStrategy - openIdAuthSession', () => {
|
||||
/** @type {OidcAuthStrategy} */
|
||||
let strategy
|
||||
|
||||
beforeEach(() => {
|
||||
global.RouterBasePath = ''
|
||||
strategy = new OidcAuthStrategy()
|
||||
})
|
||||
|
||||
it('configures a 10 minute TTL with autopurge so abandoned mobile logins are cleaned up', () => {
|
||||
expect(strategy.openIdAuthSession.ttl).to.equal(10 * 60 * 1000)
|
||||
expect(strategy.openIdAuthSession.ttlAutopurge).to.equal(true)
|
||||
})
|
||||
|
||||
it('still supports the same set/has/get/delete usage as a plain Map', () => {
|
||||
strategy.openIdAuthSession.set('some-state', { mobile_redirect_uri: 'myapp://callback' })
|
||||
expect(strategy.openIdAuthSession.has('some-state')).to.equal(true)
|
||||
expect(strategy.openIdAuthSession.get('some-state')).to.deep.equal({ mobile_redirect_uri: 'myapp://callback' })
|
||||
strategy.openIdAuthSession.delete('some-state')
|
||||
expect(strategy.openIdAuthSession.has('some-state')).to.equal(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,96 @@
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const fs = require('graceful-fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const fsExtra = require('../../server/libs/fsExtra')
|
||||
|
||||
describe('fsExtra copy fallback', () => {
|
||||
let dir
|
||||
let source
|
||||
let dest
|
||||
let contents
|
||||
|
||||
beforeEach(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'abs-copy-'))
|
||||
source = path.join(dir, 'source')
|
||||
dest = path.join(dir, 'dest')
|
||||
contents = Buffer.alloc(2 * 1024 * 1024 + 17, 0x61)
|
||||
fs.writeFileSync(source, contents)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
fs.rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('copies through streams when asynchronous copyFile is denied', async () => {
|
||||
sinon.stub(fs, 'copyFile').callsFake((src, target, callback) => {
|
||||
callback(Object.assign(new Error('copyfile denied'), { code: 'EPERM' }))
|
||||
})
|
||||
|
||||
await fsExtra.copy(source, dest)
|
||||
|
||||
expect(fs.readFileSync(dest).equals(contents)).to.be.true
|
||||
})
|
||||
|
||||
it('moves across devices when asynchronous copyFile is denied', async () => {
|
||||
sinon.stub(fs, 'rename').callsFake((src, target, callback) => {
|
||||
callback(Object.assign(new Error('cross-device link'), { code: 'EXDEV' }))
|
||||
})
|
||||
sinon.stub(fs, 'copyFile').callsFake((src, target, callback) => {
|
||||
callback(Object.assign(new Error('copyfile denied'), { code: 'EPERM' }))
|
||||
})
|
||||
|
||||
await fsExtra.move(source, dest)
|
||||
|
||||
expect(fs.existsSync(source)).to.be.false
|
||||
expect(fs.readFileSync(dest).equals(contents)).to.be.true
|
||||
})
|
||||
|
||||
it('moves over an existing file when asynchronous rename and copyFile are denied', async () => {
|
||||
fs.writeFileSync(dest, 'old database')
|
||||
sinon.stub(fs, 'rename').callsFake((src, target, callback) => {
|
||||
callback(Object.assign(new Error('rename denied'), { code: 'EPERM' }))
|
||||
})
|
||||
sinon.stub(fs, 'copyFile').callsFake((src, target, callback) => {
|
||||
callback(Object.assign(new Error('copyfile denied'), { code: 'EPERM' }))
|
||||
})
|
||||
|
||||
await fsExtra.move(source, dest, { overwrite: true })
|
||||
|
||||
expect(fs.existsSync(source)).to.be.false
|
||||
expect(fs.readFileSync(dest).equals(contents)).to.be.true
|
||||
})
|
||||
|
||||
it('keeps the source when a move cannot copy it', async () => {
|
||||
sinon.stub(fs, 'rename').callsFake((src, target, callback) => {
|
||||
callback(Object.assign(new Error('rename denied'), { code: 'EPERM' }))
|
||||
})
|
||||
sinon.stub(fs, 'copyFile').callsFake((src, target, callback) => {
|
||||
callback(Object.assign(new Error('read failed'), { code: 'EIO' }))
|
||||
})
|
||||
|
||||
try {
|
||||
await fsExtra.move(source, dest)
|
||||
expect.fail('Expected move to fail')
|
||||
} catch (error) {
|
||||
expect(error.code).to.equal('EIO')
|
||||
}
|
||||
expect(fs.readFileSync(source).equals(contents)).to.be.true
|
||||
})
|
||||
|
||||
it('propagates unrelated copy errors', async () => {
|
||||
sinon.stub(fs, 'copyFile').callsFake((src, target, callback) => {
|
||||
callback(Object.assign(new Error('file missing'), { code: 'ENOENT' }))
|
||||
})
|
||||
|
||||
try {
|
||||
await fsExtra.copy(source, dest)
|
||||
expect.fail('Expected copy to fail')
|
||||
} catch (error) {
|
||||
expect(error.code).to.equal('ENOENT')
|
||||
}
|
||||
expect(fs.existsSync(dest)).to.be.false
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user