• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 12807768804

16 Jan 2025 11:16AM UTC coverage: 58.746% (+0.04%) from 58.709%
12807768804

Pull #9232

github

Abdulkbk
docs: add release note
Pull Request #9232: chanbackup: archive old channel backup files

55 of 70 new or added lines in 3 files covered. (78.57%)

27 existing lines in 10 files now uncovered.

135505 of 230662 relevant lines covered (58.75%)

19223.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.62
/chanbackup/backupfile.go
1
package chanbackup
2

3
import (
4
        "fmt"
5
        "io"
6
        "os"
7
        "path/filepath"
8
        "time"
9

10
        "github.com/lightningnetwork/lnd/keychain"
11
)
12

13
const (
14
        // DefaultBackupFileName is the default name of the auto updated static
15
        // channel backup fie.
16
        DefaultBackupFileName = "channel.backup"
17

18
        // DefaultTempBackupFileName is the default name of the temporary SCB
19
        // file that we'll use to atomically update the primary back up file
20
        // when new channel are detected.
21
        DefaultTempBackupFileName = "temp-dont-use.backup"
22

23
        // DefaultChanBackupArchiveDirName is the default name of the directory
24
        // that we'll use to store old channel backups.
25
        DefaultChanBackupArchiveDirName = "chan-backup-archives"
26
)
27

28
var (
29
        // ErrNoBackupFileExists is returned if caller attempts to call
30
        // UpdateAndSwap with the file name not set.
31
        ErrNoBackupFileExists = fmt.Errorf("back up file name not set")
32

33
        // ErrNoTempBackupFile is returned if caller attempts to call
34
        // UpdateAndSwap with the temp back up file name not set.
35
        ErrNoTempBackupFile = fmt.Errorf("temp backup file not set")
36
)
37

38
// MultiFile represents a file on disk that a caller can use to read the packed
39
// multi backup into an unpacked one, and also atomically update the contents
40
// on disk once new channels have been opened, and old ones closed. This struct
41
// relies on an atomic file rename property which most widely use file systems
42
// have.
43
type MultiFile struct {
44
        // fileName is the file name of the main back up file.
45
        fileName string
46

47
        // tempFileName is the name of the file that we'll use to stage a new
48
        // packed multi-chan backup, and the rename to the main back up file.
49
        tempFileName string
50

51
        // tempFile is an open handle to the temp back up file.
52
        tempFile *os.File
53

54
        // archiveDir is the directory where we'll store old channel backups.
55
        archiveDir string
56

57
        // deleteOldBackup indicates whether old backups should be deleted
58
        // rather than archived.
59
        deleteOldBackup bool
60
}
61

62
// NewMultiFile create a new multi-file instance at the target location on the
63
// file system.
64
func NewMultiFile(fileName string, deleteOldBackup bool) *MultiFile {
11✔
65
        // We'll our temporary backup file in the very same directory as the
11✔
66
        // main backup file.
11✔
67
        backupFileDir := filepath.Dir(fileName)
11✔
68
        tempFileName := filepath.Join(
11✔
69
                backupFileDir, DefaultTempBackupFileName,
11✔
70
        )
11✔
71
        archiveDir := filepath.Join(
11✔
72
                backupFileDir, DefaultChanBackupArchiveDirName,
11✔
73
        )
11✔
74

11✔
75
        return &MultiFile{
11✔
76
                fileName:        fileName,
11✔
77
                tempFileName:    tempFileName,
11✔
78
                archiveDir:      archiveDir,
11✔
79
                deleteOldBackup: deleteOldBackup,
11✔
80
        }
11✔
81
}
11✔
82

83
// UpdateAndSwap will attempt write a new temporary backup file to disk with
84
// the newBackup encoded, then atomically swap (via rename) the old file for
85
// the new file by updating the name of the new file to the old. It also checks
86
// if the old file should be archived first before swapping it.
87
func (b *MultiFile) UpdateAndSwap(newBackup PackedMulti) error {
12✔
88
        // If the main backup file isn't set, then we can't proceed.
12✔
89
        if b.fileName == "" {
13✔
90
                return ErrNoBackupFileExists
1✔
91
        }
1✔
92

93
        log.Infof("Updating backup file at %v", b.fileName)
11✔
94

11✔
95
        // If the old back up file still exists, then we'll delete it before
11✔
96
        // proceeding.
11✔
97
        if _, err := os.Stat(b.tempFileName); err == nil {
12✔
98
                log.Infof("Found old temp backup @ %v, removing before swap",
1✔
99
                        b.tempFileName)
1✔
100

1✔
101
                err = os.Remove(b.tempFileName)
1✔
102
                if err != nil {
1✔
103
                        return fmt.Errorf("unable to remove temp "+
×
104
                                "backup file: %v", err)
×
105
                }
×
106
        }
107

108
        // Now that we know the staging area is clear, we'll create the new
109
        // temporary back up file.
110
        var err error
11✔
111
        b.tempFile, err = os.Create(b.tempFileName)
11✔
112
        if err != nil {
14✔
113
                return fmt.Errorf("unable to create temp file: %w", err)
3✔
114
        }
3✔
115

116
        // With the file created, we'll write the new packed multi backup and
117
        // remove the temporary file all together once this method exits.
118
        _, err = b.tempFile.Write([]byte(newBackup))
11✔
119
        if err != nil {
11✔
120
                return fmt.Errorf("unable to write backup to temp file: %w",
×
121
                        err)
×
122
        }
×
123
        if err := b.tempFile.Sync(); err != nil {
11✔
124
                return fmt.Errorf("unable to sync temp file: %w", err)
×
125
        }
×
126
        defer os.Remove(b.tempFileName)
11✔
127

11✔
128
        log.Infof("Swapping old multi backup file from %v to %v",
11✔
129
                b.tempFileName, b.fileName)
11✔
130

11✔
131
        // Before we rename the swap (atomic name swap), we'll make
11✔
132
        // sure to close the current file as some OSes don't support
11✔
133
        // renaming a file that's already open (Windows).
11✔
134
        if err := b.tempFile.Close(); err != nil {
11✔
135
                return fmt.Errorf("unable to close file: %w", err)
×
136
        }
×
137

138
        if !b.deleteOldBackup {
20✔
139
                // We check if the main backup file exists before proceeding
9✔
140
                // with the archive. If no backup file exists yet (e.g. on the
9✔
141
                // first startup of the node), os.Stat will return an error,
9✔
142
                // which we just log and not error out.
9✔
143
                _, err = os.Stat(b.fileName)
9✔
144
                if err != nil {
13✔
145
                        log.Debugf("Unable to get backup file info: %v", err)
4✔
146
                } else {
12✔
147
                        log.Infof("Archiving old backup file at %v", b.fileName)
8✔
148

8✔
149
                        if err := createArchiveFile(
8✔
150
                                b.archiveDir, b.fileName,
8✔
151
                        ); err != nil {
8✔
NEW
152
                                return fmt.Errorf("unable to archive old "+
×
NEW
153
                                        "backup file: %w", err)
×
NEW
154
                        }
×
155
                }
156
        }
157

158
        // Finally, we'll attempt to atomically rename the temporary file to
159
        // the main back up file. If this succeeds, then we'll only have a
160
        // single file on disk once this method exits.
161
        return os.Rename(b.tempFileName, b.fileName)
11✔
162
}
163

164
// ExtractMulti attempts to extract the packed multi backup we currently point
165
// to into an unpacked version. This method will fail if no backup file
166
// currently exists as the specified location.
167
func (b *MultiFile) ExtractMulti(keyChain keychain.KeyRing) (*Multi, error) {
7✔
168
        var err error
7✔
169

7✔
170
        // We'll return an error if the main file isn't currently set.
7✔
171
        if b.fileName == "" {
8✔
172
                return nil, ErrNoBackupFileExists
1✔
173
        }
1✔
174

175
        // Now that we've confirmed the target file is populated, we'll read
176
        // all the contents of the file. This function ensures that file is
177
        // always closed, even if we can't read the contents.
178
        multiBytes, err := os.ReadFile(b.fileName)
6✔
179
        if err != nil {
10✔
180
                return nil, err
4✔
181
        }
4✔
182

183
        // Finally, we'll attempt to unpack the file and return the unpack
184
        // version to the caller.
185
        packedMulti := PackedMulti(multiBytes)
5✔
186
        return packedMulti.Unpack(keyChain)
5✔
187
}
188

189
// createArchiveFile creates an archive file with a timestamped name in the
190
// specified archive directory, and copies the contents of the main backup file
191
// to the new archive file.
192
func createArchiveFile(archiveDir string, fileName string) error {
11✔
193
        // Generate archive file path with timestamped name.
11✔
194
        baseFileName := filepath.Base(fileName)
11✔
195
        timestamp := time.Now().Format("2006-01-02-15-04-05")
11✔
196

11✔
197
        archiveFileName := fmt.Sprintf("%s-%s", baseFileName, timestamp)
11✔
198
        archiveFilePath := filepath.Join(archiveDir, archiveFileName)
11✔
199

11✔
200
        oldBackupFile, err := os.Open(fileName)
11✔
201
        if err != nil {
12✔
202
                return fmt.Errorf("unable to open old backup file: %w", err)
1✔
203
        }
1✔
204
        defer func() {
20✔
205
                if cerr := oldBackupFile.Close(); cerr != nil && err == nil {
10✔
NEW
206
                        err = fmt.Errorf("unable to close old backup file: %w",
×
NEW
207
                                cerr)
×
NEW
208
                }
×
209
        }()
210

211
        // Ensure the archive directory exists. If it doesn't we create it.
212
        const archiveDirPermissions = 0o700
10✔
213
        err = os.MkdirAll(archiveDir, archiveDirPermissions)
10✔
214
        if err != nil {
10✔
NEW
215
                return fmt.Errorf("unable to create archive directory: %w", err)
×
NEW
216
        }
×
217

218
        // Create new archive file.
219
        archiveFile, err := os.Create(archiveFilePath)
10✔
220
        if err != nil {
11✔
221
                return fmt.Errorf("unable to create archive file: %w", err)
1✔
222
        }
1✔
223
        defer func() {
18✔
224
                if cerr := archiveFile.Close(); cerr != nil && err == nil {
9✔
NEW
225
                        err = fmt.Errorf("unable to close archive file: %w",
×
NEW
226
                                cerr)
×
NEW
227
                }
×
228
        }()
229

230
        // Copy contents of old backup to the newly created archive file.
231
        if _, err := io.Copy(archiveFile, oldBackupFile); err != nil {
9✔
NEW
232
                return fmt.Errorf("unable to copy to archive file: %w", err)
×
NEW
233
        }
×
234
        if err := archiveFile.Sync(); err != nil {
9✔
NEW
235
                return fmt.Errorf("unable to sync archive file: %w", err)
×
NEW
236
        }
×
237

238
        return nil
9✔
239
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc