From a51dfe9a7df51d4f5041aeb7e95afa6be5d188ed Mon Sep 17 00:00:00 2001 From: Abdullahi Yunus Date: Tue, 29 Oct 2024 11:56:59 +0100 Subject: [PATCH] chanbackup: archive old channel backups In this commit, we first check if a previous backup file exists, if it does we copy it to archive folder before replacing it with a new backup file. We also added a test for archiving chan backups. --- chanbackup/backupfile.go | 85 +++++++++++++++++++++++++++++++++++ chanbackup/backupfile_test.go | 67 +++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/chanbackup/backupfile.go b/chanbackup/backupfile.go index 5ba6aa756..9f9b7da5e 100644 --- a/chanbackup/backupfile.go +++ b/chanbackup/backupfile.go @@ -2,10 +2,13 @@ package chanbackup import ( "fmt" + "io" "os" "path/filepath" + "time" "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnrpc" ) const ( @@ -17,6 +20,10 @@ const ( // file that we'll use to atomically update the primary back up file // when new channel are detected. DefaultTempBackupFileName = "temp-dont-use.backup" + + // DefaultChanBackupArchiveDirName is the default name of the directory + // that we'll use to store old channel backups. + DefaultChanBackupArchiveDirName = "chan-backup-archives" ) var ( @@ -44,6 +51,9 @@ type MultiFile struct { // tempFile is an open handle to the temp back up file. tempFile *os.File + + // archiveDir is the directory where we'll store old channel backups. + archiveDir string } // NewMultiFile create a new multi-file instance at the target location on the @@ -56,10 +66,14 @@ func NewMultiFile(fileName string) *MultiFile { tempFileName := filepath.Join( backupFileDir, DefaultTempBackupFileName, ) + archiveDir := filepath.Join( + backupFileDir, DefaultChanBackupArchiveDirName, + ) return &MultiFile{ fileName: fileName, tempFileName: tempFileName, + archiveDir: archiveDir, } } @@ -117,6 +131,12 @@ func (b *MultiFile) UpdateAndSwap(newBackup PackedMulti) error { return fmt.Errorf("unable to close file: %w", err) } + // Archive the old channel backup file before replacing. + if err := b.createArchiveFile(); err != nil { + return fmt.Errorf("unable to archive old channel "+ + "backup file: %w", err) + } + // Finally, we'll attempt to atomically rename the temporary file to // the main back up file. If this succeeds, then we'll only have a // single file on disk once this method exits. @@ -147,3 +167,68 @@ func (b *MultiFile) ExtractMulti(keyChain keychain.KeyRing) (*Multi, error) { packedMulti := PackedMulti(multiBytes) return packedMulti.Unpack(keyChain) } + +// createArchiveFile creates an archive file with a timestamped name in the +// specified archive directory, and copies the contents of the main backup file +// to the new archive file. +func (b *MultiFile) createArchiveFile() error { + // We check for old channel backup file first. + oldFileExists := lnrpc.FileExists(b.fileName) + if !oldFileExists { + log.Debug("No old channel backup file to archive") + return nil + } + + log.Infof("Archiving old channel backup to %v", b.archiveDir) + + // Generate archive file path with timestamped name. + baseFileName := filepath.Base(b.fileName) + timestamp := time.Now().Format("2006-01-02-15-04-05") + + archiveFileName := fmt.Sprintf("%s-%s", baseFileName, timestamp) + archiveFilePath := filepath.Join(b.archiveDir, archiveFileName) + + oldBackupFile, err := os.Open(b.fileName) + if err != nil { + return fmt.Errorf("unable to open old channel backup file: "+ + "%w", err) + } + defer func() { + err := oldBackupFile.Close() + if err != nil { + log.Errorf("unable to close old channel backup file: "+ + "%v", err) + } + }() + + // Ensure the archive directory exists. If it doesn't we create it. + const archiveDirPermissions = 0o700 + err = os.MkdirAll(b.archiveDir, archiveDirPermissions) + if err != nil { + return fmt.Errorf("unable to create archive directory: %w", err) + } + + // Create new archive file. + archiveFile, err := os.Create(archiveFilePath) + if err != nil { + return fmt.Errorf("unable to create archive file: %w", err) + } + defer func() { + err := archiveFile.Close() + if err != nil { + log.Errorf("unable to close archive file: %v", err) + } + }() + + // Copy contents of old backup to the newly created archive files. + _, err = io.Copy(archiveFile, oldBackupFile) + if err != nil { + return fmt.Errorf("unable to copy to archive file: %w", err) + } + err = archiveFile.Sync() + if err != nil { + return fmt.Errorf("unable to sync archive file: %w", err) + } + + return nil +} diff --git a/chanbackup/backupfile_test.go b/chanbackup/backupfile_test.go index b0d3ba66f..27804990a 100644 --- a/chanbackup/backupfile_test.go +++ b/chanbackup/backupfile_test.go @@ -274,3 +274,70 @@ func TestExtractMulti(t *testing.T) { assertMultiEqual(t, &unpackedMulti, freshUnpackedMulti) } } + +// TestCreateArchiveFile tests that we're able to create an archive file +// with a timestamped name in the specified archive directory, and copy the +// contents of the main backup file to the new archive file. +func TestCreateArchiveFile(t *testing.T) { + t.Parallel() + + // First, we'll create a temporary directory for our test files. + tempDir := t.TempDir() + archiveDir := filepath.Join(tempDir, DefaultChanBackupArchiveDirName) + + // Next, we'll create a test backup file and write some content to it. + backupFile := filepath.Join(tempDir, DefaultBackupFileName) + testContent := []byte("test backup content") + err := os.WriteFile(backupFile, testContent, 0644) + require.NoError(t, err) + + tests := []struct { + name string + setup func() + wantError bool + }{ + { + name: "successful archive", + }, + { + name: "invalid archive directory permissions", + setup: func() { + // Create dir with no write permissions. + err := os.MkdirAll(archiveDir, 0500) + require.NoError(t, err) + }, + wantError: true, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + defer os.RemoveAll(archiveDir) + if tc.setup != nil { + tc.setup() + } + + multiFile := NewMultiFile(backupFile) + + err := multiFile.createArchiveFile() + if tc.wantError { + require.Error(t, err) + return + } + + require.NoError(t, err) + + // Verify archive exists and content matches. + files, err := os.ReadDir(archiveDir) + require.NoError(t, err) + require.Len(t, files, 1) + + archivedContent, err := os.ReadFile( + filepath.Join(archiveDir, files[0].Name()), + ) + require.NoError(t, err) + assertBackupMatches(t, backupFile, archivedContent) + }) + } +}