package chunker import ( "bytes" "context" "flag" "fmt" "io" "path" "regexp" "strings" "testing" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/config/configmap" "github.com/rclone/rclone/fs/fspath" "github.com/rclone/rclone/fs/hash" "github.com/rclone/rclone/fs/object" "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.Kibi), }) }) } type settings map[string]interface{} func deriveFs(ctx context.Context, t *testing.T, f fs.Fs, path string, opts settings) fs.Fs { fsName := strings.Split(f.Name(), "{")[0] // strip off hash configMap := configmap.Simple{} for key, val := range opts { configMap[key] = fmt.Sprintf("%v", val) } rpath := fspath.JoinRootPath(f.Root(), path) remote := fmt.Sprintf("%s,%s:%s", fsName, configMap.String(), rpath) fixFs, err := fs.NewFs(ctx, remote) require.NoError(t, err) return fixFs } var mtime1 = fstest.Time("2001-02-03T04:05:06.499999999Z") func testPutFile(ctx context.Context, t *testing.T, f fs.Fs, name, contents, message string, check bool) fs.Object { item := fstest.Item{Path: name, ModTime: mtime1} obj := fstests.PutTestContents(ctx, t, f, &item, contents, check) assert.NotNil(t, obj, message) return obj } // 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 := io.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") billyTxn := billyObj.(*Object).xactID if f.useNoRename { require.True(t, billyTxn != "") } else { require.True(t, billyTxn == "") } billyChunkName := func(chunkNo int) string { return f.makeChunkName(billyObj.Remote(), chunkNo, "", billyTxn) } 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) _, err = f.base.NewObject(ctx, billyChunk4Name) require.NoError(t, err) _, 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 = io.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") willyTxn := willyObj.(*Object).xactID willyChunkName := f.makeChunkName(willyObj.Remote(), 1, "", willyTxn) 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 = io.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) (obj fs.Object, filename string, txnID 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) if chunkObj, isChunkObj := obj.(*Object); isChunkObj { txnID = chunkObj.xactID } return } f.opt.FailHard = false file, fileName, fileTxn := newFile(f, "wreaker") wreak, _, _ := newFile(f.base, f.makeChunkName("wreaker", wreakNumber, "", fileTxn)) 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 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 := testPutFile(ctx, t, f.base, f.makeChunkName(filename, 0, "", ""), "oops", "", true) _ = testPutFile(ctx, t, 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 := io.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") } // Test that chunker refuses to change on objects with future/unknown metadata func testFutureProof(t *testing.T, f *Fs) { if !f.useMeta { t.Skip("this test requires metadata support") } saveOpt := f.opt ctx := context.Background() f.opt.FailHard = true const dir = "future" const file = dir + "/test" defer func() { f.opt.FailHard = false _ = operations.Purge(ctx, f.base, dir) f.opt = saveOpt }() modTime := fstest.Time("2001-02-03T04:05:06.499999999Z") putPart := func(name string, part int, data, msg string) { if part > 0 { name = f.makeChunkName(name, part-1, "", "") } item := fstest.Item{Path: name, ModTime: modTime} obj := fstests.PutTestContents(ctx, t, f.base, &item, data, true) assert.NotNil(t, obj, msg) } // simulate chunked object from future meta := `{"ver":999,"nchunks":3,"size":9,"garbage":"litter","sha1":"0707f2970043f9f7c22029482db27733deaec029"}` putPart(file, 0, meta, "metaobject") putPart(file, 1, "abc", "chunk1") putPart(file, 2, "def", "chunk2") putPart(file, 3, "ghi", "chunk3") // List should succeed ls, err := f.List(ctx, dir) assert.NoError(t, err) assert.Equal(t, 1, len(ls)) assert.Equal(t, int64(9), ls[0].Size()) // NewObject should succeed obj, err := f.NewObject(ctx, file) assert.NoError(t, err) assert.Equal(t, file, obj.Remote()) assert.Equal(t, int64(9), obj.Size()) // Hash must fail _, err = obj.Hash(ctx, hash.SHA1) assert.Equal(t, ErrMetaUnknown, err) // Move must fail mobj, err := operations.Move(ctx, f, nil, file+"2", obj) assert.Nil(t, mobj) assert.Error(t, err) if err != nil { assert.Contains(t, err.Error(), "please upgrade rclone") } // Put must fail oi := object.NewStaticObjectInfo(file, modTime, 3, true, nil, nil) buf := bytes.NewBufferString("abc") _, err = f.Put(ctx, buf, oi) assert.Error(t, err) // Rcat must fail in := io.NopCloser(bytes.NewBufferString("abc")) robj, err := operations.Rcat(ctx, f, file, in, modTime, nil) assert.Nil(t, robj) assert.NotNil(t, err) if err != nil { assert.Contains(t, err.Error(), "please upgrade rclone") } } // The newer method of doing transactions without renaming should still be able to correctly process chunks that were created with renaming // If you attempt to do the inverse, however, the data chunks will be ignored causing commands to perform incorrectly func testBackwardsCompatibility(t *testing.T, f *Fs) { if !f.useMeta { t.Skip("Can't do norename transactions without metadata") } const dir = "backcomp" ctx := context.Background() saveOpt := f.opt saveUseNoRename := f.useNoRename defer func() { f.opt.FailHard = false _ = operations.Purge(ctx, f.base, dir) f.opt = saveOpt f.useNoRename = saveUseNoRename }() f.opt.ChunkSize = fs.SizeSuffix(10) modTime := fstest.Time("2001-02-03T04:05:06.499999999Z") contents := random.String(250) 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 f.useNoRename = false file, fileName := newFile(f, "renamefile") f.opt.FailHard = false item := fstest.NewItem(fileName, contents, modTime) var items []fstest.Item items = append(items, item) f.useNoRename = true fstest.CheckListingWithRoot(t, f, dir, items, nil, f.Precision()) _, err := f.NewObject(ctx, fileName) assert.NoError(t, err) f.opt.FailHard = true _, err = f.List(ctx, dir) assert.NoError(t, err) f.opt.FailHard = false _ = file.Remove(ctx) } func testChunkerServerSideMove(t *testing.T, f *Fs) { if !f.useMeta { t.Skip("Can't test norename transactions without metadata") } ctx := context.Background() const dir = "servermovetest" subRemote := fmt.Sprintf("%s:%s/%s", f.Name(), f.Root(), dir) subFs1, err := fs.NewFs(ctx, subRemote+"/subdir1") assert.NoError(t, err) fs1, isChunkerFs := subFs1.(*Fs) assert.True(t, isChunkerFs) fs1.useNoRename = false fs1.opt.ChunkSize = fs.SizeSuffix(3) subFs2, err := fs.NewFs(ctx, subRemote+"/subdir2") assert.NoError(t, err) fs2, isChunkerFs := subFs2.(*Fs) assert.True(t, isChunkerFs) fs2.useNoRename = true fs2.opt.ChunkSize = fs.SizeSuffix(3) modTime := fstest.Time("2001-02-03T04:05:06.499999999Z") item := fstest.Item{Path: "movefile", ModTime: modTime} contents := "abcdef" file := fstests.PutTestContents(ctx, t, fs1, &item, contents, true) dstOverwritten, _ := fs2.NewObject(ctx, "movefile") dstFile, err := operations.Move(ctx, fs2, dstOverwritten, "movefile", file) assert.NoError(t, err) assert.Equal(t, int64(len(contents)), dstFile.Size()) r, err := dstFile.Open(ctx) assert.NoError(t, err) assert.NotNil(t, r) data, err := io.ReadAll(r) assert.NoError(t, err) assert.Equal(t, contents, string(data)) _ = r.Close() _ = operations.Purge(ctx, f.base, dir) } // Test that md5all creates metadata even for small files func testMD5AllSlow(t *testing.T, f *Fs) { ctx := context.Background() fsResult := deriveFs(ctx, t, f, "md5all", settings{ "chunk_size": "1P", "name_format": "*.#", "hash_type": "md5all", "transactions": "rename", "meta_format": "simplejson", }) chunkFs, ok := fsResult.(*Fs) require.True(t, ok, "fs must be a chunker remote") baseFs := chunkFs.base if !baseFs.Features().SlowHash { t.Skipf("this test needs a base fs with slow hash, e.g. local") } assert.True(t, chunkFs.useMD5, "must use md5") assert.True(t, chunkFs.hashAll, "must hash all files") _ = testPutFile(ctx, t, chunkFs, "file", "-", "error", true) obj, err := chunkFs.NewObject(ctx, "file") require.NoError(t, err) sum, err := obj.Hash(ctx, hash.MD5) assert.NoError(t, err) assert.Equal(t, "336d5ebc5436534e61d16e63ddfca327", sum) list, err := baseFs.List(ctx, "") require.NoError(t, err) assert.Equal(t, 2, len(list)) _, err = baseFs.NewObject(ctx, "file") assert.NoError(t, err, "metadata must be created") _, err = baseFs.NewObject(ctx, "file.1") assert.NoError(t, err, "first chunk must be created") require.NoError(t, operations.Purge(ctx, baseFs, "")) } // 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) }) t.Run("FutureProof", func(t *testing.T) { testFutureProof(t, f) }) t.Run("BackwardsCompatibility", func(t *testing.T) { testBackwardsCompatibility(t, f) }) t.Run("ChunkerServerSideMove", func(t *testing.T) { testChunkerServerSideMove(t, f) }) t.Run("MD5AllSlow", func(t *testing.T) { testMD5AllSlow(t, f) }) } var _ fstests.InternalTester = (*Fs)(nil)