anki/rslib/src/collection.rs
Damien Elmes 676f4e74a8 store config in separate DB table
- mtime is tracked on each key individually, which will allow
merging of config changes when syncing in the future
- added col.(get|set|remove)_config()
- in order to support existing code that was mutating returned
values (eg col.conf["something"]["another"] = 5), the returned list/dict
will be automatically wrapped so that when the value is dropped, it
will save the mutated item back to the DB if it's changed. Code that
is fetching lists/dicts from the config like so:

col.conf["foo"]["bar"] = baz
col.setMod()

will continue to work in most case, but should be gradually updated to:

conf = col.get_config("foo")
conf["bar"] = baz
col.set_config("foo", conf)
2020-04-06 15:39:47 +10:00

188 lines
4.9 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::err::{AnkiError, Result};
use crate::i18n::I18n;
use crate::log::Logger;
use crate::timestamp::TimestampSecs;
use crate::types::Usn;
use crate::{
sched::cutoff::{sched_timing_today, SchedTimingToday},
storage::SqliteStorage,
undo::UndoManager,
};
use std::path::PathBuf;
pub fn open_collection<P: Into<PathBuf>>(
path: P,
media_folder: P,
media_db: P,
server: bool,
i18n: I18n,
log: Logger,
) -> Result<Collection> {
let col_path = path.into();
let storage = SqliteStorage::open_or_create(&col_path, &i18n)?;
let col = Collection {
storage,
col_path,
media_folder: media_folder.into(),
media_db: media_db.into(),
i18n,
log,
server,
state: CollectionState::default(),
};
Ok(col)
}
#[cfg(test)]
pub fn open_test_collection() -> Collection {
use crate::log;
let i18n = I18n::new(&[""], "", log::terminal());
open_collection(":memory:", "", "", false, i18n, log::terminal()).unwrap()
}
#[derive(Debug, Default)]
pub struct CollectionState {
task_state: CollectionTaskState,
pub(crate) undo: UndoManager,
timing_today: Option<SchedTimingToday>,
}
#[derive(Debug, PartialEq)]
pub enum CollectionTaskState {
Normal,
// in this state, the DB must not be closed
MediaSyncRunning,
}
impl Default for CollectionTaskState {
fn default() -> Self {
Self::Normal
}
}
pub struct Collection {
pub(crate) storage: SqliteStorage,
#[allow(dead_code)]
pub(crate) col_path: PathBuf,
pub(crate) media_folder: PathBuf,
pub(crate) media_db: PathBuf,
pub(crate) i18n: I18n,
pub(crate) log: Logger,
pub(crate) server: bool,
pub(crate) state: CollectionState,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CollectionOp {
UpdateCard,
}
impl Collection {
/// Execute the provided closure in a transaction, rolling back if
/// an error is returned.
pub(crate) fn transact<F, R>(&mut self, op: Option<CollectionOp>, func: F) -> Result<R>
where
F: FnOnce(&mut Collection) -> Result<R>,
{
self.storage.begin_rust_trx()?;
self.state.undo.begin_step(op);
let mut res = func(self);
if res.is_ok() {
if let Err(e) = self.storage.mark_modified() {
res = Err(e);
} else if let Err(e) = self.storage.commit_rust_trx() {
res = Err(e);
}
}
if res.is_err() {
self.state.undo.discard_step();
self.storage.rollback_rust_trx()?;
} else {
self.state.undo.end_step();
}
res
}
pub(crate) fn set_media_sync_running(&mut self) -> Result<()> {
if self.state.task_state == CollectionTaskState::Normal {
self.state.task_state = CollectionTaskState::MediaSyncRunning;
Ok(())
} else {
Err(AnkiError::invalid_input("media sync already running"))
}
}
pub(crate) fn set_media_sync_finished(&mut self) -> Result<()> {
if self.state.task_state == CollectionTaskState::MediaSyncRunning {
self.state.task_state = CollectionTaskState::Normal;
Ok(())
} else {
Err(AnkiError::invalid_input("media sync not running"))
}
}
pub(crate) fn can_close(&self) -> bool {
self.state.task_state == CollectionTaskState::Normal
}
pub(crate) fn close(self, downgrade: bool) -> Result<()> {
self.storage.close(downgrade)
}
// fixme: invalidate when config changes
pub fn timing_today(&mut self) -> Result<SchedTimingToday> {
if let Some(timing) = &self.state.timing_today {
if timing.next_day_at > TimestampSecs::now().0 {
return Ok(*timing);
}
}
let local_offset = if self.server {
self.get_local_mins_west()
} else {
None
};
let timing = sched_timing_today(
self.storage.creation_stamp()?,
TimestampSecs::now(),
self.get_creation_mins_west(),
local_offset,
self.get_rollover(),
);
self.state.timing_today = Some(timing);
Ok(timing)
}
pub(crate) fn usn(&self) -> Result<Usn> {
// if we cache this in the future, must make sure to invalidate cache when usn bumped in sync.finish()
self.storage.usn(self.server)
}
pub(crate) fn ensure_schema_modified(&self) -> Result<()> {
if !self.storage.schema_modified()? {
Err(AnkiError::SchemaChange)
} else {
Ok(())
}
}
pub(crate) fn before_upload(&self) -> Result<()> {
self.storage.clear_tag_usns()?;
self.storage.clear_deck_conf_usns()?;
Ok(())
}
}