Files
servo/components/storage/client_storage.rs
Jan Varga 902d5d10d9 storage: Add support for temporary storage (#44433)
Add support for temporary storage via a new config option
`temporary_storage`
and a corresponding command-line argument `--temporary-storage`.

When enabled, client storage uses a storage directory
(e.g. `clientstorage/temporary/<uuid>`) instead of the shared default
location.
This can be used to provide isolation between concurrent servo
instances.

This is especially useful for WPT runs, where multiple Servo instances
may
execute in parallel and would otherwise share the same storage, leading
to
cross-test interference.

Based on that, this PR also updates the WPT runner to enable temporary
storage
by default.

Testing: Manual testing and a full try run.

Signed-off-by: Jan Varga <jvarga@igalia.com>
2026-04-25 10:18:21 +00:00

760 lines
27 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::fmt::Debug;
use std::path::PathBuf;
use std::str::FromStr;
use std::{fs, thread};
use log::warn;
use rusqlite::{Connection, Transaction};
use servo_base::generic_channel::{self, GenericReceiver, GenericSender};
use servo_base::id::{BrowsingContextId, WebViewId};
use servo_url::ImmutableOrigin;
use storage_traits::client_storage::{
ClientStorageErrorr, ClientStorageThreadHandle, ClientStorageThreadMessage, Mode,
StorageIdentifier, StorageProxyMap, StorageType,
};
use uuid::Uuid;
/// <https://storage.spec.whatwg.org/#storage-quota>
/// The storage quota of a storage shelf is an implementation-defined conservative estimate of the
/// total amount of byttes it can hold. We use 10 GiB per shelf, matching Firefox's documented
/// limit (<https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria>).
const STORAGE_SHELF_QUOTA_BYTES: u64 = 10 * 1024 * 1024 * 1024;
trait RegistryEngine {
type Error: Debug;
fn create_database(
&mut self,
bottle_id: i64,
name: String,
) -> Result<PathBuf, ClientStorageErrorr<Self::Error>>;
fn delete_database(
&mut self,
bottle_id: i64,
name: String,
) -> Result<(), ClientStorageErrorr<Self::Error>>;
fn obtain_a_storage_bottle_map(
&mut self,
storage_type: StorageType,
webview: WebViewId,
storage_identifier: StorageIdentifier,
origin: ImmutableOrigin,
sender: &GenericSender<ClientStorageThreadMessage>,
) -> Result<StorageProxyMap, ClientStorageErrorr<Self::Error>>;
fn persisted(&mut self, origin: ImmutableOrigin) -> Result<bool, String>;
fn persist(
&mut self,
origin: ImmutableOrigin,
permission_granted: bool,
) -> Result<bool, String>;
fn estimate(&mut self, origin: ImmutableOrigin) -> Result<(u64, u64), String>;
}
struct SqliteEngine {
connection: Connection,
base_dir: PathBuf,
}
impl SqliteEngine {
fn new(base_dir: PathBuf) -> rusqlite::Result<Self> {
let db_path = base_dir.join("reg.sqlite");
let connection = Connection::open(db_path)?;
Self::init(&connection)?;
Ok(SqliteEngine {
connection,
base_dir,
})
}
fn memory() -> rusqlite::Result<Self> {
let connection = Connection::open_in_memory()?;
Self::init(&connection)?;
Ok(SqliteEngine {
connection,
base_dir: PathBuf::new(),
})
}
fn init(connection: &Connection) -> rusqlite::Result<()> {
connection.execute(r#"PRAGMA foreign_keys = ON;"#, [])?;
connection.execute(
r#"CREATE TABLE IF NOT EXISTS sheds (
id INTEGER PRIMARY KEY,
storage_type TEXT NOT NULL,
browsing_context TEXT
);"#,
[],
)?;
// Note: indices required for ON CONFLICT to work.
connection.execute(
r#"CREATE UNIQUE INDEX IF NOT EXISTS idx_sheds_local
ON sheds(storage_type) WHERE browsing_context IS NULL;"#,
[],
)?;
connection.execute(
r#"CREATE UNIQUE INDEX IF NOT EXISTS idx_sheds_session
ON sheds(browsing_context) WHERE browsing_context IS NOT NULL;"#,
[],
)?;
connection.execute(
r#"CREATE TABLE IF NOT EXISTS shelves (
id INTEGER PRIMARY KEY,
shed_id INTEGER NOT NULL,
origin TEXT NOT NULL,
UNIQUE (shed_id, origin),
FOREIGN KEY (shed_id) REFERENCES sheds(id) ON DELETE CASCADE
);"#,
[],
)?;
// Note: name is to support https://wicg.github.io/storage-buckets/
connection.execute(
r#"CREATE TABLE IF NOT EXISTS buckets (
id INTEGER PRIMARY KEY,
shelf_id INTEGER NOT NULL UNIQUE,
persisted BOOLEAN DEFAULT 0,
name TEXT,
mode TEXT,
expires DATETIME,
FOREIGN KEY (shelf_id) REFERENCES shelves(id) ON DELETE CASCADE
);"#,
[],
)?;
// Note: quota not in db, hardcoded at https://storage.spec.whatwg.org/#storage-endpoint-quota
connection.execute(
r#"CREATE TABLE IF NOT EXISTS bottles (
id INTEGER PRIMARY KEY,
bucket_id INTEGER NOT NULL,
identifier TEXT NOT NULL, -- "idb", "ls", "opfs", "cache"
UNIQUE (bucket_id, identifier),
FOREIGN KEY (bucket_id) REFERENCES buckets(id) ON DELETE CASCADE
);"#,
[],
)?;
connection.execute(
r#"CREATE TABLE IF NOT EXISTS databases (
id INTEGER PRIMARY KEY,
bottle_id INTEGER NOT NULL,
name TEXT NOT NULL,
UNIQUE (bottle_id, name),
FOREIGN KEY (bottle_id) REFERENCES bottles(id) ON DELETE CASCADE
);
"#,
[],
)?;
connection.execute(
r#"CREATE TABLE IF NOT EXISTS directories (
id INTEGER PRIMARY KEY,
database_id INTEGER NOT NULL UNIQUE,
path TEXT NOT NULL,
FOREIGN KEY (database_id) REFERENCES databases(id) ON DELETE CASCADE
);"#,
[],
)?;
connection.execute_batch(
r#"
CREATE UNIQUE INDEX IF NOT EXISTS sheds_local_identity_idx
ON sheds(storage_type)
WHERE storage_type = 'local' AND browsing_context IS NULL;
CREATE UNIQUE INDEX IF NOT EXISTS sheds_session_identity_idx
ON sheds(storage_type, browsing_context)
WHERE storage_type = 'session' AND browsing_context IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS shelves_origin_shed_identity_idx
ON shelves(origin, shed_id);
"#,
)?;
// TODO: Delete expired and non-persistent buckets on startup
Ok(())
}
}
fn ensure_storage_shed(
storage_type: &StorageType,
browsing_context: Option<String>,
tx: &Transaction,
) -> rusqlite::Result<i64> {
match browsing_context {
Some(browsing_context) => {
tx.execute(
"INSERT INTO sheds (storage_type, browsing_context) VALUES (?1, ?2) ON CONFLICT DO NOTHING;",
(storage_type.as_str(), browsing_context.as_str()),
)?;
tx.query_row(
"SELECT id FROM sheds WHERE storage_type = ?1 AND browsing_context = ?2;",
(storage_type.as_str(), browsing_context.as_str()),
|row| row.get(0),
)
},
None => {
tx.execute(
"INSERT INTO sheds (storage_type, browsing_context) VALUES (?1, NULL) ON CONFLICT DO NOTHING;",
[storage_type.as_str()],
)?;
tx.query_row(
"SELECT id FROM sheds WHERE storage_type = ?1 AND browsing_context IS NULL;",
[storage_type.as_str()],
|row| row.get(0),
)
},
}
}
/// <https://storage.spec.whatwg.org/#create-a-storage-bucket>
fn create_a_storage_bucket(
shelf_id: i64,
storage_type: StorageType,
tx: &Transaction,
) -> rusqlite::Result<i64> {
// Step 1. Let bucket be null.
// Step 2. If type is "local", then set bucket to a new local storage bucket.
let bucket_id: i64 = if let StorageType::Local = storage_type {
tx.query_row(
"INSERT INTO buckets (mode, shelf_id) VALUES (?1, ?2)
ON CONFLICT(shelf_id) DO UPDATE SET shelf_id = excluded.shelf_id
RETURNING id;",
[Mode::default().as_str(), &shelf_id.to_string()],
|row| row.get(0),
)?
} else {
// Step 3. Otherwise:
// Step 3.1. Assert: type is "session".
// Step 3.2. Set bucket to a new session storage bucket.
tx.query_row(
"INSERT INTO buckets (shelf_id) VALUES (?1)
ON CONFLICT(shelf_id) DO UPDATE SET shelf_id = excluded.shelf_id
RETURNING id;",
[&shelf_id.to_string()],
|row| row.get(0),
)?
};
// Step 4. For each endpoint of registered storage endpoints whose types contain type,
// set buckets bottle map[endpoints identifier] to
// a new storage bottle whose quota is endpoints quota.
// <https://storage.spec.whatwg.org/#registered-storage-endpoints>
let registered_endpoints = match storage_type {
StorageType::Local => vec![
StorageIdentifier::Caches,
StorageIdentifier::IndexedDB,
StorageIdentifier::LocalStorage,
StorageIdentifier::ServiceWorkerRegistrations,
],
StorageType::Session => vec![StorageIdentifier::SessionStorage],
};
for identifier in registered_endpoints {
tx.execute(
"INSERT INTO bottles (bucket_id, identifier) VALUES (?1, ?2)
ON CONFLICT(bucket_id, identifier) DO NOTHING;",
(bucket_id, identifier.as_str()),
)?;
}
// Step 5. Return bucket.
Ok(bucket_id)
}
/// <https://storage.spec.whatwg.org/#create-a-storage-shelf>
fn create_a_storage_shelf(
shed: i64,
origin: &ImmutableOrigin,
storage_type: StorageType,
tx: &Transaction,
) -> rusqlite::Result<StorageShelf> {
// To create a storage shelf, given a storage type type, run these steps:
// Step 1. Let shelf be a new storage shelf.
// Step 2. Set shelfs bucket map["default"] to the result of running create a storage bucket with type.
let shelf_id: i64 = tx.query_row(
"INSERT INTO shelves (shed_id, origin) VALUES (?1, ?2)
ON CONFLICT(shed_id, origin) DO UPDATE SET origin = excluded.origin
RETURNING id;",
[&shed.to_string(), &origin.ascii_serialization()],
|row| row.get(0),
)?;
// Step 3. Return shelf.
Ok(StorageShelf {
default_bucket_id: create_a_storage_bucket(shelf_id, storage_type, tx)?,
})
}
/// <https://storage.spec.whatwg.org/#obtain-a-storage-shelf>
fn obtain_a_storage_shelf(
shed: i64,
origin: &ImmutableOrigin,
storage_type: StorageType,
tx: &Transaction,
) -> rusqlite::Result<StorageShelf> {
create_a_storage_shelf(shed, origin, storage_type, tx)
}
/// <https://storage.spec.whatwg.org/#storage-shelf>
///
/// A storage shelf exists for each storage key within a storage shed. It holds a bucket map, which
/// is a map of strings to storage buckets.
struct StorageShelf {
default_bucket_id: i64,
}
/// <https://storage.spec.whatwg.org/#obtain-a-local-storage-shelf>
///
/// To obtain a local storage shelf, given an environment settings object environment, return the
/// result of running obtain a storage shelf with the user agents storage shed, environment, and
/// "local".
fn obtain_a_local_storage_shelf(
origin: &ImmutableOrigin,
tx: &Transaction,
) -> Result<StorageShelf, String> {
if !origin.is_tuple() {
return Err("Storage is unavailable for opaque origins".to_owned());
}
let shed =
ensure_storage_shed(&StorageType::Local, None, tx).map_err(|error| error.to_string())?;
obtain_a_storage_shelf(shed, origin, StorageType::Local, tx).map_err(|error| error.to_string())
}
/// <https://storage.spec.whatwg.org/#bucket-mode>
///
/// A local storage bucket has a mode, which is "best-effort" or "persistent". It is initially
/// "best-effort".
fn bucket_mode(bucket_id: i64, tx: &Transaction) -> rusqlite::Result<Mode> {
let mode: String = tx.query_row(
"SELECT mode FROM buckets WHERE id = ?1;",
[bucket_id],
|row| row.get(0),
)?;
Ok(Mode::from_str(&mode).unwrap_or_default())
}
/// <https://storage.spec.whatwg.org/#dom-storagemanager-persist>
///
/// Set buckets mode to "persistent".
fn set_bucket_mode(bucket_id: i64, mode: Mode, tx: &Transaction) -> rusqlite::Result<()> {
tx.execute(
"UPDATE buckets SET mode = ?1, persisted = ?2 WHERE id = ?3;",
(mode.as_str(), matches!(mode, Mode::Persistent), bucket_id),
)?;
Ok(())
}
/// <https://storage.spec.whatwg.org/#storage-usage>
///
/// The storage usage of a storage shelf is an implementation-defined rough estimate of the amount
/// of bytes used by it.
///
/// This cannot be an exact amount as user agents might, and are encouraged to, use deduplication,
/// compression, and other techniques that obscure exactly how much bytes a storage shelf uses.
fn storage_usage_for_bucket(bucket_id: i64, tx: &Transaction) -> Result<u64, String> {
let mut stmt = tx
.prepare(
"SELECT directories.path
FROM directories
JOIN databases ON directories.database_id = databases.id
JOIN bottles ON databases.bottle_id = bottles.id
WHERE bottles.bucket_id = ?1;",
)
.map_err(|error| error.to_string())?;
let rows = stmt
.query_map([bucket_id], |row| row.get::<_, String>(0))
.map_err(|error| error.to_string())?;
let mut usage = 0_u64;
for path in rows {
usage += directory_size(&PathBuf::from(path.map_err(|error| error.to_string())?))?;
}
Ok(usage)
}
/// <https://storage.spec.whatwg.org/#storage-quota>
///
/// The storage quota of a storage shelf is an implementation-defined conservative estimate of the
/// total amount of bytes it can hold. This amount should be less than the total storage space on
/// the device. It must not be a function of the available storage space on the device.
///
/// User agents are strongly encouraged to consider navigation frequency, recency of visits,
/// bookmarking, and permission for "persistent-storage" when determining quotas.
///
/// Directly or indirectly revealing available storage space can lead to fingerprinting and leaking
/// information outside the scope of the origin involved.
fn storage_quota_for_bucket(_bucket_id: i64, _tx: &Transaction) -> Result<u64, String> {
Ok(STORAGE_SHELF_QUOTA_BYTES)
}
/// <https://storage.spec.whatwg.org/#storage-usage>
///
/// The storage usage of a storage shelf is an implementation-defined rough estimate of the amount
/// of bytes used by it.
fn directory_size(path: &PathBuf) -> Result<u64, String> {
let metadata = fs::metadata(path).map_err(|error| error.to_string())?;
if metadata.is_file() {
return Ok(metadata.len());
}
if !metadata.is_dir() {
return Ok(0);
}
let mut size = 0_u64;
for entry in fs::read_dir(path).map_err(|error| error.to_string())? {
let entry = entry.map_err(|error| error.to_string())?;
size += directory_size(&entry.path())?;
}
Ok(size)
}
impl RegistryEngine for SqliteEngine {
type Error = rusqlite::Error;
/// Create a database for the indexedDB endpoint.
fn create_database(
&mut self,
bottle_id: i64,
name: String,
) -> Result<PathBuf, ClientStorageErrorr<Self::Error>> {
let tx = self.connection.transaction()?;
let dir = Uuid::new_v4().to_string();
let cluster = dir.chars().last().unwrap();
let path = self
.base_dir
.join("bottles")
.join(cluster.to_string())
.join(dir);
let path_str = path.to_str().ok_or_else(|| {
ClientStorageErrorr::Internal(rusqlite::Error::InvalidParameterName(String::from(
"path",
)))
})?;
let database_id: i64 = tx
.query_row(
"INSERT INTO databases (bottle_id, name) VALUES (?1, ?2)
ON CONFLICT(bottle_id, name) DO NOTHING
RETURNING id;",
(bottle_id, name),
|row| row.get(0),
)
.map_err(|e| match e {
rusqlite::Error::QueryReturnedNoRows => ClientStorageErrorr::DatabaseAlreadyExists,
e => ClientStorageErrorr::Internal(e),
})?;
tx.execute(
"INSERT INTO directories (database_id, path) VALUES (?1, ?2);",
(database_id, path_str),
)?;
std::fs::create_dir_all(&path).map_err(|_| ClientStorageErrorr::DirectoryCreationFailed)?;
tx.commit()?;
Ok(path)
}
/// Delete a database for the indexedDB endpoint.
fn delete_database(
&mut self,
bottle_id: i64,
name: String,
) -> Result<(), ClientStorageErrorr<Self::Error>> {
let tx = self.connection.transaction()?;
let database_id: i64 = tx.query_row(
"SELECT id FROM databases WHERE bottle_id = ?1 AND name = ?2;",
(bottle_id, name.clone()),
|row| row.get(0),
)?;
let path: String = tx.query_row(
"SELECT path FROM directories WHERE database_id = ?1;",
[database_id],
|row| row.get(0),
)?;
tx.execute(
"DELETE FROM databases WHERE bottle_id = ?1 AND name = ?2;",
(bottle_id, name),
)?;
if tx.changes() == 0 {
return Err(ClientStorageErrorr::DatabaseDoesNotExist);
}
// Note: directory deleted through SQL cascade.
// Delete the directory on disk
std::fs::remove_dir_all(&path).map_err(|_| ClientStorageErrorr::DirectoryDeletionFailed)?;
tx.commit()?;
Ok(())
}
/// <https://storage.spec.whatwg.org/#obtain-a-storage-bottle-map>
fn obtain_a_storage_bottle_map(
&mut self,
storage_type: StorageType,
webview: WebViewId,
storage_identifier: StorageIdentifier,
origin: ImmutableOrigin,
sender: &GenericSender<ClientStorageThreadMessage>,
) -> Result<StorageProxyMap, ClientStorageErrorr<Self::Error>> {
let tx = self.connection.transaction()?;
// Step 1. Let shed be null.
let shed_id: i64 = match storage_type {
StorageType::Local => {
// Step 2. If type is "local", then set shed to the user agents storage shed.
ensure_storage_shed(&storage_type, None, &tx)?
},
StorageType::Session => {
// Step 3. Otherwise:
// Step 3.1. Assert: type is "session".
// Step 3.2. Set shed to environments global objects associated Documents node
// navigables traversable navigables storage shed.
ensure_storage_shed(
&storage_type,
Some(Into::<BrowsingContextId>::into(webview).to_string()),
&tx,
)?
},
};
// Step 4. Let shelf be the result of running obtain a storage shelf, with shed,
// environment, and type.
// Step 5. If shelf is failure, then return failure.
let shelf = obtain_a_storage_shelf(shed_id, &origin, storage_type, &tx)?;
// Step 6. Let bucket be shelfs bucket map["default"].
let bucket_id = shelf.default_bucket_id;
let bottle_id: i64 = tx.query_row(
"SELECT id FROM bottles WHERE bucket_id = ?1 AND identifier = ?2;",
(bucket_id, storage_identifier.as_str()),
|row| row.get(0),
)?;
tx.commit()?;
// Step 7. Let bottle be buckets bottle map[identifier].
// Step 8. Let proxyMap be a new storage proxy map whose backing map is bottles map.
// Step 9. Append proxyMap to bottles proxy map reference set.
// Step 10. Return proxyMap.
Ok(StorageProxyMap {
bottle_id,
handle: ClientStorageThreadHandle::new(sender.clone()),
})
}
fn persisted(&mut self, origin: ImmutableOrigin) -> Result<bool, String> {
let tx = self
.connection
.transaction()
.map_err(|error| error.to_string())?;
// <https://storage.spec.whatwg.org/#dom-storagemanager-persisted>
// Let shelf be the result of running obtain a local storage shelf with thiss relevant
// settings object.
let shelf = obtain_a_local_storage_shelf(&origin, &tx)?;
// Let persisted be true if shelfs bucket map["default"]'s mode is "persistent";
// otherwise false.
// It will be false when theres an internal error.
let persisted = bucket_mode(shelf.default_bucket_id, &tx)
.is_ok_and(|mode| mode == Mode::Persistent) &&
tx.commit().is_ok();
Ok(persisted)
}
fn persist(
&mut self,
origin: ImmutableOrigin,
permission_granted: bool,
) -> Result<bool, String> {
let tx = self
.connection
.transaction()
.map_err(|error| error.to_string())?;
// <https://storage.spec.whatwg.org/#dom-storagemanager-persist>
// Let shelf be the result of running obtain a local storage shelf with thiss relevant
// settings object.
let shelf = obtain_a_local_storage_shelf(&origin, &tx)?;
// Let bucket be shelfs bucket map["default"].
let bucket_id = shelf.default_bucket_id;
// Let persisted be true if buckets mode is "persistent"; otherwise false.
// It will be false when theres an internal error.
let mut persisted = bucket_mode(bucket_id, &tx).is_ok_and(|mode| mode == Mode::Persistent);
// If persisted is false and permission is "granted", then:
// Set buckets mode to "persistent".
// If there was no internal error, then set persisted to true.
if !persisted && permission_granted {
persisted = set_bucket_mode(bucket_id, Mode::Persistent, &tx).is_ok();
}
if tx.commit().is_err() {
persisted = false;
}
Ok(persisted)
}
fn estimate(&mut self, origin: ImmutableOrigin) -> Result<(u64, u64), String> {
let tx = self
.connection
.transaction()
.map_err(|error| error.to_string())?;
// <https://storage.spec.whatwg.org/#dom-storagemanager-estimate>
// Let shelf be the result of running obtain a local storage shelf with thiss relevant
// settings object.
let shelf = obtain_a_local_storage_shelf(&origin, &tx)?;
// Let usage be storage usage for shelf.
let usage = storage_usage_for_bucket(shelf.default_bucket_id, &tx)?;
// Let quota be storage quota for shelf.
let quota = storage_quota_for_bucket(shelf.default_bucket_id, &tx)?;
tx.commit().map_err(|error| error.to_string())?;
Ok((usage, quota))
}
}
pub trait ClientStorageThreadFactory {
fn new(config_dir: Option<PathBuf>, temporary_storage: bool) -> Self;
}
impl ClientStorageThreadFactory for ClientStorageThreadHandle {
fn new(config_dir: Option<PathBuf>, temporary_storage: bool) -> ClientStorageThreadHandle {
let (generic_sender, generic_receiver) = generic_channel::channel().unwrap();
let base_dir = config_dir
.unwrap_or_else(|| {
let tmp_dir = tempfile::tempdir().unwrap();
tmp_dir.path().to_path_buf()
})
.join("clientstorage");
let storage_dir = if temporary_storage {
let unique_id = uuid::Uuid::new_v4().to_string();
base_dir.join("temporary").join(unique_id)
} else {
base_dir.join("default_v1")
};
std::fs::create_dir_all(&storage_dir)
.expect("Failed to create ClientStorage storage directory");
let sender_clone = generic_sender.clone();
thread::Builder::new()
.name("ClientStorageThread".to_owned())
.spawn(move || {
let engine = SqliteEngine::new(storage_dir).unwrap_or_else(|error| {
warn!("Failed to initialize ClientStorage engine into storage dir: {error:?}");
SqliteEngine::memory().unwrap()
});
ClientStorageThread::new(sender_clone, generic_receiver, engine).start();
})
.expect("Thread spawning failed");
ClientStorageThreadHandle::new(generic_sender)
}
}
struct ClientStorageThread<E: RegistryEngine> {
receiver: GenericReceiver<ClientStorageThreadMessage>,
sender: GenericSender<ClientStorageThreadMessage>,
engine: E,
}
impl<E> ClientStorageThread<E>
where
E: RegistryEngine,
{
pub fn new(
sender: GenericSender<ClientStorageThreadMessage>,
receiver: GenericReceiver<ClientStorageThreadMessage>,
engine: E,
) -> ClientStorageThread<E> {
ClientStorageThread {
sender,
receiver,
engine,
}
}
pub fn start(&mut self) {
while let Ok(message) = self.receiver.recv() {
match message {
ClientStorageThreadMessage::ObtainBottleMap {
storage_type,
storage_identifier,
webview,
origin,
sender,
} => {
let result = self.engine.obtain_a_storage_bottle_map(
storage_type,
webview,
storage_identifier,
origin,
&self.sender,
);
let _ = sender.send(result.map_err(|e| format!("{:?}", e)));
},
ClientStorageThreadMessage::CreateDatabase {
bottle_id,
name,
sender,
} => {
let result = self.engine.create_database(bottle_id, name);
let _ = sender.send(result.map_err(|e| format!("{:?}", e)));
},
ClientStorageThreadMessage::DeleteDatabase {
bottle_id,
name,
sender,
} => {
let result = self.engine.delete_database(bottle_id, name);
let _ = sender.send(result.map_err(|e| format!("{:?}", e)));
},
ClientStorageThreadMessage::Persisted { origin, sender } => {
let _ = sender.send(self.engine.persisted(origin));
},
ClientStorageThreadMessage::Persist {
origin,
permission_granted,
sender,
} => {
let _ = sender.send(self.engine.persist(origin, permission_granted));
},
ClientStorageThreadMessage::Estimate { origin, sender } => {
let _ = sender.send(self.engine.estimate(origin));
},
ClientStorageThreadMessage::Exit(sender) => {
let _ = sender.send(());
break;
},
}
}
}
}