mirror of
https://github.com/Nezumi-2711/onedrive-vercel-index.git
synced 2026-09-22 13:38:45 +00:00
refactor new file download functions to new file
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ import Image from 'next/image'
|
|||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { FunctionComponent } from 'react'
|
import { FunctionComponent } from 'react'
|
||||||
|
|
||||||
import { matchProtectedRoute } from '../utils/tools'
|
import { matchProtectedRoute } from '../utils/protectedRouteHandler'
|
||||||
import useLocalStorage from '../utils/useLocalStorage'
|
import useLocalStorage from '../utils/useLocalStorage'
|
||||||
|
|
||||||
const Auth: FunctionComponent<{ redirect: string }> = ({ redirect }) => {
|
const Auth: FunctionComponent<{ redirect: string }> = ({ redirect }) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import toast, { Toaster } from 'react-hot-toast'
|
|||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useClipboard } from 'use-clipboard-copy'
|
import { useClipboard } from 'use-clipboard-copy'
|
||||||
|
|
||||||
import { getBaseUrl } from '../utils/tools'
|
import { getBaseUrl } from "../utils/getBaseUrl"
|
||||||
|
|
||||||
const DownloadBtn: FunctionComponent<{ downloadUrl: string }> = ({ downloadUrl }) => {
|
const DownloadBtn: FunctionComponent<{ downloadUrl: string }> = ({ downloadUrl }) => {
|
||||||
const { asPath } = useRouter()
|
const { asPath } = useRouter()
|
||||||
|
|||||||
@@ -12,13 +12,9 @@ import dynamic from 'next/dynamic'
|
|||||||
|
|
||||||
import { getExtension, getFileIcon, hasKey } from '../utils/getFileIcon'
|
import { getExtension, getFileIcon, hasKey } from '../utils/getFileIcon'
|
||||||
import { extensions, preview } from '../utils/getPreviewType'
|
import { extensions, preview } from '../utils/getPreviewType'
|
||||||
import {
|
import { useProtectedSWRInfinite } from '../utils/fetchWithSWR'
|
||||||
getBaseUrl,
|
import { getBaseUrl } from '../utils/getBaseUrl'
|
||||||
traverseFolder,
|
import { downloadMultipleFiles, downloadTreelikeMultipleFiles, traverseFolder } from '../utils/downloadMultipleFiles'
|
||||||
downloadMultipleFiles,
|
|
||||||
useProtectedSWRInfinite,
|
|
||||||
downloadTreelikeMultipleFiles,
|
|
||||||
} from '../utils/tools'
|
|
||||||
|
|
||||||
import { VideoPreview } from './previews/VideoPreview'
|
import { VideoPreview } from './previews/VideoPreview'
|
||||||
import { AudioPreview } from './previews/AudioPreview'
|
import { AudioPreview } from './previews/AudioPreview'
|
||||||
@@ -290,7 +286,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
|
|||||||
} else if (files.length > 1) {
|
} else if (files.length > 1) {
|
||||||
setTotalGenerating(true)
|
setTotalGenerating(true)
|
||||||
const toastId = toast.loading('Downloading selected files. Refresh to cancel, this may take some time...')
|
const toastId = toast.loading('Downloading selected files. Refresh to cancel, this may take some time...')
|
||||||
downloadMultipleFiles(toastId, files, folder)
|
downloadMultipleFiles({ toastId, files, folder })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setTotalGenerating(false)
|
setTotalGenerating(false)
|
||||||
toast.dismiss(toastId)
|
toast.dismiss(toastId)
|
||||||
@@ -320,7 +316,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
|
|||||||
setFolderGenerating({ ...folderGenerating, [id]: true })
|
setFolderGenerating({ ...folderGenerating, [id]: true })
|
||||||
const toastId = toast.loading('Downloading folder. Refresh to cancel, this may take some time...')
|
const toastId = toast.loading('Downloading folder. Refresh to cancel, this may take some time...')
|
||||||
|
|
||||||
downloadTreelikeMultipleFiles(toastId, files, path, name)
|
downloadTreelikeMultipleFiles({ toastId, files, basePath: path, folder: name })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setFolderGenerating({ ...folderGenerating, [id]: false })
|
setFolderGenerating({ ...folderGenerating, [id]: false })
|
||||||
toast.dismiss(toastId)
|
toast.dismiss(toastId)
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { useEffect, FunctionComponent } from 'react'
|
|||||||
import Prism from 'prismjs'
|
import Prism from 'prismjs'
|
||||||
|
|
||||||
import { getExtension } from '../../utils/getFileIcon'
|
import { getExtension } from '../../utils/getFileIcon'
|
||||||
import { useStaleSWR } from '../../utils/tools'
|
import { useStaleSWR } from '../../utils/fetchWithSWR'
|
||||||
import FourOhFour from '../FourOhFour'
|
import FourOhFour from '../FourOhFour'
|
||||||
import Loading from '../Loading'
|
import Loading from '../Loading'
|
||||||
import DownloadBtn from '../DownloadBtn'
|
import DownloadBtn from '../DownloadBtn'
|
||||||
|
|
||||||
const CodePreview: FunctionComponent<{ file: any }> = ({ file }) => {
|
const CodePreview: FunctionComponent<{ file: any }> = ({ file }) => {
|
||||||
const { data, error } = useStaleSWR(file['@microsoft.graph.downloadUrl'])
|
const { data, error } = useStaleSWR({ url: file['@microsoft.graph.downloadUrl'] })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ import 'katex/dist/katex.min.css'
|
|||||||
import FourOhFour from '../FourOhFour'
|
import FourOhFour from '../FourOhFour'
|
||||||
import Loading from '../Loading'
|
import Loading from '../Loading'
|
||||||
import DownloadBtn from '../DownloadBtn'
|
import DownloadBtn from '../DownloadBtn'
|
||||||
import { useStaleSWR } from '../../utils/tools'
|
import { useStaleSWR } from '../../utils/fetchWithSWR'
|
||||||
|
|
||||||
const MarkdownPreview: FunctionComponent<{ file: any; path: string; standalone?: boolean }> = ({
|
const MarkdownPreview: FunctionComponent<{ file: any; path: string; standalone?: boolean }> = ({
|
||||||
file,
|
file,
|
||||||
path,
|
path,
|
||||||
standalone = true,
|
standalone = true,
|
||||||
}) => {
|
}) => {
|
||||||
const { data, error } = useStaleSWR(file['@microsoft.graph.downloadUrl'])
|
const { data, error } = useStaleSWR({ url: file['@microsoft.graph.downloadUrl'] })
|
||||||
|
|
||||||
// The parent folder of the markdown file, which is also the relative image folder
|
// The parent folder of the markdown file, which is also the relative image folder
|
||||||
const parentPath = path.substring(0, path.lastIndexOf('/'))
|
const parentPath = path.substring(0, path.lastIndexOf('/'))
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { FunctionComponent } from 'react'
|
|||||||
import FourOhFour from '../FourOhFour'
|
import FourOhFour from '../FourOhFour'
|
||||||
import Loading from '../Loading'
|
import Loading from '../Loading'
|
||||||
import DownloadBtn from '../DownloadBtn'
|
import DownloadBtn from '../DownloadBtn'
|
||||||
import { useStaleSWR } from '../../utils/tools'
|
import { useStaleSWR } from '../../utils/fetchWithSWR'
|
||||||
|
|
||||||
const TextPreview: FunctionComponent<{ file: any }> = ({ file }) => {
|
const TextPreview: FunctionComponent<{ file: any }> = ({ file }) => {
|
||||||
const { data, error } = useStaleSWR(file['@microsoft.graph.downloadUrl'])
|
const { data, error } = useStaleSWR({ url: file['@microsoft.graph.downloadUrl'] })
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-gray-900 p-3 bg-white rounded">
|
<div className="dark:bg-gray-900 p-3 bg-white rounded">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useClipboard } from 'use-clipboard-copy'
|
|||||||
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 { getBaseUrl } from '../../utils/tools'
|
import { getBaseUrl } from "../../utils/getBaseUrl"
|
||||||
|
|
||||||
export const VideoPreview: FunctionComponent<{ file: any }> = ({ file }) => {
|
export const VideoPreview: FunctionComponent<{ file: any }> = ({ file }) => {
|
||||||
const { asPath } = useRouter()
|
const { asPath } = useRouter()
|
||||||
|
|||||||
+8
-3
@@ -4,7 +4,7 @@ import { posix as pathPosix } from 'path'
|
|||||||
|
|
||||||
import apiConfig from '../../config/api.json'
|
import apiConfig from '../../config/api.json'
|
||||||
import siteConfig from '../../config/site.json'
|
import siteConfig from '../../config/site.json'
|
||||||
import { compareHashedToken } from '../../utils/tools'
|
import { compareHashedToken } from '../../utils/protectedRouteHandler'
|
||||||
|
|
||||||
const basePath = pathPosix.resolve('/', apiConfig.base)
|
const basePath = pathPosix.resolve('/', apiConfig.base)
|
||||||
const encodePath = (path: string) => {
|
const encodePath = (path: string) => {
|
||||||
@@ -77,7 +77,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const odProtectedToken = await axios.get(token.data['@microsoft.graph.downloadUrl'])
|
const odProtectedToken = await axios.get(token.data['@microsoft.graph.downloadUrl'])
|
||||||
// console.log(req.headers['od-protected-token'], odProtectedToken.data.trim())
|
// console.log(req.headers['od-protected-token'], odProtectedToken.data.trim())
|
||||||
|
|
||||||
if (!compareHashedToken(req.headers['od-protected-token'] as string, odProtectedToken.data)) {
|
if (
|
||||||
|
!compareHashedToken({
|
||||||
|
odTokenHeader: req.headers['od-protected-token'] as string,
|
||||||
|
dotPassword: odProtectedToken.data,
|
||||||
|
})
|
||||||
|
) {
|
||||||
res.status(401).json({ error: 'Password required for this folder.' })
|
res.status(401).json({ error: 'Password required for this folder.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -127,7 +132,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
})
|
})
|
||||||
|
|
||||||
if ('folder' in identityData) {
|
if ('folder' in identityData) {
|
||||||
const { data: folderData } = await axios.get(`${requestUrl}${isRoot ? '': ':'}/children`, {
|
const { data: folderData } = await axios.get(`${requestUrl}${isRoot ? '' : ':'}/children`, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
params: next
|
params: next
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -1,141 +1,40 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import sha256 from 'crypto-js/sha256'
|
|
||||||
import useSWR, { cache, Key, useSWRInfinite } from 'swr'
|
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
|
|
||||||
import siteConfig from '../config/site.json'
|
import { fetcher } from './fetchWithSWR'
|
||||||
|
import { getStoredToken } from './protectedRouteHandler'
|
||||||
|
|
||||||
/**
|
// Blob download helper
|
||||||
* Extract the current web page's base url
|
export function downloadBlob({ blob, name }: { blob: Blob; name: string }) {
|
||||||
* @returns base url of the page
|
// Prepare for download
|
||||||
*/
|
const el = document.createElement('a')
|
||||||
export const getBaseUrl = () => {
|
el.style.display = 'none'
|
||||||
if (typeof window !== 'undefined') {
|
document.body.appendChild(el)
|
||||||
return window.location.origin
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common axios fetch function for use with useSWR
|
// Download zip file
|
||||||
const fetcher = (url: string, token?: string) => {
|
const bUrl = window.URL.createObjectURL(blob)
|
||||||
return token
|
el.href = bUrl
|
||||||
? axios
|
el.download = name
|
||||||
.get(url, {
|
el.click()
|
||||||
headers: { 'od-protected-token': token },
|
window.URL.revokeObjectURL(bUrl)
|
||||||
})
|
el.remove()
|
||||||
.then(res => res.data)
|
|
||||||
: axios.get(url).then(res => res.data)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Use stale SWR instead of revalidating on each request. Not ideal for this scenario but have to do
|
|
||||||
* if fetching serverside props from component instead of pages.
|
|
||||||
* @param url request url
|
|
||||||
* @returns useSWR instance
|
|
||||||
*/
|
|
||||||
export const useStaleSWR = (url: Key, path: string = '') => {
|
|
||||||
const revalidationOptions = {
|
|
||||||
revalidateOnMount: !(cache.has(`arg@"${url}"@null`) || cache.has(url)),
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedToken = getStoredToken(path)
|
|
||||||
return useSWR([url, hashedToken], fetcher, revalidationOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Paging with useSWRInfinite + protected token support
|
|
||||||
* @param path Current query directory path
|
|
||||||
* @returns useSWRInfinite API
|
|
||||||
*/
|
|
||||||
export const useProtectedSWRInfinite = (path: string = '') => {
|
|
||||||
const hashedToken = getStoredToken(path)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Next page infinite loading for useSWR
|
|
||||||
* @param pageIdx The index of this paging collection
|
|
||||||
* @param prevPageData Previous page information
|
|
||||||
* @param path Directory path
|
|
||||||
* @returns API to the next page
|
|
||||||
*/
|
|
||||||
const getNextKey = (pageIndex, previousPageData) => {
|
|
||||||
// Reached the end of the collection
|
|
||||||
if (previousPageData && !previousPageData.folder) return null
|
|
||||||
|
|
||||||
// First page with no prevPageData
|
|
||||||
if (pageIndex === 0) return [`/api?path=${path}`, hashedToken]
|
|
||||||
|
|
||||||
// Add nextPage token to API endpoint
|
|
||||||
return [`/api?path=${path}&next=${previousPageData.next}`, hashedToken]
|
|
||||||
}
|
|
||||||
|
|
||||||
const revalidationOptions = {
|
|
||||||
revalidateOnMount: !(cache.has(`arg@"/api?path=${path}"@null`) || cache.has(`/api?path=${path}`)),
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: true,
|
|
||||||
}
|
|
||||||
return useSWRInfinite(getNextKey, fetcher, revalidationOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash password token with SHA256
|
|
||||||
const encryptToken = (token: string) => {
|
|
||||||
return sha256(token).toString()
|
|
||||||
}
|
|
||||||
// Fetch stored token from localStorage and encrypt with SHA256
|
|
||||||
const getStoredToken = (path: string) => {
|
|
||||||
const storedToken =
|
|
||||||
typeof window !== 'undefined' ? JSON.parse(localStorage.getItem(matchProtectedRoute(path)) as string) : ''
|
|
||||||
return storedToken ? encryptToken(storedToken) : null
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Compares the hash of .password and od-protected-token header
|
|
||||||
* @param odTokenHeader od-protected-token header (sha256 hashed token)
|
|
||||||
* @param dotPassword non-hashed .password file
|
|
||||||
* @returns whether the two hashes are the same
|
|
||||||
*/
|
|
||||||
export const compareHashedToken = (odTokenHeader: string, dotPassword: string) => {
|
|
||||||
return encryptToken(dotPassword.trim()) === odTokenHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Match the specified route against a list of predefined routes
|
|
||||||
* @param route directory path
|
|
||||||
* @returns whether the directory is protected
|
|
||||||
*/
|
|
||||||
export const matchProtectedRoute = (route: string) => {
|
|
||||||
const protectedRoutes: string[] = siteConfig.protectedRoutes
|
|
||||||
let authTokenPath = ''
|
|
||||||
|
|
||||||
for (const r of protectedRoutes) {
|
|
||||||
// protected route array could be empty
|
|
||||||
if (r) {
|
|
||||||
if (
|
|
||||||
route.startsWith(
|
|
||||||
r
|
|
||||||
.split('/')
|
|
||||||
.map(p => encodeURIComponent(p))
|
|
||||||
.join('/')
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
authTokenPath = r
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return authTokenPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download multiple files after compressing them into a zip
|
* Download multiple files after compressing them into a zip
|
||||||
|
* @param toastId Toast ID to be used for toast notification
|
||||||
* @param files Files to be downloaded
|
* @param files Files to be downloaded
|
||||||
* @param folder Optional folder name to hold files, otherwise flatten files in the zip
|
* @param folder Optional folder name to hold files, otherwise flatten files in the zip
|
||||||
*/
|
*/
|
||||||
export const downloadMultipleFiles = async (
|
export async function downloadMultipleFiles({
|
||||||
toastId: string,
|
toastId,
|
||||||
files: { name: string; url: string }[],
|
files,
|
||||||
|
folder,
|
||||||
|
}: {
|
||||||
|
toastId: string
|
||||||
|
files: { name: string; url: string }[]
|
||||||
folder?: string
|
folder?: string
|
||||||
) => {
|
}): Promise<void> {
|
||||||
const zip = new JSZip()
|
const zip = new JSZip()
|
||||||
const dir = folder ? zip.folder(folder)! : zip
|
const dir = folder ? zip.folder(folder)! : zip
|
||||||
|
|
||||||
@@ -155,23 +54,74 @@ export const downloadMultipleFiles = async (
|
|||||||
id: toastId,
|
id: toastId,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
downloadBlob(b, folder ? folder + '.zip' : 'download.zip')
|
downloadBlob({ blob: b, name: folder ? folder + '.zip' : 'download.zip' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blob download helper
|
/**
|
||||||
const downloadBlob = (b: Blob, name: string) => {
|
* Download hierarchical tree-like files after compressing them into a zip
|
||||||
// Prepare for download
|
* @param toastId Toast ID to be used for toast notification
|
||||||
const el = document.createElement('a')
|
* @param files Files to be downloaded. Array of file and folder items excluding root folder.
|
||||||
el.style.display = 'none'
|
* Folder items MUST be in front of its children items in the array.
|
||||||
document.body.appendChild(el)
|
* Use async generator because generation of the array may be slow.
|
||||||
|
* When waiting for its generation, we can meanwhile download bodies of already got items.
|
||||||
|
* Only folder items can have url undefined.
|
||||||
|
* @param basePath Root dir path of files to be downloaded
|
||||||
|
* @param folder Optional folder name to hold files, otherwise flatten files in the zip
|
||||||
|
*/
|
||||||
|
|
||||||
// Download zip file
|
export async function downloadTreelikeMultipleFiles({
|
||||||
const bUrl = window.URL.createObjectURL(b)
|
toastId,
|
||||||
el.href = bUrl
|
files,
|
||||||
el.download = name
|
basePath,
|
||||||
el.click()
|
folder,
|
||||||
window.URL.revokeObjectURL(bUrl)
|
}: {
|
||||||
el.remove()
|
toastId: string
|
||||||
|
files: AsyncGenerator<{
|
||||||
|
name: string
|
||||||
|
url?: string
|
||||||
|
path: string
|
||||||
|
isFolder: boolean
|
||||||
|
}>
|
||||||
|
basePath: string
|
||||||
|
folder?: string
|
||||||
|
}): Promise<void> {
|
||||||
|
const zip = new JSZip()
|
||||||
|
const root = folder ? zip.folder(folder)! : zip
|
||||||
|
const map = [{ path: basePath, dir: root }]
|
||||||
|
|
||||||
|
// Add selected file blobs to zip according to its path
|
||||||
|
for await (const { name, url, path, isFolder } of files) {
|
||||||
|
// Search parent dir in map
|
||||||
|
const i = map
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.findIndex(
|
||||||
|
({ path: parent }) =>
|
||||||
|
path.substring(0, parent.length) === parent && path.substring(parent.length + 1).indexOf('/') === -1
|
||||||
|
)
|
||||||
|
if (i === -1) {
|
||||||
|
throw new Error('File array does not satisfy requirement')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add file or folder to zip
|
||||||
|
const dir = map[map.length - 1 - i].dir
|
||||||
|
if (isFolder) {
|
||||||
|
map.push({ path, dir: dir.folder(name)! })
|
||||||
|
} else {
|
||||||
|
dir.file(
|
||||||
|
name,
|
||||||
|
fetch(url!).then(r => r.blob())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create zip file and download it
|
||||||
|
const b = await zip.generateAsync({ type: 'blob' }, metadata => {
|
||||||
|
toast.loading(`Downloading ${metadata.percent.toFixed(0)}%. Refresh to cancel...`, {
|
||||||
|
id: toastId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
downloadBlob({ blob: b, name: folder ? folder + '.zip' : 'download.zip' })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -218,63 +168,3 @@ export async function* traverseFolder(path: string): AsyncGenerator<
|
|||||||
folderPaths = items.filter(i => i.isFolder).map(i => i.path)
|
folderPaths = items.filter(i => i.isFolder).map(i => i.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Download hierarchical tree-like files after compressing them into a zip
|
|
||||||
* @param files Files to be downloaded. Array of file and folder items excluding root folder.
|
|
||||||
* Folder items MUST be in front of its children items in the array.
|
|
||||||
* Use async generator because generation of the array may be slow.
|
|
||||||
* When waiting for its generation, we can meanwhile download bodies of already got items.
|
|
||||||
* Only folder items can have url undefined.
|
|
||||||
* @param basePath Root dir path of files to be downloaded
|
|
||||||
* @param folder Optional folder name to hold files, otherwise flatten files in the zip
|
|
||||||
*/
|
|
||||||
export const downloadTreelikeMultipleFiles = async (
|
|
||||||
toastId: string,
|
|
||||||
files: AsyncGenerator<{
|
|
||||||
name: string
|
|
||||||
url?: string
|
|
||||||
path: string
|
|
||||||
isFolder: boolean
|
|
||||||
}>,
|
|
||||||
basePath: string,
|
|
||||||
folder?: string
|
|
||||||
) => {
|
|
||||||
const zip = new JSZip()
|
|
||||||
const root = folder ? zip.folder(folder)! : zip
|
|
||||||
const map = [{ path: basePath, dir: root }]
|
|
||||||
|
|
||||||
// Add selected file blobs to zip according to its path
|
|
||||||
for await (const { name, url, path, isFolder } of files) {
|
|
||||||
// Search parent dir in map
|
|
||||||
const i = map
|
|
||||||
.slice()
|
|
||||||
.reverse()
|
|
||||||
.findIndex(
|
|
||||||
({ path: parent }) =>
|
|
||||||
path.substring(0, parent.length) === parent && path.substring(parent.length + 1).indexOf('/') === -1
|
|
||||||
)
|
|
||||||
if (i === -1) {
|
|
||||||
throw new Error('File array does not satisfy requirement')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add file or folder to zip
|
|
||||||
const dir = map[map.length - 1 - i].dir
|
|
||||||
if (isFolder) {
|
|
||||||
map.push({ path, dir: dir.folder(name)! })
|
|
||||||
} else {
|
|
||||||
dir.file(
|
|
||||||
name,
|
|
||||||
fetch(url!).then(r => r.blob())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create zip file and download it
|
|
||||||
const b = await zip.generateAsync({ type: 'blob' }, metadata => {
|
|
||||||
toast.loading(`Downloading ${metadata.percent.toFixed(0)}%. Refresh to cancel...`, {
|
|
||||||
id: toastId,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
downloadBlob(b, folder ? folder + '.zip' : 'download.zip')
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import useSWR, { cache, Key, useSWRInfinite } from 'swr'
|
||||||
|
|
||||||
|
import { getStoredToken } from './protectedRouteHandler'
|
||||||
|
|
||||||
|
// Common axios fetch function for use with useSWR
|
||||||
|
export function fetcher(url: string, token?: string): Promise<any> {
|
||||||
|
return token
|
||||||
|
? axios
|
||||||
|
.get(url, {
|
||||||
|
headers: { 'od-protected-token': token },
|
||||||
|
})
|
||||||
|
.then(res => res.data)
|
||||||
|
: axios.get(url).then(res => res.data)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Use stale SWR instead of revalidating on each request. Not ideal for this scenario but have to do
|
||||||
|
* if fetching serverside props from component instead of pages.
|
||||||
|
* @param url request url
|
||||||
|
* @returns useSWR instance
|
||||||
|
*/
|
||||||
|
export function useStaleSWR({ url, path = '' }: { url: Key; path?: string }) {
|
||||||
|
const revalidationOptions = {
|
||||||
|
revalidateOnMount: !(cache.has(`arg@"${url}"@null`) || cache.has(url)),
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedToken = getStoredToken(path)
|
||||||
|
return useSWR([url, hashedToken], fetcher, revalidationOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paging with useSWRInfinite + protected token support
|
||||||
|
* @param path Current query directory path
|
||||||
|
* @returns useSWRInfinite API
|
||||||
|
*/
|
||||||
|
export function useProtectedSWRInfinite(path: string = '') {
|
||||||
|
const hashedToken = getStoredToken(path)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next page infinite loading for useSWR
|
||||||
|
* @param pageIdx The index of this paging collection
|
||||||
|
* @param prevPageData Previous page information
|
||||||
|
* @param path Directory path
|
||||||
|
* @returns API to the next page
|
||||||
|
*/
|
||||||
|
function getNextKey(pageIndex, previousPageData): (string | null)[] | null {
|
||||||
|
// Reached the end of the collection
|
||||||
|
if (previousPageData && !previousPageData.folder) return null
|
||||||
|
|
||||||
|
// First page with no prevPageData
|
||||||
|
if (pageIndex === 0) return [`/api?path=${path}`, hashedToken]
|
||||||
|
|
||||||
|
// Add nextPage token to API endpoint
|
||||||
|
return [`/api?path=${path}&next=${previousPageData.next}`, hashedToken]
|
||||||
|
}
|
||||||
|
|
||||||
|
const revalidationOptions = {
|
||||||
|
revalidateOnMount: !(cache.has(`arg@"/api?path=${path}"@null`) || cache.has(`/api?path=${path}`)),
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: true,
|
||||||
|
}
|
||||||
|
return useSWRInfinite(getNextKey, fetcher, revalidationOptions)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Extract the current web page's base url
|
||||||
|
* @returns base url of the page
|
||||||
|
*/
|
||||||
|
export function getBaseUrl(): string {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return window.location.origin
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
@@ -83,15 +83,15 @@ const extensions = {
|
|||||||
* @param key The index key
|
* @param key The index key
|
||||||
* @returns Whether or not the key exists inside the object
|
* @returns Whether or not the key exists inside the object
|
||||||
*/
|
*/
|
||||||
export const hasKey = <O>(obj: O, key: PropertyKey): key is keyof O => {
|
export function hasKey<O>(obj: O, key: PropertyKey): key is keyof O {
|
||||||
return key in obj
|
return key in obj
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getExtension = (fileName: string) => {
|
export function getExtension(fileName: string): string {
|
||||||
return fileName.slice(((fileName.lastIndexOf('.') - 1) >>> 0) + 2).toLowerCase()
|
return fileName.slice(((fileName.lastIndexOf('.') - 1) >>> 0) + 2).toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFileIcon = (fileName: string) => {
|
export function getFileIcon(fileName: string): [IconPrefix, IconName] {
|
||||||
const extension = getExtension(fileName)
|
const extension = getExtension(fileName)
|
||||||
return hasKey(extensions, extension) ? extensions[extension] : icons.file
|
return hasKey(extensions, extension) ? extensions[extension] : icons.file
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import sha256 from 'crypto-js/sha256'
|
||||||
|
import siteConfig from '../config/site.json'
|
||||||
|
|
||||||
|
// Hash password token with SHA256
|
||||||
|
function encryptToken(token: string): string {
|
||||||
|
return sha256(token).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch stored token from localStorage and encrypt with SHA256
|
||||||
|
export function getStoredToken(path: string): string | null {
|
||||||
|
const storedToken =
|
||||||
|
typeof window !== 'undefined' ? JSON.parse(localStorage.getItem(matchProtectedRoute(path)) as string) : ''
|
||||||
|
return storedToken ? encryptToken(storedToken) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares the hash of .password and od-protected-token header
|
||||||
|
* @param odTokenHeader od-protected-token header (sha256 hashed token)
|
||||||
|
* @param dotPassword non-hashed .password file
|
||||||
|
* @returns whether the two hashes are the same
|
||||||
|
*/
|
||||||
|
export function compareHashedToken({
|
||||||
|
odTokenHeader,
|
||||||
|
dotPassword,
|
||||||
|
}: {
|
||||||
|
odTokenHeader: string
|
||||||
|
dotPassword: string
|
||||||
|
}): boolean {
|
||||||
|
return encryptToken(dotPassword.trim()) === odTokenHeader
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Match the specified route against a list of predefined routes
|
||||||
|
* @param route directory path
|
||||||
|
* @returns whether the directory is protected
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function matchProtectedRoute(route: string): string {
|
||||||
|
const protectedRoutes: string[] = siteConfig.protectedRoutes
|
||||||
|
let authTokenPath = ''
|
||||||
|
|
||||||
|
for (const r of protectedRoutes) {
|
||||||
|
// protected route array could be empty
|
||||||
|
if (r) {
|
||||||
|
if (
|
||||||
|
route.startsWith(
|
||||||
|
r
|
||||||
|
.split('/')
|
||||||
|
.map(p => encodeURIComponent(p))
|
||||||
|
.join('/')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
authTokenPath = r
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return authTokenPath
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user