Initial DDC release

This commit is contained in:
SirBlob committed 2026-10-25 20:24:50 -04:00
commit 434f6441da
57 files changed
+4606

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
package files
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"syscall"
"ddc/internal/diskspace"
)
var ErrDestinationExists = errors.New("an item with this name already exists in the destination")
func MoveInto(source, destinationDir string, reservedBytes int64, progress func(int64)) error {
name := filepath.Base(source)
target := filepath.Join(destinationDir, name)
if exists(target) {
return fmt.Errorf("%w: %s", ErrDestinationExists, name)
}
size, err := TreeSize(source)
if err != nil {
return err
}
renameErr := os.Rename(source, target)
if renameErr == nil {
progress(size)
return nil
}
if !errors.Is(renameErr, syscall.EXDEV) {
return renameErr
}
if err := diskspace.EnsureFree(destinationDir, size, reservedBytes); err != nil {
return err
}
if err := copyTree(source, target, progress); err != nil {
os.RemoveAll(target)
return err
}
return os.RemoveAll(source)
}
func copyTree(source, target string, progress func(int64)) error {
return filepath.WalkDir(source, func(current string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
relative, err := filepath.Rel(source, current)
if err != nil {
return err
}
destination := filepath.Join(target, relative)
switch {
case entry.IsDir():
return os.MkdirAll(destination, 0o755)
case entry.Type().IsRegular():
return copyFile(current, destination, progress)
default:
return nil
}
})
}
func copyFile(source, destination string, progress func(int64)) error {
input, err := os.Open(source)
if err != nil {
return err
}
defer input.Close()
output, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
if err != nil {
return err
}
_, copyErr := io.Copy(output, &progressReader{reader: input, progress: progress})
closeErr := output.Close()
if copyErr != nil {
return copyErr
}
return closeErr
}
type progressReader struct {
reader io.Reader
progress func(int64)
}
func (reader *progressReader) Read(buffer []byte) (int, error) {
count, err := reader.reader.Read(buffer)
if count > 0 {
reader.progress(int64(count))
}
return count, err
}
+99
View File
@@ -0,0 +1,99 @@
package files
import (
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
)
const (
PartFileName = ".download.part"
maxSubfolderDepth = 4
maxNameBytes = 255
)
var ErrUnsafePath = errors.New("path is not allowed")
func ResolveInside(root, relative string) (string, error) {
cleaned := filepath.Clean(filepath.FromSlash(relative))
if relative == "" || cleaned == "." || !filepath.IsLocal(cleaned) {
return "", ErrUnsafePath
}
resolvedRoot, err := filepath.EvalSymlinks(root)
if err != nil {
return "", err
}
candidate := filepath.Join(resolvedRoot, cleaned)
info, err := os.Lstat(candidate)
if err != nil {
return "", err
}
if info.Mode()&fs.ModeSymlink != 0 {
return "", ErrUnsafePath
}
resolved, err := filepath.EvalSymlinks(candidate)
if err != nil {
return "", err
}
if !IsWithin(resolvedRoot, resolved) {
return "", ErrUnsafePath
}
return resolved, nil
}
func IsWithin(root, target string) bool {
relative, err := filepath.Rel(root, target)
return err == nil && relative != "." && filepath.IsLocal(relative)
}
func SanitizeSubfolder(raw string) (string, error) {
trimmed := strings.Trim(strings.TrimSpace(raw), "/")
if trimmed == "" {
return "", nil
}
parts := strings.Split(trimmed, "/")
if len(parts) > maxSubfolderDepth {
return "", ErrUnsafePath
}
for index, part := range parts {
part = strings.TrimSpace(part)
if part == "" || part == "." || part == ".." || len(part) > maxNameBytes || strings.ContainsFunc(part, isUnsafeRune) {
return "", ErrUnsafePath
}
parts[index] = part
}
return filepath.Join(parts...), nil
}
func EnsureSubdirectory(root, subfolder string) (string, error) {
current := root
if subfolder == "" {
return current, nil
}
for _, part := range strings.Split(subfolder, string(filepath.Separator)) {
current = filepath.Join(current, part)
info, err := os.Lstat(current)
switch {
case errors.Is(err, fs.ErrNotExist):
if err := os.Mkdir(current, 0o755); err != nil && !errors.Is(err, fs.ErrExist) {
return "", err
}
case err != nil:
return "", err
case !info.IsDir():
return "", ErrUnsafePath
}
}
return current, nil
}
func isUnsafeRune(character rune) bool {
return character < 0x20 || character == 0x7f || character == '\\'
}
+52
View File
@@ -0,0 +1,52 @@
package files
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
)
func TreeSize(root string) (int64, error) {
var total int64
err := filepath.WalkDir(root, func(_ string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if !entry.Type().IsRegular() {
return nil
}
info, err := entry.Info()
if err != nil {
return err
}
total += info.Size()
return nil
})
return total, err
}
func UniquePath(candidate string) string {
if !exists(candidate) {
return candidate
}
directory, name := filepath.Split(candidate)
extension := filepath.Ext(name)
base := strings.TrimSuffix(name, extension)
for index := 1; index < 1000; index++ {
next := filepath.Join(directory, fmt.Sprintf("%s (%d)%s", base, index, extension))
if !exists(next) {
return next
}
}
return filepath.Join(directory, fmt.Sprintf("%s-%d%s", base, time.Now().UnixNano(), extension))
}
func exists(path string) bool {
_, err := os.Lstat(path)
return !errors.Is(err, fs.ErrNotExist)
}
+85
View File
@@ -0,0 +1,85 @@
package files
import (
"os"
"path"
"path/filepath"
"sort"
"strings"
"ddc/internal/extract"
)
const (
maxTreeEntries = 20000
maxTreeDepth = 32
)
type Node struct {
Name string `json:"name"`
Path string `json:"path"`
IsDir bool `json:"isDir"`
IsArchive bool `json:"isArchive"`
Size int64 `json:"size"`
Children []Node `json:"children,omitempty"`
}
func BuildTree(root string) ([]Node, error) {
entryCount := 0
return readLevel(root, "", 0, &entryCount)
}
func readLevel(root, relative string, depth int, entryCount *int) ([]Node, error) {
entries, err := os.ReadDir(filepath.Join(root, relative))
if err != nil {
return nil, err
}
nodes := make([]Node, 0, len(entries))
for _, entry := range entries {
if *entryCount >= maxTreeEntries {
break
}
entryType := entry.Type()
if entry.Name() == PartFileName || (!entryType.IsDir() && !entryType.IsRegular()) {
continue
}
*entryCount++
node := Node{
Name: entry.Name(),
Path: path.Join(filepath.ToSlash(relative), entry.Name()),
IsDir: entry.IsDir(),
}
if node.IsDir {
if depth < maxTreeDepth {
children, err := readLevel(root, filepath.Join(relative, entry.Name()), depth+1, entryCount)
if err != nil {
return nil, err
}
node.Children = children
for _, child := range children {
node.Size += child.Size
}
}
} else {
info, err := entry.Info()
if err != nil {
continue
}
node.Size = info.Size()
node.IsArchive = extract.IsArchive(entry.Name())
}
nodes = append(nodes, node)
}
sort.SliceStable(nodes, func(first, second int) bool {
if nodes[first].IsDir != nodes[second].IsDir {
return nodes[first].IsDir
}
return strings.ToLower(nodes[first].Name) < strings.ToLower(nodes[second].Name)
})
return nodes, nil
}