diff --git a/cmd/stbak/cmd/recovery_restore.go b/cmd/stbak/cmd/recovery_restore.go index 813066e..91b157b 100644 --- a/cmd/stbak/cmd/recovery_restore.go +++ b/cmd/stbak/cmd/recovery_restore.go @@ -34,74 +34,95 @@ var recoveryRestoreCmd = &cobra.Command{ boil.DebugMode = true } - f, isRegular, err := openTapeReadOnly(viper.GetString(tapeFlag)) - if err != nil { - return err - } - defer f.Close() + return restoreFromRecordAndBlock( + viper.GetString(tapeFlag), + viper.GetInt(recordSizeFlag), + viper.GetInt(recordFlag), + viper.GetInt(blockFlag), + viper.GetString(dstFlag), + viper.GetBool(previewFlag), + true, + ) + }, +} - var tr *tar.Reader - if isRegular { - // Seek to record and block - if _, err := f.Seek(int64((viper.GetInt(recordSizeFlag)*controllers.BlockSize*viper.GetInt(recordFlag))+viper.GetInt(blockFlag)*controllers.BlockSize), io.SeekStart); err != nil { - return err - } +func restoreFromRecordAndBlock( + tape string, + recordSize int, + record int, + block int, + dst string, + preview bool, + showHeader bool, +) error { + f, isRegular, err := openTapeReadOnly(tape) + if err != nil { + return err + } + defer f.Close() - tr = tar.NewReader(f) - } else { - // Seek to record - if err := controllers.SeekToRecordOnTape(f, int32(viper.GetInt(recordFlag))); err != nil { - return err - } - - // Seek to block - br := bufio.NewReaderSize(f, controllers.BlockSize*viper.GetInt(recordSizeFlag)) - if _, err := br.Read(make([]byte, viper.GetInt(blockFlag)*controllers.BlockSize)); err != nil { - return err - } - - tr = tar.NewReader(br) - } - - hdr, err := tr.Next() - if err != nil { + var tr *tar.Reader + if isRegular { + // Seek to record and block + if _, err := f.Seek(int64((recordSize*controllers.BlockSize*record)+block*controllers.BlockSize), io.SeekStart); err != nil { return err } + tr = tar.NewReader(f) + } else { + // Seek to record + if err := controllers.SeekToRecordOnTape(f, int32(record)); err != nil { + return err + } + + // Seek to block + br := bufio.NewReaderSize(f, controllers.BlockSize*recordSize) + if _, err := br.Read(make([]byte, block*controllers.BlockSize)); err != nil { + return err + } + + tr = tar.NewReader(br) + } + + hdr, err := tr.Next() + if err != nil { + return err + } + + if showHeader { if err := formatting.PrintCSV(formatting.TARHeaderCSV); err != nil { return err } - if err := formatting.PrintCSV(formatting.GetTARHeaderAsCSV(int64(viper.GetInt(recordFlag)), int64(viper.GetInt(blockFlag)), hdr)); err != nil { + if err := formatting.PrintCSV(formatting.GetTARHeaderAsCSV(int64(record), int64(block), hdr)); err != nil { + return err + } + } + + if !preview { + if dst == "" { + dst = filepath.Base(hdr.Name) + } + + if hdr.Typeflag == tar.TypeDir { + return os.MkdirAll(dst, hdr.FileInfo().Mode()) + } + + dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, hdr.FileInfo().Mode()) + if err != nil { return err } - if !viper.GetBool(previewFlag) { - dst := viper.GetString(dstFlag) - if dst == "" { - dst = filepath.Base(hdr.Name) - } - - if hdr.Typeflag == tar.TypeDir { - return os.MkdirAll(dst, hdr.FileInfo().Mode()) - } - - dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, hdr.FileInfo().Mode()) - if err != nil { - return err - } - - if err := dstFile.Truncate(0); err != nil { - return err - } - - if _, err := io.Copy(dstFile, tr); err != nil { - return err - } + if err := dstFile.Truncate(0); err != nil { + return err } - return nil - }, + if _, err := io.Copy(dstFile, tr); err != nil { + return err + } + } + + return nil } func init() { diff --git a/cmd/stbak/cmd/restore.go b/cmd/stbak/cmd/restore.go new file mode 100644 index 0000000..d4e86f0 --- /dev/null +++ b/cmd/stbak/cmd/restore.go @@ -0,0 +1,124 @@ +package cmd + +import ( + "archive/tar" + "context" + "database/sql" + "path" + "path/filepath" + "strings" + + "github.com/pojntfx/stfs/pkg/converters" + models "github.com/pojntfx/stfs/pkg/db/sqlite/models/metadata" + "github.com/pojntfx/stfs/pkg/formatting" + "github.com/pojntfx/stfs/pkg/persisters" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/volatiletech/sqlboiler/v4/boil" +) + +const ( + flattenFlag = "flatten" +) + +var restoreCmd = &cobra.Command{ + Use: "restore", + Aliases: []string{"r"}, + Short: "Restore a file or directory", + RunE: func(cmd *cobra.Command, args []string) error { + if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { + return err + } + + if viper.GetBool(verboseFlag) { + boil.DebugMode = true + } + + metadataPersister := persisters.NewMetadataPersister(viper.GetString(metadataFlag)) + if err := metadataPersister.Open(); err != nil { + return err + } + + headersToRestore := []*models.Header{} + src := strings.TrimSuffix(viper.GetString(srcFlag), "/") + dbhdr, err := metadataPersister.GetHeader(context.Background(), src) + if err != nil { + if err == sql.ErrNoRows { + src = src + "/" + + dbhdr, err = metadataPersister.GetHeader(context.Background(), src) + if err != nil { + return err + } + } else { + return err + } + } + headersToRestore = append(headersToRestore, dbhdr) + + // If the header refers to a directory, get it's children + if dbhdr.Typeflag == tar.TypeDir { + dbhdrs, err := metadataPersister.GetHeaderChildren(context.Background(), src) + if err != nil { + return err + } + + headersToRestore = append(headersToRestore, dbhdrs...) + } + + for i, dbhdr := range headersToRestore { + if i == 0 { + if err := formatting.PrintCSV(formatting.TARHeaderCSV); err != nil { + return err + } + } + + hdr, err := converters.DBHeaderToTarHeader(dbhdr) + if err != nil { + return err + } + + if err := formatting.PrintCSV(formatting.GetTARHeaderAsCSV(dbhdr.Record, dbhdr.Block, hdr)); err != nil { + return err + } + + dst := dbhdr.Name + if viper.GetString(dstFlag) != "" { + if viper.GetBool(flattenFlag) { + dst = viper.GetString(dstFlag) + } else { + dst = filepath.Join(viper.GetString(dstFlag), strings.TrimPrefix(dst, viper.GetString(srcFlag))) + + if strings.TrimSuffix(dst, "/") == strings.TrimSuffix(viper.GetString(dstFlag), "/") { + dst = filepath.Join(dst, path.Base(dbhdr.Name)) // Append the name so we don't overwrite + } + } + } + + if err := restoreFromRecordAndBlock( + viper.GetString(tapeFlag), + viper.GetInt(recordSizeFlag), + int(dbhdr.Record), + int(dbhdr.Block), + dst, + false, + false, + ); err != nil { + return err + } + } + + return nil + }, +} + +func init() { + restoreCmd.PersistentFlags().IntP(recordSizeFlag, "e", 20, "Amount of 512-bit blocks per record") + restoreCmd.PersistentFlags().StringP(srcFlag, "s", "", "File or directory to restore") + restoreCmd.PersistentFlags().StringP(dstFlag, "d", "", "File or directory restore to (archived name by default)") + restoreCmd.PersistentFlags().BoolP(flattenFlag, "f", false, "Ignore the folder hierarchy on the tape or tar file") + + viper.AutomaticEnv() + + rootCmd.AddCommand(restoreCmd) +}