Proper error handling in file indexing

No longer unwrap like there's no tomorrow
This commit is contained in:
Antoine Gersant 2016-12-03 13:42:06 -08:00
parent c02b971984
commit 0a729184e4
2 changed files with 141 additions and 136 deletions

View file

@ -8,6 +8,7 @@ use iron::status::Status;
use lewton; use lewton;
use metaflac; use metaflac;
use regex; use regex;
use sqlite;
use std; use std;
error_chain! { error_chain! {
@ -20,6 +21,7 @@ error_chain! {
Image(image::ImageError); Image(image::ImageError);
Io(std::io::Error); Io(std::io::Error);
Regex(regex::Error); Regex(regex::Error);
SQLite(sqlite::Error);
Vorbis(lewton::VorbisError); Vorbis(lewton::VorbisError);
} }

View file

@ -95,22 +95,20 @@ struct IndexBuilder<'db> {
} }
impl<'db> IndexBuilder<'db> { impl<'db> IndexBuilder<'db> {
fn new(db: &Connection) -> IndexBuilder { fn new(db: &Connection) -> Result<IndexBuilder> {
let mut queue = Vec::new(); let mut queue = Vec::new();
queue.reserve_exact(INDEX_BUILDING_INSERT_BUFFER_SIZE); queue.reserve_exact(INDEX_BUILDING_INSERT_BUFFER_SIZE);
IndexBuilder { Ok(IndexBuilder {
queue: queue, queue: queue,
db: db, db: db,
insert_directory: insert_directory:
db.prepare("INSERT OR REPLACE INTO directories (path, parent, artwork, year, \ db.prepare("INSERT OR REPLACE INTO directories (path, parent, artwork, year, \
artist, album) VALUES (?, ?, ?, ?, ?, ?)") artist, album) VALUES (?, ?, ?, ?, ?, ?)")?,
.unwrap(),
insert_song: insert_song:
db.prepare("INSERT OR REPLACE INTO songs (path, parent, disc_number, track_number, \ db.prepare("INSERT OR REPLACE INTO songs (path, parent, disc_number, track_number, \
title, year, album_artist, artist, album, artwork) VALUES (?, ?, ?, ?, \ title, year, album_artist, artist, album, artwork) VALUES (?, ?, ?, ?, \
?, ?, ?, ?, ?, ?)") ?, ?, ?, ?, ?, ?)")?,
.unwrap(), })
}
} }
fn get_parent(path: &str) -> Option<String> { fn get_parent(path: &str) -> Option<String> {
@ -123,63 +121,56 @@ impl<'db> IndexBuilder<'db> {
None None
} }
fn flush(&mut self) { fn flush(&mut self) -> Result<()> {
self.db.execute("BEGIN TRANSACTION").ok(); self.db.execute("BEGIN TRANSACTION")?;
while let Some(file) = self.queue.pop() { while let Some(file) = self.queue.pop() {
match file { match file {
// Insert directory // Insert directory
CollectionFile::Directory(directory) => { CollectionFile::Directory(directory) => {
let parent = IndexBuilder::get_parent(directory.path.as_str()); let parent = IndexBuilder::get_parent(directory.path.as_str());
self.insert_directory.reset().ok(); self.insert_directory.reset()?;
self.insert_directory.bind(1, &Value::String(directory.path)).unwrap(); self.insert_directory.bind(1, &Value::String(directory.path))?;
self.insert_directory.bind(2, &string_option_to_value(parent)).unwrap(); self.insert_directory.bind(2, &string_option_to_value(parent))?;
self.insert_directory self.insert_directory
.bind(3, &string_option_to_value(directory.artwork)) .bind(3, &string_option_to_value(directory.artwork))?;
.unwrap(); self.insert_directory.bind(4, &i32_option_to_value(directory.year))?;
self.insert_directory.bind(4, &i32_option_to_value(directory.year)).unwrap();
self.insert_directory self.insert_directory
.bind(5, &string_option_to_value(directory.artist)) .bind(5, &string_option_to_value(directory.artist))?;
.unwrap();
self.insert_directory self.insert_directory
.bind(6, &string_option_to_value(directory.album)) .bind(6, &string_option_to_value(directory.album))?;
.unwrap(); self.insert_directory.next()?;
self.insert_directory.next().ok();
} }
// Insert song // Insert song
CollectionFile::Song(song) => { CollectionFile::Song(song) => {
let parent = IndexBuilder::get_parent(song.path.as_str()); let parent = IndexBuilder::get_parent(song.path.as_str());
self.insert_song.reset().ok(); self.insert_song.reset()?;
self.insert_song.bind(1, &Value::String(song.path)).unwrap(); self.insert_song.bind(1, &Value::String(song.path))?;
self.insert_song.bind(2, &string_option_to_value(parent)).unwrap(); self.insert_song.bind(2, &string_option_to_value(parent))?;
self.insert_song.bind(3, &u32_option_to_value(song.disc_number)).unwrap(); self.insert_song.bind(3, &u32_option_to_value(song.disc_number))?;
self.insert_song.bind(4, &u32_option_to_value(song.track_number)).unwrap(); self.insert_song.bind(4, &u32_option_to_value(song.track_number))?;
self.insert_song.bind(5, &string_option_to_value(song.title)).unwrap(); self.insert_song.bind(5, &string_option_to_value(song.title))?;
self.insert_song.bind(6, &i32_option_to_value(song.year)).unwrap(); self.insert_song.bind(6, &i32_option_to_value(song.year))?;
self.insert_song.bind(7, &string_option_to_value(song.album_artist)).unwrap(); self.insert_song.bind(7, &string_option_to_value(song.album_artist))?;
self.insert_song.bind(8, &string_option_to_value(song.artist)).unwrap(); self.insert_song.bind(8, &string_option_to_value(song.artist))?;
self.insert_song.bind(9, &string_option_to_value(song.album)).unwrap(); self.insert_song.bind(9, &string_option_to_value(song.album))?;
self.insert_song.bind(10, &string_option_to_value(song.artwork)).unwrap(); self.insert_song.bind(10, &string_option_to_value(song.artwork))?;
self.insert_song.next().ok(); self.insert_song.next()?;
} }
} }
} }
self.db.execute("END TRANSACTION").ok(); self.db.execute("END TRANSACTION")?;
Ok(())
} }
fn push(&mut self, file: CollectionFile) { fn push(&mut self, file: CollectionFile) -> Result<()> {
if self.queue.len() == self.queue.capacity() { if self.queue.len() == self.queue.capacity() {
self.flush(); self.flush()?;
} }
self.queue.push(file); self.queue.push(file);
} Ok(())
}
impl<'db> Drop for IndexBuilder<'db> {
fn drop(&mut self) {
self.flush();
} }
} }
@ -200,18 +191,18 @@ impl Index {
if path.exists() { if path.exists() {
// Migration // Migration
} else { } else {
index.init(); index.init()?;
} }
Ok(index) Ok(index)
} }
fn init(&self) { fn init(&self) -> Result<()> {
println!("Initializing index database"); println!("Initializing index database");
let db = self.connect(); let db = self.connect()?;
db.execute("PRAGMA synchronous = NORMAL").unwrap(); db.execute("PRAGMA synchronous = NORMAL")?;
db.execute(" db.execute("
CREATE TABLE version CREATE TABLE version
@ -254,62 +245,68 @@ impl Index {
, UNIQUE(path) , UNIQUE(path)
); );
") ")?;
.unwrap();
Ok(())
} }
fn connect(&self) -> Connection { fn connect(&self) -> Result<Connection> {
let mut db = sqlite::open(self.path.clone()).unwrap(); let mut db = sqlite::open(self.path.clone())?;
db.set_busy_timeout(INDEX_LOCK_TIMEOUT).ok(); db.set_busy_timeout(INDEX_LOCK_TIMEOUT)?;
db Ok(db)
} }
fn update_index(&self, db: &Connection) { fn update_index(&self, db: &Connection) -> Result<()> {
let start = time::Instant::now(); let start = time::Instant::now();
println!("Beginning library index update"); println!("Beginning library index update");
self.clean(db); self.clean(db)?;
self.populate(db); self.populate(db)?;
println!("Library index update took {} seconds", println!("Library index update took {} seconds",
start.elapsed().as_secs()); start.elapsed().as_secs());
Ok(())
} }
fn clean(&self, db: &Connection) { fn clean(&self, db: &Connection) -> Result<()> {
{ {
let mut select = db.prepare("SELECT path FROM songs").unwrap(); let mut select = db.prepare("SELECT path FROM songs")?;
let mut delete = db.prepare("DELETE FROM songs WHERE path = ?").unwrap(); let mut delete = db.prepare("DELETE FROM songs WHERE path = ?")?;
while let State::Row = select.next().unwrap() { while let Ok(State::Row) = select.next() {
let path_string: String = select.read(0).unwrap(); let path_string: String = select.read(0)?;
let path = Path::new(path_string.as_str()); let path = Path::new(path_string.as_str());
if !path.exists() || self.vfs.real_to_virtual(path).is_err() { if !path.exists() || self.vfs.real_to_virtual(path).is_err() {
delete.reset().ok(); delete.reset()?;
delete.bind(1, &Value::String(path_string.to_owned())).ok(); delete.bind(1, &Value::String(path_string.to_owned()))?;
delete.next().ok(); delete.next()?;
} }
} }
} }
{ {
let mut select = db.prepare("SELECT path FROM directories").unwrap(); let mut select = db.prepare("SELECT path FROM directories")?;
let mut delete = db.prepare("DELETE FROM directories WHERE path = ?").unwrap(); let mut delete = db.prepare("DELETE FROM directories WHERE path = ?")?;
while let State::Row = select.next().unwrap() { while let Ok(State::Row) = select.next() {
let path_string: String = select.read(0).unwrap(); let path_string: String = select.read(0)?;
let path = Path::new(path_string.as_str()); let path = Path::new(path_string.as_str());
if !path.exists() || self.vfs.real_to_virtual(path).is_err() { if !path.exists() || self.vfs.real_to_virtual(path).is_err() {
delete.reset().ok(); delete.reset()?;
delete.bind(1, &Value::String(path_string.to_owned())).ok(); delete.bind(1, &Value::String(path_string.to_owned()))?;
delete.next().ok(); delete.next()?;
} }
} }
} }
Ok(())
} }
fn populate(&self, db: &Connection) { fn populate(&self, db: &Connection) -> Result<()> {
let vfs = self.vfs.deref(); let vfs = self.vfs.deref();
let mount_points = vfs.get_mount_points(); let mount_points = vfs.get_mount_points();
let mut builder = IndexBuilder::new(db); let mut builder = IndexBuilder::new(db)?;
for (_, target) in mount_points { for (_, target) in mount_points {
self.populate_directory(&mut builder, target.as_path()); self.populate_directory(&mut builder, target.as_path())?;
} }
builder.flush()?;
Ok(())
} }
fn get_artwork(&self, dir: &Path) -> Option<String> { fn get_artwork(&self, dir: &Path) -> Option<String> {
@ -333,15 +330,12 @@ impl Index {
None None
} }
fn populate_directory(&self, builder: &mut IndexBuilder, path: &Path) { fn populate_directory(&self, builder: &mut IndexBuilder, path: &Path) -> Result<()> {
// Find artwork // Find artwork
let artwork = self.get_artwork(path); let artwork = self.get_artwork(path);
let path_string = match path.to_str() { let path_string = path.to_str().ok_or("Invalid directory path")?;
Some(p) => p,
_ => return,
};
let mut directory_album = None; let mut directory_album = None;
let mut directory_year = None; let mut directory_year = None;
@ -359,7 +353,7 @@ impl Index {
}; };
if file_path.is_dir() { if file_path.is_dir() {
self.populate_directory(builder, file_path.as_path()); self.populate_directory(builder, file_path.as_path())?;
} else { } else {
if let Some(file_path_string) = file_path.to_str() { if let Some(file_path_string) = file_path.to_str() {
if let Ok(tags) = metadata::read(file_path.as_path()) { if let Ok(tags) = metadata::read(file_path.as_path()) {
@ -372,19 +366,18 @@ impl Index {
if tags.album.is_some() { if tags.album.is_some() {
inconsistent_directory_album |= directory_album.is_some() && inconsistent_directory_album |= directory_album.is_some() &&
directory_album != tags.album; directory_album != tags.album;
directory_album = Some(tags.album.as_ref().unwrap().clone()); directory_album = tags.album.as_ref().map(|a| a.clone());
} }
if tags.album_artist.is_some() { if tags.album_artist.is_some() {
inconsistent_directory_artist |= directory_artist.is_some() && inconsistent_directory_artist |= directory_artist.is_some() &&
directory_artist != directory_artist !=
tags.album_artist; tags.album_artist;
directory_artist = directory_artist = tags.album_artist.as_ref().map(|a| a.clone());
Some(tags.album_artist.as_ref().unwrap().clone());
} else if tags.artist.is_some() { } else if tags.artist.is_some() {
inconsistent_directory_artist |= directory_artist.is_some() && inconsistent_directory_artist |= directory_artist.is_some() &&
directory_artist != tags.artist; directory_artist != tags.artist;
directory_artist = Some(tags.artist.as_ref().unwrap().clone()); directory_artist = tags.artist.as_ref().map(|a| a.clone());
} }
let song = Song { let song = Song {
@ -399,7 +392,7 @@ impl Index {
artwork: artwork.as_ref().map(|s| s.to_owned()), artwork: artwork.as_ref().map(|s| s.to_owned()),
}; };
builder.push(CollectionFile::Song(song)); builder.push(CollectionFile::Song(song))?;
} }
} }
} }
@ -424,47 +417,55 @@ impl Index {
artist: directory_artist, artist: directory_artist,
year: directory_year, year: directory_year,
}; };
builder.push(CollectionFile::Directory(directory)); builder.push(CollectionFile::Directory(directory))?;
Ok(())
} }
pub fn run(&self) { pub fn run(&self) {
loop { loop {
{ if let Ok(db) = self.connect() {
let db = self.connect(); if let Err(e) = self.update_index(&db) {
self.update_index(&db); println!("Error while updating index: {}", e);
}
} }
thread::sleep(time::Duration::from_secs(self.sleep_duration)); thread::sleep(time::Duration::from_secs(self.sleep_duration));
} }
} }
fn select_songs(&self, select: &mut Statement) -> Vec<Song> { fn select_songs(&self, select: &mut Statement) -> Result<Vec<Song>> {
let mut output = Vec::new(); let mut output = Vec::new();
while let State::Row = select.next().unwrap() { while let State::Row = select.next()? {
let song_path: String = select.read(0).unwrap(); let song_path: String = select.read(0)?;
let disc_number: Value = select.read(1).unwrap(); let disc_number: Value = select.read(1)?;
let track_number: Value = select.read(2).unwrap(); let track_number: Value = select.read(2)?;
let title: Value = select.read(3).unwrap(); let title: Value = select.read(3)?;
let year: Value = select.read(4).unwrap(); let year: Value = select.read(4)?;
let album_artist: Value = select.read(5).unwrap(); let album_artist: Value = select.read(5)?;
let artist: Value = select.read(6).unwrap(); let artist: Value = select.read(6)?;
let album: Value = select.read(7).unwrap(); let album: Value = select.read(7)?;
let artwork: Value = select.read(8).unwrap(); let artwork: Value = select.read(8)?;
let song_path = Path::new(song_path.as_str()); let song_path = Path::new(song_path.as_str());
let song_path = match self.vfs.real_to_virtual(song_path) { let song_path = match self.vfs.real_to_virtual(song_path) {
Ok(p) => p, Ok(p) => p.to_str().ok_or("Invalid song path")?.to_owned(),
_ => continue, _ => continue,
}; };
let artwork = artwork.as_string() let artwork_path = artwork.as_string().map(|p| Path::new(p));
.map(|p| Path::new(p)) let mut artwork = None;
.and_then(|p| self.vfs.real_to_virtual(p).ok()); if let Some(artwork_path) = artwork_path {
artwork = match self.vfs.real_to_virtual(artwork_path) {
Ok(p) => Some(p.to_str().ok_or("Invalid song artwork path")?.to_owned()),
_ => None,
};
}
let song = Song { let song = Song {
path: song_path.to_str().unwrap().to_owned(), path: song_path,
disc_number: disc_number.as_integer().map(|n| n as u32), disc_number: disc_number.as_integer().map(|n| n as u32),
track_number: track_number.as_integer().map(|n| n as u32), track_number: track_number.as_integer().map(|n| n as u32),
title: title.as_string().map(|s| s.to_owned()), title: title.as_string().map(|s| s.to_owned()),
@ -472,47 +473,51 @@ impl Index {
album_artist: album_artist.as_string().map(|s| s.to_owned()), album_artist: album_artist.as_string().map(|s| s.to_owned()),
artist: artist.as_string().map(|s| s.to_owned()), artist: artist.as_string().map(|s| s.to_owned()),
album: album.as_string().map(|s| s.to_owned()), album: album.as_string().map(|s| s.to_owned()),
artwork: artwork.map(|p| p.to_str().unwrap().to_owned()), artwork: artwork,
}; };
output.push(song); output.push(song);
} }
output Ok(output)
} }
// List sub-directories within a directory // List sub-directories within a directory
fn browse_directories(&self, real_path: &Path) -> Vec<CollectionFile> { fn browse_directories(&self, real_path: &Path) -> Result<Vec<CollectionFile>> {
let db = self.connect(); let db = self.connect()?;
let mut output = Vec::new(); let mut output = Vec::new();
let path_string = real_path.to_string_lossy(); let path_string = real_path.to_string_lossy();
let mut select = let mut select =
db.prepare("SELECT path, artwork, year, artist, album FROM directories WHERE \ db.prepare("SELECT path, artwork, year, artist, album FROM directories WHERE \
parent = ? ORDER BY path COLLATE NOCASE ASC") parent = ? ORDER BY path COLLATE NOCASE ASC")?;
.unwrap(); select.bind(1, &Value::String(path_string.deref().to_owned()))?;
select.bind(1, &Value::String(path_string.deref().to_owned())).unwrap();
while let State::Row = select.next().unwrap() { while let State::Row = select.next()? {
let directory_value: String = select.read(0).unwrap(); let directory_value: String = select.read(0)?;
let artwork_path: Value = select.read(1).unwrap(); let artwork_path: Value = select.read(1)?;
let year: Value = select.read(2).unwrap(); let year: Value = select.read(2)?;
let artist: Value = select.read(3).unwrap(); let artist: Value = select.read(3)?;
let album: Value = select.read(4).unwrap(); let album: Value = select.read(4)?;
let directory_path = Path::new(directory_value.as_str()); let directory_path = Path::new(directory_value.as_str());
let directory_path = match self.vfs.real_to_virtual(directory_path) { let directory_path = match self.vfs.real_to_virtual(directory_path) {
Ok(p) => p, Ok(p) => p.to_str().ok_or("Invalid directory path")?.to_owned(),
_ => continue, _ => continue,
}; };
let artwork_path = artwork_path.as_string() let artwork_path = artwork_path.as_string().map(|p| Path::new(p));
.map(|p| Path::new(p)) let mut artwork = None;
.and_then(|p| self.vfs.real_to_virtual(p).ok()); if let Some(artwork_path) = artwork_path {
artwork = match self.vfs.real_to_virtual(artwork_path) {
Ok(p) => Some(p.to_str().ok_or("Invalid directory artwork path")?.to_owned()),
_ => None,
};
}
let directory = Directory { let directory = Directory {
path: directory_path.to_str().unwrap().to_owned(), path: directory_path,
artwork: artwork_path.map(|p| p.to_str().unwrap().to_owned()), artwork: artwork,
year: year.as_integer().map(|n| n as i32), year: year.as_integer().map(|n| n as i32),
artist: artist.as_string().map(|s| s.to_owned()), artist: artist.as_string().map(|s| s.to_owned()),
album: album.as_string().map(|s| s.to_owned()), album: album.as_string().map(|s| s.to_owned()),
@ -520,20 +525,19 @@ impl Index {
output.push(CollectionFile::Directory(directory)); output.push(CollectionFile::Directory(directory));
} }
output Ok(output)
} }
// List songs within a directory // List songs within a directory
fn browse_songs(&self, real_path: &Path) -> Vec<CollectionFile> { fn browse_songs(&self, real_path: &Path) -> Result<Vec<CollectionFile>> {
let db = self.connect(); let db = self.connect()?;
let path_string = real_path.to_string_lossy(); let path_string = real_path.to_string_lossy();
let mut select = let mut select =
db.prepare("SELECT path, disc_number, track_number, title, year, album_artist, \ db.prepare("SELECT path, disc_number, track_number, title, year, album_artist, \
artist, album, artwork FROM songs WHERE parent = ? ORDER BY path \ artist, album, artwork FROM songs WHERE parent = ? ORDER BY path \
COLLATE NOCASE ASC") COLLATE NOCASE ASC")?;
.unwrap(); select.bind(1, &Value::String(path_string.deref().to_owned()))?;
select.bind(1, &Value::String(path_string.deref().to_owned())).unwrap(); Ok(self.select_songs(&mut select)?.into_iter().map(|s| CollectionFile::Song(s)).collect())
self.select_songs(&mut select).into_iter().map(|s| CollectionFile::Song(s)).collect()
} }
pub fn browse(&self, virtual_path: &Path) -> Result<Vec<CollectionFile>> { pub fn browse(&self, virtual_path: &Path) -> Result<Vec<CollectionFile>> {
@ -556,8 +560,8 @@ impl Index {
// Browse sub-directory // Browse sub-directory
} else { } else {
let real_path = self.vfs.virtual_to_real(virtual_path)?; let real_path = self.vfs.virtual_to_real(virtual_path)?;
let directories = self.browse_directories(real_path.as_path()); let directories = self.browse_directories(real_path.as_path())?;
let songs = self.browse_songs(real_path.as_path()); let songs = self.browse_songs(real_path.as_path())?;
output.extend(directories); output.extend(directories);
output.extend(songs); output.extend(songs);
} }
@ -566,15 +570,14 @@ impl Index {
} }
pub fn flatten(&self, virtual_path: &Path) -> Result<Vec<Song>> { pub fn flatten(&self, virtual_path: &Path) -> Result<Vec<Song>> {
let db = self.connect(); let db = self.connect()?;
let real_path = self.vfs.virtual_to_real(virtual_path)?; let real_path = self.vfs.virtual_to_real(virtual_path)?;
let path_string = real_path.to_string_lossy().into_owned() + "%"; let path_string = real_path.to_string_lossy().into_owned() + "%";
let mut select = let mut select =
db.prepare("SELECT path, disc_number, track_number, title, year, album_artist, \ db.prepare("SELECT path, disc_number, track_number, title, year, album_artist, \
artist, album, artwork FROM songs WHERE path LIKE ? ORDER BY path \ artist, album, artwork FROM songs WHERE path LIKE ? ORDER BY path \
COLLATE NOCASE ASC") COLLATE NOCASE ASC")?;
.unwrap(); select.bind(1, &Value::String(path_string.deref().to_owned()))?;
select.bind(1, &Value::String(path_string.deref().to_owned())).unwrap(); self.select_songs(&mut select)
Ok(self.select_songs(&mut select))
} }
} }