Add active profile local data export

This commit is contained in:
2026-05-09 08:23:37 -04:00
parent 2b6de367b0
commit 8b9c4579c1
17 changed files with 1209 additions and 90 deletions
+3
View File
@@ -24,6 +24,9 @@ pub enum CoreError {
#[error("invalid .elybookmarks package: {reason}")]
InvalidBookmarkPackage { reason: String },
#[error("invalid .elydata package: {reason}")]
InvalidLocalDataPackage { reason: String },
#[error("trashed space not found: {id}")]
TrashedSpaceNotFound { id: SpaceId },
+4 -4
View File
@@ -5,8 +5,8 @@ mod state;
pub use error::CoreError;
pub use state::{
BookmarkImportSummary, BrowserCore, BrowserSnapshot, ELYBOOKMARKS_FILE_EXTENSION,
ELYBOOKMARKS_SCHEMA_VERSION, ELYSPACE_FILE_EXTENSION, ELYSPACE_SCHEMA_VERSION,
ElyBookmarksPackage, ElySpacePackage, InitialBrowserConfig, InstalledPlugin,
LocalDataInventory, PluginAuditAction, PluginAuditEvent, SiteDataClearance,
SpaceImportProfileMapping, TrashedSpace,
ELYBOOKMARKS_SCHEMA_VERSION, ELYDATA_FILE_EXTENSION, ELYDATA_SCHEMA_VERSION,
ELYSPACE_FILE_EXTENSION, ELYSPACE_SCHEMA_VERSION, ElyBookmarksPackage, ElyLocalDataPackage,
ElySpacePackage, InitialBrowserConfig, InstalledPlugin, LocalDataInventory, PluginAuditAction,
PluginAuditEvent, SiteDataClearance, SpaceImportProfileMapping, TrashedSpace,
};
+4 -1
View File
@@ -345,12 +345,15 @@ const SETTINGS_ROUTE_MATCHES: &[SettingsRouteMatch] = &[
"history recording",
"diagnostics",
"diagnostic reporting",
"export local data",
"export privacy data",
],
search_terms: &[
"Privacy & Security",
"History recording, diagnostics, and profile-scoped privacy controls.",
"History recording, diagnostics, local data export, and profile-scoped privacy controls.",
"profile privacy",
"recording policy",
"local data export",
],
},
SettingsRouteMatch {
+3
View File
@@ -17,6 +17,8 @@ mod commands;
mod diagnostics;
mod downloads;
mod history;
mod local_data_export_records;
mod local_data_exports;
mod notes;
mod plugins;
mod privacy;
@@ -39,6 +41,7 @@ pub use bookmarks::{
BookmarkImportSummary, ELYBOOKMARKS_FILE_EXTENSION, ELYBOOKMARKS_SCHEMA_VERSION,
ElyBookmarksPackage,
};
pub use local_data_exports::{ELYDATA_FILE_EXTENSION, ELYDATA_SCHEMA_VERSION, ElyLocalDataPackage};
pub use plugins::{InstalledPlugin, PluginAuditAction, PluginAuditEvent};
pub use privacy::LocalDataInventory;
pub use site_data::SiteDataClearance;
@@ -313,6 +313,16 @@ impl BrowserCore {
self.open_tab(space_settings_url()?);
Ok(true)
}
"export-local-data"
| "export local data"
| "export-privacy-data"
| "export privacy data" => {
let Some(url) = settings_page_url("privacy")? else {
return Ok(false);
};
self.open_tab(url);
Ok(true)
}
"site-settings" | "open-site-settings" | "open site settings" => {
let Some(url) = self.active_tab_site_settings_url()? else {
return Ok(false);
@@ -0,0 +1,482 @@
use std::time::{SystemTime, UNIX_EPOCH};
use ely_domain::{
ArchiveSource, ArchivedTab, BookmarkEntry, BrowserTab, DownloadChecksum, DownloadDestination,
DownloadEntry, DownloadSecurity, DownloadState, HistoryEntry, NoteEntry, NoteTarget, Profile,
ProfileKind, ReadingListEntry, ReadingProgress, SitePermissionAuditAction,
SitePermissionAuditEvent, SitePermissionEntry, TabFlags, TabState,
};
use serde::{Deserialize, Serialize};
use super::LocalDataInventory;
use crate::CoreError;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ElyLocalDataPackage {
pub(super) version: u16,
pub(super) exported_at_unix_seconds: u64,
pub(super) profile: ElyLocalProfileRecord,
pub(super) inventory: LocalDataInventory,
pub(super) open_tabs: Vec<ElyLocalTabRecord>,
pub(super) archived_tabs: Vec<ElyLocalArchivedTabRecord>,
pub(super) bookmarks: Vec<ElyLocalBookmarkRecord>,
pub(super) notes: Vec<ElyLocalNoteRecord>,
pub(super) reading_list: Vec<ElyLocalReadingListRecord>,
pub(super) history: Vec<ElyLocalHistoryRecord>,
pub(super) downloads: Vec<ElyLocalDownloadRecord>,
pub(super) site_permissions: Vec<ElyLocalSitePermissionRecord>,
pub(super) site_permission_audit_events: Vec<ElyLocalSitePermissionAuditRecord>,
}
impl ElyLocalDataPackage {
#[must_use]
pub fn version(&self) -> u16 {
self.version
}
#[must_use]
pub fn profile_id(&self) -> &str {
&self.profile.id
}
#[must_use]
pub fn profile_name(&self) -> &str {
&self.profile.name
}
#[must_use]
pub fn inventory(&self) -> LocalDataInventory {
self.inventory
}
pub(super) fn unix_seconds(time: SystemTime) -> Result<u64, CoreError> {
time.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.map_err(|error| CoreError::InvalidLocalDataPackage { reason: error.to_string() })
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ElyLocalProfileRecord {
id: String,
name: String,
kind: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ElyLocalTabRecord {
id: String,
space_id: String,
space_name: String,
title: String,
url: String,
favicon_key: Option<String>,
parent_tab_id: Option<String>,
state: String,
flags: ElyLocalTabFlagsRecord,
group_id: Option<String>,
split_id: Option<String>,
sort_key: u64,
sync_enabled: bool,
created_at_unix_seconds: u64,
last_active_at_unix_seconds: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
struct ElyLocalTabFlagsRecord {
pinned: bool,
favorite: bool,
muted: bool,
unread: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ElyLocalArchivedTabRecord {
tab: ElyLocalTabRecord,
archived_at_unix_seconds: u64,
source: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ElyLocalBookmarkRecord {
id: String,
space_id: String,
space_name: String,
collection_name: String,
title: String,
url: String,
tags: Vec<String>,
note: Option<String>,
thumbnail_key: Option<String>,
added_at_unix_seconds: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ElyLocalNoteRecord {
id: String,
space_id: String,
space_name: String,
target: ElyLocalNoteTargetRecord,
title: String,
source_url: String,
body: String,
created_at_unix_seconds: u64,
updated_at_unix_seconds: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum ElyLocalNoteTargetRecord {
Url { url: String },
Tab { tab_id: String },
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ElyLocalReadingListRecord {
id: String,
space_id: String,
space_name: String,
title: String,
source_url: String,
progress: ElyLocalReadingProgressRecord,
added_at_unix_seconds: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "state", rename_all = "snake_case")]
enum ElyLocalReadingProgressRecord {
Unread,
InProgress { percent: u8 },
Finished,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ElyLocalHistoryRecord {
space_id: String,
space_name: String,
source_tab_id: String,
title: String,
url: String,
favicon_key: Option<String>,
visited_at_unix_seconds: u64,
visit_count: u32,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ElyLocalDownloadRecord {
id: String,
source_url: String,
file_name: String,
destination: ElyLocalDownloadDestinationRecord,
target_file_path: Option<String>,
security: String,
state: String,
received_bytes: u64,
total_bytes: Option<u64>,
checksum: Option<ElyLocalDownloadChecksumRecord>,
security_prompt_confirmed: bool,
started_at_unix_seconds: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum ElyLocalDownloadDestinationRecord {
AskEveryTime,
FixedDirectory { path: String },
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
struct ElyLocalDownloadChecksumRecord {
algorithm: String,
value: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ElyLocalSitePermissionRecord {
origin: String,
feature: String,
decision: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ElyLocalSitePermissionAuditRecord {
origin: String,
feature: String,
action: ElyLocalSitePermissionAuditActionRecord,
created_at_unix_seconds: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum ElyLocalSitePermissionAuditActionRecord {
Set { decision: String },
Revoked,
}
impl ElyLocalProfileRecord {
pub(super) fn from_profile(profile: &Profile) -> Self {
Self {
id: profile.id().as_str().to_string(),
name: profile.name().to_string(),
kind: profile_kind(profile.kind()).to_string(),
}
}
}
impl ElyLocalTabRecord {
pub(super) fn from_tab(tab: &BrowserTab, space_name: String) -> Result<Self, CoreError> {
Ok(Self {
id: tab.id().as_str().to_string(),
space_id: tab.space_id().as_str().to_string(),
space_name,
title: tab.title().to_string(),
url: tab.url().as_str().to_string(),
favicon_key: tab.favicon_key().map(str::to_string),
parent_tab_id: tab.parent_tab_id().map(|id| id.as_str().to_string()),
state: tab_state(tab.state()).to_string(),
flags: ElyLocalTabFlagsRecord::from_flags(tab.flags()),
group_id: tab.group_id().map(|id| id.as_str().to_string()),
split_id: tab.split_id().map(|id| id.as_str().to_string()),
sort_key: tab.sort_key(),
sync_enabled: tab.sync_enabled(),
created_at_unix_seconds: ElyLocalDataPackage::unix_seconds(tab.created_at())?,
last_active_at_unix_seconds: ElyLocalDataPackage::unix_seconds(tab.last_active_at())?,
})
}
}
impl ElyLocalTabFlagsRecord {
fn from_flags(flags: &TabFlags) -> Self {
Self {
pinned: flags.pinned,
favorite: flags.favorite,
muted: flags.muted,
unread: flags.unread,
}
}
}
impl ElyLocalArchivedTabRecord {
pub(super) fn from_archived_tab(
archived: &ArchivedTab,
tab: ElyLocalTabRecord,
) -> Result<Self, CoreError> {
Ok(Self {
tab,
archived_at_unix_seconds: ElyLocalDataPackage::unix_seconds(archived.archived_at())?,
source: archive_source(archived.source()).to_string(),
})
}
}
impl ElyLocalBookmarkRecord {
pub(super) fn from_bookmark(
bookmark: &BookmarkEntry,
space_name: String,
) -> Result<Self, CoreError> {
Ok(Self {
id: bookmark.id().as_str().to_string(),
space_id: bookmark.space_id().as_str().to_string(),
space_name,
collection_name: bookmark.collection_name().to_string(),
title: bookmark.title().to_string(),
url: bookmark.url().as_str().to_string(),
tags: bookmark.tags().to_vec(),
note: bookmark.note().map(str::to_string),
thumbnail_key: bookmark.thumbnail_key().map(str::to_string),
added_at_unix_seconds: ElyLocalDataPackage::unix_seconds(bookmark.added_at())?,
})
}
}
impl ElyLocalNoteRecord {
pub(super) fn from_note(note: &NoteEntry, space_name: String) -> Result<Self, CoreError> {
Ok(Self {
id: note.id().as_str().to_string(),
space_id: note.space_id().as_str().to_string(),
space_name,
target: ElyLocalNoteTargetRecord::from_note_target(note.target()),
title: note.title().to_string(),
source_url: note.source_url().as_str().to_string(),
body: note.body().to_string(),
created_at_unix_seconds: ElyLocalDataPackage::unix_seconds(note.created_at())?,
updated_at_unix_seconds: ElyLocalDataPackage::unix_seconds(note.updated_at())?,
})
}
}
impl ElyLocalReadingListRecord {
pub(super) fn from_entry(
entry: &ReadingListEntry,
space_name: String,
) -> Result<Self, CoreError> {
Ok(Self {
id: entry.id().as_str().to_string(),
space_id: entry.space_id().as_str().to_string(),
space_name,
title: entry.title().to_string(),
source_url: entry.source_url().as_str().to_string(),
progress: ElyLocalReadingProgressRecord::from_progress(*entry.progress()),
added_at_unix_seconds: ElyLocalDataPackage::unix_seconds(entry.added_at())?,
})
}
}
impl ElyLocalHistoryRecord {
pub(super) fn from_entry(entry: &HistoryEntry, space_name: String) -> Result<Self, CoreError> {
Ok(Self {
space_id: entry.space_id().as_str().to_string(),
space_name,
source_tab_id: entry.source_tab_id().as_str().to_string(),
title: entry.title().to_string(),
url: entry.url().as_str().to_string(),
favicon_key: entry.favicon_key().map(str::to_string),
visited_at_unix_seconds: ElyLocalDataPackage::unix_seconds(entry.visited_at())?,
visit_count: entry.visit_count(),
})
}
}
impl ElyLocalDownloadRecord {
pub(super) fn from_download(entry: &DownloadEntry) -> Result<Self, CoreError> {
Ok(Self {
id: entry.id().as_str().to_string(),
source_url: entry.source_url().as_str().to_string(),
file_name: entry.file_name().to_string(),
destination: ElyLocalDownloadDestinationRecord::from_destination(entry.destination()),
target_file_path: entry.target_file_path().map(|path| path.display().to_string()),
security: download_security(entry.security()).to_string(),
state: download_state(entry.state()).to_string(),
received_bytes: entry.received_bytes(),
total_bytes: entry.total_bytes(),
checksum: entry.checksum().map(ElyLocalDownloadChecksumRecord::from_checksum),
security_prompt_confirmed: entry.security_prompt_confirmed(),
started_at_unix_seconds: ElyLocalDataPackage::unix_seconds(entry.started_at())?,
})
}
}
impl ElyLocalDownloadDestinationRecord {
fn from_destination(destination: &DownloadDestination) -> Self {
match destination {
DownloadDestination::AskEveryTime => Self::AskEveryTime,
DownloadDestination::FixedDirectory(path) => {
Self::FixedDirectory { path: path.display().to_string() }
}
}
}
}
impl ElyLocalDownloadChecksumRecord {
fn from_checksum(checksum: &DownloadChecksum) -> Self {
Self {
algorithm: checksum.algorithm().as_str().to_string(),
value: checksum.value().to_string(),
}
}
}
impl ElyLocalSitePermissionRecord {
pub(super) fn from_site_permission(entry: &SitePermissionEntry) -> Self {
Self {
origin: entry.origin().as_str().to_string(),
feature: entry.feature().as_str().to_string(),
decision: entry.decision().as_str().to_string(),
}
}
}
impl ElyLocalSitePermissionAuditRecord {
pub(super) fn from_audit_event(event: &SitePermissionAuditEvent) -> Result<Self, CoreError> {
Ok(Self {
origin: event.origin().as_str().to_string(),
feature: event.feature().as_str().to_string(),
action: ElyLocalSitePermissionAuditActionRecord::from_action(event.action()),
created_at_unix_seconds: ElyLocalDataPackage::unix_seconds(event.created_at())?,
})
}
}
impl ElyLocalNoteTargetRecord {
fn from_note_target(target: &NoteTarget) -> Self {
match target {
NoteTarget::Url(url) => Self::Url { url: url.as_str().to_string() },
NoteTarget::Tab(tab_id) => Self::Tab { tab_id: tab_id.as_str().to_string() },
}
}
}
impl ElyLocalReadingProgressRecord {
fn from_progress(progress: ReadingProgress) -> Self {
match progress {
ReadingProgress::Unread => Self::Unread,
ReadingProgress::InProgress(percent) => Self::InProgress { percent: percent.value() },
ReadingProgress::Finished => Self::Finished,
}
}
}
impl ElyLocalSitePermissionAuditActionRecord {
fn from_action(action: &SitePermissionAuditAction) -> Self {
match action {
SitePermissionAuditAction::Set(decision) => {
Self::Set { decision: decision.as_str().to_string() }
}
SitePermissionAuditAction::Revoked => Self::Revoked,
}
}
}
fn profile_kind(kind: &ProfileKind) -> &'static str {
match kind {
ProfileKind::Standard => "standard",
ProfileKind::Private => "private",
}
}
fn tab_state(state: &TabState) -> &'static str {
match state {
TabState::Loading => "loading",
TabState::Ready => "ready",
TabState::Crashed => "crashed",
TabState::Discarded => "discarded",
TabState::Archived => "archived",
}
}
fn archive_source(source: &ArchiveSource) -> &'static str {
match source {
ArchiveSource::ManualClose => "manual_close",
ArchiveSource::AutoArchive => "auto_archive",
}
}
fn download_security(security: &DownloadSecurity) -> &'static str {
match security {
DownloadSecurity::Standard => "standard",
DownloadSecurity::DangerousExtension => "dangerous_extension",
}
}
fn download_state(state: &DownloadState) -> &'static str {
match state {
DownloadState::InProgress => "in_progress",
DownloadState::Paused => "paused",
DownloadState::Completed => "completed",
DownloadState::Cancelled => "cancelled",
DownloadState::Failed => "failed",
}
}
@@ -0,0 +1,136 @@
use std::time::SystemTime;
use ely_domain::{ArchivedTab, BookmarkEntry, BrowserTab, HistoryEntry, NoteEntry, SpaceId};
use super::BrowserCore;
pub use super::local_data_export_records::ElyLocalDataPackage;
use super::local_data_export_records::{
ElyLocalArchivedTabRecord, ElyLocalBookmarkRecord, ElyLocalDownloadRecord,
ElyLocalHistoryRecord, ElyLocalNoteRecord, ElyLocalProfileRecord, ElyLocalReadingListRecord,
ElyLocalSitePermissionAuditRecord, ElyLocalSitePermissionRecord, ElyLocalTabRecord,
};
use crate::CoreError;
pub const ELYDATA_SCHEMA_VERSION: u16 = 1;
pub const ELYDATA_FILE_EXTENSION: &str = "elydata";
impl BrowserCore {
pub fn export_local_data_package_json(&self) -> Result<String, CoreError> {
serde_json::to_string_pretty(&self.export_local_data_package()?)
.map_err(invalid_local_data_package)
}
pub fn export_local_data_package(&self) -> Result<ElyLocalDataPackage, CoreError> {
let profile = self.active_profile()?;
let profile_id = profile.id();
Ok(ElyLocalDataPackage {
version: ELYDATA_SCHEMA_VERSION,
exported_at_unix_seconds: ElyLocalDataPackage::unix_seconds(SystemTime::now())?,
profile: ElyLocalProfileRecord::from_profile(profile),
inventory: self.active_profile_local_data_inventory(),
open_tabs: self
.tabs
.iter()
.filter(|tab| tab.profile_id() == profile_id)
.map(|tab| self.local_tab_record(tab))
.collect::<Result<Vec<_>, _>>()?,
archived_tabs: self
.archived_tabs
.iter()
.filter(|archived| archived.tab().profile_id() == profile_id)
.map(|archived| self.local_archived_tab_record(archived))
.collect::<Result<Vec<_>, _>>()?,
bookmarks: self
.bookmarks
.iter()
.filter(|bookmark| bookmark.profile_id() == profile_id)
.map(|bookmark| self.local_bookmark_record(bookmark))
.collect::<Result<Vec<_>, _>>()?,
notes: self
.notes
.iter()
.filter(|note| note.profile_id() == profile_id)
.map(|note| self.local_note_record(note))
.collect::<Result<Vec<_>, _>>()?,
reading_list: self
.reading_list
.iter()
.filter(|entry| entry.profile_id() == profile_id)
.map(|entry| {
self.space_name(entry.space_id()).and_then(|space_name| {
ElyLocalReadingListRecord::from_entry(entry, space_name)
})
})
.collect::<Result<Vec<_>, _>>()?,
history: self
.history_entries
.iter()
.filter(|entry| entry.profile_id() == profile_id)
.map(|entry| self.local_history_record(entry))
.collect::<Result<Vec<_>, _>>()?,
downloads: self
.download_entries
.iter()
.filter(|entry| entry.profile_id() == profile_id)
.map(ElyLocalDownloadRecord::from_download)
.collect::<Result<Vec<_>, _>>()?,
site_permissions: self
.site_permissions
.iter()
.filter(|entry| entry.profile_id() == profile_id)
.map(ElyLocalSitePermissionRecord::from_site_permission)
.collect(),
site_permission_audit_events: self
.site_permission_audit_events
.iter()
.filter(|event| event.profile_id() == profile_id)
.map(ElyLocalSitePermissionAuditRecord::from_audit_event)
.collect::<Result<Vec<_>, _>>()?,
})
}
fn local_tab_record(&self, tab: &BrowserTab) -> Result<ElyLocalTabRecord, CoreError> {
ElyLocalTabRecord::from_tab(tab, self.space_name(tab.space_id())?)
}
fn local_archived_tab_record(
&self,
archived: &ArchivedTab,
) -> Result<ElyLocalArchivedTabRecord, CoreError> {
ElyLocalArchivedTabRecord::from_archived_tab(
archived,
self.local_tab_record(archived.tab())?,
)
}
fn local_bookmark_record(
&self,
bookmark: &BookmarkEntry,
) -> Result<ElyLocalBookmarkRecord, CoreError> {
ElyLocalBookmarkRecord::from_bookmark(bookmark, self.space_name(bookmark.space_id())?)
}
fn local_note_record(&self, note: &NoteEntry) -> Result<ElyLocalNoteRecord, CoreError> {
ElyLocalNoteRecord::from_note(note, self.space_name(note.space_id())?)
}
fn local_history_record(
&self,
entry: &HistoryEntry,
) -> Result<ElyLocalHistoryRecord, CoreError> {
ElyLocalHistoryRecord::from_entry(entry, self.space_name(entry.space_id())?)
}
fn space_name(&self, space_id: &SpaceId) -> Result<String, CoreError> {
self.spaces
.iter()
.find(|space| space.id() == space_id)
.map(|space| space.name().to_string())
.ok_or_else(|| CoreError::SpaceNotFound { id: space_id.clone() })
}
}
fn invalid_local_data_package(error: serde_json::Error) -> CoreError {
CoreError::InvalidLocalDataPackage { reason: error.to_string() }
}
+3 -1
View File
@@ -1,8 +1,10 @@
use ely_domain::{DiagnosticsReportingPolicy, HistoryRecordingPolicy};
use serde::{Deserialize, Serialize};
use super::BrowserCore;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct LocalDataInventory {
open_tabs: usize,
archived_tabs: usize,
+75 -2
View File
@@ -1,7 +1,9 @@
use std::error::Error;
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{ProfileKind, SiteOrigin, SitePermissionDecision, SitePermissionFeature, UrlText};
use ely_browser_core::{BrowserCore, ELYDATA_SCHEMA_VERSION, InitialBrowserConfig};
use ely_domain::{
CommandIntent, ProfileKind, SiteOrigin, SitePermissionDecision, SitePermissionFeature, UrlText,
};
#[test]
fn local_data_inventory_counts_active_profile_data() -> Result<(), Box<dyn Error>> {
@@ -66,3 +68,74 @@ fn local_data_inventory_counts_active_profile_data() -> Result<(), Box<dyn Error
assert_eq!(personal_inventory.downloads(), 1);
Ok(())
}
#[test]
fn local_data_export_contains_active_profile_records() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let default_profile_id = core.snapshot()?.active_profile_id;
core.open_tab(UrlText::parse("https://example.com/research")?);
core.bookmark_active_tab()?;
core.save_active_url_note("profile note")?;
core.save_active_tab_to_reading_list()?;
core.record_download_started(
UrlText::parse("https://example.com/report.pdf")?,
"report.pdf",
Some(2048),
)?;
core.set_site_permission(
SiteOrigin::parse("https://example.com")?,
SitePermissionFeature::Camera,
SitePermissionDecision::AllowAlways,
)?;
let archived_tab_id = core.open_tab(UrlText::parse("https://servo.org/")?);
core.close_tab(&archived_tab_id)?;
core.create_profile("Personal", 0xf54e00, ProfileKind::Standard)?;
core.open_tab(UrlText::parse("https://personal.example/research")?);
core.bookmark_active_tab()?;
core.select_profile(&default_profile_id)?;
let package = core.export_local_data_package()?;
let package_json = core.export_local_data_package_json()?;
let document: serde_json::Value = serde_json::from_str(&package_json)?;
assert_eq!(package.version(), ELYDATA_SCHEMA_VERSION);
assert_eq!(package.profile_id(), default_profile_id.as_str());
assert_eq!(package.profile_name(), "Default");
assert_eq!(package.inventory().total_items(), 11);
assert_eq!(document["version"], ELYDATA_SCHEMA_VERSION);
assert_eq!(document["profile"]["id"], default_profile_id.as_str());
assert_eq!(array_len(&document, "open_tabs"), 2);
assert_eq!(array_len(&document, "archived_tabs"), 1);
assert_eq!(array_len(&document, "bookmarks"), 1);
assert_eq!(array_len(&document, "notes"), 1);
assert_eq!(array_len(&document, "reading_list"), 1);
assert_eq!(array_len(&document, "history"), 2);
assert_eq!(array_len(&document, "downloads"), 1);
assert_eq!(array_len(&document, "site_permissions"), 1);
assert_eq!(array_len(&document, "site_permission_audit_events"), 1);
assert_eq!(document["bookmarks"][0]["url"], "https://example.com/research");
assert_eq!(document["downloads"][0]["file_name"], "report.pdf");
assert_eq!(document["site_permissions"][0]["origin"], "https://example.com");
Ok(())
}
#[test]
fn export_local_data_command_opens_privacy_security_page() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
core.set_command_query(">export-local-data");
let intent = core.submit_command()?;
let active_tab = core.active_tab()?;
assert_eq!(intent, Some(CommandIntent::Command("export-local-data".to_string())));
assert_eq!(active_tab.title(), "Privacy & Security Settings");
assert_eq!(active_tab.url().as_str(), "ely://settings/privacy-security");
assert_eq!(core.snapshot()?.command_query, "");
Ok(())
}
fn array_len(document: &serde_json::Value, field: &str) -> usize {
document[field].as_array().map_or(0, Vec::len)
}