Compare commits
2 commits
main
...
perf-optim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fcb3a2706 | ||
|
|
269b3d6ac0 |
8 changed files with 23 additions and 346 deletions
|
|
@ -1,333 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Snider/Borg/pkg/compress"
|
|
||||||
"github.com/Snider/Borg/pkg/datanode"
|
|
||||||
"github.com/Snider/Borg/pkg/tim"
|
|
||||||
"github.com/Snider/Borg/pkg/trix"
|
|
||||||
"github.com/Snider/Borg/pkg/ui"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CollectLocalCmd struct {
|
|
||||||
cobra.Command
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCollectLocalCmd creates a new collect local command
|
|
||||||
func NewCollectLocalCmd() *CollectLocalCmd {
|
|
||||||
c := &CollectLocalCmd{}
|
|
||||||
c.Command = cobra.Command{
|
|
||||||
Use: "local [directory]",
|
|
||||||
Short: "Collect files from a local directory",
|
|
||||||
Long: `Collect files from a local directory and store them in a DataNode.
|
|
||||||
|
|
||||||
If no directory is specified, the current working directory is used.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
borg collect local
|
|
||||||
borg collect local ./src
|
|
||||||
borg collect local /path/to/project --output project.tar
|
|
||||||
borg collect local . --format stim --password secret
|
|
||||||
borg collect local . --exclude "*.log" --exclude "node_modules"`,
|
|
||||||
Args: cobra.MaximumNArgs(1),
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
directory := "."
|
|
||||||
if len(args) > 0 {
|
|
||||||
directory = args[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
outputFile, _ := cmd.Flags().GetString("output")
|
|
||||||
format, _ := cmd.Flags().GetString("format")
|
|
||||||
compression, _ := cmd.Flags().GetString("compression")
|
|
||||||
password, _ := cmd.Flags().GetString("password")
|
|
||||||
excludes, _ := cmd.Flags().GetStringSlice("exclude")
|
|
||||||
includeHidden, _ := cmd.Flags().GetBool("hidden")
|
|
||||||
respectGitignore, _ := cmd.Flags().GetBool("gitignore")
|
|
||||||
|
|
||||||
finalPath, err := CollectLocal(directory, outputFile, format, compression, password, excludes, includeHidden, respectGitignore)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Fprintln(cmd.OutOrStdout(), "Files saved to", finalPath)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
c.Flags().String("output", "", "Output file for the DataNode")
|
|
||||||
c.Flags().String("format", "datanode", "Output format (datanode, tim, trix, or stim)")
|
|
||||||
c.Flags().String("compression", "none", "Compression format (none, gz, or xz)")
|
|
||||||
c.Flags().String("password", "", "Password for encryption (required for stim/trix format)")
|
|
||||||
c.Flags().StringSlice("exclude", nil, "Patterns to exclude (can be specified multiple times)")
|
|
||||||
c.Flags().Bool("hidden", false, "Include hidden files and directories")
|
|
||||||
c.Flags().Bool("gitignore", true, "Respect .gitignore files (default: true)")
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
collectCmd.AddCommand(&NewCollectLocalCmd().Command)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CollectLocal collects files from a local directory into a DataNode
|
|
||||||
func CollectLocal(directory string, outputFile string, format string, compression string, password string, excludes []string, includeHidden bool, respectGitignore bool) (string, error) {
|
|
||||||
// Validate format
|
|
||||||
if format != "datanode" && format != "tim" && format != "trix" && format != "stim" {
|
|
||||||
return "", fmt.Errorf("invalid format: %s (must be 'datanode', 'tim', 'trix', or 'stim')", format)
|
|
||||||
}
|
|
||||||
if (format == "stim" || format == "trix") && password == "" {
|
|
||||||
return "", fmt.Errorf("password is required for %s format", format)
|
|
||||||
}
|
|
||||||
if compression != "none" && compression != "gz" && compression != "xz" {
|
|
||||||
return "", fmt.Errorf("invalid compression: %s (must be 'none', 'gz', or 'xz')", compression)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve directory path
|
|
||||||
absDir, err := filepath.Abs(directory)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error resolving directory path: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := os.Stat(absDir)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error accessing directory: %w", err)
|
|
||||||
}
|
|
||||||
if !info.IsDir() {
|
|
||||||
return "", fmt.Errorf("not a directory: %s", absDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load gitignore patterns if enabled
|
|
||||||
var gitignorePatterns []string
|
|
||||||
if respectGitignore {
|
|
||||||
gitignorePatterns = loadGitignore(absDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create DataNode and collect files
|
|
||||||
dn := datanode.New()
|
|
||||||
var fileCount int
|
|
||||||
|
|
||||||
bar := ui.NewProgressBar(-1, "Scanning files")
|
|
||||||
defer bar.Finish()
|
|
||||||
|
|
||||||
err = filepath.WalkDir(absDir, func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get relative path
|
|
||||||
relPath, err := filepath.Rel(absDir, path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip root
|
|
||||||
if relPath == "." {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip hidden files/dirs unless explicitly included
|
|
||||||
if !includeHidden && isHidden(relPath) {
|
|
||||||
if d.IsDir() {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check gitignore patterns
|
|
||||||
if respectGitignore && matchesGitignore(relPath, d.IsDir(), gitignorePatterns) {
|
|
||||||
if d.IsDir() {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check exclude patterns
|
|
||||||
if matchesExclude(relPath, excludes) {
|
|
||||||
if d.IsDir() {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip directories (they're implicit in DataNode)
|
|
||||||
if d.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file content
|
|
||||||
content, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error reading %s: %w", relPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to DataNode with forward slashes (tar convention)
|
|
||||||
dn.AddData(filepath.ToSlash(relPath), content)
|
|
||||||
fileCount++
|
|
||||||
bar.Describe(fmt.Sprintf("Collected %d files", fileCount))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error walking directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileCount == 0 {
|
|
||||||
return "", fmt.Errorf("no files found in %s", directory)
|
|
||||||
}
|
|
||||||
|
|
||||||
bar.Describe(fmt.Sprintf("Packaging %d files", fileCount))
|
|
||||||
|
|
||||||
// Convert to output format
|
|
||||||
var data []byte
|
|
||||||
if format == "tim" {
|
|
||||||
t, err := tim.FromDataNode(dn)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error creating tim: %w", err)
|
|
||||||
}
|
|
||||||
data, err = t.ToTar()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error serializing tim: %w", err)
|
|
||||||
}
|
|
||||||
} else if format == "stim" {
|
|
||||||
t, err := tim.FromDataNode(dn)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error creating tim: %w", err)
|
|
||||||
}
|
|
||||||
data, err = t.ToSigil(password)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error encrypting stim: %w", err)
|
|
||||||
}
|
|
||||||
} else if format == "trix" {
|
|
||||||
data, err = trix.ToTrix(dn, password)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error serializing trix: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data, err = dn.ToTar()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error serializing DataNode: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply compression
|
|
||||||
compressedData, err := compress.Compress(data, compression)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error compressing data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine output filename
|
|
||||||
if outputFile == "" {
|
|
||||||
baseName := filepath.Base(absDir)
|
|
||||||
if baseName == "." || baseName == "/" {
|
|
||||||
baseName = "local"
|
|
||||||
}
|
|
||||||
outputFile = baseName + "." + format
|
|
||||||
if compression != "none" {
|
|
||||||
outputFile += "." + compression
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(outputFile, compressedData, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error writing output file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outputFile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isHidden checks if a path component starts with a dot
|
|
||||||
func isHidden(path string) bool {
|
|
||||||
parts := strings.Split(filepath.ToSlash(path), "/")
|
|
||||||
for _, part := range parts {
|
|
||||||
if strings.HasPrefix(part, ".") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadGitignore loads patterns from .gitignore if it exists
|
|
||||||
func loadGitignore(dir string) []string {
|
|
||||||
var patterns []string
|
|
||||||
|
|
||||||
gitignorePath := filepath.Join(dir, ".gitignore")
|
|
||||||
content, err := os.ReadFile(gitignorePath)
|
|
||||||
if err != nil {
|
|
||||||
return patterns
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(string(content), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
// Skip empty lines and comments
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
patterns = append(patterns, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
return patterns
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchesGitignore checks if a path matches any gitignore pattern
|
|
||||||
func matchesGitignore(path string, isDir bool, patterns []string) bool {
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
// Handle directory-only patterns
|
|
||||||
if strings.HasSuffix(pattern, "/") {
|
|
||||||
if !isDir {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pattern = strings.TrimSuffix(pattern, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle negation (simplified - just skip negated patterns)
|
|
||||||
if strings.HasPrefix(pattern, "!") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match against path components
|
|
||||||
matched, _ := filepath.Match(pattern, filepath.Base(path))
|
|
||||||
if matched {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also try matching the full path
|
|
||||||
matched, _ = filepath.Match(pattern, path)
|
|
||||||
if matched {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle ** patterns (simplified)
|
|
||||||
if strings.Contains(pattern, "**") {
|
|
||||||
simplePattern := strings.ReplaceAll(pattern, "**", "*")
|
|
||||||
matched, _ = filepath.Match(simplePattern, path)
|
|
||||||
if matched {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchesExclude checks if a path matches any exclude pattern
|
|
||||||
func matchesExclude(path string, excludes []string) bool {
|
|
||||||
for _, pattern := range excludes {
|
|
||||||
// Match against basename
|
|
||||||
matched, _ := filepath.Match(pattern, filepath.Base(path))
|
|
||||||
if matched {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match against full path
|
|
||||||
matched, _ = filepath.Match(pattern, path)
|
|
||||||
if matched {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
BIN
examples/demo-sample.smsg
Normal file
BIN
examples/demo-sample.smsg
Normal file
Binary file not shown.
2
go.mod
2
go.mod
|
|
@ -60,7 +60,7 @@ require (
|
||||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.45.0 // indirect
|
golang.org/x/crypto v0.44.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/term v0.37.0 // indirect
|
golang.org/x/term v0.37.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -155,8 +155,8 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
|
|
|
||||||
BIN
js/borg-stmf/dist/stmf.wasm
vendored
BIN
js/borg-stmf/dist/stmf.wasm
vendored
Binary file not shown.
|
|
@ -306,13 +306,26 @@ export class BorgSTMF {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async waitForWasm(timeout = 5000): Promise<void> {
|
private async waitForWasm(timeout = 5000): Promise<void> {
|
||||||
const start = Date.now();
|
if (window.BorgSTMF?.ready) {
|
||||||
while (!window.BorgSTMF?.ready) {
|
return;
|
||||||
if (Date.now() - start > timeout) {
|
|
||||||
throw new Error('Timeout waiting for WASM module to initialize');
|
|
||||||
}
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let timeoutId: number;
|
||||||
|
|
||||||
|
const onReady = () => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
document.removeEventListener('borgstmf:ready', onReady);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
document.removeEventListener('borgstmf:ready', onReady);
|
||||||
|
reject(new Error('Timeout waiting for WASM module to initialize'));
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
document.addEventListener('borgstmf:ready', onReady, { once: true });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadScript(src: string): Promise<void> {
|
private async loadScript(src: string): Promise<void> {
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -8,10 +8,7 @@ import (
|
||||||
// Assets embeds all frontend files for the media player
|
// Assets embeds all frontend files for the media player
|
||||||
// These are served both by Wails (memory) and HTTP (fallback)
|
// These are served both by Wails (memory) and HTTP (fallback)
|
||||||
//
|
//
|
||||||
//go:embed frontend/index.html
|
//go:embed frontend
|
||||||
//go:embed frontend/wasm_exec.js
|
|
||||||
//go:embed frontend/stmf.wasm
|
|
||||||
//go:embed frontend/demo-track.smsg
|
|
||||||
var assets embed.FS
|
var assets embed.FS
|
||||||
|
|
||||||
// Assets returns the embedded filesystem with frontend/ prefix stripped
|
// Assets returns the embedded filesystem with frontend/ prefix stripped
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue