Files

224 lines
6.1 KiB
Go
Raw Permalink 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"
)
var (
// ErrNotExist means the folder or file is missing.
// Callers should use IsNotExist rather than comparing text.
ErrNotExist = errors.New("not found")
// ErrForbidden means the caller may not read or write the path.
// Callers should use IsForbidden rather than comparing text.
ErrForbidden = errors.New("forbidden")
// ErrNotDir means the path exists and is not a directory.
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 returns first-level names in the Working Folder.
List() ([]Entry, error)
// Read returns the contents of a basename in the Working Folder.
Read(name string) ([]byte, error)
// Write creates or replaces a basename in the Working Folder.
Write(name string, data []byte) error
// Exists reports whether name is a non-directory file in the Working Folder.
Exists(name string) (bool, error)
// Remove deletes a basename in the Working Folder.
Remove(name string) 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.
// It returns an error wrapping ErrNotExist, ErrForbidden, or ErrNotDir,
// or a plain error when rel is not a valid User Files Root–relative path.
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 and its parents if missing, then opens it.
// It returns an error wrapping ErrForbidden when the process cannot create
// the directory, or a plain error when rel is not valid.
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.
// The result is empty when rel is empty, contains "..", is a URL, or is a
// bare numeric Nextcloud file ID. An empty result is not a usable path.
// Backslashes are treated as slashes. A leading or trailing slash is removed.
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}
}
// List implements Folder for a local directory.
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
}
// Read implements Folder for a local directory.
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))
}
// Write implements Folder for a local directory.
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)
}
// Exists implements Folder for a local directory.
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
}
// Remove implements Folder for a local directory.
func (f localFolder) Remove(name string) error {
if err := RequireBasename(name); err != nil {
return err
}
err := os.Remove(filepath.Join(f.dir, name))
if err != nil && os.IsNotExist(err) {
return fmt.Errorf("%w: %s", ErrNotExist, name)
}
return err
}
// 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
}