diff options
| -rw-r--r-- | ccp.go | 86 | ||||
| -rw-r--r-- | internal/cp/cp.go | 218 |
2 files changed, 164 insertions, 140 deletions
@@ -27,10 +27,6 @@ type measurement struct { type model struct { progress progress.Model - msgs chan tea.Msg - - srcs []cp.FSPath - dst cp.FSPath max int64 current atomic.Int64 @@ -38,6 +34,7 @@ type model struct { copyingFiles map[string]string copyingFile string errs []string + done bool } type ( @@ -54,37 +51,23 @@ type ( doneMsg struct{} ) -func (m *model) listen() tea.Cmd { - return func() tea.Msg { - return <-m.msgs - } -} - func tick() tea.Cmd { return tea.Tick(10*time.Millisecond, func(t time.Time) tea.Msg { return tickMsg(t) }) } func (m *model) Init() tea.Cmd { - return tea.Batch( - func() tea.Msg { - cp.Copy(m, m.srcs, m.dst, *f) - return doneMsg{} - }, - m.listen(), - tick()) + return tick() } func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case maxMsg: m.max = int64(msg) - return m, m.listen() case fileStartMsg: m.copyingFiles[msg.from] = msg.to if m.copyingFile == "" { m.copyingFile = msg.from } - return m, m.listen() case fileDoneMsg: delete(m.copyingFiles, msg.name) if m.copyingFile == msg.name { @@ -97,12 +80,16 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.err != nil { m.errs = append(m.errs, msg.err.Error()) } - return m, m.listen() case doneMsg: - return m, tea.Sequence( - m.progress.SetPercent(float64(m.current.Load())/float64(m.max)), - tea.Tick(500*time.Millisecond, func(time.Time) tea.Msg { return nil }), - tea.Quit) + m.done = true + var cmd tea.Cmd + if m.max > 0 { + cmd = m.progress.SetPercent(float64(m.current.Load()) / float64(m.max)) + } + if !m.progress.IsAnimating() { + return m, tea.Quit + } + return m, cmd case tickMsg: n := m.current.Load() @@ -119,17 +106,18 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, tea.Batch(cmds...) - case tea.WindowSizeMsg: - m.progress.Width = msg.Width - 4 - return m, nil // FrameMsg is sent when the progress bar wants to animate itself case progress.FrameMsg: progressModel, cmd := m.progress.Update(msg) m.progress = progressModel.(progress.Model) + if m.done && !m.progress.IsAnimating() { + return m, tea.Quit + } return m, cmd - default: - return m, nil + case tea.WindowSizeMsg: + m.progress.Width = msg.Width - 4 } + return m, nil } var warningStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("3")).Render @@ -142,11 +130,11 @@ func (m *model) View() string { etaStr := "calculating..." if m.max > 0 && m.measurements.Len() > 1 { first := m.measurements.At(0) - last := m.measurements.At(m.measurements.Len() - 1) - deltaT := last.t.Sub(first.t) - delta := last.i - first.i + current := m.current.Load() + deltaT := time.Since(first.t) + delta := current - first.i if delta != 0 { - etaStr = time.Duration(float64(m.max-last.i) / float64(delta) * float64(deltaT)).Round(time.Second).String() + etaStr = time.Duration(float64(m.max-current) / float64(delta) * float64(deltaT)).Round(time.Second).String() } } return "\n" + @@ -156,20 +144,25 @@ func (m *model) View() string { warningStyle(strings.Join(m.errs, "\n")) + "\n" } -func (m *model) Max(n int64) { - m.msgs <- maxMsg(n) +type progressUpdater struct { + p *tea.Program + current *atomic.Int64 +} + +func (pu *progressUpdater) Max(n int64) { + pu.p.Send(maxMsg(n)) } -func (m *model) Progress(n int64) { - m.current.Add(n) +func (pu *progressUpdater) Progress(n int64) { + pu.current.Add(n) } -func (m *model) FileStart(from, to string) { - m.msgs <- fileStartMsg{from, to} +func (pu *progressUpdater) FileStart(from, to string) { + pu.p.Send(fileStartMsg{from, to}) } -func (m *model) FileDone(name string, err error) { - m.msgs <- fileDoneMsg{name, err} +func (pu *progressUpdater) FileDone(name string, err error) { + pu.p.Send(fileDoneMsg{name, err}) } func splitHostPath(target string) (string, string) { @@ -217,13 +210,14 @@ func run() error { } m := &model{ progress: progress.New(progress.WithDefaultGradient(), progress.WithoutPercentage()), - msgs: make(chan tea.Msg), copyingFiles: make(map[string]string), - - srcs: srcs, - dst: dst, } - if _, err := tea.NewProgram(m, tea.WithInput(nil), tea.WithOutput(os.Stderr)).Run(); err != nil { + p := tea.NewProgram(m, tea.WithInput(nil), tea.WithOutput(os.Stderr)) + go func() { + cp.Copy(&progressUpdater{p, &m.current}, srcs, dst, *f) + p.Send(doneMsg{}) + }() + if _, err := p.Run(); err != nil { return err } if len(m.errs) > 0 { diff --git a/internal/cp/cp.go b/internal/cp/cp.go index d944f9c..7350343 100644 --- a/internal/cp/cp.go +++ b/internal/cp/cp.go @@ -6,7 +6,8 @@ import ( "io" "io/fs" "path" - "sync" + "slices" + "strings" "gitlab.com/rhogenson/ccp/internal/wfs" "gitlab.com/rhogenson/ccp/internal/wfs/sftpfs" @@ -31,10 +32,54 @@ func (p FSPath) String() string { return p.Path } +func (p FSPath) WalkDir(fn fs.WalkDirFunc) error { + return fs.WalkDir(p.FS, p.Path, fn) +} + +func (p FSPath) Stat() (fs.FileInfo, error) { + return fs.Stat(p.FS, p.Path) +} + +func (p FSPath) Lstat() (fs.FileInfo, error) { + return wfs.Lstat(p.FS, p.Path) +} + +func (p FSPath) RemoveAll() error { + return wfs.RemoveAll(p.FS, p.Path) +} + +func (p FSPath) Open() (fs.File, error) { + return p.FS.Open(p.Path) +} + +func (p FSPath) Create(mode fs.FileMode) (io.WriteCloser, error) { + return p.FS.Create(p.Path, mode) +} + +func (p FSPath) ReadLink() (string, error) { + return wfs.ReadLink(p.FS, p.Path) +} + +func (p FSPath) SymlinkFrom(target string) error { + return p.FS.Symlink(target, p.Path) +} + +func (p FSPath) Mkdir() error { + return p.FS.Mkdir(p.Path) +} + +func (p FSPath) MkdirMode(mode fs.FileMode) error { + return wfs.MkdirMode(p.FS, p.Path, mode) +} + +func (p FSPath) Chmod(mode fs.FileMode) error { + return p.FS.Chmod(p.Path, mode) +} + func size(srcs []FSPath) int64 { var n int64 = 0 for _, src := range srcs { - fs.WalkDir(src.FS, src.Path, func(_ string, d fs.DirEntry, err error) error { + src.WalkDir(func(_ string, d fs.DirEntry, err error) error { if err != nil { return nil } @@ -54,31 +99,21 @@ func size(srcs []FSPath) int64 { return n } -func fileExists(path FSPath) bool { - _, err := wfs.Lstat(path.FS, path.Path) +func (p FSPath) exists() bool { + _, err := p.Lstat() return !errors.Is(err, fs.ErrNotExist) } type copier struct { - p Progress - sem chan struct{} - + p Progress force bool } -func (c *copier) g(fn func()) { - c.sem <- struct{}{} - go func() { - defer func() { <-c.sem }() - fn() - }() -} - func (c *copier) openWithRetry(path FSPath, fn func() error) error { - if err := fn(); err == nil || !c.force || !fileExists(path) { + if err := fn(); err == nil || !c.force || !path.exists() { return err } - if err := wfs.RemoveAll(path.FS, path.Path); err != nil { + if err := path.RemoveAll(); err != nil { return err } return fn() @@ -87,7 +122,7 @@ func (c *copier) openWithRetry(path FSPath, fn func() error) error { func (c *copier) copyRegularFile(src, dst FSPath) error { c.p.FileStart(src.String(), dst.String()) - in, err := src.FS.Open(src.Path) + in, err := src.Open() if err != nil { return err } @@ -99,7 +134,7 @@ func (c *copier) copyRegularFile(src, dst FSPath) error { var out io.WriteCloser if err := c.openWithRetry(dst, func() error { var err error - out, err = dst.FS.Create(dst.Path, stat.Mode().Perm()) + out, err = dst.Create(stat.Mode().Perm()) return err }); err != nil { return err @@ -125,83 +160,31 @@ func (c *copier) copyRegularFile(src, dst FSPath) error { return nil } -func (c *copier) copySymlink(src FSPath, d fs.DirEntry, dst FSPath) error { - target, err := wfs.ReadLink(src.FS, src.Path) - if err != nil { - return err - } - if err := c.openWithRetry(dst, func() error { - return dst.FS.Symlink(target, dst.Path) - }); err != nil { - return err - } - c.p.Progress(1) - return nil -} - -func (c *copier) copyDir(src FSPath, d fs.DirEntry, dst FSPath) error { - stat, err := d.Info() +func (c *copier) copySymlink(src FSPath, dst FSPath) error { + target, err := src.ReadLink() if err != nil { return err } - hasWritePerm := stat.Mode()&0300 == 0300 if err := c.openWithRetry(dst, func() error { - if hasWritePerm { - return wfs.MkdirMode(dst.FS, dst.Path, stat.Mode().Perm()) - } else { - return dst.FS.Mkdir(dst.Path) - } + return dst.SymlinkFrom(target) }); err != nil { return err } - entries, err := fs.ReadDir(src.FS, src.Path) - wg := new(sync.WaitGroup) - <-c.sem - for _, entry := range entries { - wg.Add(1) - c.g(func() { - defer wg.Done() - src := FSPath{src.FS, path.Join(src.Path, entry.Name())} - if err := c.copyFile(src, entry, FSPath{dst.FS, path.Join(dst.Path, entry.Name())}); err != nil { - c.p.FileDone(src.String(), err) - } - }) - } - wg.Wait() - c.sem <- struct{}{} - if err != nil { - return err - } - if !hasWritePerm { - if err := dst.FS.Chmod(dst.Path, stat.Mode().Perm()); err != nil { - return err - } - } c.p.Progress(1) return nil } -func (c *copier) copyFile(src FSPath, d fs.DirEntry, dst FSPath) error { - switch d.Type() { - case 0: // regular file - return c.copyRegularFile(src, dst) - case fs.ModeDir: - return c.copyDir(src, d, dst) - case fs.ModeSymlink: - return c.copySymlink(src, d, dst) - default: - return fmt.Errorf("%s: unknown file type %s", src, d.Type()) - } -} - func Copy(progress Progress, srcs []FSPath, dstRoot FSPath, force bool) { + done := make(chan struct{}) go func() { + defer close(done) progress.Max(size(srcs)) }() + defer func() { <-done }() dstIsDir := true if len(srcs) == 1 { - stat, err := fs.Stat(dstRoot.FS, dstRoot.Path) + stat, err := dstRoot.Stat() dstIsDir = err == nil && stat.IsDir() } @@ -210,28 +193,75 @@ func Copy(progress Progress, srcs []FSPath, dstRoot FSPath, force bool) { sem := make(chan struct{}, maxConcurrency) c := &copier{ p: progress, - sem: sem, force: force, } - wg := new(sync.WaitGroup) + type roDir struct { + path FSPath + mode fs.FileMode + } + var roDirs []roDir for _, srcRoot := range srcs { - wg.Add(1) - c.g(func() { - defer wg.Done() - dstRoot := dstRoot - if dstIsDir { - dstRoot.Path = path.Join(dstRoot.Path, path.Base(srcRoot.Path)) - } - stat, err := fs.Stat(srcRoot.FS, srcRoot.Path) + dstRoot := dstRoot + if dstIsDir { + dstRoot.Path = path.Join(dstRoot.Path, path.Base(srcRoot.Path)) + } + srcRoot.Path = path.Clean(srcRoot.Path) + srcRoot.WalkDir(func(srcPath string, d fs.DirEntry, err error) error { + src := FSPath{srcRoot.FS, srcPath} + dst := FSPath{dstRoot.FS, path.Join(dstRoot.Path, strings.TrimPrefix(srcPath, srcRoot.Path))} if err != nil { - progress.FileDone(srcRoot.String(), err) - return + progress.FileDone(src.String(), err) + return nil } - if err := c.copyFile(srcRoot, fs.FileInfoToDirEntry(stat), dstRoot); err != nil { - progress.FileDone(srcRoot.String(), err) - return + switch d.Type() { + case 0: + sem <- struct{}{} + go func() { + defer func() { <-sem }() + if err := c.copyRegularFile(src, dst); err != nil { + progress.FileDone(src.String(), err) + } + }() + case fs.ModeDir: + stat, err := d.Info() + if err != nil { + progress.FileDone(src.String(), err) + return nil + } + hasWritePerm := stat.Mode()&0300 == 0300 + if err := c.openWithRetry(dst, func() error { + if hasWritePerm { + return dst.MkdirMode(stat.Mode().Perm()) + } else { + return dst.Mkdir() + } + }); err != nil { + progress.FileDone(src.String(), err) + return nil + } + if hasWritePerm { + progress.Progress(1) + } else { + roDirs = append(roDirs, roDir{dst, stat.Mode().Perm()}) + } + case fs.ModeSymlink: + if err := c.copySymlink(src, dst); err != nil { + progress.FileDone(src.String(), err) + } + default: + progress.FileDone(src.String(), fmt.Errorf("%s: unknown file type %s", src, d.Type())) } + return nil }) } - wg.Wait() + for range maxConcurrency { + sem <- struct{}{} + } + for _, d := range slices.Backward(roDirs) { + if err := d.path.Chmod(d.mode); err != nil { + progress.FileDone(d.path.String(), err) + continue + } + progress.Progress(1) + } } |
