feat: file view
This commit is contained in:
parent
28e3a75593
commit
34880f4abc
12 changed files with 394 additions and 25 deletions
|
@ -3,6 +3,7 @@ package routes
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"git.wh64.net/devproje/kuma-archive/internal/service"
|
"git.wh64.net/devproje/kuma-archive/internal/service"
|
||||||
"github.com/gin-contrib/static"
|
"github.com/gin-contrib/static"
|
||||||
|
@ -14,22 +15,28 @@ func New(app *gin.Engine, apiOnly bool) {
|
||||||
{
|
{
|
||||||
api.GET("/path/*path", func(ctx *gin.Context) {
|
api.GET("/path/*path", func(ctx *gin.Context) {
|
||||||
worker := service.NewWorkerService()
|
worker := service.NewWorkerService()
|
||||||
|
|
||||||
path := ctx.Param("path")
|
path := ctx.Param("path")
|
||||||
data, err := worker.Read(path)
|
data, err := worker.Read(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
ctx.Status(404)
|
ctx.Status(404)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.IsDir {
|
if !data.IsDir {
|
||||||
ctx.FileAttachment(data.Path, data.Name)
|
ctx.JSON(200, gin.H{
|
||||||
|
"ok": 1,
|
||||||
|
"path": path,
|
||||||
|
"total": data.FileSize,
|
||||||
|
"is_dir": false,
|
||||||
|
"entries": nil,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
raw, err := os.ReadDir(data.Path)
|
raw, err := os.ReadDir(data.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
ctx.Status(500)
|
ctx.Status(500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -44,7 +51,8 @@ func New(app *gin.Engine, apiOnly bool) {
|
||||||
|
|
||||||
entries = append(entries, service.DirEntry{
|
entries = append(entries, service.DirEntry{
|
||||||
Name: entry.Name(),
|
Name: entry.Name(),
|
||||||
Path: path,
|
Path: filepath.Join(path, entry.Name()),
|
||||||
|
Date: finfo.ModTime().Unix(),
|
||||||
FileSize: uint64(finfo.Size()),
|
FileSize: uint64(finfo.Size()),
|
||||||
IsDir: finfo.IsDir(),
|
IsDir: finfo.IsDir(),
|
||||||
})
|
})
|
||||||
|
@ -53,9 +61,47 @@ func New(app *gin.Engine, apiOnly bool) {
|
||||||
ctx.JSON(200, gin.H{
|
ctx.JSON(200, gin.H{
|
||||||
"ok": 1,
|
"ok": 1,
|
||||||
"path": path,
|
"path": path,
|
||||||
|
"total": data.FileSize,
|
||||||
|
"is_dir": true,
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
api.GET("/raw/*path", func(ctx *gin.Context) {
|
||||||
|
worker := service.NewWorkerService()
|
||||||
|
path := ctx.Param("path")
|
||||||
|
data, err := worker.Read(path)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
ctx.Status(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.IsDir {
|
||||||
|
ctx.String(400, "current path is not file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.File(data.Path)
|
||||||
|
})
|
||||||
|
|
||||||
|
api.GET("/download/*path", func(ctx *gin.Context) {
|
||||||
|
worker := service.NewWorkerService()
|
||||||
|
path := ctx.Param("path")
|
||||||
|
data, err := worker.Read(path)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
ctx.Status(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.IsDir {
|
||||||
|
ctx.String(400, "current path is not file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.FileAttachment(data.Path, data.Name)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if apiOnly {
|
if apiOnly {
|
||||||
|
|
|
@ -12,6 +12,7 @@ type WorkerService struct{}
|
||||||
type DirEntry struct {
|
type DirEntry struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
Date int64 `json:"date"`
|
||||||
FileSize uint64 `json:"file_size"`
|
FileSize uint64 `json:"file_size"`
|
||||||
IsDir bool `json:"is_dir"`
|
IsDir bool `json:"is_dir"`
|
||||||
}
|
}
|
||||||
|
@ -29,8 +30,10 @@ func (sv *WorkerService) Read(path string) (*DirEntry, error) {
|
||||||
|
|
||||||
ret := DirEntry{
|
ret := DirEntry{
|
||||||
Name: info.Name(),
|
Name: info.Name(),
|
||||||
|
Date: info.ModTime().Unix(),
|
||||||
Path: fullpath,
|
Path: fullpath,
|
||||||
FileSize: uint64(info.Size()),
|
FileSize: uint64(info.Size()),
|
||||||
|
IsDir: info.IsDir(),
|
||||||
}
|
}
|
||||||
return &ret, nil
|
return &ret, nil
|
||||||
}
|
}
|
||||||
|
|
27
src/App.scss
27
src/App.scss
|
@ -43,17 +43,40 @@
|
||||||
|
|
||||||
.ka-menu-item {
|
.ka-menu-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 55px;
|
height: 45px;
|
||||||
padding: 5px 0.5rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding: 0 0.5rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--nav-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
color: var(--focus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ka-footer {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
63
src/App.tsx
63
src/App.tsx
|
@ -1,10 +1,12 @@
|
||||||
import { BrowserRouter, Route, Routes, useLocation } from "react-router";
|
|
||||||
import { usePath } from "./store/path";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import Directory from "./components/directory";
|
||||||
|
import { DirEntry, usePath } from "./store/path";
|
||||||
|
import { DynamicIcon, IconName } from "lucide-react/dynamic";
|
||||||
|
import { BrowserRouter, Route, Routes, useLocation } from "react-router";
|
||||||
|
|
||||||
import "./App.scss";
|
import "./App.scss";
|
||||||
import kuma from "./assets/kuma.png";
|
import kuma from "./assets/kuma.png";
|
||||||
import { DynamicIcon, IconName } from "lucide-react/dynamic";
|
import FileView from "./components/file-view";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
@ -25,6 +27,8 @@ function Dashboard() {
|
||||||
if (!load) {
|
if (!load) {
|
||||||
path.update(location.pathname.substring(1, location.pathname.length));
|
path.update(location.pathname.substring(1, location.pathname.length));
|
||||||
setLoad(true);
|
setLoad(true);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
|
@ -34,9 +38,25 @@ function Dashboard() {
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [load, path, location]);
|
}, [load, path, location]);
|
||||||
|
|
||||||
|
if (!load) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="container-md ka-view">
|
<main className="container-md ka-view">
|
||||||
<Header />
|
<Header />
|
||||||
|
{typeof path.data !== "undefined" ? path.data.is_dir ? <Directory /> : <FileView /> : (
|
||||||
|
<>
|
||||||
|
<h1>404 Not Found</h1>
|
||||||
|
|
||||||
|
<button className="primary" onClick={ev => {
|
||||||
|
ev.preventDefault();
|
||||||
|
document.location.href = "/";
|
||||||
|
}}>Back to home</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -46,7 +66,7 @@ function Header() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="ka-nav">
|
<nav className="ka-nav">
|
||||||
<a className="title">
|
<a className="title" href="/">
|
||||||
<img src={kuma} alt="" />
|
<img src={kuma} alt="" />
|
||||||
<h4 className="title-content">Kuma Archive</h4>
|
<h4 className="title-content">Kuma Archive</h4>
|
||||||
</a>
|
</a>
|
||||||
|
@ -77,16 +97,47 @@ function MenuItem({ icon, name, block }: { icon: IconName, name: string, block?:
|
||||||
return (
|
return (
|
||||||
<a className={"ka-menu-item link"} onClick={ev => {
|
<a className={"ka-menu-item link"} onClick={ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
if (typeof block === "undefined")
|
if (typeof block === "undefined")
|
||||||
return;
|
return;
|
||||||
|
|
||||||
block();
|
block();
|
||||||
}}>
|
}}>
|
||||||
<DynamicIcon name={icon} className="link" />
|
<DynamicIcon name={icon} />
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Footer() {
|
||||||
|
const path = usePath();
|
||||||
|
let file = 0;
|
||||||
|
let dir = 0;
|
||||||
|
|
||||||
|
if (typeof path.data !== "undefined") {
|
||||||
|
if (path.data.is_dir) {
|
||||||
|
path.data.entries.forEach((entry: DirEntry) => {
|
||||||
|
if (entry.is_dir) {
|
||||||
|
dir += 1;
|
||||||
|
} else {
|
||||||
|
file += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="ka-footer">
|
||||||
|
{path.data ? path.data.is_dir ? (
|
||||||
|
<div className="searched">
|
||||||
|
Found {dir === 1 ? `${dir} directory` : `${dir} directories`}, {file === 1 ? `${file} file` : `${file} files`}
|
||||||
|
</div>
|
||||||
|
) : <></> : <></>}
|
||||||
|
|
||||||
|
<div className="footer">
|
||||||
|
© 2020-2025 Project_IO. MIT License. Powered by WSERVER.
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -1 +1,30 @@
|
||||||
|
.ka-dir {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
margin: 10px 0;
|
||||||
|
flex-direction: column;
|
||||||
|
border-bottom: solid 2px var(--foreground);
|
||||||
|
|
||||||
|
.ka-dir-top {
|
||||||
|
border-bottom: solid 2px var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ka-dir-row {
|
||||||
|
padding: 0 10px;
|
||||||
|
width: 100%;
|
||||||
|
height: 25px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 0.1fr 2fr 1fr 1fr;
|
||||||
|
|
||||||
|
.ka-dir-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover.ka-dir-item {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,68 @@
|
||||||
|
import { convert } from "../../util/unit";
|
||||||
|
import { DynamicIcon } from "lucide-react/dynamic";
|
||||||
|
import { DirEntry, usePath } from "../../store/path";
|
||||||
|
|
||||||
|
import "./directory.scss";
|
||||||
|
|
||||||
function Directory() {
|
function Directory() {
|
||||||
|
const path = usePath();
|
||||||
|
if (typeof path.data === "undefined")
|
||||||
|
return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<></>
|
<div className="ka-dir">
|
||||||
|
<div className="ka-dir-row ka-dir-top">
|
||||||
|
<div className="ka-dir-item"></div>
|
||||||
|
<b className="ka-dir-item">Name</b>
|
||||||
|
<b className="ka-dir-item">Size</b>
|
||||||
|
<b className="ka-dir-item">Date</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{path.data.path === "/" ? <></> : (
|
||||||
|
<DirItem data={{
|
||||||
|
name: "../",
|
||||||
|
path: path.data.path.endsWith("/") ? path.data.path += ".." : path.data.path += "/..",
|
||||||
|
date: -1,
|
||||||
|
file_size: -1,
|
||||||
|
is_dir: true,
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{path.data.entries.map((entry, key) => {
|
||||||
|
return <DirItem data={entry} key={key} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DirItem({ data }: { data: DirEntry }) {
|
||||||
|
return (
|
||||||
|
<div className="ka-dir-row">
|
||||||
|
<div className="ka-dir-item">
|
||||||
|
{data.is_dir ? (
|
||||||
|
<DynamicIcon name="folder" size={18} />
|
||||||
|
) : <></>}
|
||||||
|
</div>
|
||||||
|
<a className="ka-dir-item" href={data.path}>
|
||||||
|
{data.name}
|
||||||
|
</a>
|
||||||
|
<span className="ka-dir-item">
|
||||||
|
{data.is_dir ? (
|
||||||
|
"-"
|
||||||
|
): convert(data.file_size)}
|
||||||
|
</span>
|
||||||
|
<span className="ka-dir-item">
|
||||||
|
{data.date === -1 ? "-" : new Date(data.date * 1000).toLocaleString("en-US", {
|
||||||
|
weekday: "short",
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false
|
||||||
|
}).replace(/,/g, "")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
.ka-fileview {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
margin: 1rem 0;
|
||||||
|
min-height: 300px;
|
||||||
|
border-radius: 15px;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--nav-color);
|
||||||
|
border: 1px solid var(--foreground);
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
padding: 5px 10px;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
border-radius: 15px 15px 0 0;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: var(--nav-hover);
|
||||||
|
border-bottom: 1px solid var(--foreground);
|
||||||
|
|
||||||
|
.name {
|
||||||
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
display: flex;
|
||||||
|
width: fit-content;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre, code {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 0 0 15px 15px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,63 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRaw } from "../../store/raw";
|
||||||
|
import "./fview.scss";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
import { DynamicIcon } from "lucide-react/dynamic";
|
||||||
|
|
||||||
function FileView() {
|
function FileView() {
|
||||||
|
const raw = useRaw();
|
||||||
|
const location = useLocation();
|
||||||
|
const [load, setLoad] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!load) {
|
||||||
|
raw.update(location.pathname.substring(1, location.pathname.length));
|
||||||
|
setLoad(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = setInterval(() => {
|
||||||
|
raw.update(location.pathname.substring(1, location.pathname.length));
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [raw, location, load]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<></>
|
<div className="ka-fileview">
|
||||||
|
<span className="title">
|
||||||
|
<div className="name">
|
||||||
|
<a className="link" href={location.pathname.endsWith("/") ? location.pathname + ".." : location.pathname + "/.."}>
|
||||||
|
<DynamicIcon name="chevron-left" />
|
||||||
|
</a>
|
||||||
|
<span>{location.pathname}</span>
|
||||||
|
</div>
|
||||||
|
<div className="action-row">
|
||||||
|
<a className="btn link" onClick={ev => {
|
||||||
|
ev.preventDefault();
|
||||||
|
fetch(`/api/download${location.pathname}`)
|
||||||
|
.then(response => response.blob())
|
||||||
|
.then(blob => {
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
|
||||||
|
a.style.display = "none";
|
||||||
|
a.href = url;
|
||||||
|
a.download = location.pathname.split("/").pop() || "download";
|
||||||
|
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
})
|
||||||
|
.catch(error => console.error("Download failed:", error));
|
||||||
|
}}>
|
||||||
|
<DynamicIcon name="download" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<pre>{raw.data}</pre>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css");
|
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css");
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #242424;
|
--background: #242424;
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
--btn-danger-focus: #a72532;
|
--btn-danger-focus: #a72532;
|
||||||
|
|
||||||
--font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
|
--font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
|
@ -184,3 +186,7 @@ input, button {
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
padding: 0.25rem 15px;
|
padding: 0.25rem 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code, pre {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
interface PathState {
|
interface PathState {
|
||||||
data: PathResponse | string | undefined;
|
data: PathResponse | undefined;
|
||||||
update(path: string): Promise<void>;
|
update(path: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PathResponse {
|
interface PathResponse {
|
||||||
ok: number;
|
ok: number;
|
||||||
path: string;
|
path: string;
|
||||||
|
total: number;
|
||||||
|
is_dir: boolean;
|
||||||
entries: Array<DirEntry>
|
entries: Array<DirEntry>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DirEntry {
|
export interface DirEntry {
|
||||||
name: string;
|
name: string;
|
||||||
|
path: string;
|
||||||
|
date: number;
|
||||||
file_size: number;
|
file_size: number;
|
||||||
|
is_dir: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePath = create<PathState>((set) => ({
|
export const usePath = create<PathState>((set) => ({
|
||||||
|
@ -25,10 +30,6 @@ export const usePath = create<PathState>((set) => ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
set({ data: await res.json() });
|
set({ data: await res.json() });
|
||||||
} catch {
|
|
||||||
set({ data: `/api/path/${path}` });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
22
src/store/raw.ts
Normal file
22
src/store/raw.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface RawState {
|
||||||
|
data: string | undefined;
|
||||||
|
update(path: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRaw = create<RawState>((set) => ({
|
||||||
|
data: undefined,
|
||||||
|
update: async (path: string) => {
|
||||||
|
const res = await fetch(`/api/raw/${path}`, {
|
||||||
|
cache: "no-cache"
|
||||||
|
});
|
||||||
|
if (res.status !== 200 && res.status !== 304) {
|
||||||
|
set({ data: undefined });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
set({ data: text });
|
||||||
|
}
|
||||||
|
}));
|
18
src/util/unit.ts
Normal file
18
src/util/unit.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
export function convert(bytes: number): string {
|
||||||
|
if (bytes >= 1125899906842624)
|
||||||
|
return (bytes / 1125899906842624).toFixed(2) + " PiB";
|
||||||
|
|
||||||
|
if (bytes >= 1099511627776)
|
||||||
|
return (bytes / 1099511627776).toFixed(2) + " TiB";
|
||||||
|
|
||||||
|
if (bytes >= 1073741824)
|
||||||
|
return (bytes / 1073741824).toFixed(2) + " GiB";
|
||||||
|
|
||||||
|
if (bytes >= 1048576)
|
||||||
|
return (bytes / 1048576).toFixed(2) + " MiB";
|
||||||
|
|
||||||
|
if (bytes >= 1024)
|
||||||
|
return (bytes / 1024).toFixed(2) + " KiB";
|
||||||
|
|
||||||
|
return bytes + " Byte";
|
||||||
|
}
|
Loading…
Reference in a new issue