Files
ofdgo/ofdgo_archive.go
T
2026-10-19 08:43:50 +08:00

121 lines
3.6 KiB
Go

// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"archive/zip"
"compress/flate"
"context"
"fmt"
"io"
"io/fs"
"path"
"strings"
)
// ArchiveFile 描述已生成的归档文件,不关闭调用方提供的源读取器
type ArchiveFile struct {
Name string
Input *Input
}
// ArchiveOptions 设置ZIP封装,压缩不重新编码归档内已生成的文档
type ArchiveOptions struct {
Compression CompressionOptions
OnProgress func(completed, total int) error
}
// WriteArchive 分块归档文件,保持文件内容,有损选项在此仅提高ZIP压缩等级
// 入参: ctx 取消上下文, writer 输出流, files 文件列表, options 归档配置
// 返回: int64 已写字节数, error 读写错误,出错时应丢弃输出
func WriteArchive(ctx context.Context, writer io.Writer, files []ArchiveFile, options ArchiveOptions) (int64, error) {
if err := options.Compression.Validate(); err != nil {
return 0, err
}
names := map[string]bool{}
for _, file := range files {
if !fs.ValidPath(file.Name) || file.Name == "." || strings.Contains(file.Name, "\\") || names[file.Name] || file.Input == nil {
return 0, fmt.Errorf("invalid archive file %q", file.Name)
}
names[file.Name] = true
}
var count int64
archive := zip.NewWriter(convertWriter{ctx: ctx, writer: writer, count: &count})
configureArchive(archive, options.Compression)
var buffer []byte
for i, file := range files {
if err := ctx.Err(); err != nil {
return count, err
}
if options.OnProgress != nil {
if err := options.OnProgress(i, len(files)); err != nil {
return count, err
}
}
if err := ctx.Err(); err != nil {
return count, err
}
method := uint16(zip.Deflate)
switch strings.ToLower(path.Ext(file.Name)) {
case ".ofd", ".pdf", ".png", ".jpg", ".jpeg":
method = zip.Store
}
entry, err := archive.CreateHeader(&zip.FileHeader{Name: file.Name, Method: method})
if err != nil {
return count, err
}
input := convertReader{ctx: ctx, input: file.Input}
if buffer == nil {
buffer = make([]byte, 32<<10)
}
written, err := io.CopyBuffer(entry, io.NewSectionReader(input, 0, file.Input.Size()), buffer)
if err != nil {
return count, err
}
if written != file.Input.Size() {
return count, io.ErrUnexpectedEOF
}
}
if err := archive.Close(); err != nil {
return count, err
}
if err := ctx.Err(); err != nil {
return count, err
}
if options.OnProgress != nil {
if err := options.OnProgress(len(files), len(files)); err != nil {
return count, err
}
}
return count, ctx.Err()
}
// configureArchive 统一归档压缩等级,在单个归档内复用压缩器
// 入参: archive 归档写入器, options 压缩配置
func configureArchive(archive *zip.Writer, options CompressionOptions) {
if options.Mode != CompressionUnchanged {
var compressor *flate.Writer
archive.RegisterCompressor(zip.Deflate, func(writer io.Writer) (io.WriteCloser, error) {
if compressor == nil {
var err error
compressor, err = flate.NewWriter(writer, flate.BestCompression)
return compressor, err
}
compressor.Reset(writer)
return compressor, nil
})
}
}