Files
ci/internal/workspace/workspace.go
2026-06-18 21:19:47 +08:00

187 lines
4.1 KiB
Go

package workspace
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// Manager handles workspace directories for projects.
type Manager struct {
BaseDir string
}
// New creates a new workspace Manager.
func New(baseDir string) (*Manager, error) {
if err := os.MkdirAll(baseDir, 0755); err != nil {
return nil, err
}
return &Manager{BaseDir: baseDir}, nil
}
// ProjectDir returns the root workspace directory for a project.
func (m *Manager) ProjectDir(projectID uint) string {
return filepath.Join(m.BaseDir, fmt.Sprintf("project-%d", projectID))
}
// SrcDir returns the source directory for a project.
func (m *Manager) SrcDir(projectID uint) string {
return filepath.Join(m.ProjectDir(projectID), "src")
}
// BuildsDir returns the builds directory for a project.
func (m *Manager) BuildsDir(projectID uint) string {
return filepath.Join(m.ProjectDir(projectID), "builds")
}
// LogsDir returns the logs directory for a project.
func (m *Manager) LogsDir(projectID uint) string {
return filepath.Join(m.ProjectDir(projectID), "logs")
}
// InitProject creates all required directories for a project.
func (m *Manager) InitProject(projectID uint) error {
dirs := []string{
m.SrcDir(projectID),
m.BuildsDir(projectID),
m.LogsDir(projectID),
}
for _, d := range dirs {
if err := os.MkdirAll(d, 0755); err != nil {
return err
}
}
return nil
}
// CleanProject removes the entire project workspace.
func (m *Manager) CleanProject(projectID uint) error {
return os.RemoveAll(m.ProjectDir(projectID))
}
// ExtractArchive detects archive type and extracts to the project src directory.
// Supports .zip, .tar.gz, .tgz.
func (m *Manager) ExtractArchive(projectID uint, filePath string) error {
dest := m.SrcDir(projectID)
// Clean destination first
os.RemoveAll(dest)
os.MkdirAll(dest, 0755)
lower := strings.ToLower(filePath)
switch {
case strings.HasSuffix(lower, ".zip"):
return extractZip(filePath, dest)
case strings.HasSuffix(lower, ".tar.gz"), strings.HasSuffix(lower, ".tgz"):
return extractTarGz(filePath, dest)
default:
return fmt.Errorf("unsupported archive format: %s (use .zip or .tar.gz)", filePath)
}
}
func extractZip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
// Prevent zip slip
path := filepath.Join(dest, f.Name)
if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("illegal file path in zip: %s", f.Name)
}
if f.FileInfo().IsDir() {
os.MkdirAll(path, 0755)
continue
}
os.MkdirAll(filepath.Dir(path), 0755)
out, err := os.Create(path)
if err != nil {
return err
}
rc, err := f.Open()
if err != nil {
out.Close()
return err
}
_, err = io.Copy(out, rc)
rc.Close()
out.Close()
if err != nil {
return err
}
}
return nil
}
func extractTarGz(src, dest string) error {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
gzReader, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gzReader.Close()
tarReader := tar.NewReader(gzReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
path := filepath.Join(dest, header.Name)
if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("illegal file path in tar: %s", header.Name)
}
switch header.Typeflag {
case tar.TypeDir:
os.MkdirAll(path, 0755)
case tar.TypeReg:
os.MkdirAll(filepath.Dir(path), 0755)
out, err := os.Create(path)
if err != nil {
return err
}
_, err = io.Copy(out, tarReader)
out.Close()
if err != nil {
return err
}
}
}
return nil
}
// SaveUploadedFile saves an uploaded file to a temp location.
func (m *Manager) SaveUploadedFile(projectID uint, reader io.Reader, filename string) (string, error) {
dir := m.ProjectDir(projectID)
os.MkdirAll(dir, 0755)
path := filepath.Join(dir, filename)
f, err := os.Create(path)
if err != nil {
return "", err
}
defer f.Close()
_, err = io.Copy(f, reader)
return path, err
}