Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
@@ -0,0 +1,132 @@
//! Supporting types for persisting cloud objects to SQLite.
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use crate::{
auth::UserUid,
cloud_object::{CloudLinkSharing, CloudObjectGuest, ServerObjectContainer},
drive::sharing::{SharingAccessLevel, Subject, TeamKind, UserKind},
ids::ServerId,
};
/// Decode a link-sharing setting.
pub fn decode_link_sharing(
encoded_access_level: &str,
encoded_source: Option<&[u8]>,
) -> anyhow::Result<CloudLinkSharing> {
let access_level = encoded_access_level.parse()?;
let source = encoded_source.map(bincode::deserialize).transpose()?;
Ok(CloudLinkSharing {
access_level,
source,
})
}
/// Encode a link-sharing setting.
pub fn encode_link_sharing(
link_sharing: &CloudLinkSharing,
) -> anyhow::Result<(&'static str, Option<Vec<u8>>)> {
let source = link_sharing
.source
.as_ref()
.map(bincode::serialize)
.transpose()?;
Ok((link_sharing.access_level.to_serializable_value(), source))
}
/// Deserialize encoded object guests.
pub fn decode_guests(encoded_guests: &[u8]) -> anyhow::Result<Vec<CloudObjectGuest>> {
let persisted_guests = bincode::deserialize::<Vec<PersistedGuest>>(encoded_guests)?;
Ok(persisted_guests
.into_iter()
.map(PersistedGuest::into_cloud_object_guest)
.collect())
}
/// Encode object guests for persistence.
pub fn encode_guests(guests: &[CloudObjectGuest]) -> anyhow::Result<Vec<u8>> {
let persisted_guests = guests
.iter()
.map(PersistedGuest::try_from_cloud_object_guest)
.collect::<anyhow::Result<Vec<PersistedGuest>>>()?;
Ok(bincode::serialize(&persisted_guests)?)
}
/// Database representation of an object guest. These are [`bincode`]-serialized to support storing
/// an arbitrarily-long guest list.
#[derive(Serialize, Deserialize)]
struct PersistedGuest {
subject: PersistedSubject,
access_level: SharingAccessLevel,
source: Option<ServerObjectContainer>,
}
/// Database representation of a guest subject. This is restricted compared to the [`Subject`] type
/// since not all subjects are persisted.
#[derive(Serialize, Deserialize)]
enum PersistedSubject {
User { firebase_uid: String },
PendingUser { email: Option<String> },
Team { team_uid: ServerId },
}
impl PersistedGuest {
pub fn into_cloud_object_guest(self) -> CloudObjectGuest {
CloudObjectGuest {
subject: self.subject.into_subject(),
access_level: self.access_level,
source: self.source,
}
}
pub fn try_from_cloud_object_guest(guest: &CloudObjectGuest) -> anyhow::Result<Self> {
Ok(PersistedGuest {
subject: PersistedSubject::try_from_subject(&guest.subject)?,
access_level: guest.access_level,
source: guest.source,
})
}
}
impl PersistedSubject {
pub fn into_subject(self) -> Subject {
match self {
PersistedSubject::User { firebase_uid } => {
Subject::User(UserKind::Account(UserUid::new(&firebase_uid)))
}
PersistedSubject::PendingUser { email } => Subject::PendingUser { email },
PersistedSubject::Team { team_uid } => Subject::Team(TeamKind::Team { team_uid }),
}
}
/// Convert a [`Subject`] into a guest subject type. This is only supported for subjects that
/// may be direct object guests.
pub fn try_from_subject(subject: &Subject) -> anyhow::Result<Self> {
match subject {
Subject::User(user_kind) => match user_kind {
UserKind::Account(user_uid) => Ok(PersistedSubject::User {
firebase_uid: user_uid.to_string(),
}),
UserKind::SharedSessionParticipant(_) => {
// Shared sessions are transient, so we don't persist their ACLs to SQLite.
Err(anyhow!("Session-sharing participants not supported"))
}
},
Subject::PendingUser { email } => Ok(PersistedSubject::PendingUser {
email: email.clone(),
}),
Subject::Team(team_kind) => match team_kind {
TeamKind::Team { team_uid } => Ok(PersistedSubject::Team {
team_uid: *team_uid,
}),
TeamKind::SharedSessionTeam { .. } => {
// Shared sessions are transient, so we don't persist their ACLs to SQLite.
Err(anyhow!("Session-sharing teams not supported"))
}
},
// Link sharing is persisted separately in the schema.
Subject::AnyoneWithLink(_) => Err(anyhow!("Anyone with the link not supported")),
}
}
}
@@ -0,0 +1,247 @@
//! Persistence utilities for cloud objects.
mod cloud_objects;
use diesel::SqliteConnection;
use diesel::result::Error;
pub use cloud_objects::{decode_guests, decode_link_sharing, encode_guests, encode_link_sharing};
use crate::cloud_object::{
CloudObjectMetadata, CloudObjectPermissions, ObjectIdType, ObjectType, Owner,
};
use crate::ids::SyncId;
use persistence::model::{NewObjectMetadata, NewObjectPermissions, ObjectMetadata};
use persistence::schema;
use warp_core::features::FeatureFlag;
/// The sqlite id of a cloud object.
pub type CloudObjectId = i32;
/// When upserting a cloud object, this callback is used to create the cloud
/// object itself. It returns the id of the created cloud object.
/// Note: the supplied conn has already started a transaction.
pub type CreateCloudObjectFn =
Box<dyn FnOnce(&mut SqliteConnection) -> Result<CloudObjectId, Error>>;
/// When upserting a cloud object, this callback is used to update the cloud
/// object. It takes the id of the cloud object to update as a parameter.
/// The supplied conn has already started a transaction.
pub type UpdateCloudObjectFn =
Box<dyn FnOnce(&mut SqliteConnection, CloudObjectId) -> Result<(), Error>>;
pub fn upsert_cloud_object(
conn: &mut SqliteConnection,
cloud_object_type: ObjectType,
sync_id: SyncId,
cloud_object_metadata: CloudObjectMetadata,
cloud_object_permissions: CloudObjectPermissions,
create_object_fn: CreateCloudObjectFn,
update_object_fn: UpdateCloudObjectFn,
) -> Result<(), Error> {
use schema::object_metadata::dsl::{
client_id, current_editor, folder_id, is_pending, last_editor_uid,
metadata_last_updated_ts, object_metadata, revision_ts, server_id, trashed_ts,
};
use schema::object_permissions::dsl::{
anyone_with_link_access_level, anyone_with_link_source, object_guests, object_metadata_id,
object_permissions, permissions_last_updated_at, subject_id, subject_type, subject_uid,
};
use diesel::prelude::*;
let (subject_type_value, subject_id_value, subject_uid_value) =
match cloud_object_permissions.owner {
Owner::User { user_uid } => ("USER", Some(user_uid.to_string()), user_uid.to_string()),
Owner::Team { team_uid } => ("TEAM", None, team_uid.to_string()),
};
let permissions_ts = cloud_object_permissions
.permissions_last_updated_ts
.map(|ts| ts.timestamp_micros());
let guests = if FeatureFlag::SharedWithMe.is_enabled() {
match encode_guests(&cloud_object_permissions.guests) {
Ok(guests) => Some(guests),
Err(err) => {
log::warn!("Unable to encode guests: {err:#}");
None
}
}
} else {
None
};
let (anyone_with_link_access_level_value, anyone_with_link_source_value) =
if FeatureFlag::SharedWithMe.is_enabled() {
match cloud_object_permissions
.anyone_with_link
.as_ref()
.map(encode_link_sharing)
{
Some(Ok((access_level, source))) => (Some(access_level), source),
Some(Err(err)) => {
log::warn!("Unable to encode link-sharing setting: {err:#}");
(None, None)
}
None => (None, None),
}
} else {
(None, None)
};
let revision = cloud_object_metadata
.revision
.as_ref()
.map(|r| r.timestamp_micros());
let has_pending_content_changes = cloud_object_metadata.has_pending_content_changes();
// Filter to find metadata row.
// The diesel types for `filter`s are dependent on the columns being filtered
// so while the `hashed_sync_id` will only match one of `client_id` and `server_id`,
// we filter on both here for ergonomics.
let hashed_sync_id = sync_id.sqlite_uid_hash(cloud_object_type.into());
let metadata_filter = object_metadata
.filter(client_id.eq(Some(hashed_sync_id.as_str())))
.or_filter(server_id.eq(Some(hashed_sync_id.as_str())));
let metadata: Option<ObjectMetadata> = metadata_filter.first(conn).ok();
match metadata {
Some(metadata) => {
// The object already exists in sqlite so update the object.
update_object_fn(conn, metadata.shareable_object_id)?;
let metadata_last_updated_at = cloud_object_metadata
.metadata_last_updated_ts
.map(|ts| ts.timestamp_micros());
let trashed_timestamp = cloud_object_metadata
.trashed_ts
.map(|ts| ts.timestamp_micros());
let folder_id_str = cloud_object_metadata
.folder_id
.map(|folder_sync_id| folder_sync_id.sqlite_uid_hash(ObjectIdType::Folder));
// Update the metadata. Note: this is holistic write of all the metadata based on the current state of the in-memory object.
// TODO: we need to update author_id as well.
diesel::update(metadata_filter)
.set((
revision_ts.eq(revision),
is_pending.eq(has_pending_content_changes),
last_editor_uid.eq(cloud_object_metadata.last_editor_uid),
))
.execute(conn)?;
if !cloud_object_metadata
.pending_changes_statuses
.has_pending_metadata_change
{
diesel::update(metadata_filter)
.set((
metadata_last_updated_ts.eq(metadata_last_updated_at),
trashed_ts.eq(trashed_timestamp),
folder_id.eq(folder_id_str),
current_editor.eq(cloud_object_metadata.current_editor_uid),
))
.execute(conn)?;
}
// Update the permissions.
if !cloud_object_metadata
.pending_changes_statuses
.has_pending_permissions_change
{
let permissions_filter =
object_permissions.filter(object_metadata_id.eq(metadata.id));
diesel::update(permissions_filter)
.set((
subject_type.eq(subject_type_value),
subject_id.eq(subject_id_value),
subject_uid.eq(subject_uid_value),
permissions_last_updated_at.eq(permissions_ts),
object_guests.eq(guests),
anyone_with_link_access_level.eq(anyone_with_link_access_level_value),
anyone_with_link_source.eq(anyone_with_link_source_value),
))
.execute(conn)?;
}
}
None => {
// The object doesn't exist in sqlite so create the object.
let object_id = create_object_fn(conn)?;
// Create the metadata.
let mut new_object_metadata = NewObjectMetadata {
object_type: cloud_object_type.sqlite_object_type_as_str().to_string(),
revision_ts: revision,
shareable_object_id: object_id,
is_pending: has_pending_content_changes,
retry_count: 0,
// TODO: we need to deserialize this from graphql.
author_id: None,
// One of these is set below.
client_id: None,
server_id: None,
metadata_last_updated_ts: cloud_object_metadata
.metadata_last_updated_ts
.map(|ts| ts.timestamp_micros()),
trashed_ts: cloud_object_metadata
.trashed_ts
.map(|ts| ts.timestamp_micros()),
folder_id: cloud_object_metadata
.folder_id
.map(|sync_id| sync_id.sqlite_uid_hash(ObjectIdType::Folder)),
// When we insert an object, mark whether it's a welcome object. This
// field won't ever be updated and this is the only pathway for it to be set.
is_welcome_object: cloud_object_metadata.is_welcome_object,
creator_uid: cloud_object_metadata.creator_uid,
last_editor_uid: cloud_object_metadata.last_editor_uid,
current_editor: cloud_object_metadata.current_editor_uid,
};
// There are two distinct cases:
// - If the client created this object, the clientId will be set. There is another model event to set the server id.
// - Otherwise, the server notified the client about this object so only the serverId will be set.
match sync_id {
SyncId::ClientId(_) => {
new_object_metadata.client_id = Some(hashed_sync_id);
}
SyncId::ServerId(_) => {
new_object_metadata.server_id = Some(hashed_sync_id);
}
}
diesel::insert_into(schema::object_metadata::dsl::object_metadata)
.values(new_object_metadata)
.execute(conn)?;
// Retrieve the ID of the row that was just inserted. We need to
// do it this way because sqlite doesn't support RETURNING.
let metadata_id: i32 = schema::object_metadata::dsl::object_metadata
.select(schema::object_metadata::dsl::id)
.order(schema::object_metadata::dsl::id.desc())
.first(conn)?;
// Create the permissions.
let new_object_permissions = NewObjectPermissions {
object_metadata_id: metadata_id,
subject_type: subject_type_value.to_owned(),
subject_id: subject_id_value,
subject_uid: subject_uid_value,
permissions_last_updated_at: permissions_ts,
object_guests: guests,
anyone_with_link_access_level: anyone_with_link_access_level_value,
anyone_with_link_source: anyone_with_link_source_value,
};
diesel::insert_into(schema::object_permissions::dsl::object_permissions)
.values(new_object_permissions)
.execute(conn)?;
}
}
Ok(())
}