mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-11-05 06:43:48 -05:00
Surface peer replicas in sources.list and serve remote listings
sources.list now appends paired devices' sources from the peer-mount replica set, attributed with the owning device's id and label and marked status "replica". The overview device card renders them, so a paired NAS shows its sources instead of "No volumes". directory_listing serves paths on another device straight from that device's replicated index: same conversion as the local cached path (extracted into shared helpers), no indexer dispatch, since indexing happens on the owner. Verified from titan against the Mac's replica. sync_device now holds a per-device lock; the refresh loop clears the debounce before running, so a reconnect in that window was loading the same snapshots twice in parallel.
This commit is contained in:
1 parent
a0180d1b7e
commit
3ac1f0af78
6 files changed
+342
-66
No files matched your search
@@ -145,6 +145,26 @@ impl DirectoryListingQuery {
|
||||
use crate::domain::file::File;
|
||||
use crate::ops::indexing::{IndexScope, IndexerJob, IndexerJobConfig};
|
||||
|
||||
// A path on another device is served from its replica, kept current
|
||||
// by the peer-mount plane. Indexing happens on the owner, so there is
|
||||
// no dispatch fall-through: what the replica holds is the answer.
|
||||
if let SdPath::Physical { device_slug, path } = &self.input.path {
|
||||
if *device_slug != crate::device::get_current_device_slug() {
|
||||
if let Some(output) = self.list_remote_replica(&context, device_slug, path).await {
|
||||
return Ok(output);
|
||||
}
|
||||
tracing::debug!(
|
||||
"no replica covers {} on device '{device_slug}'",
|
||||
path.display()
|
||||
);
|
||||
return Ok(DirectoryListingOutput {
|
||||
files: Vec::new(),
|
||||
total_count: 0,
|
||||
has_more: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get the local path for cache lookup
|
||||
let local_path = match &self.input.path {
|
||||
SdPath::Physical { path, .. } => path.clone(),
|
||||
@@ -199,67 +219,12 @@ impl DirectoryListingQuery {
|
||||
local_path.display()
|
||||
);
|
||||
|
||||
// Convert cached entries to File objects with lazy UUID assignment
|
||||
let mut index_write = index.write().await;
|
||||
let mut files = Vec::new();
|
||||
|
||||
for child_path in children {
|
||||
if let Some(metadata) = index_write.get_entry_ref(&child_path) {
|
||||
// Bundle internals are lensed out with hidden files:
|
||||
// the package browses as one opaque item.
|
||||
if !self.input.include_hidden.unwrap_or(false)
|
||||
&& (metadata.is_hidden
|
||||
|| crate::ops::indexing::lens::is_bundle_internal(&child_path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let entry_uuid = index_write.get_or_assign_uuid(&child_path);
|
||||
|
||||
let entry_sd_path = SdPath::Physical {
|
||||
device_slug: match &self.input.path {
|
||||
SdPath::Physical { device_slug, .. } => device_slug.clone(),
|
||||
_ => String::new(),
|
||||
},
|
||||
path: child_path.clone(),
|
||||
};
|
||||
|
||||
let content_kind = index_write.get_content_kind(&child_path);
|
||||
|
||||
let mut file =
|
||||
File::from_ephemeral(entry_uuid, &metadata, entry_sd_path);
|
||||
file.content_kind = content_kind;
|
||||
// Directories report their subtree rollup rather
|
||||
// than the directory entry's own on-disk size.
|
||||
if metadata.kind == crate::ops::indexing::state::EntryKind::Directory {
|
||||
if let Some(bytes) = index_write.subtree_size(&child_path) {
|
||||
file.size = bytes;
|
||||
}
|
||||
}
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
drop(index_write);
|
||||
|
||||
self.sort_files(&mut files);
|
||||
|
||||
let total_count = files.len() as u32;
|
||||
let has_more = if let Some(limit) = self.input.limit {
|
||||
if files.len() > limit as usize {
|
||||
files.truncate(limit as usize);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
let device_slug = match &self.input.path {
|
||||
SdPath::Physical { device_slug, .. } => device_slug.clone(),
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
return Ok(DirectoryListingOutput {
|
||||
files,
|
||||
total_count,
|
||||
has_more,
|
||||
});
|
||||
let files = self.files_from_index(&index, children, &device_slug).await;
|
||||
return Ok(self.finalize_listing(files));
|
||||
}
|
||||
} else {
|
||||
// Index exists but doesn't have this directory yet
|
||||
@@ -365,6 +330,100 @@ impl DirectoryListingQuery {
|
||||
}
|
||||
|
||||
/// Sort files according to the input options
|
||||
/// Serve a listing from a paired device's replicated index. `None` when
|
||||
/// no replica covers the path, which a caller reports as an empty
|
||||
/// directory rather than an error: the owner may simply not share it.
|
||||
async fn list_remote_replica(
|
||||
&self,
|
||||
context: &Arc<CoreContext>,
|
||||
device_slug: &str,
|
||||
path: &std::path::Path,
|
||||
) -> Option<DirectoryListingOutput> {
|
||||
let shares = crate::service::mounts::peer::remote_shares().await;
|
||||
let share = shares.iter().find(|share| {
|
||||
path.starts_with(&share.info.root)
|
||||
&& context
|
||||
.device_manager
|
||||
.get_device_slug(share.device_id)
|
||||
.is_some_and(|slug| slug == device_slug)
|
||||
})?;
|
||||
|
||||
let children = { share.index.read().await.list_directory(path) }?;
|
||||
let files = self
|
||||
.files_from_index(&share.index, children, device_slug)
|
||||
.await;
|
||||
Some(self.finalize_listing(files))
|
||||
}
|
||||
|
||||
/// Convert one directory's children in an in-memory index to `File`s,
|
||||
/// applying the hidden/bundle lens. Shared by the local cache path and
|
||||
/// peer replicas.
|
||||
async fn files_from_index(
|
||||
&self,
|
||||
index: &Arc<tokio::sync::RwLock<crate::ops::indexing::ephemeral::EphemeralIndex>>,
|
||||
children: Vec<std::path::PathBuf>,
|
||||
device_slug: &str,
|
||||
) -> Vec<File> {
|
||||
let mut index_write = index.write().await;
|
||||
let mut files = Vec::new();
|
||||
|
||||
for child_path in children {
|
||||
if let Some(metadata) = index_write.get_entry_ref(&child_path) {
|
||||
// Bundle internals are lensed out with hidden files:
|
||||
// the package browses as one opaque item.
|
||||
if !self.input.include_hidden.unwrap_or(false)
|
||||
&& (metadata.is_hidden
|
||||
|| crate::ops::indexing::lens::is_bundle_internal(&child_path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let entry_uuid = index_write.get_or_assign_uuid(&child_path);
|
||||
let entry_sd_path = SdPath::Physical {
|
||||
device_slug: device_slug.to_string(),
|
||||
path: child_path.clone(),
|
||||
};
|
||||
let content_kind = index_write.get_content_kind(&child_path);
|
||||
|
||||
let mut file = File::from_ephemeral(entry_uuid, &metadata, entry_sd_path);
|
||||
file.content_kind = content_kind;
|
||||
// Directories report their subtree rollup rather
|
||||
// than the directory entry's own on-disk size.
|
||||
if metadata.kind == crate::ops::indexing::state::EntryKind::Directory {
|
||||
if let Some(bytes) = index_write.subtree_size(&child_path) {
|
||||
file.size = bytes;
|
||||
}
|
||||
}
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
files
|
||||
}
|
||||
|
||||
/// Sort and cap a listing the way the input asked for.
|
||||
fn finalize_listing(&self, mut files: Vec<File>) -> DirectoryListingOutput {
|
||||
self.sort_files(&mut files);
|
||||
|
||||
let total_count = files.len() as u32;
|
||||
let has_more = if let Some(limit) = self.input.limit {
|
||||
if files.len() > limit as usize {
|
||||
files.truncate(limit as usize);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
DirectoryListingOutput {
|
||||
files,
|
||||
total_count,
|
||||
has_more,
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_files(&self, files: &mut Vec<File>) {
|
||||
use crate::domain::file::EntryKind;
|
||||
|
||||
|
||||
@@ -36,6 +36,12 @@ pub struct SourceInfo {
|
||||
/// Last time the origin answered. Absent for an adapter, whose registry
|
||||
/// tracks a sync cursor rather than an attachment.
|
||||
pub last_seen_at: Option<String>,
|
||||
/// The paired device this source was replicated from. Absent for a
|
||||
/// source this library registered itself; a replica is read-only here
|
||||
/// and its registry ops run on the owning device.
|
||||
pub device_id: Option<Uuid>,
|
||||
/// The owning device's display name, for a replica.
|
||||
pub device_label: Option<String>,
|
||||
}
|
||||
|
||||
impl SourceInfo {
|
||||
@@ -80,6 +86,39 @@ impl SourceInfo {
|
||||
attached,
|
||||
total_bytes,
|
||||
last_seen_at,
|
||||
device_id: None,
|
||||
device_label: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A paired device's source, replicated through the peer-mount plane.
|
||||
/// The metadata is the replica's own: counts and bytes from the owner's
|
||||
/// listing, `last_synced` from when the snapshot arrived here.
|
||||
pub fn from_remote_share(share: &crate::service::mounts::peer::RemoteShare) -> Self {
|
||||
let root = &share.info.root;
|
||||
let name = root
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| share.info.id.to_string());
|
||||
let synced_at =
|
||||
chrono::DateTime::<chrono::Utc>::from_timestamp(share.synced_at_secs as i64, 0)
|
||||
.map(|at| at.to_rfc3339());
|
||||
|
||||
Self {
|
||||
id: share.info.id,
|
||||
name,
|
||||
data_type: "filesystem".to_string(),
|
||||
adapter_id: None,
|
||||
item_count: share.info.entry_count.unwrap_or(0) as i64,
|
||||
last_synced: synced_at.clone(),
|
||||
status: "replica".to_string(),
|
||||
root: Some(root.to_string_lossy().into_owned()),
|
||||
volume_uuid: share.info.volume_uuid,
|
||||
attached: share.info.attached,
|
||||
total_bytes: share.info.total_bytes.map(|bytes| bytes as i64),
|
||||
last_seen_at: synced_at,
|
||||
device_id: Some(share.device_id),
|
||||
device_label: Some(share.device_label.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ impl LibraryQuery for ListSourcesQuery {
|
||||
.await
|
||||
.map_err(|e| QueryError::Internal(format!("Failed to list sources: {e}")))?;
|
||||
|
||||
Ok(rows
|
||||
let mut sources: Vec<SourceInfo> = rows
|
||||
.into_iter()
|
||||
.filter(|row| {
|
||||
self.input
|
||||
@@ -103,7 +103,23 @@ impl LibraryQuery for ListSourcesQuery {
|
||||
.and_then(|uuid| attached_mounts.get(&uuid).cloned());
|
||||
SourceInfo::from_row(row, mount.as_deref())
|
||||
})
|
||||
.collect())
|
||||
.collect();
|
||||
|
||||
// Paired devices' sources, already replicated by the peer-mount
|
||||
// plane. They live in memory rather than the library database, so
|
||||
// they are appended here instead of joined above.
|
||||
if self
|
||||
.input
|
||||
.data_type
|
||||
.as_ref()
|
||||
.is_none_or(|filter| filter == "filesystem")
|
||||
{
|
||||
for share in crate::service::mounts::peer::remote_shares().await {
|
||||
sources.push(SourceInfo::from_remote_share(&share));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(sources)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -171,6 +171,22 @@ pub(super) fn should_sync(device_id: Uuid) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// One replication per device at a time. The refresh loop clears the
|
||||
/// debounce before it runs, so a reconnect landing in that window would
|
||||
/// otherwise fetch and load the same snapshots concurrently; serializing
|
||||
/// here lets the loser skip on the generation check instead.
|
||||
static SYNC_LOCKS: OnceLock<std::sync::Mutex<HashMap<Uuid, Arc<tokio::sync::Mutex<()>>>>> =
|
||||
OnceLock::new();
|
||||
|
||||
fn sync_lock(device_id: Uuid) -> Arc<tokio::sync::Mutex<()>> {
|
||||
let map = SYNC_LOCKS.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
|
||||
map.lock()
|
||||
.unwrap()
|
||||
.entry(device_id)
|
||||
.or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Pull a connected device's source list and replicate every snapshot it can
|
||||
/// provide. Returns how many sources are now served for that device.
|
||||
pub async fn sync_device(
|
||||
@@ -178,6 +194,8 @@ pub async fn sync_device(
|
||||
device_id: Uuid,
|
||||
device_label: String,
|
||||
) -> anyhow::Result<usize> {
|
||||
let lock = sync_lock(device_id);
|
||||
let _guard = lock.lock().await;
|
||||
let (response, _) = request(context, device_id, &ByteRangeRequest::ListSources).await?;
|
||||
let sources = match response {
|
||||
ByteRangeResponse::Sources(s) => s,
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
Location,
|
||||
LocationsListOutput,
|
||||
LocationsListQueryInput,
|
||||
SourceInfo,
|
||||
Volume,
|
||||
VolumeListOutput,
|
||||
VolumeListQueryInput
|
||||
@@ -35,6 +36,7 @@ import {useJobsContext} from '../../components/JobManager/hooks/JobsContext';
|
||||
import {
|
||||
getDeviceIcon,
|
||||
useCoreQuery,
|
||||
useLibraryQuery,
|
||||
useNormalizedQuery
|
||||
} from '../../contexts/SpacedriveContext';
|
||||
import {VolumeBar} from './VolumeBar';
|
||||
@@ -118,6 +120,13 @@ export function DevicePanel({onLocationSelect}: DevicePanelProps = {}) {
|
||||
input: {}
|
||||
});
|
||||
|
||||
// Sources, including paired devices' replicas. A replica carries its
|
||||
// owning device's id, which is how it lands on that device's card.
|
||||
const {data: sourcesData} = useLibraryQuery({
|
||||
type: 'sources.list',
|
||||
input: {data_type: null}
|
||||
});
|
||||
|
||||
// Merge local and remote jobs
|
||||
const allJobs = [
|
||||
...localJobs,
|
||||
@@ -190,6 +199,19 @@ export function DevicePanel({onLocationSelect}: DevicePanelProps = {}) {
|
||||
{} as Record<string, Location[]>
|
||||
);
|
||||
|
||||
// Group replicated sources by their owning device
|
||||
const remoteSourcesByDevice = ([...(sourcesData ?? [])] as SourceInfo[]).reduce(
|
||||
(acc, source) => {
|
||||
if (!source.device_id) return acc;
|
||||
if (!acc[source.device_id]) {
|
||||
acc[source.device_id] = [];
|
||||
}
|
||||
acc[source.device_id].push(source);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, SourceInfo[]>
|
||||
);
|
||||
|
||||
// Group jobs by device_id
|
||||
const jobsByDevice = allJobs.reduce(
|
||||
(acc, job) => {
|
||||
@@ -221,12 +243,15 @@ export function DevicePanel({onLocationSelect}: DevicePanelProps = {}) {
|
||||
const deviceJobs = jobsByDevice[device.id] || [];
|
||||
const deviceLocations =
|
||||
locationsByDeviceSlug[device.slug] || [];
|
||||
const deviceRemoteSources =
|
||||
remoteSourcesByDevice[device.id] || [];
|
||||
|
||||
return (
|
||||
<DeviceCard
|
||||
key={device.id}
|
||||
device={device}
|
||||
volumes={deviceVolumes}
|
||||
remoteSources={deviceRemoteSources}
|
||||
jobs={deviceJobs}
|
||||
locations={deviceLocations}
|
||||
selectedLocationId={selectedLocationId}
|
||||
@@ -329,6 +354,7 @@ function ConnectionBadge({method, online, current, icon: customIcon, color: cust
|
||||
interface DeviceCardProps {
|
||||
device?: DeviceWithConnection;
|
||||
volumes: Volume[];
|
||||
remoteSources: SourceInfo[];
|
||||
jobs: JobListItem[];
|
||||
locations: Location[];
|
||||
selectedLocationId: string | null;
|
||||
@@ -340,6 +366,7 @@ interface DeviceCardProps {
|
||||
function DeviceCard({
|
||||
device,
|
||||
volumes,
|
||||
remoteSources,
|
||||
jobs,
|
||||
locations,
|
||||
selectedLocationId,
|
||||
@@ -509,7 +536,7 @@ function DeviceCard({
|
||||
index={idx}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
) : remoteSources.length > 0 ? null : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<div className="text-ink-faint">
|
||||
<HardDrive className="mx-auto mb-2 size-8 opacity-20" />
|
||||
@@ -517,6 +544,56 @@ function DeviceCard({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{remoteSources.map((source) => (
|
||||
<RemoteSourceRow key={source.id} source={source} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// A paired device's source, replicated locally through the peer-mount
|
||||
// plane. There is no capacity bar because the replica knows the source's
|
||||
// contents, not the drive underneath it.
|
||||
function RemoteSourceRow({source}: {source: SourceInfo}) {
|
||||
return (
|
||||
<div className="bg-app-box border-app-line/50 overflow-hidden rounded-lg border">
|
||||
<div className="flex h-[64px] items-center gap-3 px-3">
|
||||
<img
|
||||
src={HDDIcon}
|
||||
alt=""
|
||||
className="size-10 flex-shrink-0 opacity-80"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1.5 flex items-center gap-2">
|
||||
<span className="text-ink truncate text-sm font-semibold">
|
||||
{source.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-ink-dull flex h-[18px] items-center gap-1.5 text-[10px]">
|
||||
<span className="bg-app-box border-app-line rounded border px-1.5 py-0.5">
|
||||
Replica
|
||||
</span>
|
||||
{source.item_count > 0 && (
|
||||
<span className="bg-accent/20 border-accent/30 text-accent rounded border px-1.5 py-0.5 font-medium">
|
||||
{source.item_count.toLocaleString()} files
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 flex-col justify-between text-right">
|
||||
{source.total_bytes != null && source.total_bytes > 0 && (
|
||||
<div className="text-ink text-sm font-medium">
|
||||
{formatBytes(source.total_bytes)}
|
||||
</div>
|
||||
)}
|
||||
{source.last_synced && (
|
||||
<div className="text-ink-dull text-[10px]">
|
||||
synced{' '}
|
||||
{new Date(source.last_synced).toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2969,7 +2969,7 @@ export type OperationSnapshot = { broadcasts_sent: number; state_changes_broadca
|
||||
*/
|
||||
export type Origin =
|
||||
/**
|
||||
* Written at library creation from the platform's known folders.
|
||||
* Written by an older library initializer from the platform's known folders.
|
||||
*/
|
||||
"default" |
|
||||
/**
|
||||
@@ -3077,11 +3077,54 @@ lastSeen: string };
|
||||
|
||||
export type PairingSessionSummary = { id: string; state: SerializablePairingState; remote_device_id: string | null; expires_at: string | null };
|
||||
|
||||
export type PathAvailability = "available" | "permission_denied" | "missing" | "unavailable" | "remote" | "unsupported";
|
||||
|
||||
export type PathContextInput = { path: SdPath };
|
||||
|
||||
export type PathContextOutput = {
|
||||
/**
|
||||
* The path as the owning volume spells it. This removes APFS aliases from
|
||||
* all containment checks while the explorer can keep displaying its input.
|
||||
*/
|
||||
canonical_path: SdPath; availability: PathAvailability; map_state: PathMapState; indexing_root: string | null; watcher_state: PathWatcherState; watcher_root: string | null; volume: PathVolumeContext | null; source: PathSourceContext | null;
|
||||
/**
|
||||
* The closest explicit pin containing this path, if one exists.
|
||||
*/
|
||||
location: PathLocationContext | null;
|
||||
/**
|
||||
* A computed system Place at this exact path, such as Desktop.
|
||||
*/
|
||||
system_place: string | null; storage: PathStorageContext };
|
||||
|
||||
export type PathLocationContext = { id: string; name: string; root: SdPath; origin: Origin; exact: boolean };
|
||||
|
||||
export type PathMapState = "unseen" | "indexing" | "detailed" | "summarised";
|
||||
|
||||
/**
|
||||
* Path mapping for resolving virtual paths to actual storage locations
|
||||
*/
|
||||
export type PathMapping = { virtual_path: string; actual_path: string };
|
||||
|
||||
export type PathSourceContext = { id: string; name: string; root: string; attached: boolean };
|
||||
|
||||
export type PathStorageContext = {
|
||||
/**
|
||||
* The active volume arena can answer reads about this path.
|
||||
*/
|
||||
memory: boolean;
|
||||
/**
|
||||
* The volume arena has a machine-local restart snapshot.
|
||||
*/
|
||||
restart_cache: boolean;
|
||||
/**
|
||||
* The source has created its durable record store.
|
||||
*/
|
||||
source_store: boolean };
|
||||
|
||||
export type PathVolumeContext = { id: string; name: string; mount_point: string; tracked: boolean };
|
||||
|
||||
export type PathWatcherState = "active" | "inactive" | "unavailable";
|
||||
|
||||
/**
|
||||
* Per-peer activity information
|
||||
*/
|
||||
@@ -3753,7 +3796,17 @@ attached: boolean; total_bytes: number | null;
|
||||
* Last time the origin answered. Absent for an adapter, whose registry
|
||||
* tracks a sync cursor rather than an attachment.
|
||||
*/
|
||||
last_seen_at: string | null };
|
||||
last_seen_at: string | null;
|
||||
/**
|
||||
* The paired device this source was replicated from. Absent for a
|
||||
* source this library registered itself; a replica is read-only here
|
||||
* and its registry ops run on the owning device.
|
||||
*/
|
||||
device_id: string | null;
|
||||
/**
|
||||
* The owning device's display name, for a replica.
|
||||
*/
|
||||
device_label: string | null };
|
||||
|
||||
export type SourceItem = { id: string; external_id: string; title: string; preview: string | null; subtitle: string | null };
|
||||
|
||||
@@ -3782,7 +3835,7 @@ export type Space = {
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Human-friendly name (e.g., "All Devices", "Work Files")
|
||||
* Human-friendly name (e.g., "Default", "Work Files")
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
@@ -3933,6 +3986,16 @@ export type SpacesListOutput = { spaces: Space[] };
|
||||
|
||||
export type SpacesListQueryInput = null;
|
||||
|
||||
export type StartupIndexingDisposition = "started" | "already_started" | "disabled";
|
||||
|
||||
export type StartupIndexingInput = {
|
||||
/**
|
||||
* Ignore the automatic-start preference for an explicit CLI request.
|
||||
*/
|
||||
force?: boolean };
|
||||
|
||||
export type StartupIndexingOutput = { disposition: StartupIndexingDisposition };
|
||||
|
||||
/**
|
||||
* State transition event
|
||||
*/
|
||||
@@ -5015,6 +5078,7 @@ export type LibraryAction =
|
||||
| { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt }
|
||||
| { type: 'files.rename'; input: FileRenameInput; output: JobReceipt }
|
||||
| { type: 'indexing.start'; input: IndexInput; output: JobReceipt }
|
||||
| { type: 'indexing.startup'; input: StartupIndexingInput; output: StartupIndexingOutput }
|
||||
| { type: 'jobs.cancel'; input: JobCancelInput; output: JobCancelOutput }
|
||||
| { type: 'jobs.pause'; input: JobPauseInput; output: JobPauseOutput }
|
||||
| { type: 'jobs.resume'; input: JobResumeInput; output: JobResumeOutput }
|
||||
@@ -5100,6 +5164,7 @@ export type LibraryQuery =
|
||||
| { type: 'locations.list'; input: LocationsListQueryInput; output: LocationsListOutput }
|
||||
| { type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput }
|
||||
| { type: 'locations.validate_path'; input: ValidateLocationPathInput; output: ValidateLocationPathOutput }
|
||||
| { type: 'paths.context'; input: PathContextInput; output: PathContextOutput }
|
||||
| { type: 'redundancy.summary'; input: RedundancySummaryInput; output: RedundancySummaryOutput }
|
||||
| { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput }
|
||||
| { type: 'sources.get'; input: GetSourceInput; output: SourceInfo }
|
||||
@@ -5164,6 +5229,7 @@ export const WIRE_METHODS = {
|
||||
'files.delete': 'action:files.delete.input',
|
||||
'files.rename': 'action:files.rename.input',
|
||||
'indexing.start': 'action:indexing.start.input',
|
||||
'indexing.startup': 'action:indexing.startup.input',
|
||||
'jobs.cancel': 'action:jobs.cancel.input',
|
||||
'jobs.pause': 'action:jobs.pause.input',
|
||||
'jobs.resume': 'action:jobs.resume.input',
|
||||
@@ -5249,6 +5315,7 @@ export const WIRE_METHODS = {
|
||||
'locations.list': 'query:locations.list',
|
||||
'locations.suggested': 'query:locations.suggested',
|
||||
'locations.validate_path': 'query:locations.validate_path',
|
||||
'paths.context': 'query:paths.context',
|
||||
'redundancy.summary': 'query:redundancy.summary',
|
||||
'search.files': 'query:search.files',
|
||||
'sources.get': 'query:sources.get',
|
||||
|
||||
Reference in new issue
Block a user