package archive import ( "errors" "fmt" "io/fs" "log/slog" "os" "path" "path/filepath" "code.icb4dc0.de/buildr/buildr/internal/ioutils" "code.icb4dc0.de/buildr/buildr/modules" "github.com/klauspost/compress/zip" ) var _ modules.Module = (*ZipArchive)(nil) type ZipArchive struct { Name string `hcl:"archive_name"` WrapInDir string `hcl:"wrap_in_directory,optional"` } func (z ZipArchive) Category() modules.Category { return modules.CategoryPackage } func (z ZipArchive) Type() string { return "zip_archive" } func (z ZipArchive) Execute(ctx modules.ExecutionContext) (err error) { if z.Name == "" { return fmt.Errorf("zip archive name may not be empty") } logger := ctx.Logger() logger.Info("Packaging archive", slog.String("archive_name", z.Name)) outFilePath := filepath.Join(ctx.OutDir(), z.Name) logger.Debug("Creating out file", slog.String("out_file", outFilePath)) zipFile, err := os.Create(outFilePath) if err != nil { return fmt.Errorf("failed to create out file for zip archive: %w", err) } defer func() { err = errors.Join(err, zipFile.Close()) }() zipWriter := zip.NewWriter(zipFile) defer func() { if zipCloseErr := zipWriter.Close(); zipCloseErr != nil { err = errors.Join(err, fmt.Errorf("failed to close zip archive: %w", zipCloseErr)) } }() if err = filepath.WalkDir(ctx.WorkingDir(), z.walker(logger, zipWriter, ctx.WorkingDir(), z.WrapInDir)); err != nil { return fmt.Errorf("failed to recursively add files to zip archive: %w", err) } return nil } func (z ZipArchive) walker(logger *slog.Logger, zipWriter *zip.Writer, basePath, commonPrefix string) fs.WalkDirFunc { return func(currentPath string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } logger.Debug("Adding file to zip archive", slog.String("file", currentPath)) archivePath := filepath.ToSlash(currentPath) if filepath.IsAbs(archivePath) { if archivePath, err = filepath.Rel(basePath, archivePath); err != nil { return err } } if commonPrefix != "" { archivePath = path.Join(commonPrefix, archivePath) } info, err := d.Info() if err != nil { return fmt.Errorf("failed to get file info for file %s: %w", currentPath, err) } header, err := zip.FileInfoHeader(info) if err != nil { return fmt.Errorf("failed to get file header for file %s: %w", currentPath, err) } sourceFile, err := os.Open(currentPath) if err != nil { return fmt.Errorf("failed to open file %s: %w", currentPath, err) } defer sourceFile.Close() header.Method = zip.Deflate header.Name = filepath.ToSlash(archivePath) zipFileWriter, err := zipWriter.CreateHeader(header) if err != nil { return fmt.Errorf("failed to write header for file %s: %w", currentPath, err) } _, err = ioutils.CopyWithPooledBuffer(zipFileWriter, sourceFile) if err != nil { return fmt.Errorf("failed to copy file to zip archive %s: %w", currentPath, err) } return nil } }