delete files etc ..
This commit is contained in:
@@ -74,7 +74,7 @@ CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
|
||||
CREATE TABLE resource_tags (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tag_id UUID NOT NULL REFERENCES tags(id),
|
||||
resource_id UUID NOT NULL REFERENCES resources(id),
|
||||
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
||||
UNIQUE(tag_id, resource_id)
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE resource_tags DROP CONSTRAINT resource_tags_resource_id_fkey,
|
||||
ADD CONSTRAINT resource_tags_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES resources(id);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE resource_tags DROP CONSTRAINT resource_tags_resource_id_fkey,
|
||||
ADD CONSTRAINT resource_tags_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE;
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -248,10 +249,15 @@ func (h *ResourceHandler) Get(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *ResourceHandler) Delete(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
id := c.Param("id")
|
||||
|
||||
result, err := h.resources.DeleteRecursive(id)
|
||||
result, err := h.resources.DeleteRecursive(id, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrForbidden) {
|
||||
api.Error(c, http.StatusForbidden, "FORBIDDEN", "You do not own this resource")
|
||||
return
|
||||
}
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to delete resource")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"github.com/vaultdrop/backend/internal/model"
|
||||
)
|
||||
|
||||
var ErrForbidden = errors.New("forbidden")
|
||||
|
||||
type ResourceService struct {
|
||||
db *sql.DB
|
||||
queries *db.Queries
|
||||
@@ -200,14 +202,22 @@ func (s *ResourceService) Delete(id string) error {
|
||||
return s.queries.DeleteResource(context.Background(), resourceUUID)
|
||||
}
|
||||
|
||||
func (s *ResourceService) DeleteRecursive(id string) (*DeleteResult, error) {
|
||||
func (s *ResourceService) DeleteRecursive(id, userID string) (*DeleteResult, error) {
|
||||
resourceUUID, _ := uuid.Parse(id)
|
||||
ownerUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return nil, ErrForbidden
|
||||
}
|
||||
|
||||
r, err := s.queries.GetResource(context.Background(), resourceUUID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get resource: %w", err)
|
||||
}
|
||||
|
||||
if r.OwnerID != ownerUUID {
|
||||
return nil, ErrForbidden
|
||||
}
|
||||
|
||||
result := &DeleteResult{}
|
||||
|
||||
if r.IsFolder {
|
||||
@@ -217,7 +227,7 @@ func (s *ResourceService) DeleteRecursive(id string) (*DeleteResult, error) {
|
||||
}
|
||||
|
||||
for _, child := range children {
|
||||
childResult, err := s.DeleteRecursive(child.ID.String())
|
||||
childResult, err := s.DeleteRecursive(child.ID.String(), userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("delete child %s: %w", child.ID, err)
|
||||
}
|
||||
|
||||
+39
-6
@@ -16,6 +16,7 @@ import { ConfirmModal, type ConfirmOption } from '../components/ConfirmModal';
|
||||
import { UploadModal } from '../components/UploadModal';
|
||||
import { SyncStatusIcon } from '../components/SyncStatusIcon';
|
||||
import { useSyncQueue } from '../hooks/useSyncQueue';
|
||||
import { useUploadQueue } from '../hooks/useUploadQueue';
|
||||
import { useAutoSync } from '../hooks/useAutoSync';
|
||||
import { safDirectory, SyncMode, SyncGlobalMode } from '../services/safDirectory';
|
||||
import { fileStore } from '../services/fileStore';
|
||||
@@ -67,6 +68,8 @@ const FileGridItem = React.memo(function FileGridItem({ file, size, onPress, onL
|
||||
size={size}
|
||||
syncStatus={file.syncStatus}
|
||||
isFolder={file.isFolder}
|
||||
isUploading={file.isUploading}
|
||||
uploadProgress={file.uploadProgress}
|
||||
/>
|
||||
{selected && (
|
||||
<View style={styles.selectedOverlay}>
|
||||
@@ -123,6 +126,7 @@ export function HomeScreen() {
|
||||
const [confirmDeleteState, setConfirmDeleteState] = useState<{ message: string; options: ConfirmOption[] } | null>(null);
|
||||
const [removeFolderConfirmId, setRemoveFolderConfirmId] = useState<string | null>(null);
|
||||
const { pendingCount, isSyncing } = useSyncQueue();
|
||||
const { tasks: uploadTasks } = useUploadQueue();
|
||||
useAutoSync();
|
||||
|
||||
const pinchScale = useSharedValue(1);
|
||||
@@ -159,6 +163,8 @@ export function HomeScreen() {
|
||||
<SyncStatusIcon
|
||||
isSyncing={isSyncing}
|
||||
pendingCount={pendingCount}
|
||||
isUploading={uploadTasks.some(t => t.status === 'uploading')}
|
||||
uploadPendingCount={uploadTasks.filter(t => t.status === 'pending' || t.status === 'uploading').length}
|
||||
onPress={() => navigation.navigate('SyncDetail')}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setUploadModalVisible(true)} style={{ padding: 8 }}>
|
||||
@@ -170,7 +176,8 @@ export function HomeScreen() {
|
||||
</View>
|
||||
),
|
||||
});
|
||||
}, [navigation, pendingCount, isSyncing]);
|
||||
}, [navigation, pendingCount, isSyncing, uploadTasks]);
|
||||
|
||||
|
||||
const selectionMode = selectedIds.size > 0;
|
||||
|
||||
@@ -191,14 +198,40 @@ export function HomeScreen() {
|
||||
});
|
||||
}, [filteredFiles, mediaFilter]);
|
||||
|
||||
const sortedFiles = useMemo(() =>
|
||||
[...mediaFilteredFiles].sort((a, b) => {
|
||||
const uploadGhostItems = useMemo(() => {
|
||||
return uploadTasks
|
||||
.filter((t) => t.status === 'pending' || t.status === 'uploading')
|
||||
.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.file.name,
|
||||
mimeType: t.file.type,
|
||||
size: 0,
|
||||
createdAt: new Date(t.createdAt).toISOString(),
|
||||
source: 'local' as const,
|
||||
syncStatus: 'local' as const,
|
||||
localUri: t.file.uri,
|
||||
tags: [],
|
||||
isFolder: false,
|
||||
isDeviceFile: false,
|
||||
isUploading: true,
|
||||
uploadProgress: t.progress,
|
||||
uploadStatus: t.status,
|
||||
}));
|
||||
}, [uploadTasks]);
|
||||
|
||||
const sortedFiles = useMemo(() => {
|
||||
const uploadedExistingIds = new Set(
|
||||
mediaFilteredFiles.map((f) => f.localUri).filter(Boolean)
|
||||
);
|
||||
const ghosts = uploadGhostItems.filter(
|
||||
(g) => g.localUri && !uploadedExistingIds.has(g.localUri)
|
||||
);
|
||||
return [...ghosts, ...mediaFilteredFiles].sort((a, b) => {
|
||||
const da = parseBackendDate(a.createdAt);
|
||||
const db = parseBackendDate(b.createdAt);
|
||||
return (db?.getTime() ?? 0) - (da?.getTime() ?? 0);
|
||||
}),
|
||||
[mediaFilteredFiles]
|
||||
);
|
||||
});
|
||||
}, [mediaFilteredFiles, uploadGhostItems]);
|
||||
|
||||
const fileIdToIndex = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
|
||||
@@ -40,9 +40,11 @@ interface FileThumbnailProps {
|
||||
isLoading?: boolean;
|
||||
syncStatus?: SyncStatus;
|
||||
isFolder?: boolean;
|
||||
isUploading?: boolean;
|
||||
uploadProgress?: number;
|
||||
}
|
||||
|
||||
export const FileThumbnail = React.memo(function FileThumbnail({ uri, thumbnailUrl, mimeType, fileName, size, isLoading, syncStatus, isFolder }: FileThumbnailProps) {
|
||||
export const FileThumbnail = React.memo(function FileThumbnail({ uri, thumbnailUrl, mimeType, fileName, size, isLoading, syncStatus, isFolder, isUploading, uploadProgress }: FileThumbnailProps) {
|
||||
const info = getFileInfo(mimeType, fileName);
|
||||
const ext = getExtension(fileName);
|
||||
|
||||
@@ -75,16 +77,33 @@ export const FileThumbnail = React.memo(function FileThumbnail({ uri, thumbnailU
|
||||
cachePolicy="memory-disk"
|
||||
/>
|
||||
{syncStatus && <SyncStatusBadge status={syncStatus} />}
|
||||
{isUploading && (
|
||||
<View style={styles.uploadOverlay}>
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
<Text style={styles.uploadProgressText}>{uploadProgress ?? 0}%</Text>
|
||||
<View style={[styles.uploadProgressBar, { width: `${uploadProgress ?? 0}%` }]} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { width: size, height: size, backgroundColor: info.bg }]}>
|
||||
<View style={[styles.container, { width: size, height: size, backgroundColor: isUploading ? '#E3F2FD' : info.bg }]}>
|
||||
{isUploading ? (
|
||||
<View style={styles.uploadGhost}>
|
||||
<MaterialIcons name="cloud-upload" size={size * 0.3} color="#1976D2" />
|
||||
<ActivityIndicator size="small" color="#1976D2" />
|
||||
<Text style={styles.uploadPercent}>{uploadProgress ?? 0}%</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<MaterialIcons name={info.icon} size={size * 0.35} color={info.color} />
|
||||
{ext.length <= 4 && (
|
||||
<Text style={[styles.ext, { color: info.color }]}>{ext}</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{syncStatus && <SyncStatusBadge status={syncStatus} />}
|
||||
</View>
|
||||
);
|
||||
@@ -104,4 +123,34 @@ const styles = StyleSheet.create({
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
},
|
||||
uploadGhost: {
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
uploadPercent: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
color: '#1976D2',
|
||||
},
|
||||
uploadOverlay: {
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: 'rgba(0,0,0,0.45)',
|
||||
borderRadius: 6,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
uploadProgressText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
uploadProgressBar: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
height: 3,
|
||||
backgroundColor: '#1976D2',
|
||||
borderBottomLeftRadius: 6,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,14 +14,18 @@ import Animated, {
|
||||
interface SyncStatusIconProps {
|
||||
isSyncing: boolean;
|
||||
pendingCount: number;
|
||||
isUploading: boolean;
|
||||
uploadPendingCount: number;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export function SyncStatusIcon({ isSyncing, pendingCount, onPress }: SyncStatusIconProps) {
|
||||
export function SyncStatusIcon({ isSyncing, pendingCount, isUploading, uploadPendingCount, onPress }: SyncStatusIconProps) {
|
||||
const rotation = useSharedValue(0);
|
||||
const isActive = isSyncing || isUploading;
|
||||
const totalPending = pendingCount + uploadPendingCount;
|
||||
|
||||
useEffect(() => {
|
||||
if (isSyncing) {
|
||||
if (isActive) {
|
||||
rotation.value = withRepeat(
|
||||
withSequence(
|
||||
withTiming(360, { duration: 1000, easing: Easing.linear }),
|
||||
@@ -33,7 +37,7 @@ export function SyncStatusIcon({ isSyncing, pendingCount, onPress }: SyncStatusI
|
||||
cancelAnimation(rotation);
|
||||
rotation.value = withTiming(0, { duration: 200 });
|
||||
}
|
||||
}, [isSyncing, rotation]);
|
||||
}, [isActive, rotation]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ rotate: `${rotation.value}deg` }],
|
||||
@@ -45,13 +49,13 @@ export function SyncStatusIcon({ isSyncing, pendingCount, onPress }: SyncStatusI
|
||||
<MaterialIcons
|
||||
name="sync"
|
||||
size={22}
|
||||
color={isSyncing ? '#1976D2' : pendingCount > 0 ? '#F57C00' : '#666'}
|
||||
color={isActive ? '#1976D2' : totalPending > 0 ? '#F57C00' : '#666'}
|
||||
/>
|
||||
</Animated.View>
|
||||
{pendingCount > 0 && !isSyncing && (
|
||||
{totalPending > 0 && !isActive && (
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>
|
||||
{pendingCount > 99 ? '99+' : pendingCount}
|
||||
{totalPending > 99 ? '99+' : totalPending}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -236,6 +236,12 @@ class UploadQueue {
|
||||
fieldName: 'file',
|
||||
mimeType: task.file.type,
|
||||
headers,
|
||||
onProgress: (progress) => {
|
||||
if (progress.totalBytes > 0) {
|
||||
task.progress = Math.round((progress.bytesSent / progress.totalBytes) * 100);
|
||||
this.notify();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status >= 400) {
|
||||
|
||||
@@ -28,6 +28,9 @@ export interface UnifiedFileItem {
|
||||
thumbnailUrl?: string;
|
||||
variants?: Variant[];
|
||||
isDeviceFile?: boolean;
|
||||
isUploading?: boolean;
|
||||
uploadProgress?: number;
|
||||
uploadStatus?: 'pending' | 'uploading' | 'done' | 'error';
|
||||
}
|
||||
|
||||
export type FileItem = UnifiedFileItem;
|
||||
|
||||
Reference in New Issue
Block a user