mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-19 13:55:40 -04:00
Add cloud volume management operations
- Introduced VolumeCmd for handling cloud volume operations in the CLI. - Implemented VolumeAddCloudArgs and VolumeRemoveCloudArgs for adding and removing cloud storage volumes. - Created VolumeAddCloudAction and VolumeRemoveCloudAction for managing cloud volume actions. - Updated directory listing and file query operations to support cloud paths. - Enhanced volume backend integration to accommodate cloud storage services.
This commit is contained in:
@@ -8,3 +8,4 @@ pub mod logs;
|
||||
pub mod network;
|
||||
pub mod search;
|
||||
pub mod tag;
|
||||
pub mod volume;
|
||||
|
||||
127
apps/cli/src/domains/volume/args.rs
Normal file
127
apps/cli/src/domains/volume/args.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use clap::Args;
|
||||
use sd_core::{
|
||||
ops::volumes::{
|
||||
add_cloud::{CloudStorageConfig, VolumeAddCloudInput},
|
||||
remove_cloud::VolumeRemoveCloudInput,
|
||||
},
|
||||
volume::{backend::CloudServiceType, VolumeFingerprint},
|
||||
};
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct VolumeAddCloudArgs {
|
||||
/// Display name for the cloud volume
|
||||
pub name: String,
|
||||
|
||||
/// Cloud service type
|
||||
#[arg(long, value_enum)]
|
||||
pub service: CloudServiceArg,
|
||||
|
||||
/// S3 bucket name (for S3 service)
|
||||
#[arg(long, required_if_eq("service", "s3"))]
|
||||
pub bucket: Option<String>,
|
||||
|
||||
/// S3 region (for S3 service)
|
||||
#[arg(long, required_if_eq("service", "s3"))]
|
||||
pub region: Option<String>,
|
||||
|
||||
/// S3 access key ID (for S3 service)
|
||||
#[arg(long, required_if_eq("service", "s3"))]
|
||||
pub access_key_id: Option<String>,
|
||||
|
||||
/// S3 secret access key (for S3 service)
|
||||
#[arg(long, required_if_eq("service", "s3"))]
|
||||
pub secret_access_key: Option<String>,
|
||||
|
||||
/// Custom S3 endpoint (optional, for S3-compatible services like MinIO, R2, etc.)
|
||||
#[arg(long)]
|
||||
pub endpoint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Debug)]
|
||||
pub enum CloudServiceArg {
|
||||
S3,
|
||||
GoogleDrive,
|
||||
Dropbox,
|
||||
OneDrive,
|
||||
GoogleCloudStorage,
|
||||
AzureBlob,
|
||||
BackblazeB2,
|
||||
Wasabi,
|
||||
DigitalOceanSpaces,
|
||||
}
|
||||
|
||||
impl From<CloudServiceArg> for CloudServiceType {
|
||||
fn from(arg: CloudServiceArg) -> Self {
|
||||
match arg {
|
||||
CloudServiceArg::S3 => CloudServiceType::S3,
|
||||
CloudServiceArg::GoogleDrive => CloudServiceType::GoogleDrive,
|
||||
CloudServiceArg::Dropbox => CloudServiceType::Dropbox,
|
||||
CloudServiceArg::OneDrive => CloudServiceType::OneDrive,
|
||||
CloudServiceArg::GoogleCloudStorage => CloudServiceType::GoogleCloudStorage,
|
||||
CloudServiceArg::AzureBlob => CloudServiceType::AzureBlob,
|
||||
CloudServiceArg::BackblazeB2 => CloudServiceType::BackblazeB2,
|
||||
CloudServiceArg::Wasabi => CloudServiceType::Wasabi,
|
||||
CloudServiceArg::DigitalOceanSpaces => CloudServiceType::DigitalOceanSpaces,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VolumeAddCloudArgs {
|
||||
pub fn validate_and_build(self) -> Result<VolumeAddCloudInput, String> {
|
||||
let service = CloudServiceType::from(self.service.clone());
|
||||
|
||||
let config = match self.service {
|
||||
CloudServiceArg::S3 => {
|
||||
let bucket = self.bucket.ok_or("--bucket is required for S3")?;
|
||||
let region = self.region.ok_or("--region is required for S3")?;
|
||||
let access_key_id = self
|
||||
.access_key_id
|
||||
.ok_or("--access-key-id is required for S3")?;
|
||||
let secret_access_key = self
|
||||
.secret_access_key
|
||||
.ok_or("--secret-access-key is required for S3")?;
|
||||
|
||||
CloudStorageConfig::S3 {
|
||||
bucket,
|
||||
region,
|
||||
access_key_id,
|
||||
secret_access_key,
|
||||
endpoint: self.endpoint,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Service {:?} is not yet supported. Only S3 is currently available.",
|
||||
self.service
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(VolumeAddCloudInput {
|
||||
service,
|
||||
display_name: self.name,
|
||||
config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct VolumeRemoveCloudArgs {
|
||||
/// Volume fingerprint (from volume list)
|
||||
pub fingerprint: String,
|
||||
|
||||
/// Skip confirmation prompt
|
||||
#[arg(long, short = 'y', default_value_t = false)]
|
||||
pub yes: bool,
|
||||
}
|
||||
|
||||
impl TryFrom<VolumeRemoveCloudArgs> for VolumeRemoveCloudInput {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(args: VolumeRemoveCloudArgs) -> Result<Self, String> {
|
||||
let fingerprint = VolumeFingerprint::from_string(&args.fingerprint)
|
||||
.map_err(|e| format!("Invalid fingerprint: {}", e))?;
|
||||
|
||||
Ok(Self { fingerprint })
|
||||
}
|
||||
}
|
||||
65
apps/cli/src/domains/volume/mod.rs
Normal file
65
apps/cli/src/domains/volume/mod.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
mod args;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
|
||||
use crate::util::prelude::*;
|
||||
|
||||
use crate::context::Context;
|
||||
use sd_core::ops::volumes::{
|
||||
add_cloud::VolumeAddCloudOutput, remove_cloud::VolumeRemoveCloudOutput,
|
||||
};
|
||||
|
||||
use self::args::*;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum VolumeCmd {
|
||||
/// Add a cloud storage volume to the library
|
||||
AddCloud(VolumeAddCloudArgs),
|
||||
/// Remove a cloud storage volume from the library
|
||||
RemoveCloud(VolumeRemoveCloudArgs),
|
||||
}
|
||||
|
||||
pub async fn run(ctx: &Context, cmd: VolumeCmd) -> Result<()> {
|
||||
match cmd {
|
||||
VolumeCmd::AddCloud(args) => {
|
||||
let display_name = args.name.clone();
|
||||
let service = format!("{:?}", args.service);
|
||||
|
||||
let input = args.validate_and_build().map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
let out: VolumeAddCloudOutput = execute_action!(ctx, input);
|
||||
|
||||
print_output!(ctx, &out, |o: &VolumeAddCloudOutput| {
|
||||
println!(
|
||||
"Added cloud volume '{}' ({})",
|
||||
o.volume_name,
|
||||
o.fingerprint.short_id()
|
||||
);
|
||||
println!("Service: {:?}", o.service);
|
||||
println!("Fingerprint: {}", o.fingerprint);
|
||||
});
|
||||
}
|
||||
VolumeCmd::RemoveCloud(args) => {
|
||||
let fingerprint_display = args.fingerprint.clone();
|
||||
|
||||
confirm_or_abort(
|
||||
&format!(
|
||||
"This will remove cloud volume {} from the library. Credentials will be deleted. Continue?",
|
||||
fingerprint_display
|
||||
),
|
||||
args.yes,
|
||||
)?;
|
||||
|
||||
let input: sd_core::ops::volumes::remove_cloud::VolumeRemoveCloudInput =
|
||||
args.try_into().map_err(|e: String| anyhow::anyhow!(e))?;
|
||||
|
||||
let out: VolumeRemoveCloudOutput = execute_action!(ctx, input);
|
||||
|
||||
print_output!(ctx, &out, |o: &VolumeRemoveCloudOutput| {
|
||||
println!("Removed cloud volume {}", o.fingerprint);
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -57,6 +57,7 @@ use crate::domains::{
|
||||
network::{self, NetworkCmd},
|
||||
search::{self, SearchCmd},
|
||||
tag::{self, TagCmd},
|
||||
volume::{self, VolumeCmd},
|
||||
};
|
||||
|
||||
// OutputFormat is defined in context.rs and shared across domains
|
||||
@@ -195,6 +196,9 @@ enum Commands {
|
||||
/// Tag operations
|
||||
#[command(subcommand)]
|
||||
Tag(TagCmd),
|
||||
/// Volume operations
|
||||
#[command(subcommand)]
|
||||
Volume(VolumeCmd),
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -658,6 +662,7 @@ async fn run_client_command(
|
||||
Commands::Logs(cmd) => logs::run(&ctx, cmd).await?,
|
||||
Commands::Search(cmd) => search::run(&ctx, cmd).await?,
|
||||
Commands::Tag(cmd) => tag::run(&ctx, cmd).await?,
|
||||
Commands::Volume(cmd) => volume::run(&ctx, cmd).await?,
|
||||
_ => {} // Start and Stop are handled in main
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -14,6 +14,7 @@ use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::library_key_manager::LibraryKeyManager;
|
||||
use std::sync::Arc;
|
||||
|
||||
const KEYRING_SERVICE: &str = "SpacedriveCloudCredentials";
|
||||
|
||||
@@ -43,11 +44,11 @@ pub enum CloudCredentialError {
|
||||
|
||||
/// Manages cloud service credentials encrypted with library keys
|
||||
pub struct CloudCredentialManager {
|
||||
library_key_manager: LibraryKeyManager,
|
||||
library_key_manager: Arc<LibraryKeyManager>,
|
||||
}
|
||||
|
||||
impl CloudCredentialManager {
|
||||
pub fn new(library_key_manager: LibraryKeyManager) -> Self {
|
||||
pub fn new(library_key_manager: Arc<LibraryKeyManager>) -> Self {
|
||||
Self {
|
||||
library_key_manager,
|
||||
}
|
||||
@@ -274,7 +275,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_credential() {
|
||||
let library_key_manager = LibraryKeyManager::new().unwrap();
|
||||
let library_key_manager = Arc::new(LibraryKeyManager::new().unwrap());
|
||||
let manager = CloudCredentialManager::new(library_key_manager);
|
||||
|
||||
let library_id = Uuid::new_v4();
|
||||
|
||||
@@ -10,8 +10,24 @@ use serde_json::Value as JsonValue;
|
||||
use specta::Type;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::infra::db::entities::*;
|
||||
use crate::volume::VolumeBackend;
|
||||
|
||||
/// Domain representation of content identity
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct ContentIdentity {
|
||||
pub uuid: Uuid,
|
||||
pub kind: ContentKind,
|
||||
pub content_hash: String,
|
||||
pub integrity_hash: Option<String>,
|
||||
pub mime_type_id: Option<i32>,
|
||||
pub text_content: Option<String>,
|
||||
pub total_size: i64,
|
||||
pub entry_count: i32,
|
||||
pub first_seen_at: DateTime<Utc>,
|
||||
pub last_verified_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Type of content
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, IntEnum, Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -34,138 +50,36 @@ pub enum ContentKind {
|
||||
Key = 14,
|
||||
Executable = 15,
|
||||
Binary = 16,
|
||||
Spreadsheet = 17,
|
||||
Presentation = 18,
|
||||
Email = 19,
|
||||
Calendar = 20,
|
||||
Contact = 21,
|
||||
Web = 22,
|
||||
Shortcut = 23,
|
||||
Package = 24,
|
||||
ModelEntry = 25,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ContentKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
ContentKind::Unknown => "unknown",
|
||||
ContentKind::Image => "image",
|
||||
ContentKind::Video => "video",
|
||||
ContentKind::Audio => "audio",
|
||||
ContentKind::Document => "document",
|
||||
ContentKind::Archive => "archive",
|
||||
ContentKind::Code => "code",
|
||||
ContentKind::Text => "text",
|
||||
ContentKind::Database => "database",
|
||||
ContentKind::Book => "book",
|
||||
ContentKind::Font => "font",
|
||||
ContentKind::Mesh => "mesh",
|
||||
ContentKind::Config => "config",
|
||||
ContentKind::Encrypted => "encrypted",
|
||||
ContentKind::Key => "key",
|
||||
ContentKind::Executable => "executable",
|
||||
ContentKind::Binary => "binary",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ContentKind {
|
||||
fn from(name: &str) -> Self {
|
||||
match name {
|
||||
"image" => ContentKind::Image,
|
||||
"video" => ContentKind::Video,
|
||||
"audio" => ContentKind::Audio,
|
||||
"document" => ContentKind::Document,
|
||||
"archive" => ContentKind::Archive,
|
||||
"code" => ContentKind::Code,
|
||||
"text" => ContentKind::Text,
|
||||
"database" => ContentKind::Database,
|
||||
"book" => ContentKind::Book,
|
||||
"font" => ContentKind::Font,
|
||||
"mesh" => ContentKind::Mesh,
|
||||
"config" => ContentKind::Config,
|
||||
"encrypted" => ContentKind::Encrypted,
|
||||
"key" => ContentKind::Key,
|
||||
"executable" => ContentKind::Executable,
|
||||
"binary" => ContentKind::Binary,
|
||||
_ => ContentKind::Unknown,
|
||||
// Translate database entity into domain model
|
||||
impl From<content_identity::Model> for ContentIdentity {
|
||||
fn from(model: content_identity::Model) -> Self {
|
||||
Self {
|
||||
uuid: model.uuid.unwrap_or_else(Uuid::new_v4),
|
||||
kind: ContentKind::try_from(model.kind_id).unwrap_or(ContentKind::Unknown),
|
||||
content_hash: model.content_hash,
|
||||
integrity_hash: model.integrity_hash,
|
||||
mime_type_id: model.mime_type_id,
|
||||
text_content: model.text_content,
|
||||
total_size: model.total_size,
|
||||
entry_count: model.entry_count,
|
||||
first_seen_at: model.first_seen_at,
|
||||
last_verified_at: model.last_verified_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ContentKind {
|
||||
fn from(name: String) -> Self {
|
||||
Self::from(name.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Media-specific metadata
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct MediaData {
|
||||
/// Width in pixels (for images/video)
|
||||
pub width: Option<u32>,
|
||||
|
||||
/// Height in pixels (for images/video)
|
||||
pub height: Option<u32>,
|
||||
|
||||
/// Duration in seconds (for audio/video)
|
||||
pub duration: Option<f64>,
|
||||
|
||||
/// Bitrate in bits per second
|
||||
pub bitrate: Option<u32>,
|
||||
|
||||
/// Frame rate (for video)
|
||||
pub fps: Option<f32>,
|
||||
|
||||
/// EXIF data (for images)
|
||||
pub exif: Option<ExifData>,
|
||||
|
||||
/// Additional metadata as JSON
|
||||
pub extra: JsonValue,
|
||||
}
|
||||
|
||||
/// EXIF metadata for images
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct ExifData {
|
||||
/// Camera make
|
||||
pub make: Option<String>,
|
||||
|
||||
/// Camera model
|
||||
pub model: Option<String>,
|
||||
|
||||
/// Date taken
|
||||
pub date_taken: Option<DateTime<Utc>>,
|
||||
|
||||
/// GPS coordinates
|
||||
pub gps: Option<GpsCoordinates>,
|
||||
|
||||
/// ISO speed
|
||||
pub iso: Option<u32>,
|
||||
|
||||
/// Aperture (f-stop)
|
||||
pub aperture: Option<f32>,
|
||||
|
||||
/// Shutter speed in seconds
|
||||
pub shutter_speed: Option<f32>,
|
||||
|
||||
/// Focal length in mm
|
||||
pub focal_length: Option<f32>,
|
||||
}
|
||||
|
||||
/// GPS coordinates
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct GpsCoordinates {
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
pub altitude: Option<f32>,
|
||||
}
|
||||
|
||||
impl ContentKind {
|
||||
/// Determine content kind from MIME type
|
||||
pub fn from_mime_type(mime_type: &str) -> Self {
|
||||
match mime_type.split('/').next() {
|
||||
Some("image") => ContentKind::Image,
|
||||
Some("video") => ContentKind::Video,
|
||||
Some("audio") => ContentKind::Audio,
|
||||
Some("text") => ContentKind::Text,
|
||||
_ if mime_type.contains("pdf") => ContentKind::Document,
|
||||
_ if mime_type.contains("zip") || mime_type.contains("tar") => ContentKind::Archive,
|
||||
_ => ContentKind::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get content kind from file type
|
||||
pub fn from_file_type(file_type: &crate::filetype::FileType) -> Self {
|
||||
file_type.category
|
||||
@@ -213,7 +127,7 @@ impl ContentHashGenerator {
|
||||
hasher.finalize().to_hex()[..16].to_string()
|
||||
}
|
||||
|
||||
/// Generate content hash using a volume backend (supports cloud storage)
|
||||
/// Generate content hash using a volume backend
|
||||
///
|
||||
/// This uses the same sampling algorithm but works with any VolumeBackend,
|
||||
/// enabling efficient content hashing for cloud files without full downloads.
|
||||
@@ -321,34 +235,75 @@ pub enum ContentHashError {
|
||||
FileTooLarge,
|
||||
}
|
||||
|
||||
/// Domain representation of content identity
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct ContentIdentity {
|
||||
pub uuid: Uuid,
|
||||
pub kind: ContentKind,
|
||||
pub hash: String,
|
||||
pub media_data: Option<MediaData>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
impl std::fmt::Display for ContentKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
ContentKind::Unknown => "unknown",
|
||||
ContentKind::Image => "image",
|
||||
ContentKind::Video => "video",
|
||||
ContentKind::Audio => "audio",
|
||||
ContentKind::Document => "document",
|
||||
ContentKind::Archive => "archive",
|
||||
ContentKind::Code => "code",
|
||||
ContentKind::Text => "text",
|
||||
ContentKind::Database => "database",
|
||||
ContentKind::Book => "book",
|
||||
ContentKind::Font => "font",
|
||||
ContentKind::Mesh => "mesh",
|
||||
ContentKind::Config => "config",
|
||||
ContentKind::Encrypted => "encrypted",
|
||||
ContentKind::Key => "key",
|
||||
ContentKind::Executable => "executable",
|
||||
ContentKind::Binary => "binary",
|
||||
ContentKind::Spreadsheet => "spreadsheet",
|
||||
ContentKind::Presentation => "presentation",
|
||||
ContentKind::Email => "email",
|
||||
ContentKind::Calendar => "calendar",
|
||||
ContentKind::Contact => "contact",
|
||||
ContentKind::Web => "web",
|
||||
ContentKind::Shortcut => "shortcut",
|
||||
ContentKind::Package => "package",
|
||||
ContentKind::ModelEntry => "model_entry",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::infra::db::entities::content_identity::Model> for ContentIdentity {
|
||||
fn from(model: crate::infra::db::entities::content_identity::Model) -> Self {
|
||||
Self {
|
||||
uuid: model.uuid.unwrap_or_else(Uuid::new_v4),
|
||||
kind: ContentKind::Unknown, // TODO: Implement proper conversion from kind_id
|
||||
hash: model.content_hash,
|
||||
media_data: model.media_data.map(|json| {
|
||||
serde_json::from_value(json).unwrap_or_else(|_| MediaData {
|
||||
width: None,
|
||||
height: None,
|
||||
duration: None,
|
||||
bitrate: None,
|
||||
fps: None,
|
||||
exif: None,
|
||||
extra: serde_json::Value::Null,
|
||||
})
|
||||
}),
|
||||
created_at: model.first_seen_at,
|
||||
impl From<&str> for ContentKind {
|
||||
fn from(name: &str) -> Self {
|
||||
match name {
|
||||
"image" => ContentKind::Image,
|
||||
"video" => ContentKind::Video,
|
||||
"audio" => ContentKind::Audio,
|
||||
"document" => ContentKind::Document,
|
||||
"archive" => ContentKind::Archive,
|
||||
"code" => ContentKind::Code,
|
||||
"text" => ContentKind::Text,
|
||||
"database" => ContentKind::Database,
|
||||
"book" => ContentKind::Book,
|
||||
"font" => ContentKind::Font,
|
||||
"mesh" => ContentKind::Mesh,
|
||||
"config" => ContentKind::Config,
|
||||
"encrypted" => ContentKind::Encrypted,
|
||||
"key" => ContentKind::Key,
|
||||
"executable" => ContentKind::Executable,
|
||||
"binary" => ContentKind::Binary,
|
||||
"spreadsheet" => ContentKind::Spreadsheet,
|
||||
"presentation" => ContentKind::Presentation,
|
||||
"email" => ContentKind::Email,
|
||||
"calendar" => ContentKind::Calendar,
|
||||
"contact" => ContentKind::Contact,
|
||||
"web" => ContentKind::Web,
|
||||
"shortcut" => ContentKind::Shortcut,
|
||||
"package" => ContentKind::Package,
|
||||
"model_entry" => ContentKind::ModelEntry,
|
||||
_ => ContentKind::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ContentKind {
|
||||
fn from(name: String) -> Self {
|
||||
Self::from(name.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,10 @@ impl SdPathSerialized {
|
||||
device_id: *device_id,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
}),
|
||||
SdPath::Cloud { .. } => None, // Can't serialize cloud paths to this format
|
||||
SdPath::Cloud { volume_id, path } => Some(Self {
|
||||
device_id: *volume_id, // Use volume_id as device_id for cloud paths
|
||||
path: path.clone(),
|
||||
}),
|
||||
SdPath::Content { .. } => None, // Can't serialize content paths to this format
|
||||
}
|
||||
}
|
||||
@@ -184,11 +187,7 @@ impl TryFrom<(crate::infra::db::entities::entry::Model, SdPath)> for Entry {
|
||||
) -> Result<Self, Self::Error> {
|
||||
let device_uuid = match &parent_sd_path {
|
||||
SdPath::Physical { device_id, .. } => *device_id,
|
||||
SdPath::Cloud { .. } => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Cloud storage paths not yet supported for directory listing"
|
||||
))
|
||||
}
|
||||
SdPath::Cloud { volume_id, .. } => *volume_id,
|
||||
SdPath::Content { .. } => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Content-addressed paths not supported for directory listing"
|
||||
|
||||
@@ -347,7 +347,6 @@ mod tests {
|
||||
uuid: Uuid::new_v4(),
|
||||
kind: ContentKind::Image,
|
||||
hash: "abc123".to_string(),
|
||||
media_data: None,
|
||||
created_at: Utc::now(),
|
||||
};
|
||||
|
||||
|
||||
@@ -17,9 +17,7 @@ pub mod volume;
|
||||
|
||||
// Re-export commonly used types
|
||||
pub use addressing::{PathResolutionError, SdPath, SdPathBatch, SdPathParseError};
|
||||
pub use content_identity::{
|
||||
ContentHashError, ContentHashGenerator, ContentIdentity, ContentKind, MediaData,
|
||||
};
|
||||
pub use content_identity::{ContentHashError, ContentHashGenerator, ContentIdentity, ContentKind};
|
||||
pub use device::{Device, OperatingSystem};
|
||||
pub use entry::{Entry, EntryKind, SdPathSerialized};
|
||||
pub use file::{File, FileConstructionData, Sidecar};
|
||||
|
||||
@@ -13,6 +13,14 @@ pub static BUILTIN_DEFINITIONS: Lazy<Vec<&'static str>> = Lazy::new(|| {
|
||||
include_str!("definitions/documents.toml"),
|
||||
include_str!("definitions/code.toml"),
|
||||
include_str!("definitions/archives.toml"),
|
||||
include_str!("definitions/spreadsheets.toml"),
|
||||
include_str!("definitions/presentations.toml"),
|
||||
include_str!("definitions/email.toml"),
|
||||
include_str!("definitions/calendar.toml"),
|
||||
include_str!("definitions/contacts.toml"),
|
||||
include_str!("definitions/web.toml"),
|
||||
include_str!("definitions/shortcuts.toml"),
|
||||
include_str!("definitions/packages.toml"),
|
||||
include_str!("definitions/misc.toml"),
|
||||
]
|
||||
});
|
||||
|
||||
48
core/src/filetype/definitions/calendar.toml
Normal file
48
core/src/filetype/definitions/calendar.toml
Normal file
@@ -0,0 +1,48 @@
|
||||
# Calendar file type definitions
|
||||
|
||||
[[file_types]]
|
||||
id = "text/calendar"
|
||||
name = "iCalendar"
|
||||
extensions = ["ics", "ical", "ifb", "icalendar"]
|
||||
mime_types = ["text/calendar"]
|
||||
uti = "com.apple.ical.ics"
|
||||
category = "calendar"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
|
||||
[[file_types]]
|
||||
id = "text/x-vcalendar"
|
||||
name = "vCalendar"
|
||||
extensions = ["vcs"]
|
||||
mime_types = ["text/x-vcalendar"]
|
||||
category = "calendar"
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
legacy = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.google-apps.calendar"
|
||||
name = "Google Calendar"
|
||||
extensions = ["gcalendar"]
|
||||
mime_types = ["application/vnd.google-apps.calendar"]
|
||||
category = "calendar"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
google_workspace = true
|
||||
cloud_native = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-outlook-calendar"
|
||||
name = "Outlook Calendar Item"
|
||||
extensions = ["icalendar"]
|
||||
mime_types = ["text/calendar"]
|
||||
category = "calendar"
|
||||
priority = 95
|
||||
|
||||
[file_types.metadata]
|
||||
outlook = true
|
||||
47
core/src/filetype/definitions/contacts.toml
Normal file
47
core/src/filetype/definitions/contacts.toml
Normal file
@@ -0,0 +1,47 @@
|
||||
# Contact file type definitions
|
||||
|
||||
[[file_types]]
|
||||
id = "text/vcard"
|
||||
name = "vCard"
|
||||
extensions = ["vcf", "vcard"]
|
||||
mime_types = ["text/vcard", "text/x-vcard"]
|
||||
uti = "public.vcard"
|
||||
category = "contact"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
|
||||
[[file_types]]
|
||||
id = "text/directory"
|
||||
name = "vCard Directory"
|
||||
extensions = ["vcf"]
|
||||
mime_types = ["text/directory"]
|
||||
category = "contact"
|
||||
priority = 95
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.apple.contacts"
|
||||
name = "Apple Contacts"
|
||||
extensions = ["abcdp"]
|
||||
mime_types = ["application/vnd.apple.contacts"]
|
||||
category = "contact"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
apple = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-ldif"
|
||||
name = "LDAP Data Interchange Format"
|
||||
extensions = ["ldif"]
|
||||
mime_types = ["application/x-ldif"]
|
||||
category = "contact"
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
directory_service = true
|
||||
97
core/src/filetype/definitions/email.toml
Normal file
97
core/src/filetype/definitions/email.toml
Normal file
@@ -0,0 +1,97 @@
|
||||
# Email file type definitions
|
||||
|
||||
[[file_types]]
|
||||
id = "message/rfc822"
|
||||
name = "Email Message"
|
||||
extensions = ["eml", "emlx"]
|
||||
mime_types = ["message/rfc822"]
|
||||
category = "email"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
text_based = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.ms-outlook"
|
||||
name = "Outlook Message"
|
||||
extensions = ["msg"]
|
||||
mime_types = ["application/vnd.ms-outlook"]
|
||||
uti = "com.microsoft.outlook.msg"
|
||||
category = "email"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "D0 CF 11 E0 A1 B1 1A E1"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[[file_types]]
|
||||
id = "application/mbox"
|
||||
name = "MBOX Mailbox"
|
||||
extensions = ["mbox"]
|
||||
mime_types = ["application/mbox"]
|
||||
category = "email"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
text_based = true
|
||||
container = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.ms-outlook.pst"
|
||||
name = "Outlook Personal Folders"
|
||||
extensions = ["pst"]
|
||||
mime_types = ["application/vnd.ms-outlook.pst"]
|
||||
category = "email"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "21 42 44 4E"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
container = true
|
||||
database = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.ms-outlook.ost"
|
||||
name = "Outlook Offline Folders"
|
||||
extensions = ["ost"]
|
||||
mime_types = ["application/vnd.ms-outlook.ost"]
|
||||
category = "email"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "21 42 44 4E"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
container = true
|
||||
database = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.apple.mail"
|
||||
name = "Apple Mail"
|
||||
extensions = ["mailbundle"]
|
||||
mime_types = ["application/vnd.apple.mail"]
|
||||
category = "email"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
apple = true
|
||||
bundle = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-gmail-archive"
|
||||
name = "Gmail Archive"
|
||||
extensions = ["mbox"]
|
||||
mime_types = ["application/x-gmail-archive"]
|
||||
category = "email"
|
||||
priority = 95
|
||||
|
||||
[file_types.metadata]
|
||||
text_based = true
|
||||
container = true
|
||||
google = true
|
||||
@@ -59,75 +59,6 @@ pattern = "4D 5A"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-apple-diskimage"
|
||||
name = "Apple Disk Image"
|
||||
extensions = ["dmg"]
|
||||
mime_types = ["application/x-apple-diskimage"]
|
||||
uti = "com.apple.disk-image"
|
||||
category = "executable"
|
||||
priority = 100
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.android.package-archive"
|
||||
name = "Android Package"
|
||||
extensions = ["apk"]
|
||||
mime_types = ["application/vnd.android.package-archive"]
|
||||
category = "executable"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-debian-package"
|
||||
name = "Debian Package"
|
||||
extensions = ["deb"]
|
||||
mime_types = ["application/x-debian-package", "application/vnd.debian.binary-package"]
|
||||
category = "executable"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "21 3C 61 72 63 68 3E"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-redhat-package"
|
||||
name = "RPM Package"
|
||||
extensions = ["rpm"]
|
||||
mime_types = ["application/x-rpm", "application/x-redhat-package-manager"]
|
||||
category = "executable"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "ED AB EE DB"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-apple-installer"
|
||||
name = "macOS Package"
|
||||
extensions = ["pkg"]
|
||||
mime_types = ["application/x-apple-installer"]
|
||||
category = "executable"
|
||||
priority = 100
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-msi"
|
||||
name = "Windows Installer"
|
||||
extensions = ["msi"]
|
||||
mime_types = ["application/x-msi", "application/x-msdownload"]
|
||||
category = "executable"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "D0 CF 11 E0 A1 B1 1A E1"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[[file_types]]
|
||||
id = "application/java-archive"
|
||||
name = "Java Archive"
|
||||
@@ -157,18 +88,6 @@ priority = 80
|
||||
text_file = true
|
||||
windows_script = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-apple-application"
|
||||
name = "macOS Application"
|
||||
extensions = ["app"]
|
||||
mime_types = ["application/x-apple-application"]
|
||||
uti = "com.apple.application-bundle"
|
||||
category = "executable"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
bundle = true
|
||||
|
||||
# Fonts
|
||||
[[file_types]]
|
||||
id = "font/ttf"
|
||||
@@ -377,19 +296,6 @@ priority = 80
|
||||
text_file = true
|
||||
feed = true
|
||||
|
||||
[[file_types]]
|
||||
id = "text/csv"
|
||||
name = "CSV"
|
||||
extensions = ["csv"]
|
||||
mime_types = ["text/csv"]
|
||||
uti = "public.comma-separated-values-text"
|
||||
category = "config"
|
||||
priority = 70
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
tabular = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-config"
|
||||
name = "Configuration File"
|
||||
@@ -523,4 +429,4 @@ name = "Apple Keychain"
|
||||
extensions = ["keychain"]
|
||||
mime_types = ["application/x-apple-keychain"]
|
||||
category = "key"
|
||||
priority = 100
|
||||
priority = 100
|
||||
|
||||
185
core/src/filetype/definitions/packages.toml
Normal file
185
core/src/filetype/definitions/packages.toml
Normal file
@@ -0,0 +1,185 @@
|
||||
# Package and installer file type definitions
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-debian-package"
|
||||
name = "Debian Package"
|
||||
extensions = ["deb"]
|
||||
mime_types = ["application/x-debian-package", "application/vnd.debian.binary-package"]
|
||||
category = "package"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "21 3C 61 72 63 68 3E"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
linux = true
|
||||
installer = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-redhat-package"
|
||||
name = "RPM Package"
|
||||
extensions = ["rpm"]
|
||||
mime_types = ["application/x-rpm", "application/x-redhat-package-manager"]
|
||||
category = "package"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "ED AB EE DB"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
linux = true
|
||||
installer = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-apple-installer"
|
||||
name = "macOS Package"
|
||||
extensions = ["pkg"]
|
||||
mime_types = ["application/x-apple-installer"]
|
||||
uti = "com.apple.installer-package-archive"
|
||||
category = "package"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
apple = true
|
||||
installer = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-apple-diskimage"
|
||||
name = "Apple Disk Image"
|
||||
extensions = ["dmg"]
|
||||
mime_types = ["application/x-apple-diskimage"]
|
||||
uti = "com.apple.disk-image"
|
||||
category = "package"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
apple = true
|
||||
installer = true
|
||||
disk_image = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-msi"
|
||||
name = "Windows Installer"
|
||||
extensions = ["msi"]
|
||||
mime_types = ["application/x-msi", "application/x-msdownload"]
|
||||
category = "package"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "D0 CF 11 E0 A1 B1 1A E1"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
windows = true
|
||||
installer = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.android.package-archive"
|
||||
name = "Android Package"
|
||||
extensions = ["apk"]
|
||||
mime_types = ["application/vnd.android.package-archive"]
|
||||
category = "package"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
android = true
|
||||
installer = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-xpinstall"
|
||||
name = "Firefox Extension"
|
||||
extensions = ["xpi"]
|
||||
mime_types = ["application/x-xpinstall"]
|
||||
category = "package"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
firefox = true
|
||||
extension = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-chrome-extension"
|
||||
name = "Chrome Extension"
|
||||
extensions = ["crx"]
|
||||
mime_types = ["application/x-chrome-extension"]
|
||||
category = "package"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "43 72 32 34"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
chrome = true
|
||||
extension = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.snap"
|
||||
name = "Snap Package"
|
||||
extensions = ["snap"]
|
||||
mime_types = ["application/vnd.snap"]
|
||||
category = "package"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
linux = true
|
||||
installer = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-flatpak"
|
||||
name = "Flatpak Package"
|
||||
extensions = ["flatpak", "flatpakref"]
|
||||
mime_types = ["application/x-flatpak"]
|
||||
category = "package"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
linux = true
|
||||
installer = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.microsoft.portable-executable"
|
||||
name = "Windows Installer (MSI)"
|
||||
extensions = ["msix", "appx"]
|
||||
mime_types = ["application/vnd.microsoft.portable-executable"]
|
||||
category = "package"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
windows = true
|
||||
installer = true
|
||||
uwp = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-apple-bundle"
|
||||
name = "macOS Application Bundle"
|
||||
extensions = ["app"]
|
||||
mime_types = ["application/x-apple-application"]
|
||||
uti = "com.apple.application-bundle"
|
||||
category = "package"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
apple = true
|
||||
bundle = true
|
||||
111
core/src/filetype/definitions/presentations.toml
Normal file
111
core/src/filetype/definitions/presentations.toml
Normal file
@@ -0,0 +1,111 @@
|
||||
# Presentation file type definitions
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.ms-powerpoint"
|
||||
name = "Microsoft PowerPoint"
|
||||
extensions = ["ppt"]
|
||||
mime_types = ["application/vnd.ms-powerpoint"]
|
||||
uti = "com.microsoft.powerpoint.ppt"
|
||||
category = "presentation"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "D0 CF 11 E0 A1 B1 1A E1"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
name = "Microsoft PowerPoint (OpenXML)"
|
||||
extensions = ["pptx"]
|
||||
mime_types = ["application/vnd.openxmlformats-officedocument.presentationml.presentation"]
|
||||
uti = "org.openxmlformats.presentationml.presentation"
|
||||
category = "presentation"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.openxmlformats-officedocument.presentationml.template"
|
||||
name = "PowerPoint Template (OpenXML)"
|
||||
extensions = ["potx"]
|
||||
mime_types = ["application/vnd.openxmlformats-officedocument.presentationml.template"]
|
||||
category = "presentation"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.ms-powerpoint.presentation.macroEnabled.12"
|
||||
name = "PowerPoint Macro-Enabled"
|
||||
extensions = ["pptm"]
|
||||
mime_types = ["application/vnd.ms-powerpoint.presentation.macroEnabled.12"]
|
||||
category = "presentation"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.openxmlformats-officedocument.presentationml.slideshow"
|
||||
name = "PowerPoint Slideshow"
|
||||
extensions = ["ppsx"]
|
||||
mime_types = ["application/vnd.openxmlformats-officedocument.presentationml.slideshow"]
|
||||
category = "presentation"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.oasis.opendocument.presentation"
|
||||
name = "OpenDocument Presentation"
|
||||
extensions = ["odp"]
|
||||
mime_types = ["application/vnd.oasis.opendocument.presentation"]
|
||||
uti = "org.oasis-open.opendocument.presentation"
|
||||
category = "presentation"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-iwork-keynote-sffkey"
|
||||
name = "Apple Keynote"
|
||||
extensions = ["key"]
|
||||
mime_types = ["application/x-iwork-keynote-sffkey"]
|
||||
uti = "com.apple.iwork.keynote.key"
|
||||
category = "presentation"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
iwork = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.google-apps.presentation"
|
||||
name = "Google Slides"
|
||||
extensions = ["gslides"]
|
||||
mime_types = ["application/vnd.google-apps.presentation"]
|
||||
category = "presentation"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
google_workspace = true
|
||||
cloud_native = true
|
||||
87
core/src/filetype/definitions/shortcuts.toml
Normal file
87
core/src/filetype/definitions/shortcuts.toml
Normal file
@@ -0,0 +1,87 @@
|
||||
# Shortcut and link file type definitions
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-ms-shortcut"
|
||||
name = "Windows Shortcut"
|
||||
extensions = ["lnk"]
|
||||
mime_types = ["application/x-ms-shortcut"]
|
||||
uti = "com.microsoft.windows-shortcut"
|
||||
category = "shortcut"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "4C 00 00 00 01 14 02 00"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[[file_types]]
|
||||
id = "application/internet-shortcut"
|
||||
name = "Windows Internet Shortcut"
|
||||
extensions = ["url"]
|
||||
mime_types = ["application/internet-shortcut"]
|
||||
category = "shortcut"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
internet = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-apple-alias"
|
||||
name = "macOS Alias"
|
||||
extensions = ["alias"]
|
||||
mime_types = ["application/x-apple-alias"]
|
||||
uti = "com.apple.alias-file"
|
||||
category = "shortcut"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
apple = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-apple-webloc"
|
||||
name = "macOS Web Location"
|
||||
extensions = ["webloc"]
|
||||
mime_types = ["application/x-apple-webloc"]
|
||||
uti = "com.apple.web-internet-location"
|
||||
category = "shortcut"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
apple = true
|
||||
internet = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-desktop"
|
||||
name = "Linux Desktop Entry"
|
||||
extensions = ["desktop"]
|
||||
mime_types = ["application/x-desktop"]
|
||||
category = "shortcut"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
linux = true
|
||||
|
||||
[[file_types]]
|
||||
id = "inode/symlink"
|
||||
name = "Symbolic Link"
|
||||
extensions = []
|
||||
mime_types = ["inode/symlink"]
|
||||
category = "shortcut"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
filesystem = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-wine-extension-ini"
|
||||
name = "Wine Desktop Entry"
|
||||
extensions = ["lnk"]
|
||||
mime_types = ["application/x-wine-extension-ini"]
|
||||
category = "shortcut"
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
wine = true
|
||||
131
core/src/filetype/definitions/spreadsheets.toml
Normal file
131
core/src/filetype/definitions/spreadsheets.toml
Normal file
@@ -0,0 +1,131 @@
|
||||
# Spreadsheet file type definitions
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.ms-excel"
|
||||
name = "Microsoft Excel"
|
||||
extensions = ["xls"]
|
||||
mime_types = ["application/vnd.ms-excel"]
|
||||
uti = "com.microsoft.excel.xls"
|
||||
category = "spreadsheet"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "D0 CF 11 E0 A1 B1 1A E1"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
name = "Microsoft Excel (OpenXML)"
|
||||
extensions = ["xlsx"]
|
||||
mime_types = ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]
|
||||
uti = "org.openxmlformats.spreadsheetml.sheet"
|
||||
category = "spreadsheet"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.openxmlformats-officedocument.spreadsheetml.template"
|
||||
name = "Excel Template (OpenXML)"
|
||||
extensions = ["xltx"]
|
||||
mime_types = ["application/vnd.openxmlformats-officedocument.spreadsheetml.template"]
|
||||
category = "spreadsheet"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.ms-excel.sheet.macroEnabled.12"
|
||||
name = "Excel Macro-Enabled"
|
||||
extensions = ["xlsm"]
|
||||
mime_types = ["application/vnd.ms-excel.sheet.macroEnabled.12"]
|
||||
category = "spreadsheet"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.ms-excel.sheet.binary.macroEnabled.12"
|
||||
name = "Excel Binary Workbook"
|
||||
extensions = ["xlsb"]
|
||||
mime_types = ["application/vnd.ms-excel.sheet.binary.macroEnabled.12"]
|
||||
category = "spreadsheet"
|
||||
priority = 100
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.oasis.opendocument.spreadsheet"
|
||||
name = "OpenDocument Spreadsheet"
|
||||
extensions = ["ods"]
|
||||
mime_types = ["application/vnd.oasis.opendocument.spreadsheet"]
|
||||
uti = "org.oasis-open.opendocument.spreadsheet"
|
||||
category = "spreadsheet"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-iwork-numbers-sffnumbers"
|
||||
name = "Apple Numbers"
|
||||
extensions = ["numbers"]
|
||||
mime_types = ["application/x-iwork-numbers-sffnumbers"]
|
||||
uti = "com.apple.iwork.numbers.numbers"
|
||||
category = "spreadsheet"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "50 4B 03 04"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
iwork = true
|
||||
|
||||
[[file_types]]
|
||||
id = "text/csv"
|
||||
name = "CSV"
|
||||
extensions = ["csv"]
|
||||
mime_types = ["text/csv"]
|
||||
uti = "public.comma-separated-values-text"
|
||||
category = "spreadsheet"
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
tabular = true
|
||||
|
||||
[[file_types]]
|
||||
id = "text/tab-separated-values"
|
||||
name = "TSV"
|
||||
extensions = ["tsv"]
|
||||
mime_types = ["text/tab-separated-values"]
|
||||
category = "spreadsheet"
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
tabular = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/vnd.google-apps.spreadsheet"
|
||||
name = "Google Sheets"
|
||||
extensions = ["gsheet"]
|
||||
mime_types = ["application/vnd.google-apps.spreadsheet"]
|
||||
category = "spreadsheet"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
google_workspace = true
|
||||
cloud_native = true
|
||||
110
core/src/filetype/definitions/web.toml
Normal file
110
core/src/filetype/definitions/web.toml
Normal file
@@ -0,0 +1,110 @@
|
||||
# Web file type definitions
|
||||
|
||||
[[file_types]]
|
||||
id = "text/html"
|
||||
name = "HTML Document"
|
||||
extensions = ["html", "htm"]
|
||||
mime_types = ["text/html"]
|
||||
uti = "public.html"
|
||||
category = "web"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "3C 21 44 4F 43 54 59 50 45 20 68 74 6D 6C"
|
||||
offset = 0
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
markup = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/xhtml+xml"
|
||||
name = "XHTML Document"
|
||||
extensions = ["xhtml", "xht"]
|
||||
mime_types = ["application/xhtml+xml"]
|
||||
uti = "public.xhtml"
|
||||
category = "web"
|
||||
priority = 100
|
||||
|
||||
[[file_types.magic_bytes]]
|
||||
pattern = "3C 3F 78 6D 6C"
|
||||
offset = 0
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
markup = true
|
||||
|
||||
[[file_types]]
|
||||
id = "message/rfc822-html"
|
||||
name = "MHTML Web Archive"
|
||||
extensions = ["mhtml", "mht"]
|
||||
mime_types = ["message/rfc822", "multipart/related"]
|
||||
category = "web"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
archive = true
|
||||
web_archive = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-webarchive"
|
||||
name = "Safari Web Archive"
|
||||
extensions = ["webarchive"]
|
||||
mime_types = ["application/x-webarchive"]
|
||||
uti = "com.apple.webarchive"
|
||||
category = "web"
|
||||
priority = 100
|
||||
|
||||
[file_types.metadata]
|
||||
apple = true
|
||||
archive = true
|
||||
web_archive = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-httpd-php"
|
||||
name = "PHP Script"
|
||||
extensions = ["php", "phtml"]
|
||||
mime_types = ["application/x-httpd-php", "text/x-php"]
|
||||
category = "web"
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
server_side = true
|
||||
|
||||
[[file_types]]
|
||||
id = "text/asp"
|
||||
name = "ASP Script"
|
||||
extensions = ["asp", "aspx"]
|
||||
mime_types = ["text/asp"]
|
||||
category = "web"
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
server_side = true
|
||||
|
||||
[[file_types]]
|
||||
id = "application/x-jsp"
|
||||
name = "JavaServer Pages"
|
||||
extensions = ["jsp"]
|
||||
mime_types = ["application/x-jsp"]
|
||||
category = "web"
|
||||
priority = 90
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
server_side = true
|
||||
|
||||
[[file_types]]
|
||||
id = "text/x-component"
|
||||
name = "Web Component"
|
||||
extensions = ["htc"]
|
||||
mime_types = ["text/x-component"]
|
||||
category = "web"
|
||||
priority = 80
|
||||
|
||||
[file_types.metadata]
|
||||
text_file = true
|
||||
@@ -331,6 +331,14 @@ impl FileTypeRegistry {
|
||||
"config" => ContentKind::Config,
|
||||
"encrypted" => ContentKind::Encrypted,
|
||||
"key" => ContentKind::Key,
|
||||
"spreadsheet" => ContentKind::Spreadsheet,
|
||||
"presentation" => ContentKind::Presentation,
|
||||
"email" => ContentKind::Email,
|
||||
"calendar" => ContentKind::Calendar,
|
||||
"contact" => ContentKind::Contact,
|
||||
"web" => ContentKind::Web,
|
||||
"shortcut" => ContentKind::Shortcut,
|
||||
"package" => ContentKind::Package,
|
||||
_ => ContentKind::Unknown,
|
||||
};
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ pub struct Model {
|
||||
pub integrity_hash: Option<String>, // Full hash for file validation (generated by validate job)
|
||||
pub content_hash: String, // Fast sampled hash for deduplication (generated during content identification)
|
||||
pub mime_type_id: Option<i32>,
|
||||
pub kind_id: i32, // ContentKind foreign key
|
||||
pub media_data: Option<Json>, // MediaData as JSON
|
||||
pub kind_id: i32, // ContentKind foreign key
|
||||
pub text_content: Option<String>,
|
||||
pub total_size: i64, // Size of one instance of this content
|
||||
pub entry_count: i32, // Entries in THIS library only
|
||||
|
||||
@@ -214,7 +214,6 @@ impl MigrationTrait for Migration {
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(ContentIdentities::MediaData).json())
|
||||
.col(ColumnDef::new(ContentIdentities::TextContent).text())
|
||||
.col(
|
||||
ColumnDef::new(ContentIdentities::TotalSize)
|
||||
@@ -835,7 +834,6 @@ enum ContentIdentities {
|
||||
ContentHash,
|
||||
MimeTypeId,
|
||||
KindId,
|
||||
MediaData,
|
||||
TextContent,
|
||||
TotalSize,
|
||||
EntryCount,
|
||||
|
||||
@@ -156,9 +156,14 @@ impl LibraryQuery for DirectoryListingQuery {
|
||||
e.parent_id as entry_parent_id,
|
||||
ci.id as content_identity_id,
|
||||
ci.uuid as content_identity_uuid,
|
||||
ci.content_hash as content_identity_hash,
|
||||
ci.media_data as content_identity_media_data,
|
||||
ci.first_seen_at as content_identity_first_seen_at,
|
||||
ci.content_hash as content_hash,
|
||||
ci.integrity_hash as integrity_hash,
|
||||
ci.mime_type_id as mime_type_id,
|
||||
ci.text_content as text_content,
|
||||
ci.total_size as total_size,
|
||||
ci.entry_count as entry_count,
|
||||
ci.first_seen_at as first_seen_at,
|
||||
ci.last_verified_at as last_verified_at,
|
||||
ck.id as content_kind_id,
|
||||
ck.name as content_kind_name
|
||||
FROM entries e
|
||||
@@ -233,17 +238,19 @@ impl LibraryQuery for DirectoryListingQuery {
|
||||
let entry_inode: Option<i64> = row.try_get("", "entry_inode").ok();
|
||||
|
||||
// Content identity data
|
||||
let content_identity_id: Option<i32> = row.try_get("", "content_identity_id").ok();
|
||||
let content_identity_uuid: Option<Uuid> = row.try_get("", "content_identity_uuid").ok();
|
||||
let content_identity_hash: Option<String> =
|
||||
row.try_get("", "content_identity_hash").ok();
|
||||
let content_identity_media_data: Option<serde_json::Value> =
|
||||
row.try_get("", "content_identity_media_data").ok();
|
||||
let content_identity_first_seen_at: Option<chrono::DateTime<chrono::Utc>> =
|
||||
row.try_get("", "content_identity_first_seen_at").ok();
|
||||
let content_hash: Option<String> = row.try_get("", "content_hash").ok();
|
||||
let integrity_hash: Option<String> = row.try_get("", "integrity_hash").ok();
|
||||
let mime_type_id: Option<i32> = row.try_get("", "mime_type_id").ok();
|
||||
let text_content: Option<String> = row.try_get("", "text_content").ok();
|
||||
let total_size: Option<i64> = row.try_get("", "total_size").ok();
|
||||
let entry_count: Option<i32> = row.try_get("", "entry_count").ok();
|
||||
let first_seen_at: Option<chrono::DateTime<chrono::Utc>> =
|
||||
row.try_get("", "first_seen_at").ok();
|
||||
let last_verified_at: Option<chrono::DateTime<chrono::Utc>> =
|
||||
row.try_get("", "last_verified_at").ok();
|
||||
|
||||
// Content kind data
|
||||
let content_kind_id: Option<i32> = row.try_get("", "content_kind_id").ok();
|
||||
let content_kind_name: Option<String> = row.try_get("", "content_kind_name").ok();
|
||||
|
||||
// Use entry ID as UUID if uuid is None
|
||||
@@ -291,37 +298,31 @@ impl LibraryQuery for DirectoryListingQuery {
|
||||
};
|
||||
|
||||
// Create content identity if available
|
||||
let content_identity = if let (Some(ci_uuid), Some(ci_hash), Some(ci_first_seen)) = (
|
||||
content_identity_uuid,
|
||||
content_identity_hash,
|
||||
content_identity_first_seen_at,
|
||||
) {
|
||||
// Convert content_kind name to ContentKind enum
|
||||
let kind = content_kind_name
|
||||
.as_ref()
|
||||
.map(|name| crate::domain::ContentKind::from(name.as_str()))
|
||||
.unwrap_or(crate::domain::ContentKind::Unknown);
|
||||
let content_identity =
|
||||
if let (Some(ci_uuid), Some(ci_hash), Some(ci_first_seen), Some(ci_last_verified)) =
|
||||
(content_identity_uuid, content_hash, first_seen_at, last_verified_at)
|
||||
{
|
||||
// Convert content_kind name to ContentKind enum
|
||||
let kind = content_kind_name
|
||||
.as_ref()
|
||||
.map(|name| crate::domain::ContentKind::from(name.as_str()))
|
||||
.unwrap_or(crate::domain::ContentKind::Unknown);
|
||||
|
||||
Some(crate::domain::ContentIdentity {
|
||||
uuid: ci_uuid,
|
||||
kind,
|
||||
hash: ci_hash,
|
||||
media_data: content_identity_media_data.map(|json| {
|
||||
serde_json::from_value(json).unwrap_or_else(|_| crate::domain::MediaData {
|
||||
width: None,
|
||||
height: None,
|
||||
duration: None,
|
||||
bitrate: None,
|
||||
fps: None,
|
||||
exif: None,
|
||||
extra: serde_json::Value::Null,
|
||||
})
|
||||
}),
|
||||
created_at: ci_first_seen,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(crate::domain::ContentIdentity {
|
||||
uuid: ci_uuid,
|
||||
kind,
|
||||
content_hash: ci_hash,
|
||||
integrity_hash,
|
||||
mime_type_id,
|
||||
text_content,
|
||||
total_size: total_size.unwrap_or(0),
|
||||
entry_count: entry_count.unwrap_or(0),
|
||||
first_seen_at: ci_first_seen,
|
||||
last_verified_at: ci_last_verified,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Create file construction data
|
||||
let file_data = FileConstructionData {
|
||||
@@ -400,11 +401,42 @@ impl DirectoryListingQuery {
|
||||
}
|
||||
}
|
||||
}
|
||||
SdPath::Cloud { .. } => {
|
||||
// Cloud storage directory browsing is not yet implemented
|
||||
Err(QueryError::Internal(
|
||||
"Cloud storage directory browsing is not yet implemented".to_string(),
|
||||
))
|
||||
SdPath::Cloud { volume_id, path } => {
|
||||
// Cloud storage directory browsing
|
||||
tracing::debug!(" Looking for cloud directory: volume={}, path='{}'", volume_id, path);
|
||||
|
||||
// Find directory entry by path in directory_paths table
|
||||
// Cloud paths are stored the same way as physical paths
|
||||
tracing::debug!(" Querying directory_paths table...");
|
||||
let directory_path = directory_paths::Entity::find()
|
||||
.filter(directory_paths::Column::Path.eq(path))
|
||||
.one(db)
|
||||
.await?;
|
||||
tracing::debug!(" Directory path query result: {:?}", directory_path);
|
||||
|
||||
match directory_path {
|
||||
Some(dp) => {
|
||||
tracing::debug!(" Found directory path entry: {:?}", dp);
|
||||
tracing::debug!(" Looking for entry with ID: {}", dp.entry_id);
|
||||
|
||||
// Get the entry for this directory
|
||||
let entry_result = entry::Entity::find_by_id(dp.entry_id).one(db).await?;
|
||||
tracing::debug!(" Entry query result: {:?}", entry_result);
|
||||
|
||||
entry_result.ok_or_else(|| {
|
||||
QueryError::Internal(format!(
|
||||
"Entry not found for cloud directory: {}",
|
||||
dp.entry_id
|
||||
))
|
||||
})
|
||||
}
|
||||
None => {
|
||||
tracing::debug!(" Cloud directory not found in directory_paths table");
|
||||
Err(QueryError::Internal(
|
||||
format!("Cloud directory '{}' has not been indexed yet. Please ensure the cloud volume is connected and indexing is complete.", path)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
SdPath::Content { .. } => {
|
||||
// Content-addressed paths are not supported for directory browsing
|
||||
|
||||
@@ -132,21 +132,20 @@ impl FileByPathQuery {
|
||||
db: &DatabaseConnection,
|
||||
) -> QueryResult<entry::Model> {
|
||||
match sd_path {
|
||||
SdPath::Physical { device_id, path } => {
|
||||
// More efficient approach: find the file by its filename and parent directory
|
||||
let file_name = path
|
||||
SdPath::Physical { .. } | SdPath::Cloud { .. } => {
|
||||
// Use SdPath API for consistent path handling
|
||||
let file_name = sd_path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.ok_or_else(|| QueryError::Internal("Invalid file name in path".to_string()))?;
|
||||
|
||||
let parent_path = path
|
||||
let parent_sd_path = sd_path
|
||||
.parent()
|
||||
.ok_or_else(|| QueryError::Internal("No parent directory".to_string()))?;
|
||||
|
||||
// Extract filename without extension for the database query
|
||||
let (name, extension) = if let Some(ext) = path.extension().and_then(|e| e.to_str())
|
||||
{
|
||||
let name_without_ext = file_name.trim_end_matches(&format!(".{}", ext));
|
||||
// Parse extension from filename
|
||||
let (name, extension) = if let Some(dot_idx) = file_name.rfind('.') {
|
||||
let name_without_ext = &file_name[..dot_idx];
|
||||
let ext = &file_name[dot_idx + 1..];
|
||||
(name_without_ext.to_string(), Some(ext.to_string()))
|
||||
} else {
|
||||
(file_name.to_string(), None)
|
||||
@@ -163,6 +162,13 @@ impl FileByPathQuery {
|
||||
|
||||
let entries = query.all(db).await?;
|
||||
|
||||
// Get parent path string for comparison
|
||||
let parent_path_str = match &parent_sd_path {
|
||||
SdPath::Physical { path, .. } => path.to_string_lossy().to_string(),
|
||||
SdPath::Cloud { path, .. } => path.clone(),
|
||||
_ => return Err(QueryError::Internal("Invalid parent path".to_string())),
|
||||
};
|
||||
|
||||
// For each matching entry, check if its parent directory path matches
|
||||
for entry_model in entries {
|
||||
if let Some(parent_id) = entry_model.parent_id {
|
||||
@@ -176,7 +182,7 @@ impl FileByPathQuery {
|
||||
{
|
||||
if let Some(parent_path_model) = parent_path_model {
|
||||
// Check if the parent directory path matches
|
||||
if PathBuf::from(&parent_path_model.path) == parent_path {
|
||||
if parent_path_model.path == parent_path_str {
|
||||
return Ok(entry_model);
|
||||
}
|
||||
}
|
||||
@@ -186,15 +192,9 @@ impl FileByPathQuery {
|
||||
|
||||
Err(QueryError::Internal(format!(
|
||||
"File not found at path: {}",
|
||||
path.display()
|
||||
sd_path.display()
|
||||
)))
|
||||
}
|
||||
SdPath::Cloud { .. } => {
|
||||
// Cloud storage file queries are not yet implemented
|
||||
Err(QueryError::Internal(
|
||||
"Cloud storage file queries are not yet implemented".to_string(),
|
||||
))
|
||||
}
|
||||
SdPath::Content { content_id } => {
|
||||
// For content-addressed paths, find any entry with this content_id
|
||||
// First we need to find the content_identity with this UUID
|
||||
|
||||
@@ -709,7 +709,6 @@ impl EntryProcessor {
|
||||
content_hash: Set(content_hash.clone()),
|
||||
mime_type_id: Set(mime_type_id),
|
||||
kind_id: Set(kind_id),
|
||||
media_data: Set(None), // Set during media analysis
|
||||
text_content: Set(None), // TODO: Extract text content for indexing
|
||||
total_size: Set(file_size),
|
||||
entry_count: Set(1),
|
||||
|
||||
@@ -15,7 +15,6 @@ use crate::{
|
||||
};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, TransactionTrait};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -26,7 +25,6 @@ pub async fn run_processing_phase(
|
||||
ctx: &JobContext<'_>,
|
||||
mode: IndexMode,
|
||||
location_root_path: &Path,
|
||||
volume_backend: Option<&Arc<dyn crate::volume::VolumeBackend>>,
|
||||
) -> Result<(), JobError> {
|
||||
let total_batches = state.entry_batches.len();
|
||||
ctx.log(format!(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Live Photo detection and handling
|
||||
//!
|
||||
//! NOTE: This should be moved to the Photos extension
|
||||
//!
|
||||
//! When enabled, Live Photos are handled as follows:
|
||||
//! 1. During indexing, when we encounter an image file (HEIC/JPEG), we check for a matching video (MOV/MP4)
|
||||
//! 2. If found, the video becomes a virtual sidecar of the image
|
||||
|
||||
158
core/src/ops/volumes/add_cloud/action.rs
Normal file
158
core/src/ops/volumes/add_cloud/action.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
//! Add cloud volume action
|
||||
//!
|
||||
//! This action adds a cloud storage volume (S3, Google Drive, etc.) to a library,
|
||||
//! storing encrypted credentials and creating a virtual volume for indexing.
|
||||
|
||||
use super::output::VolumeAddCloudOutput;
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
crypto::cloud_credentials::{CloudCredential, CloudCredentialManager},
|
||||
infra::action::{error::ActionError, LibraryAction},
|
||||
volume::{backend::CloudServiceType, CloudBackend, Volume, VolumeFingerprint},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct VolumeAddCloudInput {
|
||||
pub service: CloudServiceType,
|
||||
pub display_name: String,
|
||||
pub config: CloudStorageConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum CloudStorageConfig {
|
||||
S3 {
|
||||
bucket: String,
|
||||
region: String,
|
||||
access_key_id: String,
|
||||
secret_access_key: String,
|
||||
endpoint: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VolumeAddCloudAction {
|
||||
input: VolumeAddCloudInput,
|
||||
}
|
||||
|
||||
impl VolumeAddCloudAction {
|
||||
pub fn new(input: VolumeAddCloudInput) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
|
||||
impl LibraryAction for VolumeAddCloudAction {
|
||||
type Input = VolumeAddCloudInput;
|
||||
type Output = VolumeAddCloudOutput;
|
||||
|
||||
fn from_input(input: VolumeAddCloudInput) -> Result<Self, String> {
|
||||
Ok(VolumeAddCloudAction::new(input))
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: Arc<crate::library::Library>,
|
||||
context: Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
let device_id = context
|
||||
.device_manager
|
||||
.device_id()
|
||||
.map_err(|e| ActionError::InvalidInput(format!("Failed to get device ID: {}", e)))?;
|
||||
let library_id = library.id();
|
||||
|
||||
let (backend, credential, mount_point) = match &self.input.config {
|
||||
CloudStorageConfig::S3 {
|
||||
bucket,
|
||||
region,
|
||||
access_key_id,
|
||||
secret_access_key,
|
||||
endpoint,
|
||||
} => {
|
||||
let backend = CloudBackend::new_s3(
|
||||
bucket,
|
||||
region,
|
||||
access_key_id,
|
||||
secret_access_key,
|
||||
endpoint.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ActionError::InvalidInput(format!("Failed to create S3 backend: {}", e)))?;
|
||||
|
||||
let credential = CloudCredential::new_access_key(
|
||||
CloudServiceType::S3,
|
||||
access_key_id.clone(),
|
||||
secret_access_key.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
let mount_point = PathBuf::from(format!("cloud://s3/{}", bucket));
|
||||
|
||||
(backend, credential, mount_point)
|
||||
}
|
||||
};
|
||||
|
||||
let fingerprint = VolumeFingerprint::new(
|
||||
&self.input.display_name,
|
||||
0, // Cloud volumes don't have a fixed size
|
||||
&format!("{:?}", self.input.service),
|
||||
);
|
||||
|
||||
let backend_arc: Arc<dyn crate::volume::VolumeBackend> = Arc::new(backend);
|
||||
|
||||
let volume = Volume {
|
||||
fingerprint: fingerprint.clone(),
|
||||
device_id,
|
||||
name: self.input.display_name.clone(),
|
||||
mount_type: crate::volume::types::MountType::Network,
|
||||
volume_type: crate::volume::types::VolumeType::Network,
|
||||
mount_point: mount_point.clone(),
|
||||
mount_points: vec![mount_point],
|
||||
is_mounted: true,
|
||||
disk_type: crate::volume::types::DiskType::Unknown,
|
||||
file_system: crate::volume::types::FileSystem::Other(format!("{:?}", self.input.service)),
|
||||
total_bytes_capacity: 0,
|
||||
total_bytes_available: 0,
|
||||
read_only: false,
|
||||
hardware_id: None,
|
||||
error_status: None,
|
||||
apfs_container: None,
|
||||
container_volume_id: None,
|
||||
path_mappings: Vec::new(),
|
||||
backend: Some(backend_arc),
|
||||
read_speed_mbps: None,
|
||||
write_speed_mbps: None,
|
||||
auto_track_eligible: false,
|
||||
is_user_visible: true,
|
||||
last_updated: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let credential_manager = CloudCredentialManager::new(context.library_key_manager.clone());
|
||||
credential_manager
|
||||
.store_credential(library_id, &fingerprint.0, &credential)
|
||||
.map_err(|e| {
|
||||
ActionError::InvalidInput(format!("Failed to store credentials: {}", e))
|
||||
})?;
|
||||
|
||||
let tracked = context
|
||||
.volume_manager
|
||||
.track_volume(&library, &fingerprint, Some(self.input.display_name.clone()))
|
||||
.await
|
||||
.map_err(|e| ActionError::InvalidInput(format!("Volume tracking failed: {}", e)))?;
|
||||
|
||||
Ok(VolumeAddCloudOutput::new(
|
||||
fingerprint,
|
||||
self.input.display_name,
|
||||
self.input.service,
|
||||
))
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"volumes.add_cloud"
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(VolumeAddCloudAction, "volumes.add_cloud");
|
||||
7
core/src/ops/volumes/add_cloud/mod.rs
Normal file
7
core/src/ops/volumes/add_cloud/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! Add cloud volume operation
|
||||
|
||||
pub mod action;
|
||||
pub mod output;
|
||||
|
||||
pub use action::{CloudStorageConfig, VolumeAddCloudAction, VolumeAddCloudInput};
|
||||
pub use output::VolumeAddCloudOutput;
|
||||
22
core/src/ops/volumes/add_cloud/output.rs
Normal file
22
core/src/ops/volumes/add_cloud/output.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
//! Volume add cloud operation output types
|
||||
|
||||
use crate::volume::{backend::CloudServiceType, VolumeFingerprint};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct VolumeAddCloudOutput {
|
||||
pub fingerprint: VolumeFingerprint,
|
||||
pub volume_name: String,
|
||||
pub service: CloudServiceType,
|
||||
}
|
||||
|
||||
impl VolumeAddCloudOutput {
|
||||
pub fn new(fingerprint: VolumeFingerprint, volume_name: String, service: CloudServiceType) -> Self {
|
||||
Self {
|
||||
fingerprint,
|
||||
volume_name,
|
||||
service,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,16 @@
|
||||
//! This module provides operations for managing volumes in Spacedrive:
|
||||
//! - Tracking/untracking volumes in libraries
|
||||
//! - Speed testing volume performance
|
||||
//! - Adding/removing cloud volumes
|
||||
|
||||
pub mod add_cloud;
|
||||
pub mod remove_cloud;
|
||||
pub mod speed_test;
|
||||
pub mod track;
|
||||
pub mod untrack;
|
||||
|
||||
pub use add_cloud::{action::VolumeAddCloudAction, VolumeAddCloudOutput};
|
||||
pub use remove_cloud::{action::VolumeRemoveCloudAction, VolumeRemoveCloudOutput};
|
||||
pub use speed_test::{action::VolumeSpeedTestAction, VolumeSpeedTestOutput};
|
||||
pub use track::{action::VolumeTrackAction, VolumeTrackOutput};
|
||||
pub use untrack::{action::VolumeUntrackAction, VolumeUntrackOutput};
|
||||
|
||||
70
core/src/ops/volumes/remove_cloud/action.rs
Normal file
70
core/src/ops/volumes/remove_cloud/action.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
//! Remove cloud volume action
|
||||
//!
|
||||
//! This action removes a cloud storage volume from a library, deleting encrypted
|
||||
//! credentials and untracking the volume.
|
||||
|
||||
use super::output::VolumeRemoveCloudOutput;
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
crypto::cloud_credentials::CloudCredentialManager,
|
||||
infra::action::{error::ActionError, LibraryAction},
|
||||
volume::VolumeFingerprint,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct VolumeRemoveCloudInput {
|
||||
pub fingerprint: VolumeFingerprint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct VolumeRemoveCloudAction {
|
||||
input: VolumeRemoveCloudInput,
|
||||
}
|
||||
|
||||
impl VolumeRemoveCloudAction {
|
||||
pub fn new(input: VolumeRemoveCloudInput) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
|
||||
impl LibraryAction for VolumeRemoveCloudAction {
|
||||
type Input = VolumeRemoveCloudInput;
|
||||
type Output = VolumeRemoveCloudOutput;
|
||||
|
||||
fn from_input(input: VolumeRemoveCloudInput) -> Result<Self, String> {
|
||||
Ok(VolumeRemoveCloudAction::new(input))
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: std::sync::Arc<crate::library::Library>,
|
||||
context: std::sync::Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
let library_id = library.id();
|
||||
|
||||
context
|
||||
.volume_manager
|
||||
.untrack_volume(&library, &self.input.fingerprint)
|
||||
.await
|
||||
.map_err(|e| ActionError::InvalidInput(format!("Volume untracking failed: {}", e)))?;
|
||||
|
||||
let credential_manager = CloudCredentialManager::new(context.library_key_manager.clone());
|
||||
if let Err(e) = credential_manager.delete_credential(library_id, &self.input.fingerprint.0) {
|
||||
tracing::warn!(
|
||||
"Failed to delete credentials for volume {}: {}",
|
||||
self.input.fingerprint.0,
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
Ok(VolumeRemoveCloudOutput::new(self.input.fingerprint))
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"volumes.remove_cloud"
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(VolumeRemoveCloudAction, "volumes.remove_cloud");
|
||||
7
core/src/ops/volumes/remove_cloud/mod.rs
Normal file
7
core/src/ops/volumes/remove_cloud/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! Remove cloud volume operation
|
||||
|
||||
pub mod action;
|
||||
pub mod output;
|
||||
|
||||
pub use action::{VolumeRemoveCloudAction, VolumeRemoveCloudInput};
|
||||
pub use output::VolumeRemoveCloudOutput;
|
||||
16
core/src/ops/volumes/remove_cloud/output.rs
Normal file
16
core/src/ops/volumes/remove_cloud/output.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
//! Volume remove cloud operation output types
|
||||
|
||||
use crate::volume::VolumeFingerprint;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct VolumeRemoveCloudOutput {
|
||||
pub fingerprint: VolumeFingerprint,
|
||||
}
|
||||
|
||||
impl VolumeRemoveCloudOutput {
|
||||
pub fn new(fingerprint: VolumeFingerprint) -> Self {
|
||||
Self { fingerprint }
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ pub enum BackendType {
|
||||
}
|
||||
|
||||
/// Cloud service type identifier
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum CloudServiceType {
|
||||
S3,
|
||||
GoogleDrive,
|
||||
|
||||
Reference in New Issue
Block a user