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:
Ericson "Fogo" Soares 2023-04-19 02:03:28 -03:00 committed by GitHub
parent 288f4947e1
commit f684660a90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 91 additions and 47 deletions

View file

@ -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)
}

View file

@ -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(

View file

@ -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,