691 lines
28 KiB
Go
691 lines
28 KiB
Go
package chunker
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/hash"
|
|
"github.com/rclone/rclone/fs/operations"
|
|
"github.com/rclone/rclone/fstest"
|
|
"github.com/rclone/rclone/fstest/fstests"
|
|
"github.com/rclone/rclone/lib/random"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Command line flags
|
|
var (
|
|
UploadKilobytes = flag.Int("upload-kilobytes", 0, "Upload size in Kilobytes, set this to test large uploads")
|
|
)
|
|
|
|
// test that chunking does not break large uploads
|
|
func testPutLarge(t *testing.T, f *Fs, kilobytes int) {
|
|
t.Run(fmt.Sprintf("PutLarge%dk", kilobytes), func(t *testing.T) {
|
|
fstests.TestPutLarge(context.Background(), t, f, &fstest.Item{
|
|
ModTime: fstest.Time("2001-02-03T04:05:06.499999999Z"),
|
|
Path: fmt.Sprintf("chunker-upload-%dk", kilobytes),
|
|
Size: int64(kilobytes) * int64(fs.KibiByte),
|
|
})
|
|
})
|
|
}
|
|
|
|
// test chunk name parser
|
|
func testChunkNameFormat(t *testing.T, f *Fs) {
|
|
saveOpt := f.opt
|
|
defer func() {
|
|
// restore original settings (f is pointer, f.opt is struct)
|
|
f.opt = saveOpt
|
|
_ = f.setChunkNameFormat(f.opt.NameFormat)
|
|
}()
|
|
|
|
assertFormat := func(pattern, wantDataFormat, wantCtrlFormat, wantNameRegexp string) {
|
|
err := f.setChunkNameFormat(pattern)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, wantDataFormat, f.dataNameFmt)
|
|
assert.Equal(t, wantCtrlFormat, f.ctrlNameFmt)
|
|
assert.Equal(t, wantNameRegexp, f.nameRegexp.String())
|
|
}
|
|
|
|
assertFormatValid := func(pattern string) {
|
|
err := f.setChunkNameFormat(pattern)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
assertFormatInvalid := func(pattern string) {
|
|
err := f.setChunkNameFormat(pattern)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
assertMakeName := func(wantChunkName, mainName string, chunkNo int, ctrlType, xactID string) {
|
|
gotChunkName := ""
|
|
assert.NotPanics(t, func() {
|
|
gotChunkName = f.makeChunkName(mainName, chunkNo, ctrlType, xactID)
|
|
}, "makeChunkName(%q,%d,%q,%q) must not panic", mainName, chunkNo, ctrlType, xactID)
|
|
if gotChunkName != "" {
|
|
assert.Equal(t, wantChunkName, gotChunkName)
|
|
}
|
|
}
|
|
|
|
assertMakeNamePanics := func(mainName string, chunkNo int, ctrlType, xactID string) {
|
|
assert.Panics(t, func() {
|
|
_ = f.makeChunkName(mainName, chunkNo, ctrlType, xactID)
|
|
}, "makeChunkName(%q,%d,%q,%q) should panic", mainName, chunkNo, ctrlType, xactID)
|
|
}
|
|
|
|
assertParseName := func(fileName, wantMainName string, wantChunkNo int, wantCtrlType, wantXactID string) {
|
|
gotMainName, gotChunkNo, gotCtrlType, gotXactID := f.parseChunkName(fileName)
|
|
assert.Equal(t, wantMainName, gotMainName)
|
|
assert.Equal(t, wantChunkNo, gotChunkNo)
|
|
assert.Equal(t, wantCtrlType, gotCtrlType)
|
|
assert.Equal(t, wantXactID, gotXactID)
|
|
}
|
|
|
|
const newFormatSupported = false // support for patterns not starting with base name (*)
|
|
|
|
// valid formats
|
|
assertFormat(`*.rclone_chunk.###`, `%s.rclone_chunk.%03d`, `%s.rclone_chunk._%s`, `^(.+?)\.rclone_chunk\.(?:([0-9]{3,})|_([a-z][a-z0-9]{2,6}))(?:_([0-9a-z]{4,9})|\.\.tmp_([0-9]{10,13}))?$`)
|
|
assertFormat(`*.rclone_chunk.#`, `%s.rclone_chunk.%d`, `%s.rclone_chunk._%s`, `^(.+?)\.rclone_chunk\.(?:([0-9]+)|_([a-z][a-z0-9]{2,6}))(?:_([0-9a-z]{4,9})|\.\.tmp_([0-9]{10,13}))?$`)
|
|
assertFormat(`*_chunk_#####`, `%s_chunk_%05d`, `%s_chunk__%s`, `^(.+?)_chunk_(?:([0-9]{5,})|_([a-z][a-z0-9]{2,6}))(?:_([0-9a-z]{4,9})|\.\.tmp_([0-9]{10,13}))?$`)
|
|
assertFormat(`*-chunk-#`, `%s-chunk-%d`, `%s-chunk-_%s`, `^(.+?)-chunk-(?:([0-9]+)|_([a-z][a-z0-9]{2,6}))(?:_([0-9a-z]{4,9})|\.\.tmp_([0-9]{10,13}))?$`)
|
|
assertFormat(`*-chunk-#-%^$()[]{}.+-!?:\`, `%s-chunk-%d-%%^$()[]{}.+-!?:\`, `%s-chunk-_%s-%%^$()[]{}.+-!?:\`, `^(.+?)-chunk-(?:([0-9]+)|_([a-z][a-z0-9]{2,6}))-%\^\$\(\)\[\]\{\}\.\+-!\?:\\(?:_([0-9a-z]{4,9})|\.\.tmp_([0-9]{10,13}))?$`)
|
|
if newFormatSupported {
|
|
assertFormat(`_*-chunk-##,`, `_%s-chunk-%02d,`, `_%s-chunk-_%s,`, `^_(.+?)-chunk-(?:([0-9]{2,})|_([a-z][a-z0-9]{2,6})),(?:_([0-9a-z]{4,9})|\.\.tmp_([0-9]{10,13}))?$`)
|
|
}
|
|
|
|
// invalid formats
|
|
assertFormatInvalid(`chunk-#`)
|
|
assertFormatInvalid(`*-chunk`)
|
|
assertFormatInvalid(`*-*-chunk-#`)
|
|
assertFormatInvalid(`*-chunk-#-#`)
|
|
assertFormatInvalid(`#-chunk-*`)
|
|
assertFormatInvalid(`*/#`)
|
|
|
|
assertFormatValid(`*#`)
|
|
assertFormatInvalid(`**#`)
|
|
assertFormatInvalid(`#*`)
|
|
assertFormatInvalid(``)
|
|
assertFormatInvalid(`-`)
|
|
|
|
// quick tests
|
|
if newFormatSupported {
|
|
assertFormat(`part_*_#`, `part_%s_%d`, `part_%s__%s`, `^part_(.+?)_(?:([0-9]+)|_([a-z][a-z0-9]{2,6}))(?:_([0-9][0-9a-z]{3,8})\.\.tmp_([0-9]{10,13}))?$`)
|
|
f.opt.StartFrom = 1
|
|
|
|
assertMakeName(`part_fish_1`, "fish", 0, "", "")
|
|
assertParseName(`part_fish_43`, "fish", 42, "", "")
|
|
assertMakeName(`part_fish__locks`, "fish", -2, "locks", "")
|
|
assertParseName(`part_fish__locks`, "fish", -1, "locks", "")
|
|
assertMakeName(`part_fish__x2y`, "fish", -2, "x2y", "")
|
|
assertParseName(`part_fish__x2y`, "fish", -1, "x2y", "")
|
|
assertMakeName(`part_fish_3_0004`, "fish", 2, "", "4")
|
|
assertParseName(`part_fish_4_0005`, "fish", 3, "", "0005")
|
|
assertMakeName(`part_fish__blkinfo_jj5fvo3wr`, "fish", -3, "blkinfo", "jj5fvo3wr")
|
|
assertParseName(`part_fish__blkinfo_zz9fvo3wr`, "fish", -1, "blkinfo", "zz9fvo3wr")
|
|
|
|
// old-style temporary suffix (parse only)
|
|
assertParseName(`part_fish_4..tmp_0000000011`, "fish", 3, "", "000b")
|
|
assertParseName(`part_fish__blkinfo_jj5fvo3wr`, "fish", -1, "blkinfo", "jj5fvo3wr")
|
|
}
|
|
|
|
// prepare format for long tests
|
|
assertFormat(`*.chunk.###`, `%s.chunk.%03d`, `%s.chunk._%s`, `^(.+?)\.chunk\.(?:([0-9]{3,})|_([a-z][a-z0-9]{2,6}))(?:_([0-9a-z]{4,9})|\.\.tmp_([0-9]{10,13}))?$`)
|
|
f.opt.StartFrom = 2
|
|
|
|
// valid data chunks
|
|
assertMakeName(`fish.chunk.003`, "fish", 1, "", "")
|
|
assertParseName(`fish.chunk.003`, "fish", 1, "", "")
|
|
assertMakeName(`fish.chunk.021`, "fish", 19, "", "")
|
|
assertParseName(`fish.chunk.021`, "fish", 19, "", "")
|
|
|
|
// valid temporary data chunks
|
|
assertMakeName(`fish.chunk.011_4321`, "fish", 9, "", "4321")
|
|
assertParseName(`fish.chunk.011_4321`, "fish", 9, "", "4321")
|
|
assertMakeName(`fish.chunk.011_00bc`, "fish", 9, "", "00bc")
|
|
assertParseName(`fish.chunk.011_00bc`, "fish", 9, "", "00bc")
|
|
assertMakeName(`fish.chunk.1916_5jjfvo3wr`, "fish", 1914, "", "5jjfvo3wr")
|
|
assertParseName(`fish.chunk.1916_5jjfvo3wr`, "fish", 1914, "", "5jjfvo3wr")
|
|
assertMakeName(`fish.chunk.1917_zz9fvo3wr`, "fish", 1915, "", "zz9fvo3wr")
|
|
assertParseName(`fish.chunk.1917_zz9fvo3wr`, "fish", 1915, "", "zz9fvo3wr")
|
|
|
|
// valid temporary data chunks (old temporary suffix, only parse)
|
|
assertParseName(`fish.chunk.004..tmp_0000000047`, "fish", 2, "", "001b")
|
|
assertParseName(`fish.chunk.323..tmp_9994567890123`, "fish", 321, "", "3jjfvo3wr")
|
|
|
|
// parsing invalid data chunk names
|
|
assertParseName(`fish.chunk.3`, "", -1, "", "")
|
|
assertParseName(`fish.chunk.001`, "", -1, "", "")
|
|
assertParseName(`fish.chunk.21`, "", -1, "", "")
|
|
assertParseName(`fish.chunk.-21`, "", -1, "", "")
|
|
|
|
assertParseName(`fish.chunk.004abcd`, "", -1, "", "") // missing underscore delimiter
|
|
assertParseName(`fish.chunk.004__1234`, "", -1, "", "") // extra underscore delimiter
|
|
assertParseName(`fish.chunk.004_123`, "", -1, "", "") // too short temporary suffix
|
|
assertParseName(`fish.chunk.004_1234567890`, "", -1, "", "") // too long temporary suffix
|
|
assertParseName(`fish.chunk.004_-1234`, "", -1, "", "") // temporary suffix must be positive
|
|
assertParseName(`fish.chunk.004_123E`, "", -1, "", "") // uppercase not allowed
|
|
assertParseName(`fish.chunk.004_12.3`, "", -1, "", "") // punctuation not allowed
|
|
|
|
// parsing invalid data chunk names (old temporary suffix)
|
|
assertParseName(`fish.chunk.004.tmp_0000000021`, "", -1, "", "")
|
|
assertParseName(`fish.chunk.003..tmp_123456789`, "", -1, "", "")
|
|
assertParseName(`fish.chunk.003..tmp_012345678901234567890123456789`, "", -1, "", "")
|
|
assertParseName(`fish.chunk.323..tmp_12345678901234`, "", -1, "", "")
|
|
assertParseName(`fish.chunk.003..tmp_-1`, "", -1, "", "")
|
|
|
|
// valid control chunks
|
|
assertMakeName(`fish.chunk._info`, "fish", -1, "info", "")
|
|
assertMakeName(`fish.chunk._locks`, "fish", -2, "locks", "")
|
|
assertMakeName(`fish.chunk._blkinfo`, "fish", -3, "blkinfo", "")
|
|
assertMakeName(`fish.chunk._x2y`, "fish", -4, "x2y", "")
|
|
|
|
assertParseName(`fish.chunk._info`, "fish", -1, "info", "")
|
|
assertParseName(`fish.chunk._locks`, "fish", -1, "locks", "")
|
|
assertParseName(`fish.chunk._blkinfo`, "fish", -1, "blkinfo", "")
|
|
assertParseName(`fish.chunk._x2y`, "fish", -1, "x2y", "")
|
|
|
|
// valid temporary control chunks
|
|
assertMakeName(`fish.chunk._info_0001`, "fish", -1, "info", "1")
|
|
assertMakeName(`fish.chunk._locks_4321`, "fish", -2, "locks", "4321")
|
|
assertMakeName(`fish.chunk._uploads_abcd`, "fish", -3, "uploads", "abcd")
|
|
assertMakeName(`fish.chunk._blkinfo_xyzabcdef`, "fish", -4, "blkinfo", "xyzabcdef")
|
|
assertMakeName(`fish.chunk._x2y_1aaa`, "fish", -5, "x2y", "1aaa")
|
|
|
|
assertParseName(`fish.chunk._info_0001`, "fish", -1, "info", "0001")
|
|
assertParseName(`fish.chunk._locks_4321`, "fish", -1, "locks", "4321")
|
|
assertParseName(`fish.chunk._uploads_9abc`, "fish", -1, "uploads", "9abc")
|
|
assertParseName(`fish.chunk._blkinfo_xyzabcdef`, "fish", -1, "blkinfo", "xyzabcdef")
|
|
assertParseName(`fish.chunk._x2y_1aaa`, "fish", -1, "x2y", "1aaa")
|
|
|
|
// valid temporary control chunks (old temporary suffix, parse only)
|
|
assertParseName(`fish.chunk._info..tmp_0000000047`, "fish", -1, "info", "001b")
|
|
assertParseName(`fish.chunk._locks..tmp_0000054321`, "fish", -1, "locks", "15wx")
|
|
assertParseName(`fish.chunk._uploads..tmp_0000000000`, "fish", -1, "uploads", "0000")
|
|
assertParseName(`fish.chunk._blkinfo..tmp_9994567890123`, "fish", -1, "blkinfo", "3jjfvo3wr")
|
|
assertParseName(`fish.chunk._x2y..tmp_0000000000`, "fish", -1, "x2y", "0000")
|
|
|
|
// parsing invalid control chunk names
|
|
assertParseName(`fish.chunk.metadata`, "", -1, "", "") // must be prepended by underscore
|
|
assertParseName(`fish.chunk.info`, "", -1, "", "")
|
|
assertParseName(`fish.chunk.locks`, "", -1, "", "")
|
|
assertParseName(`fish.chunk.uploads`, "", -1, "", "")
|
|
|
|
assertParseName(`fish.chunk._os`, "", -1, "", "") // too short
|
|
assertParseName(`fish.chunk._metadata`, "", -1, "", "") // too long
|
|
assertParseName(`fish.chunk._blockinfo`, "", -1, "", "") // way too long
|
|
assertParseName(`fish.chunk._4me`, "", -1, "", "") // cannot start with digit
|
|
assertParseName(`fish.chunk._567`, "", -1, "", "") // cannot be all digits
|
|
assertParseName(`fish.chunk._me_ta`, "", -1, "", "") // punctuation not allowed
|
|
assertParseName(`fish.chunk._in-fo`, "", -1, "", "")
|
|
assertParseName(`fish.chunk._.bin`, "", -1, "", "")
|
|
assertParseName(`fish.chunk._.2xy`, "", -1, "", "")
|
|
|
|
// parsing invalid temporary control chunks
|
|
assertParseName(`fish.chunk._blkinfo1234`, "", -1, "", "") // missing underscore delimiter
|
|
assertParseName(`fish.chunk._info__1234`, "", -1, "", "") // extra underscore delimiter
|
|
assertParseName(`fish.chunk._info_123`, "", -1, "", "") // too short temporary suffix
|
|
assertParseName(`fish.chunk._info_1234567890`, "", -1, "", "") // too long temporary suffix
|
|
assertParseName(`fish.chunk._info_-1234`, "", -1, "", "") // temporary suffix must be positive
|
|
assertParseName(`fish.chunk._info_123E`, "", -1, "", "") // uppercase not allowed
|
|
assertParseName(`fish.chunk._info_12.3`, "", -1, "", "") // punctuation not allowed
|
|
|
|
assertParseName(`fish.chunk._locks..tmp_123456789`, "", -1, "", "")
|
|
assertParseName(`fish.chunk._meta..tmp_-1`, "", -1, "", "")
|
|
assertParseName(`fish.chunk._blockinfo..tmp_012345678901234567890123456789`, "", -1, "", "")
|
|
|
|
// short control chunk names: 3 letters ok, 1-2 letters not allowed
|
|
assertMakeName(`fish.chunk._ext`, "fish", -1, "ext", "")
|
|
assertParseName(`fish.chunk._int`, "fish", -1, "int", "")
|
|
|
|
assertMakeNamePanics("fish", -1, "in", "")
|
|
assertMakeNamePanics("fish", -1, "up", "4")
|
|
assertMakeNamePanics("fish", -1, "x", "")
|
|
assertMakeNamePanics("fish", -1, "c", "1z")
|
|
|
|
assertMakeName(`fish.chunk._ext_0000`, "fish", -1, "ext", "0")
|
|
assertMakeName(`fish.chunk._ext_0026`, "fish", -1, "ext", "26")
|
|
assertMakeName(`fish.chunk._int_0abc`, "fish", -1, "int", "abc")
|
|
assertMakeName(`fish.chunk._int_9xyz`, "fish", -1, "int", "9xyz")
|
|
assertMakeName(`fish.chunk._out_jj5fvo3wr`, "fish", -1, "out", "jj5fvo3wr")
|
|
assertMakeName(`fish.chunk._out_jj5fvo3wr`, "fish", -1, "out", "jj5fvo3wr")
|
|
|
|
assertParseName(`fish.chunk._ext_0000`, "fish", -1, "ext", "0000")
|
|
assertParseName(`fish.chunk._ext_0026`, "fish", -1, "ext", "0026")
|
|
assertParseName(`fish.chunk._int_0abc`, "fish", -1, "int", "0abc")
|
|
assertParseName(`fish.chunk._int_9xyz`, "fish", -1, "int", "9xyz")
|
|
assertParseName(`fish.chunk._out_jj5fvo3wr`, "fish", -1, "out", "jj5fvo3wr")
|
|
assertParseName(`fish.chunk._out_jj5fvo3wr`, "fish", -1, "out", "jj5fvo3wr")
|
|
|
|
// base file name can sometimes look like a valid chunk name
|
|
assertParseName(`fish.chunk.003.chunk.004`, "fish.chunk.003", 2, "", "")
|
|
assertParseName(`fish.chunk.003.chunk._info`, "fish.chunk.003", -1, "info", "")
|
|
assertParseName(`fish.chunk.003.chunk._Meta`, "", -1, "", "")
|
|
|
|
assertParseName(`fish.chunk._info.chunk.004`, "fish.chunk._info", 2, "", "")
|
|
assertParseName(`fish.chunk._info.chunk._info`, "fish.chunk._info", -1, "info", "")
|
|
assertParseName(`fish.chunk._info.chunk._info.chunk._Meta`, "", -1, "", "")
|
|
|
|
// base file name looking like a valid chunk name (old temporary suffix)
|
|
assertParseName(`fish.chunk.003.chunk.005..tmp_0000000022`, "fish.chunk.003", 3, "", "000m")
|
|
assertParseName(`fish.chunk.003.chunk._x..tmp_0000054321`, "", -1, "", "")
|
|
assertParseName(`fish.chunk._info.chunk.005..tmp_0000000023`, "fish.chunk._info", 3, "", "000n")
|
|
assertParseName(`fish.chunk._info.chunk._info.chunk._x..tmp_0000054321`, "", -1, "", "")
|
|
|
|
assertParseName(`fish.chunk.003.chunk._blkinfo..tmp_9994567890123`, "fish.chunk.003", -1, "blkinfo", "3jjfvo3wr")
|
|
assertParseName(`fish.chunk._info.chunk._blkinfo..tmp_9994567890123`, "fish.chunk._info", -1, "blkinfo", "3jjfvo3wr")
|
|
|
|
assertParseName(`fish.chunk.004..tmp_0000000021.chunk.004`, "fish.chunk.004..tmp_0000000021", 2, "", "")
|
|
assertParseName(`fish.chunk.004..tmp_0000000021.chunk.005..tmp_0000000025`, "fish.chunk.004..tmp_0000000021", 3, "", "000p")
|
|
assertParseName(`fish.chunk.004..tmp_0000000021.chunk._info`, "fish.chunk.004..tmp_0000000021", -1, "info", "")
|
|
assertParseName(`fish.chunk.004..tmp_0000000021.chunk._blkinfo..tmp_9994567890123`, "fish.chunk.004..tmp_0000000021", -1, "blkinfo", "3jjfvo3wr")
|
|
assertParseName(`fish.chunk.004..tmp_0000000021.chunk._Meta`, "", -1, "", "")
|
|
assertParseName(`fish.chunk.004..tmp_0000000021.chunk._x..tmp_0000054321`, "", -1, "", "")
|
|
|
|
assertParseName(`fish.chunk._blkinfo..tmp_9994567890123.chunk.004`, "fish.chunk._blkinfo..tmp_9994567890123", 2, "", "")
|
|
assertParseName(`fish.chunk._blkinfo..tmp_9994567890123.chunk.005..tmp_0000000026`, "fish.chunk._blkinfo..tmp_9994567890123", 3, "", "000q")
|
|
assertParseName(`fish.chunk._blkinfo..tmp_9994567890123.chunk._info`, "fish.chunk._blkinfo..tmp_9994567890123", -1, "info", "")
|
|
assertParseName(`fish.chunk._blkinfo..tmp_9994567890123.chunk._blkinfo..tmp_9994567890123`, "fish.chunk._blkinfo..tmp_9994567890123", -1, "blkinfo", "3jjfvo3wr")
|
|
assertParseName(`fish.chunk._blkinfo..tmp_9994567890123.chunk._info.chunk._Meta`, "", -1, "", "")
|
|
assertParseName(`fish.chunk._blkinfo..tmp_9994567890123.chunk._info.chunk._x..tmp_0000054321`, "", -1, "", "")
|
|
|
|
assertParseName(`fish.chunk._blkinfo..tmp_1234567890123456789.chunk.004`, "fish.chunk._blkinfo..tmp_1234567890123456789", 2, "", "")
|
|
assertParseName(`fish.chunk._blkinfo..tmp_1234567890123456789.chunk.005..tmp_0000000022`, "fish.chunk._blkinfo..tmp_1234567890123456789", 3, "", "000m")
|
|
assertParseName(`fish.chunk._blkinfo..tmp_1234567890123456789.chunk._info`, "fish.chunk._blkinfo..tmp_1234567890123456789", -1, "info", "")
|
|
assertParseName(`fish.chunk._blkinfo..tmp_1234567890123456789.chunk._blkinfo..tmp_9994567890123`, "fish.chunk._blkinfo..tmp_1234567890123456789", -1, "blkinfo", "3jjfvo3wr")
|
|
assertParseName(`fish.chunk._blkinfo..tmp_1234567890123456789.chunk._info.chunk._Meta`, "", -1, "", "")
|
|
assertParseName(`fish.chunk._blkinfo..tmp_1234567890123456789.chunk._info.chunk._x..tmp_0000054321`, "", -1, "", "")
|
|
|
|
// attempts to make invalid chunk names
|
|
assertMakeNamePanics("fish", -1, "", "") // neither data nor control
|
|
assertMakeNamePanics("fish", 0, "info", "") // both data and control
|
|
assertMakeNamePanics("fish", -1, "metadata", "") // control type too long
|
|
assertMakeNamePanics("fish", -1, "blockinfo", "") // control type way too long
|
|
assertMakeNamePanics("fish", -1, "2xy", "") // first digit not allowed
|
|
assertMakeNamePanics("fish", -1, "123", "") // all digits not allowed
|
|
assertMakeNamePanics("fish", -1, "Meta", "") // only lower case letters allowed
|
|
assertMakeNamePanics("fish", -1, "in-fo", "") // punctuation not allowed
|
|
assertMakeNamePanics("fish", -1, "_info", "")
|
|
assertMakeNamePanics("fish", -1, "info_", "")
|
|
assertMakeNamePanics("fish", -2, ".bind", "")
|
|
assertMakeNamePanics("fish", -2, "bind.", "")
|
|
|
|
assertMakeNamePanics("fish", -1, "", "1") // neither data nor control
|
|
assertMakeNamePanics("fish", 0, "info", "23") // both data and control
|
|
assertMakeNamePanics("fish", -1, "metadata", "45") // control type too long
|
|
assertMakeNamePanics("fish", -1, "blockinfo", "7") // control type way too long
|
|
assertMakeNamePanics("fish", -1, "2xy", "abc") // first digit not allowed
|
|
assertMakeNamePanics("fish", -1, "123", "def") // all digits not allowed
|
|
assertMakeNamePanics("fish", -1, "Meta", "mnk") // only lower case letters allowed
|
|
assertMakeNamePanics("fish", -1, "in-fo", "xyz") // punctuation not allowed
|
|
assertMakeNamePanics("fish", -1, "_info", "5678")
|
|
assertMakeNamePanics("fish", -1, "info_", "999")
|
|
assertMakeNamePanics("fish", -2, ".bind", "0")
|
|
assertMakeNamePanics("fish", -2, "bind.", "0")
|
|
|
|
assertMakeNamePanics("fish", 0, "", "1234567890") // temporary suffix too long
|
|
assertMakeNamePanics("fish", 0, "", "123F4") // uppercase not allowed
|
|
assertMakeNamePanics("fish", 0, "", "123.") // punctuation not allowed
|
|
assertMakeNamePanics("fish", 0, "", "_123")
|
|
}
|
|
|
|
func testSmallFileInternals(t *testing.T, f *Fs) {
|
|
const dir = "small"
|
|
ctx := context.Background()
|
|
saveOpt := f.opt
|
|
defer func() {
|
|
f.opt.FailHard = false
|
|
_ = operations.Purge(ctx, f.base, dir)
|
|
f.opt = saveOpt
|
|
}()
|
|
f.opt.FailHard = false
|
|
|
|
modTime := fstest.Time("2001-02-03T04:05:06.499999999Z")
|
|
|
|
checkSmallFileInternals := func(obj fs.Object) {
|
|
assert.NotNil(t, obj)
|
|
o, ok := obj.(*Object)
|
|
assert.True(t, ok)
|
|
assert.NotNil(t, o)
|
|
if o == nil {
|
|
return
|
|
}
|
|
switch {
|
|
case !f.useMeta:
|
|
// If meta format is "none", non-chunked file (even empty)
|
|
// internally is a single chunk without meta object.
|
|
assert.Nil(t, o.main)
|
|
assert.True(t, o.isComposite()) // sorry, sometimes a name is misleading
|
|
assert.Equal(t, 1, len(o.chunks))
|
|
case f.hashAll:
|
|
// Consistent hashing forces meta object on small files too
|
|
assert.NotNil(t, o.main)
|
|
assert.True(t, o.isComposite())
|
|
assert.Equal(t, 1, len(o.chunks))
|
|
default:
|
|
// normally non-chunked file is kept in the Object's main field
|
|
assert.NotNil(t, o.main)
|
|
assert.False(t, o.isComposite())
|
|
assert.Equal(t, 0, len(o.chunks))
|
|
}
|
|
}
|
|
|
|
checkContents := func(obj fs.Object, contents string) {
|
|
assert.NotNil(t, obj)
|
|
assert.Equal(t, int64(len(contents)), obj.Size())
|
|
|
|
r, err := obj.Open(ctx)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, r)
|
|
if r == nil {
|
|
return
|
|
}
|
|
data, err := ioutil.ReadAll(r)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, contents, string(data))
|
|
_ = r.Close()
|
|
}
|
|
|
|
checkHashsum := func(obj fs.Object) {
|
|
var ht hash.Type
|
|
switch {
|
|
case !f.hashAll:
|
|
return
|
|
case f.useMD5:
|
|
ht = hash.MD5
|
|
case f.useSHA1:
|
|
ht = hash.SHA1
|
|
default:
|
|
return
|
|
}
|
|
// even empty files must have hashsum in consistent mode
|
|
sum, err := obj.Hash(ctx, ht)
|
|
assert.NoError(t, err)
|
|
assert.NotEqual(t, sum, "")
|
|
}
|
|
|
|
checkSmallFile := func(name, contents string) {
|
|
filename := path.Join(dir, name)
|
|
item := fstest.Item{Path: filename, ModTime: modTime}
|
|
_, put := fstests.PutTestContents(ctx, t, f, &item, contents, false)
|
|
assert.NotNil(t, put)
|
|
checkSmallFileInternals(put)
|
|
checkContents(put, contents)
|
|
checkHashsum(put)
|
|
|
|
// objects returned by Put and NewObject must have similar structure
|
|
obj, err := f.NewObject(ctx, filename)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, obj)
|
|
checkSmallFileInternals(obj)
|
|
checkContents(obj, contents)
|
|
checkHashsum(obj)
|
|
|
|
_ = obj.Remove(ctx)
|
|
_ = put.Remove(ctx) // for good
|
|
}
|
|
|
|
checkSmallFile("emptyfile", "")
|
|
checkSmallFile("smallfile", "Ok")
|
|
}
|
|
|
|
func testPreventCorruption(t *testing.T, f *Fs) {
|
|
if f.opt.ChunkSize > 50 {
|
|
t.Skip("this test requires small chunks")
|
|
}
|
|
const dir = "corrupted"
|
|
ctx := context.Background()
|
|
saveOpt := f.opt
|
|
defer func() {
|
|
f.opt.FailHard = false
|
|
_ = operations.Purge(ctx, f.base, dir)
|
|
f.opt = saveOpt
|
|
}()
|
|
f.opt.FailHard = true
|
|
|
|
contents := random.String(250)
|
|
modTime := fstest.Time("2001-02-03T04:05:06.499999999Z")
|
|
const overlapMessage = "chunk overlap"
|
|
|
|
assertOverlapError := func(err error) {
|
|
assert.Error(t, err)
|
|
if err != nil {
|
|
assert.Contains(t, err.Error(), overlapMessage)
|
|
}
|
|
}
|
|
|
|
newFile := func(name string) fs.Object {
|
|
item := fstest.Item{Path: path.Join(dir, name), ModTime: modTime}
|
|
_, obj := fstests.PutTestContents(ctx, t, f, &item, contents, true)
|
|
require.NotNil(t, obj)
|
|
return obj
|
|
}
|
|
billyObj := newFile("billy")
|
|
|
|
billyChunkName := func(chunkNo int) string {
|
|
return f.makeChunkName(billyObj.Remote(), chunkNo, "", "")
|
|
}
|
|
|
|
err := f.Mkdir(ctx, billyChunkName(1))
|
|
assertOverlapError(err)
|
|
|
|
_, err = f.Move(ctx, newFile("silly1"), billyChunkName(2))
|
|
assert.Error(t, err)
|
|
assert.True(t, err == fs.ErrorCantMove || (err != nil && strings.Contains(err.Error(), overlapMessage)))
|
|
|
|
_, err = f.Copy(ctx, newFile("silly2"), billyChunkName(3))
|
|
assert.Error(t, err)
|
|
assert.True(t, err == fs.ErrorCantCopy || (err != nil && strings.Contains(err.Error(), overlapMessage)))
|
|
|
|
// accessing chunks in strict mode is prohibited
|
|
f.opt.FailHard = true
|
|
billyChunk4Name := billyChunkName(4)
|
|
billyChunk4, err := f.NewObject(ctx, billyChunk4Name)
|
|
assertOverlapError(err)
|
|
|
|
f.opt.FailHard = false
|
|
billyChunk4, err = f.NewObject(ctx, billyChunk4Name)
|
|
assert.NoError(t, err)
|
|
require.NotNil(t, billyChunk4)
|
|
|
|
f.opt.FailHard = true
|
|
_, err = f.Put(ctx, bytes.NewBufferString(contents), billyChunk4)
|
|
assertOverlapError(err)
|
|
|
|
// you can freely read chunks (if you have an object)
|
|
r, err := billyChunk4.Open(ctx)
|
|
assert.NoError(t, err)
|
|
var chunkContents []byte
|
|
assert.NotPanics(t, func() {
|
|
chunkContents, err = ioutil.ReadAll(r)
|
|
_ = r.Close()
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.NotEqual(t, contents, string(chunkContents))
|
|
|
|
// but you can't change them
|
|
err = billyChunk4.Update(ctx, bytes.NewBufferString(contents), newFile("silly3"))
|
|
assertOverlapError(err)
|
|
|
|
// Remove isn't special, you can't corrupt files even if you have an object
|
|
err = billyChunk4.Remove(ctx)
|
|
assertOverlapError(err)
|
|
|
|
// recreate billy in case it was anyhow corrupted
|
|
willyObj := newFile("willy")
|
|
willyChunkName := f.makeChunkName(willyObj.Remote(), 1, "", "")
|
|
f.opt.FailHard = false
|
|
willyChunk, err := f.NewObject(ctx, willyChunkName)
|
|
f.opt.FailHard = true
|
|
assert.NoError(t, err)
|
|
require.NotNil(t, willyChunk)
|
|
|
|
_, err = operations.Copy(ctx, f, willyChunk, willyChunkName, newFile("silly4"))
|
|
assertOverlapError(err)
|
|
|
|
// operations.Move will return error when chunker's Move refused
|
|
// to corrupt target file, but reverts to copy/delete method
|
|
// still trying to delete target chunk. Chunker must come to rescue.
|
|
_, err = operations.Move(ctx, f, willyChunk, willyChunkName, newFile("silly5"))
|
|
assertOverlapError(err)
|
|
r, err = willyChunk.Open(ctx)
|
|
assert.NoError(t, err)
|
|
assert.NotPanics(t, func() {
|
|
_, err = ioutil.ReadAll(r)
|
|
_ = r.Close()
|
|
})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func testChunkNumberOverflow(t *testing.T, f *Fs) {
|
|
if f.opt.ChunkSize > 50 {
|
|
t.Skip("this test requires small chunks")
|
|
}
|
|
const dir = "wreaked"
|
|
const wreakNumber = 10200300
|
|
ctx := context.Background()
|
|
saveOpt := f.opt
|
|
defer func() {
|
|
f.opt.FailHard = false
|
|
_ = operations.Purge(ctx, f.base, dir)
|
|
f.opt = saveOpt
|
|
}()
|
|
|
|
modTime := fstest.Time("2001-02-03T04:05:06.499999999Z")
|
|
contents := random.String(100)
|
|
|
|
newFile := func(f fs.Fs, name string) (fs.Object, string) {
|
|
filename := path.Join(dir, name)
|
|
item := fstest.Item{Path: filename, ModTime: modTime}
|
|
_, obj := fstests.PutTestContents(ctx, t, f, &item, contents, true)
|
|
require.NotNil(t, obj)
|
|
return obj, filename
|
|
}
|
|
|
|
f.opt.FailHard = false
|
|
file, fileName := newFile(f, "wreaker")
|
|
wreak, _ := newFile(f.base, f.makeChunkName("wreaker", wreakNumber, "", ""))
|
|
|
|
f.opt.FailHard = false
|
|
fstest.CheckListingWithRoot(t, f, dir, nil, nil, f.Precision())
|
|
_, err := f.NewObject(ctx, fileName)
|
|
assert.Error(t, err)
|
|
|
|
f.opt.FailHard = true
|
|
_, err = f.List(ctx, dir)
|
|
assert.Error(t, err)
|
|
_, err = f.NewObject(ctx, fileName)
|
|
assert.Error(t, err)
|
|
|
|
f.opt.FailHard = false
|
|
_ = wreak.Remove(ctx)
|
|
_ = file.Remove(ctx)
|
|
}
|
|
|
|
func testMetadataInput(t *testing.T, f *Fs) {
|
|
const minChunkForTest = 50
|
|
if f.opt.ChunkSize < minChunkForTest {
|
|
t.Skip("this test requires chunks that fit metadata")
|
|
}
|
|
|
|
const dir = "usermeta"
|
|
ctx := context.Background()
|
|
saveOpt := f.opt
|
|
defer func() {
|
|
f.opt.FailHard = false
|
|
_ = operations.Purge(ctx, f.base, dir)
|
|
f.opt = saveOpt
|
|
}()
|
|
f.opt.FailHard = false
|
|
|
|
modTime := fstest.Time("2001-02-03T04:05:06.499999999Z")
|
|
|
|
putFile := func(f fs.Fs, name, contents, message string, check bool) fs.Object {
|
|
item := fstest.Item{Path: name, ModTime: modTime}
|
|
_, obj := fstests.PutTestContents(ctx, t, f, &item, contents, check)
|
|
assert.NotNil(t, obj, message)
|
|
return obj
|
|
}
|
|
|
|
runSubtest := func(contents, name string) {
|
|
description := fmt.Sprintf("file with %s metadata", name)
|
|
filename := path.Join(dir, name)
|
|
require.True(t, len(contents) > 2 && len(contents) < minChunkForTest, description+" test data is correct")
|
|
|
|
part := putFile(f.base, f.makeChunkName(filename, 0, "", ""), "oops", "", true)
|
|
_ = putFile(f, filename, contents, "upload "+description, false)
|
|
|
|
obj, err := f.NewObject(ctx, filename)
|
|
assert.NoError(t, err, "access "+description)
|
|
assert.NotNil(t, obj)
|
|
assert.Equal(t, int64(len(contents)), obj.Size(), "size "+description)
|
|
|
|
o, ok := obj.(*Object)
|
|
assert.NotNil(t, ok)
|
|
if o != nil {
|
|
assert.True(t, o.isComposite() && len(o.chunks) == 1, description+" is forced composite")
|
|
o = nil
|
|
}
|
|
|
|
defer func() {
|
|
_ = obj.Remove(ctx)
|
|
_ = part.Remove(ctx)
|
|
}()
|
|
|
|
r, err := obj.Open(ctx)
|
|
assert.NoError(t, err, "open "+description)
|
|
assert.NotNil(t, r, "open stream of "+description)
|
|
if err == nil && r != nil {
|
|
data, err := ioutil.ReadAll(r)
|
|
assert.NoError(t, err, "read all of "+description)
|
|
assert.Equal(t, contents, string(data), description+" contents is ok")
|
|
_ = r.Close()
|
|
}
|
|
}
|
|
|
|
metaData, err := marshalSimpleJSON(ctx, 3, 1, "", "")
|
|
require.NoError(t, err)
|
|
todaysMeta := string(metaData)
|
|
runSubtest(todaysMeta, "today")
|
|
|
|
pastMeta := regexp.MustCompile(`"ver":[0-9]+`).ReplaceAllLiteralString(todaysMeta, `"ver":1`)
|
|
pastMeta = regexp.MustCompile(`"size":[0-9]+`).ReplaceAllLiteralString(pastMeta, `"size":0`)
|
|
runSubtest(pastMeta, "past")
|
|
|
|
futureMeta := regexp.MustCompile(`"ver":[0-9]+`).ReplaceAllLiteralString(todaysMeta, `"ver":999`)
|
|
futureMeta = regexp.MustCompile(`"nchunks":[0-9]+`).ReplaceAllLiteralString(futureMeta, `"nchunks":0,"x":"y"`)
|
|
runSubtest(futureMeta, "future")
|
|
}
|
|
|
|
// InternalTest dispatches all internal tests
|
|
func (f *Fs) InternalTest(t *testing.T) {
|
|
t.Run("PutLarge", func(t *testing.T) {
|
|
if *UploadKilobytes <= 0 {
|
|
t.Skip("-upload-kilobytes is not set")
|
|
}
|
|
testPutLarge(t, f, *UploadKilobytes)
|
|
})
|
|
t.Run("ChunkNameFormat", func(t *testing.T) {
|
|
testChunkNameFormat(t, f)
|
|
})
|
|
t.Run("SmallFileInternals", func(t *testing.T) {
|
|
testSmallFileInternals(t, f)
|
|
})
|
|
t.Run("PreventCorruption", func(t *testing.T) {
|
|
testPreventCorruption(t, f)
|
|
})
|
|
t.Run("ChunkNumberOverflow", func(t *testing.T) {
|
|
testChunkNumberOverflow(t, f)
|
|
})
|
|
t.Run("MetadataInput", func(t *testing.T) {
|
|
testMetadataInput(t, f)
|
|
})
|
|
}
|
|
|
|
var _ fstests.InternalTester = (*Fs)(nil)
|