Files
go-nc-files/files.go
T
Konrad NeitzelandCursor 9068d8ba8e Extract Files seam, WebDAV, and Visit from CheckDNS.
Publish go-nc-files v1 with local/WebDAV Root, DefaultPathStore, and
neutral Working Folder language; depends on go-nc-exapp v0.1.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 19:16:53 +02:00

192 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package goncfiles
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"strings"
)
// Sentinel errors for Visit / Working Folder resolution.
var (
ErrNotExist = errors.New("not found")
ErrForbidden = errors.New("forbidden")
ErrNotDir = errors.New("not a directory")
)
// Entry is a first-level name in a Working Folder.
type Entry struct {
Name string
IsDir bool
}
// Folder is flat storage for one Working Folder (basenames only).
type Folder interface {
List() ([]Entry, error)
Read(name string) ([]byte, error)
Write(name string, data []byte) error
Exists(name string) (bool, error)
}
// Root is User Files Root–relative folder access.
type Root interface {
// OpenFolder opens an existing folder at rel (User Files Root–relative).
OpenFolder(rel string) (Folder, error)
// EnsureFolder creates rel (and parents) if needed, then opens it.
EnsureFolder(rel string) (Folder, error)
}
// Local is a User Files Root on the local filesystem.
type Local struct {
RootDir string
}
// OpenFolder opens an existing directory under the local root.
func (l Local) OpenFolder(rel string) (Folder, error) {
dir, err := l.resolve(rel)
if err != nil {
return nil, err
}
info, err := os.Stat(dir)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("%w: %s", ErrNotExist, rel)
}
if os.IsPermission(err) {
return nil, fmt.Errorf("%w: %s", ErrForbidden, rel)
}
return nil, err
}
if !info.IsDir() {
return nil, fmt.Errorf("%w: %s", ErrNotDir, rel)
}
return localFolder{dir: dir}, nil
}
// EnsureFolder creates the directory if missing, then opens it.
func (l Local) EnsureFolder(rel string) (Folder, error) {
dir, err := l.resolve(rel)
if err != nil {
return nil, err
}
if err := os.MkdirAll(dir, 0o755); err != nil {
if os.IsPermission(err) {
return nil, fmt.Errorf("%w: %s", ErrForbidden, rel)
}
return nil, err
}
return localFolder{dir: dir}, nil
}
func (l Local) resolve(rel string) (string, error) {
clean := CleanRel(rel)
if clean == "" {
return "", fmt.Errorf("invalid Working Folder path")
}
return filepath.Join(l.RootDir, filepath.FromSlash(clean)), nil
}
// CleanRel normalizes a User Files Root–relative path; empty means invalid.
// Absolute URLs and numeric file IDs are rejected.
func CleanRel(rel string) string {
rel = strings.TrimSpace(rel)
rel = strings.ReplaceAll(rel, "\\", "/")
lower := strings.ToLower(rel)
if strings.Contains(rel, "://") || strings.HasPrefix(lower, "http:") || strings.HasPrefix(lower, "https:") {
return ""
}
rel = strings.Trim(rel, "/")
if rel == "" || strings.Contains(rel, "..") {
return ""
}
// Bare numeric Nextcloud file IDs are not Working Folder paths.
if isAllDigits(rel) {
return ""
}
clean := path.Clean(rel)
if clean == "." || strings.HasPrefix(clean, "..") {
return ""
}
return clean
}
func isAllDigits(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}
type localFolder struct {
dir string
}
// LocalFolder returns a Folder backed by an absolute local directory.
func LocalFolder(dir string) Folder {
return localFolder{dir: dir}
}
func (f localFolder) List() ([]Entry, error) {
entries, err := os.ReadDir(f.dir)
if err != nil {
return nil, err
}
out := make([]Entry, 0, len(entries))
for _, e := range entries {
out = append(out, Entry{Name: e.Name(), IsDir: e.IsDir()})
}
return out, nil
}
func (f localFolder) Read(name string) ([]byte, error) {
if err := RequireBasename(name); err != nil {
return nil, err
}
return os.ReadFile(filepath.Join(f.dir, name))
}
func (f localFolder) Write(name string, data []byte) error {
if err := RequireBasename(name); err != nil {
return err
}
return os.WriteFile(filepath.Join(f.dir, name), data, 0o644)
}
func (f localFolder) Exists(name string) (bool, error) {
if err := RequireBasename(name); err != nil {
return false, err
}
info, err := os.Stat(filepath.Join(f.dir, name))
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return !info.IsDir(), nil
}
// RequireBasename rejects path segments that are not a single basename.
func RequireBasename(name string) error {
if name == "" || name != path.Base(name) || strings.ContainsAny(name, `/\`) || strings.Contains(name, "..") {
return fmt.Errorf("name must be a basename in the Working Folder, not %q", name)
}
return nil
}
// LocalPath returns the absolute directory when f is a local folder.
func LocalPath(f Folder) (string, bool) {
lf, ok := f.(localFolder)
if !ok {
return "", false
}
return lf.dir, true
}