Files
oak-editor/crates/oakstorage/src/registry.rs
T
Mike-Solar 2248be8567 feat(storage): real oakstorage file backends + full-timeline .ove serializer
oakstorage (new workspace member): URI dispatch, pluggable backends
(ove-xml built in, otio/fcpxml via oakotio, C-vtable foreign
registration), the M10 C API surface, version info codes, last-error
and alive accounting; round-trip tests per backend.

oaknode serializer: persists the full timeline — sequence track lists,
track block lists, block ranges/media_in/speed/flags, clip footage
references, footage filename+streams, folder children — through
<custom> behavior hooks with two-phase reference resolution; loads the
C++ <olive><project><layout> containers (golden: tests/
project_with_footage.ove); round-trip is field-by-field and
byte-idempotent.
2026-08-14 15:14:38 +08:00

210 lines
5.8 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! The backend registry: registration, arbitration, lookup.
//!
//! The process-wide registry is pre-populated with the built-in
//! backends (ove-xml, otio) at first access; foreign backends (the
//! future database backend, or a test mock) register through
//! [`Registry::register`] or the C vtable pair in `ffi.rs`.
use std::sync::{Arc, Mutex, OnceLock};
use crate::backend::StorageBackend;
use crate::error::{Error, Result};
use crate::uri::StorageUri;
/// Process-wide backend registry (built-ins pre-registered).
pub struct Registry {
backends: Mutex<Vec<Arc<dyn StorageBackend>>>,
}
impl Registry {
/// Global registry; built-ins are installed on first access.
pub fn global() -> &'static Registry {
static REGISTRY: OnceLock<Registry> = OnceLock::new();
REGISTRY.get_or_init(|| {
let registry = Registry {
backends: Mutex::new(Vec::new()),
};
{
let mut guard = lock(&registry.backends);
for backend in crate::backends::builtins() {
guard.push(backend);
}
}
registry
})
}
/// Register a backend (registration order = arbitration order
/// within a scheme). Duplicate names are rejected with E_STATE.
pub fn register(&self, backend: Arc<dyn StorageBackend>) -> Result<()> {
let mut guard = lock(&self.backends);
if guard.iter().any(|b| b.name() == backend.name()) {
return Err(Error::State);
}
guard.push(backend);
Ok(())
}
/// Unregister by name; unknown name is E_NOT_FOUND.
pub fn unregister(&self, name: &str) -> Result<()> {
let mut guard = lock(&self.backends);
let before = guard.len();
guard.retain(|b| b.name() != name);
if guard.len() == before {
Err(Error::NotFound)
} else {
Ok(())
}
}
/// First backend claiming `uri` (E_NO_BACKEND when none).
pub fn resolve(&self, uri: &StorageUri) -> Result<Arc<dyn StorageBackend>> {
let guard = lock(&self.backends);
guard
.iter()
.find(|b| b.can_handle(uri))
.cloned()
.ok_or(Error::NoBackend)
}
}
/// Lock a mutex, tolerating poisoning (a panic inside this crate must
/// not cascade into every later call).
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(|e| e.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::{LoadResult, StorageBackend};
use crate::handle::CHandle;
/// A trivial in-crate backend for registry unit tests.
struct MemBackend;
impl StorageBackend for MemBackend {
fn name(&self) -> &'static str {
"mem-test"
}
fn uri_scheme(&self) -> &'static str {
"mem"
}
fn can_handle(&self, uri: &StorageUri) -> bool {
uri.scheme == "mem"
}
fn load(&self, _uri: &StorageUri) -> Result<LoadResult> {
Ok(LoadResult::success(CHandle::null()))
}
fn save(&self, _project: CHandle, _uri: &StorageUri, _options: u32) -> Result<()> {
Ok(())
}
}
#[test]
fn builtins_are_preregistered() {
let names: Vec<String> = {
let guard = lock(&Registry::global().backends);
guard.iter().map(|b| b.name().to_string()).collect()
};
assert!(names.iter().any(|n| n == "ove-xml"), "builtins: {names:?}");
assert!(names.iter().any(|n| n == "otio"), "builtins: {names:?}");
}
#[test]
fn register_unregister_resolve() {
let registry = Registry {
backends: Mutex::new(Vec::new()),
};
let backend = Arc::new(MemBackend);
assert!(registry.resolve(&StorageUri::parse("mem://x").unwrap()).is_err());
registry.register(backend).unwrap();
// Duplicate name rejected.
assert!(registry.register(Arc::new(MemBackend)).is_err());
assert!(
registry
.resolve(&StorageUri::parse("mem://x").unwrap())
.is_ok()
);
// Unknown scheme still unresolvable.
assert!(
registry
.resolve(&StorageUri::parse("file:///a.ove").unwrap())
.is_err()
);
// Unregister flips it back to E_NO_BACKEND; unknown names E_NOT_FOUND.
registry.unregister("mem-test").unwrap();
assert!(registry.unregister("mem-test").is_err());
assert!(
registry
.resolve(&StorageUri::parse("mem://x").unwrap())
.is_err()
);
}
#[test]
fn resolve_is_first_registered_winner() {
let registry = Registry {
backends: Mutex::new(Vec::new()),
};
struct First;
struct Second;
impl StorageBackend for First {
fn name(&self) -> &'static str {
"first"
}
fn uri_scheme(&self) -> &'static str {
"x"
}
fn can_handle(&self, _uri: &StorageUri) -> bool {
true
}
fn load(&self, _uri: &StorageUri) -> Result<LoadResult> {
Ok(LoadResult::success(CHandle::null()))
}
fn save(&self, _project: CHandle, _uri: &StorageUri, _options: u32) -> Result<()> {
Ok(())
}
}
impl StorageBackend for Second {
fn name(&self) -> &'static str {
"second"
}
fn uri_scheme(&self) -> &'static str {
"x"
}
fn can_handle(&self, _uri: &StorageUri) -> bool {
true
}
fn load(&self, _uri: &StorageUri) -> Result<LoadResult> {
Ok(LoadResult::success(CHandle::null()))
}
fn save(&self, _project: CHandle, _uri: &StorageUri, _options: u32) -> Result<()> {
Ok(())
}
}
registry.register(Arc::new(First)).unwrap();
registry.register(Arc::new(Second)).unwrap();
let backend = registry
.resolve(&StorageUri::parse("x://a").unwrap())
.unwrap();
assert_eq!(backend.name(), "first");
}
}