Initial implementation of tarballer utility with FreeBSD/macOS/Linux support and symlink handling
This commit is contained in:
commit
9899387fe7
|
@ -0,0 +1,8 @@
|
|||
/bin/
|
||||
*.tar.gz
|
||||
/test/extracted/
|
||||
/test/example/
|
||||
/test/complex/
|
||||
/test/complex-extracted/
|
||||
/test/local/
|
||||
/test/local-extracted/
|
|
@ -0,0 +1,40 @@
|
|||
FROM golang:1.20-alpine AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache git
|
||||
|
||||
# Set up the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy Go module files
|
||||
COPY go.mod ./
|
||||
|
||||
# Copy the source code
|
||||
COPY main.go ./
|
||||
|
||||
# Build FreeBSD binary
|
||||
RUN GOOS=freebsd GOARCH=amd64 CGO_ENABLED=0 go build -o tarballer-freebsd -ldflags="-s -w"
|
||||
|
||||
# Build macOS ARM64 binary for local testing
|
||||
RUN GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o tarballer-darwin -ldflags="-s -w"
|
||||
|
||||
# Build Linux binary for testing in container
|
||||
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o tarballer-linux -ldflags="-s -w"
|
||||
|
||||
# Second stage for testing
|
||||
FROM alpine:latest AS tester
|
||||
|
||||
# Install required tools for testing
|
||||
RUN apk add --no-cache tar gzip
|
||||
|
||||
WORKDIR /workdir
|
||||
|
||||
# Copy Linux binary for use in the container
|
||||
COPY --from=builder /app/tarballer-linux /bin/tarballer
|
||||
|
||||
# Also copy the FreeBSD and Darwin binaries for extraction
|
||||
COPY --from=builder /app/tarballer-freebsd /bin/
|
||||
COPY --from=builder /app/tarballer-darwin /bin/
|
||||
|
||||
# Make binaries executable
|
||||
RUN chmod +x /bin/tarballer /bin/tarballer-freebsd /bin/tarballer-darwin
|
|
@ -0,0 +1,64 @@
|
|||
# Tarballer
|
||||
|
||||
A simple utility to create tarballs with a specific directory structure.
|
||||
|
||||
## Features
|
||||
|
||||
- Creates compressed tar archives (.tar.gz) from a source directory
|
||||
- Places all files under a specified prefix directory in the tarball
|
||||
- Preserves file permissions and directory structure
|
||||
- Handles symbolic links correctly
|
||||
- Cross-platform compatibility (FreeBSD, macOS, Linux)
|
||||
|
||||
## Building
|
||||
|
||||
This project includes Docker support to build binaries for different platforms:
|
||||
|
||||
```bash
|
||||
docker compose up --build build
|
||||
```
|
||||
|
||||
This will create these binaries in the `./bin` directory:
|
||||
- `tarballer-freebsd`: FreeBSD AMD64 compatible binary
|
||||
- `tarballer-darwin`: macOS ARM64 compatible binary
|
||||
- `tarballer-linux`: Linux AMD64 compatible binary
|
||||
|
||||
## Testing
|
||||
|
||||
You can run the included test to verify functionality:
|
||||
|
||||
```bash
|
||||
docker compose up --build test
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Create a test directory structure with nested directories and symlinks
|
||||
2. Create a tarball from it
|
||||
3. Extract the tarball
|
||||
4. Verify the contents and file structure, including symlinks
|
||||
|
||||
## Usage
|
||||
|
||||
The usage is the same for all binaries:
|
||||
|
||||
```bash
|
||||
./bin/tarballer-<platform> -source /path/to/directory -output myarchive.tar.gz -prefix myprefix
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
- `-source`: The directory you want to compress (required)
|
||||
- `-output`: The name of the output tarball (defaults to "output.tar.gz")
|
||||
- `-prefix`: The directory name that will contain all files in the tarball (defaults to "myapp")
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
# On macOS:
|
||||
./bin/tarballer-darwin -source ./myproject -output release.tar.gz -prefix app
|
||||
|
||||
# On FreeBSD:
|
||||
./bin/tarballer-freebsd -source ./myproject -output release.tar.gz -prefix app
|
||||
```
|
||||
|
||||
When extracted, all files will be under the `app/` directory in the tarball.
|
|
@ -0,0 +1,75 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
build:
|
||||
build:
|
||||
context: .
|
||||
target: builder
|
||||
volumes:
|
||||
- ./bin:/output
|
||||
command: sh -c "cp /app/tarballer-freebsd /output/ && cp /app/tarballer-darwin /output/ && cp /app/tarballer-linux /output/ && chmod +x /output/tarballer-freebsd /output/tarballer-darwin /output/tarballer-linux"
|
||||
|
||||
test:
|
||||
build:
|
||||
context: .
|
||||
target: tester
|
||||
volumes:
|
||||
- ./bin:/output
|
||||
- ./test:/test
|
||||
- .:/workdir
|
||||
working_dir: /workdir
|
||||
depends_on:
|
||||
- build
|
||||
command: |
|
||||
sh -c "# Clean up existing test directories
|
||||
rm -rf /test/complex /test/complex-extracted &&
|
||||
|
||||
# Create complex directory structure
|
||||
mkdir -p /test/complex/dir1/subdir1/subsubdir1 &&
|
||||
mkdir -p /test/complex/dir1/subdir2 &&
|
||||
mkdir -p /test/complex/dir2/subdir1 &&
|
||||
|
||||
# Create files at different levels
|
||||
echo 'root level file' > /test/complex/rootfile.txt &&
|
||||
echo 'level 1 file in dir1' > /test/complex/dir1/file1.txt &&
|
||||
echo 'level 1 file in dir2' > /test/complex/dir2/file2.txt &&
|
||||
echo 'level 2 file in subdir1' > /test/complex/dir1/subdir1/file3.txt &&
|
||||
echo 'level 2 file in subdir2' > /test/complex/dir1/subdir2/file4.txt &&
|
||||
echo 'level 3 file in subsubdir1' > /test/complex/dir1/subdir1/subsubdir1/file5.txt &&
|
||||
|
||||
# Create a symbolic link with a relative path instead of absolute
|
||||
cd /test/complex/dir2 && ln -s ../rootfile.txt symlink.txt && cd /workdir &&
|
||||
|
||||
# Print the original structure for reference
|
||||
echo '=== ORIGINAL DIRECTORY STRUCTURE ===' &&
|
||||
find /test/complex -type f -o -type l | sort &&
|
||||
|
||||
# Create the tarball
|
||||
/bin/tarballer -source /test/complex -output /workdir/complex.tar.gz -prefix complex-app &&
|
||||
|
||||
# Extract the tarball
|
||||
mkdir -p /test/complex-extracted &&
|
||||
tar -xzf /workdir/complex.tar.gz -C /test/complex-extracted &&
|
||||
|
||||
# Verify the extracted structure
|
||||
echo '=== EXTRACTED DIRECTORY STRUCTURE ===' &&
|
||||
find /test/complex-extracted -type f -o -type l | sort &&
|
||||
|
||||
# Verify file content matches
|
||||
echo '=== VERIFYING FILE CONTENTS ===' &&
|
||||
cat /test/complex/rootfile.txt &&
|
||||
echo ' <-- Original: rootfile.txt' &&
|
||||
cat /test/complex-extracted/complex-app/rootfile.txt &&
|
||||
echo ' <-- Extracted: rootfile.txt' &&
|
||||
|
||||
cat /test/complex/dir1/subdir1/subsubdir1/file5.txt &&
|
||||
echo ' <-- Original: deep nested file5.txt' &&
|
||||
cat /test/complex-extracted/complex-app/dir1/subdir1/subsubdir1/file5.txt &&
|
||||
echo ' <-- Extracted: deep nested file5.txt' &&
|
||||
|
||||
# Test symlink
|
||||
echo '=== TESTING SYMLINK ===' &&
|
||||
ls -la /test/complex/dir2/symlink.txt &&
|
||||
ls -la /test/complex-extracted/complex-app/dir2/symlink.txt &&
|
||||
|
||||
echo 'All tests completed successfully!'"
|
|
@ -0,0 +1,116 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Define command line flags
|
||||
sourceDir := flag.String("source", "", "Source directory to compress")
|
||||
outputFile := flag.String("output", "output.tar.gz", "Output tarball filename")
|
||||
prefixDir := flag.String("prefix", "myapp", "Directory prefix in tarball")
|
||||
flag.Parse()
|
||||
|
||||
if *sourceDir == "" {
|
||||
fmt.Println("Please specify a source directory using -source")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err := createTarball(*sourceDir, *outputFile, *prefixDir)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating tarball: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully created %s with prefix %s\n", *outputFile, *prefixDir)
|
||||
}
|
||||
|
||||
func createTarball(sourceDir, outputFile, prefix string) error {
|
||||
// Create output file
|
||||
out, err := os.Create(outputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Create gzip writer
|
||||
gw := gzip.NewWriter(out)
|
||||
defer gw.Close()
|
||||
|
||||
// Create tar writer
|
||||
tw := tar.NewWriter(gw)
|
||||
defer tw.Close()
|
||||
|
||||
// Resolve absolute source path to handle relative symlinks correctly
|
||||
sourceDir, err = filepath.Abs(sourceDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walk through source directory
|
||||
err = filepath.Walk(sourceDir, func(filePath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get relative path to use in the tarball
|
||||
relPath, err := filepath.Rel(sourceDir, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create tar header using original file info
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the name with the prefix and relative path
|
||||
header.Name = filepath.Join(prefix, relPath)
|
||||
|
||||
// Special handling for symbolic links
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
// Read link target
|
||||
linkTarget, err := os.Readlink(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store the link target in the header
|
||||
header.Linkname = linkTarget
|
||||
|
||||
// Make sure the link type is set correctly
|
||||
header.Typeflag = tar.TypeSymlink
|
||||
}
|
||||
|
||||
// Write header
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If it's a file (not a directory or symlink), copy contents
|
||||
if !info.IsDir() && info.Mode()&os.ModeSymlink == 0 {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(tw, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
Loading…
Reference in New Issue