diff --git a/components/FolderGridLayout.tsx b/components/FolderGridLayout.tsx index b64a3f4..1a04c44 100644 --- a/components/FolderGridLayout.tsx +++ b/components/FolderGridLayout.tsx @@ -11,9 +11,20 @@ import { formatModifiedDateTime } from '../utils/fileDetails' import { getReadablePath } from '../utils/getReadablePath' import { Checkbox, ChildIcon, Downloading, formatChildName } from './FileListing' -const GridItem = ({ c }: { c: OdFolderChildren }) => { +const GridItem = ({ c, path }: { c: OdFolderChildren; path: string }) => { // We use the generated medium thumbnail for rendering preview images - const thumbnail = c.thumbnails && c.thumbnails.length > 0 ? c.thumbnails[0].medium : null + const thumbnailUrl = + 'folder' in c + ? // Folders don't have thumbnails + null + : c.thumbnails + ? // Most OneDrive versions, including E5 developer, should have thumbnails returned + c.thumbnails.length > 0 + ? c.thumbnails[0].medium.url + : null + : // According to OneDrive docs, OneDrive for Business and SharePoint does not + // (can not retrieve thumbnails via expand). But currently we only see OneDrive 世纪互联 really does not. + `/api/thumbnail?path=${path}` // Some thumbnails are broken, so we check for onerror event in the image component const [brokenThumbnail, setBrokenThumbnail] = useState(false) @@ -21,11 +32,11 @@ const GridItem = ({ c }: { c: OdFolderChildren }) => { return (
- {thumbnail && !brokenThumbnail ? ( + {thumbnailUrl && !brokenThumbnail ? ( // eslint-disable-next-line @next/next/no-img-element {c.name} setBrokenThumbnail(true)} /> @@ -69,6 +80,9 @@ const FolderGridLayout = ({ const { t } = useTranslation() + // Get item path from item name + const getItemPath = (name: string) => `${path === '/' ? '' : path}/${encodeURIComponent(name)}` + return (
@@ -108,9 +122,7 @@ const FolderGridLayout = ({ title={t('Copy folder permalink')} className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600" onClick={() => { - clipboard.copy( - `${getBaseUrl()}${getReadablePath(`${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`)}` - ) + clipboard.copy(`${getBaseUrl()}${getReadablePath(getItemPath(c.name))}`) toast(t('Copied folder permalink.'), { icon: '👌' }) }} > @@ -122,10 +134,7 @@ const FolderGridLayout = ({ { - const p = `${path === '/' ? '' : path}/${encodeURIComponent(c.name)}` - handleFolderDownload(p, c.id, c.name)() - }} + onClick={handleFolderDownload(getItemPath(c.name), c.id, c.name)} > @@ -137,11 +146,7 @@ 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( - `${path === '/' ? '' : path}/${encodeURIComponent(c.name)}` - )}&raw=true` - ) + clipboard.copy(`${getBaseUrl()}/api?path=${getReadablePath(getItemPath(c.name))}&raw=true`) toast.success(t('Copied raw file permalink.')) }} > @@ -172,9 +177,9 @@ const FolderGridLayout = ({ )}
- + - +
diff --git a/pages/api/index.ts b/pages/api/index.ts index e4fb79b..15994e2 100644 --- a/pages/api/index.ts +++ b/pages/api/index.ts @@ -90,6 +90,23 @@ export async function getAccessToken(): Promise { return '' } +/** + * Match protected routes in site config to get path to required auth token + * @param path Path cleaned in advance + * @returns Path to required auth token. If not required, return empty string. + */ +export function getAuthTokenPath(path: string) { + const protectedRoutes = siteConfig.protectedRoutes + let authTokenPath = '' + for (const r of protectedRoutes) { + if (path.startsWith(r)) { + authTokenPath = `${r}/.password` + break + } + } + return authTokenPath +} + 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') { @@ -135,14 +152,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } // Handle authentication through .password - const protectedRoutes = siteConfig.protectedRoutes - let authTokenPath = '' - for (const r of protectedRoutes) { - if (cleanPath.startsWith(r)) { - authTokenPath = `${r}/.password` - break - } - } + const authTokenPath = getAuthTokenPath(cleanPath) // Fetch password from remote file content if (authTokenPath !== '') { diff --git a/pages/api/thumbnail.ts b/pages/api/thumbnail.ts new file mode 100644 index 0000000..825f5bd --- /dev/null +++ b/pages/api/thumbnail.ts @@ -0,0 +1,60 @@ +import type { OdThumbnail } from '../../types' + +import { posix as pathPosix } from 'path' + +import axios from 'axios' +import type { NextApiRequest, NextApiResponse } from 'next' + +import { encodePath, getAccessToken, getAuthTokenPath } 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() + + // Get item thumbnails by its path since we will later check if it is protected + const { path = '' } = 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)) + + // 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.' }) + return + } + + const requestPath = encodePath(cleanPath) + // Handle response from OneDrive API + const requestUrl = `${apiConfig.driveApi}/root${requestPath}` + // Whether path is root, which requires some special treatment + const isRoot = requestPath === '' + + try { + const { data } = await axios.get(`${requestUrl}${isRoot ? '' : ':'}/thumbnails`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }) + + const thumbnailUrl = data.value && data.value.length > 0 ? (data.value[0] as OdThumbnail).medium.url : null + if (thumbnailUrl) { + res.redirect(thumbnailUrl) + } else { + 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 }) + } + return +}