diff --git a/.idea/prettier.xml b/.idea/prettier.xml index b0ab31a..0c83ac4 100644 --- a/.idea/prettier.xml +++ b/.idea/prettier.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js index 8d5469f..c3e0bf1 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,9 +1,7 @@ "use strict"; -const tailwindConfig = require("prettier-plugin-tailwindcss"); - module.exports = { - plugins: [tailwindConfig], - printWidth: 80, + plugins: [require("prettier-plugin-tailwindcss")], + printWidth: 60, tabWidth: 2, useTabs: false, semi: true, diff --git a/package.json b/package.json index 2fee51f..711a552 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,12 @@ "googleapis": "^118.0.0", "jsonwebtoken": "^9.0.0", "mime-types": "^2.1.35", - "next": "13.3.0", + "next": "^13.4.3", "next-seo": "^6.0.0", "nextjs-progressbar": "^0.0.16", "postcss": "8.4.22", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-h5-audio-player": "^3.8.6", "react-icons": "^4.8.0", "react-loading": "^2.0.3", @@ -55,8 +55,9 @@ "@types/node": "18.15.11", "@types/three": "^0.150.2", "cypress": "^12.10.0", + "encoding": "^0.1.13", "eslint": "8.38.0", - "eslint-config-next": "13.3.0", + "eslint-config-next": "^13.4.3", "prettier": "^2.8.7", "prettier-plugin-tailwindcss": "^0.2.7", "typescript": "5.0.4" diff --git a/src/app/(api)/api/banner/route.ts b/src/app/(api)/api/banner/route.ts new file mode 100644 index 0000000..8e70b86 --- /dev/null +++ b/src/app/(api)/api/banner/route.ts @@ -0,0 +1,89 @@ +import { NextRequest, NextResponse } from "next/server"; +import { ErrorResponse } from "types/googleapis"; +import getSearchParams from "utils/getSearchParams"; +import { ExtendedError } from "utils/driveHelper"; +import driveClient from "utils/driveClient"; +import { shortDecrypt } from "utils/encryptionHelper"; + +export async function GET(request: NextRequest) { + const _start = Date.now(); + try { + const { id } = getSearchParams(request.url, ["id"]); + if (!id) { + throw new ExtendedError( + "No id provided", + 400, + "badRequest", + ); + } + + const getMetadata = driveClient.files.get({ + fileId: shortDecrypt(id), + fields: "id, name, mimeType", + }); + const getStream = driveClient.files.get( + { + fileId: shortDecrypt(id), + alt: "media", + }, + { responseType: "stream" }, + ); + + const [metadata, stream] = await Promise.all([ + getMetadata, + getStream, + ]); + + const arrayBuffer = await new Promise( + (resolve, reject) => { + const chunks: Buffer[] = []; + stream.data.on("data", (chunk) => + chunks.push(chunk), + ); + stream.data.on("end", () => { + const buffer = Buffer.concat(chunks); + resolve( + buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength, + ), + ); + }); + stream.data.on("error", reject); + }, + ); + + return new NextResponse(arrayBuffer, { + status: 200, + headers: { + "Content-Type": + metadata.data.mimeType || + "application/octet-stream", + "Cache-Control": + "public, max-age=31536000, immutable", + "Content-Disposition": `inline; filename="${metadata.data.name}"`, + }, + }); + } catch (error: any) { + const payload: ErrorResponse = { + success: false, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + code: error.code || 500, + errors: { + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", + }, + }; + + return NextResponse.json(payload, { + status: payload.code || 500, + }); + } +} diff --git a/src/app/(api)/api/file/[...path]/route.ts b/src/app/(api)/api/file/[...path]/route.ts new file mode 100644 index 0000000..31f7359 --- /dev/null +++ b/src/app/(api)/api/file/[...path]/route.ts @@ -0,0 +1,211 @@ +import { NextRequest, NextResponse } from "next/server"; +import { ErrorResponse } from "types/googleapis"; +import { ExtendedError } from "utils/driveHelper"; +import apiConfig from "config/api.config"; +import driveClient from "utils/driveClient"; +import { + shortDecrypt, + shortEncrypt, +} from "utils/encryptionHelper"; + +export async function GET( + request: NextRequest, + { params }: { params: { path: string[] } }, +) { + const _start = Date.now(); + try { + const pathArray = params.path; + + const getRootId = + apiConfig.files.rootFolder !== "root" + ? apiConfig.files.rootFolder + : driveClient.files + .get({ + fileId: "root", + fields: "id", + }) + .then((res) => res.data.id); + const getPathId = pathArray.map(async (path, index) => { + const query = [ + "trashed = false", + "'me' in owners", + `name = '${path}'`, + ]; + const fetchFolderContents = + await driveClient.files.list({ + q: `${query.join(" and ")}`, + fields: "files(id, name, mimeType, parents)", + }); + + if (!fetchFolderContents.data.files?.length) { + throw new ExtendedError( + `Path ${path} is not found`, + 404, + "notFound", + ); + } + + return { + path, + data: fetchFolderContents.data.files.map( + (file) => ({ + id: file.id, + parents: file.parents?.[0], + mimeType: file.mimeType, + }), + ), + }; + }); + + const [rootId, pathId] = await Promise.all([ + getRootId, + Promise.all(getPathId), + ]); + + const selectedPath: string[] = []; + const mapIds: Record[] = []; + + // Check path validity + pathId.forEach((path, index) => { + if (index === 0) { + // Check if root folder inside data id + const checkRoot = path.data.find( + (item) => item.parents === rootId, + ); + if (!checkRoot) { + throw new ExtendedError( + `Path "${path.path}" is either not found or not valid`, + 400, + "badRequest", + ); + } + + selectedPath.push(checkRoot.id as string); + mapIds.push({ + name: path.path, + id: shortEncrypt(checkRoot.id as string), + mimeType: checkRoot.mimeType as string, + }); + } else { + const checkPath = path.data.find( + (item) => + item.parents === selectedPath[index - 1], + ); + if (!checkPath) { + throw new ExtendedError( + `Path "${path.path}" is either not found or not valid`, + 400, + "badRequest", + ); + } + + selectedPath.push(checkPath.id as string); + mapIds.push({ + name: path.path, + id: shortEncrypt(checkPath.id as string), + mimeType: checkPath.mimeType as string, + }); + } + }); + + // Check for protected folder + const getPassword = mapIds.map(async (path) => { + if ( + path.mimeType !== + "application/vnd.google-apps.folder" + ) + return; + + const query = [ + "trashed = false", + "'me' in owners", + `'${shortDecrypt(path.id)}' in parents`, + `name = '${apiConfig.files.specialFile.password}'`, + ]; + + const folderContents = await driveClient.files.list({ + q: `${query.join(" and ")}`, + fields: "files(id, name, mimeType, parents)", + }); + + if (!folderContents.data.files?.length) return; + + return { + path: path.name, + password: folderContents.data.files?.[0].id, + }; + }); + + let password = await Promise.all(getPassword); + password = password.filter( + (item) => item !== undefined, + ); + const lastProtectedFolder = + password[password.length - 1]; + + // Check password + if (lastProtectedFolder) { + const pass = request.headers.get( + "next-gdrive-password", + ); + if (!pass) { + throw new ExtendedError( + `You are trying to access a protected folder, please provide a password`, + 401, + "unauthorized", + ); + } + const passwordFile = await driveClient.files.get( + { + fileId: lastProtectedFolder.password as string, + alt: "media", + }, + { responseType: "text" }, + ); + + if (passwordFile.data !== pass) { + throw new ExtendedError( + `The password you entered is incorrect`, + 401, + "unauthorized", + ); + } + } + + const payload = { + success: true, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + code: 200, + data: mapIds, + }; + + return NextResponse.json(payload, { + status: 200, + headers: { + "Cache-Control": apiConfig.cache, + }, + }); + } catch (error: any) { + const payload: ErrorResponse = { + success: false, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + code: error.code || 500, + errors: { + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", + }, + }; + + return NextResponse.json(payload, { + status: payload.code || 500, + }); + } +} diff --git a/src/app/(api)/api/files/[encryptedId]/route.ts b/src/app/(api)/api/files/[encryptedId]/route.ts new file mode 100644 index 0000000..1ffc0c1 --- /dev/null +++ b/src/app/(api)/api/files/[encryptedId]/route.ts @@ -0,0 +1,206 @@ +import { + ErrorResponse, + FileResponse, + FilesResponse, +} from "types/googleapis"; +import { NextRequest, NextResponse } from "next/server"; +import getSearchParams from "utils/getSearchParams"; +import { + shortDecrypt, + shortEncrypt, +} from "utils/encryptionHelper"; +import driveClient from "utils/driveClient"; +import apiConfig from "config/api.config"; +import { + ExtendedError, + hiddenFiles, +} from "utils/driveHelper"; + +export async function GET( + request: NextRequest, + { params }: { params: { encryptedId: string } }, +) { + const _start = Date.now(); + + try { + const { pageToken, banner, thumbnail } = + getSearchParams(request.url, ["pageToken", "banner"]); + const id = shortDecrypt(params.encryptedId); + if (id === apiConfig.files.rootFolder) { + return NextResponse.redirect( + `${apiConfig.basePath}/api/files`, + { + status: 301, + }, + ); + } + + const file = await driveClient.files.get({ + fileId: id, + fields: apiConfig.files.field, + }); + + if (!file || file.data.trashed) { + const msg = file.data.trashed + ? "File has been deleted" + : "File not found"; + throw new ExtendedError(msg, 404, "notFound"); + } + + if ( + file.data.mimeType !== + "application/vnd.google-apps.folder" + ) { + if (thumbnail === "1") { + return NextResponse.redirect( + file.data.thumbnailLink as string, + { + status: 302, + }, + ); + } + + const payload: FileResponse = { + success: true, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + file: { + ...file.data, + id: shortEncrypt(file.data.id as string), + webContentLink: + shortEncrypt( + file.data.webContentLink as string, + ) || undefined, + }, + }; + + return NextResponse.json(payload, { + status: 200, + headers: { + "Cache-Control": apiConfig.cache, + }, + }); + } + + const query = [ + `'${id}' in parents`, + "trashed = false", + "'me' in owners", + ]; + const folderContents = await driveClient.files.list({ + q: `${query.join(" and ")}`, + fields: `files(${apiConfig.files.field}), nextPageToken`, + orderBy: apiConfig.files.orderBy, + pageSize: apiConfig.files.itemsPerPage, + pageToken: pageToken || undefined, + }); + + const readmeFile = folderContents.data.files?.find( + (file) => + file.name === apiConfig.files.specialFile.readme, + ); + const bannerFile = folderContents.data.files?.find( + (file) => + file.name?.startsWith( + apiConfig.files.specialFile.banner, + ) && file.mimeType?.startsWith("image/"), + ); + + if (banner === "1") { + if (!bannerFile) { + throw new ExtendedError( + "Banner not found.", + 404, + "notFound", + ); + } + if ( + Number(bannerFile.size) > + apiConfig.files.download.maxFileSize + ) { + return NextResponse.redirect( + bannerFile.webContentLink as string, + { status: 302 }, + ); + } + + return NextResponse.redirect( + `${apiConfig.basePath}/api/banner?id=${shortEncrypt( + bannerFile.id as string, + )}`, + { + status: 302, + }, + ); + } + + const folderList = + folderContents.data.files + ?.filter( + (file) => + file.mimeType === + "application/vnd.google-apps.folder", + ) + .map((file) => ({ + ...file, + id: shortEncrypt(file.id as string), + })) || []; + const fileList = + folderContents.data.files + ?.filter( + (file) => + file.mimeType !== + "application/vnd.google-apps.folder" && + !hiddenFiles.some((hiddenFile) => + file.name?.startsWith(hiddenFile), + ), + ) + .map((file) => ({ + ...file, + id: shortEncrypt(file.id as string), + webContentLink: + shortEncrypt(file.webContentLink as string) || + undefined, + })) || []; + + const payload: FilesResponse = { + success: true, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + folders: folderList, + files: fileList, + isReadmeExists: !!readmeFile, + isBannerExists: !!bannerFile, + nextPageToken: + folderContents.data.nextPageToken || undefined, + }; + + return NextResponse.json(payload, { + status: 200, + headers: { + "Cache-Control": apiConfig.cache, + }, + }); + } catch (error: any) { + const payload: ErrorResponse = { + success: false, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + code: error.code || 500, + errors: { + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", + }, + }; + + return NextResponse.json(payload, { + status: payload.code || 500, + }); + } +} diff --git a/src/app/(api)/api/files/route.ts b/src/app/(api)/api/files/route.ts new file mode 100644 index 0000000..5a3dfaa --- /dev/null +++ b/src/app/(api)/api/files/route.ts @@ -0,0 +1,145 @@ +import { NextRequest, NextResponse } from "next/server"; +import apiConfig from "config/api.config"; +import getSearchParams from "utils/getSearchParams"; +import { + ErrorResponse, + FilesResponse, +} from "types/googleapis"; +import driveClient from "utils/driveClient"; +import { + ExtendedError, + hiddenFiles, +} from "utils/driveHelper"; +import { shortEncrypt } from "utils/encryptionHelper"; + +export async function GET(request: NextRequest) { + const _start = Date.now(); + try { + const { pageToken, banner } = getSearchParams( + request.url, + ["pageToken", "banner"], + ); + + const query: string[] = [ + "trashed = false", + "'me' in owners", + `parents = '${apiConfig.files.rootFolder}'`, + ]; + const fetchFolderContents = + await driveClient.files.list({ + q: `${query.join(" and ")}`, + fields: `files(${apiConfig.files.field}), nextPageToken`, + orderBy: apiConfig.files.orderBy, + pageSize: apiConfig.files.itemsPerPage, + pageToken: pageToken || undefined, + }); + + const readmeFile = fetchFolderContents.data.files?.find( + (file) => + file.name === apiConfig.files.specialFile.readme, + ); + const bannerFile = fetchFolderContents.data.files?.find( + (file) => + file.name?.startsWith( + apiConfig.files.specialFile.banner, + ) && file.mimeType?.startsWith("image/"), + ); + + if (banner === "1") { + if (!bannerFile) { + throw new ExtendedError( + "Banner not found.", + 404, + "notFound", + ); + } + if ( + Number(bannerFile.size) > + apiConfig.files.download.maxFileSize + ) { + return NextResponse.redirect( + bannerFile.webContentLink as string, + { status: 302 }, + ); + } + + return NextResponse.redirect( + `${apiConfig.basePath}/api/banner?id=${shortEncrypt( + bannerFile.id as string, + )}`, + { + status: 302, + }, + ); + } + + const folderList = + fetchFolderContents.data.files + ?.filter( + (file) => + file.mimeType === + "application/vnd.google-apps.folder", + ) + .map((file) => ({ + ...file, + id: shortEncrypt(file.id as string), + })) || []; + const fileList = + fetchFolderContents.data.files + ?.filter( + (file) => + file.mimeType !== + "application/vnd.google-apps.folder" && + !hiddenFiles.some((hiddenFile) => + file.name?.startsWith(hiddenFile), + ), + ) + .map((file) => ({ + ...file, + id: shortEncrypt(file.id as string), + webContentLink: + shortEncrypt(file.webContentLink as string) || + undefined, + })) || []; + + const payload: FilesResponse = { + success: true, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + folders: folderList, + files: fileList, + isReadmeExists: !!readmeFile, + isBannerExists: !!bannerFile, + nextPageToken: + fetchFolderContents.data.nextPageToken || undefined, + }; + + return NextResponse.json(payload, { + status: 200, + headers: { + "Cache-Control": apiConfig.cache, + }, + }); + } catch (error: any) { + const payload: ErrorResponse = { + success: false, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + code: error.code || 500, + errors: { + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", + }, + }; + + return NextResponse.json(payload, { + status: payload.code || 500, + }); + } +} diff --git a/src/app/(api)/api/test/route.ts b/src/app/(api)/api/test/route.ts new file mode 100644 index 0000000..6137760 --- /dev/null +++ b/src/app/(api)/api/test/route.ts @@ -0,0 +1,94 @@ +import { NextResponse } from "next/server"; +import { ErrorResponse } from "types/googleapis"; +import driveClient from "utils/driveClient"; + +export async function GET() { + const _start = Date.now(); + try { + const ids = [ + "18h88Fit0MgIrHTGBEtpIbcHIZ_GEgpuP", + "1KgPV6QB1GYT8fmn2uTfbtr9rDXqcRR0j", + "1pctigJQKaF7GbDU1t8s0gT-0fPfNhl0B", + "16p09HGtImeuuvn06W6hDlDzSPqF4e80g", + ]; + const fields = "id, name, mimeType, parents"; + const startFetch0 = Date.now(); + const id0 = driveClient.files + .get({ + fileId: ids[0], + fields, + }) + .then((res) => res.data); + const startFetch1 = Date.now(); + const id1 = driveClient.files.get({ + fileId: ids[1], + fields, + }); + const startFetch2 = Date.now(); + const id2 = driveClient.files + .get({ + fileId: ids[2], + fields, + }) + .then((res) => res.data); + const startFetch3 = Date.now(); + const id3 = driveClient.files.get({ + fileId: ids[3], + fields, + }); + const fetchStarted = Date.now(); + const files = await Promise.all([id0, id1, id2, id3]); + const fetchFinished = Date.now(); + // const files = await Promise.all( + // ids.map(async (id) => { + // const file = await driveClient.files.get({ + // fileId: id, + // fields: "id, name, mimeType, parents", + // }); + // return file.data; + // }), + // ); + + const payload = { + success: true, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + code: 200, + timing: { + fetch0: startFetch0, + fetch1: startFetch1, + fetch2: startFetch2, + fetch3: startFetch3, + fetchStarted, + fetchFinished, + duration: fetchFinished - startFetch0, + }, + files, + }; + + return NextResponse.json(payload, { + status: 200, + }); + } catch (error: any) { + const payload: ErrorResponse = { + success: false, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + code: error.code || 500, + errors: { + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", + }, + }; + + return NextResponse.json(payload, { + status: payload.code || 500, + }); + } +} diff --git a/src/app/(api)/api/validatePath/route.ts b/src/app/(api)/api/validatePath/route.ts new file mode 100644 index 0000000..8b770b2 --- /dev/null +++ b/src/app/(api)/api/validatePath/route.ts @@ -0,0 +1,241 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + ErrorResponse, + TPath, + ValidatePathResponse, +} from "types/googleapis"; +import { ExtendedError } from "utils/driveHelper"; +import apiConfig from "config/api.config"; +import driveClient from "utils/driveClient"; +import { + shortDecrypt, + shortEncrypt, +} from "utils/encryptionHelper"; +import { cookies } from "next/headers"; + +export async function GET(request: NextRequest) { + const _start = Date.now(); + try { + const path = new URL(request.url).searchParams.get( + "path", + ); + if (!path) { + throw new ExtendedError( + "No path provided", + 400, + "badRequest", + ); + } + + let pathArray: string[] = path.split(/[\\/]/); + pathArray = pathArray.filter((path) => path !== ""); + + const getRootId = + apiConfig.files.rootFolder !== "root" + ? apiConfig.files.rootFolder + : driveClient.files + .get({ + fileId: "root", + fields: "id", + }) + .then((res) => res.data.id); + const getPathId = pathArray.map(async (path, index) => { + const query = [ + "trashed = false", + "'me' in owners", + `name = '${path}'`, + ]; + const fetchFolderContents = + await driveClient.files.list({ + q: `${query.join(" and ")}`, + fields: "files(id, name, mimeType, parents)", + }); + + if (!fetchFolderContents.data.files?.length) { + throw new ExtendedError( + `Path ${path} is not found`, + 404, + "notFound", + ); + } + + return { + path, + data: fetchFolderContents.data.files.map( + (file) => ({ + id: file.id, + parents: file.parents?.[0], + mimeType: file.mimeType, + }), + ), + }; + }); + + const [rootId, pathId] = await Promise.all([ + getRootId, + Promise.all(getPathId), + ]); + + const selectedPath: string[] = []; + const mapIds: TPath[] = []; + + // Check path validity + pathId.forEach((path, index) => { + if (index === 0) { + // Check if root folder inside data id + const checkRoot = path.data.find( + (item) => item.parents === rootId, + ); + if (!checkRoot) { + throw new ExtendedError( + `Path "${path.path}" is either not found or not valid`, + 400, + "badRequest", + ); + } + + selectedPath.push(checkRoot.id as string); + mapIds.push({ + name: path.path, + id: shortEncrypt(checkRoot.id as string), + mimeType: checkRoot.mimeType as string, + }); + } else { + const checkPath = path.data.find( + (item) => + item.parents === selectedPath[index - 1], + ); + if (!checkPath) { + throw new ExtendedError( + `Path "${path.path}" is either not found or not valid`, + 400, + "badRequest", + ); + } + + selectedPath.push(checkPath.id as string); + mapIds.push({ + name: path.path, + id: shortEncrypt(checkPath.id as string), + mimeType: checkPath.mimeType as string, + }); + } + }); + + // Check for protected folder + const getPassword = mapIds.map(async (path) => { + if ( + path.mimeType !== + "application/vnd.google-apps.folder" + ) + return; + + const query = [ + "trashed = false", + "'me' in owners", + `'${shortDecrypt(path.id)}' in parents`, + `name = '${apiConfig.files.specialFile.password}'`, + ]; + + const folderContents = await driveClient.files.list({ + q: `${query.join(" and ")}`, + fields: "files(id, name, mimeType, parents)", + }); + + if (!folderContents.data.files?.length) return; + + return { + path: path.name, + password: folderContents.data.files?.[0].id, + }; + }); + + let password = await Promise.all(getPassword); + password = password.filter( + (item) => item !== undefined, + ); + const lastProtectedFolder = + password[password.length - 1]; + + // Check password + if (lastProtectedFolder?.password) { + const fetchPassword = await driveClient.files.get( + { + fileId: lastProtectedFolder.password, + alt: "media", + }, + { responseType: "text" }, + ); + + const userPassword = cookies().get( + `next-gdrive-password/${encodeURIComponent( + lastProtectedFolder.path, + )}`, + )?.value; + console.log( + `next-gdrive-password/${encodeURIComponent( + lastProtectedFolder.path, + )}`, + shortEncrypt("loremipsum"), + ); + if (!userPassword) { + throw new ExtendedError( + `You are not authorized to access this folder`, + 401, + "unauthorized", + ); + } + console.log( + shortDecrypt(userPassword), + fetchPassword.data, + ); + if ( + shortDecrypt(userPassword) !== fetchPassword.data + ) { + throw new ExtendedError( + `The password you entered is incorrect`, + 401, + "unauthorized", + ); + } + } + + const payload: ValidatePathResponse = { + success: true, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + data: mapIds, + password: lastProtectedFolder?.password + ? shortEncrypt(lastProtectedFolder.password) + : undefined, + }; + + return NextResponse.json(payload, { + status: 200, + headers: { + "Cache-Control": apiConfig.cache, + }, + }); + } catch (error: any) { + const payload: ErrorResponse = { + success: false, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + code: error.code || 500, + errors: { + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", + }, + }; + + return NextResponse.json(payload, { + status: payload.code || 500, + }); + } +} diff --git a/src/app/(setup)/next-gdrive-index/page.tsx b/src/app/(setup)/next-gdrive-index/page.tsx new file mode 100644 index 0000000..1ad0374 --- /dev/null +++ b/src/app/(setup)/next-gdrive-index/page.tsx @@ -0,0 +1,3 @@ +function SetupPage() {} + +export default SetupPage; diff --git a/src/app/(setup)/next-gdrive-index/step-1/page.tsx b/src/app/(setup)/next-gdrive-index/step-1/page.tsx new file mode 100644 index 0000000..5c257bb --- /dev/null +++ b/src/app/(setup)/next-gdrive-index/step-1/page.tsx @@ -0,0 +1,5 @@ +function SetupFirstStep() { + return
; +} + +export default SetupFirstStep; diff --git a/src/app/[...path]/error.tsx b/src/app/[...path]/error.tsx new file mode 100644 index 0000000..68cea11 --- /dev/null +++ b/src/app/[...path]/error.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { useEffect } from "react"; + +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + useEffect(() => { + console.error(error); + }, [error]); + + return ( +
+

Something went wrong!

+ +
+ ); +} diff --git a/src/app/[...path]/page.tsx b/src/app/[...path]/page.tsx new file mode 100644 index 0000000..5c7202a --- /dev/null +++ b/src/app/[...path]/page.tsx @@ -0,0 +1,26 @@ +import { cookies } from "next/headers"; +import axios from "axios"; +import { ValidatePathResponse } from "types/googleapis"; + +async function _validatePath(path: string) { + const { data } = await axios.get( + `/api/validate-path?path=${path}`, + ); + return data; +} + +type Props = { + params: { + path: string[]; + }; +}; +async function ListIdPage({ params }: Props) { + const validatePath = await _validatePath( + params.path.join("/"), + ); + const c = cookies(); + const token = c.has("token") ? c.get("token")?.name : ""; + return
lorem - {token}
; +} + +export default ListIdPage; diff --git a/src/app/contextWrapper.tsx b/src/app/contextWrapper.tsx new file mode 100644 index 0000000..bb2c076 --- /dev/null +++ b/src/app/contextWrapper.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { ThemeContext, TTheme } from "context/themeContext"; +import { useEffect, useState } from "react"; +import { TLayout } from "context/layoutContext"; + +type Props = { + children: React.ReactNode; +}; +function ContextWrapper({ children }: Props) { + const [theme, setTheme] = useState("light"); + const [layout, setLayout] = useState("grid"); + + useEffect(() => { + console.log("ContextWrapper useEffect"); + if (typeof window === "undefined") return; + const lsTheme = localStorage.getItem("theme"); + const lsLayout = localStorage.getItem("layout"); + const lsSitePassword = + localStorage.getItem("sitePassword"); + + if ( + lsTheme && + (lsTheme === "dark" || lsTheme === "light") + ) { + setTheme(lsTheme as TTheme); + } + if ( + lsLayout && + (lsLayout === "grid" || lsLayout === "list") + ) { + setLayout(lsLayout as TLayout); + } + }, []); + + useEffect(() => { + if (theme === "light") { + document.body.classList.remove("dark"); + } else { + document.body.classList.add("dark"); + } + }, [theme]); + + return ( + <> + { + if (typeof window !== "undefined") { + localStorage.setItem("theme", theme); + } + setTheme(theme); + }, + }} + > + {children} + + + ); +} + +export default ContextWrapper; diff --git a/src/app/download/[...path]/route.ts b/src/app/download/[...path]/route.ts new file mode 100644 index 0000000..f47ca13 --- /dev/null +++ b/src/app/download/[...path]/route.ts @@ -0,0 +1,142 @@ +import { ErrorResponse } from "types/googleapis"; +import { NextRequest, NextResponse } from "next/server"; +import driveClient from "utils/driveClient"; +import apiConfig from "config/api.config"; +import { ExtendedError } from "utils/driveHelper"; +import axios from "axios"; + +export async function GET( + request: NextRequest, + { params }: { params: { path: string[] } }, +) { + const _start = Date.now(); + + try { + const validatePath = await axios.get( + `${ + apiConfig.basePath + }/api/validatePath?path=${params.path.join("/")}`, + ); + + return NextResponse.json( + { data: validatePath.data }, + { status: 200 }, + ); + + let id = ""; + // const id = shortDecrypt(params.encryptedId); + // const { token } = getSearchParams(request.url, [ + // "token", + // ]); + // + // if (!apiConfig.files.download.allowProtectedFile) { + // const decryptToken = shortDecrypt(token ? token : ""); + // const parsedToken = JSON.parse( + // decryptToken || "{}", + // ) as DownloadToken; + // if (!parsedToken) { + // throw new ExtendedError( + // "Protected file not allowed.", + // 403, + // "forbidden", + // ); + // } + // } + + const getMetadata = driveClient.files.get({ + fileId: id, + fields: apiConfig.files.field, + }); + const getStream = driveClient.files.get( + { + fileId: id, + alt: "media", + }, + { responseType: "stream" }, + ); + + const [metadata, stream] = await Promise.all([ + getMetadata, + getStream, + ]); + + if (!metadata || metadata.data.trashed) { + const msg = metadata.data.trashed + ? "File has been deleted" + : "File not found"; + throw new ExtendedError(msg, 404, "notFound"); + } + + if ( + metadata.data.mimeType === + "application/vnd.google-apps.folder" + ) { + throw new ExtendedError( + "Can't download folder", + 400, + "badRequest", + ); + } + if ( + Number(metadata.data.size) > + apiConfig.files.download.maxFileSize + ) { + return NextResponse.redirect( + metadata.data.webContentLink as string, + { status: 302 }, + ); + } + + const arrayBuffer = await new Promise( + (resolve, reject) => { + const chunks: Buffer[] = []; + stream.data.on("data", (chunk) => + chunks.push(chunk), + ); + stream.data.on("end", () => { + const buffer = Buffer.concat(chunks); + resolve( + buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength, + ), + ); + }); + stream.data.on("error", reject); + }, + ); + + return new NextResponse(arrayBuffer, { + status: 200, + headers: { + "Content-Type": + metadata.data.mimeType || + "application/octet-stream", + "Cache-Control": + "public, max-age=31536000, immutable", + "Content-Disposition": `inline; filename="${metadata.data.name}"`, + }, + }); + } catch (error: any) { + const payload: ErrorResponse = { + success: false, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + code: error.code || 500, + errors: { + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", + }, + }; + + return NextResponse.json(payload, { + status: payload.code || 500, + }); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..be3a0d1 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,70 @@ +import { Metadata } from "next"; + +import Navbar from "components/Navbar"; +import Footer from "components/Footer"; +import ContextWrapper from "./contextWrapper"; +import { + Exo_2, + JetBrains_Mono, + Source_Sans_Pro, +} from "next/font/google"; + +import siteConfig from "config/site.config"; +import "styles/globals.css"; + +const exo2 = Exo_2({ + weight: ["300", "400", "600", "700"], + style: ["normal", "italic"], + display: "auto", + subsets: ["latin", "latin-ext"], + variable: "--font-exo2", +}); +const sourceSansPro = Source_Sans_Pro({ + weight: ["300", "400", "600", "700"], + style: ["normal", "italic"], + display: "auto", + subsets: ["latin", "latin-ext"], + variable: "--font-source-sans-pro", +}); +const jetBrainsMono = JetBrains_Mono({ + weight: ["300", "400", "600", "700"], + style: ["normal", "italic"], + display: "auto", + subsets: ["latin", "latin-ext"], + variable: "--font-jetbrains-mono", +}); + +export const metadata: Metadata = { + title: siteConfig.siteName, + description: siteConfig.siteDescription, +}; + +type Props = { + children: React.ReactNode; +}; + +function RootLayout({ children }: Props) { + return ( + + + +
+ +
+ {children} +
+
+
+
+ + + ); +} + +export default RootLayout; diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..9baac6d --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,22 @@ +import driveClient from "utils/driveClient"; + +async function getRootFiles() {} +async function getPassword() {} +async function getReadme() {} + +async function RootPage() { + const start = new Date().getTime(); + const [files, password, readme] = await Promise.all([ + getRootFiles(), + getPassword(), + getReadme(), + ]); + const end = new Date().getTime(); + return ( +
+

{end - start}

+
+ ); +} + +export default RootPage; diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx new file mode 100644 index 0000000..76766eb --- /dev/null +++ b/src/components/Footer/index.tsx @@ -0,0 +1,31 @@ +import siteConfig from "config/site.config"; +import Link from "next/link"; + +function Footer() { + return ( +
+ + {siteConfig.footer.renderYear && + new Date().getFullYear()}{" "} + {siteConfig.footer.text} - Powered by{" "} + + next-gdrive-index + {" "} + ❤️ + +
+ ); +} + +export default Footer; diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx new file mode 100644 index 0000000..f559e94 --- /dev/null +++ b/src/components/Navbar/index.tsx @@ -0,0 +1,186 @@ +"use client"; + +import Link from "next/link"; + +import siteConfig from "config/site.config"; +import { + MdClose, + MdDarkMode, + MdLightMode, + MdLogout, + MdMenu, + MdSearch, +} from "react-icons/md"; +import { useContext, useState } from "react"; +import { + ThemeContext, + TThemeContext, +} from "context/themeContext"; + +function Navbar() { + const { theme, setTheme } = + useContext(ThemeContext); + const [isMenuOpen, setIsMenuOpen] = + useState(false); + + return ( + <> + + +
+ {siteConfig.navbar.links.map((link, index) => ( + + {link.icon && } + {link.name} + + ))} +
+ + ); +} + +export default Navbar; diff --git a/src/components/layout/Footer/index.tsx b/src/components/layout/Footer/index.tsx deleted file mode 100644 index 8919336..0000000 --- a/src/components/layout/Footer/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import siteConfig from "config/site.config"; - -export default function Footer() { - const currentYear = new Date().getFullYear(); - - return ( -
- - {currentYear} {siteConfig.footerText} - Powered by{" "} - - next-gdrive-index - {" "} - ❤️ - -
- ); -} diff --git a/src/config/api.config.js b/src/config/api.config.js index bb65dd8..f019bba 100644 --- a/src/config/api.config.js +++ b/src/config/api.config.js @@ -2,46 +2,111 @@ module.exports = { // Client ID are safe to expose client_id: "126409166174-l2unckghm8d5m1gp3deu0uaps5son64f.apps.googleusercontent.com", - // Client secret and refresh token are encrypted - // Using encryption key - // https://drive.mbaharip.com/gudora-setup - client_secret: - "54f0505d11a8fe04d22ec642cdde4728:a6f911c70b0305a51160a2133a9bd2ada3c120567857002311f17bf29cc69e07d7f1a9ca20fd119d684191aee6e3866a", - refresh_token: - "209b4d2ff44bf877ce0636ddc81cdc9a:4a40e172ba5d6c1ee8daf50389841bd2c9d1153163196da0dab63c07b6f305bedafaa209a9e6adbb8d2377c74ba98cf818b6e73b8f57098686ed0b89bec93b0186047ccaab8804310a2ab4e46069d1cac8c322f0f1e206808ac62c18639610be07559eebad904905ba185cae8ba9fbe9", + dev_client_id: + "126409166174-l0f9hdblsrmhkt9jeue9m8o93skfs1sr.apps.googleusercontent.com", - files: { - // How many files to show per page - itemsPerPage: 25, - // Max number of files to show in search result - searchResult: 5, - // Starting point of the drive - // Use 'root' to use My Drive as starting point - // Or use folder id to use a specific folder as starting point - // TODO: Change when final - // rootFolder: "root", - rootFolder: "1KgPV6QB1GYT8fmn2uTfbtr9rDXqcRR0j", // Test folder - // Limit breadcrumb to specific depth - // 0 = Unlimited - // 1 = Only show current folder - // 2 = Show current folder and its parent - // 3 = Show current folder and its parent and grandparent - // and so on. - // Warning: More parent = More API calls = Slower loading time - // There are no workaround for this yet, since Google Drive API v3 return only 1 parent - // Default: 2 - breadcrumbDepth: 2, + /** + * Change this value to match your platform. + */ + basePath: + process.env.NODE_ENV === "production" + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:5000", + + old: { + files: { + // How many files to show per page + itemsPerPage: 25, + // Max number of files to show in search result + searchResult: 5, + // Starting point of the drive + // Use 'root' to use My Drive as starting point + // Or use folder id to use a specific folder as starting point + // TODO: Change when final + // rootFolder: "root", + rootFolder: "1KgPV6QB1GYT8fmn2uTfbtr9rDXqcRR0j", // Test folder + // Limit breadcrumb to specific depth + // 0 = Unlimited + // 1 = Only show current folder + // 2 = Show current folder and its parent + // 3 = Show current folder and its parent and grandparent + // and so on. + // Warning: More parent = More API calls = Slower loading time + // There are no workaround for this yet, since Google Drive API v3 return only 1 parent + // Default: 2 + breadcrumbDepth: 2, + }, + + // Cache control header + // https://web.dev/uses-long-cache-ttl/ + // Default: 1 minute + cache: "s-maxage=60, stale-while-revalidate", + + // Max response body size + // If you're using Vercel, the max response size is 4.5MB + // https://vercel.com/docs/platform/limits#serverless-function-payload-size-limit + // If you're using another platform that has different limit, + // change this value to match your platform. + maxResponseSize: 4 * 1024 * 1024, }, - // Cache control header - // https://web.dev/uses-long-cache-ttl/ - // Default: 1 minute - cache: "s-maxage=60, stale-while-revalidate", + files: { + field: + "id, name, mimeType, thumbnailLink, fileExtension, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, webContentLink, iconLink, trashed", + orderBy: "folder, name asc, modifiedTime desc", + /** + * Special file names that will be used for certain purposes + * These files will be ignored when searching for files + * and will be hidden from the file list + */ + specialFile: { + password: ".password", + readme: ".readme.md", + /** + * Banner are used for generating custom open graph image for folder + * By default, all folder will use the og.png inside public folder + */ + banner: ".banner", + }, + itemsPerPage: 50, + searchResult: 10, + /** + * Starting point of the drive + * Use 'root' to use My Drive as starting point + * Or use folder id to use a specific folder as starting point + */ + rootFolder: "1KgPV6QB1GYT8fmn2uTfbtr9rDXqcRR0j", + /** + * Limit breadcrumb to specific depth + * 0 = Unlimited + * 1 = Only show current file + * 2 = Show current file and its parent + * 3 = Show current file, its parent and grandparent + * and so on. + * Default: 2 + */ + breadcrumbDepth: 2, + download: { + /** + * Allow user to download protected file without password + * If this set to false, the download link will have temporary token to download the files + * If this set to true, the download link will be permanent + * Default: false + */ + allowProtectedFile: false, + temporaryTokenDuration: 60 * 60, // 1 hour + /** + * If you're using Vercel, the max response size is 4.5MB + * https://vercel.com/docs/platform/limits#serverless-function-payload-size-limit + * If you're using another platform that has different limit, + * change this value to match your platform. + */ + maxFileSize: 4 * 1024 * 1024, + }, + }, - // Max response body size - // If you're using Vercel, the max response size is 4.5MB - // https://vercel.com/docs/platform/limits#serverless-function-payload-size-limit - // If you're using another platform that has different limit, - // change this value to match your platform. - maxResponseSize: 4 * 1024 * 1024, + /** + * https://web.dev/uses-long-cache-ttl/ + */ + cache: "s-maxage=60, stale-while-revalidate", }; diff --git a/src/config/site.config.js b/src/config/site.config.js index 32e20a7..29a94e7 100644 --- a/src/config/site.config.js +++ b/src/config/site.config.js @@ -1,12 +1,18 @@ -import { BsDiscord, BsGithub, BsPaypal } from "react-icons/bs"; +import { + BsDiscord, + BsEnvelopeAt, + BsGithub, + BsPaypal, +} from "react-icons/bs"; -const config = { +const oldConfig = { /* Site MetaData */ // The name of the site siteName: "mbahArip Stash", // The description of the site // Used in meta tags and document head - siteDescription: "Personal Stash of mbahArip, a place to store my files.", + siteDescription: + "Personal Stash of mbahArip, a place to store my files.", // Fav icon of the site // Also used as the logo of the site on the navbar siteIcon: "/favicon.svg", @@ -90,5 +96,59 @@ const config = { pdfProvider: "mozilla", }, }; +const config = { + /* Site MetaData */ + siteName: "mbahArip Stash", + siteDescription: + "Personal Stash of mbahArip, a place to store my files.", + + /* General */ + /** + * Site wide password protection + */ + privateIndex: true, + indexPassword: + "640e3e38dd31aec254f214ba38541a82ddb615e73ce9c6129f33ef549b154ab9", + + navbar: { + /** + * Title beside the logo + * If not set, will be using `siteName` instead + */ + title: "", + links: [ + { + icon: BsGithub, + name: "GitHub", + href: "https://www.github.com/mbaharip", + newTab: true, + }, + { + icon: BsPaypal, + name: "Donate", + href: "https://www.paypal.me/mbaharip", + newTab: true, + }, + { + icon: BsEnvelopeAt, + name: "Email", + href: "mailto:support@mbaharip.com", + }, + { + name: "Main Website", + href: "https://www.mbaharip.com", + newTab: true, + }, + ], + }, + footer: { + /** + * Footer text will be rendered as + * {year} {footerText} - Powered by next-gdrive-index ❤️ + */ + text: "mbahArip Stash", + renderYear: true, + }, +}; export default config; diff --git a/src/context/themeContext.tsx b/src/context/themeContext.tsx index 92ccdcd..71cd06c 100644 --- a/src/context/themeContext.tsx +++ b/src/context/themeContext.tsx @@ -1,4 +1,10 @@ -import React, { createContext, useState, useEffect } from "react"; +"use client"; + +import React, { + createContext, + useState, + useEffect, +} from "react"; export type TTheme = "dark" | "light"; diff --git a/src/pages/api/banner/[folderId]/index.ts b/src/pages/apis/banner/[folderId]/index.ts similarity index 74% rename from src/pages/api/banner/[folderId]/index.ts rename to src/pages/apis/banner/[folderId]/index.ts index 377ec96..ece6f05 100644 --- a/src/pages/api/banner/[folderId]/index.ts +++ b/src/pages/apis/banner/[folderId]/index.ts @@ -1,9 +1,12 @@ import initMiddleware from "utils/apiMiddleware"; import { NextApiRequest, NextApiResponse } from "next"; -import { BannerResponse, ErrorResponse } from "types/googleapis"; +import { + BannerResponse, + ErrorResponse, +} from "types/googleapis"; import { ExtendedError } from "utils/driveHelper"; import driveClient from "utils/driveClient"; -import { urlEncrypt } from "utils/encryptionHelper"; +import { shortEncrypt } from "utils/encryptionHelper"; export default initMiddleware(async function handler( request: NextApiRequest, @@ -14,7 +17,9 @@ export default initMiddleware(async function handler( try { const { folderId } = request.query; - const [name, partialId] = (folderId as string).split(":"); + const [name, partialId] = (folderId as string).split( + ":", + ); if (!name || !partialId || partialId.length !== 8) { throw new ExtendedError( @@ -45,7 +50,7 @@ export default initMiddleware(async function handler( } payload.banner = { - id: urlEncrypt(banner.id as string), + id: shortEncrypt(banner.id as string), name: banner.name as string, }; @@ -57,11 +62,19 @@ export default initMiddleware(async function handler( responseTime: Date.now() - _start, code: error.code || 500, errors: { - message: error.errors?.[0].message || error.message || "Unknown error", - reason: error.errors?.[0].reason || error.cause || "internalError", + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", }, }; - return response.status(payload.code || 500).json(payload); + return response + .status(payload.code || 500) + .json(payload); } }); diff --git a/src/pages/api/banner/index.ts b/src/pages/apis/banner/index.ts similarity index 74% rename from src/pages/api/banner/index.ts rename to src/pages/apis/banner/index.ts index b7faeb7..8ba889a 100644 --- a/src/pages/api/banner/index.ts +++ b/src/pages/apis/banner/index.ts @@ -1,9 +1,12 @@ -import { BannerResponse, ErrorResponse } from "types/googleapis"; +import { + BannerResponse, + ErrorResponse, +} from "types/googleapis"; import { NextApiRequest, NextApiResponse } from "next"; import initMiddleware from "utils/apiMiddleware"; import apiConfig from "config/api.config"; import driveClient from "utils/driveClient"; -import { urlEncrypt } from "utils/encryptionHelper"; +import { shortEncrypt } from "utils/encryptionHelper"; export default initMiddleware(async function handler( request: NextApiRequest, @@ -31,7 +34,7 @@ export default initMiddleware(async function handler( } payload.banner = { - id: urlEncrypt(banner.id as string), + id: shortEncrypt(banner.id as string), name: banner.name as string, }; @@ -43,11 +46,19 @@ export default initMiddleware(async function handler( responseTime: Date.now() - _start, code: error.code || 500, errors: { - message: error.errors?.[0].message || error.message || "Unknown error", - reason: error.errors?.[0].reason || error.cause || "internalError", + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", }, }; - return response.status(payload.code || 500).json(payload); + return response + .status(payload.code || 500) + .json(payload); } }); diff --git a/src/pages/api/encrypt.ts b/src/pages/apis/encrypt.ts similarity index 57% rename from src/pages/api/encrypt.ts rename to src/pages/apis/encrypt.ts index 6558716..fbbaceb 100644 --- a/src/pages/api/encrypt.ts +++ b/src/pages/apis/encrypt.ts @@ -4,8 +4,8 @@ import { ExtendedError } from "utils/driveHelper"; import { decrypt, encrypt, - urlDecrypt, - urlEncrypt, + shortDecrypt, + shortEncrypt, } from "utils/encryptionHelper"; export default function handler( @@ -15,23 +15,36 @@ export default function handler( const _start = Date.now(); try { const { text, isUrl, isDecrypt } = request.query; - if (!text) throw new ExtendedError("Missing text", 400, "missingText"); + if (!text) + throw new ExtendedError( + "Missing text", + 400, + "missingText", + ); let encrypted; let decrypted; if (!isDecrypt) { if (isUrl) { - const encodedText = encodeURIComponent(text as string); - encrypted = urlEncrypt(encodedText); + const encodedText = encodeURIComponent( + text as string, + ); + encrypted = shortEncrypt(encodedText); } else { - const encodedText = encodeURIComponent(text as string); + const encodedText = encodeURIComponent( + text as string, + ); encrypted = encrypt(encodedText); } } else { if (isUrl) { - decrypted = decodeURIComponent(urlDecrypt(text as string)); + decrypted = decodeURIComponent( + shortDecrypt(text as string), + ); } else { - decrypted = decodeURIComponent(decrypt(text as string)); + decrypted = decodeURIComponent( + decrypt(text as string), + ); } } @@ -55,11 +68,19 @@ export default function handler( responseTime: Date.now() - _start, code: error.code || 500, errors: { - message: error.errors?.[0].message || error.message || "Unknown error", - reason: error.errors?.[0].reason || error.cause || "internalError", + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", }, }; - return response.status(payload.code || 500).json(payload); + return response + .status(payload.code || 500) + .json(payload); } } diff --git a/src/pages/apis/exchangeAuthCode.ts b/src/pages/apis/exchangeAuthCode.ts new file mode 100644 index 0000000..9b21848 --- /dev/null +++ b/src/pages/apis/exchangeAuthCode.ts @@ -0,0 +1,58 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { ErrorResponse } from "types/googleapis"; +import { ExtendedError } from "utils/driveHelper"; +import { + decrypt, + encrypt, + shortDecrypt, + shortEncrypt, +} from "utils/encryptionHelper"; +import { OAuth2Client } from "google-auth-library"; +import apiConfig from "config/api.config"; + +export default async function handler( + request: NextApiRequest, + response: NextApiResponse, +) { + const _start = Date.now(); + try { + const { code } = request.body; + if (!code) + throw new ExtendedError( + "Missing code", + 400, + "missingCode", + ); + + const oauth2Client = new OAuth2Client( + "126409166174-l0f9hdblsrmhkt9jeue9m8o93skfs1sr.apps.googleusercontent.com", + "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", + "http://localhost", + ); + + const tokens = await oauth2Client.getToken(code); + + return response.status(200).json(tokens.tokens); + } catch (error: any) { + const payload: ErrorResponse = { + success: false, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + code: error.code || 500, + errors: { + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", + }, + }; + + return response + .status(payload.code || 500) + .json(payload); + } +} diff --git a/src/pages/apis/exchangeRefreshCode.ts b/src/pages/apis/exchangeRefreshCode.ts new file mode 100644 index 0000000..655ca0c --- /dev/null +++ b/src/pages/apis/exchangeRefreshCode.ts @@ -0,0 +1,61 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { ErrorResponse } from "types/googleapis"; +import { ExtendedError } from "utils/driveHelper"; +import { + decrypt, + encrypt, + shortDecrypt, + shortEncrypt, +} from "utils/encryptionHelper"; +import { OAuth2Client } from "google-auth-library"; +import apiConfig from "config/api.config"; + +export default async function handler( + request: NextApiRequest, + response: NextApiResponse, +) { + const _start = Date.now(); + try { + const { code } = request.body; + if (!code) + throw new ExtendedError( + "Missing code", + 400, + "missingCode", + ); + + const oauth2Client = new OAuth2Client( + "126409166174-l0f9hdblsrmhkt9jeue9m8o93skfs1sr.apps.googleusercontent.com", + "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", + "http://localhost", + ); + oauth2Client.setCredentials({ + refresh_token: code, + }); + + const tokens = await oauth2Client.getAccessToken(); + + return response.status(200).json(tokens); + } catch (error: any) { + const payload: ErrorResponse = { + success: false, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + code: error.code || 500, + errors: { + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", + }, + }; + + return response + .status(payload.code || 500) + .json(payload); + } +} diff --git a/src/pages/api/files/[id]/getPath.ts b/src/pages/apis/files/[id]/getPath.ts similarity index 100% rename from src/pages/api/files/[id]/getPath.ts rename to src/pages/apis/files/[id]/getPath.ts diff --git a/src/pages/api/files/[id]/index.ts b/src/pages/apis/files/[id]/index.ts similarity index 59% rename from src/pages/api/files/[id]/index.ts rename to src/pages/apis/files/[id]/index.ts index 40e0995..cf2074d 100644 --- a/src/pages/api/files/[id]/index.ts +++ b/src/pages/apis/files/[id]/index.ts @@ -2,12 +2,19 @@ // [filename]:[partialId] import { NextApiRequest, NextApiResponse } from "next"; -import { ErrorResponse, FileResponse, FilesResponse } from "types/googleapis"; -import { ExtendedError, hiddenFiles } from "utils/driveHelper"; +import { + ErrorResponse, + FileResponse, + FilesResponse, +} from "types/googleapis"; +import { + ExtendedError, + hiddenFiles, +} from "utils/driveHelper"; import driveClient from "utils/driveClient"; import apiConfig from "config/api.config"; import initMiddleware from "utils/apiMiddleware"; -import { urlEncrypt } from "utils/encryptionHelper"; +import { shortEncrypt } from "utils/encryptionHelper"; export default initMiddleware(async function handler( request: NextApiRequest, @@ -16,7 +23,8 @@ export default initMiddleware(async function handler( const _start = Date.now(); try { - const { id, pageToken, download, thumbnail, banner } = request.query; + const { id, pageToken, download, thumbnail, banner } = + request.query; const [name, partialId] = (id as string).split(":"); @@ -40,7 +48,11 @@ export default initMiddleware(async function handler( (file.id as string).startsWith(partialId), ); if (!file) { - throw new ExtendedError("File not found.", 404, "notFound"); + throw new ExtendedError( + "File not found.", + 404, + "notFound", + ); } if (file.id === apiConfig.files.rootFolder) { return response.status(301).redirect("/api/files"); @@ -48,14 +60,23 @@ export default initMiddleware(async function handler( response.setHeader("Cache-Control", apiConfig.cache); - if (file.mimeType !== "application/vnd.google-apps.folder") { + if ( + file.mimeType !== "application/vnd.google-apps.folder" + ) { if (thumbnail === "1") { - return response.status(301).redirect(file.thumbnailLink as string); + return response + .status(301) + .redirect(file.thumbnailLink as string); } if (download === "1") { // Check size - if (Number(file.size as string) > apiConfig.maxResponseSize) { - return response.status(301).redirect(file.webContentLink as string); + if ( + Number(file.size as string) > + apiConfig.maxResponseSize + ) { + return response + .status(301) + .redirect(file.webContentLink as string); } const fileStream = await driveClient.files.get( @@ -72,9 +93,14 @@ export default initMiddleware(async function handler( ); response.setHeader( "Content-Disposition", - `attachment; filename=${encodeURIComponent(file.name as string)}`, + `attachment; filename=${encodeURIComponent( + file.name as string, + )}`, + ); + response.setHeader( + "Content-Length", + file.size as string, ); - response.setHeader("Content-Length", file.size as string); return response.status(200).send(fileStream.data); } @@ -85,9 +111,9 @@ export default initMiddleware(async function handler( responseTime: Date.now() - _start, file: { ...file, - id: urlEncrypt(file.id as string), + id: shortEncrypt(file.id as string), webContentLink: file.webContentLink - ? urlEncrypt(file.webContentLink) + ? shortEncrypt(file.webContentLink) : undefined, }, }; @@ -100,29 +126,37 @@ export default initMiddleware(async function handler( "trashed = false", "'me' in owners", ]; - const fetchFolderContents = await driveClient.files.list({ - q: `${query.join(" and ")}`, - fields: - "files(id, name, mimeType, thumbnailLink, fileExtension, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, webContentLink, iconLink), nextPageToken", - orderBy: "folder, name asc, createdTime", - pageSize: apiConfig.files.itemsPerPage, - pageToken: (pageToken as string) || undefined, - }); + const fetchFolderContents = + await driveClient.files.list({ + q: `${query.join(" and ")}`, + fields: + "files(id, name, mimeType, thumbnailLink, fileExtension, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, webContentLink, iconLink), nextPageToken", + orderBy: "folder, name asc, createdTime", + pageSize: apiConfig.files.itemsPerPage, + pageToken: (pageToken as string) || undefined, + }); - const isReadmeExists = !!fetchFolderContents.data.files?.find( - (file) => file.name === ".readme.md", - ); - const isBannerExists = !!fetchFolderContents.data.files?.find((file) => - file.name?.startsWith(".banner"), - ); + const isReadmeExists = + !!fetchFolderContents.data.files?.find( + (file) => file.name === ".readme.md", + ); + const isBannerExists = + !!fetchFolderContents.data.files?.find((file) => + file.name?.startsWith(".banner"), + ); if (banner === "1") { if (!isBannerExists) { - throw new ExtendedError("Banner not found.", 404, "notFound"); + throw new ExtendedError( + "Banner not found.", + 404, + "notFound", + ); } - const bannerFile = fetchFolderContents.data.files?.find((file) => - file.name?.startsWith(".banner"), - ); + const bannerFile = + fetchFolderContents.data.files?.find((file) => + file.name?.startsWith(".banner"), + ); const bannerFileStream = await driveClient.files.get( { fileId: bannerFile?.id as string, @@ -140,19 +174,26 @@ export default initMiddleware(async function handler( bannerFile?.name as string, )}`, ); - response.setHeader("Content-Length", bannerFile?.size as string); - return response.status(200).send(bannerFileStream.data); + response.setHeader( + "Content-Length", + bannerFile?.size as string, + ); + return response + .status(200) + .send(bannerFileStream.data); } // Get only folder, since we order the folder to be first, we can just get all the folder first const folderList = fetchFolderContents.data.files ?.filter( - (item) => item.mimeType === "application/vnd.google-apps.folder", + (item) => + item.mimeType === + "application/vnd.google-apps.folder", ) .map((item) => ({ ...item, - id: urlEncrypt(item.id as string), + id: shortEncrypt(item.id as string), })) || []; // Filter the files, excluding every google apps files and hidden files (.password and .readme.md) // .password currently not used, but will probably be used in the future @@ -160,7 +201,9 @@ export default initMiddleware(async function handler( fetchFolderContents.data.files ?.filter( (item) => - !item.mimeType?.startsWith("application/vnd.google-apps") && + !item.mimeType?.startsWith( + "application/vnd.google-apps", + ) && // !hiddenFiles.includes(item.name as string), !hiddenFiles.some((hiddenFile) => item.name?.startsWith(hiddenFile), @@ -168,9 +211,9 @@ export default initMiddleware(async function handler( ) .map((item) => ({ ...item, - id: urlEncrypt(item.id as string), + id: shortEncrypt(item.id as string), webContentLink: item.webContentLink - ? urlEncrypt(item.webContentLink) + ? shortEncrypt(item.webContentLink) : undefined, })) || []; @@ -181,7 +224,8 @@ export default initMiddleware(async function handler( isReadmeExists: isReadmeExists, folders: folderList, files: fileList, - nextPageToken: fetchFolderContents.data.nextPageToken || undefined, + nextPageToken: + fetchFolderContents.data.nextPageToken || undefined, }; return response.status(200).json(payload); @@ -192,11 +236,19 @@ export default initMiddleware(async function handler( responseTime: Date.now() - _start, code: error.code || 500, errors: { - message: error.errors?.[0].message || error.message || "Unknown error", - reason: error.errors?.[0].reason || error.cause || "internalError", + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", }, }; - return response.status(payload.code || 500).json(payload); + return response + .status(payload.code || 500) + .json(payload); } }); diff --git a/src/pages/api/files/index.ts b/src/pages/apis/files/index.ts similarity index 52% rename from src/pages/api/files/index.ts rename to src/pages/apis/files/index.ts index adff645..a0202ca 100644 --- a/src/pages/api/files/index.ts +++ b/src/pages/apis/files/index.ts @@ -1,10 +1,16 @@ import { NextApiRequest, NextApiResponse } from "next"; import driveClient from "utils/driveClient"; import apiConfig from "config/api.config"; -import { ExtendedError, hiddenFiles } from "utils/driveHelper"; +import { + ExtendedError, + hiddenFiles, +} from "utils/driveHelper"; import initMiddleware from "utils/apiMiddleware"; -import { ErrorResponse, FilesResponse } from "types/googleapis"; -import { urlEncrypt } from "utils/encryptionHelper"; +import { + ErrorResponse, + FilesResponse, +} from "types/googleapis"; +import { shortEncrypt } from "utils/encryptionHelper"; export default initMiddleware(async function handler( request: NextApiRequest, @@ -20,32 +26,41 @@ export default initMiddleware(async function handler( "'me' in owners", `parents = '${apiConfig.files.rootFolder}'`, ]; - const fetchFolderContents = await driveClient.files.list({ - q: `${query.join(" and ")}`, - fields: - "files(id, name, mimeType, thumbnailLink, fileExtension, fullFileExtension, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, webContentLink, iconLink), nextPageToken", - orderBy: "folder, name asc, createdTime", - pageSize: apiConfig.files.itemsPerPage, - pageToken: (pageToken as string) || undefined, - }); + const fetchFolderContents = + await driveClient.files.list({ + q: `${query.join(" and ")}`, + fields: + "files(id, name, mimeType, thumbnailLink, fileExtension, fullFileExtension, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, webContentLink, iconLink), nextPageToken", + orderBy: "folder, name asc, createdTime", + pageSize: apiConfig.files.itemsPerPage, + pageToken: (pageToken as string) || undefined, + }); - const isReadmeExists = fetchFolderContents.data.files?.find( - (file) => file.name === ".readme.md", - ); - const isBannerExists = fetchFolderContents.data.files?.find((file) => - file.name?.startsWith(".banner"), - ); - const isPasswordExists = fetchFolderContents.data.files?.find((file) => - file.name?.startsWith(".password"), - ); + const isReadmeExists = + fetchFolderContents.data.files?.find( + (file) => file.name === ".readme.md", + ); + const isBannerExists = + fetchFolderContents.data.files?.find((file) => + file.name?.startsWith(".banner"), + ); + const isPasswordExists = + fetchFolderContents.data.files?.find((file) => + file.name?.startsWith(".password"), + ); if (banner === "1") { if (!isBannerExists) { - throw new ExtendedError("Banner not found.", 404, "notFound"); + throw new ExtendedError( + "Banner not found.", + 404, + "notFound", + ); } - const bannerFile = fetchFolderContents.data.files?.find((file) => - file.name?.startsWith(".banner"), - ); + const bannerFile = + fetchFolderContents.data.files?.find((file) => + file.name?.startsWith(".banner"), + ); const bannerFileStream = await driveClient.files.get( { fileId: bannerFile?.id as string, @@ -63,24 +78,33 @@ export default initMiddleware(async function handler( bannerFile?.name as string, )}`, ); - response.setHeader("Content-Length", bannerFile?.size as string); - return response.status(200).send(bannerFileStream.data); + response.setHeader( + "Content-Length", + bannerFile?.size as string, + ); + return response + .status(200) + .send(bannerFileStream.data); } const folderList = fetchFolderContents.data.files ?.filter( - (item) => item.mimeType === "application/vnd.google-apps.folder", + (item) => + item.mimeType === + "application/vnd.google-apps.folder", ) .map((item) => ({ ...item, - id: urlEncrypt(item.id as string), + id: shortEncrypt(item.id as string), })) || []; const fileList = fetchFolderContents.data.files ?.filter( (item) => - !item.mimeType?.startsWith("application/vnd.google-apps") && + !item.mimeType?.startsWith( + "application/vnd.google-apps", + ) && // !hiddenFiles.includes(item.name as string), !hiddenFiles.some((hiddenFile) => item.name?.startsWith(hiddenFile), @@ -88,9 +112,9 @@ export default initMiddleware(async function handler( ) .map((item) => ({ ...item, - id: urlEncrypt(item.id as string), + id: shortEncrypt(item.id as string), webContentLink: item.webContentLink - ? urlEncrypt(item.webContentLink) + ? shortEncrypt(item.webContentLink) : undefined, })) || []; @@ -103,7 +127,8 @@ export default initMiddleware(async function handler( isReadmeExists: !!isReadmeExists, isBannerExists: !!isBannerExists, isPasswordExists: !!isPasswordExists, - nextPageToken: fetchFolderContents.data.nextPageToken || undefined, + nextPageToken: + fetchFolderContents.data.nextPageToken || undefined, }; return response @@ -117,11 +142,19 @@ export default initMiddleware(async function handler( responseTime: Date.now() - _start, code: error.code || 500, errors: { - message: error.errors?.[0].message || error.message || "Unknown error", - reason: error.errors?.[0].reason || error.cause || "internalError", + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", }, }; - return response.status(payload.code || 500).json(payload); + return response + .status(payload.code || 500) + .json(payload); } }); diff --git a/src/pages/api/og-folder.tsx b/src/pages/apis/og-folder.tsx similarity index 90% rename from src/pages/api/og-folder.tsx rename to src/pages/apis/og-folder.tsx index c14734a..15944d0 100644 --- a/src/pages/api/og-folder.tsx +++ b/src/pages/apis/og-folder.tsx @@ -3,27 +3,38 @@ import siteConfig from "config/site.config"; import { NextRequest } from "next/server"; export const config = { - runtime: "edge", + runtime: "experimental-edge", }; const fontBold = fetch( - new URL("../../../public/fonts/Exo2-Bold.ttf", import.meta.url), + new URL( + "../../../public/fonts/Exo2-Bold.ttf", + import.meta.url, + ), ).then((res) => res.arrayBuffer()); const fontRegular = fetch( - new URL("../../../public/fonts/Exo2-Regular.ttf", import.meta.url), + new URL( + "../../../public/fonts/Exo2-Regular.ttf", + import.meta.url, + ), ).then((res) => res.arrayBuffer()); -export default async function handler(request: NextRequest) { +export default async function handler( + request: NextRequest, +) { try { const exoFont = await fontRegular; const exoFontBold = await fontBold; const { searchParams } = new URL(request.url); - const siteName = searchParams.get("siteName") || siteConfig.siteName; + const siteName = + searchParams.get("siteName") || siteConfig.siteName; const fileId = searchParams.get("fileId") || null; const fileName = - searchParams.get("fileName") || fileId?.split(":")[0] || null; + searchParams.get("fileName") || + fileId?.split(":")[0] || + null; if (!fileId) { throw new Error("No file ID"); diff --git a/src/pages/api/og.tsx b/src/pages/apis/og.tsx similarity index 88% rename from src/pages/api/og.tsx rename to src/pages/apis/og.tsx index 90feafa..bf2851f 100644 --- a/src/pages/api/og.tsx +++ b/src/pages/apis/og.tsx @@ -3,32 +3,47 @@ import siteConfig from "config/site.config"; import { NextRequest } from "next/server"; export const config = { - runtime: "edge", + runtime: "experimental-edge", }; const fontBold = fetch( - new URL("../../../public/fonts/Exo2-Bold.ttf", import.meta.url), + new URL( + "../../../public/fonts/Exo2-Bold.ttf", + import.meta.url, + ), ).then((res) => res.arrayBuffer()); const fontRegular = fetch( - new URL("../../../public/fonts/Exo2-Regular.ttf", import.meta.url), + new URL( + "../../../public/fonts/Exo2-Regular.ttf", + import.meta.url, + ), ).then((res) => res.arrayBuffer()); -export default async function handler(request: NextRequest) { +export default async function handler( + request: NextRequest, +) { try { const exoFont = await fontRegular; const exoFontBold = await fontBold; const { searchParams } = new URL(request.url); - const siteName = searchParams.get("siteName") || siteConfig.siteName; + const siteName = + searchParams.get("siteName") || siteConfig.siteName; const fileId = searchParams.get("fileId") || null; const fileName = - searchParams.get("fileName") || fileId?.split(":")[0] || null; + searchParams.get("fileName") || + fileId?.split(":")[0] || + null; const fileExt = fileName?.split(".").pop() || null; - const isImage = ["jpg", "jpeg", "png", "gif", "webp"].includes( - fileExt || "", - ); + const isImage = [ + "jpg", + "jpeg", + "png", + "gif", + "webp", + ].includes(fileExt || ""); if (!fileId) { throw new Error("No file ID"); diff --git a/src/pages/api/readme/[folderId]/index.tsx b/src/pages/apis/readme/[folderId]/index.tsx similarity index 100% rename from src/pages/api/readme/[folderId]/index.tsx rename to src/pages/apis/readme/[folderId]/index.tsx diff --git a/src/pages/api/readme/index.ts b/src/pages/apis/readme/index.ts similarity index 100% rename from src/pages/api/readme/index.ts rename to src/pages/apis/readme/index.ts diff --git a/src/pages/api/search.ts b/src/pages/apis/search.ts similarity index 72% rename from src/pages/api/search.ts rename to src/pages/apis/search.ts index c4ba1f9..c6ccf34 100644 --- a/src/pages/api/search.ts +++ b/src/pages/apis/search.ts @@ -1,8 +1,11 @@ import apiConfig from "config/api.config"; import { NextApiRequest, NextApiResponse } from "next"; -import { ErrorResponse, SearchResponse } from "types/googleapis"; +import { + ErrorResponse, + SearchResponse, +} from "types/googleapis"; import driveClient from "utils/driveClient"; -import { urlEncrypt } from "utils/encryptionHelper"; +import { shortEncrypt } from "utils/encryptionHelper"; import { hiddenFiles } from "utils/driveHelper"; export default async function handler( @@ -43,12 +46,15 @@ export default async function handler( !hiddenFiles.some((hiddenFile) => item.name?.startsWith(hiddenFile), ) && - (!item.mimeType?.startsWith("application/vnd.google-apps") || - item.mimeType === "application/vnd.google-apps.folder"), + (!item.mimeType?.startsWith( + "application/vnd.google-apps", + ) || + item.mimeType === + "application/vnd.google-apps.folder"), ) .map((item) => ({ ...item, - id: urlEncrypt(item.id as string), + id: shortEncrypt(item.id as string), })) || [], }; @@ -63,11 +69,19 @@ export default async function handler( responseTime: Date.now() - _start, code: error.code || 500, errors: { - message: error.errors?.[0].message || error.message || "Unknown error", - reason: error.errors?.[0].reason || error.cause || "internalError", + message: + error.errors?.[0].message || + error.message || + "Unknown error", + reason: + error.errors?.[0].reason || + error.cause || + "internalError", }, }; - return response.status(payload.code || 500).json(payload); + return response + .status(payload.code || 500) + .json(payload); } } diff --git a/src/pages/file/[id].tsx b/src/pages/file/[id].tsx index 553309d..a1617c7 100644 --- a/src/pages/file/[id].tsx +++ b/src/pages/file/[id].tsx @@ -1,13 +1,19 @@ import { GetServerSideProps } from "next"; import axios from "axios"; -import { ErrorResponse, FileResponse } from "types/googleapis"; +import { + ErrorResponse, + FileResponse, +} from "types/googleapis"; import { useEffect, useState } from "react"; import useSWR from "swr"; -import { urlDecrypt } from "utils/encryptionHelper"; +import { shortDecrypt } from "utils/encryptionHelper"; import DefaultLayout from "components/layout/DefaultLayout"; import { NextSeo } from "next-seo"; import SWRLayout from "components/layout/SWRLayout"; -import { getFilePreview, getFileType } from "utils/mimeTypesHelper"; +import { + getFilePreview, + getFileType, +} from "utils/mimeTypesHelper"; import { capitalize, formatBytes, @@ -27,7 +33,8 @@ type Metadata = { }; export default function File({ id, fileName }: Props) { const [data, setData] = useState(); - const [PreviewComponent, setPreviewComponent] = useState(); + const [PreviewComponent, setPreviewComponent] = + useState(); const [metadata, setMetadata] = useState([]); const copyLink = useCopyText(); @@ -41,7 +48,9 @@ export default function File({ id, fileName }: Props) { data: swrData, error, isLoading, - } = useSWR(`/api/files/${id}`); + } = useSWR( + `/api/files/${id}`, + ); useEffect(() => { if (swrData) { @@ -49,8 +58,10 @@ export default function File({ id, fileName }: Props) { ...swrData, file: { ...swrData.file, - id: urlDecrypt(swrData.file.id as string), - webContentLink: urlDecrypt(swrData.file.webContentLink as string), + id: shortDecrypt(swrData.file.id as string), + webContentLink: shortDecrypt( + swrData.file.webContentLink as string, + ), }, }; setData(decryptedData); @@ -90,11 +101,15 @@ export default function File({ id, fileName }: Props) { }, { label: "Created", - value: formatDate(new Date(data.file.createdTime as string)), + value: formatDate( + new Date(data.file.createdTime as string), + ), }, { label: "Modified", - value: formatDate(new Date(data.file.modifiedTime as string)), + value: formatDate( + new Date(data.file.modifiedTime as string), + ), }, ]; if (data.file.imageMediaMetadata) { @@ -107,7 +122,8 @@ export default function File({ id, fileName }: Props) { defaultMetadata.push({ label: "Duration", value: formatDuration( - data.file.videoMediaMetadata.durationMillis as string, + data.file.videoMediaMetadata + .durationMillis as string, ), }); } @@ -121,13 +137,19 @@ export default function File({ id, fileName }: Props) { renderSwitchLayout={false} > (
- {item.label} + {item.label} + + {item.value} @@ -195,14 +225,20 @@ export default function File({ id, fileName }: Props) {
-
+
- +