better dev runner, bug fixes, design changes, and more
This commit is contained in:
18
README.md
18
README.md
@@ -4,3 +4,21 @@
|
|||||||
|
|
||||||
- bun
|
- bun
|
||||||
- go
|
- go
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
To run filething, run
|
||||||
|
|
||||||
|
```BASH
|
||||||
|
go generate
|
||||||
|
go build -tags netgo -ldflags=-s
|
||||||
|
DB_HOST=localhost:5432 DB_NAME=filething DB_USER=postgres STORAGE_PATH=data ./filething
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
To run filething in dev mode with a hot reloading Ui server and auto rebuilding backend server, run
|
||||||
|
|
||||||
|
```BASH
|
||||||
|
DB_HOST=localhost:5432 DB_NAME=filething DB_USER=postgres STORAGE_PATH=data CompileDaemon --build="go build -tags netgo,dev -ldflags=-s" --command=./filething --exclude-dir=data/ --graceful-kill
|
||||||
|
```
|
||||||
|
|||||||
15
main.go
15
main.go
@@ -1,5 +1,5 @@
|
|||||||
//go:generate bun --cwd=./ui install
|
//go:generate sh -c "NODE_ENV=production bun --cwd=./ui install"
|
||||||
//go:generate bun --bun --cwd=./ui run generate
|
//go:generate sh -c "NODE_ENV=production bun --bun --cwd=./ui run generate"
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -22,7 +22,10 @@ import (
|
|||||||
"github.com/uptrace/bun/driver/pgdriver"
|
"github.com/uptrace/bun/driver/pgdriver"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var initUi func(e *echo.Echo)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
dbHost := os.Getenv("DB_HOST")
|
dbHost := os.Getenv("DB_HOST")
|
||||||
dbName := os.Getenv("DB_NAME")
|
dbName := os.Getenv("DB_NAME")
|
||||||
dbUser := os.Getenv("DB_USER")
|
dbUser := os.Getenv("DB_USER")
|
||||||
@@ -77,7 +80,7 @@ func main() {
|
|||||||
|
|
||||||
api.POST("/files/upload*", routes.UploadFile)
|
api.POST("/files/upload*", routes.UploadFile)
|
||||||
api.GET("/files/get/*", routes.GetFiles)
|
api.GET("/files/get/*", routes.GetFiles)
|
||||||
api.GET("/files/download/*", routes.GetFile)
|
api.GET("/files/download*", routes.GetFile)
|
||||||
api.POST("/files/delete*", routes.DeleteFiles)
|
api.POST("/files/delete*", routes.DeleteFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,11 +88,13 @@ func main() {
|
|||||||
// this isnt explicitly required, but it provides a better experience than doing this same thing clientside
|
// this isnt explicitly required, but it provides a better experience than doing this same thing clientside
|
||||||
e.Use(middleware.AuthCheckMiddleware)
|
e.Use(middleware.AuthCheckMiddleware)
|
||||||
|
|
||||||
e.GET("/*", echo.StaticDirectoryHandler(ui.DistDirFS, false))
|
initUi(e)
|
||||||
|
|
||||||
e.HTTPErrorHandler = customHTTPErrorHandler
|
e.HTTPErrorHandler = customHTTPErrorHandler
|
||||||
|
|
||||||
e.Logger.Fatal(e.Start(":1323"))
|
if err := e.Start(":1323"); err != nil && err != http.ErrServerClosed {
|
||||||
|
fmt.Println("Error starting HTTP server:", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func customHTTPErrorHandler(err error, c echo.Context) {
|
func customHTTPErrorHandler(err error, c echo.Context) {
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ var authenticatedPages = []string{
|
|||||||
func AuthCheckMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
func AuthCheckMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
path := c.Request().URL.Path
|
path := c.Request().URL.Path
|
||||||
|
|
||||||
|
// bypass auth checks for static and dev resources
|
||||||
|
if strings.HasPrefix(path, "/_nuxt/") || strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".css") {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
|
||||||
_, cookieErr := c.Cookie("sessionToken")
|
_, cookieErr := c.Cookie("sessionToken")
|
||||||
authenticated := cookieErr == nil
|
authenticated := cookieErr == nil
|
||||||
|
|
||||||
|
|||||||
126
routes/files.go
126
routes/files.go
@@ -1,10 +1,13 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
"filething/models"
|
"filething/models"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -78,11 +81,6 @@ func UploadFile(c echo.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
filepath := filepath.Join(basePath, part.FileName())
|
filepath := filepath.Join(basePath, part.FileName())
|
||||||
|
|
||||||
if _, err = os.Stat(filepath); err == nil {
|
if _, err = os.Stat(filepath); err == nil {
|
||||||
@@ -152,14 +150,11 @@ func UploadFile(c echo.Context) error {
|
|||||||
|
|
||||||
func calculateStorageUsage(basePath string) (int64, error) {
|
func calculateStorageUsage(basePath string) (int64, error) {
|
||||||
var totalSize int64
|
var totalSize int64
|
||||||
|
|
||||||
// Read the directory
|
|
||||||
entries, err := os.ReadDir(basePath)
|
entries, err := os.ReadDir(basePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over directory entries
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
// Recursively calculate size of directories
|
// Recursively calculate size of directories
|
||||||
@@ -226,9 +221,122 @@ func GetFile(c echo.Context) error {
|
|||||||
user := c.Get("user").(*models.User)
|
user := c.Get("user").(*models.User)
|
||||||
|
|
||||||
fullPath := strings.Trim(c.Param("*"), "/")
|
fullPath := strings.Trim(c.Param("*"), "/")
|
||||||
basePath := fmt.Sprintf("%s/%s/%s", os.Getenv("STORAGE_PATH"), user.ID, fullPath)
|
|
||||||
|
|
||||||
|
fileNamesParam := c.QueryParam("filenames")
|
||||||
|
var fileNames []string
|
||||||
|
if fileNamesParam != "" {
|
||||||
|
fileNames = strings.Split(fileNamesParam, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if fullPath == "" && len(fileNames) == 0 {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"message": "A file is required"})
|
||||||
|
}
|
||||||
|
|
||||||
|
basePath := fmt.Sprintf("%s/%s", os.Getenv("STORAGE_PATH"), user.ID)
|
||||||
|
if fullPath != "" {
|
||||||
|
basePath = filepath.Join(basePath, fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo, err := os.Stat(basePath)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusNotFound, map[string]string{"message": "No file found!"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
c.Response().Header().Set(echo.HeaderContentType, "application/zip")
|
||||||
|
|
||||||
|
if len(fileNames) != 0 {
|
||||||
|
err := zipFiles(&buf, filepath.Join(basePath, fullPath), fileNames)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = buf.WriteTo(c.Response().Writer)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := zipFiles(&buf, basePath, []string{""})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = buf.WriteTo(c.Response().Writer)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
return c.File(basePath)
|
return c.File(basePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func zipFiles(buf *bytes.Buffer, basePath string, files []string) error {
|
||||||
|
zipWriter := zip.NewWriter(buf)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
for _, filePath := range files {
|
||||||
|
unescapedFilePath, err := url.PathUnescape(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = processFile(zipWriter, basePath, unescapedFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processFile(zipWriter *zip.Writer, basePath string, filePath string) error {
|
||||||
|
fullFilePath := filepath.Join(basePath, filePath)
|
||||||
|
|
||||||
|
fileInfo, err := os.Stat(fullFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := zip.FileInfoHeader(fileInfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
header.Method = zip.Deflate
|
||||||
|
|
||||||
|
header.Name = filepath.ToSlash(filePath)
|
||||||
|
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
header.Name += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
headerWriter, err := zipWriter.CreateHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
files, err := os.ReadDir(fullFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
err := processFile(zipWriter, basePath, filepath.Join(filePath, file.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(fullFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(headerWriter, file)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeleteRequest struct {
|
type DeleteRequest struct {
|
||||||
|
|||||||
16
server.go
Normal file
16
server.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//go:build !dev
|
||||||
|
// +build !dev
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"filething/ui"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initUi = func(e *echo.Echo) {
|
||||||
|
e.GET("/*", echo.StaticDirectoryHandler(ui.DistDirFS, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
69
server_dev.go
Normal file
69
server_dev.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
//go:build dev
|
||||||
|
// +build dev
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
echoMiddleware "github.com/labstack/echo/v4/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initUi = func(e *echo.Echo) {
|
||||||
|
shutdown := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
|
go func() {
|
||||||
|
cmd := exec.Command("bun", "--cwd=ui", "run", "dev")
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
// use a preocess group since otherwise the node processes spawned by bun wont die
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM); err != nil {
|
||||||
|
fmt.Println("Error sending SIGTERM to Nuxt dev server group:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Error starting Nuxt dev server:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-shutdown
|
||||||
|
|
||||||
|
if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM); err != nil {
|
||||||
|
fmt.Println("Error sending SIGTERM to Nuxt dev server group:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
fmt.Println("Error waiting for Nuxt dev server to exit:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Nuxt dev server stopped")
|
||||||
|
|
||||||
|
if err := e.Shutdown(context.Background()); err != nil {
|
||||||
|
fmt.Println("Error shutting down HTTP server:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
target := "localhost:3000"
|
||||||
|
e.Group("/*").Use(echoMiddleware.ProxyWithConfig(echoMiddleware.ProxyConfig{
|
||||||
|
Balancer: echoMiddleware.NewRoundRobinBalancer([]*echoMiddleware.ProxyTarget{
|
||||||
|
{URL: &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: target,
|
||||||
|
}},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,9 +13,12 @@
|
|||||||
--highlight-low: 11 18 22;
|
--highlight-low: 11 18 22;
|
||||||
--highlight-med: 32 37 38;
|
--highlight-med: 32 37 38;
|
||||||
--highlight-high: 49 55 58;
|
--highlight-high: 49 55 58;
|
||||||
--color-foam: 86 148 159;
|
--color-foam: 32 159 181;
|
||||||
--color-love: 220 100 130;
|
--color-love: 220 100 130;
|
||||||
--color-pine: 40 105 131;
|
--color-pine: 40 105 131;
|
||||||
|
--color-accent: 136 57 239;
|
||||||
|
--color-accent-20: #dac9f1;
|
||||||
|
|
||||||
|
|
||||||
--nav-height: 48px;
|
--nav-height: 48px;
|
||||||
}
|
}
|
||||||
@@ -31,9 +34,11 @@
|
|||||||
--highlight-low: 244 237 232;
|
--highlight-low: 244 237 232;
|
||||||
--highlight-med: 223 218 217;
|
--highlight-med: 223 218 217;
|
||||||
--highlight-high: 206 202 205;
|
--highlight-high: 206 202 205;
|
||||||
--color-foam: 156 207 216;
|
--color-foam: 145 215 227;
|
||||||
--color-love: 235 111 146;
|
--color-love: 235 111 146;
|
||||||
--color-pine: 49 116 143;
|
--color-pine: 49 116 143;
|
||||||
|
--color-accent: 154 87 237;
|
||||||
|
--color-accent-20: #342c3f;
|
||||||
|
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
ui/bun.lockb
BIN
ui/bun.lockb
Binary file not shown.
@@ -25,7 +25,7 @@ const crumbs = computed(() => {
|
|||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="m9 6l6 6l-6 6" />
|
d="m9 6l6 6l-6 6" />
|
||||||
</svg>
|
</svg>
|
||||||
<NuxtLink class="hover:text-text" :class="index === crumbs.length - 1 ? 'text-foam' : 'text-subtle'"
|
<NuxtLink :class="index === crumbs.length - 1 ? 'text-foam' : 'text-subtle hover:text-text'"
|
||||||
:to="crumb.link">{{
|
:to="crumb.link">{{
|
||||||
crumb.name }}</NuxtLink>
|
crumb.name }}</NuxtLink>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-on:click="toggle()" class="w-5 h-5 border rounded cursor-pointer flex items-center justify-center"
|
<div v-on:click="toggle()" class="w-5 h-5 border rounded cursor-pointer flex items-center justify-center"
|
||||||
:class="state === 'unchecked' ? 'hover:bg-muted/5 active:bg-muted/15' : 'bg-foam/10 hover:bg-foam/15 active:bg-foam/25 text-foam'">
|
:class="state === 'unchecked' ? 'hover:bg-muted/5 active:bg-muted/15' : 'bg-accent/10 hover:bg-accent/15 active:bg-accent/25 text-accent'">
|
||||||
<div v-if="state === 'some'" class="w-8/12 h-0.5 bg-current rounded-full"></div>
|
<div v-if="state === 'some'" class="w-8/12 h-0.5 bg-current rounded-full"></div>
|
||||||
<span v-else-if="state === 'checked'">
|
<span v-else-if="state === 'checked'">
|
||||||
<svg class="w-full h-full" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg class="w-full h-full" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const isInFolder = computed(() => route.path.startsWith('/home/') && route.path
|
|||||||
<NuxtLink to="/home"
|
<NuxtLink to="/home"
|
||||||
class="flex py-1.5 px-4 rounded-lg transition-bg duration-300 hover:bg-muted/10"
|
class="flex py-1.5 px-4 rounded-lg transition-bg duration-300 hover:bg-muted/10"
|
||||||
:class="{ 'bg-muted/10': isAllFilesActive }">
|
:class="{ 'bg-muted/10': isAllFilesActive }">
|
||||||
|
<div class="flex relative">
|
||||||
<svg class="m-0.5 mr-2" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
|
<svg class="m-0.5 mr-2" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
|
||||||
viewBox="0 0 256 256">
|
viewBox="0 0 256 256">
|
||||||
<g fill="currentColor">
|
<g fill="currentColor">
|
||||||
@@ -55,12 +56,16 @@ const isInFolder = computed(() => route.path.startsWith('/home/') && route.path
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
All files
|
All files
|
||||||
|
<div class="absolute -left-1.5 top-px bottom-px bg-accent w-[2px]"
|
||||||
|
:class="{ 'hidden': !isAllFilesActive }"></div>
|
||||||
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex flex-col">
|
<!-- <li class="flex flex-col">
|
||||||
<NuxtLink to="/home/name"
|
<NuxtLink to="/home/name"
|
||||||
class="flex py-1.5 px-4 rounded-lg transition-bg duration-300 hover:bg-muted/10"
|
class="flex py-1.5 px-4 rounded-lg transition-bg duration-300 hover:bg-muted/10"
|
||||||
:class="{ 'bg-muted/10': isInFolder }">
|
:class="{ 'bg-muted/10': isInFolder }">
|
||||||
|
<div class="flex relative">
|
||||||
<svg v-if="isInFolder" class="m-0.5 mr-2" xmlns="http://www.w3.org/2000/svg" width="20"
|
<svg v-if="isInFolder" class="m-0.5 mr-2" xmlns="http://www.w3.org/2000/svg" width="20"
|
||||||
height="20" viewBox="0 0 24 24">
|
height="20" viewBox="0 0 24 24">
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
@@ -74,34 +79,20 @@ const isInFolder = computed(() => route.path.startsWith('/home/') && route.path
|
|||||||
d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2" />
|
d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2" />
|
||||||
</svg>
|
</svg>
|
||||||
Folders
|
Folders
|
||||||
|
<div class="absolute -left-1.5 top-px bottom-px bg-accent w-[2px]"
|
||||||
|
:class="{ 'hidden': !isInFolder }"></div>
|
||||||
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<!-- <ul class="flex flex-col gap-y-2 w-4/5 mt-2 ml-auto">
|
</li> -->
|
||||||
<li>
|
|
||||||
<a href="/folder/thing" class="flex py-1.5 px-4 rounded-lg transition-bg duration-300"
|
|
||||||
:class="isActive('/folder/thing') ? 'bg-muted/10' : 'hover:bg-muted/10'">
|
|
||||||
<svg class="m-0.5 mr-2" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
|
|
||||||
viewBox="0 0 256 256">
|
|
||||||
<g fill="currentColor">
|
|
||||||
<path d="M208 72v112a8 8 0 0 1-8 8h-24v-88l-40-40H80V40a8 8 0 0 1 8-8h80Z"
|
|
||||||
opacity=".2" />
|
|
||||||
<path
|
|
||||||
d="m213.66 66.34l-40-40A8 8 0 0 0 168 24H88a16 16 0 0 0-16 16v16H56a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h112a16 16 0 0 0 16-16v-16h16a16 16 0 0 0 16-16V72a8 8 0 0 0-2.34-5.66ZM168 216H56V72h76.69L168 107.31V216Zm32-32h-16v-80a8 8 0 0 0-2.34-5.66l-40-40A8 8 0 0 0 136 56H88V40h76.69L200 75.31Zm-56-32a8 8 0 0 1-8 8H88a8 8 0 0 1 0-16h48a8 8 0 0 1 8 8Zm0 32a8 8 0 0 1-8 8H88a8 8 0 0 1 0-16h48a8 8 0 0 1 8 8Z" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
All files
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul> -->
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2 w-[calc(100%-16px)]">
|
<div class="m-2 w-[calc(100%-16px)]">
|
||||||
<div class="p-3 bg-overlay border rounded-lg flex items-end">
|
<div class="p-3 bg-overlay border rounded-lg flex items-end">
|
||||||
<svg width="32" height="32" class="-rotate-90 mr-2" xmlns="http://www.w3.org/2000/svg">
|
<svg width="32" height="32" class="-rotate-90 mr-2" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Background Track -->
|
<!-- Background Track -->
|
||||||
<circle class="stroke-foam/20" cx="16" cy="16" :r="radius" fill="none" stroke-width="3" />
|
<circle class="stroke-accent/20" cx="16" cy="16" :r="radius" fill="none" stroke-width="3" />
|
||||||
<!-- Progress Track -->
|
<!-- Progress Track -->
|
||||||
<circle class="stroke-foam" cx="16" cy="16" :r="radius" fill="none" stroke-width="3"
|
<circle class="stroke-accent" cx="16" cy="16" :r="radius" fill="none" stroke-width="3"
|
||||||
:stroke-dasharray="circumference" :stroke-dashoffset="offset" stroke-linecap="round" />
|
:stroke-dasharray="circumference" :stroke-dashoffset="offset" stroke-linecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
<p class="text-sm h-min"> {{ usage }} of {{ capacity }}</p>
|
<p class="text-sm h-min"> {{ usage }} of {{ capacity }}</p>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
ssr: true,
|
ssr: process.env.NODE_ENV === 'production' ? true : false,
|
||||||
compatibilityDate: '2024-04-03',
|
compatibilityDate: '2024-04-03',
|
||||||
|
|
||||||
css: ['~/assets/css/main.css'],
|
css: ['~/assets/css/main.css'],
|
||||||
@@ -11,10 +11,6 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
|
|
||||||
experimental: {
|
|
||||||
buildCache: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxtjs/color-mode',
|
'@nuxtjs/color-mode',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -208,9 +208,14 @@ const openFilePicker = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createFolder = async () => {
|
const createFolder = async () => {
|
||||||
let { data, error } = await useFetch('/api/files/upload/' + route.path.replace(/^\/home/, '') + '/' + folderName.value, {
|
const { data, error } = await useAsyncData(
|
||||||
method: "POST"
|
() => $fetch('/api/files/upload' + route.path.replace(/^\/home/, '') + '/' + folderName.value, {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
files: selectedFiles.value?.map(file => ({ name: file.name }))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
if (error.value != null) {
|
if (error.value != null) {
|
||||||
folderError.value = error.value.data.message;
|
folderError.value = error.value.data.message;
|
||||||
@@ -224,7 +229,7 @@ const createFolder = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteFiles = async () => {
|
const deleteFiles = async () => {
|
||||||
await useFetch('/api/files/delete' + route.path.replace(/^\/home/, ''), {
|
await $fetch('/api/files/delete' + route.path.replace(/^\/home/, ''), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
files: selectedFiles.value?.map(file => ({ name: file.name }))
|
files: selectedFiles.value?.map(file => ({ name: file.name }))
|
||||||
@@ -233,6 +238,47 @@ const deleteFiles = async () => {
|
|||||||
|
|
||||||
files.value = files.value?.filter(file => !selectedFiles.value?.includes(file))
|
files.value = files.value?.filter(file => !selectedFiles.value?.includes(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const downloadFile = (file) => {
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.href = '/api/files/download/' + file.name;
|
||||||
|
anchor.download = file.name;
|
||||||
|
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
anchor.click();
|
||||||
|
document.body.removeChild(anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadFiles = async () => {
|
||||||
|
let filenames = ""
|
||||||
|
|
||||||
|
selectedFiles.value?.forEach((file, i) => {
|
||||||
|
filenames += encodeURIComponent(file.name)
|
||||||
|
if (i != selectedFiles.value?.length - 1) {
|
||||||
|
filenames += ",";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let { data, error } = await useAsyncData(
|
||||||
|
() => $fetch('/api/files/download', {
|
||||||
|
params: {
|
||||||
|
"filenames": filenames
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log("DATA", data.value)
|
||||||
|
|
||||||
|
if (error.value == null) {
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.href = window.URL.createObjectURL(data.value)
|
||||||
|
anchor.download = "filething.zip";
|
||||||
|
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
anchor.click();
|
||||||
|
document.body.removeChild(anchor);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -251,10 +297,10 @@ const deleteFiles = async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="ml-auto flex gap-x-1.5">
|
<div class="ml-auto flex gap-x-1.5">
|
||||||
<button v-on:click="popupVisable = !popupVisable"
|
<button v-on:click="popupVisable = !popupVisable"
|
||||||
class=" px-2 py-1 rounded-md text-sm border bg-muted/10 hover:bg-muted/15 active:bg-muted/25">Close</button>
|
class=" px-2 py-1 rounded-md text-sm border bg-muted/10 hover:bg-muted/15 active:bg-muted/25 transition-bg">Close</button>
|
||||||
<button v-on:click="createFolder" :disabled="folderName === ''"
|
<button v-on:click="createFolder" :disabled="folderName === ''"
|
||||||
class=" px-2 py-1 rounded-md text-sm
|
class=" px-2 py-1 rounded-md text-sm
|
||||||
disabled:bg-highlight-med/50 bg-highlight-med hover:brightness-105 active:brightness-110 transition-[background-color,filter] text-surface disabled:cursor-not-allowed">Confirm</button>
|
disabled:bg-highlight-med/50 bg-highlight-med not:hover:brightness-105 not:active:brightness-110 transition-[background-color,filter] text-surface disabled:cursor-not-allowed">Confirm</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
@@ -271,7 +317,7 @@ const deleteFiles = async () => {
|
|||||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
stroke-width="2">
|
stroke-width="2">
|
||||||
<path d="M7 18a4.6 4.4 0 0 1 0-9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-1" />
|
<path d="M7 18a4.6 4.4 0 0 1 0-9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-1" />
|
||||||
<path d="m9 15l3-3l3 3m-3-3v9" />
|
<path stroke="rgb(var(--color-accent))" d="m9 15l3-3l3 3m-3-3v9" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
Upload
|
Upload
|
||||||
@@ -279,9 +325,11 @@ const deleteFiles = async () => {
|
|||||||
<button v-on:click="popupVisable = !popupVisable"
|
<button v-on:click="popupVisable = !popupVisable"
|
||||||
class="rounded-xl border-2 border-surface flex flex-col gap-y-2 px-2 py-3 w-40 justify-center items-center hover:bg-muted/10 active:bg-muted/20 transition-bg">
|
class="rounded-xl border-2 border-surface flex flex-col gap-y-2 px-2 py-3 w-40 justify-center items-center hover:bg-muted/10 active:bg-muted/20 transition-bg">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2">
|
||||||
d="M12 19H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h4l3 3h7a2 2 0 0 1 2 2v3.5M16 19h6m-3-3v6" />
|
<path d="M12 19H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h4l3 3h7a2 2 0 0 1 2 2v3.5M16" />
|
||||||
|
<path stroke="rgb(var(--color-accent))" d="M16 19h6m-3-3v6" />
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
New folder
|
New folder
|
||||||
</button>
|
</button>
|
||||||
@@ -294,7 +342,16 @@ const deleteFiles = async () => {
|
|||||||
<Breadcrumbs :path="route.path" />
|
<Breadcrumbs :path="route.path" />
|
||||||
</h3>
|
</h3>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<div v-if="selectedFiles?.length > 0">
|
<div class="flex flex-row gap-x-2" v-if="selectedFiles?.length > 0">
|
||||||
|
<button v-on:click="downloadFiles"
|
||||||
|
class="flex flex-row px-2 py-1 rounded-md transition-bg text-xs border hover:bg-muted/10 active:bg-muted/20 items-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||||
|
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M4 20h16m-8-6V4m0 10l4-4m-4 4l-4-4" />
|
||||||
|
</svg>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
<button v-on:click="deleteFiles"
|
<button v-on:click="deleteFiles"
|
||||||
class="flex flex-row px-2 py-1 rounded-md transition-bg text-xs border hover:bg-love/10 active:bg-love/20 hover:text-love active:text-love items-center">
|
class="flex flex-row px-2 py-1 rounded-md transition-bg text-xs border hover:bg-love/10 active:bg-love/20 hover:text-love active:text-love items-center">
|
||||||
<svg class="mr-1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
<svg class="mr-1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||||
@@ -329,8 +386,9 @@ const deleteFiles = async () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="block">
|
<tbody class="block">
|
||||||
<tr class="flex flex-row h-10 group items-center border-b hover:bg-muted/10 transition-bg"
|
<tr class="flex border-l-2 flex-row h-10 group items-center border-b active:bg-surface/45 transition-bg relative"
|
||||||
v-for="file in sortedFiles">
|
v-for="file in sortedFiles"
|
||||||
|
:class="file.toggled === 'checked' ? 'bg-accent/20 border-l-accent' : 'border-l-transparent hover:bg-surface'">
|
||||||
<td class="-ml-7 pr-4 flex-shrink-0">
|
<td class="-ml-7 pr-4 flex-shrink-0">
|
||||||
<div class="w-5 h-5">
|
<div class="w-5 h-5">
|
||||||
<Checkbox class="group-hover:flex"
|
<Checkbox class="group-hover:flex"
|
||||||
@@ -371,6 +429,18 @@ const deleteFiles = async () => {
|
|||||||
<td class="min-w-28 text-start sm:block hidden">
|
<td class="min-w-28 text-start sm:block hidden">
|
||||||
{{ file.last_modified }}
|
{{ file.last_modified }}
|
||||||
</td>
|
</td>
|
||||||
|
<td :class="file.toggled === 'checked' ? 'context-active' : 'context'"
|
||||||
|
class="absolute pl-6 top-0 bottom-0 right-0 hidden group-hover:flex items-center pr-8">
|
||||||
|
<button v-on:click="downloadFile(file)"
|
||||||
|
class="p-2 rounded hover:bg-muted/10 active:bg-muted/20">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M4 20h16m-8-6V4m0 10l4-4m-4 4l-4-4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -388,4 +458,12 @@ th {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-active {
|
||||||
|
background: linear-gradient(to right, transparent, var(--color-accent-20) 16px, var(--color-accent-20) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context {
|
||||||
|
background: linear-gradient(to right, transparent, rgb(var(--color-surface)) 16px, rgb(var(--color-surface)) 100%);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -21,6 +21,7 @@ module.exports = {
|
|||||||
foam: "rgb(var(--color-foam))",
|
foam: "rgb(var(--color-foam))",
|
||||||
love: "rgb(var(--color-love))",
|
love: "rgb(var(--color-love))",
|
||||||
pine: "rgb(var(--color-pine))",
|
pine: "rgb(var(--color-pine))",
|
||||||
|
accent: "rgb(var(--color-accent))",
|
||||||
"highlight-low": "rgb(var(--highlight-low))",
|
"highlight-low": "rgb(var(--highlight-low))",
|
||||||
"highlight-med": "rgb(var(--highlight-med))",
|
"highlight-med": "rgb(var(--highlight-med))",
|
||||||
"highlight-high": "rgb(var(--highlight-high))",
|
"highlight-high": "rgb(var(--highlight-high))",
|
||||||
@@ -30,5 +31,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
future: {
|
||||||
|
hoverOnlyWhenSupported: true,
|
||||||
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user