[ENG-864] Create directory (#1458)

* folder

* wrote function

* Abstracting duplicate on file name

* Spliting between ephemeral and indexed

* Now with more type safety

* Forgot to prep

* location + path based

* bruh

* link frontend + error toast

* strip main separator

* dumb

* bruh

* create directory

Co-authored-by: Brendan Allan <Brendonovich@users.noreply.github.com>

* make some reactivity happen

---------

Co-authored-by: Ericson Fogo Soares <ericson.ds999@gmail.com>
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
Co-authored-by: Brendan Allan <Brendonovich@users.noreply.github.com>
This commit is contained in:
Jamie Pine
2023-10-10 17:47:56 -07:00
committed by GitHub
parent 6fd8bd4ad8
commit 01dbc2caf7
30 changed files with 335 additions and 132 deletions

View File

@@ -5,14 +5,16 @@ use crate::{
library::Library,
location::{
file_path_helper::{
file_path_to_isolate, file_path_to_isolate_with_id, FilePathError, IsolatedFilePathData,
file_path_to_full_path, file_path_to_isolate, file_path_to_isolate_with_id,
FilePathError, IsolatedFilePathData,
},
get_location_path_from_location_id, LocationError,
},
object::{
fs::{
copy::FileCopierJobInit, cut::FileCutterJobInit, delete::FileDeleterJobInit,
erase::FileEraserJobInit,
erase::FileEraserJobInit, error::FileSystemJobsError,
find_available_filename_for_duplicate,
},
media::{
media_data_extractor::{
@@ -31,7 +33,7 @@ use sd_media_metadata::MediaMetadata;
use std::{
ffi::OsString,
path::{Path, PathBuf},
path::{Path, PathBuf, MAIN_SEPARATOR, MAIN_SEPARATOR_STR},
str::FromStr,
sync::Arc,
};
@@ -47,6 +49,8 @@ use tracing::{error, warn};
use super::{Ctx, R};
const UNTITLED_FOLDER_STR: &str = "Untitled Folder";
pub(crate) fn mount() -> AlphaRouter<Ctx> {
R.router()
.procedure("get", {
@@ -194,6 +198,53 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
Ok(())
})
})
.procedure("createFolder", {
#[derive(Type, Deserialize)]
pub struct CreateFolderArgs {
pub location_id: location::id::Type,
pub sub_path: Option<PathBuf>,
pub name: Option<String>,
}
R.with2(library()).mutation(
|(_, library),
CreateFolderArgs {
location_id,
sub_path,
name,
}: CreateFolderArgs| async move {
let mut path =
get_location_path_from_location_id(&library.db, location_id).await?;
if let Some(sub_path) = sub_path
.as_ref()
.and_then(|sub_path| sub_path.strip_prefix("/").ok())
{
path.push(sub_path);
}
path.push(name.as_deref().unwrap_or(UNTITLED_FOLDER_STR));
dbg!(&path);
create_directory(path, &library).await
},
)
})
.procedure("createEphemeralFolder", {
#[derive(Type, Deserialize)]
pub struct CreateEphemeralFolderArgs {
pub path: PathBuf,
pub name: Option<String>,
}
R.with2(library()).mutation(
|(_, library),
CreateEphemeralFolderArgs { mut path, name }: CreateEphemeralFolderArgs| async move {
path.push(name.as_deref().unwrap_or(UNTITLED_FOLDER_STR));
create_directory(path, &library).await
},
)
})
.procedure("updateAccessTime", {
R.with2(library())
.mutation(|(_, library), ids: Vec<i32>| async move {
@@ -692,3 +743,43 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
)
})
}
async fn create_directory(
mut target_path: PathBuf,
library: &Library,
) -> Result<String, rspc::Error> {
match fs::metadata(&target_path).await {
Ok(metadata) if metadata.is_dir() => {
target_path = find_available_filename_for_duplicate(&target_path).await?;
}
Ok(_) => {
return Err(FileSystemJobsError::WouldOverwrite(target_path.into_boxed_path()).into())
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
// Everything is awesome!
}
Err(e) => {
return Err(FileIOError::from((
target_path,
e,
"Failed to access file system and get metadata on directory to be created",
))
.into())
}
};
fs::create_dir(&target_path)
.await
.map_err(|e| FileIOError::from((&target_path, e, "Failed to create directory")))?;
println!("Created directory: {}", target_path.display());
invalidate_query!(library, "search.objects");
invalidate_query!(library, "search.paths");
Ok(target_path
.file_name()
.expect("Failed to get file name")
.to_string_lossy()
.to_string())
}

View File

@@ -29,7 +29,7 @@ use uuid::Uuid;
use super::{JobManagerError, JobReport, JobStatus, StatefulJob};
// db is single threaded, nerd
const MAX_WORKERS: usize = 1;
const MAX_WORKERS: usize = 5;
pub enum JobManagerEvent {
IngestJob(Arc<Library>, Box<dyn DynJob>),

View File

@@ -13,7 +13,7 @@ use crate::{
},
};
use std::{ffi::OsStr, hash::Hash, path::PathBuf};
use std::{hash::Hash, path::PathBuf};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -22,8 +22,8 @@ use tokio::{fs, io};
use tracing::{trace, warn};
use super::{
append_digit_to_filename, construct_target_filename, error::FileSystemJobsError,
fetch_source_and_target_location_paths, get_file_data_from_isolated_file_path,
construct_target_filename, error::FileSystemJobsError, fetch_source_and_target_location_paths,
find_available_filename_for_duplicate, get_file_data_from_isolated_file_path,
get_many_files_datas, FileData,
};
@@ -173,68 +173,27 @@ impl StatefulJob for FileCopierJobInit {
} else {
match fs::metadata(target_full_path).await {
Ok(_) => {
let new_file_name =
target_full_path
.file_stem()
.ok_or(JobError::JobDataNotFound(
"No stem on file path, but it's supposed to be a file".to_string(),
))?;
let new_file_full_path_without_suffix = target_full_path.parent().map_or_else(
|| {
Err(JobError::JobDataNotFound(
"No parent for file path, which is supposed to be directory"
.to_string(),
))
},
|x| Ok(x.to_path_buf()),
)?;
for i in 1..u32::MAX {
let mut new_file_full_path_candidate =
new_file_full_path_without_suffix.clone();
append_digit_to_filename(
&mut new_file_full_path_candidate,
new_file_name.to_str().ok_or(JobError::JobDataNotFound(
"Unable to convert file name to &str".to_string(),
))?,
target_full_path.extension().and_then(OsStr::to_str),
i,
);
match fs::metadata(&new_file_full_path_candidate).await {
Ok(_) => {
// This candidate already exists, so we try the next one
continue;
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
fs::copy(
&source_file_data.full_path,
&new_file_full_path_candidate,
)
// Already exist a file with this name, so we need to find an available name
match find_available_filename_for_duplicate(target_full_path).await {
Ok(new_path) => {
fs::copy(&source_file_data.full_path, &new_path)
.await
// Using the ? here because we don't want to increase the completed task
// count in case of file system errors
.map_err(|e| {
FileIOError::from((new_file_full_path_candidate, e))
})?;
.map_err(|e| FileIOError::from((new_path, e)))?;
break;
}
Err(e) => {
return Err(
FileIOError::from((new_file_full_path_candidate, e)).into()
)
}
Ok(().into())
}
}
Ok(JobRunErrors(vec![FileSystemJobsError::WouldOverwrite(
target_full_path.clone().into_boxed_path(),
)
.to_string()])
.into())
Err(FileSystemJobsError::FailedToFindAvailableName(path)) => {
Ok(JobRunErrors(vec![
FileSystemJobsError::WouldOverwrite(path).to_string()
])
.into())
}
Err(e) => Err(e.into()),
}
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
trace!(
@@ -251,7 +210,7 @@ impl StatefulJob for FileCopierJobInit {
Ok(().into())
}
Err(e) => return Err(FileIOError::from((target_full_path, e)).into()),
Err(e) => Err(FileIOError::from((target_full_path, e)).into()),
}
}
}

View File

@@ -1,30 +0,0 @@
// use crate::{
// library::LibraryContext,
// location::{check_virtual_path_exists, fetch_location, LocationError},
// };
// use super::error::VirtualFSError;
// use crate::prisma::{file_path, location};
// // TODO: we should create an action handler for all FS operations, that can work for both local and remote locations
// // if the location is remote, we queue a job for that client specifically
// // the actual create_folder function should be an option on an enum for all vfs actions
// pub async fn create_folder(
// location_id: location::id::Type,
// path: &str,
// name: Option<&str>,
// library: &LibraryContext,
// ) -> Result<(), VirtualFSError> {
// let location = fetch_location(library, location_id)
// .exec()
// .await?
// .ok_or(LocationError::IdNotFound(location_id))?;
// let name = name.unwrap_or("Untitled Folder");
// let exists = check_virtual_path_exists(library, location_id, subpath).await?;
// std::fs::create_dir_all(&obj_path)?;
// Ok(())
// }

View File

@@ -1,7 +1,10 @@
use crate::{
location::{file_path_helper::FilePathError, LocationError},
prisma::file_path,
util::{db::MissingFieldError, error::FileIOError},
util::{
db::MissingFieldError,
error::{FileIOError, NonUtf8PathError},
},
};
use std::path::Path;
@@ -28,4 +31,20 @@ pub enum FileSystemJobsError {
WouldOverwrite(Box<Path>),
#[error("missing-field: {0}")]
MissingField(#[from] MissingFieldError),
#[error("no parent for path, which is supposed to be directory: <path='{}'>", .0.display())]
MissingParentPath(Box<Path>),
#[error("no stem on file path, but it's supposed to be a file: <path='{}'>", .0.display())]
MissingFileStem(Box<Path>),
#[error(transparent)]
FileIO(#[from] FileIOError),
#[error(transparent)]
NonUTF8Path(#[from] NonUtf8PathError),
#[error("failed to find an available name to avoid duplication: <path='{}'>", .0.display())]
FailedToFindAvailableName(Box<Path>),
}
impl From<FileSystemJobsError> for rspc::Error {
fn from(e: FileSystemJobsError) -> Self {
Self::with_cause(rspc::ErrorCode::InternalServerError, e.to_string(), e)
}
}

View File

@@ -4,16 +4,21 @@ use crate::{
LocationError,
},
prisma::{file_path, location, PrismaClient},
util::db::{maybe_missing, MissingFieldError},
util::{
db::{maybe_missing, MissingFieldError},
error::{FileIOError, NonUtf8PathError},
},
};
use std::path::{Path, PathBuf};
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
pub mod create;
pub mod delete;
pub mod erase;
@@ -26,6 +31,7 @@ pub mod cut;
pub mod error;
use error::FileSystemJobsError;
use tokio::{fs, io};
static DUPLICATE_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new(r" \(\d+\)").expect("Failed to compile hardcoded regex"));
@@ -167,3 +173,48 @@ pub fn append_digit_to_filename(
final_path.push(format!("{new_file_name} ({current_int})"));
}
}
pub async fn find_available_filename_for_duplicate(
target_path: impl AsRef<Path>,
) -> Result<PathBuf, FileSystemJobsError> {
let target_path = target_path.as_ref();
let new_file_name = target_path
.file_stem()
.ok_or_else(|| {
FileSystemJobsError::MissingFileStem(target_path.to_path_buf().into_boxed_path())
})?
.to_str()
.ok_or_else(|| NonUtf8PathError(target_path.to_path_buf().into_boxed_path()))?;
let new_file_full_path_without_suffix =
target_path.parent().map(Path::to_path_buf).ok_or_else(|| {
FileSystemJobsError::MissingParentPath(target_path.to_path_buf().into_boxed_path())
})?;
for i in 1..u32::MAX {
let mut new_file_full_path_candidate = new_file_full_path_without_suffix.clone();
append_digit_to_filename(
&mut new_file_full_path_candidate,
new_file_name,
target_path.extension().and_then(OsStr::to_str),
i,
);
match fs::metadata(&new_file_full_path_candidate).await {
Ok(_) => {
// This candidate already exists, so we try the next one
continue;
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
return Ok(new_file_full_path_candidate);
}
Err(e) => return Err(FileIOError::from((new_file_full_path_candidate, e)).into()),
}
}
Err(FileSystemJobsError::FailedToFindAvailableName(
target_path.to_path_buf().into_boxed_path(),
))
}

View File

@@ -3,6 +3,7 @@ import { libraryClient, useLibraryMutation } from '@sd/client';
import { ContextMenu, dialogManager, ModifierKeys, toast } from '@sd/ui';
import { Menu } from '~/components/Menu';
import { useKeybindFactory } from '~/hooks/useKeybindFactory';
import { useQuickRescan } from '~/hooks/useQuickRescan';
import { isNonEmpty } from '~/util';
import { useExplorerContext } from '../../Context';
@@ -27,6 +28,8 @@ export const Delete = new ConditionalItem({
Component: ({ selectedFilePaths, locationId }) => {
const keybind = useKeybindFactory();
const rescan = useQuickRescan();
return (
<Menu.Item
icon={Trash}
@@ -37,6 +40,7 @@ export const Delete = new ConditionalItem({
dialogManager.create((dp) => (
<DeleteDialog
{...dp}
rescan={rescan}
locationId={locationId}
pathIds={selectedFilePaths.map((p) => p.id)}
/>

View File

@@ -3,6 +3,7 @@ import { CheckBox, Dialog, Tooltip, useDialog, UseDialogProps } from '@sd/ui';
interface Props extends UseDialogProps {
locationId: number;
rescan?: () => void;
pathIds: number[];
}
@@ -14,12 +15,14 @@ export default (props: Props) => {
return (
<Dialog
form={form}
onSubmit={form.handleSubmit(() =>
deleteFile.mutateAsync({
onSubmit={form.handleSubmit(async () => {
await deleteFile.mutateAsync({
location_id: props.locationId,
file_path_ids: props.pathIds
})
)}
});
props.rescan?.();
})}
dialog={useDialog(props)}
title="Delete a file"
description="Warning: This will delete your file forever, we don't have a trash can yet..."

View File

@@ -1,5 +1,6 @@
import {
ArrowClockwise,
FolderPlus,
Key,
MonitorPlay,
Rows,
@@ -8,11 +9,14 @@ import {
SquaresFour,
Tag
} from '@phosphor-icons/react';
import { useQueryClient } from '@tanstack/react-query';
import clsx from 'clsx';
import { useEffect, useRef } from 'react';
import { useRspcLibraryContext } from '@sd/client';
import { useKeyMatcher } from '~/hooks';
import { useLibraryMutation, useLibraryQuery, useRspcLibraryContext } from '@sd/client';
import { ModifierKeys, toast } from '@sd/ui';
import { useKeybind, useKeyMatcher, useOperatingSystem } from '~/hooks';
import { useQuickRescan } from '../../../hooks/useQuickRescan';
import { KeyManager } from '../KeyManager';
import TopBarOptions, { ToolOption, TOP_BAR_ICON_STYLE } from '../TopBar/TopBarOptions';
import { useExplorerContext } from './Context';
@@ -24,9 +28,21 @@ export const useExplorerTopBarOptions = () => {
const explorerStore = useExplorerStore();
const explorer = useExplorerContext();
const controlIcon = useKeyMatcher('Meta').icon;
const settings = explorer.useSettingsSnapshot();
const rescan = useQuickRescan();
const createFolder = useLibraryMutation(['files.createFolder'], {
onError: (e) => {
toast.error({ title: 'Error creating folder', body: `Error: ${e}.` });
console.error(e);
},
onSuccess: (folder) => {
toast.success({ title: `Created new folder "${folder}"` });
rescan();
}
});
const viewOptions: ToolOption[] = [
{
toolTipLabel: 'Grid view',
@@ -67,7 +83,7 @@ export const useExplorerTopBarOptions = () => {
icon: <SlidersHorizontal className={TOP_BAR_ICON_STYLE} />,
popOverComponent: <OptionsPanel />,
individual: true,
showAtResolution: 'xl:flex'
showAtResolution: 'sm:flex'
},
{
toolTipLabel: 'Show Inspector',
@@ -87,19 +103,28 @@ export const useExplorerTopBarOptions = () => {
}
];
// subscription so that we can cancel it if in progress
const quickRescanSubscription = useRef<() => void | undefined>();
// gotta clean up any rescan subscriptions if the exist
useEffect(() => () => quickRescanSubscription.current?.(), []);
const { client } = useRspcLibraryContext();
const { parent } = useExplorerContext();
const [{ path }] = useExplorerSearchParams();
const os = useOperatingSystem();
useKeybind([os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control, 'r'], () => rescan());
const toolOptions = [
parent?.type === 'Location' && {
toolTipLabel: 'New Folder',
icon: <FolderPlus className={TOP_BAR_ICON_STYLE} />,
onClick: () => {
createFolder.mutate({
location_id: parent.location.id,
sub_path: path || null,
name: null
});
},
individual: true,
showAtResolution: 'xs:flex'
},
{
toolTipLabel: 'Key Manager',
icon: <Key className={TOP_BAR_ICON_STYLE} />,
@@ -122,19 +147,7 @@ export const useExplorerTopBarOptions = () => {
},
parent?.type === 'Location' && {
toolTipLabel: 'Reload',
onClick: () => {
quickRescanSubscription.current?.();
quickRescanSubscription.current = client.addSubscription(
[
'locations.quickRescan',
{
location_id: parent.location.id,
sub_path: path ?? ''
}
],
{ onData() {} }
);
},
onClick: rescan,
icon: <ArrowClockwise className={TOP_BAR_ICON_STYLE} />,
individual: true,
showAtResolution: 'xl:flex'

View File

@@ -53,8 +53,6 @@ export const EphemeralSection = () => {
return locationIdsMap;
}, [locations.data, volumes.data]);
console.log('locationIdsForVolumes', locationIdsForVolumes);
const items = [
{ type: 'network' },
home ? { type: 'home', path: home } : null,

View File

@@ -60,8 +60,6 @@ export const LibrarySection = () => {
null
);
const [seeMoreLocations, setSeeMoreLocations] = useState(false);
useEffect(() => {
const outsideClick = () => {
document.addEventListener('click', () => {

View File

@@ -1,7 +1,11 @@
import { Info } from '@phosphor-icons/react';
import { getIcon, iconNames } from '@sd/assets/util';
import clsx from 'clsx';
import { useCallback, useEffect, useMemo } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { stringify } from 'uuid';
import {
arraysEqual,
ExplorerSettings,
FilePathFilterArgs,
FilePathOrder,
@@ -10,19 +14,23 @@ import {
useLibraryMutation,
useLibraryQuery,
useLibrarySubscription,
useOnlineLocations,
useRspcLibraryContext
} from '@sd/client';
import { Tooltip } from '@sd/ui';
import { LocationIdParamsSchema } from '~/app/route-schemas';
import { Folder } from '~/components';
import { useKeyDeleteFile, useZodRouteParams } from '~/hooks';
import Explorer from '../Explorer';
import { ExplorerContextProvider } from '../Explorer/Context';
import { InfoPill } from '../Explorer/Inspector';
import { usePathsInfiniteQuery } from '../Explorer/queries';
import { createDefaultExplorerSettings, filePathOrderingKeysSchema } from '../Explorer/store';
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
import { useExplorer, UseExplorerSettings, useExplorerSettings } from '../Explorer/useExplorer';
import { useExplorerSearchParams } from '../Explorer/util';
import { EmptyNotice } from '../Explorer/View';
import { TopBarPortal } from '../TopBar/Portal';
import LocationOptions from './LocationOptions';
@@ -32,6 +40,14 @@ export const Component = () => {
const location = useLibraryQuery(['locations.get', locationId]);
const rspc = useRspcLibraryContext();
const onlineLocations = useOnlineLocations();
const locationOnline = useMemo(() => {
const pub_id = location.data?.pub_id;
if (!pub_id) return false;
return onlineLocations.some((l) => arraysEqual(pub_id, l));
}, [location.data?.pub_id, onlineLocations]);
const preferences = useLibraryQuery(['preferences.get']);
const updatePreferences = useLibraryMutation('preferences.update');
@@ -117,6 +133,11 @@ export const Component = () => {
? getLastSectionOfPath(path)
: location.data?.name}
</span>
{!locationOnline && (
<Tooltip label="Location is offline, you can still browse and organize.">
<Info className="text-ink-faint" />
</Tooltip>
)}
{location.data && (
<LocationOptions location={location.data} path={path || ''} />
)}
@@ -125,7 +146,15 @@ export const Component = () => {
right={<DefaultTopBarOptions />}
/>
<Explorer />
<Explorer
emptyNotice={
<EmptyNotice
loading={location.isFetching}
icon={<img className="h-32 w-32" src={getIcon(iconNames.FolderNoSpace)} />}
message="No files found here"
/>
}
/>
</ExplorerContextProvider>
);
};

View File

@@ -0,0 +1,38 @@
import { useEffect, useRef } from 'react';
import { useRspcLibraryContext } from '@sd/client';
import { useExplorerContext } from '../app/$libraryId/Explorer/Context';
import { useExplorerSearchParams } from '../app/$libraryId/Explorer/util';
import { useOperatingSystem } from './useOperatingSystem';
export const useQuickRescan = () => {
// subscription so that we can cancel it if in progress
const quickRescanSubscription = useRef<() => void | undefined>();
// gotta clean up any rescan subscriptions if the exist
useEffect(() => () => quickRescanSubscription.current?.(), []);
const { client } = useRspcLibraryContext();
const { parent } = useExplorerContext();
const [{ path }] = useExplorerSearchParams();
const rescan = () => {
if (parent?.type === 'Location') {
quickRescanSubscription.current?.();
quickRescanSubscription.current = client.addSubscription(
[
'locations.quickRescan',
{
location_id: parent.location.id,
sub_path: path ?? ''
}
],
{ onData() {} }
);
}
};
return rescan;
};

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -34,6 +34,16 @@ import Document_xls_Light from './Document_xls_Light.png';
import Document_xls from './Document_xls.png';
import Document from './Document.png';
import Drive_Light from './Drive_Light.png';
import DriveAmazonS3 from './Drive-AmazonS3.png';
import DriveBackBlaze from './Drive-BackBlaze.png';
import DriveBox from './Drive-Box.png';
import DriveDAV from './Drive-DAV.png';
import DriveDropbox from './Drive-Dropbox.png';
import DriveGoogleDrive from './Drive-GoogleDrive.png';
import DriveMega from './Drive-Mega.png';
import DriveOneDrive from './Drive-OneDrive.png';
import DriveOpenStack from './Drive-OpenStack.png';
import DrivePCloud from './Drive-PCloud.png';
import Drive from './Drive.png';
import Dropbox from './Dropbox.png';
import Encrypted_Light from './Encrypted_Light.png';
@@ -49,6 +59,7 @@ import Folder_Light from './Folder_Light.png';
import Folder from './Folder.png';
import FolderGrey_Light from './FolderGrey_Light.png';
import FolderGrey from './FolderGrey.png';
import FolderNoSpace from './FolderNoSpace.png';
import Game_Light from './Game_Light.png';
import Game from './Game.png';
import Globe from './Globe.png';
@@ -91,6 +102,7 @@ import ScreenshotAlt from './ScreenshotAlt.png';
import SD from './SD.png';
import Server_Light from './Server_Light.png';
import Server from './Server.png';
import Spacedrop1 from './Spacedrop-1.png';
import Spacedrop from './Spacedrop.png';
import Tablet_Light from './Tablet_Light.png';
import Tablet from './Tablet.png';
@@ -145,6 +157,16 @@ export {
Document_pdf_Light,
Document_xls,
Document_xls_Light,
DriveAmazonS3,
DriveBackBlaze,
DriveBox,
DriveDAV,
DriveDropbox,
DriveGoogleDrive,
DriveMega,
DriveOneDrive,
DriveOpenStack,
DrivePCloud,
Drive,
Drive_Light,
Dropbox,
@@ -160,6 +182,7 @@ export {
Folder,
FolderGrey,
FolderGrey_Light,
FolderNoSpace,
Folder_Light,
Game,
Game_Light,
@@ -203,6 +226,7 @@ export {
ScreenshotAlt,
Server,
Server_Light,
Spacedrop1,
Spacedrop,
Tablet,
Tablet_Light,

View File

@@ -48,6 +48,8 @@ export type Procedures = {
{ key: "backups.restore", input: string, result: null } |
{ key: "files.convertImage", input: LibraryArgs<ConvertImageArgs>, result: null } |
{ key: "files.copyFiles", input: LibraryArgs<FileCopierJobInit>, result: null } |
{ key: "files.createEphemeralFolder", input: LibraryArgs<CreateEphemeralFolderArgs>, result: string } |
{ key: "files.createFolder", input: LibraryArgs<CreateFolderArgs>, result: string } |
{ key: "files.cutFiles", input: LibraryArgs<FileCutterJobInit>, result: null } |
{ key: "files.deleteFiles", input: LibraryArgs<FileDeleterJobInit>, result: null } |
{ key: "files.duplicateFiles", input: LibraryArgs<FileCopierJobInit>, result: null } |
@@ -136,6 +138,10 @@ export type ConvertImageArgs = { location_id: number; file_path_id: number; dele
export type ConvertableExtension = "bmp" | "dib" | "ff" | "gif" | "ico" | "jpg" | "jpeg" | "png" | "pnm" | "qoi" | "tga" | "icb" | "vda" | "vst" | "tiff" | "tif" | "heif" | "heifs" | "heic" | "heics" | "avif" | "avci" | "avcs" | "svg" | "svgz" | "pdf"
export type CreateEphemeralFolderArgs = { path: string; name: string | null }
export type CreateFolderArgs = { location_id: number; sub_path: string | null; name: string | null }
export type CreateLibraryArgs = { name: LibraryName }
export type CursorOrderItem<T> = { order: SortOrder; data: T }