mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-10 02:20:28 +00:00
feat: show extraction progress (#1217)
This commit is contained in:
1 parent
dbe37befa3
commit
b0e133b554
27 files changed
+2591
-40
No files matched your search
@@ -45,6 +45,20 @@ var (
|
||||
|
||||
type Listener func(event *Event)
|
||||
|
||||
// ExtractStatus represents the current status of archive extraction
|
||||
type ExtractStatus string
|
||||
|
||||
const (
|
||||
// ExtractStatusNone indicates extraction has not started
|
||||
ExtractStatusNone ExtractStatus = ""
|
||||
// ExtractStatusExtracting indicates extraction is in progress
|
||||
ExtractStatusExtracting ExtractStatus = "extracting"
|
||||
// ExtractStatusDone indicates extraction completed successfully
|
||||
ExtractStatusDone ExtractStatus = "done"
|
||||
// ExtractStatusError indicates extraction failed
|
||||
ExtractStatusError ExtractStatus = "error"
|
||||
)
|
||||
|
||||
type Progress struct {
|
||||
// Total download time(ns)
|
||||
Used int64 `json:"used"`
|
||||
@@ -56,6 +70,10 @@ type Progress struct {
|
||||
UploadSpeed int64 `json:"uploadSpeed"`
|
||||
// Uploaded size(bytes)
|
||||
Uploaded int64 `json:"uploaded"`
|
||||
// ExtractStatus indicates the current status of archive extraction
|
||||
ExtractStatus ExtractStatus `json:"extractStatus"`
|
||||
// ExtractProgress is the percentage of extraction completed (0-100)
|
||||
ExtractProgress int `json:"extractProgress"`
|
||||
}
|
||||
|
||||
type Downloader struct {
|
||||
@@ -881,13 +899,29 @@ func (d *Downloader) watch(task *Task) {
|
||||
// Auto-extract archive files
|
||||
if e.AutoExtract && isArchiveFile(downloadFilePath) {
|
||||
go func() {
|
||||
// Set extraction status to extracting
|
||||
task.Progress.ExtractStatus = ExtractStatusExtracting
|
||||
task.Progress.ExtractProgress = 0
|
||||
d.emit(EventKeyProgress, task)
|
||||
d.storage.Put(bucketTask, task.ID, task.clone())
|
||||
|
||||
// Extract to the same directory as the downloaded file
|
||||
destDir := task.Meta.Opts.Path
|
||||
extractErr := extractArchive(downloadFilePath, destDir, e.ArchivePassword)
|
||||
extractErr := extractArchive(downloadFilePath, destDir, e.ArchivePassword, func(extractedFiles int, totalFiles int, progress int) {
|
||||
task.Progress.ExtractProgress = progress
|
||||
d.emit(EventKeyProgress, task)
|
||||
})
|
||||
if extractErr != nil {
|
||||
d.Logger.Error().Err(extractErr).Msgf("auto extract archive failed, task id: %s", task.ID)
|
||||
task.Progress.ExtractStatus = ExtractStatusError
|
||||
d.emit(EventKeyProgress, task)
|
||||
d.storage.Put(bucketTask, task.ID, task.clone())
|
||||
} else {
|
||||
d.Logger.Info().Msgf("auto extract archive completed, task id: %s", task.ID)
|
||||
task.Progress.ExtractStatus = ExtractStatusDone
|
||||
task.Progress.ExtractProgress = 100
|
||||
d.emit(EventKeyProgress, task)
|
||||
d.storage.Put(bucketTask, task.ID, task.clone())
|
||||
// Delete archive after successful extraction if enabled
|
||||
if e.DeleteAfterExtract {
|
||||
deleteErr := os.Remove(downloadFilePath)
|
||||
|
||||
+1070
-3
File diff suppressed because it is too large.
Load diff
+119
-27
@@ -3,10 +3,12 @@ package download
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/mholt/archives"
|
||||
)
|
||||
@@ -35,6 +37,9 @@ var supportedArchiveExtensions = []string{
|
||||
".lz",
|
||||
}
|
||||
|
||||
// ExtractProgressCallback is called to report extraction progress
|
||||
type ExtractProgressCallback func(extractedFiles int, totalFiles int, progress int)
|
||||
|
||||
// isArchiveFile checks if a file is a supported archive format
|
||||
func isArchiveFile(filename string) bool {
|
||||
lowerName := strings.ToLower(filename)
|
||||
@@ -43,35 +48,35 @@ func isArchiveFile(filename string) bool {
|
||||
})
|
||||
}
|
||||
|
||||
// extractArchive extracts an archive file to a destination directory
|
||||
func extractArchive(archivePath string, destDir string, password string) error {
|
||||
// Open the archive file
|
||||
// archiveInfo holds information about an opened archive
|
||||
type archiveInfo struct {
|
||||
file *os.File
|
||||
stat os.FileInfo
|
||||
format archives.Format
|
||||
input io.Reader
|
||||
}
|
||||
|
||||
// openArchive opens an archive file and identifies its format
|
||||
func openArchive(archivePath string, password string) (*archiveInfo, error) {
|
||||
file, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Get file info for format identification
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
file.Close()
|
||||
return nil, 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
|
||||
file.Close()
|
||||
return nil, 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
|
||||
@@ -80,17 +85,57 @@ func extractArchive(archivePath string, destDir string, password string) error {
|
||||
sz.Password = password
|
||||
format = sz
|
||||
}
|
||||
// Note: Zip format in archives doesn't have a Password field
|
||||
// Password-protected zips are not fully supported
|
||||
}
|
||||
|
||||
return &archiveInfo{
|
||||
file: file,
|
||||
stat: stat,
|
||||
format: format,
|
||||
input: input,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createExtractionHandler creates a handler function for extracting files with progress tracking
|
||||
func createExtractionHandler(destDir string, totalFiles int, progressCallback ExtractProgressCallback) func(ctx context.Context, fileInfo archives.FileInfo) error {
|
||||
var extractedFiles atomic.Int32
|
||||
return func(ctx context.Context, fileInfo archives.FileInfo) error {
|
||||
err := extractFile(ctx, fileInfo, destDir)
|
||||
if err == nil && !fileInfo.IsDir() {
|
||||
extracted := int(extractedFiles.Add(1))
|
||||
if progressCallback != nil && totalFiles > 0 {
|
||||
progress := int(math.Min(float64((extracted*100)/totalFiles), 100))
|
||||
progressCallback(extracted, totalFiles, progress)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// extractArchive extracts an archive file to a destination directory
|
||||
func extractArchive(archivePath string, destDir string, password string, progressCallback ExtractProgressCallback) error {
|
||||
// Open the archive file
|
||||
info, err := openArchive(archivePath, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer info.file.Close()
|
||||
|
||||
// Create destination directory if it doesn't exist
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle extraction based on format type
|
||||
switch f := format.(type) {
|
||||
switch f := info.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)
|
||||
})
|
||||
// First, count total files for progress tracking
|
||||
totalFiles, err := countArchiveFiles(archivePath, password)
|
||||
if err != nil {
|
||||
// If counting fails, proceed without progress reporting
|
||||
totalFiles = 0
|
||||
}
|
||||
return f.Extract(context.Background(), info.input, createExtractionHandler(destDir, totalFiles, progressCallback))
|
||||
case archives.Decompressor:
|
||||
// For single-file compression formats (gz, bz2, xz, etc.)
|
||||
// Decompress to a file without the compression extension
|
||||
@@ -106,7 +151,7 @@ func extractArchive(archivePath string, destDir string, password string) error {
|
||||
}
|
||||
destPath := filepath.Join(destDir, baseName)
|
||||
|
||||
reader, err := f.OpenReader(input)
|
||||
reader, err := f.OpenReader(info.input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -118,24 +163,71 @@ func extractArchive(archivePath string, destDir string, password string) error {
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// Report progress at start and end for decompression
|
||||
if progressCallback != nil {
|
||||
progressCallback(0, 1, 0)
|
||||
}
|
||||
_, err = io.Copy(destFile, reader)
|
||||
if err == nil && progressCallback != nil {
|
||||
progressCallback(1, 1, 100)
|
||||
}
|
||||
return err
|
||||
case archives.Archiver:
|
||||
// This format is an archiver, try to extract using the extractor interface
|
||||
if ext, ok := format.(archives.Extractor); ok {
|
||||
if ext, ok := info.format.(archives.Extractor); ok {
|
||||
// Reset file position
|
||||
if seeker, ok := input.(io.Seeker); ok {
|
||||
if seeker, ok := info.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)
|
||||
})
|
||||
// Count total files for progress tracking
|
||||
totalFiles, err := countArchiveFiles(archivePath, password)
|
||||
if err != nil {
|
||||
totalFiles = 0
|
||||
}
|
||||
return ext.Extract(context.Background(), io.NewSectionReader(info.file, 0, info.stat.Size()), createExtractionHandler(destDir, totalFiles, progressCallback))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createCountingHandler creates a handler function for counting files in an archive
|
||||
func createCountingHandler(count *int) func(ctx context.Context, fileInfo archives.FileInfo) error {
|
||||
return func(ctx context.Context, fileInfo archives.FileInfo) error {
|
||||
if !fileInfo.IsDir() {
|
||||
*count++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// countArchiveFiles counts the number of files in an archive for progress calculation
|
||||
func countArchiveFiles(archivePath string, password string) (int, error) {
|
||||
info, err := openArchive(archivePath, password)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer info.file.Close()
|
||||
|
||||
count := 0
|
||||
switch f := info.format.(type) {
|
||||
case archives.Extractor:
|
||||
err = f.Extract(context.Background(), info.input, createCountingHandler(&count))
|
||||
case archives.Archiver:
|
||||
if ext, ok := info.format.(archives.Extractor); ok {
|
||||
if seeker, ok := info.input.(io.Seeker); ok {
|
||||
seeker.Seek(0, io.SeekStart)
|
||||
}
|
||||
err = ext.Extract(context.Background(), io.NewSectionReader(info.file, 0, info.stat.Size()), createCountingHandler(&count))
|
||||
}
|
||||
case archives.Decompressor:
|
||||
// Single file compression, count as 1
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
+1168
-2
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user