mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-14 01:54:04 +00:00
[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:
parent
553f50e6fe
commit
7148209343
|
@ -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',
|
||||
|
|
|
@ -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} />
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
|
|
@ -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`}
|
||||
|
|
23
core/prisma/migrations/20230608164040_/migration.sql
Normal file
23
core/prisma/migrations/20230608164040_/migration.sql
Normal 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;
|
|
@ -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[]
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:?}")]
|
||||
|
|
|
@ -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()?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -69,6 +69,7 @@ file_path::select!(file_path_to_handle_custom_uri {
|
|||
name
|
||||
extension
|
||||
location: select {
|
||||
id
|
||||
path
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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?
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
Loading…
Reference in a new issue