Standalone raw file redirects (#428)

This commit is contained in:
Spencer Woo
2022-02-14 19:33:19 +08:00
committed by GitHub
parent 0fda1c93db
commit 9493ce9f3d
30 changed files with 365 additions and 185 deletions
+12 -3
View File
@@ -6,6 +6,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useClipboard } from 'use-clipboard-copy' import { useClipboard } from 'use-clipboard-copy'
import { getBaseUrl } from '../utils/getBaseUrl' import { getBaseUrl } from '../utils/getBaseUrl'
import { getStoredToken } from '../utils/protectedRouteHandler'
import { getReadablePath } from '../utils/getReadablePath'
export default function CustomEmbedLinkMenu({ export default function CustomEmbedLinkMenu({
path, path,
@@ -18,6 +20,9 @@ export default function CustomEmbedLinkMenu({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const clipboard = useClipboard() const clipboard = useClipboard()
const hashedToken = getStoredToken(path)
const closeMenu = () => setMenuOpen(false) const closeMenu = () => setMenuOpen(false)
const filename = path.substring(path.lastIndexOf('/') + 1) const filename = path.substring(path.lastIndexOf('/') + 1)
@@ -77,13 +82,13 @@ export default function CustomEmbedLinkMenu({
/> />
<h4 className="py-2 text-xs font-medium uppercase tracking-wider">{t('Default')}</h4> <h4 className="py-2 text-xs font-medium uppercase tracking-wider">{t('Default')}</h4>
<div className="mb-2 rounded border border-gray-400/20 bg-gray-50 p-1 font-mono dark:bg-gray-800"> <div className="mb-2 rounded border border-gray-400/20 bg-gray-50 p-1 font-mono dark:bg-gray-800">
{`${getBaseUrl()}/api?path=${path}&raw=true`} {`${getBaseUrl()}/api/raw/?path=${getReadablePath(path)}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
</div> </div>
<h4 className="py-2 text-xs font-medium uppercase tracking-wider">{t('Customised')}</h4> <h4 className="py-2 text-xs font-medium uppercase tracking-wider">{t('Customised')}</h4>
<div className="mb-2 rounded border border-gray-400/20 bg-gray-50 p-1 font-mono dark:bg-gray-800"> <div className="mb-2 rounded border border-gray-400/20 bg-gray-50 p-1 font-mono dark:bg-gray-800">
<span>{`${getBaseUrl()}/api/name/`}</span> <span>{`${getBaseUrl()}/api/name/`}</span>
<span className="underline decoration-blue-400 decoration-wavy">{name}</span> <span className="underline decoration-blue-400 decoration-wavy">{name}</span>
<span>{`?path=${path}&raw=true`}</span> <span>{`/?path=${getReadablePath(path)}${hashedToken ? `&odpt=${hashedToken}` : ''}`}</span>
</div> </div>
</div> </div>
@@ -91,7 +96,11 @@ export default function CustomEmbedLinkMenu({
<button <button
className="rounded-lg bg-gradient-to-r from-cyan-500 to-blue-500 px-4 py-2 text-center text-sm font-medium text-white hover:bg-gradient-to-bl focus:ring-4 focus:ring-cyan-300 dark:focus:ring-cyan-800" className="rounded-lg bg-gradient-to-r from-cyan-500 to-blue-500 px-4 py-2 text-center text-sm font-medium text-white hover:bg-gradient-to-bl focus:ring-4 focus:ring-cyan-300 dark:focus:ring-cyan-800"
onClick={() => { onClick={() => {
clipboard.copy(`${getBaseUrl()}/api/name/${name}?path=${path}&raw=true`) clipboard.copy(
`${getBaseUrl()}/api/name/${name}/?path=${getReadablePath(path)}${
hashedToken ? `&odpt=${hashedToken}` : ''
}`
)
toast.success(t('Copied customised link to clipboard.')) toast.success(t('Copied customised link to clipboard.'))
closeMenu() closeMenu()
}} }}
+14 -9
View File
@@ -10,6 +10,7 @@ import { useRouter } from 'next/router'
import { getBaseUrl } from '../utils/getBaseUrl' import { getBaseUrl } from '../utils/getBaseUrl'
import { getReadablePath } from '../utils/getReadablePath' import { getReadablePath } from '../utils/getReadablePath'
import { getStoredToken } from '../utils/protectedRouteHandler'
import CustomEmbedLinkMenu from './CustomEmbedLinkMenu' import CustomEmbedLinkMenu from './CustomEmbedLinkMenu'
const btnStyleMap = (btnColor?: string) => { const btnStyleMap = (btnColor?: string) => {
@@ -62,8 +63,10 @@ export const DownloadButton = ({
) )
} }
const DownloadButtonGroup: React.FC<{ downloadUrl: string }> = ({ downloadUrl }) => { const DownloadButtonGroup = () => {
const { asPath } = useRouter() const { asPath } = useRouter()
const hashedToken = getStoredToken(asPath)
const clipboard = useClipboard() const clipboard = useClipboard()
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
@@ -74,22 +77,24 @@ const DownloadButtonGroup: React.FC<{ downloadUrl: string }> = ({ downloadUrl })
<CustomEmbedLinkMenu menuOpen={menuOpen} setMenuOpen={setMenuOpen} path={asPath} /> <CustomEmbedLinkMenu menuOpen={menuOpen} setMenuOpen={setMenuOpen} path={asPath} />
<div className="flex flex-wrap justify-center gap-2"> <div className="flex flex-wrap justify-center gap-2">
<DownloadButton <DownloadButton
onClickCallback={() => window.open(downloadUrl)} onClickCallback={() => window.open(`/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`)}
btnColor="blue" btnColor="blue"
btnText={t('Download')} btnText={t('Download')}
btnIcon="file-download" btnIcon="file-download"
btnTitle={t('Download the file directly through OneDrive')} btnTitle={t('Download the file directly through OneDrive')}
/> />
{/* <DownloadButton {/* <DownloadButton
onClickCallback={() => window.open(`/api/proxy?url=${encodeURIComponent(downloadUrl)}`)} onClickCallback={() => window.open(`/api/proxy?url=${encodeURIComponent(downloadUrl)}`)}
btnColor="teal" btnColor="teal"
btnText={t('Proxy download')} btnText={t('Proxy download')}
btnIcon="download" btnIcon="download"
btnTitle={t('Download the file with the stream proxied through Vercel Serverless')} btnTitle={t('Download the file with the stream proxied through Vercel Serverless')}
/> */} /> */}
<DownloadButton <DownloadButton
onClickCallback={() => { onClickCallback={() => {
clipboard.copy(`${getBaseUrl()}/api?path=${getReadablePath(asPath)}&raw=true`) clipboard.copy(
`${getBaseUrl()}/api/raw/?path=${getReadablePath(asPath)}${hashedToken ? `&odpt=${hashedToken}` : ''}`
)
toast.success(t('Copied direct link to clipboard.')) toast.success(t('Copied direct link to clipboard.'))
}} }}
btnColor="pink" btnColor="pink"
+13 -8
View File
@@ -1,10 +1,10 @@
import type { OdFileObject, OdFolderChildren, OdFolderObject } from '../types'
import { ParsedUrlQuery } from 'querystring'
import { FC, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import toast, { Toaster } from 'react-hot-toast' import toast, { Toaster } from 'react-hot-toast'
import emojiRegex from 'emoji-regex' import emojiRegex from 'emoji-regex'
import { ParsedUrlQuery } from 'querystring'
import { FC, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from 'react'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
@@ -13,6 +13,7 @@ import useLocalStorage from '../utils/useLocalStorage'
import { getPreviewType, preview } from '../utils/getPreviewType' import { getPreviewType, preview } from '../utils/getPreviewType'
import { useProtectedSWRInfinite } from '../utils/fetchWithSWR' import { useProtectedSWRInfinite } from '../utils/fetchWithSWR'
import { getExtension, getFileIcon } from '../utils/getFileIcon' import { getExtension, getFileIcon } from '../utils/getFileIcon'
import { getStoredToken } from '../utils/protectedRouteHandler'
import { import {
DownloadingToast, DownloadingToast,
downloadMultipleFiles, downloadMultipleFiles,
@@ -36,7 +37,6 @@ import ImagePreview from './previews/ImagePreview'
import DefaultPreview from './previews/DefaultPreview' import DefaultPreview from './previews/DefaultPreview'
import { PreviewContainer } from './previews/Containers' import { PreviewContainer } from './previews/Containers'
import type { OdFileObject, OdFolderChildren, OdFolderObject } from '../types'
import FolderListLayout from './FolderListLayout' import FolderListLayout from './FolderListLayout'
import FolderGridLayout from './FolderGridLayout' import FolderGridLayout from './FolderGridLayout'
@@ -134,9 +134,9 @@ export const Checkbox: FC<{
) )
} }
export const Downloading: FC<{ title: string }> = ({ title }) => { export const Downloading: FC<{ title: string; style: string }> = ({ title, style }) => {
return ( return (
<span title={title} className="rounded p-2" role="status"> <span title={title} className={`${style} rounded`} role="status">
<LoadingIcon <LoadingIcon
// Use fontawesome far theme via class `svg-inline--fa` to get style `vertical-align` only // Use fontawesome far theme via class `svg-inline--fa` to get style `vertical-align` only
// for consistent icon alignment, as class `align-*` cannot satisfy it // for consistent icon alignment, as class `align-*` cannot satisfy it
@@ -155,6 +155,7 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
}>({}) }>({})
const router = useRouter() const router = useRouter()
const hashedToken = getStoredToken(router.asPath)
const [layout, _] = useLocalStorage('preferredLayout', layouts[0]) const [layout, _] = useLocalStorage('preferredLayout', layouts[0])
const { t } = useTranslation() const { t } = useTranslation()
@@ -237,7 +238,10 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
const folder = folderName ? decodeURIComponent(folderName) : undefined const folder = folderName ? decodeURIComponent(folderName) : undefined
const files = getFiles() const files = getFiles()
.filter(c => selected[c.id]) .filter(c => selected[c.id])
.map(c => ({ name: c.name, url: c['@microsoft.graph.downloadUrl'] })) .map(c => ({
name: c.name,
url: `/api/raw/?path=${path}/${c.name}${hashedToken ? `&odpt=${hashedToken}` : ''}`,
}))
if (files.length == 1) { if (files.length == 1) {
const el = document.createElement('a') const el = document.createElement('a')
@@ -278,9 +282,10 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
) )
continue continue
} }
const hashedTokenForPath = getStoredToken(p)
yield { yield {
name: c?.name, name: c?.name,
url: c ? c['@microsoft.graph.downloadUrl'] : undefined, url: `/api/raw/?path=${p}${hashedTokenForPath ? `&odpt=${hashedTokenForPath}` : ''}`,
path: p, path: p,
isFolder, isFolder,
} }
+15 -5
View File
@@ -10,10 +10,13 @@ import { getBaseUrl } from '../utils/getBaseUrl'
import { formatModifiedDateTime } from '../utils/fileDetails' import { formatModifiedDateTime } from '../utils/fileDetails'
import { getReadablePath } from '../utils/getReadablePath' import { getReadablePath } from '../utils/getReadablePath'
import { Checkbox, ChildIcon, ChildName, Downloading } from './FileListing' import { Checkbox, ChildIcon, ChildName, Downloading } from './FileListing'
import { getStoredToken } from '../utils/protectedRouteHandler'
const GridItem = ({ c, path }: { c: OdFolderChildren; path: string }) => { const GridItem = ({ c, path }: { c: OdFolderChildren; path: string }) => {
// We use the generated medium thumbnail for rendering preview images (excluding folders) // We use the generated medium thumbnail for rendering preview images (excluding folders)
const thumbnailUrl = 'folder' in c ? null : `/api/thumbnail?path=${path}&size=medium` const hashedToken = getStoredToken(path)
const thumbnailUrl =
'folder' in c ? null : `/api/thumbnail/?path=${path}&size=medium${hashedToken ? `&odpt=${hashedToken}` : ''}`
// Some thumbnails are broken, so we check for onerror event in the image component // Some thumbnails are broken, so we check for onerror event in the image component
const [brokenThumbnail, setBrokenThumbnail] = useState(false) const [brokenThumbnail, setBrokenThumbnail] = useState(false)
@@ -66,6 +69,7 @@ const FolderGridLayout = ({
toast, toast,
}) => { }) => {
const clipboard = useClipboard() const clipboard = useClipboard()
const hashedToken = getStoredToken(path)
const { t } = useTranslation() const { t } = useTranslation()
@@ -84,7 +88,7 @@ const FolderGridLayout = ({
title={t('Select all files')} title={t('Select all files')}
/> />
{totalGenerating ? ( {totalGenerating ? (
<Downloading title={t('Downloading selected files, refresh page to cancel')} /> <Downloading title={t('Downloading selected files, refresh page to cancel')} style="p-1.5" />
) : ( ) : (
<button <button
title={t('Download selected files')} title={t('Download selected files')}
@@ -118,7 +122,7 @@ const FolderGridLayout = ({
<FontAwesomeIcon icon={['far', 'copy']} /> <FontAwesomeIcon icon={['far', 'copy']} />
</span> </span>
{folderGenerating[c.id] ? ( {folderGenerating[c.id] ? (
<Downloading title={t('Downloading folder, refresh page to cancel')} /> <Downloading title={t('Downloading folder, refresh page to cancel')} style="px-1.5 py-1" />
) : ( ) : (
<span <span
title={t('Download folder')} title={t('Download folder')}
@@ -135,7 +139,11 @@ const FolderGridLayout = ({
title={t('Copy raw file permalink')} title={t('Copy raw file permalink')}
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600" className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
onClick={() => { onClick={() => {
clipboard.copy(`${getBaseUrl()}/api?path=${getReadablePath(getItemPath(c.name))}&raw=true`) clipboard.copy(
`${getBaseUrl()}/api/raw/?path=${getReadablePath(getItemPath(c.name))}${
hashedToken ? `&odpt=${hashedToken}` : ''
}`
)
toast.success(t('Copied raw file permalink.')) toast.success(t('Copied raw file permalink.'))
}} }}
> >
@@ -144,7 +152,9 @@ const FolderGridLayout = ({
<a <a
title={t('Download file')} title={t('Download file')}
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600" className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
href={c['@microsoft.graph.downloadUrl']} href={`${getBaseUrl()}/api/raw/?path=${getReadablePath(getItemPath(c.name))}${
hashedToken ? `&odpt=${hashedToken}` : ''
}`}
> >
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} /> <FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} />
</a> </a>
+11 -6
View File
@@ -11,6 +11,7 @@ import { humanFileSize, formatModifiedDateTime } from '../utils/fileDetails'
import { getReadablePath } from '../utils/getReadablePath' import { getReadablePath } from '../utils/getReadablePath'
import { Downloading, Checkbox, ChildIcon, ChildName } from './FileListing' import { Downloading, Checkbox, ChildIcon, ChildName } from './FileListing'
import { getStoredToken } from '../utils/protectedRouteHandler'
const FileListItem: FC<{ fileContent: OdFolderChildren }> = ({ fileContent: c }) => { const FileListItem: FC<{ fileContent: OdFolderChildren }> = ({ fileContent: c }) => {
return ( return (
@@ -45,9 +46,13 @@ const FolderListLayout = ({
toast, toast,
}) => { }) => {
const clipboard = useClipboard() const clipboard = useClipboard()
const hashedToken = getStoredToken(path)
const { t } = useTranslation() const { t } = useTranslation()
// Get item path from item name
const getItemPath = (name: string) => `${path === '/' ? '' : path}/${encodeURIComponent(name)}`
return ( return (
<div className="rounded bg-white dark:bg-gray-900 dark:text-gray-100"> <div className="rounded bg-white dark:bg-gray-900 dark:text-gray-100">
<div className="grid grid-cols-12 items-center space-x-2 border-b border-gray-900/10 px-3 dark:border-gray-500/30"> <div className="grid grid-cols-12 items-center space-x-2 border-b border-gray-900/10 px-3 dark:border-gray-500/30">
@@ -72,7 +77,7 @@ const FolderListLayout = ({
title={t('Select files')} title={t('Select files')}
/> />
{totalGenerating ? ( {totalGenerating ? (
<Downloading title={t('Downloading selected files, refresh page to cancel')} /> <Downloading title={t('Downloading selected files, refresh page to cancel')} style="p-1.5" />
) : ( ) : (
<button <button
title={t('Download selected files')} title={t('Download selected files')}
@@ -113,7 +118,7 @@ const FolderListLayout = ({
<FontAwesomeIcon icon={['far', 'copy']} /> <FontAwesomeIcon icon={['far', 'copy']} />
</span> </span>
{folderGenerating[c.id] ? ( {folderGenerating[c.id] ? (
<Downloading title={t('Downloading folder, refresh page to cancel')} /> <Downloading title={t('Downloading folder, refresh page to cancel')} style="px-1.5 py-1" />
) : ( ) : (
<span <span
title={t('Download folder')} title={t('Download folder')}
@@ -134,9 +139,9 @@ const FolderListLayout = ({
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600" className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
onClick={() => { onClick={() => {
clipboard.copy( clipboard.copy(
`${getBaseUrl()}/api?path=${getReadablePath( `${getBaseUrl()}/api/raw/?path=${getReadablePath(getItemPath(c.name))}${
`${path === '/' ? '' : path}/${encodeURIComponent(c.name)}` hashedToken ? `&odpt=${hashedToken}` : ''
)}&raw=true` }`
) )
toast.success(t('Copied raw file permalink.')) toast.success(t('Copied raw file permalink.'))
}} }}
@@ -146,7 +151,7 @@ const FolderListLayout = ({
<a <a
title={t('Download file')} title={t('Download file')}
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600" className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
href={c['@microsoft.graph.downloadUrl']} href={`/api/raw/?path=${getItemPath(c.name)}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
> >
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} /> <FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} />
</a> </a>
+1 -1
View File
@@ -189,7 +189,7 @@ export async function* traverseFolder(path: string): AsyncGenerator<TraverseItem
i, i,
path, path,
data: await fetcher( data: await fetcher(
next ? `/api?path=${path}&next=${next}` : `/api?path=${path}`, next ? `/api/?path=${path}&next=${next}` : `/api?path=${path}`,
hashedToken ?? undefined hashedToken ?? undefined
).catch(error => ({ i, path, error })), ).catch(error => ({ i, path, error })),
} }
+2 -2
View File
@@ -45,7 +45,7 @@ function mapAbsolutePath(path: string): string {
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}`)
// 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 => {
@@ -111,7 +111,7 @@ function SearchResultItemTemplate({
} }
function SearchResultItemLoadRemote({ result }: { result: OdSearchResult[number] }) { function SearchResultItemLoadRemote({ result }: { result: OdSearchResult[number] }) {
const { data, error }: SWRResponse<OdDriveItem, string> = useSWR(`/api/item?id=${result.id}`, fetcher) const { data, error }: SWRResponse<OdDriveItem, string> = useSWR(`/api/item/?id=${result.id}`, fetcher)
const { t } = useTranslation() const { t } = useTranslation()
+5 -3
View File
@@ -10,6 +10,7 @@ import DownloadButtonGroup from '../DownloadBtnGtoup'
import { DownloadBtnContainer, PreviewContainer } from './Containers' import { DownloadBtnContainer, PreviewContainer } from './Containers'
import { LoadingIcon } from '../Loading' import { LoadingIcon } from '../Loading'
import { formatModifiedDateTime } from '../../utils/fileDetails' import { formatModifiedDateTime } from '../../utils/fileDetails'
import { getStoredToken } from '../../utils/protectedRouteHandler'
enum PlayerState { enum PlayerState {
Loading, Loading,
@@ -21,12 +22,13 @@ enum PlayerState {
const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => { const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { asPath } = useRouter() const { asPath } = useRouter()
const hashedToken = getStoredToken(asPath)
const rapRef = useRef<ReactAudioPlayer>(null) const rapRef = useRef<ReactAudioPlayer>(null)
const [playerStatus, setPlayerStatus] = useState(PlayerState.Loading) const [playerStatus, setPlayerStatus] = useState(PlayerState.Loading)
// Render audio thumbnail, and also check for broken thumbnails // Render audio thumbnail, and also check for broken thumbnails
const thumbnail = `/api/thumbnail?path=${asPath}&size=medium` const thumbnail = `/api/thumbnail/?path=${asPath}&size=medium${hashedToken ? `&odpt=${hashedToken}` : ''}`
const [brokenThumbnail, setBrokenThumbnail] = useState(false) const [brokenThumbnail, setBrokenThumbnail] = useState(false)
useEffect(() => { useEffect(() => {
@@ -92,7 +94,7 @@ const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => {
<ReactAudioPlayer <ReactAudioPlayer
className="h-11 w-full" className="h-11 w-full"
src={file['@microsoft.graph.downloadUrl']} src={`/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
ref={rapRef} ref={rapRef}
controls controls
preload="auto" preload="auto"
@@ -102,7 +104,7 @@ const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => {
</PreviewContainer> </PreviewContainer>
<DownloadBtnContainer> <DownloadBtnContainer>
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} /> <DownloadButtonGroup />
</DownloadBtnContainer> </DownloadBtnContainer>
</> </>
) )
+5 -3
View File
@@ -1,11 +1,12 @@
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import useSystemTheme from 'react-use-system-theme' import useSystemTheme from 'react-use-system-theme'
import { useRouter } from 'next/router'
import { LightAsync as SyntaxHighlighter } from 'react-syntax-highlighter' import { LightAsync as SyntaxHighlighter } from 'react-syntax-highlighter'
import { tomorrowNightEighties, tomorrow } from 'react-syntax-highlighter/dist/cjs/styles/hljs' import { tomorrowNightEighties, tomorrow } from 'react-syntax-highlighter/dist/cjs/styles/hljs'
import useAxiosGet from '../../utils/fetchOnMount' import useFileContent from '../../utils/fetchOnMount'
import { getLanguageByFileName } from '../../utils/getPreviewType' import { getLanguageByFileName } from '../../utils/getPreviewType'
import FourOhFour from '../FourOhFour' import FourOhFour from '../FourOhFour'
import Loading from '../Loading' import Loading from '../Loading'
@@ -13,7 +14,8 @@ 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 { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl']) const { asPath } = useRouter()
const { response: content, error, validating } = useFileContent(`/api/raw/?path=${asPath}`, asPath)
const theme = useSystemTheme('dark') const theme = useSystemTheme('dark')
const { t } = useTranslation() const { t } = useTranslation()
@@ -44,7 +46,7 @@ const CodePreview: FC<{ file: any }> = ({ file }) => {
</SyntaxHighlighter> </SyntaxHighlighter>
</PreviewContainer> </PreviewContainer>
<DownloadBtnContainer> <DownloadBtnContainer>
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} /> <DownloadButtonGroup />
</DownloadBtnContainer> </DownloadBtnContainer>
</div> </div>
) )
+5 -5
View File
@@ -35,7 +35,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
<div> <div>
<div className="py-2 text-xs font-medium uppercase opacity-80">{t('MIME type')}</div> <div className="py-2 text-xs font-medium uppercase opacity-80">{t('MIME type')}</div>
<div>{file.file?.mimeType || t('Unavailable')}</div> <div>{file.file?.mimeType ?? t('Unavailable')}</div>
</div> </div>
<div> <div>
@@ -47,7 +47,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
Quick XOR Quick XOR
</td> </td>
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400"> <td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
{file.file.hashes?.quickXorHash || t('Unavailable')} {file.file.hashes?.quickXorHash ?? t('Unavailable')}
</td> </td>
</tr> </tr>
<tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900"> <tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900">
@@ -55,7 +55,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
SHA1 SHA1
</td> </td>
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400"> <td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
{file.file.hashes?.sha1Hash || t('Unavailable')} {file.file.hashes?.sha1Hash ?? t('Unavailable')}
</td> </td>
</tr> </tr>
<tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900"> <tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900">
@@ -63,7 +63,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
SHA256 SHA256
</td> </td>
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400"> <td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
{file.file.hashes?.sha256Hash || t('Unavailable')} {file.file.hashes?.sha256Hash ?? t('Unavailable')}
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -73,7 +73,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
</div> </div>
</PreviewContainer> </PreviewContainer>
<DownloadBtnContainer> <DownloadBtnContainer>
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} /> <DownloadButtonGroup />
</DownloadBtnContainer> </DownloadBtnContainer>
</div> </div>
) )
+7 -2
View File
@@ -2,13 +2,18 @@ import type { OdFileObject } from '../../types'
import { FC, useEffect, useRef, useState } from 'react' import { FC, useEffect, useRef, useState } from 'react'
import { ReactReader } from 'react-reader' import { ReactReader } from 'react-reader'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import Loading from '../Loading' import Loading from '../Loading'
import DownloadButtonGroup from '../DownloadBtnGtoup' import DownloadButtonGroup from '../DownloadBtnGtoup'
import { DownloadBtnContainer } from './Containers' import { DownloadBtnContainer } from './Containers'
import { getStoredToken } from '../../utils/protectedRouteHandler'
const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => { const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => {
const { asPath } = useRouter()
const hashedToken = getStoredToken(asPath)
const [epubContainerWidth, setEpubContainerWidth] = useState(400) const [epubContainerWidth, setEpubContainerWidth] = useState(400)
const epubContainer = useRef<HTMLDivElement>(null) const epubContainer = useRef<HTMLDivElement>(null)
@@ -51,7 +56,7 @@ const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => {
}} }}
> >
<ReactReader <ReactReader
url={file['@microsoft.graph.downloadUrl']} url={`/api/raw/?path=${asPath}${hashedToken ? '&token=' + hashedToken : ''}`}
getRendition={rendition => fixEpub(rendition)} getRendition={rendition => fixEpub(rendition)}
loadingView={<Loading loadingText={t('Loading EPUB ...')} />} loadingView={<Loading loadingText={t('Loading EPUB ...')} />}
location={location} location={location}
@@ -63,7 +68,7 @@ const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => {
</div> </div>
</div> </div>
<DownloadBtnContainer> <DownloadBtnContainer>
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} /> <DownloadButtonGroup />
</DownloadBtnContainer> </DownloadBtnContainer>
</div> </div>
) )
+7 -2
View File
@@ -1,25 +1,30 @@
import type { OdFileObject } from '../../types' import type { OdFileObject } from '../../types'
import { FC } from 'react' import { FC } from 'react'
import { useRouter } from 'next/router'
import { PreviewContainer, DownloadBtnContainer } from './Containers' import { PreviewContainer, DownloadBtnContainer } from './Containers'
import DownloadButtonGroup from '../DownloadBtnGtoup' import DownloadButtonGroup from '../DownloadBtnGtoup'
import { getStoredToken } from '../../utils/protectedRouteHandler'
const ImagePreview: FC<{ file: OdFileObject }> = ({ file }) => { const ImagePreview: FC<{ file: OdFileObject }> = ({ file }) => {
const { asPath } = useRouter()
const hashedToken = getStoredToken(asPath)
return ( return (
<> <>
<PreviewContainer> <PreviewContainer>
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
className="mx-auto" className="mx-auto"
src={file['@microsoft.graph.downloadUrl']} src={`/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
alt={file.name} alt={file.name}
width={file.image?.width} width={file.image?.width}
height={file.image?.height} height={file.image?.height}
/> />
</PreviewContainer> </PreviewContainer>
<DownloadBtnContainer> <DownloadBtnContainer>
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} /> <DownloadButtonGroup />
</DownloadBtnContainer> </DownloadBtnContainer>
</> </>
) )
+7 -7
View File
@@ -12,7 +12,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 useAxiosGet from '../../utils/fetchOnMount' import useFileContent from '../../utils/fetchOnMount'
import { DownloadBtnContainer, PreviewContainer } from './Containers' import { DownloadBtnContainer, PreviewContainer } from './Containers'
const MarkdownPreview: FC<{ const MarkdownPreview: FC<{
@@ -20,12 +20,12 @@ const MarkdownPreview: FC<{
path: string path: string
standalone?: boolean standalone?: boolean
}> = ({ file, path, standalone = true }) => { }> = ({ file, path, standalone = true }) => {
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
const { t } = useTranslation()
// 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 = standalone ? path.substring(0, path.lastIndexOf('/')) : path const parentPath = standalone ? path.substring(0, path.lastIndexOf('/')) : path
const { response: content, error, validating } = useFileContent(`/api/raw/?path=${parentPath}/${file.name}`, path)
const { t } = useTranslation()
// Check if the image is relative path instead of a absolute url // Check if the image is relative path instead of a absolute url
const isUrlAbsolute = (url: string | string[]) => url.indexOf('://') > 0 || url.indexOf('//') === 0 const isUrlAbsolute = (url: string | string[]) => url.indexOf('://') > 0 || url.indexOf('//') === 0
// Custom renderer to render images with relative path // Custom renderer to render images with relative path
@@ -55,7 +55,7 @@ const MarkdownPreview: FC<{
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
<img <img
alt={alt} alt={alt}
src={`/api?path=${parentPath}/${src}&raw=true`} src={`/api/?path=${parentPath}/${src}&raw=true`}
title={title} title={title}
width={width} width={width}
height={height} height={height}
@@ -100,7 +100,7 @@ const MarkdownPreview: FC<{
</PreviewContainer> </PreviewContainer>
{standalone && ( {standalone && (
<DownloadBtnContainer> <DownloadBtnContainer>
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} /> <DownloadButtonGroup />
</DownloadBtnContainer> </DownloadBtnContainer>
)} )}
</div> </div>
+8 -2
View File
@@ -1,12 +1,18 @@
import type { OdFileObject } from '../../types' import type { OdFileObject } from '../../types'
import { FC, useEffect, useRef, useState } from 'react' import { FC, useEffect, useRef, useState } from 'react'
import { useRouter } from 'next/router'
import Preview from 'preview-office-docs' import Preview from 'preview-office-docs'
import DownloadButtonGroup from '../DownloadBtnGtoup' import DownloadButtonGroup from '../DownloadBtnGtoup'
import { DownloadBtnContainer } from './Containers' import { DownloadBtnContainer } from './Containers'
import { getBaseUrl } from '../../utils/getBaseUrl'
import { getStoredToken } from '../../utils/protectedRouteHandler'
const OfficePreview: FC<{ file: OdFileObject }> = ({ file }) => { const OfficePreview: FC<{ file: OdFileObject }> = ({ file }) => {
const { asPath } = useRouter()
const hashedToken = getStoredToken(asPath)
const docContainer = useRef<HTMLDivElement>(null) const docContainer = useRef<HTMLDivElement>(null)
const [docContainerWidth, setDocContainerWidth] = useState(600) const [docContainerWidth, setDocContainerWidth] = useState(600)
@@ -18,13 +24,13 @@ const OfficePreview: FC<{ file: OdFileObject }> = ({ file }) => {
<div> <div>
<div className="overflow-scroll" ref={docContainer} style={{ maxHeight: '90vh' }}> <div className="overflow-scroll" ref={docContainer} style={{ maxHeight: '90vh' }}>
<Preview <Preview
url={encodeURIComponent(file['@microsoft.graph.downloadUrl'])} url={`${getBaseUrl()}/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
width={docContainerWidth.toString()} width={docContainerWidth.toString()}
height="600" height="600"
/> />
</div> </div>
<DownloadBtnContainer> <DownloadBtnContainer>
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} /> <DownloadButtonGroup />
</DownloadBtnContainer> </DownloadBtnContainer>
</div> </div>
) )
+12 -5
View File
@@ -1,11 +1,18 @@
import { useRouter } from 'next/router'
import { getBaseUrl } from '../../utils/getBaseUrl'
import { getStoredToken } from '../../utils/protectedRouteHandler'
import DownloadButtonGroup from '../DownloadBtnGtoup' import DownloadButtonGroup from '../DownloadBtnGtoup'
import { DownloadBtnContainer } from './Containers' import { DownloadBtnContainer } from './Containers'
const PDFEmbedPreview: React.FC<{ file: any }> = ({ file }) => { const PDFEmbedPreview: React.FC<{ file: any }> = ({ file }) => {
// const url = `/api/proxy?url=${encodeURIComponent(file['@microsoft.graph.downloadUrl'])}&inline=true` const { asPath } = useRouter()
const url = `https://mozilla.github.io/pdf.js/web/viewer.html?file=${encodeURIComponent( const hashedToken = getStoredToken(asPath)
file['@microsoft.graph.downloadUrl']
)}` // const url = `/api/proxy?url=${encodeURIComponent(...)}&inline=true`
const pdfPath = encodeURIComponent(
`${getBaseUrl()}/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`
)
const url = `https://mozilla.github.io/pdf.js/web/viewer.html?file=${pdfPath}`
return ( return (
<div> <div>
@@ -13,7 +20,7 @@ const PDFEmbedPreview: React.FC<{ file: any }> = ({ file }) => {
<iframe src={url} frameBorder="0" width="100%" height="100%"></iframe> <iframe src={url} frameBorder="0" width="100%" height="100%"></iframe>
</div> </div>
<DownloadBtnContainer> <DownloadBtnContainer>
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} /> <DownloadButtonGroup />
</DownloadBtnContainer> </DownloadBtnContainer>
</div> </div>
) )
+5 -3
View File
@@ -1,15 +1,17 @@
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
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 useAxiosGet from '../../utils/fetchOnMount' import useFileContent from '../../utils/fetchOnMount'
import { DownloadBtnContainer, PreviewContainer } from './Containers' import { DownloadBtnContainer, PreviewContainer } from './Containers'
const TextPreview = ({ file }) => { const TextPreview = ({ file }) => {
const { asPath } = useRouter()
const { t } = useTranslation() const { t } = useTranslation()
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl']) const { response: content, error, validating } = useFileContent(`/api/raw/?path=${asPath}`, asPath)
if (error) { if (error) {
return ( return (
<PreviewContainer> <PreviewContainer>
@@ -40,7 +42,7 @@ const TextPreview = ({ file }) => {
<pre className="overflow-x-scroll p-0 text-sm md:p-3">{content}</pre> <pre className="overflow-x-scroll p-0 text-sm md:p-3">{content}</pre>
</PreviewContainer> </PreviewContainer>
<DownloadBtnContainer> <DownloadBtnContainer>
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} /> <DownloadButtonGroup />
</DownloadBtnContainer> </DownloadBtnContainer>
</div> </div>
) )
+6 -4
View File
@@ -1,9 +1,10 @@
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
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 useAxiosGet from '../../utils/fetchOnMount' import useFileContent 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 => {
@@ -14,9 +15,10 @@ const parseDotUrl = (content: string): string | undefined => {
} }
const TextPreview = ({ file }) => { const TextPreview = ({ file }) => {
const { asPath } = useRouter()
const { t } = useTranslation() const { t } = useTranslation()
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl']) const { response: content, error, validating } = useFileContent(`/api/raw/?path=${asPath}`, asPath)
if (error) { if (error) {
return ( return (
<PreviewContainer> <PreviewContainer>
@@ -49,11 +51,11 @@ const TextPreview = ({ file }) => {
<DownloadBtnContainer> <DownloadBtnContainer>
<div className="flex justify-center"> <div className="flex justify-center">
<DownloadButton <DownloadButton
onClickCallback={() => window.open(parseDotUrl(content) || '')} onClickCallback={() => window.open(parseDotUrl(content) ?? '')}
btnColor="blue" btnColor="blue"
btnText={t('Open URL')} btnText={t('Open URL')}
btnIcon="external-link-alt" btnIcon="external-link-alt"
btnTitle={t('Open URL{{url}}', { url: ' ' + parseDotUrl(content) || '' })} btnTitle={t('Open URL{{url}}', { url: ' ' + parseDotUrl(content) ?? '' })}
/> />
</div> </div>
</DownloadBtnContainer> </DownloadBtnContainer>
+18 -10
View File
@@ -10,6 +10,7 @@ import { useAsync } from 'react-async-hook'
import { getBaseUrl } from '../../utils/getBaseUrl' import { getBaseUrl } from '../../utils/getBaseUrl'
import { getExtension } from '../../utils/getFileIcon' import { getExtension } from '../../utils/getFileIcon'
import { getReadablePath } from '../../utils/getReadablePath' import { getReadablePath } from '../../utils/getReadablePath'
import { getStoredToken } from '../../utils/protectedRouteHandler'
import { DownloadButton } from '../DownloadBtnGtoup' import { DownloadButton } from '../DownloadBtnGtoup'
import { DownloadBtnContainer, PreviewContainer } from './Containers' import { DownloadBtnContainer, PreviewContainer } from './Containers'
import FourOhFour from '../FourOhFour' import FourOhFour from '../FourOhFour'
@@ -18,16 +19,21 @@ import CustomEmbedLinkMenu from '../CustomEmbedLinkMenu'
const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => { const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
const { asPath } = useRouter() const { asPath } = useRouter()
const hashedToken = getStoredToken(asPath)
const clipboard = useClipboard() const clipboard = useClipboard()
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()
// OneDrive generates thumbnails for its video files, we pick the thumbnail with the highest resolution // OneDrive generates thumbnails for its video files, we pick the thumbnail with the highest resolution
const thumbnail = `/api/thumbnail?path=${asPath}&size=large` const thumbnail = `/api/thumbnail/?path=${asPath}&size=large${hashedToken ? `&odpt=${hashedToken}` : ''}`
// We assume subtitle files are beside the video with the same name, only webvtt '.vtt' files are supported // We assume subtitle files are beside the video with the same name, only webvtt '.vtt' files are supported
const subtitle = `/api?path=${asPath.substring(0, asPath.lastIndexOf('.'))}.vtt&raw=true` const vtt = `${asPath.substring(0, asPath.lastIndexOf('.'))}.vtt`
const subtitle = `/api/raw/?path=${vtt}${hashedToken ? `&odpt=${hashedToken}` : ''}`
// We also format the raw video file for the in-browser player as well as all other players
const videoUrl = `/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`
const isFlv = getExtension(file.name) === 'flv' const isFlv = getExtension(file.name) === 'flv'
const { const {
@@ -42,7 +48,7 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
return ( return (
<> <>
<CustomEmbedLinkMenu path={getReadablePath(asPath)} menuOpen={menuOpen} setMenuOpen={setMenuOpen} /> <CustomEmbedLinkMenu path={asPath} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
<PreviewContainer> <PreviewContainer>
{error ? ( {error ? (
<FourOhFour errorMsg={error.message} /> <FourOhFour errorMsg={error.message} />
@@ -55,7 +61,7 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
volume: 1.0, volume: 1.0,
lang: 'en', lang: 'en',
video: { video: {
url: file['@microsoft.graph.downloadUrl'], url: videoUrl,
pic: thumbnail, pic: thumbnail,
type: isFlv ? 'customFlv' : 'auto', type: isFlv ? 'customFlv' : 'auto',
customType: { customType: {
@@ -78,14 +84,14 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
<DownloadBtnContainer> <DownloadBtnContainer>
<div className="flex flex-wrap justify-center gap-2"> <div className="flex flex-wrap justify-center gap-2">
<DownloadButton <DownloadButton
onClickCallback={() => window.open(file['@microsoft.graph.downloadUrl'])} onClickCallback={() => window.open(videoUrl)}
btnColor="blue" btnColor="blue"
btnText={t('Download')} btnText={t('Download')}
btnIcon="file-download" btnIcon="file-download"
/> />
{/* <DownloadButton {/* <DownloadButton
onClickCallback={() => onClickCallback={() =>
window.open(`/api/proxy?url=${encodeURIComponent(file['@microsoft.graph.downloadUrl'])}`) window.open(`/api/proxy?url=${encodeURIComponent(...)}`)
} }
btnColor="teal" btnColor="teal"
btnText={t('Proxy download')} btnText={t('Proxy download')}
@@ -93,7 +99,9 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
/> */} /> */}
<DownloadButton <DownloadButton
onClickCallback={() => { onClickCallback={() => {
clipboard.copy(`${getBaseUrl()}/api?path=${getReadablePath(asPath)}&raw=true`) clipboard.copy(
`${getBaseUrl()}/api/raw/?path=${getReadablePath(asPath)}${hashedToken ? `&odpt=${hashedToken}` : ''}`
)
toast.success(t('Copied direct link to clipboard.')) toast.success(t('Copied direct link to clipboard.'))
}} }}
btnColor="pink" btnColor="pink"
@@ -108,17 +116,17 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
/> />
<DownloadButton <DownloadButton
onClickCallback={() => window.open(`iina://weblink?url=${file['@microsoft.graph.downloadUrl']}`)} onClickCallback={() => window.open(`iina://weblink?url=${getBaseUrl()}${videoUrl}`)}
btnText="IINA" btnText="IINA"
btnImage="/players/iina.png" btnImage="/players/iina.png"
/> />
<DownloadButton <DownloadButton
onClickCallback={() => window.open(`vlc://${file['@microsoft.graph.downloadUrl']}`)} onClickCallback={() => window.open(`vlc://${getBaseUrl()}${videoUrl}`)}
btnText="VLC" btnText="VLC"
btnImage="/players/vlc.png" btnImage="/players/vlc.png"
/> />
<DownloadButton <DownloadButton
onClickCallback={() => window.open(`potplayer://${file['@microsoft.graph.downloadUrl']}`)} onClickCallback={() => window.open(`potplayer://${getBaseUrl()}/${videoUrl}`)}
btnText="PotPlayer" btnText="PotPlayer"
btnImage="/players/potplayer.png" btnImage="/players/potplayer.png"
/> />
+5 -2
View File
@@ -29,7 +29,10 @@ module.exports = {
// unauthorised use of the proxied download feature - but that is disabled for now. So you can safely ignore this settings. // unauthorised use of the proxied download feature - but that is disabled for now. So you can safely ignore this settings.
directLinkRegex: 'public[.].*[.]files[.]1drv[.]com', directLinkRegex: 'public[.].*[.]files[.]1drv[.]com',
// Cache-Control header, check Vercel documentation for more details. // Cache-Control header, check Vercel documentation for more details. The default settings imply:
// - max-age=0: no cache for your browser
// - s-maxage=0: cache is fresh for 60 seconds on the edge, after which it becomes stale
// - stale-while-revalidate: allow serving stale content while revalidating on the edge
// https://vercel.com/docs/concepts/edge-network/caching // https://vercel.com/docs/concepts/edge-network/caching
cacheControlHeader: 'max-age=0, s-maxage=3540, stale-while-revalidate=60' cacheControlHeader: 'max-age=0, s-maxage=60, stale-while-revalidate',
} }
+81 -67
View File
@@ -2,31 +2,17 @@ import { posix as pathPosix } from 'path'
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
import axios from 'axios' import axios from 'axios'
import Cors from 'cors'
import apiConfig from '../../config/api.config' import apiConfig from '../../config/api.config'
import siteConfig from '../../config/site.config' import siteConfig from '../../config/site.config'
import { revealObfuscatedToken } from '../../utils/oAuthHandler' import { revealObfuscatedToken } from '../../utils/oAuthHandler'
import { compareHashedToken } from '../../utils/protectedRouteHandler' import { compareHashedToken } from '../../utils/protectedRouteHandler'
import { getOdAuthTokens, storeOdAuthTokens } from '../../utils/odAuthTokenStore' import { getOdAuthTokens, storeOdAuthTokens } from '../../utils/odAuthTokenStore'
import { runCorsMiddleware } from './raw'
const basePath = pathPosix.resolve('/', siteConfig.baseDirectory) const basePath = pathPosix.resolve('/', siteConfig.baseDirectory)
const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret) const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
// CORS middleware for raw links: https://nextjs.org/docs/api-routes/api-middlewares
function runCorsMiddleware(req: NextApiRequest, res: NextApiResponse) {
const cors = Cors({ methods: ['GET', 'HEAD'] })
return new Promise((resolve, reject) => {
cors(req, res, result => {
if (result instanceof Error) {
return reject(result)
}
return resolve(result)
})
})
}
/** /**
* Encode the path of the file relative to the base directory * Encode the path of the file relative to the base directory
* *
@@ -107,6 +93,64 @@ export function getAuthTokenPath(path: string) {
return authTokenPath return authTokenPath
} }
/**
* Handles protected route authentication:
* - Match the cleanPath against an array of user defined protected routes
* - If a match is found:
* - 1. Download the .password file stored inside the protected route and parse its contents
* - 2. Check if the od-protected-token header is present in the request
* - The request is continued only if these two contents are exactly the same
*
* @param cleanPath Sanitised directory path, used for matching whether route is protected
* @param accessToken OneDrive API access token
* @param req Next.js request object
* @param res Next.js response object
*/
export async function checkAuthRoute(
cleanPath: string,
accessToken: string,
odTokenHeader: string
): Promise<{ code: 200 | 401 | 404 | 500; message: string }> {
// Handle authentication through .password
const authTokenPath = getAuthTokenPath(cleanPath)
// Fetch password from remote file content
if (authTokenPath === '') {
return { code: 200, message: '' }
}
try {
const token = await axios.get(`${apiConfig.driveApi}/root${encodePath(authTokenPath)}`, {
headers: { Authorization: `Bearer ${accessToken}` },
params: {
select: '@microsoft.graph.downloadUrl,file',
},
})
// Handle request and check for header 'od-protected-token'
const odProtectedToken = await axios.get(token.data['@microsoft.graph.downloadUrl'])
// console.log(odTokenHeader, odProtectedToken.data.trim())
if (
!compareHashedToken({
odTokenHeader: odTokenHeader,
dotPassword: odProtectedToken.data,
})
) {
return { code: 401, message: 'Password required.' }
}
} catch (error: any) {
// Password file not found, fallback to 404
if (error?.response?.status === 404) {
return { code: 404, message: "You didn't set a password." }
} else {
return { code: 500, message: 'Internal server error.' }
}
}
return { code: 200, message: 'Authenticated.' }
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// If method is POST, then the API is called by the client to store acquired tokens // If method is POST, then the API is called by the client to store acquired tokens
if (req.method === 'POST') { if (req.method === 'POST') {
@@ -119,11 +163,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return return
} }
await storeOdAuthTokens({ await storeOdAuthTokens({ accessToken, accessTokenExpiry, refreshToken })
accessToken,
accessTokenExpiry,
refreshToken,
})
res.status(200).send('OK') res.status(200).send('OK')
return return
} }
@@ -155,43 +195,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return return
} }
// Handle authentication through .password // Handle protected routes authentication
const authTokenPath = getAuthTokenPath(cleanPath) const { code, message } = await checkAuthRoute(cleanPath, accessToken, req.headers['od-protected-token'] as string)
// Status code other than 200 means user has not authenticated yet
// Fetch password from remote file content if (code !== 200) {
if (authTokenPath !== '') { res.status(code).json({ error: message })
// Don't server cached response for password protected folders return
}
// If message is empty, then the path is not protected.
// Conversely, protected routes are not allowed to serve from cache.
if (message !== '') {
res.setHeader('Cache-Control', 'no-cache') res.setHeader('Cache-Control', 'no-cache')
try {
const token = await axios.get(`${apiConfig.driveApi}/root${encodePath(authTokenPath)}`, {
headers: { Authorization: `Bearer ${accessToken}` },
params: {
select: '@microsoft.graph.downloadUrl,file',
},
})
// Handle request and check for header 'od-protected-token'
const odProtectedToken = await axios.get(token.data['@microsoft.graph.downloadUrl'])
// console.log(req.headers['od-protected-token'], odProtectedToken.data.trim())
if (
!compareHashedToken({
odTokenHeader: req.headers['od-protected-token'] as string,
dotPassword: odProtectedToken.data,
})
) {
res.status(401).json({ error: 'Password required for this folder.' })
return
}
} catch (error: any) {
// Password file not found, fallback to 404
if (error.response.status === 404) {
res.status(404).json({ error: "You didn't set a password for your protected folder." })
}
res.status(500).end()
return
}
} }
const requestPath = encodePath(cleanPath) const requestPath = encodePath(cleanPath)
@@ -201,24 +215,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const isRoot = requestPath === '' const isRoot = requestPath === ''
// Go for file raw download link, add CORS headers, and redirect to @microsoft.graph.downloadUrl // Go for file raw download link, add CORS headers, and redirect to @microsoft.graph.downloadUrl
// (kept here for backwards compatibility, and cache headers will be reverted to no-cache)
if (raw) { if (raw) {
await runCorsMiddleware(req, res) await runCorsMiddleware(req, res)
res.setHeader('Cache-Control', 'no-cache')
const { data } = await axios.get(requestUrl, { const { data } = await axios.get(requestUrl, {
headers: { Authorization: `Bearer ${accessToken}` }, headers: { Authorization: `Bearer ${accessToken}` },
params: { params: {
select: '@microsoft.graph.downloadUrl,folder,file', select: '@microsoft.graph.downloadUrl',
}, },
}) })
if ('folder' in data) { if ('@microsoft.graph.downloadUrl' in data) {
res.status(400).json({ error: "Folders doesn't have raw download urls." })
return
}
if ('file' in data) {
res.redirect(data['@microsoft.graph.downloadUrl']) res.redirect(data['@microsoft.graph.downloadUrl'])
return } else {
res.status(404).json({ error: 'No download url found.' })
} }
return
} }
// Querying current path identity (file or folder) and follow up query childrens in folder // Querying current path identity (file or folder) and follow up query childrens in folder
@@ -226,7 +240,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { data: identityData } = await axios.get(requestUrl, { const { data: identityData } = await axios.get(requestUrl, {
headers: { Authorization: `Bearer ${accessToken}` }, headers: { Authorization: `Bearer ${accessToken}` },
params: { params: {
select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file,video,image', select: 'name,size,id,lastModifiedDateTime,folder,file,video,image',
}, },
}) })
@@ -235,12 +249,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
headers: { Authorization: `Bearer ${accessToken}` }, headers: { Authorization: `Bearer ${accessToken}` },
params: next params: next
? { ? {
select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file,video,image', select: 'name,size,id,lastModifiedDateTime,folder,file,video,image',
top: siteConfig.maxItems, top: siteConfig.maxItems,
$skipToken: next, $skipToken: next,
} }
: { : {
select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file,video,image', select: 'name,size,id,lastModifiedDateTime,folder,file,video,image',
top: siteConfig.maxItems, top: siteConfig.maxItems,
}, },
}) })
@@ -261,7 +275,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.status(200).json({ file: identityData }) res.status(200).json({ file: identityData })
return return
} catch (error: any) { } catch (error: any) {
res.status(error.response.status).json({ error: error.response.data }) res.status(error?.response?.code ?? 500).json({ error: error?.response?.data ?? 'Internal server error.' })
return return
} }
} }
+1 -1
View File
@@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}) })
res.status(200).json(data) res.status(200).json(data)
} catch (error: any) { } catch (error: any) {
res.status(error.response.status).json({ error: error.response.data }) res.status(error?.response?.status ?? 500).json({ error: error?.response?.data ?? 'Internal server error.' })
} }
} else { } else {
res.status(400).json({ error: 'Invalid driveItem ID.' }) res.status(400).json({ error: 'Invalid driveItem ID.' })
+2 -2
View File
@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
import { default as indexHandler } from '..' import { default as rawFileHandler } from '../raw'
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
indexHandler(req, res) rawFileHandler(req, res)
} }
+81
View File
@@ -0,0 +1,81 @@
import { posix as pathPosix } from 'path'
import type { NextApiRequest, NextApiResponse } from 'next'
import axios from 'axios'
import Cors from 'cors'
import { driveApi } from '../../config/api.config'
import { encodePath, getAccessToken, checkAuthRoute } from '.'
// CORS middleware for raw links: https://nextjs.org/docs/api-routes/api-middlewares
export function runCorsMiddleware(req: NextApiRequest, res: NextApiResponse) {
const cors = Cors({ methods: ['GET', 'HEAD'] })
return new Promise((resolve, reject) => {
cors(req, res, result => {
if (result instanceof Error) {
return reject(result)
}
return resolve(result)
})
})
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const accessToken = await getAccessToken()
if (!accessToken) {
res.status(403).json({ error: 'No access token.' })
return
}
const { path = '/', odpt = '' } = req.query
// Sometimes the path parameter is defaulted to '[...path]' which we need to handle
if (path === '[...path]') {
res.status(400).json({ error: 'No path specified.' })
return
}
// If the path is not a valid path, return 400
if (typeof path !== 'string') {
res.status(400).json({ error: 'Path query invalid.' })
return
}
const cleanPath = pathPosix.resolve('/', pathPosix.normalize(path))
// Handle protected routes authentication
const odTokenHeader = (req.headers['od-protected-token'] as string) ?? odpt
const { code, message } = await checkAuthRoute(cleanPath, accessToken, odTokenHeader)
// Status code other than 200 means user has not authenticated yet
if (code !== 200) {
res.status(code).json({ error: message })
return
}
// If message is empty, then the path is not protected.
// Conversely, protected routes are not allowed to serve from cache.
if (message !== '') {
res.setHeader('Cache-Control', 'no-cache')
}
await runCorsMiddleware(req, res)
try {
// Handle response from OneDrive API
const requestUrl = `${driveApi}/root${encodePath(cleanPath)}`
const { data } = await axios.get(requestUrl, {
headers: { Authorization: `Bearer ${accessToken}` },
params: {
select: '@microsoft.graph.downloadUrl',
},
})
if ('@microsoft.graph.downloadUrl' in data) {
res.redirect(data['@microsoft.graph.downloadUrl'])
} else {
res.status(404).json({ error: 'No download url found.' })
}
return
} catch (error: any) {
res.status(error?.response?.status ?? 500).json({ error: error?.response?.data ?? 'Internal server error.' })
return
}
}
+1 -1
View File
@@ -53,7 +53,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}) })
res.status(200).json(data.value) res.status(200).json(data.value)
} catch (error: any) { } catch (error: any) {
res.status(error.response.status).json({ error: error.response.data }) res.status(error?.response?.status ?? 500).json({ error: error?.response?.data ?? 'Internal server error.' })
} }
} else { } else {
res.status(200).json([]) res.status(200).json([])
+18 -12
View File
@@ -5,19 +5,22 @@ import { posix as pathPosix } from 'path'
import axios from 'axios' import axios from 'axios'
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
import { encodePath, getAccessToken, getAuthTokenPath } from '.' import { checkAuthRoute, encodePath, getAccessToken } from '.'
import apiConfig from '../../config/api.config' import apiConfig from '../../config/api.config'
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Get access token from storage
const accessToken = await getAccessToken() const accessToken = await getAccessToken()
if (!accessToken) {
res.status(403).json({ error: 'No access token.' })
return
}
// Get item thumbnails by its path since we will later check if it is protected // Get item thumbnails by its path since we will later check if it is protected
const { path = '', size = 'medium' } = req.query const { path = '', size = 'medium', odpt = '' } = req.query
// Set edge function caching for faster load times, check docs: // Set edge function caching for faster load times, if route is not protected, check docs:
// https://vercel.com/docs/concepts/functions/edge-caching // https://vercel.com/docs/concepts/functions/edge-caching
res.setHeader('Cache-Control', apiConfig.cacheControlHeader) if (odpt === '') res.setHeader('Cache-Control', apiConfig.cacheControlHeader)
// Check whether the size is valid - must be one of 'large', 'medium', or 'small' // Check whether the size is valid - must be one of 'large', 'medium', or 'small'
if (size !== 'large' && size !== 'medium' && size !== 'small') { if (size !== 'large' && size !== 'medium' && size !== 'small') {
@@ -36,14 +39,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
const cleanPath = pathPosix.resolve('/', pathPosix.normalize(path)) const cleanPath = pathPosix.resolve('/', pathPosix.normalize(path))
// Check if the path is protected const { code, message } = await checkAuthRoute(cleanPath, accessToken, odpt as string)
const authTokenPath = getAuthTokenPath(cleanPath) // Status code other than 200 means user has not authenticated yet
if (code !== 200) {
// Currently protected paths are rejected to avoid file content leak res.status(code).json({ error: message })
if (authTokenPath) {
res.status(404).json({ error: 'Protected pathes are not allowed.' })
return return
} }
// If message is empty, then the path is not protected.
// Conversely, protected routes are not allowed to serve from cache.
if (message !== '') {
res.setHeader('Cache-Control', 'no-cache')
}
const requestPath = encodePath(cleanPath) const requestPath = encodePath(cleanPath)
// Handle response from OneDrive API // Handle response from OneDrive API
@@ -63,7 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.status(400).json({ error: "The item doesn't have a valid thumbnail." }) res.status(400).json({ error: "The item doesn't have a valid thumbnail." })
} }
} catch (error: any) { } catch (error: any) {
res.status(error.response.status).json({ error: error.response.data }) res.status(error?.response?.status).json({ error: error?.response?.data ?? 'Internal server error.' })
} }
return return
} }
+1 -1
View File
@@ -103,7 +103,7 @@ export default function OAuthStep2() {
<p className="py-1">{t('The authorisation code extracted is:')}</p> <p className="py-1">{t('The authorisation code extracted is:')}</p>
<p className="my-2 overflow-hidden truncate rounded border border-gray-400/20 bg-gray-50 p-2 font-mono text-sm opacity-80 dark:bg-gray-800"> <p className="my-2 overflow-hidden truncate rounded border border-gray-400/20 bg-gray-50 p-2 font-mono text-sm opacity-80 dark:bg-gray-800">
{authCode || <span className="animate-pulse">{t('Waiting for code...')}</span>} {authCode ?? <span className="animate-pulse">{t('Waiting for code...')}</span>}
</p> </p>
<p> <p>
+3 -9
View File
@@ -1,4 +1,4 @@
// API response object for /api?path=<path_to_file_or_folder>, this may return either a file or a folder. // API response object for /api/?path=<path_to_file_or_folder>, this may return either a file or a folder.
// Pagination is also declared here with the 'next' parameter. // Pagination is also declared here with the 'next' parameter.
export type OdAPIResponse = { file?: OdFileObject; folder?: OdFolderObject; next?: string } export type OdAPIResponse = { file?: OdFileObject; folder?: OdFolderObject; next?: string }
// A folder object returned from the OneDrive API. This contains the parameter 'value', which is an array of items // A folder object returned from the OneDrive API. This contains the parameter 'value', which is an array of items
@@ -8,7 +8,6 @@ export type OdFolderObject = {
'@odata.context': string '@odata.context': string
'@odata.nextLink'?: string '@odata.nextLink'?: string
value: Array<{ value: Array<{
'@microsoft.graph.downloadUrl': string
id: string id: string
name: string name: string
size: number size: number
@@ -17,14 +16,11 @@ export type OdFolderObject = {
folder?: { childCount: number; view: { sortBy: string; sortOrder: 'ascending'; viewType: 'thumbnails' } } folder?: { childCount: number; view: { sortBy: string; sortOrder: 'ascending'; viewType: 'thumbnails' } }
image?: OdImageFile image?: OdImageFile
video?: OdVideoFile video?: OdVideoFile
// 'thumbnails@odata.context'?: string
// thumbnails?: Array<OdThumbnail>
}> }>
} }
export type OdFolderChildren = OdFolderObject['value'][number] export type OdFolderChildren = OdFolderObject['value'][number]
// A file object returned from the OneDrive API. This object may contain 'video' if the file is a video. // A file object returned from the OneDrive API. This object may contain 'video' if the file is a video.
export type OdFileObject = { export type OdFileObject = {
'@microsoft.graph.downloadUrl': string
'@odata.context': string '@odata.context': string
name: string name: string
size: number size: number
@@ -33,8 +29,6 @@ export type OdFileObject = {
file: { mimeType: string; hashes: { quickXorHash: string; sha1Hash?: string; sha256Hash?: string } } file: { mimeType: string; hashes: { quickXorHash: string; sha1Hash?: string; sha256Hash?: string } }
image?: OdImageFile image?: OdImageFile
video?: OdVideoFile video?: OdVideoFile
// 'thumbnails@odata.context'?: string
// thumbnails?: Array<OdThumbnail>
} }
// A representation of a OneDrive image file. Some images do not return a width and height, so types are optional. // A representation of a OneDrive image file. Some images do not return a width and height, so types are optional.
export type OdImageFile = { export type OdImageFile = {
@@ -59,7 +53,7 @@ export type OdThumbnail = {
medium: { height: number; width: number; url: string } medium: { height: number; width: number; url: string }
small: { height: number; width: number; url: string } small: { height: number; width: number; url: string }
} }
// API response object for /api/search?q=<query>. Likewise, this array of items may also contain either files or folders. // API response object for /api/search/?q=<query>. Likewise, this array of items may also contain either files or folders.
export type OdSearchResult = Array<{ export type OdSearchResult = Array<{
id: string id: string
name: string name: string
@@ -68,7 +62,7 @@ export type OdSearchResult = Array<{
path: string path: string
parentReference: { id: string; name: string; path: string } parentReference: { id: string; name: string; path: string }
}> }>
// API response object for /api/item?id={id}. This is primarily used for determining the path of the driveItem by ID. // API response object for /api/item/?id={id}. This is primarily used for determining the path of the driveItem by ID.
export type OdDriveItem = { export type OdDriveItem = {
'@odata.context': string '@odata.context': string
'@odata.etag': string '@odata.etag': string
+16 -7
View File
@@ -1,22 +1,31 @@
import axios from 'axios' import axios from 'axios'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { getStoredToken } from './protectedRouteHandler'
// Custom hook to axios get a URL or API endpoint on mount /**
export default function useAxiosGet(fetchUrl: string): { response: any; error: string; validating: boolean } { * Custom hook for axios to fetch raw file content on component mount
* @param fetchUrl The URL pointing to the raw file content
* @param path The path of the file, used for determining whether path is protected
*/
export default function useFileContent(
fetchUrl: string,
path: string
): { response: any; error: string; validating: boolean } {
const [response, setResponse] = 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(() => {
const hashedToken = getStoredToken(path)
const url = fetchUrl + (hashedToken ? `&odpt=${hashedToken}` : '')
axios axios
// Using 'blob' as response type to get the response as a raw file blob, which is later parsed as a string. // Using 'blob' as response type to get the response as a raw file blob, which is later parsed as a string.
// Axios defaults response parsing to JSON, which causes issues when parsing JSON files. // Axios defaults response parsing to JSON, which causes issues when parsing JSON files.
.get(fetchUrl, { responseType: 'blob' }) .get(url, { responseType: 'blob' })
.then(async res => setResponse(await res.data.text())) .then(async res => setResponse(await res.data.text()))
.catch(e => setError(e.message)) .catch(e => setError(e.message))
.finally(() => { .finally(() => setValidating(false))
setValidating(false) }, [fetchUrl, path])
})
}, [fetchUrl])
return { response, error, validating } return { response, error, validating }
} }
+2 -2
View File
@@ -40,10 +40,10 @@ export function useProtectedSWRInfinite(path: string = '') {
if (previousPageData && !previousPageData.folder) return null if (previousPageData && !previousPageData.folder) return null
// First page with no prevPageData // First page with no prevPageData
if (pageIndex === 0) return [`/api?path=${path}`, hashedToken] if (pageIndex === 0) return [`/api/?path=${path}`, hashedToken]
// Add nextPage token to API endpoint // Add nextPage token to API endpoint
return [`/api?path=${path}&next=${previousPageData.next}`, hashedToken] return [`/api/?path=${path}&next=${previousPageData.next}`, hashedToken]
} }
// Disable auto-revalidate, these options are equivalent to useSWRImmutable // Disable auto-revalidate, these options are equivalent to useSWRImmutable
+1 -1
View File
@@ -43,7 +43,7 @@ export function extractAuthCodeFromRedirected(url: string): string {
// New URL search parameter // New URL search parameter
const params = new URLSearchParams(url.split('?')[1]) const params = new URLSearchParams(url.split('?')[1])
return params.get('code') || '' return params.get('code') ?? ''
} }
// After a successful authorisation, the code returned from the Microsoft OAuth 2.0 authorization URL // After a successful authorisation, the code returned from the Microsoft OAuth 2.0 authorization URL