mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-05 16:15:43 +00:00
192 lines
4.1 KiB
Go
192 lines
4.1 KiB
Go
package download
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/mholt/archives"
|
|
)
|
|
|
|
// extractZipMultiPart extracts a multi-part ZIP archive (.zip.001, .zip.002, etc.)
|
|
// These are created by simply splitting a ZIP file into chunks, so we concatenate them
|
|
func extractZipMultiPart(firstPartPath string, destDir string, password string, progressCallback ExtractProgressCallback) error {
|
|
// Find all parts
|
|
parts, err := findZipMultiParts(firstPartPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a multi-part reader that reads across all parts
|
|
multiReader := newMultiPartFileReader(parts)
|
|
defer multiReader.Close()
|
|
|
|
// Get total size
|
|
totalSize := multiReader.Size()
|
|
|
|
// Create destination directory
|
|
if err := os.MkdirAll(destDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
// First pass: count files for progress
|
|
totalFiles := 0
|
|
zip := newZipFormat()
|
|
err = zip.Extract(context.Background(), io.NewSectionReader(multiReader, 0, totalSize), func(ctx context.Context, fileInfo archives.FileInfo) error {
|
|
if !fileInfo.IsDir() {
|
|
totalFiles++
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
// If counting fails, proceed without progress
|
|
totalFiles = 0
|
|
}
|
|
|
|
// Reset reader for actual extraction
|
|
multiReader.Close()
|
|
multiReader = newMultiPartFileReader(parts)
|
|
defer multiReader.Close()
|
|
|
|
// Second pass: extract with progress tracking
|
|
return zip.Extract(context.Background(), io.NewSectionReader(multiReader, 0, totalSize), createExtractionHandler(destDir, totalFiles, progressCallback))
|
|
}
|
|
|
|
// findZipMultiParts finds all parts of a multi-part ZIP archive in order
|
|
func findZipMultiParts(firstPartPath string) ([]string, error) {
|
|
// Extract base name (e.g., "Archive.zip" from "Archive.zip.001")
|
|
dir := filepath.Dir(firstPartPath)
|
|
baseName := filepath.Base(firstPartPath)
|
|
|
|
// Remove the .001 suffix to get the base
|
|
if idx := strings.LastIndex(baseName, "."); idx > 0 {
|
|
baseName = baseName[:idx] // "Archive.zip"
|
|
}
|
|
|
|
var parts []string
|
|
partNum := 1
|
|
|
|
for {
|
|
partPath := filepath.Join(dir, baseName+fmt.Sprintf(".%03d", partNum))
|
|
if _, err := os.Stat(partPath); os.IsNotExist(err) {
|
|
break
|
|
}
|
|
parts = append(parts, partPath)
|
|
partNum++
|
|
}
|
|
|
|
if len(parts) == 0 {
|
|
return nil, fmt.Errorf("no parts found for %s", firstPartPath)
|
|
}
|
|
|
|
return parts, nil
|
|
}
|
|
|
|
// multiPartFileReader provides io.ReaderAt over multiple files concatenated
|
|
type multiPartFileReader struct {
|
|
parts []string
|
|
files []*os.File
|
|
sizes []int64
|
|
offsets []int64 // cumulative offsets for each file
|
|
totalSize int64
|
|
}
|
|
|
|
func newMultiPartFileReader(parts []string) *multiPartFileReader {
|
|
return &multiPartFileReader{parts: parts}
|
|
}
|
|
|
|
func (m *multiPartFileReader) init() error {
|
|
if m.files != nil {
|
|
return nil
|
|
}
|
|
|
|
m.files = make([]*os.File, len(m.parts))
|
|
m.sizes = make([]int64, len(m.parts))
|
|
m.offsets = make([]int64, len(m.parts))
|
|
|
|
var offset int64
|
|
for i, part := range m.parts {
|
|
f, err := os.Open(part)
|
|
if err != nil {
|
|
m.Close()
|
|
return err
|
|
}
|
|
stat, err := f.Stat()
|
|
if err != nil {
|
|
f.Close()
|
|
m.Close()
|
|
return err
|
|
}
|
|
m.files[i] = f
|
|
m.sizes[i] = stat.Size()
|
|
m.offsets[i] = offset
|
|
offset += stat.Size()
|
|
}
|
|
m.totalSize = offset
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *multiPartFileReader) Size() int64 {
|
|
if err := m.init(); err != nil {
|
|
return 0
|
|
}
|
|
return m.totalSize
|
|
}
|
|
|
|
func (m *multiPartFileReader) ReadAt(p []byte, off int64) (n int, err error) {
|
|
if err := m.init(); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if off >= m.totalSize {
|
|
return 0, io.EOF
|
|
}
|
|
|
|
// Find which file(s) to read from
|
|
for i, fileOffset := range m.offsets {
|
|
fileEnd := fileOffset + m.sizes[i]
|
|
|
|
if off >= fileEnd {
|
|
continue
|
|
}
|
|
|
|
// Read from this file
|
|
localOffset := off - fileOffset
|
|
toRead := len(p) - n
|
|
if int64(toRead) > fileEnd-off {
|
|
toRead = int(fileEnd - off)
|
|
}
|
|
|
|
read, err := m.files[i].ReadAt(p[n:n+toRead], localOffset)
|
|
n += read
|
|
off += int64(read)
|
|
|
|
if err != nil && err != io.EOF {
|
|
return n, err
|
|
}
|
|
|
|
if n >= len(p) {
|
|
return n, nil
|
|
}
|
|
}
|
|
|
|
if n == 0 {
|
|
return 0, io.EOF
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func (m *multiPartFileReader) Close() error {
|
|
for _, f := range m.files {
|
|
if f != nil {
|
|
f.Close()
|
|
}
|
|
}
|
|
m.files = nil
|
|
return nil
|
|
}
|