mirror of
https://github.com/Nezumi-2711/onedrive-vercel-index.git
synced 2026-09-22 13:38:45 +00:00
Standalone raw file redirects (#428)
This commit is contained in:
@@ -6,6 +6,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { useClipboard } from 'use-clipboard-copy'
|
||||
|
||||
import { getBaseUrl } from '../utils/getBaseUrl'
|
||||
import { getStoredToken } from '../utils/protectedRouteHandler'
|
||||
import { getReadablePath } from '../utils/getReadablePath'
|
||||
|
||||
export default function CustomEmbedLinkMenu({
|
||||
path,
|
||||
@@ -18,6 +20,9 @@ export default function CustomEmbedLinkMenu({
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const clipboard = useClipboard()
|
||||
|
||||
const hashedToken = getStoredToken(path)
|
||||
|
||||
const closeMenu = () => setMenuOpen(false)
|
||||
|
||||
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>
|
||||
<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>
|
||||
<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">
|
||||
<span>{`${getBaseUrl()}/api/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>
|
||||
|
||||
@@ -91,7 +96,11 @@ export default function CustomEmbedLinkMenu({
|
||||
<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"
|
||||
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.'))
|
||||
closeMenu()
|
||||
}}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useRouter } from 'next/router'
|
||||
|
||||
import { getBaseUrl } from '../utils/getBaseUrl'
|
||||
import { getReadablePath } from '../utils/getReadablePath'
|
||||
import { getStoredToken } from '../utils/protectedRouteHandler'
|
||||
import CustomEmbedLinkMenu from './CustomEmbedLinkMenu'
|
||||
|
||||
const btnStyleMap = (btnColor?: string) => {
|
||||
@@ -62,8 +63,10 @@ export const DownloadButton = ({
|
||||
)
|
||||
}
|
||||
|
||||
const DownloadButtonGroup: React.FC<{ downloadUrl: string }> = ({ downloadUrl }) => {
|
||||
const DownloadButtonGroup = () => {
|
||||
const { asPath } = useRouter()
|
||||
const hashedToken = getStoredToken(asPath)
|
||||
|
||||
const clipboard = useClipboard()
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
@@ -74,22 +77,24 @@ const DownloadButtonGroup: React.FC<{ downloadUrl: string }> = ({ downloadUrl })
|
||||
<CustomEmbedLinkMenu menuOpen={menuOpen} setMenuOpen={setMenuOpen} path={asPath} />
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
<DownloadButton
|
||||
onClickCallback={() => window.open(downloadUrl)}
|
||||
onClickCallback={() => window.open(`/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`)}
|
||||
btnColor="blue"
|
||||
btnText={t('Download')}
|
||||
btnIcon="file-download"
|
||||
btnTitle={t('Download the file directly through OneDrive')}
|
||||
/>
|
||||
{/* <DownloadButton
|
||||
onClickCallback={() => window.open(`/api/proxy?url=${encodeURIComponent(downloadUrl)}`)}
|
||||
btnColor="teal"
|
||||
btnText={t('Proxy download')}
|
||||
btnIcon="download"
|
||||
btnTitle={t('Download the file with the stream proxied through Vercel Serverless')}
|
||||
/> */}
|
||||
onClickCallback={() => window.open(`/api/proxy?url=${encodeURIComponent(downloadUrl)}`)}
|
||||
btnColor="teal"
|
||||
btnText={t('Proxy download')}
|
||||
btnIcon="download"
|
||||
btnTitle={t('Download the file with the stream proxied through Vercel Serverless')}
|
||||
/> */}
|
||||
<DownloadButton
|
||||
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.'))
|
||||
}}
|
||||
btnColor="pink"
|
||||
|
||||
@@ -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 toast, { Toaster } from 'react-hot-toast'
|
||||
import emojiRegex from 'emoji-regex'
|
||||
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
import { FC, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
@@ -13,6 +13,7 @@ import useLocalStorage from '../utils/useLocalStorage'
|
||||
import { getPreviewType, preview } from '../utils/getPreviewType'
|
||||
import { useProtectedSWRInfinite } from '../utils/fetchWithSWR'
|
||||
import { getExtension, getFileIcon } from '../utils/getFileIcon'
|
||||
import { getStoredToken } from '../utils/protectedRouteHandler'
|
||||
import {
|
||||
DownloadingToast,
|
||||
downloadMultipleFiles,
|
||||
@@ -36,7 +37,6 @@ import ImagePreview from './previews/ImagePreview'
|
||||
import DefaultPreview from './previews/DefaultPreview'
|
||||
import { PreviewContainer } from './previews/Containers'
|
||||
|
||||
import type { OdFileObject, OdFolderChildren, OdFolderObject } from '../types'
|
||||
import FolderListLayout from './FolderListLayout'
|
||||
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 (
|
||||
<span title={title} className="rounded p-2" role="status">
|
||||
<span title={title} className={`${style} rounded`} role="status">
|
||||
<LoadingIcon
|
||||
// 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
|
||||
@@ -155,6 +155,7 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
||||
}>({})
|
||||
|
||||
const router = useRouter()
|
||||
const hashedToken = getStoredToken(router.asPath)
|
||||
const [layout, _] = useLocalStorage('preferredLayout', layouts[0])
|
||||
|
||||
const { t } = useTranslation()
|
||||
@@ -237,7 +238,10 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
||||
const folder = folderName ? decodeURIComponent(folderName) : undefined
|
||||
const files = getFiles()
|
||||
.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) {
|
||||
const el = document.createElement('a')
|
||||
@@ -278,9 +282,10 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
||||
)
|
||||
continue
|
||||
}
|
||||
const hashedTokenForPath = getStoredToken(p)
|
||||
yield {
|
||||
name: c?.name,
|
||||
url: c ? c['@microsoft.graph.downloadUrl'] : undefined,
|
||||
url: `/api/raw/?path=${p}${hashedTokenForPath ? `&odpt=${hashedTokenForPath}` : ''}`,
|
||||
path: p,
|
||||
isFolder,
|
||||
}
|
||||
|
||||
@@ -10,10 +10,13 @@ import { getBaseUrl } from '../utils/getBaseUrl'
|
||||
import { formatModifiedDateTime } from '../utils/fileDetails'
|
||||
import { getReadablePath } from '../utils/getReadablePath'
|
||||
import { Checkbox, ChildIcon, ChildName, Downloading } from './FileListing'
|
||||
import { getStoredToken } from '../utils/protectedRouteHandler'
|
||||
|
||||
const GridItem = ({ c, path }: { c: OdFolderChildren; path: string }) => {
|
||||
// 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
|
||||
const [brokenThumbnail, setBrokenThumbnail] = useState(false)
|
||||
@@ -66,6 +69,7 @@ const FolderGridLayout = ({
|
||||
toast,
|
||||
}) => {
|
||||
const clipboard = useClipboard()
|
||||
const hashedToken = getStoredToken(path)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -84,7 +88,7 @@ const FolderGridLayout = ({
|
||||
title={t('Select all files')}
|
||||
/>
|
||||
{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
|
||||
title={t('Download selected files')}
|
||||
@@ -118,7 +122,7 @@ const FolderGridLayout = ({
|
||||
<FontAwesomeIcon icon={['far', 'copy']} />
|
||||
</span>
|
||||
{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
|
||||
title={t('Download folder')}
|
||||
@@ -135,7 +139,11 @@ const FolderGridLayout = ({
|
||||
title={t('Copy raw file permalink')}
|
||||
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||
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.'))
|
||||
}}
|
||||
>
|
||||
@@ -144,7 +152,9 @@ const FolderGridLayout = ({
|
||||
<a
|
||||
title={t('Download file')}
|
||||
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']} />
|
||||
</a>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { humanFileSize, formatModifiedDateTime } from '../utils/fileDetails'
|
||||
import { getReadablePath } from '../utils/getReadablePath'
|
||||
|
||||
import { Downloading, Checkbox, ChildIcon, ChildName } from './FileListing'
|
||||
import { getStoredToken } from '../utils/protectedRouteHandler'
|
||||
|
||||
const FileListItem: FC<{ fileContent: OdFolderChildren }> = ({ fileContent: c }) => {
|
||||
return (
|
||||
@@ -45,9 +46,13 @@ const FolderListLayout = ({
|
||||
toast,
|
||||
}) => {
|
||||
const clipboard = useClipboard()
|
||||
const hashedToken = getStoredToken(path)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Get item path from item name
|
||||
const getItemPath = (name: string) => `${path === '/' ? '' : path}/${encodeURIComponent(name)}`
|
||||
|
||||
return (
|
||||
<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">
|
||||
@@ -72,7 +77,7 @@ const FolderListLayout = ({
|
||||
title={t('Select files')}
|
||||
/>
|
||||
{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
|
||||
title={t('Download selected files')}
|
||||
@@ -113,7 +118,7 @@ const FolderListLayout = ({
|
||||
<FontAwesomeIcon icon={['far', 'copy']} />
|
||||
</span>
|
||||
{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
|
||||
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"
|
||||
onClick={() => {
|
||||
clipboard.copy(
|
||||
`${getBaseUrl()}/api?path=${getReadablePath(
|
||||
`${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`
|
||||
)}&raw=true`
|
||||
`${getBaseUrl()}/api/raw/?path=${getReadablePath(getItemPath(c.name))}${
|
||||
hashedToken ? `&odpt=${hashedToken}` : ''
|
||||
}`
|
||||
)
|
||||
toast.success(t('Copied raw file permalink.'))
|
||||
}}
|
||||
@@ -146,7 +151,7 @@ const FolderListLayout = ({
|
||||
<a
|
||||
title={t('Download file')}
|
||||
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']} />
|
||||
</a>
|
||||
|
||||
@@ -189,7 +189,7 @@ export async function* traverseFolder(path: string): AsyncGenerator<TraverseItem
|
||||
i,
|
||||
path,
|
||||
data: await fetcher(
|
||||
next ? `/api?path=${path}&next=${next}` : `/api?path=${path}`,
|
||||
next ? `/api/?path=${path}&next=${next}` : `/api?path=${path}`,
|
||||
hashedToken ?? undefined
|
||||
).catch(error => ({ i, path, error })),
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ function mapAbsolutePath(path: string): string {
|
||||
function useDriveItemSearch() {
|
||||
const [query, setQuery] = useState('')
|
||||
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
|
||||
data.map(item => {
|
||||
@@ -111,7 +111,7 @@ function SearchResultItemTemplate({
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||
import { LoadingIcon } from '../Loading'
|
||||
import { formatModifiedDateTime } from '../../utils/fileDetails'
|
||||
import { getStoredToken } from '../../utils/protectedRouteHandler'
|
||||
|
||||
enum PlayerState {
|
||||
Loading,
|
||||
@@ -21,12 +22,13 @@ enum PlayerState {
|
||||
const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
const { t } = useTranslation()
|
||||
const { asPath } = useRouter()
|
||||
const hashedToken = getStoredToken(asPath)
|
||||
|
||||
const rapRef = useRef<ReactAudioPlayer>(null)
|
||||
const [playerStatus, setPlayerStatus] = useState(PlayerState.Loading)
|
||||
|
||||
// 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)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -92,7 +94,7 @@ const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
|
||||
<ReactAudioPlayer
|
||||
className="h-11 w-full"
|
||||
src={file['@microsoft.graph.downloadUrl']}
|
||||
src={`/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
|
||||
ref={rapRef}
|
||||
controls
|
||||
preload="auto"
|
||||
@@ -102,7 +104,7 @@ const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
</PreviewContainer>
|
||||
|
||||
<DownloadBtnContainer>
|
||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
||||
<DownloadButtonGroup />
|
||||
</DownloadBtnContainer>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import useSystemTheme from 'react-use-system-theme'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { LightAsync as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
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 FourOhFour from '../FourOhFour'
|
||||
import Loading from '../Loading'
|
||||
@@ -13,7 +14,8 @@ import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||
|
||||
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 { t } = useTranslation()
|
||||
@@ -44,7 +46,7 @@ const CodePreview: FC<{ file: any }> = ({ file }) => {
|
||||
</SyntaxHighlighter>
|
||||
</PreviewContainer>
|
||||
<DownloadBtnContainer>
|
||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
||||
<DownloadButtonGroup />
|
||||
</DownloadBtnContainer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
|
||||
<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>
|
||||
@@ -47,7 +47,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
Quick XOR
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
<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
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
<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
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -73,7 +73,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
</div>
|
||||
</PreviewContainer>
|
||||
<DownloadBtnContainer>
|
||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
||||
<DownloadButtonGroup />
|
||||
</DownloadBtnContainer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,13 +2,18 @@ import type { OdFileObject } from '../../types'
|
||||
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { ReactReader } from 'react-reader'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import Loading from '../Loading'
|
||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||
import { DownloadBtnContainer } from './Containers'
|
||||
import { getStoredToken } from '../../utils/protectedRouteHandler'
|
||||
|
||||
const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
const { asPath } = useRouter()
|
||||
const hashedToken = getStoredToken(asPath)
|
||||
|
||||
const [epubContainerWidth, setEpubContainerWidth] = useState(400)
|
||||
const epubContainer = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -51,7 +56,7 @@ const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
}}
|
||||
>
|
||||
<ReactReader
|
||||
url={file['@microsoft.graph.downloadUrl']}
|
||||
url={`/api/raw/?path=${asPath}${hashedToken ? '&token=' + hashedToken : ''}`}
|
||||
getRendition={rendition => fixEpub(rendition)}
|
||||
loadingView={<Loading loadingText={t('Loading EPUB ...')} />}
|
||||
location={location}
|
||||
@@ -63,7 +68,7 @@ const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
</div>
|
||||
</div>
|
||||
<DownloadBtnContainer>
|
||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
||||
<DownloadButtonGroup />
|
||||
</DownloadBtnContainer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import type { OdFileObject } from '../../types'
|
||||
|
||||
import { FC } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { PreviewContainer, DownloadBtnContainer } from './Containers'
|
||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||
import { getStoredToken } from '../../utils/protectedRouteHandler'
|
||||
|
||||
const ImagePreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
const { asPath } = useRouter()
|
||||
const hashedToken = getStoredToken(asPath)
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreviewContainer>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
className="mx-auto"
|
||||
src={file['@microsoft.graph.downloadUrl']}
|
||||
src={`/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
|
||||
alt={file.name}
|
||||
width={file.image?.width}
|
||||
height={file.image?.height}
|
||||
/>
|
||||
</PreviewContainer>
|
||||
<DownloadBtnContainer>
|
||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
||||
<DownloadButtonGroup />
|
||||
</DownloadBtnContainer>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'katex/dist/katex.min.css'
|
||||
import FourOhFour from '../FourOhFour'
|
||||
import Loading from '../Loading'
|
||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||
import useAxiosGet from '../../utils/fetchOnMount'
|
||||
import useFileContent from '../../utils/fetchOnMount'
|
||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||
|
||||
const MarkdownPreview: FC<{
|
||||
@@ -20,12 +20,12 @@ const MarkdownPreview: FC<{
|
||||
path: string
|
||||
standalone?: boolean
|
||||
}> = ({ 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
|
||||
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
|
||||
const isUrlAbsolute = (url: string | string[]) => url.indexOf('://') > 0 || url.indexOf('//') === 0
|
||||
// Custom renderer to render images with relative path
|
||||
@@ -55,7 +55,7 @@ const MarkdownPreview: FC<{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
alt={alt}
|
||||
src={`/api?path=${parentPath}/${src}&raw=true`}
|
||||
src={`/api/?path=${parentPath}/${src}&raw=true`}
|
||||
title={title}
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -100,7 +100,7 @@ const MarkdownPreview: FC<{
|
||||
</PreviewContainer>
|
||||
{standalone && (
|
||||
<DownloadBtnContainer>
|
||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
||||
<DownloadButtonGroup />
|
||||
</DownloadBtnContainer>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import type { OdFileObject } from '../../types'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import Preview from 'preview-office-docs'
|
||||
|
||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||
import { DownloadBtnContainer } from './Containers'
|
||||
import { getBaseUrl } from '../../utils/getBaseUrl'
|
||||
import { getStoredToken } from '../../utils/protectedRouteHandler'
|
||||
|
||||
const OfficePreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
const { asPath } = useRouter()
|
||||
const hashedToken = getStoredToken(asPath)
|
||||
|
||||
const docContainer = useRef<HTMLDivElement>(null)
|
||||
const [docContainerWidth, setDocContainerWidth] = useState(600)
|
||||
|
||||
@@ -18,13 +24,13 @@ const OfficePreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
<div>
|
||||
<div className="overflow-scroll" ref={docContainer} style={{ maxHeight: '90vh' }}>
|
||||
<Preview
|
||||
url={encodeURIComponent(file['@microsoft.graph.downloadUrl'])}
|
||||
url={`${getBaseUrl()}/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}`}
|
||||
width={docContainerWidth.toString()}
|
||||
height="600"
|
||||
/>
|
||||
</div>
|
||||
<DownloadBtnContainer>
|
||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
||||
<DownloadButtonGroup />
|
||||
</DownloadBtnContainer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { getBaseUrl } from '../../utils/getBaseUrl'
|
||||
import { getStoredToken } from '../../utils/protectedRouteHandler'
|
||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||
import { DownloadBtnContainer } from './Containers'
|
||||
|
||||
const PDFEmbedPreview: React.FC<{ file: any }> = ({ file }) => {
|
||||
// const url = `/api/proxy?url=${encodeURIComponent(file['@microsoft.graph.downloadUrl'])}&inline=true`
|
||||
const url = `https://mozilla.github.io/pdf.js/web/viewer.html?file=${encodeURIComponent(
|
||||
file['@microsoft.graph.downloadUrl']
|
||||
)}`
|
||||
const { asPath } = useRouter()
|
||||
const hashedToken = getStoredToken(asPath)
|
||||
|
||||
// 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 (
|
||||
<div>
|
||||
@@ -13,7 +20,7 @@ const PDFEmbedPreview: React.FC<{ file: any }> = ({ file }) => {
|
||||
<iframe src={url} frameBorder="0" width="100%" height="100%"></iframe>
|
||||
</div>
|
||||
<DownloadBtnContainer>
|
||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
||||
<DownloadButtonGroup />
|
||||
</DownloadBtnContainer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import FourOhFour from '../FourOhFour'
|
||||
import Loading from '../Loading'
|
||||
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||
import useAxiosGet from '../../utils/fetchOnMount'
|
||||
import useFileContent from '../../utils/fetchOnMount'
|
||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||
|
||||
const TextPreview = ({ file }) => {
|
||||
const { asPath } = useRouter()
|
||||
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) {
|
||||
return (
|
||||
<PreviewContainer>
|
||||
@@ -40,7 +42,7 @@ const TextPreview = ({ file }) => {
|
||||
<pre className="overflow-x-scroll p-0 text-sm md:p-3">{content}</pre>
|
||||
</PreviewContainer>
|
||||
<DownloadBtnContainer>
|
||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
||||
<DownloadButtonGroup />
|
||||
</DownloadBtnContainer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import FourOhFour from '../FourOhFour'
|
||||
import Loading from '../Loading'
|
||||
import { DownloadButton } from '../DownloadBtnGtoup'
|
||||
import useAxiosGet from '../../utils/fetchOnMount'
|
||||
import useFileContent from '../../utils/fetchOnMount'
|
||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||
|
||||
const parseDotUrl = (content: string): string | undefined => {
|
||||
@@ -14,9 +15,10 @@ const parseDotUrl = (content: string): string | undefined => {
|
||||
}
|
||||
|
||||
const TextPreview = ({ file }) => {
|
||||
const { asPath } = useRouter()
|
||||
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) {
|
||||
return (
|
||||
<PreviewContainer>
|
||||
@@ -49,11 +51,11 @@ const TextPreview = ({ file }) => {
|
||||
<DownloadBtnContainer>
|
||||
<div className="flex justify-center">
|
||||
<DownloadButton
|
||||
onClickCallback={() => window.open(parseDotUrl(content) || '')}
|
||||
onClickCallback={() => window.open(parseDotUrl(content) ?? '')}
|
||||
btnColor="blue"
|
||||
btnText={t('Open URL')}
|
||||
btnIcon="external-link-alt"
|
||||
btnTitle={t('Open URL{{url}}', { url: ' ' + parseDotUrl(content) || '' })}
|
||||
btnTitle={t('Open URL{{url}}', { url: ' ' + parseDotUrl(content) ?? '' })}
|
||||
/>
|
||||
</div>
|
||||
</DownloadBtnContainer>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useAsync } from 'react-async-hook'
|
||||
import { getBaseUrl } from '../../utils/getBaseUrl'
|
||||
import { getExtension } from '../../utils/getFileIcon'
|
||||
import { getReadablePath } from '../../utils/getReadablePath'
|
||||
import { getStoredToken } from '../../utils/protectedRouteHandler'
|
||||
import { DownloadButton } from '../DownloadBtnGtoup'
|
||||
import { DownloadBtnContainer, PreviewContainer } from './Containers'
|
||||
import FourOhFour from '../FourOhFour'
|
||||
@@ -18,16 +19,21 @@ import CustomEmbedLinkMenu from '../CustomEmbedLinkMenu'
|
||||
|
||||
const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
const { asPath } = useRouter()
|
||||
const hashedToken = getStoredToken(asPath)
|
||||
const clipboard = useClipboard()
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 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
|
||||
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 {
|
||||
@@ -42,7 +48,7 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomEmbedLinkMenu path={getReadablePath(asPath)} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
|
||||
<CustomEmbedLinkMenu path={asPath} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
|
||||
<PreviewContainer>
|
||||
{error ? (
|
||||
<FourOhFour errorMsg={error.message} />
|
||||
@@ -55,7 +61,7 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
volume: 1.0,
|
||||
lang: 'en',
|
||||
video: {
|
||||
url: file['@microsoft.graph.downloadUrl'],
|
||||
url: videoUrl,
|
||||
pic: thumbnail,
|
||||
type: isFlv ? 'customFlv' : 'auto',
|
||||
customType: {
|
||||
@@ -78,14 +84,14 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
<DownloadBtnContainer>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
<DownloadButton
|
||||
onClickCallback={() => window.open(file['@microsoft.graph.downloadUrl'])}
|
||||
onClickCallback={() => window.open(videoUrl)}
|
||||
btnColor="blue"
|
||||
btnText={t('Download')}
|
||||
btnIcon="file-download"
|
||||
/>
|
||||
{/* <DownloadButton
|
||||
onClickCallback={() =>
|
||||
window.open(`/api/proxy?url=${encodeURIComponent(file['@microsoft.graph.downloadUrl'])}`)
|
||||
window.open(`/api/proxy?url=${encodeURIComponent(...)}`)
|
||||
}
|
||||
btnColor="teal"
|
||||
btnText={t('Proxy download')}
|
||||
@@ -93,7 +99,9 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
/> */}
|
||||
<DownloadButton
|
||||
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.'))
|
||||
}}
|
||||
btnColor="pink"
|
||||
@@ -108,17 +116,17 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
|
||||
/>
|
||||
|
||||
<DownloadButton
|
||||
onClickCallback={() => window.open(`iina://weblink?url=${file['@microsoft.graph.downloadUrl']}`)}
|
||||
onClickCallback={() => window.open(`iina://weblink?url=${getBaseUrl()}${videoUrl}`)}
|
||||
btnText="IINA"
|
||||
btnImage="/players/iina.png"
|
||||
/>
|
||||
<DownloadButton
|
||||
onClickCallback={() => window.open(`vlc://${file['@microsoft.graph.downloadUrl']}`)}
|
||||
onClickCallback={() => window.open(`vlc://${getBaseUrl()}${videoUrl}`)}
|
||||
btnText="VLC"
|
||||
btnImage="/players/vlc.png"
|
||||
/>
|
||||
<DownloadButton
|
||||
onClickCallback={() => window.open(`potplayer://${file['@microsoft.graph.downloadUrl']}`)}
|
||||
onClickCallback={() => window.open(`potplayer://${getBaseUrl()}/${videoUrl}`)}
|
||||
btnText="PotPlayer"
|
||||
btnImage="/players/potplayer.png"
|
||||
/>
|
||||
|
||||
@@ -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.
|
||||
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
|
||||
cacheControlHeader: 'max-age=0, s-maxage=3540, stale-while-revalidate=60'
|
||||
cacheControlHeader: 'max-age=0, s-maxage=60, stale-while-revalidate',
|
||||
}
|
||||
|
||||
+81
-67
@@ -2,31 +2,17 @@ import { posix as pathPosix } from 'path'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import axios from 'axios'
|
||||
import Cors from 'cors'
|
||||
|
||||
import apiConfig from '../../config/api.config'
|
||||
import siteConfig from '../../config/site.config'
|
||||
import { revealObfuscatedToken } from '../../utils/oAuthHandler'
|
||||
import { compareHashedToken } from '../../utils/protectedRouteHandler'
|
||||
import { getOdAuthTokens, storeOdAuthTokens } from '../../utils/odAuthTokenStore'
|
||||
import { runCorsMiddleware } from './raw'
|
||||
|
||||
const basePath = pathPosix.resolve('/', siteConfig.baseDirectory)
|
||||
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
|
||||
*
|
||||
@@ -107,6 +93,64 @@ export function getAuthTokenPath(path: string) {
|
||||
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) {
|
||||
// If method is POST, then the API is called by the client to store acquired tokens
|
||||
if (req.method === 'POST') {
|
||||
@@ -119,11 +163,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return
|
||||
}
|
||||
|
||||
await storeOdAuthTokens({
|
||||
accessToken,
|
||||
accessTokenExpiry,
|
||||
refreshToken,
|
||||
})
|
||||
await storeOdAuthTokens({ accessToken, accessTokenExpiry, refreshToken })
|
||||
res.status(200).send('OK')
|
||||
return
|
||||
}
|
||||
@@ -155,43 +195,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle authentication through .password
|
||||
const authTokenPath = getAuthTokenPath(cleanPath)
|
||||
|
||||
// Fetch password from remote file content
|
||||
if (authTokenPath !== '') {
|
||||
// Don't server cached response for password protected folders
|
||||
// Handle protected routes authentication
|
||||
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
|
||||
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')
|
||||
|
||||
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)
|
||||
@@ -201,24 +215,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const isRoot = requestPath === ''
|
||||
|
||||
// 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) {
|
||||
await runCorsMiddleware(req, res)
|
||||
res.setHeader('Cache-Control', 'no-cache')
|
||||
|
||||
const { data } = await axios.get(requestUrl, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
params: {
|
||||
select: '@microsoft.graph.downloadUrl,folder,file',
|
||||
select: '@microsoft.graph.downloadUrl',
|
||||
},
|
||||
})
|
||||
|
||||
if ('folder' in data) {
|
||||
res.status(400).json({ error: "Folders doesn't have raw download urls." })
|
||||
return
|
||||
}
|
||||
if ('file' in data) {
|
||||
if ('@microsoft.graph.downloadUrl' in data) {
|
||||
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
|
||||
@@ -226,7 +240,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const { data: identityData } = await axios.get(requestUrl, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
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}` },
|
||||
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,
|
||||
$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,
|
||||
},
|
||||
})
|
||||
@@ -261,7 +275,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
res.status(200).json({ file: identityData })
|
||||
return
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
})
|
||||
res.status(200).json(data)
|
||||
} 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 {
|
||||
res.status(400).json({ error: 'Invalid driveItem ID.' })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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) {
|
||||
indexHandler(req, res)
|
||||
rawFileHandler(req, res)
|
||||
}
|
||||
|
||||
@@ -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
@@ -53,7 +53,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
})
|
||||
res.status(200).json(data.value)
|
||||
} 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 {
|
||||
res.status(200).json([])
|
||||
|
||||
+18
-12
@@ -5,19 +5,22 @@ import { posix as pathPosix } from 'path'
|
||||
import axios from 'axios'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import { encodePath, getAccessToken, getAuthTokenPath } from '.'
|
||||
import { checkAuthRoute, encodePath, getAccessToken } from '.'
|
||||
import apiConfig from '../../config/api.config'
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Get access token from storage
|
||||
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
|
||||
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
|
||||
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'
|
||||
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))
|
||||
|
||||
// Check if the path is protected
|
||||
const authTokenPath = getAuthTokenPath(cleanPath)
|
||||
|
||||
// Currently protected paths are rejected to avoid file content leak
|
||||
if (authTokenPath) {
|
||||
res.status(404).json({ error: 'Protected pathes are not allowed.' })
|
||||
const { code, message } = await checkAuthRoute(cleanPath, accessToken, odpt as string)
|
||||
// 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')
|
||||
}
|
||||
|
||||
const requestPath = encodePath(cleanPath)
|
||||
// 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." })
|
||||
}
|
||||
} 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
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function OAuthStep2() {
|
||||
|
||||
<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">
|
||||
{authCode || <span className="animate-pulse">{t('Waiting for code...')}</span>}
|
||||
{authCode ?? <span className="animate-pulse">{t('Waiting for code...')}</span>}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
Vendored
+3
-9
@@ -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.
|
||||
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
|
||||
@@ -8,7 +8,6 @@ export type OdFolderObject = {
|
||||
'@odata.context': string
|
||||
'@odata.nextLink'?: string
|
||||
value: Array<{
|
||||
'@microsoft.graph.downloadUrl': string
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
@@ -17,14 +16,11 @@ export type OdFolderObject = {
|
||||
folder?: { childCount: number; view: { sortBy: string; sortOrder: 'ascending'; viewType: 'thumbnails' } }
|
||||
image?: OdImageFile
|
||||
video?: OdVideoFile
|
||||
// 'thumbnails@odata.context'?: string
|
||||
// thumbnails?: Array<OdThumbnail>
|
||||
}>
|
||||
}
|
||||
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.
|
||||
export type OdFileObject = {
|
||||
'@microsoft.graph.downloadUrl': string
|
||||
'@odata.context': string
|
||||
name: string
|
||||
size: number
|
||||
@@ -33,8 +29,6 @@ export type OdFileObject = {
|
||||
file: { mimeType: string; hashes: { quickXorHash: string; sha1Hash?: string; sha256Hash?: string } }
|
||||
image?: OdImageFile
|
||||
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.
|
||||
export type OdImageFile = {
|
||||
@@ -59,7 +53,7 @@ export type OdThumbnail = {
|
||||
medium: { 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<{
|
||||
id: string
|
||||
name: string
|
||||
@@ -68,7 +62,7 @@ export type OdSearchResult = Array<{
|
||||
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 = {
|
||||
'@odata.context': string
|
||||
'@odata.etag': string
|
||||
|
||||
+16
-7
@@ -1,22 +1,31 @@
|
||||
import axios from 'axios'
|
||||
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 [validating, setValidating] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const hashedToken = getStoredToken(path)
|
||||
const url = fetchUrl + (hashedToken ? `&odpt=${hashedToken}` : '')
|
||||
|
||||
axios
|
||||
// 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.
|
||||
.get(fetchUrl, { responseType: 'blob' })
|
||||
.get(url, { responseType: 'blob' })
|
||||
.then(async res => setResponse(await res.data.text()))
|
||||
.catch(e => setError(e.message))
|
||||
.finally(() => {
|
||||
setValidating(false)
|
||||
})
|
||||
}, [fetchUrl])
|
||||
.finally(() => setValidating(false))
|
||||
}, [fetchUrl, path])
|
||||
return { response, error, validating }
|
||||
}
|
||||
|
||||
@@ -40,10 +40,10 @@ export function useProtectedSWRInfinite(path: string = '') {
|
||||
if (previousPageData && !previousPageData.folder) return null
|
||||
|
||||
// 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
|
||||
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
|
||||
|
||||
@@ -43,7 +43,7 @@ export function extractAuthCodeFromRedirected(url: string): string {
|
||||
|
||||
// New URL search parameter
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user