Skip to content

Commit

Permalink
Update:Create library endpoint to create using new model, adding addi…
Browse files Browse the repository at this point in the history
…tional validation
  • Loading branch information
advplyr committed Aug 22, 2024
1 parent 1c0d6e9 commit 8774e6b
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 88 deletions.
5 changes: 0 additions & 5 deletions server/Database.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,11 +384,6 @@ class Database {
return Promise.all(oldBooks.map((oldBook) => this.models.book.saveFromOld(oldBook)))
}

createLibrary(oldLibrary) {
if (!this.sequelize) return false
return this.models.library.createFromOld(oldLibrary)
}

updateLibrary(oldLibrary) {
if (!this.sequelize) return false
return this.models.library.updateFromOld(oldLibrary)
Expand Down
114 changes: 91 additions & 23 deletions server/controllers/LibraryController.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,58 +41,126 @@ class LibraryController {
* @param {Response} res
*/
async create(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to create library`)
return res.sendStatus(403)
}

// Validation
if (!req.body.name || typeof req.body.name !== 'string') {
return res.status(400).send('Invalid request. Name must be a string')
}
if (
!Array.isArray(req.body.folders) ||
req.body.folders.some((f) => {
// Old model uses fullPath and new model will use path. Support both for now
const path = f?.fullPath || f?.path
return !path || typeof path !== 'string'
})
) {
return res.status(400).send('Invalid request. Folders must be a non-empty array of objects with path string')
}
const optionalStringFields = ['mediaType', 'icon', 'provider']
for (const field of optionalStringFields) {
if (req.body[field] && typeof req.body[field] !== 'string') {
return res.status(400).send(`Invalid request. ${field} must be a string`)
}
}
if (req.body.settings && (typeof req.body.settings !== 'object' || Array.isArray(req.body.settings))) {
return res.status(400).send('Invalid request. Settings must be an object')
}

const mediaType = req.body.mediaType || 'book'
const newLibraryPayload = {
...req.body
name: req.body.name,
provider: req.body.provider || 'google',
mediaType,
icon: req.body.icon || 'database',
settings: Database.libraryModel.getDefaultLibrarySettingsForMediaType(mediaType)
}
if (!newLibraryPayload.name || !newLibraryPayload.folders || !newLibraryPayload.folders.length) {
return res.status(500).send('Invalid request')

// Validate settings
if (req.body.settings) {
for (const key in req.body.settings) {
if (newLibraryPayload.settings[key] !== undefined) {
if (key === 'metadataPrecedence') {
if (!Array.isArray(req.body.settings[key])) {
return res.status(400).send('Invalid request. Settings "metadataPrecedence" must be an array')
}
newLibraryPayload.settings[key] = [...req.body.settings[key]]
} else if (key === 'autoScanCronExpression' || key === 'podcastSearchRegion') {
if (!req.body.settings[key]) continue
if (typeof req.body.settings[key] !== 'string') {
return res.status(400).send(`Invalid request. Settings "${key}" must be a string`)
}
newLibraryPayload.settings[key] = req.body.settings[key]
} else {
if (typeof req.body.settings[key] !== typeof newLibraryPayload.settings[key]) {
return res.status(400).send(`Invalid request. Setting "${key}" must be of type ${typeof newLibraryPayload.settings[key]}`)
}
newLibraryPayload.settings[key] = req.body.settings[key]
}
}
}
}

// Validate that the custom provider exists if given any
if (newLibraryPayload.provider?.startsWith('custom-')) {
if (newLibraryPayload.provider.startsWith('custom-')) {
if (!(await Database.customMetadataProviderModel.checkExistsBySlug(newLibraryPayload.provider))) {
Logger.error(`[LibraryController] Custom metadata provider "${newLibraryPayload.provider}" does not exist`)
return res.status(400).send('Custom metadata provider does not exist')
return res.status(400).send('Invalid request. Custom metadata provider does not exist')
}
}

// Validate folder paths exist or can be created & resolve rel paths
// returns 400 if a folder fails to access
newLibraryPayload.folders = newLibraryPayload.folders.map((f) => {
f.fullPath = fileUtils.filePathToPOSIX(Path.resolve(f.fullPath))
newLibraryPayload.libraryFolders = req.body.folders.map((f) => {
const fpath = f.fullPath || f.path
f.path = fileUtils.filePathToPOSIX(Path.resolve(fpath))
return f
})
for (const folder of newLibraryPayload.folders) {
for (const folder of newLibraryPayload.libraryFolders) {
try {
const direxists = await fs.pathExists(folder.fullPath)
if (!direxists) {
// If folder does not exist try to make it and set file permissions/owner
await fs.mkdir(folder.fullPath)
}
// Create folder if it doesn't exist
await fs.ensureDir(folder.path)
} catch (error) {
Logger.error(`[LibraryController] Failed to ensure folder dir "${folder.fullPath}"`, error)
return res.status(400).send(`Invalid folder directory "${folder.fullPath}"`)
Logger.error(`[LibraryController] Failed to ensure folder dir "${folder.path}"`, error)
return res.status(400).send(`Invalid request. Invalid folder directory "${folder.path}"`)
}
}

const library = new Library()

// Set display order
let currentLargestDisplayOrder = await Database.libraryModel.getMaxDisplayOrder()
if (isNaN(currentLargestDisplayOrder)) currentLargestDisplayOrder = 0
newLibraryPayload.displayOrder = currentLargestDisplayOrder + 1
library.setData(newLibraryPayload)
await Database.createLibrary(library)

// Create library with libraryFolders
const library = await Database.libraryModel
.create(newLibraryPayload, {
include: Database.libraryFolderModel
})
.catch((error) => {
Logger.error(`[LibraryController] Failed to create library "${newLibraryPayload.name}"`, error)
})
if (!library) {
return res.status(500).send('Failed to create library')
}

library.libraryFolders = await library.getLibraryFolders()

// TODO: Migrate to new library model
const oldLibrary = Database.libraryModel.getOldLibrary(library)

// Only emit to users with access to library
const userFilter = (user) => {
return user.checkCanAccessLibrary?.(library.id)
return user.checkCanAccessLibrary?.(oldLibrary.id)
}
SocketAuthority.emitter('library_added', library.toJSON(), userFilter)
SocketAuthority.emitter('library_added', oldLibrary.toJSON(), userFilter)

// Add library watcher
this.watcher.addLibrary(library)
this.watcher.addLibrary(oldLibrary)

res.json(library)
res.json(oldLibrary)
}

async findAll(req, res) {
Expand Down
51 changes: 29 additions & 22 deletions server/models/Library.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ class Library extends Model {
this.updatedAt
}

/**
*
* @param {string} mediaType
* @returns
*/
static getDefaultLibrarySettingsForMediaType(mediaType) {
if (mediaType === 'podcast') {
return {
coverAspectRatio: 1, // Square
disableWatcher: false,
autoScanCronExpression: null,
podcastSearchRegion: 'us'
}
} else {
return {
coverAspectRatio: 1, // Square
disableWatcher: false,
autoScanCronExpression: null,
skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false,
audiobooksOnly: false,
epubsAllowScriptedContent: false,
hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false,
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
}
}
}

/**
* Get all old libraries
* @returns {Promise<oldLibrary[]>}
Expand Down Expand Up @@ -89,28 +118,6 @@ class Library extends Model {
})
}

/**
* @param {object} oldLibrary
* @returns {Library|null}
*/
static async createFromOld(oldLibrary) {
const library = this.getFromOld(oldLibrary)

library.libraryFolders = oldLibrary.folders.map((folder) => {
return {
id: folder.id,
path: folder.fullPath
}
})

return this.create(library, {
include: this.sequelize.models.libraryFolder
}).catch((error) => {
Logger.error(`[Library] Failed to create library ${library.id}`, error)
return null
})
}

/**
* Update library and library folders
* @param {object} oldLibrary
Expand Down
51 changes: 13 additions & 38 deletions server/objects/Library.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const uuidv4 = require("uuid").v4
const uuidv4 = require('uuid').v4
const Folder = require('./Folder')
const LibrarySettings = require('./settings/LibrarySettings')
const { filePathToPOSIX } = require('../utils/fileUtils')
Expand Down Expand Up @@ -29,7 +29,7 @@ class Library {
}

get folderPaths() {
return this.folders.map(f => f.fullPath)
return this.folders.map((f) => f.fullPath)
}
get isPodcast() {
return this.mediaType === 'podcast'
Expand All @@ -45,14 +45,15 @@ class Library {
this.id = library.id
this.oldLibraryId = library.oldLibraryId
this.name = library.name
this.folders = (library.folders || []).map(f => new Folder(f))
this.folders = (library.folders || []).map((f) => new Folder(f))
this.displayOrder = library.displayOrder || 1
this.icon = library.icon || 'database'
this.mediaType = library.mediaType
this.provider = library.provider || 'google'

this.settings = new LibrarySettings(library.settings)
if (library.settings === undefined) { // LibrarySettings added in v2, migrate settings
if (library.settings === undefined) {
// LibrarySettings added in v2, migrate settings
this.settings.disableWatcher = !!library.disableWatcher
}

Expand Down Expand Up @@ -85,7 +86,7 @@ class Library {
id: this.id,
oldLibraryId: this.oldLibraryId,
name: this.name,
folders: (this.folders || []).map(f => f.toJSON()),
folders: (this.folders || []).map((f) => f.toJSON()),
displayOrder: this.displayOrder,
icon: this.icon,
mediaType: this.mediaType,
Expand All @@ -98,32 +99,6 @@ class Library {
}
}

setData(data) {
this.id = data.id || uuidv4()
this.name = data.name
if (data.folder) {
this.folders = [
new Folder(data.folder)
]
} else if (data.folders) {
this.folders = data.folders.map(folder => {
var newFolder = new Folder()
newFolder.setData({
fullPath: folder.fullPath,
libraryId: this.id
})
return newFolder
})
}
this.displayOrder = data.displayOrder || 1
this.icon = data.icon || 'database'
this.mediaType = data.mediaType || 'book'
this.provider = data.provider || 'google'
this.settings = new LibrarySettings(data.settings)
this.createdAt = Date.now()
this.lastUpdate = Date.now()
}

update(payload) {
let hasUpdates = false

Expand All @@ -144,12 +119,12 @@ class Library {
hasUpdates = true
}
if (payload.folders) {
const newFolders = payload.folders.filter(f => !f.id)
const removedFolders = this.folders.filter(f => !payload.folders.some(_f => _f.id === f.id))
const newFolders = payload.folders.filter((f) => !f.id)
const removedFolders = this.folders.filter((f) => !payload.folders.some((_f) => _f.id === f.id))

if (removedFolders.length) {
const removedFolderIds = removedFolders.map(f => f.id)
this.folders = this.folders.filter(f => !removedFolderIds.includes(f.id))
const removedFolderIds = removedFolders.map((f) => f.id)
this.folders = this.folders.filter((f) => !removedFolderIds.includes(f.id))
}

if (newFolders.length) {
Expand All @@ -173,11 +148,11 @@ class Library {

checkFullPathInLibrary(fullPath) {
fullPath = filePathToPOSIX(fullPath)
return this.folders.find(folder => fullPath.startsWith(filePathToPOSIX(folder.fullPath)))
return this.folders.find((folder) => fullPath.startsWith(filePathToPOSIX(folder.fullPath)))
}

getFolderById(id) {
return this.folders.find(folder => folder.id === id)
return this.folders.find((folder) => folder.id === id)
}
}
module.exports = Library
module.exports = Library

0 comments on commit 8774e6b

Please sign in to comment.