mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 16:40:32 +00:00
184 lines
4.6 KiB
Go
184 lines
4.6 KiB
Go
package download
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/mholt/archives"
|
|
)
|
|
|
|
// supportedArchiveExtensions contains file extensions supported by mholt/archives library
|
|
var supportedArchiveExtensions = []string{
|
|
// Archive formats
|
|
".zip",
|
|
".tar",
|
|
".tar.gz", ".tgz",
|
|
".tar.bz2", ".tbz2",
|
|
".tar.xz", ".txz",
|
|
".tar.lz4", ".tlz4",
|
|
".tar.sz", ".tsz",
|
|
".tar.zst", ".tzst",
|
|
".rar",
|
|
".7z",
|
|
// Compression formats
|
|
".gz",
|
|
".bz2",
|
|
".xz",
|
|
".lz4",
|
|
".sz",
|
|
".zst",
|
|
".br",
|
|
".lz",
|
|
}
|
|
|
|
// isArchiveFile checks if a file is a supported archive format
|
|
func isArchiveFile(filename string) bool {
|
|
lowerName := strings.ToLower(filename)
|
|
return slices.ContainsFunc(supportedArchiveExtensions, func(ext string) bool {
|
|
return strings.HasSuffix(lowerName, ext)
|
|
})
|
|
}
|
|
|
|
// extractArchive extracts an archive file to a destination directory
|
|
func extractArchive(archivePath string, destDir string, password string) error {
|
|
// Open the archive file
|
|
file, err := os.Open(archivePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
// Get file info for format identification
|
|
stat, err := file.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Identify the archive format
|
|
format, input, err := archives.Identify(context.Background(), archivePath, file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create destination directory if it doesn't exist
|
|
if err := os.MkdirAll(destDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Handle password-protected archives
|
|
if password != "" {
|
|
// Try to set password for formats that support it
|
|
if rar, ok := format.(archives.Rar); ok {
|
|
rar.Password = password
|
|
format = rar
|
|
}
|
|
if sz, ok := format.(archives.SevenZip); ok {
|
|
sz.Password = password
|
|
format = sz
|
|
}
|
|
// Note: Zip format in archives doesn't have a Password field
|
|
// Password-protected zips are not fully supported
|
|
}
|
|
|
|
// Handle extraction based on format type
|
|
switch f := format.(type) {
|
|
case archives.Extractor:
|
|
// For archive formats (zip, rar, 7z, tar, etc.)
|
|
return f.Extract(context.Background(), input, func(ctx context.Context, fileInfo archives.FileInfo) error {
|
|
return extractFile(ctx, fileInfo, destDir)
|
|
})
|
|
case archives.Decompressor:
|
|
// For single-file compression formats (gz, bz2, xz, etc.)
|
|
// Decompress to a file without the compression extension
|
|
baseName := filepath.Base(archivePath)
|
|
lowerBaseName := strings.ToLower(baseName)
|
|
for _, ext := range supportedArchiveExtensions {
|
|
if strings.HasSuffix(lowerBaseName, ext) {
|
|
// Get the actual suffix from the original filename (preserving case)
|
|
actualSuffix := baseName[len(baseName)-len(ext):]
|
|
baseName = strings.TrimSuffix(baseName, actualSuffix)
|
|
break
|
|
}
|
|
}
|
|
destPath := filepath.Join(destDir, baseName)
|
|
|
|
reader, err := f.OpenReader(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer reader.Close()
|
|
|
|
destFile, err := os.Create(destPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer destFile.Close()
|
|
|
|
_, err = io.Copy(destFile, reader)
|
|
return err
|
|
case archives.Archiver:
|
|
// This format is an archiver, try to extract using the extractor interface
|
|
if ext, ok := format.(archives.Extractor); ok {
|
|
// Reset file position
|
|
if seeker, ok := input.(io.Seeker); ok {
|
|
seeker.Seek(0, io.SeekStart)
|
|
}
|
|
return ext.Extract(context.Background(), io.NewSectionReader(file, 0, stat.Size()), func(ctx context.Context, fileInfo archives.FileInfo) error {
|
|
return extractFile(ctx, fileInfo, destDir)
|
|
})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractFile handles extracting a single file from an archive
|
|
func extractFile(ctx context.Context, fileInfo archives.FileInfo, destDir string) error {
|
|
// Skip directories, they will be created when extracting files
|
|
if fileInfo.IsDir() {
|
|
destPath := filepath.Join(destDir, fileInfo.NameInArchive)
|
|
return os.MkdirAll(destPath, fileInfo.Mode())
|
|
}
|
|
|
|
// Sanitize the path to prevent path traversal attacks
|
|
cleanPath := filepath.Clean(fileInfo.NameInArchive)
|
|
if strings.HasPrefix(cleanPath, "..") || filepath.IsAbs(cleanPath) {
|
|
// Skip files with suspicious paths
|
|
return nil
|
|
}
|
|
|
|
destPath := filepath.Join(destDir, cleanPath)
|
|
|
|
// Create parent directories
|
|
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Open the file from the archive
|
|
reader, err := fileInfo.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer reader.Close()
|
|
|
|
// Create the destination file
|
|
destFile, err := os.Create(destPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer destFile.Close()
|
|
|
|
// Copy the contents
|
|
_, err = io.Copy(destFile, reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set file permissions
|
|
return os.Chmod(destPath, fileInfo.Mode())
|
|
}
|