mirror of
https://github.com/Nezumi-2711/onedrive-vercel-index.git
synced 2026-09-22 13:38:45 +00:00
Merge branch 'main' into ts
This commit is contained in:
@@ -295,7 +295,11 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
|||||||
// Folder recursive download
|
// Folder recursive download
|
||||||
const handleFolderDownload = (path: string, id: string, name?: string) => () => {
|
const handleFolderDownload = (path: string, id: string, name?: string) => () => {
|
||||||
const files = (async function* () {
|
const files = (async function* () {
|
||||||
for await (const { meta: c, path: p, isFolder } of traverseFolder(path)) {
|
for await (const { meta: c, path: p, isFolder, error } of traverseFolder(path)) {
|
||||||
|
if (error) {
|
||||||
|
toast.error(`Failed to download folder ${p}: ${error.status} ${error.message} Skipped it to continue.`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
yield {
|
yield {
|
||||||
name: c?.name,
|
name: c?.name,
|
||||||
url: c ? c['@microsoft.graph.downloadUrl'] : undefined,
|
url: c ? c['@microsoft.graph.downloadUrl'] : undefined,
|
||||||
|
|||||||
@@ -161,12 +161,14 @@ export async function downloadTreelikeMultipleFiles({
|
|||||||
* @param path Folder to be traversed
|
* @param path Folder to be traversed
|
||||||
* @returns Array of items representing folders and files of traversed folder in BFS order and excluding root folder.
|
* @returns Array of items representing folders and files of traversed folder in BFS order and excluding root folder.
|
||||||
* Due to BFS, folder items are ALWAYS in front of its children items.
|
* Due to BFS, folder items are ALWAYS in front of its children items.
|
||||||
|
* Error key in the item will contain the error when there is a handleable error.
|
||||||
*/
|
*/
|
||||||
export async function* traverseFolder(path: string): AsyncGenerator<
|
export async function* traverseFolder(path: string): AsyncGenerator<
|
||||||
{
|
{
|
||||||
path: string
|
path: string
|
||||||
meta: any
|
meta: any
|
||||||
isFolder: boolean
|
isFolder: boolean
|
||||||
|
error?: { status: number; message: string }
|
||||||
},
|
},
|
||||||
void,
|
void,
|
||||||
undefined
|
undefined
|
||||||
@@ -178,7 +180,22 @@ export async function* traverseFolder(path: string): AsyncGenerator<
|
|||||||
const itemLists = await Promise.all(
|
const itemLists = await Promise.all(
|
||||||
folderPaths.map(fp =>
|
folderPaths.map(fp =>
|
||||||
(async fp => {
|
(async fp => {
|
||||||
const data = await fetcher(`/api?path=${fp}`, hashedToken ?? undefined)
|
let data: any
|
||||||
|
try {
|
||||||
|
data = await fetcher(`/api?path=${fp}`, hashedToken ?? undefined)
|
||||||
|
} catch (error: any) {
|
||||||
|
// 4xx errors are identified as handleable errors
|
||||||
|
if (Math.floor(error.status / 100) === 4) {
|
||||||
|
return {
|
||||||
|
path: fp,
|
||||||
|
isFolder: true,
|
||||||
|
error: { status: error.status, message: error.message.error },
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (data && data.folder) {
|
if (data && data.folder) {
|
||||||
return data.folder.value.map((c: any) => {
|
return data.folder.value.map((c: any) => {
|
||||||
const p = `${fp === '/' ? '' : fp}/${encodeURIComponent(c.name)}`
|
const p = `${fp === '/' ? '' : fp}/${encodeURIComponent(c.name)}`
|
||||||
@@ -191,8 +208,16 @@ export async function* traverseFolder(path: string): AsyncGenerator<
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const items = itemLists.flat() as { path: string; meta: any; isFolder: boolean }[]
|
const items = itemLists.flat() as {
|
||||||
yield* items
|
path: string
|
||||||
folderPaths = items.filter(i => i.isFolder).map(i => i.path)
|
meta: any
|
||||||
|
isFolder: boolean
|
||||||
|
error?: { status: number; message: string }
|
||||||
|
}[]
|
||||||
|
yield * items
|
||||||
|
folderPaths = items
|
||||||
|
.filter(({ error }) => !error)
|
||||||
|
.filter(i => i.isFolder)
|
||||||
|
.map(i => i.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+126
-44
@@ -1,49 +1,67 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import useSWR, { SWRResponse } from 'swr'
|
||||||
|
import { Dispatch, Fragment, SetStateAction, useState } from 'react'
|
||||||
import AwesomeDebouncePromise from 'awesome-debounce-promise'
|
import AwesomeDebouncePromise from 'awesome-debounce-promise'
|
||||||
import { useAsync } from 'react-async-hook'
|
import { useAsync } from 'react-async-hook'
|
||||||
import useConstant from 'use-constant'
|
import useConstant from 'use-constant'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
|
|
||||||
import { Dispatch, FC, Fragment, SetStateAction, useState } from 'react'
|
|
||||||
import { Dialog, Transition } from '@headlessui/react'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
|
|
||||||
import { OdSearchResult } from '../types'
|
import { OdDriveItem, OdSearchResult } from '../types'
|
||||||
import { getFileIcon } from '../utils/getFileIcon'
|
|
||||||
import siteConfig from '../config/site.json'
|
|
||||||
import { LoadingIcon } from './Loading'
|
import { LoadingIcon } from './Loading'
|
||||||
|
|
||||||
|
import { getFileIcon } from '../utils/getFileIcon'
|
||||||
|
import siteConfig from '../config/site.json'
|
||||||
|
import { fetcher } from '../utils/fetchWithSWR'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the searched item's path in field 'parentReference' and convert it to the
|
||||||
|
* absolute path represented in onedrive-vercel-index
|
||||||
|
*
|
||||||
|
* @param path Path returned from the parentReference field of the driveItem
|
||||||
|
* @returns The absolute path of the driveItem in the search result
|
||||||
|
*/
|
||||||
|
function mapAbsolutePath(path: string): string {
|
||||||
|
// path is in the format of '/drive/root:/path/to/file', if baseDirectory is '/' then we split on 'root:',
|
||||||
|
// otherwise we split on the user defined 'baseDirectory'
|
||||||
|
const absolutePath = path.split(siteConfig.baseDirectory === '/' ? 'root:' : siteConfig.baseDirectory)[1]
|
||||||
|
// path returned by the API may contain #, by doing a decodeURIComponent and then encodeURIComponent we can
|
||||||
|
// replace URL sensitive characters such as the # with %23
|
||||||
|
return encodeURIComponent(decodeURIComponent(absolutePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a debounced search function that returns a promise that resolves to an array of
|
||||||
|
* search results.
|
||||||
|
*
|
||||||
|
* @returns A react hook for a debounced async search of the drive
|
||||||
|
*/
|
||||||
function useDriveItemSearch() {
|
function useDriveItemSearch() {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const searchDriveItem = async (q: string) => {
|
const searchDriveItem = async (q: string) => {
|
||||||
const { data } = await axios.get<OdSearchResult>(`/api/search?q=${q}`)
|
const { data } = await axios.get<OdSearchResult>(`/api/search?q=${q}`)
|
||||||
|
|
||||||
// Extract the searched item's path and convert it to the absolute path in onedrive-vercel-index
|
|
||||||
function mapAbsolutePath(path: string): string {
|
|
||||||
return siteConfig.baseDirectory === '/' ? path.split('root:')[1] : path.split(siteConfig.baseDirectory)[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map parentReference to the absolute path of the search result
|
// Map parentReference to the absolute path of the search result
|
||||||
data.map(item => {
|
data.map(item => {
|
||||||
// TODO: supporting sharepoint search where the path is not returned in parentReference
|
item['path'] =
|
||||||
if ('path' in item.parentReference) {
|
'path' in item.parentReference
|
||||||
item['path'] = `${mapAbsolutePath(item.parentReference.path)}/${encodeURIComponent(item.name)}`
|
? // OneDrive International have the path returned in the parentReference field
|
||||||
} else {
|
`${mapAbsolutePath(item.parentReference.path)}/${encodeURIComponent(item.name)}`
|
||||||
throw Error(
|
: // OneDrive for Business/Education does not, so we need extra steps here
|
||||||
'We currently only support search in OneDrive international. SharePoint instances are not supported yet. See issue: https://github.com/spencerwooo/onedrive-vercel-index/issues/299'
|
''
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedNotionSearch = useConstant(() => AwesomeDebouncePromise(searchDriveItem, 1000))
|
const debouncedDriveItemSearch = useConstant(() => AwesomeDebouncePromise(searchDriveItem, 1000))
|
||||||
const results = useAsync(async () => {
|
const results = useAsync(async () => {
|
||||||
if (query.length === 0) {
|
if (query.length === 0) {
|
||||||
return []
|
return []
|
||||||
} else {
|
} else {
|
||||||
return debouncedNotionSearch(query)
|
return debouncedDriveItemSearch(query)
|
||||||
}
|
}
|
||||||
}, [query])
|
}, [query])
|
||||||
|
|
||||||
@@ -54,17 +72,95 @@ function useDriveItemSearch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchModal({
|
function SearchResultItemTemplate({
|
||||||
|
driveItem,
|
||||||
|
driveItemPath,
|
||||||
|
itemDescription,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
driveItem: OdSearchResult[number]
|
||||||
|
driveItemPath: string
|
||||||
|
itemDescription: string
|
||||||
|
disabled: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Link href={driveItemPath} passHref>
|
||||||
|
<a
|
||||||
|
className={`flex items-center space-x-4 border-b border-gray-400/30 px-4 py-1.5 hover:bg-gray-50 dark:hover:bg-gray-850 ${
|
||||||
|
disabled ? 'cursor-not-allowed pointer-events-none' : 'cursor-pointer'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={driveItem.file ? getFileIcon(driveItem.name) : ['far', 'folder']} />
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium leading-8">{driveItem.name}</div>
|
||||||
|
<div
|
||||||
|
className={`text-xs font-mono opacity-60 truncate overflow-hidden ${
|
||||||
|
itemDescription === 'Loading ...' && 'animate-pulse'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{itemDescription}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchResultItemLoadRemote({ result }: { result: OdSearchResult[number] }) {
|
||||||
|
const { data, error }: SWRResponse<OdDriveItem, string> = useSWR(`/api/item?id=${result.id}`, fetcher)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <SearchResultItemTemplate driveItem={result} driveItemPath={''} itemDescription={error} disabled={true} />
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<SearchResultItemTemplate driveItem={result} driveItemPath={''} itemDescription={'Loading ...'} disabled={true} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const driveItemPath = `${mapAbsolutePath(data.parentReference.path)}/${encodeURIComponent(data.name)}`
|
||||||
|
return (
|
||||||
|
<SearchResultItemTemplate
|
||||||
|
driveItem={result}
|
||||||
|
driveItemPath={driveItemPath}
|
||||||
|
itemDescription={decodeURIComponent(driveItemPath)}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchResultItem({ result }: { result: OdSearchResult[number] }) {
|
||||||
|
if (result.path === '') {
|
||||||
|
// path is empty, which means we need to fetch the parentReference to get the path
|
||||||
|
return <SearchResultItemLoadRemote result={result} />
|
||||||
|
} else {
|
||||||
|
// path is not an empty string in the search result, such that we can directly render the component as is
|
||||||
|
const driveItemPath = decodeURIComponent(result.path)
|
||||||
|
return (
|
||||||
|
<SearchResultItemTemplate
|
||||||
|
driveItem={result}
|
||||||
|
driveItemPath={result.path}
|
||||||
|
itemDescription={driveItemPath}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SearchModal({
|
||||||
searchOpen,
|
searchOpen,
|
||||||
setSearchOpen,
|
setSearchOpen,
|
||||||
}: {
|
}: {
|
||||||
searchOpen: boolean
|
searchOpen: boolean
|
||||||
setSearchOpen: Dispatch<SetStateAction<boolean>>
|
setSearchOpen: Dispatch<SetStateAction<boolean>>
|
||||||
}) {
|
}) {
|
||||||
const closeSearchBox = () => setSearchOpen(false)
|
|
||||||
|
|
||||||
const { query, setQuery, results } = useDriveItemSearch()
|
const { query, setQuery, results } = useDriveItemSearch()
|
||||||
|
|
||||||
|
const closeSearchBox = () => {
|
||||||
|
setSearchOpen(false)
|
||||||
|
setQuery('')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={searchOpen} as={Fragment}>
|
<Transition appear show={searchOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="inset-0 z-[200] fixed overflow-y-auto" onClose={closeSearchBox}>
|
<Dialog as="div" className="inset-0 z-[200] fixed overflow-y-auto" onClose={closeSearchBox}>
|
||||||
@@ -104,10 +200,13 @@ function SearchModal({
|
|||||||
value={query}
|
value={query}
|
||||||
onChange={e => setQuery(e.target.value)}
|
onChange={e => setQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className="px-2 py-1 rounded-lg bg-gray-100 dark:bg-gray-700 font-medium text-xs">ESC</div>
|
<div className="px-2 py-1 rounded-lg bg-gray-200 dark:bg-gray-700 font-medium text-xs">ESC</div>
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
|
|
||||||
<div className="bg-white dark:text-white dark:bg-gray-900 max-h-[80vh] overflow-x-hidden overflow-y-scroll">
|
<div
|
||||||
|
className="bg-white dark:text-white dark:bg-gray-900 max-h-[80vh] overflow-x-hidden overflow-y-scroll"
|
||||||
|
onClick={closeSearchBox}
|
||||||
|
>
|
||||||
{results.loading && (
|
{results.loading && (
|
||||||
<div className="text-center px-4 py-12 text-sm font-medium">
|
<div className="text-center px-4 py-12 text-sm font-medium">
|
||||||
<LoadingIcon className="animate-spin w-4 h-4 mr-2 inline-block svg-inline--fa" />
|
<LoadingIcon className="animate-spin w-4 h-4 mr-2 inline-block svg-inline--fa" />
|
||||||
@@ -122,22 +221,7 @@ function SearchModal({
|
|||||||
{results.result.length === 0 ? (
|
{results.result.length === 0 ? (
|
||||||
<div className="text-center px-4 py-12 text-sm font-medium">Nothing here.</div>
|
<div className="text-center px-4 py-12 text-sm font-medium">Nothing here.</div>
|
||||||
) : (
|
) : (
|
||||||
results.result.map(result => (
|
results.result.map(result => <SearchResultItem key={result.id} result={result} />)
|
||||||
<Link href={result.path} key={result.id} passHref>
|
|
||||||
<div
|
|
||||||
className="flex items-center space-x-4 border-b cursor-pointer border-gray-400/30 px-4 py-1.5 hover:bg-gray-50 dark:hover:bg-gray-850"
|
|
||||||
onClick={closeSearchBox}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={result.file ? getFileIcon(result.name) : ['far', 'folder']} />
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-medium leading-8">{result.name}</div>
|
|
||||||
<div className="text-xs font-mono opacity-60 truncate overflow-hidden">
|
|
||||||
{decodeURIComponent(result.path)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -149,5 +233,3 @@ function SearchModal({
|
|||||||
</Transition>
|
</Transition>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SearchModal
|
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { useEffect, FC } from 'react'
|
|||||||
import Prism from 'prismjs'
|
import Prism from 'prismjs'
|
||||||
|
|
||||||
import { getExtension } from '../../utils/getFileIcon'
|
import { getExtension } from '../../utils/getFileIcon'
|
||||||
import useFileContent from '../../utils/fetchOnMount'
|
import useAxiosGet from '../../utils/fetchOnMount'
|
||||||
import FourOhFour from '../FourOhFour'
|
import FourOhFour from '../FourOhFour'
|
||||||
import Loading from '../Loading'
|
import Loading from '../Loading'
|
||||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||||
|
|
||||||
const CodePreview: FC<{ file: any }> = ({ file }) => {
|
const CodePreview: FC<{ file: any }> = ({ file }) => {
|
||||||
const { content, error, validating } = useFileContent(file['@microsoft.graph.downloadUrl'])
|
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import 'katex/dist/katex.min.css'
|
|||||||
import FourOhFour from '../FourOhFour'
|
import FourOhFour from '../FourOhFour'
|
||||||
import Loading from '../Loading'
|
import Loading from '../Loading'
|
||||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||||
import useFileContent from '../../utils/fetchOnMount'
|
import useAxiosGet from '../../utils/fetchOnMount'
|
||||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||||
|
|
||||||
const MarkdownPreview: FC<{ file: any; path: string; standalone?: boolean }> = ({
|
const MarkdownPreview: FC<{ file: any; path: string; standalone?: boolean }> = ({
|
||||||
@@ -19,7 +19,7 @@ const MarkdownPreview: FC<{ file: any; path: string; standalone?: boolean }> = (
|
|||||||
path,
|
path,
|
||||||
standalone = true,
|
standalone = true,
|
||||||
}) => {
|
}) => {
|
||||||
const { content, error, validating } = useFileContent(file['@microsoft.graph.downloadUrl'])
|
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
|
||||||
|
|
||||||
// The parent folder of the markdown file, which is also the relative image folder
|
// The parent folder of the markdown file, which is also the relative image folder
|
||||||
const parentPath = path.substring(0, path.lastIndexOf('/'))
|
const parentPath = path.substring(0, path.lastIndexOf('/'))
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import FourOhFour from '../FourOhFour'
|
import FourOhFour from '../FourOhFour'
|
||||||
import Loading from '../Loading'
|
import Loading from '../Loading'
|
||||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||||
import useFileContent from '../../utils/fetchOnMount'
|
import useAxiosGet from '../../utils/fetchOnMount'
|
||||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||||
|
|
||||||
const TextPreview = ({ file }) => {
|
const TextPreview = ({ file }) => {
|
||||||
const { content, error, validating } = useFileContent(file['@microsoft.graph.downloadUrl'])
|
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<PreviewContainer>
|
<PreviewContainer>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import FourOhFour from '../FourOhFour'
|
import FourOhFour from '../FourOhFour'
|
||||||
import Loading from '../Loading'
|
import Loading from '../Loading'
|
||||||
import { DownloadButton } from '../DownloadBtnGtoup'
|
import { DownloadButton } from '../DownloadBtnGtoup'
|
||||||
import useFileContent from '../../utils/fetchOnMount'
|
import useAxiosGet from '../../utils/fetchOnMount'
|
||||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||||
|
|
||||||
const parseDotUrl = (content: string): string | undefined => {
|
const parseDotUrl = (content: string): string | undefined => {
|
||||||
@@ -12,7 +12,7 @@ const parseDotUrl = (content: string): string | undefined => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TextPreview = ({ file }) => {
|
const TextPreview = ({ file }) => {
|
||||||
const { content, error, validating } = useFileContent(file['@microsoft.graph.downloadUrl'])
|
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<PreviewContainer>
|
<PreviewContainer>
|
||||||
|
|||||||
+15
-3
@@ -12,8 +12,14 @@ import { getOdAuthTokens, storeOdAuthTokens } from '../../utils/odAuthTokenStore
|
|||||||
const basePath = pathPosix.resolve('/', siteConfig.baseDirectory)
|
const basePath = pathPosix.resolve('/', siteConfig.baseDirectory)
|
||||||
const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
|
const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the path of the file relative to the base directory
|
||||||
|
*
|
||||||
|
* @param path Relative path of the file to the base directory
|
||||||
|
* @returns Absolute path of the file inside OneDrive
|
||||||
|
*/
|
||||||
export function encodePath(path: string): string {
|
export function encodePath(path: string): string {
|
||||||
let encodedPath = pathPosix.join(basePath, pathPosix.resolve('/', path))
|
let encodedPath = pathPosix.join(basePath, path)
|
||||||
if (encodedPath === '/' || encodedPath === '') {
|
if (encodedPath === '/' || encodedPath === '') {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -21,6 +27,11 @@ export function encodePath(path: string): string {
|
|||||||
return `:${encodeURIComponent(encodedPath)}`
|
return `:${encodeURIComponent(encodedPath)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the access token from Redis storage and check if the token requires a renew
|
||||||
|
*
|
||||||
|
* @returns Access token for OneDrive API
|
||||||
|
*/
|
||||||
export async function getAccessToken(): Promise<string> {
|
export async function getAccessToken(): Promise<string> {
|
||||||
const { accessToken, refreshToken } = await getOdAuthTokens()
|
const { accessToken, refreshToken } = await getOdAuthTokens()
|
||||||
|
|
||||||
@@ -98,6 +109,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
res.status(400).json({ error: 'Path query invalid.' })
|
res.status(400).json({ error: 'Path query invalid.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const cleanPath = pathPosix.resolve('/', pathPosix.normalize(path))
|
||||||
|
|
||||||
const accessToken = await getAccessToken()
|
const accessToken = await getAccessToken()
|
||||||
|
|
||||||
@@ -111,7 +123,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const protectedRoutes = siteConfig.protectedRoutes
|
const protectedRoutes = siteConfig.protectedRoutes
|
||||||
let authTokenPath = ''
|
let authTokenPath = ''
|
||||||
for (const r of protectedRoutes) {
|
for (const r of protectedRoutes) {
|
||||||
if (path.startsWith(r)) {
|
if (cleanPath.startsWith(r)) {
|
||||||
authTokenPath = `${r}/.password`
|
authTokenPath = `${r}/.password`
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -150,7 +162,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestPath = encodePath(path)
|
const requestPath = encodePath(cleanPath)
|
||||||
// Handle response from OneDrive API
|
// Handle response from OneDrive API
|
||||||
const requestUrl = `${apiConfig.driveApi}/root${requestPath}`
|
const requestUrl = `${apiConfig.driveApi}/root${requestPath}`
|
||||||
// Whether path is root, which requires some special treatment
|
// Whether path is root, which requires some special treatment
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
|
import { getAccessToken } from '.'
|
||||||
|
import apiConfig from '../../config/api.json'
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
// Get access token from storage
|
||||||
|
const accessToken = await getAccessToken()
|
||||||
|
|
||||||
|
// Get item details (specifically, its path) by its unique ID in OneDrive
|
||||||
|
const { id = '' } = req.query
|
||||||
|
|
||||||
|
if (typeof id === 'string') {
|
||||||
|
const itemApi = `${apiConfig.driveApi}/items/${id}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(itemApi, {
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
params: {
|
||||||
|
select: 'id,name,parentReference',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
res.status(200).json(data)
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(error.response.status).json({ error: error.response.data })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(400).json({ error: 'Invalid driveItem ID.' })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
+18
-3
@@ -3,6 +3,18 @@ import type { NextApiRequest, NextApiResponse } from 'next'
|
|||||||
|
|
||||||
import { encodePath, getAccessToken } from '.'
|
import { encodePath, getAccessToken } from '.'
|
||||||
import apiConfig from '../../config/api.json'
|
import apiConfig from '../../config/api.json'
|
||||||
|
import siteConfig from '../../config/site.json'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize the search query
|
||||||
|
*
|
||||||
|
* @param query User search query, which may contain special characters
|
||||||
|
* @returns Sanitised query string which replaces non-alphanumeric characters with ' '
|
||||||
|
*/
|
||||||
|
function sanitiseQuery(query: string): string {
|
||||||
|
const sanitisedQuery = query.replace(/[^a-zA-Z0-9]/g, ' ')
|
||||||
|
return encodeURIComponent(sanitisedQuery)
|
||||||
|
}
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
// Get access token from storage
|
// Get access token from storage
|
||||||
@@ -12,15 +24,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const { q: searchQuery = '' } = req.query
|
const { q: searchQuery = '' } = req.query
|
||||||
|
|
||||||
if (typeof searchQuery === 'string') {
|
if (typeof searchQuery === 'string') {
|
||||||
// Construct Microsoft Graph Search API URL, and perform search only under the base dir
|
// Construct Microsoft Graph Search API URL, and perform search only under the base directory
|
||||||
const encodedPath = encodePath('/') === '' ? encodePath('/') : encodePath('/') + ':'
|
const searchRootPath = encodePath('/')
|
||||||
const searchApi = `${apiConfig.driveApi}/root${encodedPath}/search(q='${encodeURIComponent(searchQuery)}')`
|
const encodedPath = searchRootPath === '' ? searchRootPath : searchRootPath + ':'
|
||||||
|
|
||||||
|
const searchApi = `${apiConfig.driveApi}/root${encodedPath}/search(q='${sanitiseQuery(searchQuery)}')`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get(searchApi, {
|
const { data } = await axios.get(searchApi, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
params: {
|
params: {
|
||||||
select: 'id,name,file,folder,parentReference',
|
select: 'id,name,file,folder,parentReference',
|
||||||
|
top: siteConfig.maxItems,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
res.status(200).json(data.value)
|
res.status(200).json(data.value)
|
||||||
|
|||||||
Vendored
+14
@@ -50,3 +50,17 @@ export declare type OdSearchResult = Array<{
|
|||||||
path: string
|
path: string
|
||||||
parentReference: { id: string; name: string; path: string }
|
parentReference: { id: string; name: string; path: string }
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
// driveItem type which is returned by /api/item?id={id}
|
||||||
|
export type OdDriveItem = {
|
||||||
|
'@odata.context': string
|
||||||
|
'@odata.etag': string
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
parentReference: {
|
||||||
|
driveId: string
|
||||||
|
driveType: string
|
||||||
|
id: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
// Custom hook to fetch raw file content on mount
|
// Custom hook to axios get a URL or API endpoint on mount
|
||||||
export default function useFileContent(odRawUrl: string): { content: string; error: string; validating: boolean } {
|
export default function useAxiosGet(fetchUrl: string): { response: any; error: string; validating: boolean } {
|
||||||
const [content, setContent] = useState('')
|
const [response, setResponse] = useState('')
|
||||||
const [validating, setValidating] = useState(true)
|
const [validating, setValidating] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios
|
axios
|
||||||
.get(odRawUrl)
|
.get(fetchUrl)
|
||||||
.then(res => setContent(res.data))
|
.then(res => setResponse(res.data))
|
||||||
.catch(e => setError(e.message))
|
.catch(e => setError(e.message))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setValidating(false)
|
setValidating(false)
|
||||||
})
|
})
|
||||||
}, [odRawUrl])
|
}, [fetchUrl])
|
||||||
return { content, error, validating }
|
return { response, error, validating }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user