mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-14 04:14:04 +00:00
Checking if new location is nested or already exists (#716)
* Checking if new location is nested or already exists * Also checking for children locations on check nested * Rust fmt
This commit is contained in:
parent
288f4947e1
commit
f684660a90
|
@ -13,7 +13,7 @@ use super::{
|
|||
#[derive(Error, Debug)]
|
||||
pub enum LocationError {
|
||||
// Not Found errors
|
||||
#[error("Location not found (path: {0:?})")]
|
||||
#[error("Location not found (path: {})", .0.display())]
|
||||
PathNotFound(PathBuf),
|
||||
#[error("Location not found (uuid: {0})")]
|
||||
UuidNotFound(Uuid),
|
||||
|
@ -21,30 +21,37 @@ pub enum LocationError {
|
|||
IdNotFound(i32),
|
||||
|
||||
// User errors
|
||||
#[error("Location not a directory (path: {0:?})")]
|
||||
#[error("Location not a directory (path: {})", .0.display())]
|
||||
NotDirectory(PathBuf),
|
||||
#[error("Could not find directory in Location (path: {0:?})")]
|
||||
DirectoryNotFound(String),
|
||||
#[error("Library exists in the location metadata file, must relink: (old_path: {old_path:?}, new_path: {new_path:?})")]
|
||||
#[error("Could not find directory in Location (path: {})", .0.display())]
|
||||
DirectoryNotFound(PathBuf),
|
||||
#[error(
|
||||
"Library exists in the location metadata file, must relink: (old_path: {}, new_path: {})",
|
||||
.old_path.display(),
|
||||
.new_path.display(),
|
||||
)]
|
||||
NeedRelink {
|
||||
old_path: PathBuf,
|
||||
new_path: PathBuf,
|
||||
},
|
||||
#[error(
|
||||
"This location belongs to another library, must update .spacedrive file: (path: {0:?})"
|
||||
"This location belongs to another library, must update .spacedrive file: (path: {})",
|
||||
.0.display()
|
||||
)]
|
||||
AddLibraryToMetadata(PathBuf),
|
||||
#[error("Location metadata file not found: (path: {0:?})")]
|
||||
#[error("Location metadata file not found: (path: {})", .0.display())]
|
||||
MetadataNotFound(PathBuf),
|
||||
#[error("Location already exists (path: {0:?})")]
|
||||
#[error("Location already exists in database (path: {})", .0.display())]
|
||||
LocationAlreadyExists(PathBuf),
|
||||
#[error("Nested location currently not supported (path: {})", .0.display())]
|
||||
NestedLocation(PathBuf),
|
||||
|
||||
// Internal Errors
|
||||
#[error("Location metadata error (error: {0:?})")]
|
||||
LocationMetadataError(#[from] LocationMetadataError),
|
||||
#[error("Failed to read location path metadata info (path: {1:?}); (error: {0:?})")]
|
||||
#[error("Failed to read location path metadata info (path: {}); (error: {0:?})", .1.display())]
|
||||
LocationPathFilesystemMetadataAccess(io::Error, PathBuf),
|
||||
#[error("Missing metadata file for location (path: {0:?})")]
|
||||
#[error("Missing metadata file for location (path: {})", .0.display())]
|
||||
MissingMetadataFile(PathBuf),
|
||||
#[error("Failed to open file from local os (error: {0:?})")]
|
||||
FileReadError(io::Error),
|
||||
|
@ -71,8 +78,9 @@ impl From<LocationError> for rspc::Error {
|
|||
}
|
||||
|
||||
// User's fault errors
|
||||
// | LocationError::MissingLocalPath(_)
|
||||
LocationError::NotDirectory(_) => {
|
||||
LocationError::NotDirectory(_)
|
||||
| LocationError::NestedLocation(_)
|
||||
| LocationError::LocationAlreadyExists(_) => {
|
||||
rspc::Error::with_cause(ErrorCode::BadRequest, err.to_string(), err)
|
||||
}
|
||||
|
||||
|
|
|
@ -182,15 +182,11 @@ impl SpacedriveLocationMetadataFile {
|
|||
self.metadata.libraries.contains_key(&library_id)
|
||||
}
|
||||
|
||||
pub(super) fn location_path(
|
||||
&self,
|
||||
library_id: LibraryId,
|
||||
) -> Result<&Path, LocationMetadataError> {
|
||||
pub(super) fn location_path(&self, library_id: LibraryId) -> Option<&Path> {
|
||||
self.metadata
|
||||
.libraries
|
||||
.get(&library_id)
|
||||
.map(|l| l.path.as_path())
|
||||
.ok_or(LocationMetadataError::LibraryNotFound(library_id))
|
||||
}
|
||||
|
||||
pub(super) async fn remove_library(
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
|||
shallow_thumbnailer_job::ShallowThumbnailerJobInit, thumbnailer_job::ThumbnailerJobInit,
|
||||
},
|
||||
},
|
||||
prisma::{file_path, indexer_rules_in_location, location, node, object},
|
||||
prisma::{file_path, indexer_rules_in_location, location, node, object, PrismaClient},
|
||||
sync,
|
||||
};
|
||||
|
||||
|
@ -80,12 +80,15 @@ impl LocationCreateArgs {
|
|||
}
|
||||
|
||||
if let Some(metadata) = SpacedriveLocationMetadataFile::try_load(&self.path).await? {
|
||||
return if metadata.has_library(library.id) {
|
||||
Err(LocationError::NeedRelink {
|
||||
// SAFETY: This unwrap is ok as we checked that we have this library_id
|
||||
old_path: metadata.location_path(library.id).unwrap().to_path_buf(),
|
||||
new_path: self.path,
|
||||
})
|
||||
return if let Some(old_path) = metadata.location_path(library.id) {
|
||||
if old_path == self.path {
|
||||
Err(LocationError::LocationAlreadyExists(self.path))
|
||||
} else {
|
||||
Err(LocationError::NeedRelink {
|
||||
old_path: old_path.to_path_buf(),
|
||||
new_path: self.path,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Err(LocationError::AddLibraryToMetadata(self.path))
|
||||
};
|
||||
|
@ -452,53 +455,59 @@ async fn create_location(
|
|||
) -> Result<location_with_indexer_rules::Data, LocationError> {
|
||||
let Library { db, sync, .. } = &library;
|
||||
|
||||
let mut location_path = location_path.as_ref().to_path_buf();
|
||||
let mut path = location_path.as_ref().to_path_buf();
|
||||
|
||||
let (path, normalized_path) = location_path
|
||||
let (location_path, normalized_path) = path
|
||||
// Normalize path and also check if it exists
|
||||
.normalize()
|
||||
.and_then(|normalized_path| {
|
||||
if cfg!(windows) {
|
||||
// Use normalized path as location path on Windows
|
||||
// Use normalized path as main path on Windows
|
||||
// This ensures we always receive a valid windows formated path
|
||||
// ex: /Users/JohnDoe/Downloads will become C:\Users\JohnDoe\Downloads
|
||||
// Internally `normalize` calls `GetFullPathNameW` on Windows
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfullpathnamew
|
||||
location_path = normalized_path.as_path().to_path_buf();
|
||||
path = normalized_path.as_path().to_path_buf();
|
||||
}
|
||||
|
||||
Ok((
|
||||
// TODO: Maybe save the path bytes instead of the string representation to avoid depending on UTF-8
|
||||
location_path
|
||||
.to_str()
|
||||
.map(str::to_string)
|
||||
.ok_or(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Found non-UTF-8 path",
|
||||
))?,
|
||||
path.to_str().map(str::to_string).ok_or(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Found non-UTF-8 path",
|
||||
))?,
|
||||
normalized_path,
|
||||
))
|
||||
})
|
||||
.map_err(|_| {
|
||||
LocationError::DirectoryNotFound(location_path.to_string_lossy().to_string())
|
||||
})?;
|
||||
.map_err(|_| LocationError::DirectoryNotFound(path.clone()))?;
|
||||
|
||||
// Not needed on Windows because the normalization already handles it
|
||||
if cfg!(not(windows)) {
|
||||
// Replace location_path with normalize_path, when the first one ends in `.` or `..`
|
||||
// This is required so localize_name doesn't panic
|
||||
if let Some(component) = location_path.components().next_back() {
|
||||
match component {
|
||||
Component::CurDir | Component::ParentDir => {
|
||||
location_path = normalized_path.as_path().to_path_buf();
|
||||
}
|
||||
_ => {}
|
||||
if let Some(component) = path.components().next_back() {
|
||||
if matches!(component, Component::CurDir | Component::ParentDir) {
|
||||
path = normalized_path.as_path().to_path_buf();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if library
|
||||
.db
|
||||
.location()
|
||||
.count(vec![location::path::equals(location_path.clone())])
|
||||
.exec()
|
||||
.await? > 0
|
||||
{
|
||||
return Err(LocationError::LocationAlreadyExists(path));
|
||||
}
|
||||
|
||||
if check_nested_location(&location_path, &library.db).await? {
|
||||
return Err(LocationError::NestedLocation(path));
|
||||
}
|
||||
|
||||
// Use `to_string_lossy` because a partially corrupted but identifiable name is better than nothing
|
||||
let mut name = location_path.localize_name().to_string_lossy().to_string();
|
||||
let mut name = path.localize_name().to_string_lossy().to_string();
|
||||
|
||||
// Windows doesn't have a root directory
|
||||
if cfg!(not(windows)) && name == "/" {
|
||||
|
@ -519,14 +528,14 @@ async fn create_location(
|
|||
[
|
||||
("node", json!({ "pub_id": library.id.as_bytes() })),
|
||||
("name", json!(&name)),
|
||||
("path", json!(&path)),
|
||||
("path", json!(&location_path)),
|
||||
],
|
||||
),
|
||||
db.location()
|
||||
.create(
|
||||
location_pub_id.as_bytes().to_vec(),
|
||||
name,
|
||||
path,
|
||||
location_path,
|
||||
node::id::equals(library.node_local_id),
|
||||
vec![],
|
||||
)
|
||||
|
@ -687,6 +696,37 @@ impl From<&location_with_indexer_rules::Data> for location::Data {
|
|||
}
|
||||
}
|
||||
|
||||
async fn check_nested_location(
|
||||
location_path: impl AsRef<Path>,
|
||||
db: &PrismaClient,
|
||||
) -> Result<bool, QueryError> {
|
||||
let location_path = location_path.as_ref();
|
||||
|
||||
let (parents_count, children_count) = db
|
||||
._batch((
|
||||
db.location().count(vec![location::path::in_vec(
|
||||
location_path
|
||||
.ancestors()
|
||||
.skip(1) // skip the actual location_path, we only want the parents
|
||||
.map(|p| {
|
||||
p.to_str()
|
||||
.map(str::to_string)
|
||||
.expect("Found non-UTF-8 path")
|
||||
})
|
||||
.collect(),
|
||||
)]),
|
||||
db.location().count(vec![location::path::starts_with(
|
||||
location_path
|
||||
.to_str()
|
||||
.map(str::to_string)
|
||||
.expect("Found non-UTF-8 path"),
|
||||
)]),
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(parents_count > 0 || children_count > 0)
|
||||
}
|
||||
|
||||
// check if a path exists in our database at that location
|
||||
// pub async fn check_virtual_path_exists(
|
||||
// library: &Library,
|
||||
|
|
Loading…
Reference in a new issue