package files import ( "archive/zip" "errors" "fmt" "io" "os" "path/filepath" ) var ErrZipTooLarge = errors.New("folder archive exceeds size limit") // ZipDirectory writes dir into a temp .zip file and returns its path. func ZipDirectory(dir string, maxBytes int64) (string, error) { info, err := os.Stat(dir) if err != nil { return "", err } if !info.IsDir() { return "", errors.New("path is not a directory") } tmp, err := os.CreateTemp("", "win64_mp-zip-*.zip") if err != nil { return "", err } path := tmp.Name() cleanup := func() { _ = os.Remove(path) } zw := zip.NewWriter(tmp) var written int64 err = filepath.WalkDir(dir, func(current string, entry os.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if entry.Type()&os.ModeSymlink != 0 { return nil } if !isWithin(dir, current) { return ErrOutsideRoot } rel, err := filepath.Rel(dir, current) if err != nil { return err } if rel == "." { return nil } if entry.IsDir() { return nil } info, err := entry.Info() if err != nil { return err } if maxBytes > 0 && written+info.Size() > maxBytes { return ErrZipTooLarge } hdr, err := zip.FileInfoHeader(info) if err != nil { return err } hdr.Name = filepath.ToSlash(rel) hdr.Method = zip.Deflate w, err := zw.CreateHeader(hdr) if err != nil { return err } f, err := os.Open(current) if err != nil { return err } n, err := io.Copy(w, f) _ = f.Close() if err != nil { return err } written += n return nil }) if err != nil { _ = zw.Close() _ = tmp.Close() cleanup() return "", err } if err := zw.Close(); err != nil { _ = tmp.Close() cleanup() return "", err } if err := tmp.Close(); err != nil { cleanup() return "", err } return path, nil } // ZipArchiveName returns a safe download filename for a directory path. func ZipArchiveName(dir string) string { base := filepath.Base(dir) if base == "" || base == "." || base == string(os.PathSeparator) { return "folder.zip" } return fmt.Sprintf("%s.zip", base) }