migrate mobile with new endpoints

This commit is contained in:
m
2026-07-28 20:28:32 +02:00
parent d40ca87075
commit 9c699446dc
28 changed files with 799 additions and 444 deletions
+2 -3
View File
@@ -77,7 +77,6 @@ export function useAutoSync() {
if (globalMode === 'auto') {
// Mode auto global : tous les fichiers locaux sans backendId
// (pas de filtre par dossier)
} else {
// Mode manuel : uniquement les fichiers des dossiers en mode auto
const allFolders = safDirectory.getAll();
@@ -85,7 +84,7 @@ export function useAutoSync() {
allFolders.filter((f) => f.syncMode === 'auto').map((f) => f.id)
);
pendingFiles = pendingFiles.filter(
(entry) => entry.parentFileId && autoFolderIds.has(entry.parentFileId)
(entry) => entry.parentResourceId && autoFolderIds.has(entry.parentResourceId)
);
}
@@ -111,7 +110,7 @@ export function useAutoSync() {
}
}
queryClient.invalidateQueries({ queryKey: ['files'] });
queryClient.invalidateQueries({ queryKey: ['resources'] });
} finally {
setIsSyncing(false);
isRunning.current = false;
+103
View File
@@ -0,0 +1,103 @@
import { useState, useEffect, useCallback } from 'react';
import { Platform } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import { apiClient } from '../api/client';
import { ENDPOINTS } from '../constants/api';
import { Device as DeviceType } from '../types';
const DEVICE_ID_KEY = 'vaultdrop_device_id';
const DEVICE_NAME_KEY = 'vaultdrop_device_name';
const DEVICE_SERVER_ID_KEY = 'vaultdrop_device_server_id';
function generateDefaultDeviceName(): string {
const constants = Platform.constants as Record<string, unknown>;
const brand = String(constants?.Manufacturer ?? '');
const model = String(constants?.Model ?? '');
const suffix = Math.random().toString(36).slice(2, 6);
const base = [brand, model].filter(Boolean).join(' ') || Platform.OS;
return `${Platform.OS === 'ios' ? 'iOS' : 'Android'} ${base} (${suffix})`;
}
async function getOrCreateDeviceId(): Promise<string> {
let deviceId = await SecureStore.getItemAsync(DEVICE_ID_KEY);
if (!deviceId) {
deviceId = `device_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
await SecureStore.setItemAsync(DEVICE_ID_KEY, deviceId);
}
return deviceId;
}
export async function getStoredDeviceServerId(): Promise<string | null> {
return SecureStore.getItemAsync(DEVICE_SERVER_ID_KEY);
}
export async function getStoredDeviceName(): Promise<string | null> {
return SecureStore.getItemAsync(DEVICE_NAME_KEY);
}
export function useDeviceRegistration() {
const [device, setDevice] = useState<{ localId: string; serverId: string | null; name: string } | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isRegistered, setIsRegistered] = useState<boolean | null>(null);
const register = useCallback(async (deviceName: string) => {
const localId = await getOrCreateDeviceId();
const result = await apiClient.post<{ id: string; device_name: string; role: string }>(ENDPOINTS.DEVICES, {
device_name: deviceName,
});
await SecureStore.setItemAsync(DEVICE_SERVER_ID_KEY, result.id);
await SecureStore.setItemAsync(DEVICE_NAME_KEY, result.device_name);
setDevice({ localId, serverId: result.id, name: result.device_name });
setIsRegistered(true);
return result;
}, []);
const checkRegistration = useCallback(async () => {
try {
const localId = await getOrCreateDeviceId();
const storedName = await getStoredDeviceName();
try {
const devices = await apiClient.get<DeviceType[]>(ENDPOINTS.DEVICES);
if (devices.length > 0) {
const existing = devices[0];
await SecureStore.setItemAsync(DEVICE_SERVER_ID_KEY, existing.id);
const name = storedName || existing.device_name;
setDevice({ localId, serverId: existing.id, name });
setIsRegistered(true);
setIsLoading(false);
return;
}
} catch {
// Not registered yet
}
// Auto-register with a generated name instead of blocking
const autoName = storedName || generateDefaultDeviceName();
try {
await register(autoName);
} catch {
setDevice({ localId, serverId: null, name: autoName });
setIsRegistered(false);
}
} catch {
setIsRegistered(false);
} finally {
setIsLoading(false);
}
}, [register]);
useEffect(() => {
checkRegistration();
}, [checkRegistration]);
return {
device,
isLoading,
isRegistered,
register,
checkRegistration,
};
}
-104
View File
@@ -1,104 +0,0 @@
import { useEffect, useState, useCallback } from 'react';
import { downloadAsync, documentDirectory, makeDirectoryAsync, getInfoAsync, deleteAsync } from 'expo-file-system/legacy';
import { useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import { FileItem, PaginatedResponse } from '../types';
const CACHE_DIR = `${documentDirectory}file-images/`;
function getCacheKey(name: string): string {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
}
return Math.abs(hash).toString(36);
}
function getExtension(name: string): string {
const dot = name.lastIndexOf('.');
return dot >= 0 ? name.slice(dot) : '.jpg';
}
export function useFileImage(url: string | undefined, fileName: string | undefined) {
const [localUri, setLocalUri] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const queryClient = useQueryClient();
const refreshUrl = useCallback(async (): Promise<string | null> => {
if (!fileName) return null;
try {
const res = await apiClient.getFileUrl(fileName);
const freshUrl = res.data.url;
queryClient.setQueriesData<PaginatedResponse<FileItem>>(
{ queryKey: ['files'] },
(old) => {
if (!old) return old;
return {
...old,
data: old.data.map((f) =>
f.name === fileName ? { ...f, url: freshUrl } : f
),
};
}
);
return freshUrl;
} catch {
return null;
}
}, [fileName, queryClient]);
const download = useCallback(async (downloadUrl: string, fileUri: string) => {
await makeDirectoryAsync(CACHE_DIR, { intermediates: true });
const result = await downloadAsync(downloadUrl, fileUri);
return result.uri;
}, []);
useEffect(() => {
if (!url || !fileName) return;
const resolvedUrl = url;
let cancelled = false;
const cacheKey = getCacheKey(fileName);
const ext = getExtension(fileName);
const fileUri = `${CACHE_DIR}${cacheKey}${ext}`;
async function load() {
const info = await getInfoAsync(fileUri);
if (info.exists) {
if (!cancelled) setLocalUri(info.uri);
return;
}
setLoading(true);
try {
const uri = await download(resolvedUrl, fileUri);
if (!cancelled) setLocalUri(uri);
} catch {
const freshUrl = await refreshUrl();
if (freshUrl && !cancelled) {
try {
await deleteAsync(fileUri, { idempotent: true });
const uri = await download(freshUrl, fileUri);
if (!cancelled) setLocalUri(uri);
} catch {
if (!cancelled) setLocalUri(null);
}
} else if (!cancelled) {
setLocalUri(null);
}
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => {
cancelled = true;
};
}, [url, fileName, download, refreshUrl]);
return { localUri, loading };
}
+33 -40
View File
@@ -8,7 +8,7 @@ function recordToUnifiedItem(record: ReturnType<typeof fileStore.getById>): Unif
if (!record) return null;
return {
id: record.id,
backendFileId: record.backendId ?? undefined,
backendResourceId: record.backendId ?? undefined,
name: record.name,
mimeType: record.mimeType,
size: record.size,
@@ -20,7 +20,8 @@ function recordToUnifiedItem(record: ReturnType<typeof fileStore.getById>): Unif
ocrText: record.ocrText ?? undefined,
tags: record.tags ?? [],
isFolder: record.isFolder === 1,
parentFileId: record.parentFileId ?? undefined,
parentResourceId: record.parentResourceId ?? undefined,
ownerId: record.ownerId ?? undefined,
thumbnailUrl: record.thumbnailUrl ?? undefined,
isDeviceFile: record.source === 'local' && !record.backendId,
};
@@ -38,15 +39,15 @@ function recordsToUnifiedItems(records: ReturnType<typeof fileStore.getPaginated
export function useFiles(parentId?: string | null, page: number = 1, limit: number = 50) {
const queryKey = parentId
? ['files', parentId]
: ['files', 'root', page, limit];
? ['resources', parentId]
: ['resources', 'root', page, limit];
return useQuery({
queryKey,
queryFn: async () => {
if (parentId) {
const backendRes = await apiClient.get<{ data: FileItem[] }>(
`/files/folders/${parentId}/files?thumbnail=thumbnail`,
`${ENDPOINTS.RESOURCES}/folders/${parentId}/resources?thumbnail=thumbnail_small`,
);
fileStore.mergeFromBackend(
backendRes.data.map((f) => ({
@@ -59,7 +60,8 @@ export function useFiles(parentId?: string | null, page: number = 1, limit: numb
ocrText: f.ocrText,
tags: f.tags,
isFolder: f.isFolder,
parentFileId: f.parentFileId,
parentResourceId: f.parentResourceId,
ownerId: f.ownerId,
thumbnailUrl: f.thumbnailUrl,
})),
);
@@ -72,7 +74,7 @@ export function useFiles(parentId?: string | null, page: number = 1, limit: numb
}
const backendRes = await apiClient.get<PaginatedResponse<FileItem>>(
`${ENDPOINTS.FILES}?page=${page}&limit=${limit}&thumbnail=thumbnail`,
`${ENDPOINTS.RESOURCES}?page=${page}&limit=${limit}&thumbnail=thumbnail_small`,
);
fileStore.mergeFromBackend(
backendRes.data.map((f) => ({
@@ -85,7 +87,8 @@ export function useFiles(parentId?: string | null, page: number = 1, limit: numb
ocrText: f.ocrText,
tags: f.tags,
isFolder: f.isFolder,
parentFileId: f.parentFileId,
parentResourceId: f.parentResourceId,
ownerId: f.ownerId,
thumbnailUrl: f.thumbnailUrl,
})),
);
@@ -110,34 +113,23 @@ export function useFiles(parentId?: string | null, page: number = 1, limit: numb
export function useFile(id: string) {
return useQuery({
queryKey: ['files', id],
queryFn: () => apiClient.get<FileItem>(`${ENDPOINTS.FILES}/${id}?thumbnail=thumbnail`),
queryKey: ['resources', id],
queryFn: () => apiClient.get<FileItem>(`${ENDPOINTS.RESOURCES}/${id}?thumbnail=thumbnail_small`),
enabled: !!id,
});
}
export function useFileImage(fileId: string) {
return useQuery({
queryKey: ['fileImage', fileId],
queryFn: () =>
apiClient.get<{ data: { id: string; name: string; url: string; size: number } }>(
`${ENDPOINTS.FILE}/${fileId}`,
),
enabled: !!fileId,
});
}
export function useDeleteFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const result = await apiClient.delete(`${ENDPOINTS.FILES}/${id}`);
const result = await apiClient.delete(`${ENDPOINTS.RESOURCES}/${id}`);
fileStore.deleteByBackendId(id);
return result;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] });
queryClient.invalidateQueries({ queryKey: ['resources'] });
},
});
}
@@ -146,22 +138,22 @@ export function useAddTags() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ fileId, tags, tagType }: { fileId: string; tags: string[]; tagType?: string }) =>
apiClient.post(`${ENDPOINTS.FILES}/${fileId}/tags`, { tags, tag_type: tagType }),
mutationFn: ({ fileId, tags }: { fileId: string; tags: string[] }) =>
apiClient.post(`${ENDPOINTS.RESOURCES}/${fileId}/tags`, { tags }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] });
queryClient.invalidateQueries({ queryKey: ['resources'] });
},
});
}
export function useMoveFiles() {
export function useMoveResources() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ fileIds, parentFileId }: { fileIds: string[]; parentFileId: string | null }) =>
apiClient.post(ENDPOINTS.MOVE, { file_ids: fileIds, parent_file_id: parentFileId }),
mutationFn: ({ resourceIds, parentResourceId }: { resourceIds: string[]; parentResourceId: string | null }) =>
apiClient.post(ENDPOINTS.MOVE, { resource_ids: resourceIds, parent_resource_id: parentResourceId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] });
queryClient.invalidateQueries({ queryKey: ['resources'] });
},
});
}
@@ -182,7 +174,8 @@ export function useFolders() {
ocrText: f.ocrText,
tags: f.tags,
isFolder: f.isFolder,
parentFileId: f.parentFileId,
parentResourceId: f.parentResourceId,
ownerId: f.ownerId,
thumbnailUrl: f.thumbnailUrl,
})),
);
@@ -209,10 +202,9 @@ export function useDownloadFile() {
return file.localUri ?? '';
}
const res = await apiClient.get<{ data: { url: string; name: string } }>(
`/files/${file.backendFileId}`,
const res = await apiClient.get<{ url: string }>(
`${ENDPOINTS.RESOURCES}/${file.backendResourceId ?? file.id}`,
);
const downloadUrl = res.data.url;
const { downloadAsync, documentDirectory, makeDirectoryAsync } = await import('expo-file-system/legacy');
const DOWNLOAD_DIR = `${documentDirectory}synced-files/`;
@@ -227,28 +219,29 @@ export function useDownloadFile() {
const ext = dot >= 0 ? file.name.slice(dot) : '';
const fileUri = `${DOWNLOAD_DIR}${cacheKey}${ext}`;
const result = await downloadAsync(downloadUrl, fileUri);
const result = await downloadAsync(res.url, fileUri);
fileStore.upsert({
id: file.backendFileId ?? file.id,
backendId: file.backendFileId ?? file.id,
id: file.backendResourceId ?? file.id,
backendId: file.backendResourceId ?? file.id,
name: file.name,
mimeType: file.mimeType,
size: file.size,
source: 'synced',
localUri: result.uri,
syncStatus: 'synced',
parentFileId: file.parentFileId ?? null,
parentResourceId: file.parentResourceId ?? null,
isFolder: 0,
ocrText: file.ocrText ?? null,
thumbnailUrl: file.thumbnailUrl ?? null,
ownerId: file.ownerId ?? null,
createdAt: file.createdAt,
updatedAt: file.updatedAt ?? file.createdAt,
lastSyncedAt: new Date().toISOString(),
tags: file.tags,
});
queryClient.invalidateQueries({ queryKey: ['files'] });
queryClient.invalidateQueries({ queryKey: ['resources'] });
return result.uri;
},
});
@@ -274,7 +267,7 @@ export function useFreeLocalSpace() {
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] });
queryClient.invalidateQueries({ queryKey: ['resources'] });
},
});
}
+3 -3
View File
@@ -32,7 +32,7 @@ export function useLocalFiles() {
for (const entry of registryEntries) {
merged.set(entry.id, {
id: entry.id,
backendFileId: entry.backendId ?? undefined,
backendResourceId: entry.backendId ?? undefined,
name: entry.name,
mimeType: entry.mimeType,
size: entry.size,
@@ -42,7 +42,7 @@ export function useLocalFiles() {
localUri: entry.localUri ?? undefined,
tags: entry.tags ?? [],
isFolder: entry.isFolder === 1,
parentFileId: entry.parentFileId ?? undefined,
parentResourceId: entry.parentResourceId ?? undefined,
isDeviceFile: entry.source === 'local' && !entry.backendId,
});
}
@@ -61,7 +61,7 @@ export function useLocalFiles() {
tags: [],
isFolder: false,
isDeviceFile: true,
parentFileId: df.folderId,
parentResourceId: df.folderId,
});
}
}
+23 -19
View File
@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { downloadAsync, documentDirectory, makeDirectoryAsync } from 'expo-file-system/legacy';
import { fileStore } from '../services/fileStore';
import { apiClient } from '../api/client';
import { ENDPOINTS } from '../constants/api';
import { setIsSyncing } from './useSyncQueue';
const SYNC_DIR = `${documentDirectory}synced-files/`;
@@ -25,9 +26,11 @@ export function usePullSync() {
size: number;
createdAt: string;
url?: string;
}> }>('/files?page=1&limit=100&thumbnail=thumbnail');
thumbnailUrl?: string;
ownerId?: string;
}> }>(`${ENDPOINTS.RESOURCES}?page=1&limit=100&thumbnail=thumbnail_small`);
const backendFiles = res.data ?? [];
const backendResources = res.data ?? [];
const registry = fileStore.getAllSynced();
const existingBackendIds = new Set(
registry.filter((e) => e.backendId).map((e) => e.backendId)
@@ -35,35 +38,36 @@ export function usePullSync() {
let pulled = 0;
for (const bf of backendFiles) {
if (existingBackendIds.has(bf.id)) continue;
if (bf.size === 0) continue;
for (const br of backendResources) {
if (existingBackendIds.has(br.id)) continue;
if (br.size === 0) continue;
try {
const detail = await apiClient.get<{ data: { url: string } }>(`/files/${bf.id}`);
const downloadUrl = detail.data.url;
const detail = await apiClient.get<{ url: string }>(`${ENDPOINTS.RESOURCES}/${br.id}`);
const downloadUrl = detail.url;
await makeDirectoryAsync(SYNC_DIR, { intermediates: true });
const safeName = bf.name.replace(/[^a-zA-Z0-9._-]/g, '_');
const fileUri = `${SYNC_DIR}${bf.id}_${safeName}`;
const safeName = br.name.replace(/[^a-zA-Z0-9._-]/g, '_');
const fileUri = `${SYNC_DIR}${br.id}_${safeName}`;
const result = await downloadAsync(downloadUrl, fileUri);
fileStore.upsert({
id: bf.id,
backendId: bf.id,
name: bf.name,
mimeType: bf.mimeType,
size: bf.size,
id: br.id,
backendId: br.id,
name: br.name,
mimeType: br.mimeType,
size: br.size,
source: 'synced',
localUri: result.uri,
syncStatus: 'synced',
parentFileId: null,
parentResourceId: null,
isFolder: 0,
ocrText: null,
thumbnailUrl: null,
createdAt: bf.createdAt,
updatedAt: bf.createdAt,
thumbnailUrl: br.thumbnailUrl ?? null,
ownerId: br.ownerId ?? null,
createdAt: br.createdAt,
updatedAt: br.createdAt,
lastSyncedAt: new Date().toISOString(),
});
pulled++;
@@ -73,7 +77,7 @@ export function usePullSync() {
}
if (pulled > 0) {
queryClient.invalidateQueries({ queryKey: ['files'] });
queryClient.invalidateQueries({ queryKey: ['resources'] });
}
return { pulled };
+41 -13
View File
@@ -1,15 +1,43 @@
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import { ENDPOINTS } from '../constants/api';
import { FileItem, PaginatedResponse } from '../types';
import { useMemo } from 'react';
import { fileStore } from '../services/fileStore';
import { UnifiedFileItem } from '../types';
export function useSearch(query: string, page: number = 1, limit: number = 20) {
return useQuery({
queryKey: ['search', query, page, limit],
queryFn: () =>
apiClient.get<PaginatedResponse<FileItem>>(
`${ENDPOINTS.FILES}/search?q=${encodeURIComponent(query)}&page=${page}&limit=${limit}`
),
enabled: query.length > 0,
});
function recordToUnifiedItem(record: ReturnType<typeof fileStore.getById>): UnifiedFileItem | null {
if (!record) return null;
return {
id: record.id,
backendResourceId: record.backendId ?? undefined,
name: record.name,
mimeType: record.mimeType,
size: record.size,
createdAt: record.createdAt,
updatedAt: record.updatedAt,
source: record.source as UnifiedFileItem['source'],
syncStatus: record.syncStatus as UnifiedFileItem['syncStatus'],
localUri: record.localUri ?? undefined,
ocrText: record.ocrText ?? undefined,
tags: record.tags ?? [],
isFolder: record.isFolder === 1,
parentResourceId: record.parentResourceId ?? undefined,
ownerId: record.ownerId ?? undefined,
thumbnailUrl: record.thumbnailUrl ?? undefined,
isDeviceFile: record.source === 'local' && !record.backendId,
};
}
export function useSearch(query: string) {
const results = useMemo(() => {
if (!query.trim()) return [];
const records = fileStore.searchFts(query);
if (records.length === 0) {
const fallback = fileStore.search(query);
return fallback.map((r) => recordToUnifiedItem(r)!).filter(Boolean);
}
return records.map((r) => recordToUnifiedItem(r)!).filter(Boolean);
}, [query]);
return {
data: results,
isLoading: false,
};
}
+47
View File
@@ -0,0 +1,47 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import { ShareEntry } from '../types';
function shareEndpoint(resourceId: string) {
return `/resources/${resourceId}/share`;
}
export function useShares(resourceId: string) {
return useQuery({
queryKey: ['shares', resourceId],
queryFn: () => apiClient.get<ShareEntry[]>(shareEndpoint(resourceId)),
enabled: !!resourceId,
});
}
export function useGrantShare() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ resourceId, subjectUserId, role }: { resourceId: string; subjectUserId: string; role: string }) =>
apiClient.post(shareEndpoint(resourceId), { subject_user_id: subjectUserId, role }),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['shares', variables.resourceId] });
},
});
}
export function useRevokeShare() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ resourceId, userId }: { resourceId: string; userId: string }) =>
apiClient.delete(`${shareEndpoint(resourceId)}/${userId}`),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['shares', variables.resourceId] });
},
});
}
export function useCheckAccess(resourceId: string) {
return useQuery({
queryKey: ['access', resourceId],
queryFn: () => apiClient.get<{ role: string; access: boolean }>(`/resources/${resourceId}/access`),
enabled: !!resourceId,
});
}
+23
View File
@@ -0,0 +1,23 @@
import { useCallback, useRef } from 'react';
import { apiClient } from '../api/client';
import { ENDPOINTS } from '../constants/api';
import { SyncQueueItem } from '../types';
export function useSyncPull() {
const isRunning = useRef(false);
const pull = useCallback(async (locationId?: string) => {
if (isRunning.current) return { items: [] };
isRunning.current = true;
try {
const body = locationId ? { location_id: locationId } : {};
const result = await apiClient.post<SyncQueueItem[]>(ENDPOINTS.SYNC_PULL, body);
return { items: result };
} finally {
isRunning.current = false;
}
}, []);
return { pull };
}
+23
View File
@@ -0,0 +1,23 @@
import { useCallback, useRef } from 'react';
import { apiClient } from '../api/client';
import { ENDPOINTS } from '../constants/api';
export function useSyncPush() {
const isRunning = useRef(false);
const push = useCallback(async (locationId: string) => {
if (isRunning.current) return { pending: 0 };
isRunning.current = true;
try {
const result = await apiClient.post<{ pending: number; message: string }>(ENDPOINTS.SYNC_PUSH, {
location_id: locationId,
});
return { pending: result.pending };
} finally {
isRunning.current = false;
}
}, []);
return { push };
}
+2 -2
View File
@@ -36,8 +36,8 @@ export function useSyncQueue() {
count++;
} else {
// mode manuel : uniquement les fichiers des dossiers en mode auto
if (!entry.parentFileId) continue;
const folder = safDirectory.getAll().find((f) => f.id === entry.parentFileId);
if (!entry.parentResourceId) continue;
const folder = safDirectory.getAll().find((f) => f.id === entry.parentResourceId);
if (folder && folder.syncMode === 'auto') {
count++;
}
+1 -1
View File
@@ -71,7 +71,7 @@ export function useUpload() {
return { uploaded, errors };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] });
queryClient.invalidateQueries({ queryKey: ['resources'] });
},
});
}