diff --git a/components/Auth.tsx b/components/Auth.tsx
index fc27997..fcc4d53 100644
--- a/components/Auth.tsx
+++ b/components/Auth.tsx
@@ -4,7 +4,7 @@ import Image from 'next/image'
import { useRouter } from 'next/router'
import { FunctionComponent } from 'react'
-import { matchProtectedRoute } from '../utils/tools'
+import { matchProtectedRoute } from '../utils/protectedRouteHandler'
import useLocalStorage from '../utils/useLocalStorage'
const Auth: FunctionComponent<{ redirect: string }> = ({ redirect }) => {
diff --git a/components/DownloadBtn.tsx b/components/DownloadBtn.tsx
index 13b6641..f74cb2c 100644
--- a/components/DownloadBtn.tsx
+++ b/components/DownloadBtn.tsx
@@ -4,7 +4,7 @@ import toast, { Toaster } from 'react-hot-toast'
import { useRouter } from 'next/router'
import { useClipboard } from 'use-clipboard-copy'
-import { getBaseUrl } from '../utils/tools'
+import { getBaseUrl } from "../utils/getBaseUrl"
const DownloadBtn: FunctionComponent<{ downloadUrl: string }> = ({ downloadUrl }) => {
const { asPath } = useRouter()
diff --git a/components/FileListing.tsx b/components/FileListing.tsx
index 45804a0..b424788 100644
--- a/components/FileListing.tsx
+++ b/components/FileListing.tsx
@@ -12,13 +12,9 @@ import dynamic from 'next/dynamic'
import { getExtension, getFileIcon, hasKey } from '../utils/getFileIcon'
import { extensions, preview } from '../utils/getPreviewType'
-import {
- getBaseUrl,
- traverseFolder,
- downloadMultipleFiles,
- useProtectedSWRInfinite,
- downloadTreelikeMultipleFiles,
-} from '../utils/tools'
+import { useProtectedSWRInfinite } from '../utils/fetchWithSWR'
+import { getBaseUrl } from '../utils/getBaseUrl'
+import { downloadMultipleFiles, downloadTreelikeMultipleFiles, traverseFolder } from '../utils/downloadMultipleFiles'
import { VideoPreview } from './previews/VideoPreview'
import { AudioPreview } from './previews/AudioPreview'
@@ -290,7 +286,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
} else if (files.length > 1) {
setTotalGenerating(true)
const toastId = toast.loading('Downloading selected files. Refresh to cancel, this may take some time...')
- downloadMultipleFiles(toastId, files, folder)
+ downloadMultipleFiles({ toastId, files, folder })
.then(() => {
setTotalGenerating(false)
toast.dismiss(toastId)
@@ -320,7 +316,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
setFolderGenerating({ ...folderGenerating, [id]: true })
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(() => {
setFolderGenerating({ ...folderGenerating, [id]: false })
toast.dismiss(toastId)
diff --git a/components/previews/CodePreview.tsx b/components/previews/CodePreview.tsx
index 710a332..1f1b873 100644
--- a/components/previews/CodePreview.tsx
+++ b/components/previews/CodePreview.tsx
@@ -2,13 +2,13 @@ import { useEffect, FunctionComponent } from 'react'
import Prism from 'prismjs'
import { getExtension } from '../../utils/getFileIcon'
-import { useStaleSWR } from '../../utils/tools'
+import { useStaleSWR } from '../../utils/fetchWithSWR'
import FourOhFour from '../FourOhFour'
import Loading from '../Loading'
import DownloadBtn from '../DownloadBtn'
const CodePreview: FunctionComponent<{ file: any }> = ({ file }) => {
- const { data, error } = useStaleSWR(file['@microsoft.graph.downloadUrl'])
+ const { data, error } = useStaleSWR({ url: file['@microsoft.graph.downloadUrl'] })
useEffect(() => {
if (typeof window !== 'undefined') {
diff --git a/components/previews/MarkdownPreview.tsx b/components/previews/MarkdownPreview.tsx
index 987d977..0c80e5b 100644
--- a/components/previews/MarkdownPreview.tsx
+++ b/components/previews/MarkdownPreview.tsx
@@ -11,14 +11,14 @@ import 'katex/dist/katex.min.css'
import FourOhFour from '../FourOhFour'
import Loading from '../Loading'
import DownloadBtn from '../DownloadBtn'
-import { useStaleSWR } from '../../utils/tools'
+import { useStaleSWR } from '../../utils/fetchWithSWR'
const MarkdownPreview: FunctionComponent<{ file: any; path: string; standalone?: boolean }> = ({
file,
path,
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
const parentPath = path.substring(0, path.lastIndexOf('/'))
diff --git a/components/previews/TextPreview.tsx b/components/previews/TextPreview.tsx
index d66e8b2..06817e6 100644
--- a/components/previews/TextPreview.tsx
+++ b/components/previews/TextPreview.tsx
@@ -3,10 +3,10 @@ import { FunctionComponent } from 'react'
import FourOhFour from '../FourOhFour'
import Loading from '../Loading'
import DownloadBtn from '../DownloadBtn'
-import { useStaleSWR } from '../../utils/tools'
+import { useStaleSWR } from '../../utils/fetchWithSWR'
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) {
return (
diff --git a/components/previews/VideoPreview.tsx b/components/previews/VideoPreview.tsx
index ccaa249..8cc0045 100644
--- a/components/previews/VideoPreview.tsx
+++ b/components/previews/VideoPreview.tsx
@@ -6,7 +6,7 @@ import { useClipboard } from 'use-clipboard-copy'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import toast, { Toaster } from 'react-hot-toast'
-import { getBaseUrl } from '../../utils/tools'
+import { getBaseUrl } from "../../utils/getBaseUrl"
export const VideoPreview: FunctionComponent<{ file: any }> = ({ file }) => {
const { asPath } = useRouter()
diff --git a/pages/api/index.ts b/pages/api/index.ts
index 5c87dac..64ad414 100644
--- a/pages/api/index.ts
+++ b/pages/api/index.ts
@@ -4,7 +4,7 @@ import { posix as pathPosix } from 'path'
import apiConfig from '../../config/api.json'
import siteConfig from '../../config/site.json'
-import { compareHashedToken } from '../../utils/tools'
+import { compareHashedToken } from '../../utils/protectedRouteHandler'
const basePath = pathPosix.resolve('/', apiConfig.base)
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'])
// 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.' })
return
}
@@ -127,7 +132,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
})
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}` },
params: next
? {
diff --git a/utils/tools.ts b/utils/downloadMultipleFiles.ts
similarity index 50%
rename from utils/tools.ts
rename to utils/downloadMultipleFiles.ts
index 4603494..4113e14 100644
--- a/utils/tools.ts
+++ b/utils/downloadMultipleFiles.ts
@@ -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 JSZip from 'jszip'
-import siteConfig from '../config/site.json'
+import { fetcher } from './fetchWithSWR'
+import { getStoredToken } from './protectedRouteHandler'
-/**
- * Extract the current web page's base url
- * @returns base url of the page
- */
-export const getBaseUrl = () => {
- if (typeof window !== 'undefined') {
- return window.location.origin
- }
- return ''
-}
+// Blob download helper
+export function downloadBlob({ blob, name }: { blob: Blob; name: string }) {
+ // Prepare for download
+ const el = document.createElement('a')
+ el.style.display = 'none'
+ document.body.appendChild(el)
-// Common axios fetch function for use with useSWR
-const fetcher = (url: string, token?: string) => {
- 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 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 zip file
+ const bUrl = window.URL.createObjectURL(blob)
+ el.href = bUrl
+ el.download = name
+ el.click()
+ window.URL.revokeObjectURL(bUrl)
+ el.remove()
}
/**
* 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 folder Optional folder name to hold files, otherwise flatten files in the zip
*/
-export const downloadMultipleFiles = async (
- toastId: string,
- files: { name: string; url: string }[],
+export async function downloadMultipleFiles({
+ toastId,
+ files,
+ folder,
+}: {
+ toastId: string
+ files: { name: string; url: string }[]
folder?: string
-) => {
+}): Promise
{
const zip = new JSZip()
const dir = folder ? zip.folder(folder)! : zip
@@ -155,23 +54,74 @@ export const downloadMultipleFiles = async (
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) => {
- // Prepare for download
- const el = document.createElement('a')
- el.style.display = 'none'
- document.body.appendChild(el)
+/**
+ * Download hierarchical tree-like files after compressing them into a zip
+ * @param toastId Toast ID to be used for toast notification
+ * @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
+ */
- // Download zip file
- const bUrl = window.URL.createObjectURL(b)
- el.href = bUrl
- el.download = name
- el.click()
- window.URL.revokeObjectURL(bUrl)
- el.remove()
+export async function downloadTreelikeMultipleFiles({
+ toastId,
+ files,
+ basePath,
+ folder,
+}: {
+ toastId: string
+ files: AsyncGenerator<{
+ name: string
+ url?: string
+ path: string
+ isFolder: boolean
+ }>
+ basePath: string
+ folder?: string
+}): Promise {
+ 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)
}
}
-
-/**
- * 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')
-}
diff --git a/utils/fetchWithSWR.ts b/utils/fetchWithSWR.ts
new file mode 100644
index 0000000..b9a1b85
--- /dev/null
+++ b/utils/fetchWithSWR.ts
@@ -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 {
+ 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)
+}
diff --git a/utils/getBaseUrl.ts b/utils/getBaseUrl.ts
new file mode 100644
index 0000000..a0de661
--- /dev/null
+++ b/utils/getBaseUrl.ts
@@ -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 ''
+}
diff --git a/utils/getFileIcon.ts b/utils/getFileIcon.ts
index 0a851c6..0a1c711 100644
--- a/utils/getFileIcon.ts
+++ b/utils/getFileIcon.ts
@@ -83,15 +83,15 @@ const extensions = {
* @param key The index key
* @returns Whether or not the key exists inside the object
*/
-export const hasKey = (obj: O, key: PropertyKey): key is keyof O => {
+export function hasKey(obj: O, key: PropertyKey): key is keyof O {
return key in obj
}
-export const getExtension = (fileName: string) => {
+export function getExtension(fileName: string): string {
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)
return hasKey(extensions, extension) ? extensions[extension] : icons.file
}
diff --git a/utils/protectedRouteHandler.ts b/utils/protectedRouteHandler.ts
new file mode 100644
index 0000000..88df9a1
--- /dev/null
+++ b/utils/protectedRouteHandler.ts
@@ -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
+}