Skip to content

Commit

Permalink
feat: Files and folders displaying & basic query operations
Browse files Browse the repository at this point in the history
  • Loading branch information
NriotHrreion committed Jul 17, 2024
1 parent c4d50fe commit be71c5e
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 95 deletions.
24 changes: 13 additions & 11 deletions app/(pages)/x/[[...path]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"use client";

import { Suspense, useEffect } from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Input } from "@nextui-org/input";
import { Card } from "@nextui-org/card";
import { Button } from "@nextui-org/button";
import { Skeleton } from "@nextui-org/skeleton";
import { Tooltip } from "@nextui-org/tooltip";
import { ArrowLeft, Home } from "lucide-react";
import { to } from "preps";

import Navbar from "@/components/explorer/navbar";
import { useExplorer } from "@/hooks/useExplorer";
Expand All @@ -25,6 +25,7 @@ export default function Page({ params }: FileExplorerProps) {

const router = useRouter();
const explorer = useExplorer();
const [currentPath, setCurrentPath] = useState<string>();

const handleHome = () => {
explorer.setPath(["root"]);
Expand All @@ -36,6 +37,12 @@ export default function Page({ params }: FileExplorerProps) {
router.push("/x/root"+ explorer.stringifyPath());
};

useExplorer.subscribe((state, prevState) => {
if(to(state.path).is(prevState.path)) return;

setCurrentPath(state.stringifyPath());
});

useEffect(() => {
if(
!path ||
Expand All @@ -49,6 +56,9 @@ export default function Page({ params }: FileExplorerProps) {
}

document.title = "Ferrum - "+ explorer.stringifyPath();

// Trigger the explorer to fetch the folder info
setCurrentPath(explorer.stringifyPath());
}, []);

useDetectCookie();
Expand Down Expand Up @@ -92,15 +102,7 @@ export default function Page({ params }: FileExplorerProps) {
<div className="w-[1000px] h-full flex gap-7">
<Card className="flex-1"/>

<Suspense fallback={
<div className="w-[730px] flex flex-col gap-3">
{new Array(4).fill(0).map((_, index) => (
<Skeleton className="h-8 rounded-md" key={index}/>
))}
</div>
}>
<Explorer currentPath={explorer.stringifyPath()}/>
</Suspense>
<Explorer currentPath={currentPath}/>
</div>
</div>
);
Expand Down
67 changes: 30 additions & 37 deletions app/api/folder/route.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,40 @@
import fs from "node:fs";
import path from "node:path";

import { NextRequest, NextResponse } from "next/server";

import { tokenStorageKey } from "@/lib/global";

export async function GET(req: NextRequest) {
if(!req.cookies.get(tokenStorageKey)) return NextResponse.json({ status: 401 });

// const { searchParams } = new URL(req.url);
// const targetPath = searchParams.get("path");
// // console.log(targetPath);

// // return new NextResponse("ok");
const { searchParams } = new URL(req.url);
const targetDisk = searchParams.get("disk");
const targetPath = path.join((targetDisk ?? "C") +":", searchParams.get("path") ?? "/");

// //@ts-ignore
// if(!targetPath || !fs.existsSync(targetPath)) return NextResponse.json({ status: 404 });
try {
if(!targetPath || !fs.existsSync(targetPath)) return NextResponse.json({ status: 404 });

// //@ts-ignore
// const stat = fs.statSync(targetPath);

// if(!stat.isDirectory()) return NextResponse.json({ status: 400 });

// return NextResponse.json({
// status: 200,
// //@ts-ignore
// items: fs.readdirSync(targetPath).map((itemName) => {
// //@ts-ignore
// const item = fs.statSync(path.join(targetPath, itemName));

// return {
// name: itemName,
// type: item.isDirectory() ? "folder" : "file",
// size: item.size
// };
// })
// });

return NextResponse.json({
status: 200,
items: [
{
name: "test",
type: "folder",
size: 1024
}
]
});
const stat = fs.statSync(targetPath);

if(!stat.isDirectory()) return NextResponse.json({ status: 400 });

return NextResponse.json({
status: 200,
items: fs.readdirSync(targetPath).map((itemName) => {
const item = fs.statSync(path.join(targetPath, itemName));

return {
name: itemName,
type: item.isDirectory() ? "folder" : "file",
size: item.size
};
})
});
} catch (err) {
// eslint-disable-next-line no-console
console.log("[Server: /api/folder] "+ err);

return NextResponse.json({ status: 500 });
}
}
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function RootLayout({
return (
<html suppressHydrationWarning lang="zh-cn">
<head />
<body className={clsx("m-0 p-0 w-[100vw] h-[100vh]", fontNoto.className, fontSans.className)}>
<body className={clsx("m-0 p-0 w-[100vw] h-[100vh] overflow-x-hidden", fontNoto.className, fontSans.className)}>
<Providers themeProps={{
attribute: "class",
defaultTheme: "system",
Expand Down
33 changes: 26 additions & 7 deletions components/explorer/explorer-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
Film
} from "lucide-react";

import { DirectoryItem } from "@/types";
import { BytesType, DirectoryItem } from "@/types";
import { useExplorer } from "@/hooks/useExplorer";
import { bytesSizeTransform, getBytesType } from "@/lib/utils";

export function getIcon(folderName: string, size: number = 18, color?: string): React.ReactNode {
const folderNameLowered = folderName.toLowerCase();
Expand All @@ -34,7 +35,23 @@ export function getIcon(folderName: string, size: number = 18, color?: string):
interface ExplorerItemProps extends DirectoryItem {}

const ExplorerItem: React.FC<ExplorerItemProps> = (props) => {
const type = mime.getExtension(props.name);
const type = mime.getExtension(mime.getType(props.name) ?? "");
var size = {
value: props.size.toFixed(2),
type: BytesType.B
};

if(props.size <= 1024) {
//
} else if(props.size > 1024 && props.size <= 1048576) {
size = bytesSizeTransform(props.size, BytesType.B, BytesType.KB);
} else if(props.size > 1048576 && props.size <= 1073741824) {
size = bytesSizeTransform(props.size, BytesType.B, BytesType.MB);
} else if(props.size > 1073741824 && props.size <= 1099511627776) {
size = bytesSizeTransform(props.size, BytesType.B, BytesType.GB);
} else if(props.size > 1099511627776) {
size = bytesSizeTransform(props.size, BytesType.B, BytesType.TB);
}

const router = useRouter();
const explorer = useExplorer();
Expand All @@ -46,20 +63,22 @@ const ExplorerItem: React.FC<ExplorerItemProps> = (props) => {

return (
<div className="w-full h-8 text-md flex items-center gap-4">
<div className="flex-1 flex items-center gap-2">
<div className="flex-1 min-w-0 flex items-center gap-2">
{(
props.type === "folder" ? getIcon(props.name, 20, "#9e9e9e") : <File size={20} color="#9e9e9e"/>
props.type === "folder" ? getIcon(props.name, 20, "#9e9e9e") : (
<File size={20} color="#9e9e9e" className="min-w-[20px]"/>
)
) as ReactNode}
<button
className="hover:underline hover:text-primary-500 cursor-pointer"
className="text-ellipsis whitespace-nowrap cursor-pointer overflow-hidden hover:underline hover:text-primary-500"
onClick={() => handleClick()}>
{props.name}
</button>
</div>
<Divider orientation="vertical" className="bg-transparent"/>
<span className="flex-1 text-default-400 text-sm cursor-default">{props.type === "folder" ? "文件夹" : type}</span>
<span className="flex-1 text-default-400 text-sm cursor-default">{props.type === "folder" ? "文件夹" : (type +" 文件")}</span>
<Divider orientation="vertical" className="bg-transparent"/>
<span className="flex-1 text-default-400 text-sm cursor-default">{props.size.toFixed(2) +" KB"}</span>
<span className="flex-1 text-default-400 text-right text-sm cursor-default">{props.type === "file" ? (size?.value +" "+ getBytesType(size?.type)) : ""}</span>
</div>
);
};
Expand Down
65 changes: 27 additions & 38 deletions components/explorer/explorer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { useEffect } from "react";
import React, { useState, useEffect } from "react";
import axios from "axios";
import { Divider } from "@nextui-org/divider";

Expand All @@ -12,48 +12,35 @@ interface FolderResponseData extends BaseResponseData {
items: DirectoryItem[]
}

var cache: React.ReactNode[];

/** @see https://dev.to/tusharshahi/using-suspense-with-react-without-a-3rd-party-library-3i2b */
function fetchFolderData(path: string): React.ReactNode[] {
if(cache) return cache;

const promise = axios.get<FolderResponseData>(`/api/folder?path=${path}`)
.then(({ data }) => {
return data.items.map((item, index) => (
<ExplorerItem {...item} key={index}/>
));
})
.then((nodes) => {
cache = nodes;

return nodes;
});

throw promise;
}

interface ExplorerProps {
currentPath: string
currentPath?: string
}

const Explorer: React.FC<ExplorerProps> = ({ currentPath }) => {
const nodes = fetchFolderData(currentPath);
const [items, setItems] = useState<DirectoryItem[]>([]);
const currentDisk = "D"; // for dev

useEffect(() => {
/**
* I can't figure it out that why the browser
* just cannot stop loading the page after
* fetching the folder data...
*
* So the only thing I can do is stop it from
* loading manually by `window.stop()`
*
* This can't be a perfect way to fix this,
* but I think it's ok.
*/
window.stop(); // fuck
}, []);
if(!currentPath || currentPath === "/") return;

axios.get<FolderResponseData>(`/api/folder?disk=${currentDisk}&path=${currentPath}`)
.then(({ data }) => {
var list: DirectoryItem[] = [];

data.items.forEach((item) => {
if(item.type === "folder") list.push(item);
});
data.items.forEach((item) => {
if(item.type === "file") list.push(item);
});

setItems(list);
})
.catch((err) => {
setItems([]);
throw err;
});
}, [currentPath]);

return (
<div className="w-[730px] flex flex-col gap-2">
Expand All @@ -66,7 +53,9 @@ const Explorer: React.FC<ExplorerProps> = ({ currentPath }) => {
</div>

<div className="w-full flex-1 flex flex-col">
{nodes}
{items.map((item, index) => (
<ExplorerItem {...item} key={index}/>
))}
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion components/explorer/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const Navbar: React.FC<PropsWithCN> = ({ className }) => {
? <FolderRoot size={18}/>
: getIcon(folderName, 18)
}
{folderName}
{decodeURIComponent(folderName)}
</BreadcrumbItem>
))
}
Expand Down
26 changes: 26 additions & 0 deletions hooks/useEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect } from "react";

import Emitter from "@/lib/emitter";

type EmitterInstance = [string, (...args: any[]) => any];

/**
* Create an event listener with `EventEmitter`
*
* @example
* ```ts
* useEmitter([
* ["foo", () => console.log("bar")]
* ]);
*
* // in somewhere...
* new Emitter().emit("foo"); // bar
* ```
*/
export default function useEmitter(instances: EmitterInstance[]) {
useEffect(() => {
instances.forEach((instance: EmitterInstance) => {
Emitter.get().on(instance[0], (...args: any[]) => instance[1](...args));
});
}, []);
}
16 changes: 16 additions & 0 deletions lib/emitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { EventEmitter } from "node:events";

export default class Emitter extends EventEmitter {
private static instance: Emitter;

private constructor() {
super();
this.setMaxListeners(Infinity);
}

public static get(): Emitter {
if(!this.instance) this.instance = new Emitter();

return this.instance;
}
}
28 changes: 28 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BytesType } from "@/types";

export function getCurrentState<T>(setState: React.Dispatch<React.SetStateAction<T>>): Promise<T> {
return new Promise((resolve, _reject) => {
setState((currentState) => {
resolve(currentState);

return currentState;
});
});
}

export function bytesSizeTransform(bytes: number, from: BytesType, to: BytesType, fixed: number = 2): { value: string, type: BytesType } {
return {
value: (bytes * Math.pow(1024, from - to)).toFixed(fixed),
type: to
};
}

export function getBytesType(type: BytesType): string {
switch(type) {
case BytesType.B: return "B";
case BytesType.KB: return "KB";
case BytesType.MB: return "MB";
case BytesType.GB: return "GB";
case BytesType.TB: return "TB";
}
}
8 changes: 8 additions & 0 deletions types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ export interface DirectoryItem {
type: "folder" | "file"
size: number
}

export enum BytesType {
B = 0,
KB = 1,
MB = 2,
GB = 3,
TB = 4
}

0 comments on commit be71c5e

Please sign in to comment.