[ENG 709] Make all location fields optional (#930)

* in progress

* make all location fields optional

* generate migration

* fix formatting

* formatting
This commit is contained in:
Brendan Allan 2023-06-12 19:52:51 +02:00 committed by GitHub
parent 553f50e6fe
commit 7148209343
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 340 additions and 201 deletions

View file

@ -51,7 +51,7 @@ const DrawerLocations = ({ stackName }: DrawerLocationsProp) => {
{locations?.map((location) => (
<DrawerLocationItem
key={location.id}
folderName={location.name}
folderName={location.name ?? ''}
onPress={() =>
navigation.navigate(stackName, {
screen: 'Location',

View file

@ -20,12 +20,12 @@ import { tw, twStyle } from '~/lib/tailwind';
import { SettingsStackScreenProps } from '~/navigation/SettingsNavigator';
const schema = z.object({
displayName: z.string(),
localPath: z.string(),
displayName: z.string().nullable(),
localPath: z.string().nullable(),
indexer_rules_ids: z.array(z.string()),
generatePreviewMedia: z.boolean(),
syncPreviewMedia: z.boolean(),
hidden: z.boolean()
generatePreviewMedia: z.boolean().nullable(),
syncPreviewMedia: z.boolean().nullable(),
hidden: z.boolean().nullable()
});
const EditLocationSettingsScreen = ({
@ -115,7 +115,7 @@ const EditLocationSettingsScreen = ({
name="displayName"
control={form.control}
render={({ field: { onBlur, onChange, value } }) => (
<Input onBlur={onBlur} onChangeText={onChange} value={value} />
<Input onBlur={onBlur} onChangeText={onChange} value={value ?? undefined} />
)}
/>
<SettingsInputInfo>
@ -128,7 +128,7 @@ const EditLocationSettingsScreen = ({
name="localPath"
control={form.control}
render={({ field: { onBlur, onChange, value } }) => (
<Input onBlur={onBlur} onChangeText={onChange} value={value} />
<Input onBlur={onBlur} onChangeText={onChange} value={value ?? undefined} />
)}
/>
<SettingsInputInfo>
@ -146,7 +146,7 @@ const EditLocationSettingsScreen = ({
name="generatePreviewMedia"
control={form.control}
render={({ field: { onChange, value } }) => (
<Switch value={value} onValueChange={onChange} />
<Switch value={value ?? undefined} onValueChange={onChange} />
)}
/>
}
@ -158,7 +158,7 @@ const EditLocationSettingsScreen = ({
name="syncPreviewMedia"
control={form.control}
render={({ field: { onChange, value } }) => (
<Switch value={value} onValueChange={onChange} />
<Switch value={value ?? undefined} onValueChange={onChange} />
)}
/>
}
@ -170,7 +170,7 @@ const EditLocationSettingsScreen = ({
name="hidden"
control={form.control}
render={({ field: { onChange, value } }) => (
<Switch value={value} onValueChange={onChange} />
<Switch value={value ?? undefined} onValueChange={onChange} />
)}
/>
}

View file

@ -19,7 +19,7 @@ import { tw, twStyle } from '~/lib/tailwind';
import { SettingsStackScreenProps } from '~/navigation/SettingsNavigator';
type LocationItemProps = {
location: Location & { node: Node };
location: Location & { node: Node | null };
index: number;
navigation: SettingsStackScreenProps<'LocationSettings'>['navigation'];
};
@ -107,11 +107,13 @@ function LocationItem({ location, index, navigation }: LocationItemProps) {
<Text numberOfLines={1} style={tw`text-sm font-semibold text-ink`}>
{location.name}
</Text>
<View style={tw`mt-0.5 self-start rounded bg-app-highlight px-1 py-[1px]`}>
<Text numberOfLines={1} style={tw`text-xs font-semibold text-ink-dull`}>
{location.node.name}
</Text>
</View>
{location.node && (
<View style={tw`mt-0.5 self-start rounded bg-app-highlight px-1 py-[1px]`}>
<Text numberOfLines={1} style={tw`text-xs font-semibold text-ink-dull`}>
{location.node.name}
</Text>
</View>
)}
<Text
numberOfLines={1}
style={tw`mt-0.5 text-[10px] font-semibold text-ink-dull`}

View file

@ -0,0 +1,23 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_location" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pub_id" BLOB NOT NULL,
"node_id" INTEGER,
"name" TEXT,
"path" TEXT,
"total_capacity" INTEGER,
"available_capacity" INTEGER,
"is_archived" BOOLEAN,
"generate_preview_media" BOOLEAN,
"sync_preview_media" BOOLEAN,
"hidden" BOOLEAN,
"date_created" DATETIME,
CONSTRAINT "location_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "node" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_location" ("available_capacity", "date_created", "generate_preview_media", "hidden", "id", "is_archived", "name", "node_id", "path", "pub_id", "sync_preview_media", "total_capacity") SELECT "available_capacity", "date_created", "generate_preview_media", "hidden", "id", "is_archived", "name", "node_id", "path", "pub_id", "sync_preview_media", "total_capacity" FROM "location";
DROP TABLE "location";
ALTER TABLE "new_location" RENAME TO "location";
CREATE UNIQUE INDEX "location_pub_id_key" ON "location"("pub_id");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View file

@ -100,18 +100,18 @@ model Location {
id Int @id @default(autoincrement())
pub_id Bytes @unique
node_id Int
name String
path String
node_id Int?
name String?
path String?
total_capacity Int?
available_capacity Int?
is_archived Boolean @default(false)
generate_preview_media Boolean @default(true)
sync_preview_media Boolean @default(true)
hidden Boolean @default(false)
date_created DateTime @default(now())
is_archived Boolean?
generate_preview_media Boolean?
sync_preview_media Boolean?
hidden Boolean?
date_created DateTime?
node Node @relation(fields: [node_id], references: [id])
node Node? @relation(fields: [node_id], references: [id])
file_paths FilePath[]
indexer_rules IndexerRulesInLocation[]

View file

@ -197,7 +197,10 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
.await?
.ok_or(LocationError::IdNotFound(location_id))?;
let location_path = Path::new(&location.path);
let Some(location_path) = location.path.as_ref().map(Path::new) else {
Err(LocationError::MissingPath)?
};
fs::rename(
location_path.join(IsolatedFilePathData::from_relative_str(
location_id,

View file

@ -193,9 +193,12 @@ async fn handle_file(
.await?
.ok_or_else(|| HandleCustomUriError::NotFound("object"))?;
let Some(path) = &file_path.location.path else {
return Err(HandleCustomUriError::NoPath(file_path.location.id))
};
let lru_entry = (
Path::new(&file_path.location.path)
.join(IsolatedFilePathData::from((location_id, &file_path))),
Path::new(path).join(IsolatedFilePathData::from((location_id, &file_path))),
file_path.extension,
);
FILE_METADATA_CACHE.insert(lru_cache_key, lru_entry.clone());
@ -397,6 +400,8 @@ pub enum HandleCustomUriError {
RangeNotSatisfiable(&'static str),
#[error("HandleCustomUriError::NotFound - resource '{0}'")]
NotFound(&'static str),
#[error("no-path")]
NoPath(i32),
}
impl From<HandleCustomUriError> for Response<Vec<u8>> {
@ -439,6 +444,12 @@ impl From<HandleCustomUriError> for Response<Vec<u8>> {
.as_bytes()
.to_vec(),
),
HandleCustomUriError::NoPath(id) => {
error!("Location <id = {id}> has no path");
builder
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(b"Internal Server Error".to_vec())
}
})
// SAFETY: This unwrap is ok as we have an hardcoded the response builders.
.expect("internal error building hardcoded HTTP error response")

View file

@ -76,6 +76,8 @@ pub enum JobError {
MissingFromDb(&'static str, String),
#[error("the cas id is not set on the path data")]
MissingCasId,
#[error("missing-location-path")]
MissingPath,
// Not errors
#[error("step completed with errors: {0:?}")]

View file

@ -105,15 +105,22 @@ impl Library {
.db
.file_path()
.find_first(vec![
file_path::location::is(vec![location::node_id::equals(self.node_local_id)]),
file_path::location::is(vec![location::node_id::equals(Some(self.node_local_id))]),
file_path::id::equals(id),
])
.select(file_path_to_full_path::select())
.exec()
.await?
.map(|record| {
Path::new(&record.location.path)
.join(IsolatedFilePathData::from((record.location.id, &record)))
}))
record
.location
.path
.as_ref()
.map(|p| {
Path::new(p).join(IsolatedFilePathData::from((record.location.id, &record)))
})
.ok_or_else(|| LibraryManagerError::NoPath(record.location.id))
})
.transpose()?)
}
}

View file

@ -68,6 +68,8 @@ pub enum LibraryManagerError {
NonUtf8Path(#[from] NonUtf8PathError),
#[error("failed to watch locations: {0}")]
LocationWatcher(#[from] LocationManagerError),
#[error("no-path")]
NoPath(i32),
}
impl From<LibraryManagerError> for rspc::Error {
@ -389,7 +391,7 @@ impl LibraryManager {
for location in library
.db
.location()
.find_many(vec![location::node_id::equals(node_data.id)])
.find_many(vec![location::node_id::equals(Some(node_data.id))])
.exec()
.await?
{

View file

@ -66,6 +66,8 @@ pub enum LocationError {
FilePathError(#[from] FilePathError),
#[error(transparent)]
FileIO(#[from] FileIOError),
#[error("missing-path")]
MissingPath,
}
impl From<LocationError> for rspc::Error {

View file

@ -69,6 +69,7 @@ file_path::select!(file_path_to_handle_custom_uri {
name
extension
location: select {
id
path
}
});

View file

@ -62,7 +62,10 @@ impl StatefulJob for IndexerJob {
state: &mut JobState<Self>,
) -> Result<(), JobError> {
let location_id = state.init.location.id;
let location_path = Path::new(&state.init.location.path);
let location_path = state.init.location.path.as_ref();
let Some(location_path) = location_path.map(Path::new) else {
return Err(JobError::MissingPath)
};
let db = Arc::clone(&ctx.library.db);
@ -206,7 +209,11 @@ impl StatefulJob for IndexerJob {
}
IndexerJobStepInput::Walk(to_walk_entry) => {
let location_id = state.init.location.id;
let location_path = Path::new(&state.init.location.path);
let location_path = state.init.location.path.as_ref();
let Some(location_path) = location_path.map(Path::new) else {
return Err(JobError::MissingPath)
};
let db = Arc::clone(&ctx.library.db);
let scan_start = Instant::now();
@ -276,6 +283,11 @@ impl StatefulJob for IndexerJob {
}
async fn finalize(&mut self, ctx: WorkerContext, state: &mut JobState<Self>) -> JobResult {
finalize_indexer(&state.init.location.path, state, ctx)
let location_path = state.init.location.path.as_ref();
let Some(location_path) = location_path.map(Path::new) else {
return Err(JobError::MissingPath)
};
finalize_indexer(&location_path, state, ctx)
}
}

View file

@ -29,7 +29,9 @@ pub async fn shallow(
library: &Library,
) -> Result<(), JobError> {
let location_id = location.id;
let location_path = Path::new(&location.path);
let Some(location_path) = location.path.as_ref().map(PathBuf::from) else {
panic!();
};
let db = library.db.clone();
@ -41,16 +43,16 @@ pub async fn shallow(
.map_err(IndexerError::from)?;
let (add_root, to_walk_path) = if sub_path != Path::new("") {
let full_path = ensure_sub_path_is_in_location(location_path, &sub_path)
let full_path = ensure_sub_path_is_in_location(&location_path, &sub_path)
.await
.map_err(IndexerError::from)?;
ensure_sub_path_is_directory(location_path, &sub_path)
ensure_sub_path_is_directory(&location_path, &sub_path)
.await
.map_err(IndexerError::from)?;
(
!check_file_path_exists::<IndexerError>(
&IsolatedFilePathData::new(location_id, location_path, &full_path, true)
&IsolatedFilePathData::new(location_id, &location_path, &full_path, true)
.map_err(IndexerError::from)?,
&db,
)
@ -68,7 +70,7 @@ pub async fn shallow(
|_, _| {},
file_paths_db_fetcher_fn!(&db),
to_remove_db_fetcher_fn!(location_id, location_path, &db),
iso_file_path_factory(location_id, location_path),
iso_file_path_factory(location_id, &location_path),
add_root,
)
.await?

View file

@ -2,7 +2,7 @@ use crate::{library::Library, prisma::location};
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
path::{Path, PathBuf},
time::Duration,
};
@ -23,8 +23,13 @@ pub(super) async fn check_online(
) -> Result<bool, LocationManagerError> {
let pub_id = Uuid::from_slice(&location.pub_id)?;
if location.node_id == library.node_local_id {
match fs::metadata(&location.path).await {
let location_path = location.path.as_ref();
let Some(location_path) = location_path.map(Path::new) else {
return Err(LocationManagerError::MissingPath)
};
if location.node_id == Some(library.node_local_id) {
match fs::metadata(&location_path).await {
Ok(_) => {
library.location_manager().add_online(pub_id).await;
Ok(true)
@ -60,11 +65,14 @@ pub(super) fn watch_location(
locations_unwatched: &mut HashMap<LocationAndLibraryKey, LocationWatcher>,
) {
let location_id = location.id;
let location_path = location.path.as_ref();
let Some(location_path) = location_path.map(Path::new) else {
return
};
if let Some(mut watcher) = locations_unwatched.remove(&(location_id, library_id)) {
if watcher.check_path(&location.path) {
if watcher.check_path(location_path) {
watcher.watch();
} else {
watcher.update_data(location, true);
}
locations_watched.insert((location_id, library_id), watcher);
@ -78,11 +86,14 @@ pub(super) fn unwatch_location(
locations_unwatched: &mut HashMap<LocationAndLibraryKey, LocationWatcher>,
) {
let location_id = location.id;
let location_path = location.path.as_ref();
let Some(location_path) = location_path.map(Path::new) else {
return
};
if let Some(mut watcher) = locations_watched.remove(&(location_id, library_id)) {
if watcher.check_path(&location.path) {
if watcher.check_path(location_path) {
watcher.unwatch();
} else {
watcher.update_data(location, false)
}
locations_unwatched.insert((location_id, library_id), watcher);
@ -128,7 +139,7 @@ pub(super) async fn handle_remove_location_request(
) {
let key = (location_id, library.id);
if let Some(location) = get_location(location_id, &library).await {
if location.node_id == library.node_local_id {
if location.node_id == Some(library.node_local_id) {
unwatch_location(location, library.id, locations_watched, locations_unwatched);
locations_unwatched.remove(&key);
forced_unwatch.remove(&key);

View file

@ -102,6 +102,8 @@ pub enum LocationManagerError {
CorruptedLocationPubId(#[from] uuid::Error),
#[error("Job Manager error: (error: {0})")]
JobManager(#[from] JobManagerError),
#[error("missing-location-path")]
MissingPath,
#[error("invalid inode")]
InvalidInode,
@ -446,7 +448,7 @@ impl LocationManager {
// The time to check came for an already removed library, so we just ignore it
to_remove.remove(&key);
} else if let Some(location) = get_location(location_id, &library).await {
if location.node_id == library.node_local_id {
if location.node_id == Some(library.node_local_id) {
let is_online = match check_online(&location, &library).await {
Ok(is_online) => is_online,
Err(e) => {

View file

@ -61,7 +61,8 @@ trait EventHandler<'lib> {
#[derive(Debug)]
pub(super) struct LocationWatcher {
location: location::Data,
id: i32,
path: String,
watcher: RecommendedWatcher,
ignore_path_tx: mpsc::UnboundedSender<IgnorePath>,
handle: Option<JoinHandle<()>>,
@ -105,8 +106,13 @@ impl LocationWatcher {
stop_rx,
));
let Some(path) = location.path else {
return Err(LocationManagerError::MissingPath)
};
Ok(Self {
location,
id: location.id,
path,
watcher,
ignore_path_tx,
handle: Some(handle),
@ -212,12 +218,16 @@ impl LocationWatcher {
}
pub(super) fn check_path(&self, path: impl AsRef<Path>) -> bool {
(self.location.path.as_ref() as &Path) == path.as_ref()
Path::new(&self.path) == path.as_ref()
}
pub(super) fn watch(&mut self) {
let path = &self.location.path;
if let Err(e) = self.watcher.watch(path.as_ref(), RecursiveMode::Recursive) {
let path = &self.path;
if let Err(e) = self
.watcher
.watch(Path::new(path), RecursiveMode::Recursive)
{
error!("Unable to watch location: (path: {path}, error: {e:#?})");
} else {
debug!("Now watching location: (path: {path})");
@ -225,8 +235,8 @@ impl LocationWatcher {
}
pub(super) fn unwatch(&mut self) {
let path = &self.location.path;
if let Err(e) = self.watcher.unwatch(path.as_ref()) {
let path = &self.path;
if let Err(e) = self.watcher.unwatch(Path::new(path)) {
/**************************************** TODO: ****************************************
* According to an unit test, this error may occur when a subdirectory is removed *
* and we try to unwatch the parent directory then we have to check the implications *
@ -237,25 +247,6 @@ impl LocationWatcher {
debug!("Stop watching location: (path: {path})");
}
}
pub(super) fn update_data(&mut self, new_location: location::Data, to_watch: bool) {
assert_eq!(
self.location.id, new_location.id,
"Updated location data must have the same id"
);
let new_path = self.location.path != new_location.path;
if new_path {
self.unwatch();
}
self.location = new_location;
if new_path && to_watch {
self.watch();
}
}
}
impl Drop for LocationWatcher {
@ -264,7 +255,7 @@ impl Drop for LocationWatcher {
if stop_tx.send(()).is_err() {
error!(
"Failed to send stop signal to location watcher: <id='{}'>",
self.location.id
self.id
);
}

View file

@ -75,13 +75,17 @@ pub(super) async fn create_dir(
let path = path.as_ref();
let Some(location_path) = &location.path else {
return Err(LocationManagerError::MissingPath)
};
trace!(
"Location: <root_path ='{}'> creating directory: {}",
location.path,
location_path,
path.display()
);
let materialized_path = IsolatedFilePathData::new(location.id, &location.path, path, true)?;
let materialized_path = IsolatedFilePathData::new(location.id, &location_path, path, true)?;
let (inode, device) = {
#[cfg(target_family = "unix")]
@ -338,7 +342,9 @@ async fn inner_update_file(
.await?
.ok_or_else(|| LocationManagerError::MissingLocation(location_id))?;
let location_path = PathBuf::from(location.path);
let Some(location_path) = location.path.map(PathBuf::from) else {
return Err(LocationManagerError::MissingPath)
};
trace!(
"Location: <root_path ='{}'> updating file: {}",
@ -676,13 +682,17 @@ pub(super) async fn extract_inode_and_device_from_path(
.await?
.ok_or(LocationManagerError::MissingLocation(location_id))?;
let Some(location_path) = &location.path else {
return Err(LocationManagerError::MissingPath)
};
library
.db
.file_path()
.find_first(loose_find_existing_file_path_params(
&IsolatedFilePathData::new(location_id, &location.path, path, true)?,
&IsolatedFilePathData::new(location_id, location_path, path, true)?,
))
.select(file_path::select!( {inode device} ))
.select(file_path::select!({ inode device }))
.exec()
.await?
.map_or(
@ -715,6 +725,11 @@ pub(super) async fn extract_location_path(
.map_or(
Err(LocationManagerError::MissingLocation(location_id)),
// NOTE: The following usage of `PathBuf` doesn't incur a new allocation so it's fine
|location| Ok(PathBuf::from(location.path)),
|location| {
location
.path
.map(PathBuf::from)
.ok_or(LocationManagerError::MissingPath)
},
)
}

View file

@ -117,24 +117,24 @@ impl LocationCreateArgs {
library.id,
uuid,
&self.path,
location.name.clone(),
location.name,
)
.err_into::<LocationError>()
.and_then(|()| async move {
Ok(library
.location_manager()
.add(location.id, library.clone())
.add(location.data.id, library.clone())
.await?)
})
.await
{
delete_location(library, location.id).await?;
delete_location(library, location.data.id).await?;
Err(err)?;
}
info!("Created location: {location:?}");
info!("Created location: {:?}", &location.data);
Ok(Some(location))
Ok(Some(location.data))
} else {
Ok(None)
}
@ -183,20 +183,20 @@ impl LocationCreateArgs {
if let Some(location) = location {
metadata
.add_library(library.id, uuid, &self.path, location.name.clone())
.add_library(library.id, uuid, &self.path, location.name)
.await?;
library
.location_manager()
.add(location.id, library.clone())
.add(location.data.id, library.clone())
.await?;
info!(
"Added library (library_id = {}) to location: {location:?}",
library.id
"Added library (library_id = {}) to location: {:?}",
library.id, &location.data
);
Ok(Some(location))
Ok(Some(location.data))
} else {
Ok(None)
}
@ -232,22 +232,31 @@ impl LocationUpdateArgs {
let (sync_params, db_params): (Vec<_>, Vec<_>) = [
self.name
.clone()
.filter(|name| &location.name != name)
.map(|v| ((location::name::NAME, json!(v)), location::name::set(v))),
.filter(|name| location.name.as_ref() != Some(name))
.map(|v| {
(
(location::name::NAME, json!(v)),
location::name::set(Some(v)),
)
}),
self.generate_preview_media.map(|v| {
(
(location::generate_preview_media::NAME, json!(v)),
location::generate_preview_media::set(v),
location::generate_preview_media::set(Some(v)),
)
}),
self.sync_preview_media.map(|v| {
(
(location::sync_preview_media::NAME, json!(v)),
location::sync_preview_media::set(v),
location::sync_preview_media::set(Some(v)),
)
}),
self.hidden.map(|v| {
(
(location::hidden::NAME, json!(v)),
location::hidden::set(Some(v)),
)
}),
self.hidden
.map(|v| ((location::hidden::NAME, json!(v)), location::hidden::set(v))),
]
.into_iter()
.flatten()
@ -275,13 +284,15 @@ impl LocationUpdateArgs {
)
.await?;
if location.node_id == library.node_local_id {
if let Some(mut metadata) =
SpacedriveLocationMetadataFile::try_load(&location.path).await?
{
metadata
.update(library.id, self.name.expect("TODO"))
.await?;
if location.node_id == Some(library.node_local_id) {
if let Some(path) = &location.path {
if let Some(mut metadata) =
SpacedriveLocationMetadataFile::try_load(path).await?
{
metadata
.update(library.id, self.name.expect("TODO"))
.await?;
}
}
}
}
@ -356,7 +367,7 @@ pub async fn scan_location(
library: &Library,
location: location_with_indexer_rules::Data,
) -> Result<(), JobManagerError> {
if location.node_id != library.node_local_id {
if location.node_id != Some(library.node_local_id) {
return Ok(());
}
@ -390,7 +401,7 @@ pub async fn scan_location_sub_path(
sub_path: impl AsRef<Path>,
) -> Result<(), JobManagerError> {
let sub_path = sub_path.as_ref().to_path_buf();
if location.node_id != library.node_local_id {
if location.node_id != Some(library.node_local_id) {
return Ok(());
}
@ -424,7 +435,7 @@ pub async fn light_scan_location(
) -> Result<(), JobManagerError> {
let sub_path = sub_path.as_ref().to_path_buf();
if location.node_id != library.node_local_id {
if location.node_id != Some(library.node_local_id) {
return Ok(());
}
@ -467,7 +478,7 @@ pub async fn relink_location(
),
db.location().update(
location::pub_id::equals(pub_id),
vec![location::path::set(path)],
vec![location::path::set(Some(path))],
),
)
.await?;
@ -475,13 +486,19 @@ pub async fn relink_location(
Ok(())
}
#[derive(Debug)]
pub struct CreatedLocationResult {
pub name: String,
pub data: location_with_indexer_rules::Data,
}
async fn create_location(
library: &Library,
location_pub_id: Uuid,
location_path: impl AsRef<Path>,
indexer_rules_ids: &[i32],
dry_run: bool,
) -> Result<Option<location_with_indexer_rules::Data>, LocationError> {
) -> Result<Option<CreatedLocationResult>, LocationError> {
let Library { db, sync, .. } = &library;
let mut path = location_path.as_ref().to_path_buf();
@ -524,7 +541,7 @@ async fn create_location(
if library
.db
.location()
.count(vec![location::path::equals(location_path.clone())])
.count(vec![location::path::equals(Some(location_path.clone()))])
.exec()
.await? > 0
{
@ -559,23 +576,24 @@ async fn create_location(
pub_id: location_pub_id.as_bytes().to_vec(),
},
[
(location::name::NAME, json!(&name)),
(location::path::NAME, json!(&location_path)),
(
location::node::NAME,
json!(sync::node::SyncId {
pub_id: uuid_to_bytes(library.id)
}),
),
(location::name::NAME, json!(&name)),
(location::path::NAME, json!(&location_path)),
],
),
db.location()
.create(
location_pub_id.as_bytes().to_vec(),
name,
location_path,
node::id::equals(library.node_local_id),
vec![],
vec![
location::name::set(Some(name.clone())),
location::path::set(Some(location_path)),
location::node::connect(node::id::equals(library.node_local_id)),
],
)
.include(location_with_indexer_rules::include()),
)
@ -596,7 +614,10 @@ async fn create_location(
invalidate_query!(library, "locations.list");
Ok(Some(location))
Ok(Some(CreatedLocationResult {
data: location,
name,
}))
}
pub async fn delete_location(library: &Library, location_id: i32) -> Result<(), LocationError> {
@ -622,11 +643,11 @@ pub async fn delete_location(library: &Library, location_id: i32) -> Result<(),
.exec()
.await?;
if location.node_id == library.node_local_id {
if let Ok(Some(mut metadata)) =
SpacedriveLocationMetadataFile::try_load(&location.path).await
{
metadata.remove_library(library.id).await?;
if location.node_id == Some(library.node_local_id) {
if let Some(path) = &location.path {
if let Ok(Some(mut metadata)) = SpacedriveLocationMetadataFile::try_load(path).await {
metadata.remove_library(library.id).await?;
}
}
}

View file

@ -74,7 +74,11 @@ impl StatefulJob for FileIdentifierJob {
info!("Identifying orphan File Paths...");
let location_id = state.init.location.id;
let location_path = Path::new(&state.init.location.path);
let location_path = state.init.location.path.as_ref();
let Some(location_path) = location_path.map(Path::new) else {
return Err(JobError::MissingPath)
};
let maybe_sub_iso_file_path = if let Some(ref sub_path) = state.init.sub_path {
let full_path = ensure_sub_path_is_in_location(location_path, sub_path)

View file

@ -100,10 +100,15 @@ async fn identifier_job_step(
location: &location::Data,
file_paths: &[file_path_for_file_identifier::Data],
) -> Result<(usize, usize), JobError> {
let location_path = location.path.as_ref();
let Some(location_path) = location_path.map(Path::new) else {
return Err(JobError::MissingPath)
};
let file_path_metas = join_all(file_paths.iter().map(|file_path| async move {
// NOTE: `file_path`'s `materialized_path` begins with a `/` character so we remove it to join it with `location.path`
FileMetadata::new(
&location.path,
&location_path,
&IsolatedFilePathData::from((location.id, file_path)),
)
.await

View file

@ -33,7 +33,10 @@ pub async fn shallow(
info!("Identifying orphan File Paths...");
let location_id = location.id;
let location_path = Path::new(&location.path);
let location_path = location.path.as_ref();
let Some(location_path) = location_path.map(Path::new) else {
return Err(JobError::MissingPath)
};
let sub_iso_file_path = if sub_path != Path::new("") {
let full_path = ensure_sub_path_is_in_location(location_path, &sub_path)

View file

@ -55,6 +55,7 @@ pub async fn get_location_path_from_location_id(
value: String::from("location which matches location_id"),
})?
.path
.ok_or(JobError::MissingPath)?
.into())
}

View file

@ -35,7 +35,10 @@ pub async fn shallow_thumbnailer(
let thumbnail_dir = init_thumbnail_dir(library.config().data_directory()).await?;
let location_id = location.id;
let location_path = PathBuf::from(&location.path);
let location_path = match &location.path {
Some(v) => PathBuf::from(v),
None => return Ok(()),
};
let (path, iso_file_path) = if sub_path != Path::new("") {
let full_path = ensure_sub_path_is_in_location(&location_path, &sub_path)

View file

@ -67,7 +67,10 @@ impl StatefulJob for ThumbnailerJob {
// .join(THUMBNAIL_CACHE_DIR_NAME);
let location_id = state.init.location.id;
let location_path = PathBuf::from(&state.init.location.path);
let location_path = match &state.init.location.path {
Some(v) => PathBuf::from(v),
None => return Ok(()),
};
let (path, iso_file_path) = if let Some(ref sub_path) = state.init.sub_path {
let full_path = ensure_sub_path_is_in_location(&location_path, sub_path)

View file

@ -4,7 +4,7 @@ use crate::{
},
library::Library,
location::file_path_helper::{file_path_for_object_validator, IsolatedFilePathData},
prisma::{file_path, location},
prisma::file_path,
sync,
util::error::FileIOError,
};
@ -68,18 +68,8 @@ impl StatefulJob for ObjectValidatorJob {
.await?,
);
let location = db
.location()
.find_unique(location::id::equals(state.init.location_id))
.exec()
.await?
.ok_or(JobError::MissingFromDb(
"location",
format!("id={}", state.init.location_id),
))?;
state.data = Some(ObjectValidatorJobState {
root_path: location.path.into(),
root_path: state.init.path.clone(),
task_count: state.steps.len(),
});

View file

@ -247,21 +247,10 @@ impl SyncManager {
_ => todo!(),
},
ModelSyncData::Location(id, shared_op) => match shared_op {
SharedOperationData::Create(SharedOperationCreateData::Unique(mut data)) => {
SharedOperationData::Create(SharedOperationCreateData::Unique(data)) => {
db.location()
.create(
id.pub_id,
serde_json::from_value(data.remove(location::name::NAME).unwrap())
.unwrap(),
serde_json::from_value(data.remove(location::path::NAME).unwrap())
.unwrap(),
{
let val: std::collections::HashMap<String, Value> =
from_value(data.remove(location::node::NAME).unwrap()).unwrap();
let val = val.into_iter().next().unwrap();
node::UniqueWhereParam::deserialize(&val.0, val.1).unwrap()
},
data.into_iter()
.flat_map(|(k, v)| location::SetParam::deserialize(&k, v))
.collect(),

View file

@ -144,7 +144,7 @@ impl InitConfig {
if let Some(location) = library
.db
.location()
.find_first(vec![location::path::equals(loc.path.clone())])
.find_first(vec![location::path::equals(Some(loc.path.clone()))])
.exec()
.await?
{

View file

@ -1,59 +1,91 @@
import { Button, Popover, PopoverContainer, PopoverSection, Input, PopoverDivider, tw } from "@sd/ui";
import { Paperclip, Gear, FolderDotted, Archive, Image, Icon, IconContext, Copy } from "phosphor-react";
import { ReactComponent as Ellipsis } from '@sd/assets/svgs/ellipsis.svg';
import { Location, useLibraryMutation } from '@sd/client'
import {
Archive,
Copy,
FolderDotted,
Gear,
Icon,
IconContext,
Image,
Paperclip
} from 'phosphor-react';
import { Location, useLibraryMutation } from '@sd/client';
import {
Button,
Input,
Popover,
PopoverContainer,
PopoverDivider,
PopoverSection,
tw
} from '@sd/ui';
import TopBarButton from '../TopBar/TopBarButton';
import TopBarButton from "../TopBar/TopBarButton";
const OptionButton = tw(TopBarButton)`w-full gap-1 !px-1.5 !py-1`
export default function LocationOptions({ location, path }: { location: Location, path: string }) {
const OptionButton = tw(TopBarButton)`w-full gap-1 !px-1.5 !py-1`;
export default function LocationOptions({ location, path }: { location: Location; path: string }) {
const _scanLocation = useLibraryMutation('locations.fullRescan');
const scanLocation = () => _scanLocation.mutate(location.id);
const _regenThumbs = useLibraryMutation('jobs.generateThumbsForLocation');
const regenThumbs = () => _regenThumbs.mutate({ id: location.id, path });
const archiveLocation = () => alert("Not implemented");
const archiveLocation = () => alert('Not implemented');
let currentPath = path ? location.path + path : location.path;
currentPath = currentPath.endsWith("/") ? currentPath.substring(0, currentPath.length - 1) : currentPath;
currentPath = currentPath?.endsWith('/')
? currentPath.substring(0, currentPath.length - 1)
: currentPath;
return (
<div className='opacity-30 group-hover:opacity-70'>
<IconContext.Provider value={{ size: 20, className: "r-1 h-4 w-4 opacity-60" }}>
<Popover trigger={<Button className="!p-[5px]" variant="subtle">
<Ellipsis className="h-3 w-3" />
</Button>}>
<div className="opacity-30 group-hover:opacity-70">
<IconContext.Provider value={{ size: 20, className: 'r-1 h-4 w-4 opacity-60' }}>
<Popover
trigger={
<Button className="!p-[5px]" variant="subtle">
<Ellipsis className="h-3 w-3" />
</Button>
}
>
<PopoverContainer>
<PopoverSection>
<Input autoFocus className='mb-2' value={currentPath} right={
<Button
size="icon"
variant="outline"
className='opacity-70'
>
<Copy className="!pointer-events-none h-4 w-4" />
</Button>
} />
<OptionButton><Gear />Configure Location</OptionButton>
<Input
autoFocus
className="mb-2"
value={currentPath ?? ''}
right={
<Button size="icon" variant="outline" className="opacity-70">
<Copy className="!pointer-events-none h-4 w-4" />
</Button>
}
/>
<OptionButton>
<Gear />
Configure Location
</OptionButton>
</PopoverSection>
<PopoverDivider />
<PopoverSection>
<OptionButton onClick={scanLocation}><FolderDotted />Re-index</OptionButton>
<OptionButton onClick={regenThumbs}><Image />Regenerate Thumbs</OptionButton>
<OptionButton onClick={scanLocation}>
<FolderDotted />
Re-index
</OptionButton>
<OptionButton onClick={regenThumbs}>
<Image />
Regenerate Thumbs
</OptionButton>
</PopoverSection>
<PopoverDivider />
<PopoverSection>
<OptionButton onClick={archiveLocation}><Archive />Archive</OptionButton>
<OptionButton onClick={archiveLocation}>
<Archive />
Archive
</OptionButton>
</PopoverSection>
</PopoverContainer>
</Popover>
</IconContext.Provider>
</div>
)
);
}

View file

@ -15,13 +15,13 @@ const FlexCol = tw.label`flex flex-col flex-1`;
const ToggleSection = tw.label`flex flex-row w-full`;
const schema = z.object({
name: z.string(),
path: z.string(),
hidden: z.boolean(),
name: z.string().nullable(),
path: z.string().nullable(),
hidden: z.boolean().nullable(),
indexerRulesIds: z.array(z.number()),
locationType: z.string(),
syncPreviewMedia: z.boolean(),
generatePreviewMedia: z.boolean()
syncPreviewMedia: z.boolean().nullable(),
generatePreviewMedia: z.boolean().nullable()
});
const PARAMS = z.object({

View file

@ -8,7 +8,7 @@ import { Folder } from '~/components/Folder';
import DeleteDialog from './DeleteDialog';
interface Props {
location: Location & { node: Node };
location: Location & { node: Node | null };
}
export default ({ location }: Props) => {
@ -33,9 +33,11 @@ export default ({ location }: Props) => {
<div className="grid min-w-[110px] grid-cols-1">
<h1 className="pt-0.5 text-sm font-semibold">{location.name}</h1>
<p className="mt-0.5 select-text truncate text-sm text-ink-dull">
<span className="mr-1 rounded bg-app-selected px-1 py-[1px]">
{location.node.name}
</span>
{location.node && (
<span className="mr-1 rounded bg-app-selected px-1 py-[1px]">
{location.node.name}
</span>
)}
{location.path}
</p>
</div>

View file

@ -15,7 +15,7 @@ export const Component = () => {
const filteredLocations = useMemo(
() =>
locations.data?.filter((location) =>
location.name.toLowerCase().includes(debouncedSearch.toLowerCase())
location.name?.toLowerCase().includes(debouncedSearch.toLowerCase())
),
[debouncedSearch, locations.data]
);

View file

@ -16,7 +16,7 @@ export type Procedures = {
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: IndexerRule } |
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: IndexerRule[] } |
{ key: "locations.indexer_rules.listForLocation", input: LibraryArgs<number>, result: IndexerRule[] } |
{ key: "locations.list", input: LibraryArgs<null>, result: { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string; node: Node }[] } |
{ key: "locations.list", input: LibraryArgs<null>, result: { id: number; pub_id: number[]; node_id: number | null; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; node: Node | null }[] } |
{ key: "nodeState", input: never, result: NodeState } |
{ key: "search.objects", input: LibraryArgs<ObjectSearchArgs>, result: SearchData<ExplorerItem> } |
{ key: "search.paths", input: LibraryArgs<FilePathSearchArgs>, result: SearchData<ExplorerItem> } |
@ -149,7 +149,7 @@ export type LibraryConfigWrapped = { uuid: string; config: LibraryConfig }
export type LightScanArgs = { location_id: number; sub_path: string }
export type Location = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string }
export type Location = { id: number; pub_id: number[]; node_id: number | null; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null }
/**
* `LocationCreateArgs` is the argument received from the client using `rspc` to create a new location.
@ -168,7 +168,7 @@ export type LocationCreateArgs = { path: string; dry_run: boolean; indexer_rules
*/
export type LocationUpdateArgs = { id: number; name: string | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; indexer_rules_ids: number[] }
export type LocationWithIndexerRules = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string; indexer_rules: { indexer_rule: IndexerRule }[] }
export type LocationWithIndexerRules = { id: number; pub_id: number[]; node_id: number | null; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; indexer_rules: { indexer_rule: IndexerRule }[] }
export type MaybeNot<T> = T | { not: T }