Skip to content

Commit

Permalink
Fixed a bug that didn't preserve the version bit when copying old sna…
Browse files Browse the repository at this point in the history
…pshots

The version bit should not be set to 1 when encoding a snapshot.  Instead,
it must be set to 1 on snapshot creation.

To correctly process old snapshots encoded incorrectly with version bit set
to 1, the first byte of the encoded file list is also checked.  If the first
byte is `[`, then it must be an old snapshot, since the file list in the new
snapshot format always starts with a string encoded in msgpack, the first
byte of which can't be `[`.
  • Loading branch information
gilbertchen committed Nov 23, 2022
1 parent 0a794e6 commit 58f0d2b
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 20 deletions.
33 changes: 16 additions & 17 deletions src/duplicacy_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"strings"
"time"
"sort"
"bytes"

"github.com/vmihailenco/msgpack"

Expand Down Expand Up @@ -52,6 +51,7 @@ type Snapshot struct {
// CreateEmptySnapshot creates an empty snapshot.
func CreateEmptySnapshot(id string) (snapshto *Snapshot) {
return &Snapshot{
Version: 1,
ID: id,
Revision: 0,
StartTime: time.Now().Unix(),
Expand Down Expand Up @@ -112,22 +112,21 @@ func (snapshot *Snapshot)ListRemoteFiles(config *Config, chunkOperator *ChunkOpe
}

var chunk *Chunk
reader := sequenceReader{
sequence: snapshot.FileSequence,
buffer: new(bytes.Buffer),
refillFunc: func(chunkHash string) []byte {
if chunk != nil {
config.PutChunk(chunk)
}
chunk = chunkOperator.Download(chunkHash, 0, true)
return chunk.GetBytes()
},
}

if snapshot.Version == 0 {
reader := NewSequenceReader(snapshot.FileSequence, func(chunkHash string) []byte {
if chunk != nil {
config.PutChunk(chunk)
}
chunk = chunkOperator.Download(chunkHash, 0, true)
return chunk.GetBytes()
})

// Normally if Version is 0 then the snapshot is created by CLI v2 but unfortunately CLI 3.0.1 does not set the
// version bit correctly when copying old backups. So we need to check the first byte -- if it is '[' then it is
// the old format. The new format starts with a string encoded in msgpack and the first byte can't be '['.
if snapshot.Version == 0 || reader.GetFirstByte() == '['{
LOG_INFO("SNAPSHOT_VERSION", "snapshot %s at revision %d is encoded in an old version format", snapshot.ID, snapshot.Revision)
files := make([]*Entry, 0)
decoder := json.NewDecoder(&reader)
decoder := json.NewDecoder(reader)

// read open bracket
_, err := decoder.Token()
Expand Down Expand Up @@ -156,7 +155,7 @@ func (snapshot *Snapshot)ListRemoteFiles(config *Config, chunkOperator *ChunkOpe
}
}
} else if snapshot.Version == 1 {
decoder := msgpack.NewDecoder(&reader)
decoder := msgpack.NewDecoder(reader)

lastEndChunk := 0

Expand Down Expand Up @@ -434,7 +433,7 @@ func (snapshot *Snapshot) MarshalJSON() ([]byte, error) {

object := make(map[string]interface{})

object["version"] = 1
object["version"] = snapshot.Version
object["id"] = snapshot.ID
object["revision"] = snapshot.Revision
object["options"] = snapshot.Options
Expand Down
26 changes: 23 additions & 3 deletions src/duplicacy_snapshotmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,17 +249,27 @@ func (manager *SnapshotManager) DownloadSnapshot(snapshotID string, revision int
// the memory before passing them to the json unmarshaller.
type sequenceReader struct {
sequence []string
buffer *bytes.Buffer
buffer *bytes.Reader
index int
refillFunc func(hash string) []byte
}

func NewSequenceReader(sequence []string, refillFunc func(hash string) []byte) *sequenceReader {
newData := refillFunc(sequence[0])
return &sequenceReader{
sequence: sequence,
buffer: bytes.NewReader(newData),
index: 1,
refillFunc: refillFunc,
}
}

// Read reads a new chunk using the refill function when there is no more data in the buffer
func (reader *sequenceReader) Read(data []byte) (n int, err error) {
if len(reader.buffer.Bytes()) == 0 {
if reader.buffer.Len() == 0 {
if reader.index < len(reader.sequence) {
newData := reader.refillFunc(reader.sequence[reader.index])
reader.buffer.Write(newData)
reader.buffer = bytes.NewReader(newData)
reader.index++
} else {
return 0, io.EOF
Expand All @@ -269,6 +279,16 @@ func (reader *sequenceReader) Read(data []byte) (n int, err error) {
return reader.buffer.Read(data)
}

func (reader *sequenceReader) GetFirstByte() byte {
b, err := reader.buffer.ReadByte()
reader.buffer.UnreadByte()
if err != nil {
return 0
} else {
return b
}
}

func (manager *SnapshotManager) CreateChunkOperator(resurrect bool, rewriteChunks bool, threads int, allowFailures bool) {
if manager.chunkOperator == nil {
manager.chunkOperator = CreateChunkOperator(manager.config, manager.storage, manager.snapshotCache, resurrect, rewriteChunks, threads, allowFailures)
Expand Down

2 comments on commit 58f0d2b

@gilbertchen
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit has been mentioned on Duplicacy Forum. There might be relevant details there:

https://forum.duplicacy.com/t/failed-to-load-the-snapshot-error-problem-with-cli-3-0-1/6907/8

@gilbertchen
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit has been mentioned on Duplicacy Forum. There might be relevant details there:

https://forum.duplicacy.com/t/cli-release-3-1-0-is-now-available/7076/1

Please sign in to comment.