chore(io): migrate internal/cmd/docs and internal/cmd/dev to Medium
- internal/cmd/docs: Replace os.Stat, os.ReadFile, os.WriteFile, os.MkdirAll, os.RemoveAll with io.Local equivalents - internal/cmd/dev: Replace os.Stat, os.ReadFile, os.WriteFile, os.MkdirAll, os.ReadDir with io.Local equivalents - Fix local.Medium to allow absolute paths when root is "/" for full filesystem access (io.Local use case) Refs #113, #114 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b053206e95
commit
ec6eca99d8
5 changed files with 32 additions and 26 deletions
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/host-uk/core/pkg/errors"
|
"github.com/host-uk/core/pkg/errors"
|
||||||
"github.com/host-uk/core/pkg/git"
|
"github.com/host-uk/core/pkg/git"
|
||||||
"github.com/host-uk/core/pkg/i18n"
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
|
"github.com/host-uk/core/pkg/io"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -76,8 +77,8 @@ func runApply() error {
|
||||||
|
|
||||||
// Validate script exists
|
// Validate script exists
|
||||||
if applyScript != "" {
|
if applyScript != "" {
|
||||||
if _, err := os.Stat(applyScript); err != nil {
|
if !io.Local.Exists(applyScript) {
|
||||||
return errors.E("dev.apply", "script not found: "+applyScript, err)
|
return errors.E("dev.apply", "script not found: "+applyScript, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
package dev
|
package dev
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/cli"
|
"github.com/host-uk/core/pkg/cli"
|
||||||
"github.com/host-uk/core/pkg/i18n"
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
|
"github.com/host-uk/core/pkg/io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Workflow command flags
|
// Workflow command flags
|
||||||
|
|
@ -156,7 +156,7 @@ func runWorkflowSync(registryPath string, workflowFile string, dryRun bool) erro
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read template content
|
// Read template content
|
||||||
templateContent, err := os.ReadFile(templatePath)
|
templateContent, err := io.Local.Read(templatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.Wrap(err, i18n.T("cmd.dev.workflow.read_template_error"))
|
return cli.Wrap(err, i18n.T("cmd.dev.workflow.read_template_error"))
|
||||||
}
|
}
|
||||||
|
|
@ -189,8 +189,8 @@ func runWorkflowSync(registryPath string, workflowFile string, dryRun bool) erro
|
||||||
destPath := filepath.Join(destDir, workflowFile)
|
destPath := filepath.Join(destDir, workflowFile)
|
||||||
|
|
||||||
// Check if workflow already exists and is identical
|
// Check if workflow already exists and is identical
|
||||||
if existingContent, err := os.ReadFile(destPath); err == nil {
|
if existingContent, err := io.Local.Read(destPath); err == nil {
|
||||||
if string(existingContent) == string(templateContent) {
|
if existingContent == templateContent {
|
||||||
cli.Print(" %s %s %s\n",
|
cli.Print(" %s %s %s\n",
|
||||||
dimStyle.Render("-"),
|
dimStyle.Render("-"),
|
||||||
repoNameStyle.Render(repo.Name),
|
repoNameStyle.Render(repo.Name),
|
||||||
|
|
@ -210,7 +210,7 @@ func runWorkflowSync(registryPath string, workflowFile string, dryRun bool) erro
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create .github/workflows directory if needed
|
// Create .github/workflows directory if needed
|
||||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
if err := io.Local.EnsureDir(destDir); err != nil {
|
||||||
cli.Print(" %s %s %s\n",
|
cli.Print(" %s %s %s\n",
|
||||||
errorStyle.Render(cli.Glyph(":cross:")),
|
errorStyle.Render(cli.Glyph(":cross:")),
|
||||||
repoNameStyle.Render(repo.Name),
|
repoNameStyle.Render(repo.Name),
|
||||||
|
|
@ -220,7 +220,7 @@ func runWorkflowSync(registryPath string, workflowFile string, dryRun bool) erro
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write workflow file
|
// Write workflow file
|
||||||
if err := os.WriteFile(destPath, templateContent, 0644); err != nil {
|
if err := io.Local.Write(destPath, templateContent); err != nil {
|
||||||
cli.Print(" %s %s %s\n",
|
cli.Print(" %s %s %s\n",
|
||||||
errorStyle.Render(cli.Glyph(":cross:")),
|
errorStyle.Render(cli.Glyph(":cross:")),
|
||||||
repoNameStyle.Render(repo.Name),
|
repoNameStyle.Render(repo.Name),
|
||||||
|
|
@ -264,7 +264,7 @@ func findWorkflows(dir string) []string {
|
||||||
workflowsDir = dir
|
workflowsDir = dir
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(workflowsDir)
|
entries, err := io.Local.List(workflowsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -298,7 +298,7 @@ func findTemplateWorkflow(registryDir, workflowFile string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, candidate := range candidates {
|
for _, candidate := range candidates {
|
||||||
if _, err := os.Stat(candidate); err == nil {
|
if io.Local.Exists(candidate) {
|
||||||
return candidate
|
return candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/host-uk/core/internal/cmd/workspace"
|
"github.com/host-uk/core/internal/cmd/workspace"
|
||||||
"github.com/host-uk/core/pkg/cli"
|
"github.com/host-uk/core/pkg/cli"
|
||||||
"github.com/host-uk/core/pkg/i18n"
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
|
"github.com/host-uk/core/pkg/io"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -93,28 +94,28 @@ func scanRepoDocs(repo *repos.Repo) RepoDocInfo {
|
||||||
|
|
||||||
// Check for README.md
|
// Check for README.md
|
||||||
readme := filepath.Join(repo.Path, "README.md")
|
readme := filepath.Join(repo.Path, "README.md")
|
||||||
if _, err := os.Stat(readme); err == nil {
|
if io.Local.Exists(readme) {
|
||||||
info.Readme = readme
|
info.Readme = readme
|
||||||
info.HasDocs = true
|
info.HasDocs = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for CLAUDE.md
|
// Check for CLAUDE.md
|
||||||
claudeMd := filepath.Join(repo.Path, "CLAUDE.md")
|
claudeMd := filepath.Join(repo.Path, "CLAUDE.md")
|
||||||
if _, err := os.Stat(claudeMd); err == nil {
|
if io.Local.Exists(claudeMd) {
|
||||||
info.ClaudeMd = claudeMd
|
info.ClaudeMd = claudeMd
|
||||||
info.HasDocs = true
|
info.HasDocs = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for CHANGELOG.md
|
// Check for CHANGELOG.md
|
||||||
changelog := filepath.Join(repo.Path, "CHANGELOG.md")
|
changelog := filepath.Join(repo.Path, "CHANGELOG.md")
|
||||||
if _, err := os.Stat(changelog); err == nil {
|
if io.Local.Exists(changelog) {
|
||||||
info.Changelog = changelog
|
info.Changelog = changelog
|
||||||
info.HasDocs = true
|
info.HasDocs = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively scan docs/ directory for .md files
|
// Recursively scan docs/ directory for .md files
|
||||||
docsDir := filepath.Join(repo.Path, "docs")
|
docsDir := filepath.Join(repo.Path, "docs")
|
||||||
if _, err := os.Stat(docsDir); err == nil {
|
if io.Local.IsDir(docsDir) {
|
||||||
filepath.WalkDir(docsDir, func(path string, d fs.DirEntry, err error) error {
|
filepath.WalkDir(docsDir, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -139,9 +140,5 @@ func scanRepoDocs(repo *repos.Repo) RepoDocInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(src, dst string) error {
|
func copyFile(src, dst string) error {
|
||||||
data, err := os.ReadFile(src)
|
return io.Copy(io.Local, src, io.Local, dst)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(dst, data, 0644)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
package docs
|
package docs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/cli"
|
"github.com/host-uk/core/pkg/cli"
|
||||||
"github.com/host-uk/core/pkg/i18n"
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
|
"github.com/host-uk/core/pkg/io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Flag variables for sync command
|
// Flag variables for sync command
|
||||||
|
|
@ -127,9 +127,9 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
|
||||||
repoOutDir := filepath.Join(outputDir, outName)
|
repoOutDir := filepath.Join(outputDir, outName)
|
||||||
|
|
||||||
// Clear existing directory
|
// Clear existing directory
|
||||||
os.RemoveAll(repoOutDir)
|
_ = io.Local.DeleteAll(repoOutDir)
|
||||||
|
|
||||||
if err := os.MkdirAll(repoOutDir, 0755); err != nil {
|
if err := io.Local.EnsureDir(repoOutDir); err != nil {
|
||||||
cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), info.Name, err)
|
cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), info.Name, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +139,7 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
|
||||||
for _, f := range info.DocsFiles {
|
for _, f := range info.DocsFiles {
|
||||||
src := filepath.Join(docsDir, f)
|
src := filepath.Join(docsDir, f)
|
||||||
dst := filepath.Join(repoOutDir, f)
|
dst := filepath.Join(repoOutDir, f)
|
||||||
os.MkdirAll(filepath.Dir(dst), 0755)
|
_ = io.Local.EnsureDir(filepath.Dir(dst))
|
||||||
if err := copyFile(src, dst); err != nil {
|
if err := copyFile(src, dst); err != nil {
|
||||||
cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), f, err)
|
cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), f, err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,20 @@ func (m *Medium) path(relativePath string) (string, error) {
|
||||||
return "", errors.New("path traversal attempt detected")
|
return "", errors.New("path traversal attempt detected")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reject absolute paths - they bypass the sandbox
|
// When root is "/" (full filesystem access), allow absolute paths
|
||||||
if filepath.IsAbs(cleanPath) {
|
isRootFS := m.root == "/" || m.root == string(filepath.Separator)
|
||||||
|
|
||||||
|
// Reject absolute paths unless we're the root filesystem
|
||||||
|
if filepath.IsAbs(cleanPath) && !isRootFS {
|
||||||
return "", errors.New("path traversal attempt detected")
|
return "", errors.New("path traversal attempt detected")
|
||||||
}
|
}
|
||||||
|
|
||||||
fullPath := filepath.Join(m.root, cleanPath)
|
var fullPath string
|
||||||
|
if filepath.IsAbs(cleanPath) {
|
||||||
|
fullPath = cleanPath
|
||||||
|
} else {
|
||||||
|
fullPath = filepath.Join(m.root, cleanPath)
|
||||||
|
}
|
||||||
|
|
||||||
// Verify the resulting path is still within root (boundary-aware check)
|
// Verify the resulting path is still within root (boundary-aware check)
|
||||||
// Must use separator to prevent /tmp/root matching /tmp/root2
|
// Must use separator to prevent /tmp/root matching /tmp/root2
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue