b2: factor version handling into lib/version

Standardizes the filename version tagging so that it can be used by any
backend.
This commit is contained in:
Dominik Mydlil 2021-04-06 21:45:17 +02:00 committed by Nick Craig-Wood
parent c1492cfa28
commit c163e6b250
4 changed files with 137 additions and 62 deletions

View File

@ -2,12 +2,11 @@ package api
import (
"fmt"
"path"
"strconv"
"strings"
"time"
"github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/lib/version"
)
// Error describes a B2 error response
@ -63,16 +62,17 @@ func (t *Timestamp) UnmarshalJSON(data []byte) error {
return nil
}
const versionFormat = "-v2006-01-02-150405.000"
// HasVersion returns true if it looks like the passed filename has a timestamp on it.
//
// Note that the passed filename's timestamp may still be invalid even if this
// function returns true.
func HasVersion(remote string) bool {
return version.Match(remote)
}
// AddVersion adds the timestamp as a version string into the filename passed in.
func (t Timestamp) AddVersion(remote string) string {
ext := path.Ext(remote)
base := remote[:len(remote)-len(ext)]
s := time.Time(t).Format(versionFormat)
// Replace the '.' with a '-'
s = strings.Replace(s, ".", "-", -1)
return base + s + ext
return version.Add(remote, time.Time(t))
}
// RemoveVersion removes the timestamp from a filename as a version string.
@ -80,24 +80,9 @@ func (t Timestamp) AddVersion(remote string) string {
// It returns the new file name and a timestamp, or the old filename
// and a zero timestamp.
func RemoveVersion(remote string) (t Timestamp, newRemote string) {
newRemote = remote
ext := path.Ext(remote)
base := remote[:len(remote)-len(ext)]
if len(base) < len(versionFormat) {
return
}
versionStart := len(base) - len(versionFormat)
// Check it ends in -xxx
if base[len(base)-4] != '-' {
return
}
// Replace with .xxx for parsing
base = base[:len(base)-4] + "." + base[len(base)-3:]
newT, err := time.Parse(versionFormat, base[versionStart:])
if err != nil {
return
}
return Timestamp(newT), base[:versionStart] + ext
time, newRemote := version.Remove(remote)
t = Timestamp(time)
return
}
// IsZero returns true if the timestamp is uninitialized

View File

@ -13,7 +13,6 @@ import (
var (
emptyT api.Timestamp
t0 = api.Timestamp(fstest.Time("1970-01-01T01:01:01.123456789Z"))
t0r = api.Timestamp(fstest.Time("1970-01-01T01:01:01.123000000Z"))
t1 = api.Timestamp(fstest.Time("2001-02-03T04:05:06.123000000Z"))
)
@ -36,40 +35,6 @@ func TestTimestampUnmarshalJSON(t *testing.T) {
assert.Equal(t, (time.Time)(t1), (time.Time)(tActual))
}
func TestTimestampAddVersion(t *testing.T) {
for _, test := range []struct {
t api.Timestamp
in string
expected string
}{
{t0, "potato.txt", "potato-v1970-01-01-010101-123.txt"},
{t1, "potato", "potato-v2001-02-03-040506-123"},
{t1, "", "-v2001-02-03-040506-123"},
} {
actual := test.t.AddVersion(test.in)
assert.Equal(t, test.expected, actual, test.in)
}
}
func TestTimestampRemoveVersion(t *testing.T) {
for _, test := range []struct {
in string
expectedT api.Timestamp
expectedRemote string
}{
{"potato.txt", emptyT, "potato.txt"},
{"potato-v1970-01-01-010101-123.txt", t0r, "potato.txt"},
{"potato-v2001-02-03-040506-123", t1, "potato"},
{"-v2001-02-03-040506-123", t1, ""},
{"potato-v2A01-02-03-040506-123", emptyT, "potato-v2A01-02-03-040506-123"},
{"potato-v2001-02-03-040506=123", emptyT, "potato-v2001-02-03-040506=123"},
} {
actualT, actualRemote := api.RemoveVersion(test.in)
assert.Equal(t, test.expectedT, actualT, test.in)
assert.Equal(t, test.expectedRemote, actualRemote, test.in)
}
}
func TestTimestampIsZero(t *testing.T) {
assert.True(t, emptyT.IsZero())
assert.False(t, t0.IsZero())

52
lib/version/version.go Normal file
View File

@ -0,0 +1,52 @@
// Package version provides machinery for versioning file names
// with a timestamp-based version string
package version
import (
"path"
"regexp"
"strings"
"time"
)
const versionFormat = "-v2006-01-02-150405.000"
var versionRegexp = regexp.MustCompile("-v\\d{4}-\\d{2}-\\d{2}-\\d{6}-\\d{3}")
// Add returns fileName modified to include t as the version
func Add(fileName string, t time.Time) string {
ext := path.Ext(fileName)
base := fileName[:len(fileName)-len(ext)]
s := t.Format(versionFormat)
// Replace the '.' with a '-'
s = strings.Replace(s, ".", "-", -1)
return base + s + ext
}
// Remove returns a modified fileName without the version string and the time it represented
// If the fileName did not have a version then time.Time{} is returned along with an unmodified fileName
func Remove(fileName string) (t time.Time, fileNameWithoutVersion string) {
fileNameWithoutVersion = fileName
ext := path.Ext(fileName)
base := fileName[:len(fileName)-len(ext)]
if len(base) < len(versionFormat) {
return
}
versionStart := len(base) - len(versionFormat)
// Check it ends in -xxx
if base[len(base)-4] != '-' {
return
}
// Replace with .xxx for parsing
base = base[:len(base)-4] + "." + base[len(base)-3:]
newT, err := time.Parse(versionFormat, base[versionStart:])
if err != nil {
return
}
return newT, base[:versionStart] + ext
}
// Match returns true if the fileName has a version string
func Match(fileName string) bool {
return versionRegexp.MatchString(fileName)
}

View File

@ -0,0 +1,73 @@
package version_test
import (
"testing"
"time"
"github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/lib/version"
"github.com/stretchr/testify/assert"
)
var (
emptyT time.Time
t0 = fstest.Time("1970-01-01T01:01:01.123456789Z")
t0r = fstest.Time("1970-01-01T01:01:01.123000000Z")
t1 = fstest.Time("2001-02-03T04:05:06.123000000Z")
)
func TestVersionAdd(t *testing.T) {
for _, test := range []struct {
t time.Time
in string
expected string
}{
{t0, "potato.txt", "potato-v1970-01-01-010101-123.txt"},
{t0, "potato-v2001-02-03-040506-123.txt", "potato-v2001-02-03-040506-123-v1970-01-01-010101-123.txt"},
{t0, "123.!!lipps", "123-v1970-01-01-010101-123.!!lipps"},
{t1, "potato", "potato-v2001-02-03-040506-123"},
{t1, "", "-v2001-02-03-040506-123"},
} {
actual := version.Add(test.in, test.t)
assert.Equal(t, test.expected, actual, test.in)
}
}
func TestVersionRemove(t *testing.T) {
for _, test := range []struct {
in string
expectedT time.Time
expectedRemote string
}{
{"potato.txt", emptyT, "potato.txt"},
{"potato-v1970-01-01-010101-123.txt", t0r, "potato.txt"},
{"potato-v2001-02-03-040506-123-v1970-01-01-010101-123.txt", t0r, "potato-v2001-02-03-040506-123.txt"},
{"potato-v2001-02-03-040506-123", t1, "potato"},
{"-v2001-02-03-040506-123", t1, ""},
{"potato-v2A01-02-03-040506-123", emptyT, "potato-v2A01-02-03-040506-123"},
{"potato-v2001-02-03-040506=123", emptyT, "potato-v2001-02-03-040506=123"},
} {
actualT, actualRemote := version.Remove(test.in)
assert.Equal(t, test.expectedT, actualT, test.in)
assert.Equal(t, test.expectedRemote, actualRemote, test.in)
}
}
func TestVersionMatch(t *testing.T) {
for _, test := range []struct {
in string
expected bool
}{
{"potato.txt", false},
{"potato", false},
{"", false},
{"potato-v1970-01-01-010101-123.txt", true},
{"potato-v2001-02-03-040506-123-v1970-01-01-010101-123.txt", true},
{"potato-v2001-02-03-040506-123", true},
{"-v2001-02-03-040506-123", true},
{"-v9999-99-99-999999-999", true},
} {
actual := version.Match(test.in)
assert.Equal(t, test.expected, actual, test.in)
}
}