Skip to main content

storage/
client_storage.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4use std::fmt::Debug;
5use std::path::PathBuf;
6use std::str::FromStr;
7use std::{fs, thread};
8
9use log::warn;
10use rusqlite::{Connection, OptionalExtension, Transaction};
11use servo_base::generic_channel::{self, GenericReceiver, GenericSender};
12use servo_base::id::{BrowsingContextId, WebViewId};
13use servo_url::ImmutableOrigin;
14use storage_traits::client_storage::{
15    ClientStorageErrorr, ClientStorageThreadHandle, ClientStorageThreadMessage, Mode,
16    StorageIdentifier, StorageProxyMap, StorageType,
17};
18use uuid::Uuid;
19
20/// <https://storage.spec.whatwg.org/#storage-quota>
21/// The storage quota of a storage shelf is an implementation-defined conservative estimate of the
22/// total amount of byttes it can hold. We use 10 GiB per shelf, matching Firefox's documented
23/// limit (<https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria>).
24const STORAGE_SHELF_QUOTA_BYTES: u64 = 10 * 1024 * 1024 * 1024;
25
26trait RegistryEngine {
27    type Error: Debug;
28    fn create_database(
29        &mut self,
30        bottle_id: i64,
31        name: String,
32    ) -> Result<(PathBuf, bool), ClientStorageErrorr<Self::Error>>;
33    fn delete_database(
34        &mut self,
35        bottle_id: i64,
36        name: String,
37    ) -> Result<(), ClientStorageErrorr<Self::Error>>;
38    fn obtain_a_storage_bottle_map(
39        &mut self,
40        storage_type: StorageType,
41        webview: Option<WebViewId>,
42        storage_identifier: StorageIdentifier,
43        origin: ImmutableOrigin,
44        sender: &GenericSender<ClientStorageThreadMessage>,
45    ) -> Result<StorageProxyMap, ClientStorageErrorr<Self::Error>>;
46    fn persisted(&mut self, origin: ImmutableOrigin) -> Result<bool, String>;
47    fn persist(
48        &mut self,
49        origin: ImmutableOrigin,
50        permission_granted: bool,
51    ) -> Result<bool, String>;
52    fn estimate(&mut self, origin: ImmutableOrigin) -> Result<(u64, u64), String>;
53}
54
55struct SqliteEngine {
56    connection: Connection,
57    base_dir: PathBuf,
58}
59
60impl SqliteEngine {
61    fn new(base_dir: PathBuf) -> rusqlite::Result<Self> {
62        let db_path = base_dir.join("reg.sqlite");
63        let connection = Connection::open(db_path)?;
64        Self::init(&connection)?;
65        Ok(SqliteEngine {
66            connection,
67            base_dir,
68        })
69    }
70
71    fn memory() -> rusqlite::Result<Self> {
72        let connection = Connection::open_in_memory()?;
73        Self::init(&connection)?;
74        Ok(SqliteEngine {
75            connection,
76            base_dir: PathBuf::new(),
77        })
78    }
79
80    fn init(connection: &Connection) -> rusqlite::Result<()> {
81        connection.execute(r#"PRAGMA foreign_keys = ON;"#, [])?;
82        connection.execute(
83            r#"CREATE TABLE IF NOT EXISTS sheds (
84            id INTEGER PRIMARY KEY,
85            storage_type TEXT NOT NULL,
86            browsing_context TEXT
87        );"#,
88            [],
89        )?;
90
91        // Note: indices required for ON CONFLICT to work.
92        connection.execute(
93            r#"CREATE UNIQUE INDEX IF NOT EXISTS idx_sheds_local
94        ON sheds(storage_type) WHERE browsing_context IS NULL;"#,
95            [],
96        )?;
97        connection.execute(
98            r#"CREATE UNIQUE INDEX IF NOT EXISTS idx_sheds_session
99        ON sheds(browsing_context) WHERE browsing_context IS NOT NULL;"#,
100            [],
101        )?;
102
103        connection.execute(
104            r#"CREATE TABLE IF NOT EXISTS shelves (
105            id INTEGER PRIMARY KEY,
106            shed_id INTEGER NOT NULL,
107            origin TEXT NOT NULL,
108            UNIQUE (shed_id, origin),
109            FOREIGN KEY (shed_id) REFERENCES sheds(id) ON DELETE CASCADE
110        );"#,
111            [],
112        )?;
113
114        // Note: name is to support https://wicg.github.io/storage-buckets/
115        connection.execute(
116            r#"CREATE TABLE IF NOT EXISTS buckets (
117            id INTEGER PRIMARY KEY,
118            shelf_id INTEGER NOT NULL UNIQUE,
119            persisted BOOLEAN DEFAULT 0,
120            name TEXT,
121            mode TEXT,
122            expires DATETIME,
123            FOREIGN KEY (shelf_id) REFERENCES shelves(id) ON DELETE CASCADE
124        );"#,
125            [],
126        )?;
127
128        // Note: quota not in db, hardcoded at https://storage.spec.whatwg.org/#storage-endpoint-quota
129        connection.execute(
130            r#"CREATE TABLE IF NOT EXISTS bottles (
131                    id INTEGER PRIMARY KEY,
132                    bucket_id INTEGER NOT NULL,
133                    identifier TEXT NOT NULL,  -- "idb", "ls", "opfs", "cache"
134                    UNIQUE (bucket_id, identifier),
135                    FOREIGN KEY (bucket_id) REFERENCES buckets(id) ON DELETE CASCADE
136                );"#,
137            [],
138        )?;
139
140        connection.execute(
141            r#"CREATE TABLE IF NOT EXISTS databases (
142                    id INTEGER PRIMARY KEY,
143                    bottle_id INTEGER NOT NULL,
144                    name TEXT NOT NULL,
145                    UNIQUE (bottle_id, name),
146                    FOREIGN KEY (bottle_id) REFERENCES bottles(id) ON DELETE CASCADE
147                );
148                "#,
149            [],
150        )?;
151
152        connection.execute(
153            r#"CREATE TABLE IF NOT EXISTS directories (
154                id INTEGER PRIMARY KEY,
155                database_id INTEGER NOT NULL UNIQUE,
156                path TEXT NOT NULL,
157                FOREIGN KEY (database_id) REFERENCES databases(id) ON DELETE CASCADE
158            );"#,
159            [],
160        )?;
161
162        connection.execute_batch(
163            r#"
164                CREATE UNIQUE INDEX IF NOT EXISTS sheds_local_identity_idx
165                ON sheds(storage_type)
166                WHERE storage_type = 'local' AND browsing_context IS NULL;
167
168                CREATE UNIQUE INDEX IF NOT EXISTS sheds_session_identity_idx
169                ON sheds(storage_type, browsing_context)
170                WHERE storage_type = 'session' AND browsing_context IS NOT NULL;
171
172                CREATE UNIQUE INDEX IF NOT EXISTS shelves_origin_shed_identity_idx
173                ON shelves(origin, shed_id);
174            "#,
175        )?;
176        // TODO: Delete expired and non-persistent buckets on startup
177        Ok(())
178    }
179}
180
181fn ensure_storage_shed(
182    storage_type: &StorageType,
183    browsing_context: Option<String>,
184    tx: &Transaction,
185) -> rusqlite::Result<i64> {
186    match browsing_context {
187        Some(browsing_context) => {
188            tx.execute(
189                "INSERT INTO sheds (storage_type, browsing_context) VALUES (?1, ?2) ON CONFLICT DO NOTHING;",
190                (storage_type.as_str(), browsing_context.as_str()),
191            )?;
192
193            tx.query_row(
194                "SELECT id FROM sheds WHERE storage_type = ?1 AND browsing_context = ?2;",
195                (storage_type.as_str(), browsing_context.as_str()),
196                |row| row.get(0),
197            )
198        },
199        None => {
200            tx.execute(
201                "INSERT INTO sheds (storage_type, browsing_context) VALUES (?1, NULL) ON CONFLICT DO NOTHING;",
202                [storage_type.as_str()],
203            )?;
204
205            tx.query_row(
206                "SELECT id FROM sheds WHERE storage_type = ?1 AND browsing_context IS NULL;",
207                [storage_type.as_str()],
208                |row| row.get(0),
209            )
210        },
211    }
212}
213
214/// <https://storage.spec.whatwg.org/#create-a-storage-bucket>
215fn create_a_storage_bucket(
216    shelf_id: i64,
217    storage_type: StorageType,
218    tx: &Transaction,
219) -> rusqlite::Result<i64> {
220    // Step 1. Let bucket be null.
221    // Step 2. If type is "local", then set bucket to a new local storage bucket.
222    let bucket_id: i64 = if let StorageType::Local = storage_type {
223        tx.query_row(
224            "INSERT INTO buckets (mode, shelf_id) VALUES (?1, ?2)
225             ON CONFLICT(shelf_id) DO UPDATE SET shelf_id = excluded.shelf_id
226             RETURNING id;",
227            [Mode::default().as_str(), &shelf_id.to_string()],
228            |row| row.get(0),
229        )?
230    } else {
231        // Step 3. Otherwise:
232        // Step 3.1. Assert: type is "session".
233        // Step 3.2. Set bucket to a new session storage bucket.
234        tx.query_row(
235            "INSERT INTO buckets (shelf_id) VALUES (?1)
236             ON CONFLICT(shelf_id) DO UPDATE SET shelf_id = excluded.shelf_id
237             RETURNING id;",
238            [&shelf_id.to_string()],
239            |row| row.get(0),
240        )?
241    };
242
243    // Step 4. For each endpoint of registered storage endpoints whose types contain type,
244    // set bucket’s bottle map[endpoint’s identifier] to
245    // a new storage bottle whose quota is endpoint’s quota.
246
247    // <https://storage.spec.whatwg.org/#registered-storage-endpoints>
248    let registered_endpoints = match storage_type {
249        StorageType::Local => vec![
250            StorageIdentifier::Caches,
251            StorageIdentifier::IndexedDB,
252            StorageIdentifier::LocalStorage,
253            StorageIdentifier::ServiceWorkerRegistrations,
254        ],
255        StorageType::Session => vec![StorageIdentifier::SessionStorage],
256    };
257
258    for identifier in registered_endpoints {
259        tx.execute(
260            "INSERT INTO bottles (bucket_id, identifier) VALUES (?1, ?2)
261             ON CONFLICT(bucket_id, identifier) DO NOTHING;",
262            (bucket_id, identifier.as_str()),
263        )?;
264    }
265
266    // Step 5. Return bucket.
267    Ok(bucket_id)
268}
269
270/// <https://storage.spec.whatwg.org/#create-a-storage-shelf>
271fn create_a_storage_shelf(
272    shed: i64,
273    origin: &ImmutableOrigin,
274    storage_type: StorageType,
275    tx: &Transaction,
276) -> rusqlite::Result<StorageShelf> {
277    // To create a storage shelf, given a storage type type, run these steps:
278    // Step 1. Let shelf be a new storage shelf.
279    // Step 2.  Set shelf’s bucket map["default"] to the result of running create a storage bucket with type.
280    let shelf_id: i64 = tx.query_row(
281        "INSERT INTO shelves (shed_id, origin) VALUES (?1, ?2)
282         ON CONFLICT(shed_id, origin) DO UPDATE SET origin = excluded.origin
283         RETURNING id;",
284        [&shed.to_string(), &origin.ascii_serialization()],
285        |row| row.get(0),
286    )?;
287
288    // Step 3. Return shelf.
289    Ok(StorageShelf {
290        default_bucket_id: create_a_storage_bucket(shelf_id, storage_type, tx)?,
291    })
292}
293
294/// <https://storage.spec.whatwg.org/#obtain-a-storage-shelf>
295fn obtain_a_storage_shelf(
296    shed: i64,
297    origin: &ImmutableOrigin,
298    storage_type: StorageType,
299    tx: &Transaction,
300) -> rusqlite::Result<StorageShelf> {
301    create_a_storage_shelf(shed, origin, storage_type, tx)
302}
303
304/// <https://storage.spec.whatwg.org/#storage-shelf>
305///
306/// A storage shelf exists for each storage key within a storage shed. It holds a bucket map, which
307/// is a map of strings to storage buckets.
308struct StorageShelf {
309    default_bucket_id: i64,
310}
311
312/// <https://storage.spec.whatwg.org/#obtain-a-local-storage-shelf>
313///
314/// To obtain a local storage shelf, given an environment settings object environment, return the
315/// result of running obtain a storage shelf with the user agent’s storage shed, environment, and
316/// "local".
317fn obtain_a_local_storage_shelf(
318    origin: &ImmutableOrigin,
319    tx: &Transaction,
320) -> Result<StorageShelf, String> {
321    if !origin.is_tuple() {
322        return Err("Storage is unavailable for opaque origins".to_owned());
323    }
324
325    let shed =
326        ensure_storage_shed(&StorageType::Local, None, tx).map_err(|error| error.to_string())?;
327    obtain_a_storage_shelf(shed, origin, StorageType::Local, tx).map_err(|error| error.to_string())
328}
329
330/// <https://storage.spec.whatwg.org/#bucket-mode>
331///
332/// A local storage bucket has a mode, which is "best-effort" or "persistent". It is initially
333/// "best-effort".
334fn bucket_mode(bucket_id: i64, tx: &Transaction) -> rusqlite::Result<Mode> {
335    let mode: String = tx.query_row(
336        "SELECT mode FROM buckets WHERE id = ?1;",
337        [bucket_id],
338        |row| row.get(0),
339    )?;
340    Ok(Mode::from_str(&mode).unwrap_or_default())
341}
342
343/// <https://storage.spec.whatwg.org/#dom-storagemanager-persist>
344///
345/// Set bucket’s mode to "persistent".
346fn set_bucket_mode(bucket_id: i64, mode: Mode, tx: &Transaction) -> rusqlite::Result<()> {
347    tx.execute(
348        "UPDATE buckets SET mode = ?1, persisted = ?2 WHERE id = ?3;",
349        (mode.as_str(), matches!(mode, Mode::Persistent), bucket_id),
350    )?;
351    Ok(())
352}
353
354/// <https://storage.spec.whatwg.org/#storage-usage>
355///
356/// The storage usage of a storage shelf is an implementation-defined rough estimate of the amount
357/// of bytes used by it.
358///
359/// This cannot be an exact amount as user agents might, and are encouraged to, use deduplication,
360/// compression, and other techniques that obscure exactly how much bytes a storage shelf uses.
361fn storage_usage_for_bucket(bucket_id: i64, tx: &Transaction) -> Result<u64, String> {
362    let mut stmt = tx
363        .prepare(
364            "SELECT directories.path
365             FROM directories
366             JOIN databases ON directories.database_id = databases.id
367             JOIN bottles ON databases.bottle_id = bottles.id
368             WHERE bottles.bucket_id = ?1;",
369        )
370        .map_err(|error| error.to_string())?;
371
372    let rows = stmt
373        .query_map([bucket_id], |row| row.get::<_, String>(0))
374        .map_err(|error| error.to_string())?;
375
376    let mut usage = 0_u64;
377    for path in rows {
378        usage += directory_size(&PathBuf::from(path.map_err(|error| error.to_string())?))?;
379    }
380    Ok(usage)
381}
382
383/// <https://storage.spec.whatwg.org/#storage-quota>
384///
385/// The storage quota of a storage shelf is an implementation-defined conservative estimate of the
386/// total amount of bytes it can hold. This amount should be less than the total storage space on
387/// the device. It must not be a function of the available storage space on the device.
388///
389/// User agents are strongly encouraged to consider navigation frequency, recency of visits,
390/// bookmarking, and permission for "persistent-storage" when determining quotas.
391///
392/// Directly or indirectly revealing available storage space can lead to fingerprinting and leaking
393/// information outside the scope of the origin involved.
394fn storage_quota_for_bucket(_bucket_id: i64, _tx: &Transaction) -> Result<u64, String> {
395    Ok(STORAGE_SHELF_QUOTA_BYTES)
396}
397
398/// <https://storage.spec.whatwg.org/#storage-usage>
399///
400/// The storage usage of a storage shelf is an implementation-defined rough estimate of the amount
401/// of bytes used by it.
402fn directory_size(path: &PathBuf) -> Result<u64, String> {
403    let metadata = fs::metadata(path).map_err(|error| error.to_string())?;
404    if metadata.is_file() {
405        return Ok(metadata.len());
406    }
407
408    if !metadata.is_dir() {
409        return Ok(0);
410    }
411
412    let mut size = 0_u64;
413    for entry in fs::read_dir(path).map_err(|error| error.to_string())? {
414        let entry = entry.map_err(|error| error.to_string())?;
415        size += directory_size(&entry.path())?;
416    }
417    Ok(size)
418}
419
420impl RegistryEngine for SqliteEngine {
421    type Error = rusqlite::Error;
422
423    /// Create a database for the indexedDB endpoint.
424    fn create_database(
425        &mut self,
426        bottle_id: i64,
427        name: String,
428    ) -> Result<(PathBuf, bool), ClientStorageErrorr<Self::Error>> {
429        let tx = self.connection.transaction()?;
430
431        // TODO: combine this into a single query (utilizing WITH and a join).
432        let database_id: i64 = tx
433            .query_row(
434                "INSERT INTO databases (bottle_id, name) VALUES (?1, ?2)
435             ON CONFLICT(bottle_id, name) DO UPDATE SET name = excluded.name
436            RETURNING id;",
437                (bottle_id, name),
438                |row| row.get(0),
439            )
440            .map_err(ClientStorageErrorr::Internal)?;
441
442        let existing_path: Option<String> = tx
443            .query_row(
444                "SELECT path FROM directories WHERE database_id = ?1;",
445                [database_id],
446                |row| row.get(0),
447            )
448            .optional()
449            .map_err(ClientStorageErrorr::Internal)?;
450
451        if let Some(p) = existing_path {
452            // If it exists, we don't need the transaction anymore
453            return Ok((PathBuf::from(p), false));
454        }
455
456        let dir = Uuid::new_v4().to_string();
457        let cluster = dir.chars().last().unwrap();
458        let path = self
459            .base_dir
460            .join("bottles")
461            .join(cluster.to_string())
462            .join(dir);
463
464        let path_str = path.to_str().ok_or_else(|| {
465            ClientStorageErrorr::Internal(rusqlite::Error::InvalidParameterName(String::from(
466                "path",
467            )))
468        })?;
469
470        tx.execute(
471            "INSERT INTO directories (database_id, path) VALUES (?1, ?2);",
472            (database_id, path_str),
473        )
474        .map_err(ClientStorageErrorr::Internal)?;
475
476        tx.commit().map_err(ClientStorageErrorr::Internal)?;
477
478        std::fs::create_dir_all(&path).map_err(|_| ClientStorageErrorr::DirectoryCreationFailed)?;
479
480        Ok((path, true))
481    }
482
483    /// Delete a database for the indexedDB endpoint.
484    fn delete_database(
485        &mut self,
486        bottle_id: i64,
487        name: String,
488    ) -> Result<(), ClientStorageErrorr<Self::Error>> {
489        let tx = self.connection.transaction()?;
490
491        let database_id: i64 = tx.query_row(
492            "SELECT id FROM databases WHERE bottle_id = ?1 AND name = ?2;",
493            (bottle_id, name.clone()),
494            |row| row.get(0),
495        )?;
496
497        let path: String = tx.query_row(
498            "SELECT path FROM directories WHERE database_id = ?1;",
499            [database_id],
500            |row| row.get(0),
501        )?;
502
503        tx.execute(
504            "DELETE FROM databases WHERE bottle_id = ?1 AND name = ?2;",
505            (bottle_id, name),
506        )?;
507
508        if tx.changes() == 0 {
509            return Err(ClientStorageErrorr::DatabaseDoesNotExist);
510        }
511        // Note: directory deleted through SQL cascade.
512
513        tx.commit()?;
514
515        // Delete the directory on disk.
516        // Note: on Windows this needs to be done outside of the transaction,
517        // because the transaction holds a file lock.
518        std::fs::remove_dir_all(&path).map_err(|_| ClientStorageErrorr::DirectoryDeletionFailed)?;
519
520        Ok(())
521    }
522
523    /// <https://storage.spec.whatwg.org/#obtain-a-storage-bottle-map>
524    fn obtain_a_storage_bottle_map(
525        &mut self,
526        storage_type: StorageType,
527        webview: Option<WebViewId>,
528        storage_identifier: StorageIdentifier,
529        origin: ImmutableOrigin,
530        sender: &GenericSender<ClientStorageThreadMessage>,
531    ) -> Result<StorageProxyMap, ClientStorageErrorr<Self::Error>> {
532        let tx = self.connection.transaction()?;
533
534        // Step 1. Let shed be null.
535        let shed_id: i64 = match storage_type {
536            StorageType::Local => {
537                // Step 2. If type is "local", then set shed to the user agent’s storage shed.
538                ensure_storage_shed(&storage_type, None, &tx)?
539            },
540            StorageType::Session => {
541                // Step 3: Otherwise:
542                // Step 3.1: Assert: type is "session".
543                let Some(webview) = webview else {
544                    debug_assert!(false, "Session storage is only available on Window.");
545                    return Err(ClientStorageErrorr::SessionStorageRequiresWindow);
546                };
547
548                // Step 3.2: Set shed to environment’s global object’s associated Document’s
549                // node navigable’s traversable navigable’s storage shed.
550                // Note: using the browsing context of the webview as the traversable navigable.
551                ensure_storage_shed(
552                    &storage_type,
553                    Some(Into::<BrowsingContextId>::into(webview).to_string()),
554                    &tx,
555                )?
556            },
557        };
558
559        // Step 4. Let shelf be the result of running obtain a storage shelf, with shed,
560        // environment, and type.
561        // Step 5. If shelf is failure, then return failure.
562        let shelf = obtain_a_storage_shelf(shed_id, &origin, storage_type, &tx)?;
563
564        // Step 6. Let bucket be shelf’s bucket map["default"].
565        let bucket_id = shelf.default_bucket_id;
566
567        let bottle_id: i64 = tx.query_row(
568            "SELECT id FROM bottles WHERE bucket_id = ?1 AND identifier = ?2;",
569            (bucket_id, storage_identifier.as_str()),
570            |row| row.get(0),
571        )?;
572
573        tx.commit()?;
574
575        // Step 7. Let bottle be bucket’s bottle map[identifier].
576
577        // Step 8. Let proxyMap be a new storage proxy map whose backing map is bottle’s map.
578        // Step 9. Append proxyMap to bottle’s proxy map reference set.
579        // Step 10. Return proxyMap.
580        Ok(StorageProxyMap {
581            bottle_id,
582            handle: ClientStorageThreadHandle::new(sender.clone()),
583        })
584    }
585
586    fn persisted(&mut self, origin: ImmutableOrigin) -> Result<bool, String> {
587        let tx = self
588            .connection
589            .transaction()
590            .map_err(|error| error.to_string())?;
591
592        // <https://storage.spec.whatwg.org/#dom-storagemanager-persisted>
593        // Let shelf be the result of running obtain a local storage shelf with this’s relevant
594        // settings object.
595        let shelf = obtain_a_local_storage_shelf(&origin, &tx)?;
596
597        // Let persisted be true if shelf’s bucket map["default"]'s mode is "persistent";
598        // otherwise false.
599        // It will be false when there’s an internal error.
600        let persisted = bucket_mode(shelf.default_bucket_id, &tx)
601            .is_ok_and(|mode| mode == Mode::Persistent) &&
602            tx.commit().is_ok();
603
604        Ok(persisted)
605    }
606
607    fn persist(
608        &mut self,
609        origin: ImmutableOrigin,
610        permission_granted: bool,
611    ) -> Result<bool, String> {
612        let tx = self
613            .connection
614            .transaction()
615            .map_err(|error| error.to_string())?;
616
617        // <https://storage.spec.whatwg.org/#dom-storagemanager-persist>
618        // Let shelf be the result of running obtain a local storage shelf with this’s relevant
619        // settings object.
620        let shelf = obtain_a_local_storage_shelf(&origin, &tx)?;
621
622        // Let bucket be shelf’s bucket map["default"].
623        let bucket_id = shelf.default_bucket_id;
624
625        // Let persisted be true if bucket’s mode is "persistent"; otherwise false.
626        // It will be false when there’s an internal error.
627        let mut persisted = bucket_mode(bucket_id, &tx).is_ok_and(|mode| mode == Mode::Persistent);
628
629        // If persisted is false and permission is "granted", then:
630        // Set bucket’s mode to "persistent".
631        // If there was no internal error, then set persisted to true.
632        if !persisted && permission_granted {
633            persisted = set_bucket_mode(bucket_id, Mode::Persistent, &tx).is_ok();
634        }
635
636        if tx.commit().is_err() {
637            persisted = false;
638        }
639
640        Ok(persisted)
641    }
642
643    fn estimate(&mut self, origin: ImmutableOrigin) -> Result<(u64, u64), String> {
644        let tx = self
645            .connection
646            .transaction()
647            .map_err(|error| error.to_string())?;
648
649        // <https://storage.spec.whatwg.org/#dom-storagemanager-estimate>
650        // Let shelf be the result of running obtain a local storage shelf with this’s relevant
651        // settings object.
652        let shelf = obtain_a_local_storage_shelf(&origin, &tx)?;
653
654        // Let usage be storage usage for shelf.
655        let usage = storage_usage_for_bucket(shelf.default_bucket_id, &tx)?;
656        // Let quota be storage quota for shelf.
657        let quota = storage_quota_for_bucket(shelf.default_bucket_id, &tx)?;
658
659        tx.commit().map_err(|error| error.to_string())?;
660
661        Ok((usage, quota))
662    }
663}
664
665pub trait ClientStorageThreadFactory {
666    fn new(config_dir: Option<PathBuf>, temporary_storage: bool) -> Self;
667}
668
669impl ClientStorageThreadFactory for ClientStorageThreadHandle {
670    fn new(config_dir: Option<PathBuf>, temporary_storage: bool) -> ClientStorageThreadHandle {
671        let (generic_sender, generic_receiver) = generic_channel::channel().unwrap();
672        let mut temp_dir: Option<tempfile::TempDir> = None;
673        let base_dir = config_dir
674            .unwrap_or_else(|| {
675                let tmp_dir = tempfile::tempdir().unwrap();
676                let path = tmp_dir.path().to_path_buf();
677                temp_dir = Some(tmp_dir);
678                path
679            })
680            .join("clientstorage");
681        let storage_dir = if temporary_storage {
682            let unique_id = uuid::Uuid::new_v4().to_string();
683            base_dir.join("temporary").join(unique_id)
684        } else {
685            base_dir.join("default_v1")
686        };
687        std::fs::create_dir_all(&storage_dir)
688            .expect("Failed to create ClientStorage storage directory");
689        let sender_clone = generic_sender.clone();
690        thread::Builder::new()
691            .name("ClientStorageThread".to_owned())
692            .spawn(move || {
693                // Keep temp_dir alive while the thread runs.
694                let _ = temp_dir;
695                let engine = SqliteEngine::new(storage_dir).unwrap_or_else(|error| {
696                    warn!("Failed to initialize ClientStorage engine into storage dir: {error:?}");
697                    SqliteEngine::memory().unwrap()
698                });
699                ClientStorageThread::new(sender_clone, generic_receiver, engine).start();
700            })
701            .expect("Thread spawning failed");
702
703        ClientStorageThreadHandle::new(generic_sender)
704    }
705}
706
707struct ClientStorageThread<E: RegistryEngine> {
708    receiver: GenericReceiver<ClientStorageThreadMessage>,
709    sender: GenericSender<ClientStorageThreadMessage>,
710    engine: E,
711}
712
713impl<E> ClientStorageThread<E>
714where
715    E: RegistryEngine,
716{
717    pub fn new(
718        sender: GenericSender<ClientStorageThreadMessage>,
719        receiver: GenericReceiver<ClientStorageThreadMessage>,
720        engine: E,
721    ) -> ClientStorageThread<E> {
722        ClientStorageThread {
723            sender,
724            receiver,
725            engine,
726        }
727    }
728
729    pub fn start(&mut self) {
730        while let Ok(message) = self.receiver.recv() {
731            match message {
732                ClientStorageThreadMessage::ObtainBottleMap {
733                    storage_type,
734                    storage_identifier,
735                    webview,
736                    origin,
737                    sender,
738                } => {
739                    let result = self.engine.obtain_a_storage_bottle_map(
740                        storage_type,
741                        webview,
742                        storage_identifier,
743                        origin,
744                        &self.sender,
745                    );
746                    let _ = sender.send(result.map_err(|e| format!("{:?}", e)));
747                },
748                ClientStorageThreadMessage::CreateDatabase {
749                    bottle_id,
750                    name,
751                    sender,
752                } => {
753                    let result = self.engine.create_database(bottle_id, name);
754                    let _ = sender.send(result.map_err(|e| format!("{:?}", e)));
755                },
756                ClientStorageThreadMessage::DeleteDatabase {
757                    bottle_id,
758                    name,
759                    sender,
760                } => {
761                    let result = self.engine.delete_database(bottle_id, name);
762                    let _ = sender.send(result.map_err(|e| format!("{:?}", e)));
763                },
764                ClientStorageThreadMessage::Persisted { origin, sender } => {
765                    let _ = sender.send(self.engine.persisted(origin));
766                },
767                ClientStorageThreadMessage::Persist {
768                    origin,
769                    permission_granted,
770                    sender,
771                } => {
772                    let _ = sender.send(self.engine.persist(origin, permission_granted));
773                },
774                ClientStorageThreadMessage::Estimate { origin, sender } => {
775                    let _ = sender.send(self.engine.estimate(origin));
776                },
777                ClientStorageThreadMessage::Exit(sender) => {
778                    let _ = sender.send(());
779                    break;
780                },
781            }
782        }
783    }
784}