235 lines
6.3 KiB
Go
235 lines
6.3 KiB
Go
package downloader
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
)
|
|
|
|
func ensureTransferLayout(downloadsRoot, requestID string) (TaskPaths, error) {
|
|
paths, err := DeriveTaskPaths(downloadsRoot, requestID)
|
|
if err != nil {
|
|
return TaskPaths{}, err
|
|
}
|
|
if err := os.MkdirAll(paths.Root, 0o700); err != nil {
|
|
return TaskPaths{}, fmt.Errorf("create downloads root: %w", err)
|
|
}
|
|
for _, directory := range []string{paths.Root, paths.TasksDir, paths.FilesDir} {
|
|
if err := os.Mkdir(directory, 0o700); err != nil && !os.IsExist(err) {
|
|
return TaskPaths{}, fmt.Errorf("create download directory: %w", err)
|
|
}
|
|
info, err := os.Lstat(directory)
|
|
if err != nil {
|
|
return TaskPaths{}, fmt.Errorf("inspect download directory: %w", err)
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return TaskPaths{}, fmt.Errorf("%w: managed directory is unsafe", ErrTaskCorrupt)
|
|
}
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
func regularFileSize(filePath string) (size int64, exists bool, err error) {
|
|
info, err := os.Lstat(filePath)
|
|
if os.IsNotExist(err) {
|
|
return 0, false, nil
|
|
}
|
|
if err != nil {
|
|
return 0, false, fmt.Errorf("inspect download file: %w", err)
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
|
return 0, false, fmt.Errorf("%w: managed file is not regular", ErrTaskCorrupt)
|
|
}
|
|
return info.Size(), true, nil
|
|
}
|
|
|
|
func regularFileExists(filePath string) (bool, error) {
|
|
_, exists, err := regularFileSize(filePath)
|
|
return exists, err
|
|
}
|
|
|
|
func truncatePart(filePath string) error {
|
|
file, _, err := openVerifiedRegular(filePath, os.O_RDWR)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := file.Truncate(0); err != nil {
|
|
file.Close()
|
|
return fmt.Errorf("truncate part: %w", err)
|
|
}
|
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
file.Close()
|
|
return fmt.Errorf("seek truncated part: %w", err)
|
|
}
|
|
if err := file.Sync(); err != nil {
|
|
file.Close()
|
|
return fmt.Errorf("sync truncated part: %w", err)
|
|
}
|
|
return file.Close()
|
|
}
|
|
|
|
func openPart(paths TaskPaths, offset int64, restart bool) (*os.File, error) {
|
|
if restart {
|
|
if _, exists, err := regularFileSize(paths.Part); err != nil {
|
|
return nil, err
|
|
} else if exists {
|
|
file, _, err := openVerifiedRegular(paths.Part, os.O_RDWR)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.Truncate(0); err != nil {
|
|
file.Close()
|
|
return nil, fmt.Errorf("truncate restarted part: %w", err)
|
|
}
|
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
file.Close()
|
|
return nil, fmt.Errorf("seek restarted part: %w", err)
|
|
}
|
|
return file, nil
|
|
}
|
|
return os.OpenFile(paths.Part, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
|
|
}
|
|
size, exists, err := regularFileSize(paths.Part)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exists {
|
|
if offset != 0 {
|
|
return nil, fmt.Errorf("%w: missing part for non-zero offset", ErrTaskCorrupt)
|
|
}
|
|
return os.OpenFile(paths.Part, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
|
|
}
|
|
if size != offset {
|
|
return nil, fmt.Errorf("%w: part size changed", ErrTaskCorrupt)
|
|
}
|
|
file, _, err := openVerifiedRegular(paths.Part, os.O_WRONLY)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := file.Seek(offset, io.SeekStart); err != nil {
|
|
file.Close()
|
|
return nil, fmt.Errorf("seek part: %w", err)
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
func syncRegularFile(filePath string) error {
|
|
file, _, err := openVerifiedRegular(filePath, os.O_RDWR)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := file.Sync(); err != nil {
|
|
file.Close()
|
|
return err
|
|
}
|
|
return file.Close()
|
|
}
|
|
|
|
func activateCompleted(paths TaskPaths, expected os.FileInfo) error {
|
|
if exists, err := regularFileExists(paths.Completed); err != nil {
|
|
return err
|
|
} else if exists {
|
|
return fmt.Errorf("%w: completed path already exists", ErrTaskCorrupt)
|
|
}
|
|
if expected == nil {
|
|
part, partInfo, err := openVerifiedRegular(paths.Part, os.O_RDONLY)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := part.Close(); err != nil {
|
|
return fmt.Errorf("close part before activation: %w", err)
|
|
}
|
|
expected = partInfo
|
|
}
|
|
if err := verifyRegularIdentity(paths.Part, expected); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(paths.Part, paths.Completed); err != nil {
|
|
return fmt.Errorf("activate completed download: %w", err)
|
|
}
|
|
completedInfo, err := os.Lstat(paths.Completed)
|
|
if err != nil {
|
|
return fmt.Errorf("inspect activated download: %w", err)
|
|
}
|
|
if completedInfo.Mode()&os.ModeSymlink != 0 ||
|
|
!completedInfo.Mode().IsRegular() ||
|
|
!os.SameFile(expected, completedInfo) {
|
|
_ = removeExactRegularFile(paths.Completed)
|
|
return fmt.Errorf("%w: activated file identity changed", ErrTaskCorrupt)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func verifyRegularIdentity(filePath string, expected os.FileInfo) error {
|
|
current, err := os.Lstat(filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("inspect managed file identity: %w", err)
|
|
}
|
|
if current.Mode()&os.ModeSymlink != 0 ||
|
|
!current.Mode().IsRegular() ||
|
|
!os.SameFile(expected, current) {
|
|
return fmt.Errorf("%w: managed file identity changed", ErrTaskCorrupt)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func removeExactRegularFile(filePath string) error {
|
|
info, err := os.Lstat(filePath)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
|
return fmt.Errorf("%w: refuse removal of non-regular file", ErrTaskCorrupt)
|
|
}
|
|
return os.Remove(filePath)
|
|
}
|
|
|
|
func syncAndClose(file *os.File) error {
|
|
if file == nil {
|
|
return nil
|
|
}
|
|
if err := file.Sync(); err != nil {
|
|
file.Close()
|
|
return err
|
|
}
|
|
return file.Close()
|
|
}
|
|
|
|
func openVerifiedRegular(
|
|
filePath string,
|
|
flags int,
|
|
) (*os.File, os.FileInfo, error) {
|
|
before, err := os.Lstat(filePath)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if before.Mode()&os.ModeSymlink != 0 || !before.Mode().IsRegular() {
|
|
return nil, nil, fmt.Errorf("%w: managed file is not regular", ErrTaskCorrupt)
|
|
}
|
|
file, err := os.OpenFile(filePath, flags, 0o600)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("open managed file: %w", err)
|
|
}
|
|
opened, err := file.Stat()
|
|
if err != nil {
|
|
file.Close()
|
|
return nil, nil, fmt.Errorf("stat managed file: %w", err)
|
|
}
|
|
after, err := os.Lstat(filePath)
|
|
if err != nil {
|
|
file.Close()
|
|
return nil, nil, fmt.Errorf("restat managed file: %w", err)
|
|
}
|
|
if after.Mode()&os.ModeSymlink != 0 ||
|
|
!after.Mode().IsRegular() ||
|
|
!os.SameFile(before, opened) ||
|
|
!os.SameFile(opened, after) {
|
|
file.Close()
|
|
return nil, nil, fmt.Errorf("%w: managed file changed while opening", ErrTaskCorrupt)
|
|
}
|
|
return file, opened, nil
|
|
}
|