Initial DDC release
This commit is contained in:
commit
434f6441da
57 files changed
+4606
No files matched your search
Executable
+101
@@ -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
|
||||
}
|
||||
Executable
+99
@@ -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 == '\\'
|
||||
}
|
||||
Executable
+52
@@ -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)
|
||||
}
|
||||
Executable
+85
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user