diff --git a/README.md b/README.md index f31f61c..1910f37 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ $ go install github.com/NodyHub/golinkwrite@latest ### Help ```shell -$ golinkwrite +$ golinkwrite -h Usage: golinkwrite [flags] -Create a tar archive containing a symbolic link to a provided target and a provided file. +Create a tar archive containing a provided file and a symlink that points to the write destination. Arguments: Input file. @@ -36,23 +36,47 @@ Arguments: Output file. Flags: - -h, --help Show context-sensitive help. - -v, --verbose Enable verbose output. + -h, --help Show context-sensitive help. + -t, --type="tar" Type of the archive. (tar, zip) + -v, --verbose Enable verbose output. ``` ### In Action +### Tar + ```shell -$ echo 'Hello Alice :wave:!' | tee rabbit_hole.txt +(main[2]) ~/git/go-link-write% echo 'Hello Alice :wave:!' | tee rabbit_hole.txt Hello Alice :wave:! -$ golinkwrite -v rabbit_hole.txt /tmp/hi.txt alice.tar -time=2024-08-09T19:11:35.266+02:00 level=DEBUG msg="command line parameters" cli="{Input:rabbit_hole.txt Target:/tmp/hi.txt Output:alice.tar Verbose:true}" -time=2024-08-09T19:11:35.266+02:00 level=DEBUG msg="input permissions" perm=-rw-r--r-- -time=2024-08-09T19:11:35.266+02:00 level=DEBUG msg="input size" size=20 -time=2024-08-09T19:11:35.266+02:00 level=INFO msg="tar file created" output=alice.tar +(main[2]) ~/git/go-link-write% golinkwrite -v rabbit_hole.txt /tmp/hi.txt alice.tar +time=2024-09-20T11:52:39.211+02:00 level=DEBUG msg="command line parameters" cli="{Input:rabbit_hole.txt Target:/tmp/hi.txt Output:alice.tar Type:tar Verbose:true}" +time=2024-09-20T11:52:39.212+02:00 level=DEBUG msg="input permissions" perm=-rw-r--r-- +time=2024-09-20T11:52:39.213+02:00 level=DEBUG msg="input size" size=20 +time=2024-09-20T11:52:39.213+02:00 level=INFO msg="archive created" output=alice.tar -$ tar ztvf alice.tar +(main[2]) ~/git/go-link-write% tar ztvf alice.tar lrw-r--r-- 0 0 0 0 1 Jan 1970 rabbit_hole.txt -> /tmp/hi.txt -rw-r--r-- 0 0 0 20 1 Jan 1970 rabbit_hole.txt ``` + +### Zip + +```shell +(main[2]) ~/git/go-link-write% echo 'Hello Alice :wave:!' | tee rabbit_hole.txt +Hello Alice :wave:! + +(main[2]) ~/git/go-link-write% golinkwrite -t zip -v rabbit_hole.txt /tmp/hi.txt alice.zip +time=2024-09-20T11:54:12.300+02:00 level=DEBUG msg="command line parameters" cli="{Input:rabbit_hole.txt Target:/tmp/hi.txt Output:alice.zip Type:zip Verbose:true}" +time=2024-09-20T11:54:12.300+02:00 level=DEBUG msg="input permissions" perm=-rw-r--r-- +time=2024-09-20T11:54:12.301+02:00 level=DEBUG msg="input size" size=20 +time=2024-09-20T11:54:12.301+02:00 level=INFO msg="archive created" output=alice.zip +(main[2]) ~/git/go-link-write% unzip -l alice.zip +Archive: alice.zip + Length Date Time Name +--------- ---------- ----- ---- + 11 09-20-2024 11:54 rabbit_hole.txt + 20 08-09-2024 19:10 rabbit_hole.txt +--------- ------- + 31 2 files +``` diff --git a/main.go b/main.go index 6aec7af..f3abf27 100644 --- a/main.go +++ b/main.go @@ -2,23 +2,29 @@ package main import ( "archive/tar" + "archive/zip" + "bytes" + "fmt" + "io" + "io/fs" "log/slog" "os" + "time" "github.com/alecthomas/kong" ) -type CLI struct { +var CLI struct { Input string `arg:"" name:"input" help:"Input file."` Target string `arg:"" name:"target" help:"Target destination in the filesystem."` Output string `arg:"" name:"output" help:"Output file."` + Type string `short:"t" name:"type" default:"tar" help:"Type of the archive. (tar, zip)"` Verbose bool `short:"v" name:"verbose" help:"Enable verbose output."` } func main() { - cli := CLI{} // ctx := kong.Parse(&cli, - kong.Parse(&cli, + kong.Parse(&CLI, kong.Name("golinkwrite"), kong.Description("Create a tar archive containing a provided file and a symlink that points to the write destination."), kong.UsageOnError(), @@ -27,7 +33,7 @@ func main() { // Check for verbose output logLevel := slog.LevelError - if cli.Verbose { + if CLI.Verbose { logLevel = slog.LevelDebug } @@ -37,10 +43,10 @@ func main() { })) // log parameters - logger.Debug("command line parameters", "cli", cli) + logger.Debug("command line parameters", "cli", CLI) // get file mode from input file - fileInfo, err := os.Stat(cli.Input) + fileInfo, err := os.Stat(CLI.Input) if err != nil { logger.Error("failed to get file info", "error", err) os.Exit(1) @@ -48,7 +54,7 @@ func main() { logger.Debug("input permissions", "perm", fileInfo.Mode().Perm()) // read the input file - content, err := os.ReadFile(cli.Input) + content, err := os.ReadFile(CLI.Input) if err != nil { logger.Error("failed to read input file", "error", err) os.Exit(1) @@ -56,45 +62,127 @@ func main() { logger.Debug("input size", "size", len(content)) // prepare the tar file - out, err := os.Create(cli.Output) + out, err := os.Create(CLI.Output) if err != nil { logger.Error("failed to create output file", "error", err) os.Exit(1) } + switch CLI.Type { + + case "tar": + if err := createTar(out, fileInfo, content); err != nil { + panic(fmt.Errorf("failed to create tar file: %w", err)) + } + + case "zip": + if err := createZip(out, fileInfo, content); err != nil { + panic(fmt.Errorf("failed to create zip file: %w", err)) + } + + default: + panic(fmt.Errorf("unsupported archive type: %s", CLI.Type)) + + } + + logger.Info("archive created", "output", CLI.Output) +} + +func createTar(out io.Writer, fi fs.FileInfo, content []byte) error { + // create writer add a file and a directory writer := tar.NewWriter(out) - defer writer.Close() + defer func() { + if err := writer.Close(); err != nil { + panic(fmt.Errorf("failed to close tar writer: %w", err)) + } + }() // add a symlink to the tar file header := &tar.Header{ - Name: cli.Input, - Linkname: cli.Target, - Mode: int64(fileInfo.Mode().Perm()), + Name: CLI.Input, + Linkname: CLI.Target, + Mode: int64(fi.Mode().Perm()), Typeflag: tar.TypeSymlink, Size: 0, } - err = writer.WriteHeader(header) - if err != nil { - logger.Error("failed to write header to tar file", "error", err) - os.Exit(1) + if err := writer.WriteHeader(header); err != nil { + return fmt.Errorf("failed to write header to tar file: %w", err) } - // add a file + // add a file header header = &tar.Header{ - Name: cli.Input, - Mode: int64(fileInfo.Mode().Perm()), + Name: CLI.Input, + Mode: int64(fi.Mode().Perm()), Size: int64(len(content)), } - err = writer.WriteHeader(header) + if err := writer.WriteHeader(header); err != nil { + return fmt.Errorf("failed to write header to tar file: %w", err) + } + + // add the file content + if _, err := writer.Write([]byte(content)); err != nil { + return fmt.Errorf("failed to write file to tar file: %w", err) + } + + return nil +} + +func createZip(out io.Writer, fi fs.FileInfo, content []byte) error { + + // create zip writer + writer := zip.NewWriter(out) + defer func() { + if err := writer.Close(); err != nil { + panic(fmt.Errorf("failed to close zip writer: %w", err)) + } + }() + + // create a new file header + zipHeader := &zip.FileHeader{ + Name: CLI.Input, + Method: zip.Store, + Modified: time.Now(), + } + zipHeader.SetMode(os.ModeSymlink | 0755) + + // create a new file writer + fw, err := writer.CreateHeader(zipHeader) if err != nil { - logger.Error("failed to write header to tar file", "error", err) - os.Exit(1) + return fmt.Errorf("failed to create zip header for symlink %s: %s", CLI.Input, err) } - _, err = writer.Write([]byte(content)) + + // write the symlink to the zip archive + if _, err := fw.Write([]byte(CLI.Target)); err != nil { + return fmt.Errorf("failed to write symlink target %s to zip archive: %s", CLI.Target, err) + } + + // create a new file header + zipHeader, err = zip.FileInfoHeader(fi) if err != nil { - logger.Error("failed to write to tar file", "error", err) - os.Exit(1) + return fmt.Errorf("failed to create file header: %s", err) + } + + // set the name of the file + zipHeader.Name = CLI.Input + + // set the method of compression + zipHeader.Method = zip.Deflate + + // create a new file writer + zw, err := writer.CreateHeader(zipHeader) + if err != nil { + return fmt.Errorf("failed to create zip file header: %s", err) } - logger.Info("tar file created", "output", cli.Output) + + // write the file to the zip archive + + // create reader for byte slice + reader := bytes.NewReader(content) + + if _, err := io.Copy(zw, reader); err != nil { + return fmt.Errorf("failed to write file to zip archive: %s", err) + } + + return nil }