Skip to content
Merged
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
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
],
"devDependencies": {
"@babel/core": "^7.23.3",
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@babel/preset-env": "^7.23.3",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
Expand All @@ -80,7 +81,6 @@
"autoprefixer": "^10.4.16",
"babel-loader": "^9.1.3",
"husky": "^8.0.3",
"pako": "^2.1.0",
"postcss": "^8.4.31",
"prettier": "3.0.3",
"prettier-plugin-organize-imports": "^3.2.4",
Expand All @@ -104,14 +104,18 @@
"@aws-sdk/xhr-http-handler": "^3.451.0",
"@azure/msal-browser": "^3.5.0",
"@emotion/styled": "^11.11.0",
"@microsoft/microsoft-graph-client": "^3.0.7",
"@mui/material": "^5.14.18",
"@types/gapi": "^0.0.47",
"authProviders": "link:@microsoft/microsoft-graph-client/authProviders",
"babel-plugin-module-resolver": "^5.0.0",
"framer-motion": "^10.16.5",
"load-script": "^2.0.0",
"pako": "^2.1.0",
"react-file-viewer": "^1.2.1",
"react-icons": "^4.12.0",
"react-webcam": "^7.2.0",
"use-debounce": "^10.0.4",
"uuid": "^9.0.1"
},
"peerDependencies": {
Expand Down
11,562 changes: 5,751 additions & 5,811 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/components/OneDriveUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ const OneDriveUploader: FC<Props> = ({
setView,
loader,
}: Props) => {
const { user, oneDriveFiles, downloadFile, signOut } = useOneDrive(
oneDriveConfigs.onedrive_client_id,
)
const { user, oneDriveFiles, downloadFile, signOut, graphClient } =
useOneDrive(oneDriveConfigs.onedrive_client_id)

if (!oneDriveFiles) return loader || <div>Loading...</div>

Expand All @@ -37,6 +36,7 @@ const OneDriveUploader: FC<Props> = ({
setFiles={setFiles}
setView={setView}
downloadFile={downloadFile}
graphClient={graphClient}
/>
)
}
Expand Down
257 changes: 225 additions & 32 deletions src/components/UpupUploader/FileBrowser/BrowserOD.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import ListItem from './LiOD'

import { Client } from '@microsoft/microsoft-graph-client'
import ButtonSpinner from 'components/ButtonSpinner'
import { AnimatePresence, motion } from 'framer-motion'
import { MicrosoftUser, OneDriveFile, OneDriveRoot } from 'microsoft'
import { TbSearch } from 'react-icons/tb'
import { useDebouncedCallback } from 'use-debounce'

type Props = {
driveFiles?: OneDriveRoot | undefined
Expand All @@ -14,59 +16,240 @@ type Props = {
setFiles: Dispatch<SetStateAction<File[]>>
setView: (view: string) => void
accept?: string
graphClient: Client | null
}

const FileBrowser = ({
handleSignOut,
user,
driveFiles,
setFiles,
downloadFile,
setView,
accept,
graphClient,
}: Props) => {
const [path, setPath] = useState<OneDriveRoot[]>([])
const [selectedFiles, setSelectedFiles] = useState<OneDriveFile[]>([])
const [showLoader, setLoader] = useState(false)
const handleClick = (file: OneDriveFile | OneDriveRoot) => {
if (file.children!.length) {
setPath(prevPath => [...prevPath, file as OneDriveRoot])
const [downloadProgress, setDownloadProgress] = useState<number>(0)

const handleClick = useDebouncedCallback(async (file: OneDriveFile) => {
if (!graphClient) {
console.error('Graph client not initialized')
return
}

if (file.isFolder) {
const fetchFolderContents = async () => {
try {
const response = await graphClient
.api(`/me/drive/items/${file.id}/children`)
.select(
'id,name,folder,file,thumbnails,@microsoft.graph.downloadUrl',
)
.expand('thumbnails')
.get()

const files = response.value.map((item: any) => ({
id: item.id,
name: item.name,
isFolder: !!item.folder,
children: item.folder ? [] : undefined,
thumbnails: item.thumbnails?.[0] || null,
'@microsoft.graph.downloadUrl':
item['@microsoft.graph.downloadUrl'],
file: item.file,
}))

setPath(prevPath => [
...prevPath,
{ ...file, children: files },
])
} catch (error) {
console.error('Error fetching folder contents:', error)
}
}
fetchFolderContents()
} else {
setSelectedFiles(prevFiles =>
prevFiles.includes(file)
? prevFiles.filter(f => f.id !== file.id)
: [...prevFiles, file],
)
try {
const fileInfo = await graphClient
.api(`/me/drive/items/${file.id}`)
.select(
'id,name,file,thumbnails,@microsoft.graph.downloadUrl',
)
.get()

if (!fileInfo['@microsoft.graph.downloadUrl']) {
const permission = await graphClient
.api(`/me/drive/items/${file.id}/createLink`)
.post({
type: 'view',
scope: 'anonymous',
})

// Convert the sharing URL to a direct download URL
const shareUrl = permission.link.webUrl
const downloadUrl = shareUrl.replace('redir?', 'download?')
fileInfo['@microsoft.graph.downloadUrl'] = downloadUrl
}

const updatedFile: OneDriveFile = {
...file,
'@microsoft.graph.downloadUrl':
fileInfo['@microsoft.graph.downloadUrl'],
thumbnails: fileInfo.thumbnails?.[0] || null,
file: fileInfo.file,
}

setSelectedFiles(prevFiles => {
const newFiles = prevFiles.includes(file)
? prevFiles.filter(f => f.id !== file.id)
: [...prevFiles, updatedFile]
return newFiles
})
} catch (error) {
console.error(
`Error fetching file information for ${file.name}:`,
error,
)
}
}
}, 500)

const getDownloadUrl = async (file: OneDriveFile, graphClient: Client) => {
// First try to get direct download URL
const fileInfo = await graphClient
.api(`/me/drive/items/${file.id}`)
.select('@microsoft.graph.downloadUrl')
.get()

if (fileInfo['@microsoft.graph.downloadUrl']) {
return fileInfo['@microsoft.graph.downloadUrl']
}

// Fallback to creating a sharing link
const permission = await graphClient
.api(`/me/drive/items/${file.id}/createLink`)
.post({
type: 'view',
scope: 'anonymous',
})

const shareId = permission.link.webUrl.split('/s!/')[1]
const sharedItem = await graphClient
.api(`/shares/u!${shareId}`)
.expand('driveItem')
.get()

return sharedItem.driveItem['@microsoft.graph.downloadUrl']
}

const downloadFile = async (
url: string,
fileName: string,
mimeType = 'application/octet-stream',
) => {
const response = await fetch(url, {
method: 'GET',
})

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}

const blob = await response.blob()
return new File([blob], fileName, {
type: mimeType || 'application/octet-stream',
})
}

const downloadFiles = async (files: OneDriveFile[]) => {
const promises = files.map(async file => {
const data = await downloadFile(
file['@microsoft.graph.downloadUrl']!,
)
const downloadedFile = new File([data], file.name, {
type: file.file?.mimeType,
}) as unknown as OneDriveFile

// @ts-ignore - Fix this by refactoring how file browser works
downloadedFile['thumbnailLink'] = file.thumbnails?.large?.url

return downloadedFile
const promises = files.map(async (file, index) => {
try {
const downloadUrl = await getDownloadUrl(file, graphClient!)
if (!downloadUrl) {
throw new Error('Could not get download URL')
}

const downloadedFile = await downloadFile(
downloadUrl,
file.name,
file.file?.mimeType,
)

// Add thumbnail if available
if (file.thumbnails?.large?.url) {
Object.defineProperty(downloadedFile, 'thumbnailLink', {
value: file.thumbnails.large.url,
writable: true,
enumerable: true,
configurable: true,
})
}

// Update progress
setDownloadProgress(
Math.round(((index + 1) / files.length) * 100),
)

return downloadedFile
} catch (error) {
console.error(`Error downloading file ${file.name}:`, error)
throw new Error(
`Failed to download ${file.name}: ${
(error as Error).message
}`,
)
}
})

return await Promise.all(promises)
try {
return await Promise.all(promises)
} catch (error) {
console.error('Error in downloadFiles:', error)
throw error
}
}

const handleSubmit = async () => {
if (selectedFiles.length === 0) return

setLoader(true)
const downloadedFiles = await downloadFiles(selectedFiles)
setFiles(prevFiles => [
...prevFiles,
...(downloadedFiles as unknown as File[]),
])
setView('internal')
setLoader(false)
setDownloadProgress(0)

try {
// Process one file at a time
const downloadedFiles: File[] = []

for (let i = 0; i < selectedFiles.length; i++) {
const file = selectedFiles[i]
const [downloadedFile] = await downloadFiles([file])
downloadedFiles.push(downloadedFile)

// Update progress
const progress = Math.round(
((i + 1) / selectedFiles.length) * 100,
)
setDownloadProgress(progress)

// Add a small delay between files
await new Promise(resolve => setTimeout(resolve, 500))
}

// Update the files state
setFiles(prevFiles => [...prevFiles, ...downloadedFiles])

// Clear selection and return to internal view
setSelectedFiles([])
setDownloadProgress(0)
setView('internal')
} catch (error) {
console.error('Error processing files:', error)
alert(`Error downloading files: ${(error as Error).message}`)
} finally {
setLoader(false)
setDownloadProgress(0)
}
}

useEffect(() => {
Expand Down Expand Up @@ -156,18 +339,28 @@ const FileBrowser = ({
transition={{ duration: 0.2 }}
className="flex origin-bottom items-center justify-start gap-4 border-t bg-white p-4 py-2 dark:bg-[#1f1f1f] dark:text-[#fafafa]"
>
{!showLoader && (
{!showLoader ? (
<button
className="w-32 rounded-md bg-blue-500 p-3 font-medium text-white transition-all duration-300 hover:bg-blue-600 active:bg-blue-700"
onClick={handleSubmit}
>
Add {selectedFiles.length} files
</button>
) : (
<div className="flex items-center gap-4">
<ButtonSpinner />
<span className="text-sm">
Downloading... {downloadProgress}%
</span>
</div>
)}
{showLoader && <ButtonSpinner />}
<button
className="hover:underline"
onClick={() => setSelectedFiles([])}
onClick={() => {
setSelectedFiles([])
setDownloadProgress(0)
}}
disabled={showLoader}
>
Cancel
</button>
Expand Down
6 changes: 4 additions & 2 deletions src/components/UpupUploader/FileBrowser/LiOD.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ const ListItem = ({
selectedFiles,
accept,
}: Props) => {
const isFolder = file.children!.length
const isFolder = file.isFolder
const isFileSelected = selectedFiles.includes(file)
const isFileAccepted =
accept && accept !== '*' && accept.includes(file.name.split('.').pop()!)
accept && accept !== '*' && !isFolder
? accept.includes(file.name.split('.').pop()!)
: true

if (accept && !isFolder && !isFileAccepted) return null

Expand Down
Loading