add library management
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import type { Library } from '@/types'
|
||||
import type { Library, LibraryType } from '@/types'
|
||||
|
||||
const CONFIG_PATH = path.resolve(process.cwd(), 'libraries.json')
|
||||
const CONFIG_TMP = CONFIG_PATH + '.tmp'
|
||||
|
||||
export function getLibraries(): Library[] {
|
||||
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8')
|
||||
return JSON.parse(raw) as Library[]
|
||||
try {
|
||||
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8')
|
||||
return JSON.parse(raw) as Library[]
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function getLibrary(id: string): Library | undefined {
|
||||
@@ -36,3 +41,87 @@ export function resolveAndJail(libraryRoot: string, subpath: string): string {
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
/**
|
||||
* Slugifies a name into a URL-safe id.
|
||||
* e.g. "My Games Library" → "my-games-library"
|
||||
*/
|
||||
function slugify(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically writes the library list to disk.
|
||||
*/
|
||||
function writeLibraries(libraries: Library[]): void {
|
||||
const json = JSON.stringify(libraries, null, 2) + '\n'
|
||||
fs.writeFileSync(CONFIG_TMP, json, 'utf-8')
|
||||
fs.renameSync(CONFIG_TMP, CONFIG_PATH)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new library. Validates the path exists as a directory.
|
||||
* Throws a descriptive Error on failure.
|
||||
*/
|
||||
export function addLibrary(name: string, libraryPath: string, type: LibraryType): Library {
|
||||
const trimmedName = name.trim()
|
||||
const trimmedPath = libraryPath.trim()
|
||||
|
||||
if (!trimmedName) throw new Error('Name is required.')
|
||||
if (!trimmedPath) throw new Error('Path is required.')
|
||||
|
||||
// Validate the path exists and is a directory
|
||||
const resolved = path.isAbsolute(trimmedPath)
|
||||
? trimmedPath
|
||||
: path.resolve(process.cwd(), trimmedPath)
|
||||
|
||||
let stat: fs.Stats
|
||||
try {
|
||||
stat = fs.statSync(resolved)
|
||||
} catch {
|
||||
throw new Error(`Path does not exist: ${trimmedPath}`)
|
||||
}
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(`Path is not a directory: ${trimmedPath}`)
|
||||
}
|
||||
|
||||
const libraries = getLibraries()
|
||||
|
||||
// Generate a unique id
|
||||
const baseId = slugify(trimmedName) || 'library'
|
||||
let id = baseId
|
||||
let suffix = 2
|
||||
while (libraries.some((lib) => lib.id === id)) {
|
||||
id = `${baseId}-${suffix++}`
|
||||
}
|
||||
|
||||
// Reject exact duplicate names (same name, case-insensitive)
|
||||
const duplicate = libraries.find(
|
||||
(lib) => lib.name.toLowerCase() === trimmedName.toLowerCase()
|
||||
)
|
||||
if (duplicate) {
|
||||
throw new Error(`A library named "${trimmedName}" already exists.`)
|
||||
}
|
||||
|
||||
const newLibrary: Library = { id, name: trimmedName, path: trimmedPath, type }
|
||||
writeLibraries([...libraries, newLibrary])
|
||||
return newLibrary
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a library by id. Returns true if removed, false if not found.
|
||||
*/
|
||||
export function removeLibrary(id: string): boolean {
|
||||
const libraries = getLibraries()
|
||||
const index = libraries.findIndex((lib) => lib.id === id)
|
||||
if (index === -1) return false
|
||||
const updated = libraries.filter((lib) => lib.id !== id)
|
||||
writeLibraries(updated)
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user