50ed8abe95
- Full file browser page at /files with lazy-load, breadcrumbs, drag&drop - FileBrowserModal component for path selection from Samba/NFS forms - PathField component replaces bare inputs in share/export forms - Backend: /api/files/* routes with List, Mkdir, Rename, Delete, Chmod, Chown, Upload, Download, Preview, Search - Reuses NASCTL_ALLOWED_ROOTS for path validation - NASCTL_UPLOAD_MAX_BYTES (100MB) and NASCTL_PREVIEW_MAX_BYTES (256KB) env vars - Capabilities endpoint returns chmod/chown availability (requires root) - Version bump: 0.3.2 -> 0.4.0
298 lines
6.6 KiB
Go
298 lines
6.6 KiB
Go
package files
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
"testing"
|
|
)
|
|
|
|
func TestList(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
subdir := filepath.Join(dir, "subdir")
|
|
if err := os.MkdirAll(subdir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f1 := filepath.Join(dir, "a.txt")
|
|
if err := os.WriteFile(f1, []byte("hello"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f2 := filepath.Join(dir, "b.log")
|
|
if err := os.WriteFile(f2, []byte("world"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
res, err := List(dir, nil, 1, 50)
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if res.Total != 3 { // subdir, a.txt, b.log
|
|
t.Errorf("Total=%d, want 3", res.Total)
|
|
}
|
|
if res.HasMore {
|
|
t.Errorf("HasMore=true, want false")
|
|
}
|
|
}
|
|
|
|
func TestListPaged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
for i := 0; i < 10; i++ {
|
|
if err := os.WriteFile(filepath.Join(dir, string(rune('a'+i))+".txt"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
page1, err := List(dir, nil, 1, 3)
|
|
if err != nil {
|
|
t.Fatalf("page1: %v", err)
|
|
}
|
|
if len(page1.Entries) != 3 {
|
|
t.Errorf("page1 len=%d, want 3", len(page1.Entries))
|
|
}
|
|
if !page1.HasMore {
|
|
t.Errorf("page1.HasMore=false, want true")
|
|
}
|
|
|
|
page2, err := List(dir, nil, 2, 3)
|
|
if err != nil {
|
|
t.Fatalf("page2: %v", err)
|
|
}
|
|
if len(page2.Entries) != 3 {
|
|
t.Errorf("page2 len=%d, want 3", len(page2.Entries))
|
|
}
|
|
|
|
page4, err := List(dir, nil, 4, 3)
|
|
if err != nil {
|
|
t.Fatalf("page4: %v", err)
|
|
}
|
|
if len(page4.Entries) != 1 {
|
|
t.Errorf("page4 len=%d, want 1", len(page4.Entries))
|
|
}
|
|
if page4.HasMore {
|
|
t.Errorf("page4.HasMore=true, want false")
|
|
}
|
|
}
|
|
|
|
func TestListRejectsNotDir(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "file.txt")
|
|
if err := os.WriteFile(f, []byte("hi"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := List(f, nil, 1, 50)
|
|
if err == nil {
|
|
t.Error("expected error for non-directory")
|
|
}
|
|
}
|
|
|
|
func TestInfo(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "test.txt")
|
|
if err := os.WriteFile(f, []byte("hello world"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
info, err := Info(f, nil)
|
|
if err != nil {
|
|
t.Fatalf("Info: %v", err)
|
|
}
|
|
if info.Name != "test.txt" {
|
|
t.Errorf("Name=%q, want test.txt", info.Name)
|
|
}
|
|
if info.IsDir {
|
|
t.Errorf("IsDir=true, want false")
|
|
}
|
|
if info.Size != 11 {
|
|
t.Errorf("Size=%d, want 11", info.Size)
|
|
}
|
|
}
|
|
|
|
func TestMkdir(t *testing.T) {
|
|
dir := t.TempDir()
|
|
newDir := filepath.Join(dir, "newdir", "nested")
|
|
if err := Mkdir(newDir, nil); err != nil {
|
|
t.Fatalf("Mkdir: %v", err)
|
|
}
|
|
info, err := os.Stat(newDir)
|
|
if err != nil {
|
|
t.Fatalf("stat: %v", err)
|
|
}
|
|
if !info.IsDir() {
|
|
t.Error("expected dir")
|
|
}
|
|
}
|
|
|
|
func TestMkdirDisallowed(t *testing.T) {
|
|
dir := t.TempDir()
|
|
err := Mkdir(filepath.Join(dir, "ok"), []string{"/tmp"})
|
|
if err == nil {
|
|
t.Error("expected error for disallowed path")
|
|
}
|
|
}
|
|
|
|
func TestRename(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "old.txt")
|
|
if err := os.WriteFile(f, []byte("hi"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Rename(f, "new.txt", nil); err != nil {
|
|
t.Fatalf("Rename: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "new.txt")); err != nil {
|
|
t.Errorf("new file not found: %v", err)
|
|
}
|
|
if _, err := os.Stat(f); err == nil {
|
|
t.Error("old file still exists")
|
|
}
|
|
}
|
|
|
|
func TestRenameBadName(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "old.txt")
|
|
if err := os.WriteFile(f, []byte("hi"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tests := []string{"/etc/passwd", "..", ".", "-f", ""}
|
|
for _, name := range tests {
|
|
err := Rename(f, name, nil)
|
|
if err == nil {
|
|
t.Errorf("Rename(%q): expected error", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "todelete.txt")
|
|
if err := os.WriteFile(f, []byte("hi"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Delete(f, nil); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
if _, err := os.Stat(f); err == nil {
|
|
t.Error("file still exists after delete")
|
|
}
|
|
}
|
|
|
|
func TestDeleteRecursively(t *testing.T) {
|
|
dir := t.TempDir()
|
|
subdir := filepath.Join(dir, "sub")
|
|
if err := os.MkdirAll(subdir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(subdir, "f.txt"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Delete(subdir, nil); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
if _, err := os.Stat(subdir); err == nil {
|
|
t.Error("subdir still exists")
|
|
}
|
|
}
|
|
|
|
func TestDeleteRejectsRoot(t *testing.T) {
|
|
err := Delete("/", nil)
|
|
if err == nil {
|
|
t.Error("expected error deleting root")
|
|
}
|
|
}
|
|
|
|
func TestChmod(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "file.txt")
|
|
if err := os.WriteFile(f, []byte("hi"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Chmod(f, 0o600, nil); err != nil {
|
|
t.Fatalf("Chmod: %v", err)
|
|
}
|
|
info, _ := os.Stat(f)
|
|
if info.Mode().Perm() != 0o600 {
|
|
t.Errorf("mode=%o, want 600", info.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
func TestChown(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "file.txt")
|
|
if err := os.WriteFile(f, []byte("hi"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stat, _ := os.Stat(f)
|
|
st := stat.Sys().(*syscall.Stat_t)
|
|
if err := Chown(f, int(st.Uid), int(st.Gid), nil); err != nil {
|
|
t.Fatalf("Chown: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSearch(t *testing.T) {
|
|
dir := t.TempDir()
|
|
sub := filepath.Join(dir, "photos")
|
|
if err := os.MkdirAll(sub, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
files := map[string]string{
|
|
"myphoto.jpg": "xxx",
|
|
"photo_album.txt": "yyy",
|
|
"report.pdf": "zzz",
|
|
"data.csv": "aaa",
|
|
}
|
|
for name, content := range files {
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.WriteFile(filepath.Join(sub, "vacation.png"), []byte("img"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
hits, err := Search(dir, "photo", 50, nil)
|
|
if err != nil {
|
|
t.Fatalf("Search: %v", err)
|
|
}
|
|
if len(hits) < 2 {
|
|
t.Errorf("got %d hits, want at least 2", len(hits))
|
|
}
|
|
}
|
|
|
|
func TestSearchLimit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
for i := 0; i < 20; i++ {
|
|
if err := os.WriteFile(filepath.Join(dir, "file_match_"+string(rune('a'+i))+".txt"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
hits, err := Search(dir, "file_match", 5, nil)
|
|
if err != nil {
|
|
t.Fatalf("Search: %v", err)
|
|
}
|
|
if len(hits) != 5 {
|
|
t.Errorf("got %d hits, want 5", len(hits))
|
|
}
|
|
}
|
|
|
|
func TestValidatePathAllowed(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "allowed.txt")
|
|
if err := os.WriteFile(f, []byte("hi"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
errs := []string{
|
|
filepath.Join(dir, "..", "etc", "passwd"),
|
|
"/etc/passwd",
|
|
"/tmp/../../../secret",
|
|
}
|
|
for _, path := range errs {
|
|
_, err := List(path, []string{dir}, 1, 50)
|
|
if err == nil {
|
|
t.Errorf("expected error for %q", path)
|
|
}
|
|
}
|
|
}
|