repo_description:   A fast, modern download manager for HTTP, BitTorrent, Magnet, and ed2k. Cross-platform, built with Golang and Flutter.
repo_topics: android bittorrent cross-platform debian downloader flutter golang http https ios macos magnet torrent ubuntu windows
code: 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
  }
  