Files
SaveAny-Bot/storage/telegram/split.go
krau eda0756f0c feat: add import command and batch import functionality
- Implemented the `/import` command to allow users to import files from storage to Telegram.
- Added support for listing files in storage and filtering based on regex patterns.
- Created a batch import task to handle multiple file uploads concurrently.
- Introduced progress tracking for batch imports, providing real-time updates to users.
- Enhanced storage interfaces to support file listing and reading capabilities.
- Updated localization files for the new import command and its usage instructions.
- Added utility functions for file size formatting and speed calculation.
- Refactored Telegram storage handling to support reading from non-seekable streams.
2026-01-17 18:59:09 +08:00

142 lines
3.1 KiB
Go

package telegram
import (
"archive/zip"
"context"
"fmt"
"io"
"os"
"path/filepath"
"time"
)
type splitWriter struct {
baseName string
partSize int64
currentPart int
currentSize int64
currentFile *os.File
totalParts int
}
func newSplitWriter(baseName string, partSize int64) *splitWriter {
return &splitWriter{
baseName: baseName,
partSize: partSize,
currentPart: 0,
}
}
// Write implements io.Writer interface
func (w *splitWriter) Write(p []byte) (n int, err error) {
written := 0
for written < len(p) {
if w.currentFile == nil || w.currentSize >= w.partSize {
if err := w.nextPart(); err != nil {
return written, err
}
}
toWrite := int64(len(p) - written)
remaining := w.partSize - w.currentSize
if toWrite > remaining {
toWrite = remaining
}
nw, err := w.currentFile.Write(p[written : written+int(toWrite)])
written += nw
w.currentSize += int64(nw)
if err != nil {
return written, err
}
}
return written, nil
}
func (w *splitWriter) Close() error {
if w.currentFile != nil {
return w.currentFile.Close()
}
return nil
}
func (w *splitWriter) nextPart() error {
if w.currentFile != nil {
if err := w.currentFile.Close(); err != nil {
return err
}
}
partName := w.partName(w.currentPart)
file, err := os.Create(partName)
if err != nil {
return err
}
w.currentFile = file
w.currentSize = 0
w.currentPart++
return nil
}
func (w *splitWriter) partName(partNum int) string {
// file.zip.001, file.zip.002, ...
return fmt.Sprintf("%s.zip.%03d", w.baseName, partNum+1)
}
func (w *splitWriter) finalize() error {
w.totalParts = w.currentPart
// 如果只有一个分卷,直接重命名为 .zip
if w.totalParts == 1 {
oldName := fmt.Sprintf("%s.zip.001", w.baseName)
newName := fmt.Sprintf("%s.zip", w.baseName)
return os.Rename(oldName, newName)
}
return nil
}
func CreateSplitZip(ctx context.Context, reader io.Reader, size int64, fileName, outputBase string, partSize int64) error {
outputDir := filepath.Dir(outputBase)
if err := os.MkdirAll(outputDir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
splitWriter := newSplitWriter(outputBase, partSize)
defer splitWriter.Close()
zipWriter := zip.NewWriter(splitWriter)
defer zipWriter.Close()
header := &zip.FileHeader{
Name: fileName,
Method: zip.Store, // just store without compression
Modified: time.Now(),
}
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return fmt.Errorf("failed to create zip header: %w", err)
}
copied, err := io.Copy(writer, reader)
if err != nil {
return fmt.Errorf("failed to write data: %w", err)
}
if copied != size {
return fmt.Errorf("incomplete write: expected %d bytes, got %d bytes", size, copied)
}
if err := zipWriter.Close(); err != nil {
return fmt.Errorf("failed to close zip writer: %w", err)
}
if err := splitWriter.Close(); err != nil {
return fmt.Errorf("failed to close split writer: %w", err)
}
if err := splitWriter.finalize(); err != nil {
return fmt.Errorf("failed to rename split files: %w", err)
}
return nil
}