feat: file manager with browse, search, upload, chmod/chown
- 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
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PreviewResult struct {
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Mime string `json:"mime"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
var textMimes = map[string]bool{
|
||||
"text/plain": true,
|
||||
"text/html": true,
|
||||
"text/css": true,
|
||||
"text/javascript": true,
|
||||
"application/json": true,
|
||||
"application/xml": true,
|
||||
"application/xhtml+xml": true,
|
||||
"application/javascript": true,
|
||||
}
|
||||
|
||||
var imageMimes = map[string]bool{
|
||||
"image/png": true,
|
||||
"image/jpeg": true,
|
||||
"image/gif": true,
|
||||
"image/webp": true,
|
||||
"image/svg+xml": true,
|
||||
"image/bmp": true,
|
||||
"image/tiff": true,
|
||||
}
|
||||
|
||||
func Preview(path string, max int64, allowedRoots []string) (*PreviewResult, error) {
|
||||
data, err := ReadFile(path, max, allowedRoots)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mime := http.DetectContentType(data)
|
||||
size := int64(len(data))
|
||||
|
||||
trimmed := bytes.TrimSpace(data)
|
||||
if len(trimmed) > 0 {
|
||||
mime = http.DetectContentType(trimmed)
|
||||
}
|
||||
|
||||
result := &PreviewResult{
|
||||
Mime: mime,
|
||||
Size: size,
|
||||
}
|
||||
|
||||
if imageMimes[mime] || strings.HasPrefix(mime, "image/") {
|
||||
result.Type = "image"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if textMimes[mime] || strings.HasPrefix(mime, "text/") || isLikelyText(trimmed) {
|
||||
result.Type = "text"
|
||||
if size > 0 {
|
||||
result.Content = string(data)
|
||||
if int64(len(result.Content)) > max {
|
||||
result.Content = result.Content[:max]
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result.Type = "binary"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func isLikelyText(data []byte) bool {
|
||||
if len(data) == 0 {
|
||||
return false
|
||||
}
|
||||
nulls := 0
|
||||
for _, b := range data {
|
||||
gb := int(b)
|
||||
if (gb < 32 && gb != 9 && gb != 10 && gb != 13) || gb == 127 {
|
||||
nulls++
|
||||
}
|
||||
}
|
||||
return float64(nulls)/float64(len(data)) < 0.1
|
||||
}
|
||||
|
||||
func ExtensionIcon(name string) string {
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
switch ext {
|
||||
case ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico":
|
||||
return "image"
|
||||
case ".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm":
|
||||
return "video"
|
||||
case ".mp3", ".wav", ".ogg", ".flac", ".aac", ".wma":
|
||||
return "audio"
|
||||
case ".zip", ".tar", ".gz", ".bz2", ".xz", ".rar", ".7z":
|
||||
return "archive"
|
||||
case ".pdf":
|
||||
return "pdf"
|
||||
case ".doc", ".docx", ".odt", ".rtf":
|
||||
return "word"
|
||||
case ".xls", ".xlsx", ".csv", ".ods":
|
||||
return "spreadsheet"
|
||||
case ".txt", ".md", ".log", ".cfg", ".conf", ".ini", ".yaml", ".yml", ".toml", ".json", ".xml":
|
||||
return "text"
|
||||
default:
|
||||
return "file"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user