Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 20 additions & 27 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,16 @@ jobs:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact_name: copy
asset_name: copy-linux-x86_64
platform: linux-x86_64
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
artifact_name: copy
asset_name: copy-linux-x86_64-musl
platform: linux-x86_64-musl
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
artifact_name: copy
asset_name: copy-linux-aarch64
platform: linux-aarch64
- os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
artifact_name: copy
asset_name: copy-linux-armv7
platform: linux-armv7
Comment on lines +37 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Artifact platform names no longer match installer expectations.

Line 37-46 publishes linux-aarch64 / linux-armv7, but installer downloads *-aarch64-musl / *-armv7-musl names. That causes release download 404s on those architectures.

Please align the naming contract between workflow artifacts and installer URLs (either side can be the source of truth).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 37 - 46, The platform names
published for aarch64 and armv7 targets in the release workflow do not match
what the installer is attempting to download, causing 404 errors. Update the
platform names for the aarch64-unknown-linux-gnu and
armv7-unknown-linux-gnueabihf targets to either include the -musl suffix
(linux-aarch64-musl and linux-armv7-musl) to match installer expectations, or
alternatively update the installer download URLs to expect the current names
(linux-aarch64 and linux-armv7). Choose one approach and ensure consistency
between both the workflow artifact publishing and the installer download logic.


steps:
- uses: actions/checkout@v6
Expand All @@ -68,26 +64,23 @@ jobs:
sudo apt-get update -q
sudo apt-get install -y binutils-arm-linux-gnueabihf binutils-aarch64-linux-gnu

- name: Strip binary
- name: Strip and archive binaries
run: |
if [[ "${{ matrix.target }}" == *"musl"* ]]; then
strip target/${{ matrix.target }}/release/${{ matrix.artifact_name }} || true
elif [[ "${{ matrix.target }}" == "armv7-unknown-linux-gnueabihf" ]]; then
arm-linux-gnueabihf-strip target/${{ matrix.target }}/release/${{ matrix.artifact_name }} || true
elif [[ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]]; then
aarch64-linux-gnu-strip target/${{ matrix.target }}/release/${{ matrix.artifact_name }} || true
else
strip target/${{ matrix.target }}/release/${{ matrix.artifact_name }}
fi
case "${{ matrix.target }}" in
*musl*) strip_cmd=strip ;;
armv7-unknown-linux-gnueabihf) strip_cmd=arm-linux-gnueabihf-strip ;;
aarch64-unknown-linux-gnu) strip_cmd=aarch64-linux-gnu-strip ;;
*) strip_cmd=strip ;;
esac
rel="target/${{ matrix.target }}/release"
for bin in copy move; do
"$strip_cmd" "$rel/$bin" || true
tar czf "$bin-${{ matrix.platform }}.tar.gz" -C "$rel" "$bin"
done

- name: Create archive
run: |
cd target/${{ matrix.target }}/release
tar czf ${{ matrix.asset_name }}.tar.gz ${{ matrix.artifact_name }}
cd -
mv target/${{ matrix.target }}/release/${{ matrix.asset_name }}.tar.gz .

- name: Upload Release Asset
- name: Upload Release Assets
uses: softprops/action-gh-release@v3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read-only check: list workflow actions that are referenced by tag instead of SHA.
rg -n '^\s*uses:\s*[^@]+@v?[0-9][^ ]*$' .github/workflows

Repository: UnbreakableMJ/copy

Length of output: 470


Pin softprops/action-gh-release to a commit SHA instead of the mutable tag.

Lines 24 and 82 use @v3, which is a mutable tag. Pinning to a specific commit SHA prevents supply-chain drift if the upstream tag is retargeted or compromised.

🧰 Tools
🪛 zizmor (1.26.1)

[error] 82-82: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[info] 82-82: action functionality is already included by the runner (superfluous-actions): use gh release in a script step

(superfluous-actions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml at line 82, The `softprops/action-gh-release`
action at line 82 (and also at line 24) is pinned to the mutable tag `@v3`
instead of a specific commit SHA. Replace both instances of `uses:
softprops/action-gh-release@v3` with `uses:
softprops/action-gh-release@<specific-commit-sha>` where the commit SHA is the
full 40-character hash of a stable release commit. This prevents supply-chain
risks from upstream tag retargeting or compromise.

Source: Linters/SAST tools

with:
files: ./${{ matrix.asset_name }}.tar.gz
files: |
copy-${{ matrix.platform }}.tar.gz
move-${{ matrix.platform }}.tar.gz
10 changes: 7 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ SPDX-License-Identifier: GPL-3.0-or-later

`copy` is a Linux-first Rust 2024 CLI that replaces `cp` with parallel copying,
resume support, reflinks, symlink/hardlink modes, and configurable preservation
behavior. It is a Spacecraft Software-maintained fork of upstream `cpx`; upstream
MIT attribution is preserved in `LICENSES/MIT.txt`, and fork modifications are
distributed under GPL-3.0-or-later.
behavior. The crate also ships a sibling `move` binary (a `mv` replacement) that
reuses the copy engine: it renames in place when possible and otherwise copies
then removes the source. It is a Spacecraft Software-maintained fork of upstream
`cpx`; upstream MIT attribution is preserved in `LICENSES/MIT.txt`, and fork
modifications are distributed under GPL-3.0-or-later.

## Build, Test, Lint

Expand Down Expand Up @@ -41,6 +43,7 @@ green before handing work back.
- `copy_core()` tries hardlink preservation, reflink, Linux `copy_file_range`, then buffered fallback.
- Worker failures request cooperative cancellation for remaining parallel work. User SIGINT/SIGTERM uses the separate `options.abort` flag and maps to exit code 130 in `main.rs`.
- `README.md` and `README_CRATES.md` must stay in sync for user-facing behavior, install instructions, licensing, and release references.
- `move` (`src/bin/move.rs`, `src/core/move_op.rs`, `src/cli/move_args.rs`) is a thin second binary over the same library. It tries `std::fs::rename` first and only falls back to `copy()` + source removal on `EXDEV` (cross-device) or when `--exclude` must leave part of a directory behind. Because `copy()` always nests a directory under its destination, the directory fallback stages the copy in a temp dir on the destination filesystem and renames it into place. Never delete the source unless the copy succeeded.

## Forbidden Patterns

Expand Down Expand Up @@ -71,6 +74,7 @@ green before handing work back.
- Error types: `src/error.rs`
- Integration tests: `tests/intergration.rs`
- Standalone GNU compatibility scripts: `tests/gnu/`
- Move binary, engine, and CLI: `src/bin/move.rs`, `src/core/move_op.rs`, `src/cli/move_args.rs`
- Packaging: `nix/package.nix`, `guix.scm`, `packaging/aur/PKGBUILD`

## Release Notes For Agents
Expand Down
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ when project invariants, commands, packaging, or release workflow change.

`copy` is a Linux-first Rust 2024 CLI that replaces `cp` with parallel copying,
resume support, reflinks, symlink/hardlink modes, and configurable preservation
behavior. It is a Spacecraft Software-maintained fork of upstream `cpx`; upstream
MIT attribution is preserved in `LICENSES/MIT.txt`, and fork modifications are
distributed under GPL-3.0-or-later.
behavior. The crate also ships a sibling `move` binary (a `mv` replacement) that
reuses the copy engine: it renames in place when possible and otherwise copies
then removes the source. It is a Spacecraft Software-maintained fork of upstream
`cpx`; upstream MIT attribution is preserved in `LICENSES/MIT.txt`, and fork
modifications are distributed under GPL-3.0-or-later.

## Build, Test, Lint

Expand Down Expand Up @@ -44,6 +46,7 @@ green before handing work back.
- `copy_core()` tries hardlink preservation, reflink, Linux `copy_file_range`, then buffered fallback.
- Worker failures request cooperative cancellation for remaining parallel work. User SIGINT/SIGTERM uses the separate `options.abort` flag and maps to exit code 130 in `main.rs`.
- `README.md` and `README_CRATES.md` must stay in sync for user-facing behavior, install instructions, licensing, and release references.
- `move` (`src/bin/move.rs`, `src/core/move_op.rs`, `src/cli/move_args.rs`) is a thin second binary over the same library. It tries `std::fs::rename` first and only falls back to `copy()` + source removal on `EXDEV` (cross-device) or when `--exclude` must leave part of a directory behind. Because `copy()` always nests a directory under its destination, the directory fallback stages the copy in a temp dir on the destination filesystem and renames it into place. Never delete the source unless the copy succeeded.

## Forbidden Patterns

Expand Down Expand Up @@ -74,6 +77,7 @@ green before handing work back.
- Error types: `src/error.rs`
- Integration tests: `tests/intergration.rs`
- Standalone GNU compatibility scripts: `tests/gnu/`
- Move binary, engine, and CLI: `src/bin/move.rs`, `src/core/move_op.rs`, `src/cli/move_args.rs`
- Packaging: `nix/package.nix`, `guix.scm`, `packaging/aur/PKGBUILD`

## Release Notes For Agents
Expand Down
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

[package]
name = "copy"
description = "A modern, fast file copying tool"
description = "Modern, fast file copy and move tools"
repository = "https://github.com/UnbreakableMJ/copy"
readme = "README_CRATES.md"
keywords = ["copy", "file", "cli", "backup", "utility"]
Expand All @@ -19,6 +19,10 @@ edition = "2024"
name = "copy"
path = "src/main.rs"

[[bin]]
name = "move"
path = "src/bin/move.rs"

[dependencies]
clap = { version = "4.5.53", features = ["derive"] }
filetime = "0.2.26"
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,30 @@ mode = "auto"

**See [configuration.md](docs/configuration.md) for all options and use cases.**

## The `move` command

This project also ships a `move` binary — a modern `mv` replacement that reuses the same engine. It renames in place when possible (atomic, instant) and falls back to a copy + source removal only when the rename can't be done atomically (across filesystems, or when `--exclude` leaves part of a directory behind).

```bash
# Rename / move a file
move old.txt new.txt

# Move into a directory
move a.txt b.txt dest/
move -t dest/ a.txt b.txt

# Don't overwrite, prompt, or only move newer files
move -n src dst # never overwrite
move -i src dst # prompt before overwrite
move -u src dst # only if source is newer
move -b src dst # back up an existing destination

# Cross-filesystem move that skips logs and preserves attributes
move -e '*.log' -p project/ /mnt/backup/project/
```

Key options: `-t/--target-directory`, `-i/--interactive`, `-f/--force`, `-n/--no-clobber`, `-u/--update`, `-b/--backup`, `-v/--verbose`, and on the cross-device fallback `-j`, `--reflink`, `-e/--exclude`, `-p/--preserve`. Run `move --help` for the full reference. Both `copy` and `move` are installed together by every install method above.

## Performance

`copy` is built for speed. Quick comparison:
Expand Down
14 changes: 14 additions & 0 deletions README_CRATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,20 @@ mode = "auto"

**See [configuration.md](docs/configuration.md) for all options and use cases.**

## The `move` command

This crate also ships a `move` binary — a modern `mv` replacement that reuses the same engine. It renames in place when possible and falls back to a copy + source removal only across filesystems (or when `--exclude` leaves part of a directory behind).

```bash
move old.txt new.txt # rename
move a.txt b.txt dest/ # move into a directory
move -t dest/ a.txt b.txt # target-directory form
move -n / -i / -u / -b src dst
move -e '*.log' -p project/ /mnt/backup/project/ # cross-fs, skip logs, preserve attrs
```

Options: `-t`, `-i`, `-f`, `-n`, `-u`, `-b`, `-v`, plus `-j`, `--reflink`, `-e`, `-p` on the cross-device fallback. Run `move --help` for details. `copy` and `move` install together.

## Performance

`copy` is built for speed. Quick comparison:
Expand Down
12 changes: 8 additions & 4 deletions guix.scm
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,20 @@ directory = ~s~%" #$%vendor)))
(lambda _
(install-file "target/release/copy"
(string-append #$output "/bin"))
(install-file "target/release/move"
(string-append #$output "/bin"))
(install-file "LICENSE"
(string-append #$output "/share/licenses/copy"))
(install-file "LICENSES/MIT.txt"
(string-append #$output "/share/licenses/copy")))))))
(native-inputs (list rust (list rust "cargo")))
(home-page "https://github.com/UnbreakableMJ/copy")
(synopsis "A modern, fast file copying tool")
(synopsis "Modern, fast file copy and move tools")
(description
"copy is a modern, parallel replacement for the @command{cp} command on
Linux. It copies files and directories in parallel with progress bars, can
"This package provides @command{copy} and @command{move}, modern parallel
replacements for the @command{cp} and @command{mv} commands on Linux.
@command{copy} copies files and directories in parallel with progress bars, can
resume interrupted transfers, preserves attributes, and supports reflink and
gitignore-style exclude patterns.")
gitignore-style exclude patterns. @command{move} renames in place when possible
and otherwise falls back to a copy plus source removal.")
(license license:gpl3+))
63 changes: 30 additions & 33 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#!/bin/bash
set -e
# copy installer script
# copy + move installer script
# Usage: curl -fsSL https://raw.githubusercontent.com/UnbreakableMJ/copy/main/install.sh | bash

REPO="UnbreakableMJ/copy"
INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}"
BINARY_NAME="copy"
BINARIES="copy move"

# Colors
RED='\033[0;31m'
Expand Down Expand Up @@ -60,8 +60,8 @@ get_latest_version() {
}

# Download and install
install_copy() {
local platform version download_url tarball_name
install_binaries() {
local platform version

platform=$(detect_platform)
info "Detected platform: $platform"
Expand All @@ -72,36 +72,32 @@ install_copy() {
fi
info "Latest version: v$version"

tarball_name="copy-${platform}.tar.gz"
download_url="https://github.com/$REPO/releases/download/v${version}/${tarball_name}"

info "Downloading from: $download_url"

# Create temporary directory
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
mkdir -p "$INSTALL_DIR"

# Download
if command -v curl &> /dev/null; then
if ! curl -fsSL "$download_url" -o "$tmp_dir/$tarball_name"; then
error "Failed to download from: $download_url"
fi
elif command -v wget &> /dev/null; then
if ! wget -q "$download_url" -O "$tmp_dir/$tarball_name"; then
error "Failed to download from: $download_url"
local bin tarball_name download_url
for bin in $BINARIES; do
tarball_name="${bin}-${platform}.tar.gz"
download_url="https://github.com/$REPO/releases/download/v${version}/${tarball_name}"
info "Downloading $bin from: $download_url"

if command -v curl &> /dev/null; then
if ! curl -fsSL "$download_url" -o "$tmp_dir/$tarball_name"; then
error "Failed to download from: $download_url"
fi
elif command -v wget &> /dev/null; then
if ! wget -q "$download_url" -O "$tmp_dir/$tarball_name"; then
error "Failed to download from: $download_url"
fi
fi
fi

# Extract
info "Extracting to $INSTALL_DIR..."
tar xzf "$tmp_dir/$tarball_name" -C "$tmp_dir"

# Install
mkdir -p "$INSTALL_DIR"
cp "$tmp_dir/$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME"
chmod +x "$INSTALL_DIR/$BINARY_NAME"

info "✓ copy v$version installed to $INSTALL_DIR/$BINARY_NAME"
tar xzf "$tmp_dir/$tarball_name" -C "$tmp_dir"
cp "$tmp_dir/$bin" "$INSTALL_DIR/$bin"
chmod +x "$INSTALL_DIR/$bin"
info "✓ $bin v$version installed to $INSTALL_DIR/$bin"
done

# Check if in PATH
if ! echo "$PATH" | grep -q "$INSTALL_DIR"; then
Expand All @@ -113,9 +109,10 @@ install_copy() {
fi

# Verify installation
if "$INSTALL_DIR/$BINARY_NAME" --version &> /dev/null; then
if "$INSTALL_DIR/copy" --version &> /dev/null && "$INSTALL_DIR/move" --version &> /dev/null; then
info "Installation verified successfully!"
"$INSTALL_DIR/$BINARY_NAME" --version
"$INSTALL_DIR/copy" --version
"$INSTALL_DIR/move" --version
else
warn "Installation completed but verification failed"
fi
Expand All @@ -124,14 +121,14 @@ install_copy() {
# Main
main() {
echo "╔═══════════════════════════════════╗"
echo "║ copy - Modern File Copy Tool ║"
echo "║ copy & move file utilities ║"
echo "╚═══════════════════════════════════╝"
echo ""

install_copy
install_binaries

echo ""
echo "To get started, run: copy --help"
echo "To get started, run: copy --help or move --help"
}

main
5 changes: 4 additions & 1 deletion nix/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ rustPlatform.buildRustPackage {

cargoLock.lockFile = ../Cargo.lock; # committed lockfile -> no vendor hash

# buildRustPackage installs every [[bin]] in the crate, so both `copy` and
# `move` land in result/bin.

# Default features need no system libraries. SELinux xattr preservation is
# opt-in and requires libselinux development headers; to enable it, uncomment:
# buildFeatures = [ "selinux-support" ];
# buildInputs = [ libselinux ];

meta = {
description = "A modern, fast file copying tool";
description = "Modern, fast file copy and move tools";
homepage = "https://github.com/UnbreakableMJ/copy";
license = lib.licenses.gpl3Plus;
mainProgram = "copy";
Expand Down
5 changes: 3 additions & 2 deletions packaging/aur/.SRCINFO
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
pkgbase = copy
pkgdesc = A modern, fast file copying tool
pkgdesc = Modern, fast file copy and move tools
pkgver = 0.1.5
pkgrel = 1
url = https://github.com/UnbreakableMJ/copy
arch = x86_64
arch = aarch64
license = MIT
license = GPL-3.0-or-later
makedepends = cargo
depends = gcc-libs
provides = move
source = copy-0.1.5.tar.gz::https://github.com/UnbreakableMJ/copy/archive/refs/tags/v0.1.5.tar.gz
sha256sums = 5f9e42b24b63cfd4071a6160ed45603f34553fe48bc22fd514dce740026af614

Expand Down
6 changes: 4 additions & 2 deletions packaging/aur/PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
pkgname=copy
pkgver=0.1.5
pkgrel=1
pkgdesc="A modern, fast file copying tool"
pkgdesc="Modern, fast file copy and move tools"
arch=('x86_64' 'aarch64')
url="https://github.com/UnbreakableMJ/copy"
license=('GPL-3.0-or-later')
depends=('gcc-libs')
makedepends=('cargo')
provides=('move')
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('5f9e42b24b63cfd4071a6160ed45603f34553fe48bc22fd514dce740026af614')

Expand Down Expand Up @@ -38,7 +39,8 @@ check() {

package() {
cd "$pkgname-$pkgver"
install -Dm0755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname"
install -Dm0755 "target/release/copy" "$pkgdir/usr/bin/copy"
install -Dm0755 "target/release/move" "$pkgdir/usr/bin/move"
install -Dm0644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
install -Dm0644 LICENSES/MIT.txt "$pkgdir/usr/share/licenses/$pkgname/MIT.txt"
}
Loading