1Panel-dev / 1Panel-dev/1Panel

[FEATURE]希望面对大文件压缩时支持分卷压缩

Open
#4,492 5 comments 1 reaction 2 assignees View on GitHub

@lan-yonghui is already working on this.

Since Apr 15, 2025.

type: feature-request
Dominant language
Go
Stars
37k
Forks
3.4k
Avg merge
9h 16m
Merged PRs (30d)
105

Description

1Panel 版本

v1.10.2

请描述您的需求或者改进建议

希望可以增加针对tar归档压缩的分卷压缩支持。

当有时需要对服务器内特定路径归档备份时,产生的单文件过大(比如我们就出现了接近600GB的tar文件),这么大的单文件不利于后期处理。

可以考虑在面板压缩界面支持自定义分卷压缩,比如100GB一个数据包。

请描述你建议的实现方案

实现只需要使用linux原生命令即可,目前我们是手动输入的,但感觉过于繁琐,建议在面板中集成。

分卷压缩命令:

tar cvzf - filename | split -b 100GB -d - split_file

合并命令:

cat split_file* > filename.tar

go语言的实现参考:(分卷压缩)

package main

import (
	"compress/gzip"
	"fmt"
	"io"
	"io/ioutil"
	"os"

	"github.com/klauspost/compress/tar"
)

const chunkSize = 100 * 1024 * 1024 * 1024 // 100 GB

func main() {
	filename := "your_filename_here" // 替换为实际要处理的文件名

	// 创建临时文件,存储 tar.gz 数据
	tempFile, err := ioutil.TempFile("", "split-tar-gz-*.tar.gz")
	if err != nil {
		panic(err)
	}
	defer os.Remove(tempFile.Name()) // 删除临时文件

	// 创建 gzip.Writer
	gw := gzip.NewWriter(tempFile)
	defer gw.Close()
	tw := tar.NewWriter(gw)
	defer tw.Close()

	// 添加filename 到 tar 归档中
	err = addFileToTar(tw, filename)
	if err != nil {
		panic(err)
	}

	// 关闭 tar.Writer
	err = tw.Close()
	if err != nil {
		panic(err)
	}

	// 完成压缩
	err = gw.Close()
	if err != nil {
		panic(err)
	}

	// 重新打开临时文件读取
	f, err := os.Open(tempFile.Name())
	if err != nil {
		panic(err)
	}
	defer f.Close()

	// 分割临时文件模拟:split -b 100GB -d - split_file
	splitFileBaseName := "split_file"
	splitFiles := splitFile(f, chunkSize, splitFileBaseName)
	for _, sf := range splitFiles {
		fmt.Printf("拆分块: %s\n", sf)
	}
}

// 将指定文件添加到tar
func addFileToTar(tw *tar.Writer, filename string) error {
	info, err := os.Stat(filename)
	if err != nil {
		return err
	}

	hdr := &tar.Header{
		Name:    filename,
		Size:    info.Size(),
		Mode:    0644,
		ModTime: info.ModTime(),
	}

	err = tw.WriteHeader(hdr)
	if err != nil {
		return err
	}

	file, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	_, err = io.Copy(tw, file)
	return err
}

//模拟split -b size -d - baseName,将输入文件分割块
func splitFile(r io.Reader, size int64, baseName string) []string {
	var splitFiles []string

	buf := make([]byte, size)
	for {
		n, err := r.Read(buf)
		if err == io.EOF {
			break
		} else if err != nil {
			panic(err)
		}

		splitFileNum := len(splitFiles) + 1
		splitFileName := fmt.Sprintf("%s%02d", baseName, splitFileNum)
		splitFile, err := os.Create(splitFileName)
		if err != nil {
			panic(err)
		}
		defer splitFile.Close()

		_, err = splitFile.Write(buf[:n])
		if err != nil {
			panic(err)
		}

		splitFiles = append(splitFiles, splitFileName)
	}

	return splitFiles
}

go语言的实现参考:(合并命令)

package main

import (
	"fmt"
	"io/ioutil"
	"os"
)

func main() {
	targetFile := "filename.tar"

	target, err := os.OpenFile(targetFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return
	}
	defer target.Close()

	// 遍历split_file前缀文件
	matches, err := filepath.Glob("split_file*")
	if err != nil {
		return
	}

	for _, splitFile := range matches {
		// 打开读取每个拆分块
		content, err := ioutil.ReadFile(splitFile)
		if err != nil {
			continue
		}

		if _, err := target.Write(content); err != nil {
			return
		}
	}
}

这里需要处理的一些技术细节是阶段指定大小文件精确到字节边界的拆分。

由于我们对go语言并不是很熟悉,实际中精准拆分是用python实现的,也可以参考:

def split_file(input_file, output_prefix='split_file_', chunk_size=100 * 1024**3):
    """
    将大文件拆分为多个小文件。

    参数:
        input_file (str): 输入文件路径。
        output_prefix (str, optional): 输出拆分文件的前缀,默认为'split_file_'。
        chunk_size (int, optional): 每个拆分文件的大小(以字节为单位),默认为100GB。
    """
    with open(input_file, 'rb') as in_file:
        chunk_num = 1
        chunk_data = in_file.read(chunk_size)  # 读取第一块数据
        while chunk_data:  # 非空数据继续
            chunk_file_name = f"{output_prefix}{chunk_num}.part"
            with open(chunk_file_name, 'wb') as out_file:
                out_file.write(chunk_data)
            chunk_num += 1
            chunk_data = in_file.read(chunk_size) 


def get_split_file(input_file, prefix='split_file_'):
    """
    获取由`split_file()`生成的所有拆分文件的名称。

    参数:
        input_file (str): 原始输入文件路径。
        prefix (str, optional): 拆分文件使用的前缀,默认为'split_file_'。

    返回:
        list[str]: 拆分文件名列表。
    """
    base_dir = os.path.dirname(input_file)
    base_name = os.path.splitext(os.path.basename(input_file))[0]
    return [f"{base_dir}/{prefix}{i}.part" for i in range(10**6) if os.path.isfile(f"{prefix}{i}.part")]

综上,希望可以考虑支持,非常感谢!

附加信息

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.