Skip to content

Commit

Permalink
Merge pull request #51 from babarot/babarot/move
Browse files Browse the repository at this point in the history
Make moving files across different partitions possible by adding fallback to copy-and-delete
  • Loading branch information
babarot authored Jan 28, 2025
2 parents 982af3c + d86e8b0 commit b7f0aab
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 16 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
name: Go
on:
pull_request:
paths:
- '**/*.go'
workflow_dispatch:

jobs:
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ github:
alias:
rm: gomi
```
```console
```bash
afx install
```

Expand Down Expand Up @@ -153,15 +153,15 @@ history:

To get extra debug output (not shown in the official help), use the `--debug` flag:

```console
```bash
gomi --debug
```

This command will stream log output, similar to `tail -f`. While running `gomi --debug`, you can open another terminal or console and execute `gomi` commands to view live updates in the log as they happen. This is useful for debugging and monitoring `gomi`'s actions in real time.

If you prefer JSON formatted output, use:

```console
```bash
gomi --debug=json
```

Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ require (
github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.15.2
github.com/nxadm/tail v1.4.11
github.com/otiai10/copy v1.14.1
github.com/rs/xid v1.6.0
github.com/samber/lo v1.47.0
golang.org/x/sync v0.10.0
golang.org/x/sys v0.29.0
gopkg.in/yaml.v2 v2.4.0
)

Expand All @@ -47,12 +49,12 @@ require (
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
Expand Down
17 changes: 6 additions & 11 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ func (c CLI) Run(args []string) error {
}

func (c CLI) Restore() error {
slog.Debug("cil.restore started")
defer slog.Debug("cil.restore finished")
slog.Debug("cli.restore started")
defer slog.Debug("cli.restore finished")

if len(c.history.Files) == 0 {
fmt.Printf("The history is empty. Let's try deleting a file first\n")
Expand Down Expand Up @@ -226,7 +226,7 @@ func (c CLI) Restore() error {
if !allowed() {
continue
}
err := os.Rename(file.To, file.From)
err := move(file.To, file.From)
if err != nil {
errs = append(errs, err)
slog.Error("failed to restore! file would not be deleted from history file", "error", err)
Expand All @@ -248,8 +248,8 @@ func (c CLI) Restore() error {
}

func (c CLI) Put(args []string) error {
slog.Debug("cil.put started")
defer slog.Debug("cil.put finished")
slog.Debug("cli.put started")
defer slog.Debug("cli.put finished")

if len(args) == 0 {
return errors.New("too few arguments")
Expand All @@ -276,12 +276,7 @@ func (c CLI) Put(args []string) error {
files[i] = file
mu.Unlock()

// Ensure the directory exists before renaming the file
_ = os.MkdirAll(filepath.Dir(file.To), 0777)

// Log the file move
slog.Debug("moved", "from", file.From, "to", file.To)
return os.Rename(file.From, file.To)
return move(file.From, file.To)
})
}

Expand Down
60 changes: 60 additions & 0 deletions internal/cli/move.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package cli

import (
"fmt"
"log/slog"
"os"
"path/filepath"

cp "github.com/otiai10/copy"
)

// copyAndDelete copies a file or directory (recursively) and then deletes the original.
func copyAndDelete(src, dst string) error {
slog.Debug("starting copy and delete operation", "from", src, "to", dst)
if err := cp.Copy(src, dst); err != nil {
return fmt.Errorf("failed to copy file: %w", err)
}

// If the copy is successful, remove the original file or directory
if err := os.Remove(src); err != nil {
// If removal of the source fails after copying, attempt to delete the copied file as well
if rmErr := os.Remove(dst); rmErr != nil {
return fmt.Errorf(
"failed to remove both source and destination files: source error: %v, destination error: %v",
err, rmErr)
}
return fmt.Errorf("failed to remove source file after successful copy: %w", err)
}

return nil
}

// move attempts to rename a file or directory. If it's on different partitions, it falls back to copying and deleting.
func move(src, dst string) error {
dstDir := filepath.Dir(dst)

// Ensure the destination directory exists before attempting to move
if _, err := os.Stat(dstDir); os.IsNotExist(err) {
slog.Debug("mkdir", "dir", dstDir)
if err := os.MkdirAll(dstDir, 0777); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}
}

// Check if source and destination are on the same partition
samePartition, err := isSamePartition(src, dstDir)
if err != nil {
return err
}
defer slog.Debug("file moved", "from", src, "to", dst)

// If they are on the same partition, use os.Rename; otherwise, fallback to copy-and-delete
if samePartition {
slog.Debug("moving file with os.Rename")
return os.Rename(src, dst)
}

slog.Debug("different partitions detected, falling back to copy-and-delete operation")
return copyAndDelete(src, dst)
}
37 changes: 37 additions & 0 deletions internal/cli/partition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//go:build !windows

package cli

import (
"fmt"
"log/slog"
"os"
"syscall"
)

// isSamePartition checks if the source and destination reside on the same filesystem partition.
func isSamePartition(src, dst string) (bool, error) {
srcStat, err := os.Stat(src)
if err != nil {
return false, fmt.Errorf("failed to get source file stats: %w", err)
}

dstStat, err := os.Stat(dst)
if err != nil {
return false, fmt.Errorf("failed to get destination file stats: %w", err)
}

srcSys := srcStat.Sys().(*syscall.Stat_t)
dstSys := dstStat.Sys().(*syscall.Stat_t)

// Compare the device identifiers (st_dev) of the source and destination
// If the device IDs are the same, the files are on the same partition.
samePartition := srcSys.Dev == dstSys.Dev

slog.Debug("check src/dst file info",
"samePartition", samePartition,
"src st_dev", srcSys.Dev,
"dst st_dev", dstSys.Dev)

return samePartition, nil
}
44 changes: 44 additions & 0 deletions internal/cli/partition_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//go:build windows

package cli

import (
"fmt"
"log/slog"
"path/filepath"

"golang.org/x/sys/windows"
)

// isSamePartition checks if the source and destination reside on the same filesystem partition on Windows.
func isSamePartition(src, dst string) (bool, error) {
// Get the volume names (drive letters) for both source and destination paths
srcVolume := filepath.VolumeName(src)
dstVolume := filepath.VolumeName(dst)

if srcVolume == "" || dstVolume == "" {
return false, fmt.Errorf("failed to determine volume name from file paths")
}

// Get volume information for both source and destination volumes
var srcVolID, dstVolID uint32
err := windows.GetVolumeInformation(windows.StringToUTF16Ptr(srcVolume), nil, 0, &srcVolID, nil, nil, nil, 0)
if err != nil {
return false, fmt.Errorf("failed to get source volume information: %w", err)
}

err = windows.GetVolumeInformation(windows.StringToUTF16Ptr(dstVolume), nil, 0, &dstVolID, nil, nil, nil, 0)
if err != nil {
return false, fmt.Errorf("failed to get destination volume information: %w", err)
}

// Compare the volume IDs to determine if they are on the same partition
samePartition := srcVolID == dstVolID

slog.Debug("check src/dst volume info",
"samePartition", samePartition,
"src volume", srcVolID,
"dst volume", dstVolID)

return samePartition, nil
}
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ type parser struct{}

func validSize(fl validator.FieldLevel) bool {
value := strings.ToUpper(fl.Field().String())
re := regexp.MustCompile(`^\d+(KB|MB|GB|TB|PB)$`)
re := regexp.MustCompile(`^\d+(KB|MB|GB|TB|PB)|$`) // empty is acceptable
return re.MatchString(value)
}

Expand Down

0 comments on commit b7f0aab

Please sign in to comment.