feat: Sprint 4 — SFTP dual-pane with per-type icons, breadcrumb, drop-zone, create/delete, toast

This commit is contained in:
swanadiva
2026-07-07 13:35:01 +07:00
parent c5ac07e27f
commit 941a67b6ea
8 changed files with 364 additions and 10 deletions
+78
View File
@@ -0,0 +1,78 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"git.tukangketik.id/swanadiva/hostkeeper/v2/internal/model"
)
var fileStore = model.NewFileStore()
func SFTP(c *fiber.Ctx) error {
local, remote := fileStore.All()
return c.Render("sftp", fiber.Map{
"View": "sftp",
"Title": "SFTP",
"LocalFiles": local,
"RemoteFiles": remote,
"LocalBreadcrumb": fileStore.LocalBreadcrumb,
"RemoteBreadcrumb": fileStore.RemoteBreadcrumb,
"NavItems": navItems,
})
}
func SFTPPane(c *fiber.Ctx) error {
pane := c.Query("pane", "local")
dir := c.Query("dir", "/")
local, remote := fileStore.All()
files := local
breadcrumb := fileStore.LocalBreadcrumb
if pane == "remote" {
files = remote
breadcrumb = fileStore.RemoteBreadcrumb
}
return c.Render("sftp_pane", fiber.Map{
"Pane": pane,
"Files": files,
"Breadcrumb": breadcrumb,
"CurrentDir": dir,
})
}
func CreateFolder(c *fiber.Ctx) error {
pane := c.FormValue("pane", "local")
name := c.FormValue("name")
if name != "" {
item := model.FileItem{
Name: name, Size: "—", Modified: "Just now",
Type: "folder", Permissions: "drwxr-xr-x",
}
if pane == "remote" {
fileStore.Remote = append([]model.FileItem{item}, fileStore.Remote...)
} else {
fileStore.Local = append([]model.FileItem{item}, fileStore.Local...)
}
}
files := fileStore.Local
if pane == "remote" { files = fileStore.Remote }
return c.Render("sftp_pane", fiber.Map{
"Pane": pane, "Files": files,
"Breadcrumb": fileStore.LocalBreadcrumb,
})
}
func DeleteFile(c *fiber.Ctx) error {
pane := c.Query("pane", "local")
name := c.Query("name")
files := &fileStore.Local
if pane == "remote" { files = &fileStore.Remote }
for i, f := range *files {
if f.Name == name {
*files = append((*files)[:i], (*files)[i+1:]...)
break
}
}
return c.Render("sftp_pane", fiber.Map{
"Pane": pane, "Files": *files,
"Breadcrumb": fileStore.LocalBreadcrumb,
})
}
+60
View File
@@ -0,0 +1,60 @@
package model
type FileItem struct {
Name string `json:"name"`
Size string `json:"size"`
Modified string `json:"modified"`
Type string `json:"type"` // folder | file | image | code | lock
Extension string `json:"extension"`
Permissions string `json:"permissions"`
Path string `json:"path"`
}
type FileStore struct {
Local []FileItem
Remote []FileItem
LocalBreadcrumb []Breadcrumb
RemoteBreadcrumb []Breadcrumb
}
type Breadcrumb struct {
Name string
Path string
}
func NewFileStore() *FileStore {
return &FileStore{
Local: defaultLocalFiles(),
Remote: defaultRemoteFiles(),
LocalBreadcrumb: []Breadcrumb{{Name: "~", Path: "/home/user"}, {Name: "projects", Path: "/home/user/projects"}},
RemoteBreadcrumb: []Breadcrumb{{Name: "/", Path: "/"}, {Name: "var", Path: "/var"}, {Name: "www", Path: "/var/www"}},
}
}
func (s *FileStore) All() (local, remote []FileItem) { return s.Local, s.Remote }
func defaultLocalFiles() []FileItem {
return []FileItem{
{Name: "Downloads", Size: "—", Modified: "2 hours ago", Type: "folder", Permissions: "drwxr-xr-x", Path: "/home/user/Downloads"},
{Name: "Documents", Size: "—", Modified: "Yesterday", Type: "folder", Permissions: "drwxr-xr-x", Path: "/home/user/Documents"},
{Name: "projects", Size: "—", Modified: "Just now", Type: "folder", Permissions: "drwxrwxr-x", Path: "/home/user/projects"},
{Name: "config.yml", Size: "1.4 KB", Modified: "Yesterday", Type: "code", Extension: "yml", Permissions: "-rw-r--r--", Path: "/home/user/projects/config.yml"},
{Name: "README.md", Size: "3.2 KB", Modified: "2 days ago", Type: "code", Extension: "md", Permissions: "-rw-r--r--", Path: "/home/user/projects/README.md"},
{Name: "screenshot.png", Size: "240 KB", Modified: "3 days ago", Type: "image", Extension: "png", Permissions: "-rw-r--r--", Path: "/home/user/Downloads/screenshot.png"},
{Name: "archive.zip", Size: "42 MB", Modified: "1 week ago", Type: "file", Extension: "zip", Permissions: "-rw-r--r--", Path: "/home/user/Downloads/archive.zip"},
{Name: "vault.enc", Size: "8 KB", Modified: "2 weeks ago", Type: "lock", Extension: "enc", Permissions: "-rw-------", Path: "/home/user/vault.enc"},
}
}
func defaultRemoteFiles() []FileItem {
return []FileItem{
{Name: "html", Size: "—", Modified: "2 hours ago", Type: "folder", Permissions: "drwxr-xr-x", Path: "/var/www/html"},
{Name: "assets", Size: "—", Modified: "Yesterday", Type: "folder", Permissions: "drwxr-xr-x", Path: "/var/www/assets"},
{Name: "index.html", Size: "4.1 KB", Modified: "Just now", Type: "code", Extension: "html", Permissions: "-rw-r--r--", Path: "/var/www/index.html"},
{Name: "app.py", Size: "12.8 KB", Modified: "2 hours ago", Type: "code", Extension: "py", Permissions: "-rwxr-xr-x", Path: "/var/www/app.py"},
{Name: "Dockerfile", Size: "512 B", Modified: "Yesterday", Type: "code", Extension: "Dockerfile", Permissions: "-rw-r--r--", Path: "/var/www/Dockerfile"},
{Name: "compose.yml", Size: "2.1 KB", Modified: "Yesterday", Type: "code", Extension: "yml", Permissions: "-rw-r--r--", Path: "/var/www/compose.yml"},
{Name: ".env", Size: "156 B", Modified: "3 days ago", Type: "lock", Extension: "env", Permissions: "-rw-------", Path: "/var/www/.env"},
{Name: "logo.svg", Size: "6.7 KB", Modified: "1 week ago", Type: "image", Extension: "svg", Permissions: "-rw-r--r--", Path: "/var/www/assets/logo.svg"},
}
}
+22
View File
@@ -0,0 +1,22 @@
package model
type Transfer struct {
ID string `json:"id"`
FileName string `json:"fileName"`
Source string `json:"source"`
Destination string `json:"destination"`
Progress int `json:"progress"`
Speed string `json:"speed"`
Type string `json:"type"` // upload | download
Status string `json:"status"` // queued | transferring | completed | error
}
type TransferStore struct {
Transfers []Transfer
}
func NewTransferStore() *TransferStore {
return &TransferStore{}
}
func (s *TransferStore) All() []Transfer { return s.Transfers }