Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/hooks/useFileOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,25 @@ export const useFileOperations = () => {
}
}, []);

const downloadFile = useCallback(async (filename) => {
setIsLoading(true);
try {
const res = await fetch(`/api/files/download?file=${encodeURIComponent(filename)}`);
if (res.ok) {
return await res.text();
}
throw new Error('Failed to download file');
} finally {
setIsLoading(false);
}
}, []);

return {
createFile,
updateFile,
deleteFile,
getFiles,
downloadFile,
isLoading,
};
};
60 changes: 60 additions & 0 deletions src/pages/api/files/[id].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// src/pages/api/files/[id].js
import dbConnect from '@/lib/mongodb';
import { verifyToken } from '@/lib/auth';
import File from '@/models/File';

export default async function handler(req, res) {
await dbConnect();

if (req.method !== 'DELETE') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
const user = await verifyToken(req);
const { id } = req.query;

if (!id) {
return res.status(400).json({ error: 'File ID is required' });
}

// Find the file
const file = await File.findById(id);
if (!file) {
return res.status(404).json({ error: 'File not found' });
}

// Check if user is the owner
if (file.owner.toString() === user._id.toString()) {
// User is owner - delete the file completely
await File.findByIdAndDelete(id);
return res.status(200).json({
message: 'File deleted successfully',
deletedFile: file
});
}

// Check if user is a collaborator
const collaboratorIndex = file.collaborators.findIndex(
collab => collab.user.toString() === user._id.toString()
);

if (collaboratorIndex !== -1) {
// User is collaborator - remove them from collaborators (unshare)
file.collaborators.splice(collaboratorIndex, 1);
await file.save();

return res.status(200).json({
message: 'File unshared successfully',
unsharedFile: file
});
}

// User has no access to the file
return res.status(403).json({ error: 'Access denied' });

} catch (error) {
console.error('Delete error:', error);
res.status(500).json({ error: 'Failed to delete file' });
}
}
18 changes: 15 additions & 3 deletions src/pages/api/files/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,21 @@ export default async function handler(req, res) {
}

if (req.method === 'GET') {
const files = Array.from(fileDatabase.values());
console.log('Getting files, database size:', fileDatabase.size);
res.status(200).json(files);
const { id } = req.query;

if (id) {
// Get specific file by ID
const file = fileDatabase.get(id);
if (!file) {
return res.status(404).json({ error: 'File not found' });
}
res.status(200).json(file);
} else {
// Get all files
const files = Array.from(fileDatabase.values());
console.log('Getting files, database size:', fileDatabase.size);
res.status(200).json(files);
}
} else if (req.method === 'POST') {
const { name, content, type } = req.body;
const fileId = `file_${Date.now()}`;
Expand Down
71 changes: 71 additions & 0 deletions src/pages/api/files/download.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// src/pages/api/files/download.js
import dbConnect from '@/lib/mongodb';
import { verifyToken } from '@/lib/auth';

// Simple in-memory storage for files (replace with your actual database logic)
let fileStorage = new Map();

export default async function handler(req, res) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
await dbConnect();
const user = await verifyToken(req);
const { file: filename } = req.query;

if (!filename) {
return res.status(400).json({ error: 'Filename is required' });
}

// Get files from the database endpoint
const dbResponse = await fetch(`${getBaseUrl(req)}/api/files/database`, {
headers: {
'Cookie': req.headers.cookie || ''
}
});

if (!dbResponse.ok) {
throw new Error('Failed to fetch files from database');
}

const files = await dbResponse.json();
const file = files.find(f => f.name === filename);

if (!file) {
return res.status(404).json({ error: 'File not found' });
}

// Set headers for file download
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);

// Send file content
res.status(200).send(file.content || '');
} catch (error) {
console.error('Download error:', error);

// Fallback: try to get file from localStorage data
try {
const files = JSON.parse(localStorage.getItem('orbitos-files') || '[]');
const file = files.find(f => f.name === filename);

if (file) {
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
return res.status(200).send(file.content || '');
}
} catch (fallbackError) {
console.error('Fallback download also failed:', fallbackError);
}

res.status(500).json({ error: 'Failed to download file' });
}
}

function getBaseUrl(req) {
const host = req.headers.host;
const protocol = req.headers['x-forwarded-proto'] || 'http';
return `${protocol}://${host}`;
}
3 changes: 2 additions & 1 deletion src/pages/api/files/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export default async function handler(req, res) {
try {
const files = await File.find({
$or: [{ owner: user._id }, { 'collaborators.user': user._id }],
}).populate('owner', 'username');
}).populate('owner', 'username _id'); // Include _id in populate

res.json({ files });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch files' });
Expand Down
93 changes: 79 additions & 14 deletions src/pages/apps/filemanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import React, { useState, useEffect } from 'react';
import { useTheme } from '@/context/ThemeContext';
import { useDrive } from '@/context/DriveContext';
import { useAuth } from '@/context/AuthContext';

const FileItem = ({ item, source, theme, onDownload, onShare, onDelete }) => {
const FileItem = ({ item, source, theme, onDownload, onShare, onDelete, currentUserId }) => {
const isGdrive = source === 'gdrive';
const icon = isGdrive ? (
<img src={item.iconLink} alt="" className="w-5 h-5" />
Expand All @@ -17,6 +18,9 @@ const FileItem = ({ item, source, theme, onDownload, onShare, onDelete }) => {
// Get the correct file ID - use item.id for Google Drive, item._id for local files
const fileId = item.id || item._id;

// Check if current user is the owner (for local files)
const isOwner = !isGdrive && item.owner && item.owner._id === currentUserId;

return (
<li
className={`flex justify-between items-center p-2 rounded ${theme.app.button_subtle_hover}`}
Expand All @@ -29,6 +33,9 @@ const FileItem = ({ item, source, theme, onDownload, onShare, onDelete }) => {
>
{item.name}
{isDirectory ? '/' : ''}
{!isGdrive && !isOwner && (
<span className="text-xs text-gray-500 ml-2">(Shared)</span>
)}
</span>
</div>
<div className="flex gap-2">
Expand Down Expand Up @@ -59,11 +66,15 @@ const FileItem = ({ item, source, theme, onDownload, onShare, onDelete }) => {
Share
</button>
<button
onClick={() => onDelete(fileId)} // Also fix delete
className="px-2 py-1 text-sm rounded bg-red-500 text-white hover:bg-red-600"
title="Delete File"
onClick={() => onDelete(fileId, item.name, isOwner)} // Also fixed delete
className={`px-2 py-1 text-sm rounded ${
isOwner
? 'bg-red-500 hover:bg-red-600 text-white'
: 'bg-orange-500 hover:bg-orange-600 text-white'
}`}
title={isOwner ? 'Delete File' : 'Unshare File'}
>
Delete
{isOwner ? 'Delete' : 'Unshare'}
</button>
</>
)
Expand All @@ -81,6 +92,7 @@ const StatusMessage = ({ children }) => (

export default function FileManagerApp() {
const { theme } = useTheme();
const { user: currentUser } = useAuth(); // Get current user from auth context
const [activeSource, setActiveSource] = useState('gdrive');
const [localItems, setLocalItems] = useState([]);
const [isLoadingLocal, setIsLoadingLocal] = useState(true);
Expand Down Expand Up @@ -116,7 +128,8 @@ export default function FileManagerApp() {
const data = await res.json();
setLocalItems(data.files || []);
} catch (error) {
setError('Failed to load files.');
console.error('Failed to load files:', error);
setError('Failed to load files from server.');
setLocalItems([]);
} finally {
setIsLoadingLocal(false);
Expand Down Expand Up @@ -151,9 +164,42 @@ export default function FileManagerApp() {
}
};

const handleDownload = (filename) => {
const url = `/api/files/download?file=${encodeURIComponent(filename)}`;
window.open(url, '_blank');
const handleDownload = async (filename) => {
try {
// Use the same method as Notes app - fetch from /api/files
const response = await fetch('/api/files');

if (!response.ok) {
throw new Error(`Download failed: ${response.status}`);
}

const data = await response.json();
const files = data.files || [];
const file = files.find(f => f.name === filename);

if (!file) {
throw new Error(`File "${filename}" not found`);
}

const content = file.content || '';

// Create download
const blob = new Blob([content], { type: 'text/plain' });
const downloadUrl = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(downloadUrl);

showNotification(`Downloaded "${filename}" successfully`, 'success');
} catch (error) {
console.error('Download failed:', error);
showNotification(`Download failed: ${error.message}`, 'error');
}
};

const handleShareFile = async (fileId, userEmail, permission) => {
Expand All @@ -179,16 +225,34 @@ export default function FileManagerApp() {
showNotification(error.message, 'error');
}
};

const handleDelete = async (fileId, filename, isOwner) => {
if (!confirm(`Are you sure you want to ${isOwner ? 'delete' : 'unshare'} "${filename}"?`)) {
return;
}

const handleDelete = async (fileId) => {
try {
const res = await fetch(`/api/files/${fileId}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Delete failed');
const response = await fetch(`/api/files/${fileId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Failed to ${isOwner ? 'delete' : 'unshare'} file`);
}

const result = await response.json();

setLocalItems(prev => prev.filter(item => (item.id !== fileId && item._id !== fileId)));

showNotification(result.message, 'success');
await fetchLocalItems();
showNotification('File deleted successfully', 'success');
} catch (error) {
console.error('Delete file failed:', error);
showNotification('File deletion failed', 'error');
showNotification(error.message, 'error');
}
};

Expand Down Expand Up @@ -265,6 +329,7 @@ export default function FileManagerApp() {
onDownload={handleDownload}
onShare={handleShareClick}
onDelete={handleDelete}
currentUserId={currentUser?.id}
/>
))}
</ul>
Expand Down