2017-10-30 05:14:05 +08:00
|
|
|
package vfs
|
|
|
|
|
|
|
|
import (
|
2019-06-17 16:34:30 +08:00
|
|
|
"context"
|
2020-07-08 18:40:41 +08:00
|
|
|
"fmt"
|
2017-10-30 05:14:05 +08:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"testing"
|
|
|
|
|
2019-12-05 20:58:24 +08:00
|
|
|
"github.com/rclone/rclone/fs"
|
2019-12-13 18:55:13 +08:00
|
|
|
"github.com/rclone/rclone/fs/operations"
|
2019-07-29 01:47:38 +08:00
|
|
|
"github.com/rclone/rclone/fstest"
|
2019-09-14 20:09:07 +08:00
|
|
|
"github.com/rclone/rclone/fstest/mockfs"
|
|
|
|
"github.com/rclone/rclone/fstest/mockobject"
|
2020-02-28 22:44:15 +08:00
|
|
|
"github.com/rclone/rclone/vfs/vfscommon"
|
2017-10-30 05:14:05 +08:00
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
2020-04-17 18:18:58 +08:00
|
|
|
func fileCreate(t *testing.T, mode vfscommon.CacheMode) (r *fstest.Run, vfs *VFS, fh *File, item fstest.Item, cleanup func()) {
|
2020-02-28 22:44:15 +08:00
|
|
|
opt := vfscommon.DefaultOpt
|
2019-12-05 20:58:24 +08:00
|
|
|
opt.CacheMode = mode
|
2020-04-17 18:18:58 +08:00
|
|
|
opt.WriteBack = writeBackDelay
|
|
|
|
r, vfs, cleanup = newTestVFSOpt(t, &opt)
|
2017-10-30 05:14:05 +08:00
|
|
|
|
2019-06-17 16:34:30 +08:00
|
|
|
file1 := r.WriteObject(context.Background(), "dir/file1", "file1 contents", t1)
|
2021-11-09 19:43:36 +08:00
|
|
|
r.CheckRemoteItems(t, file1)
|
2017-10-30 05:14:05 +08:00
|
|
|
|
|
|
|
node, err := vfs.Stat("dir/file1")
|
|
|
|
require.NoError(t, err)
|
2019-08-26 20:43:11 +08:00
|
|
|
require.True(t, node.Mode().IsRegular())
|
2017-10-30 05:14:05 +08:00
|
|
|
|
2020-04-17 18:18:58 +08:00
|
|
|
return r, vfs, node.(*File), file1, cleanup
|
2017-10-30 05:14:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestFileMethods(t *testing.T) {
|
2020-04-17 18:18:58 +08:00
|
|
|
r, vfs, file, _, cleanup := fileCreate(t, vfscommon.CacheModeOff)
|
|
|
|
defer cleanup()
|
2017-10-30 05:14:05 +08:00
|
|
|
|
|
|
|
// String
|
|
|
|
assert.Equal(t, "dir/file1", file.String())
|
|
|
|
assert.Equal(t, "<nil *File>", (*File)(nil).String())
|
|
|
|
|
|
|
|
// IsDir
|
|
|
|
assert.Equal(t, false, file.IsDir())
|
|
|
|
|
|
|
|
// IsFile
|
|
|
|
assert.Equal(t, true, file.IsFile())
|
|
|
|
|
|
|
|
// Mode
|
|
|
|
assert.Equal(t, vfs.Opt.FilePerms, file.Mode())
|
|
|
|
|
|
|
|
// Name
|
|
|
|
assert.Equal(t, "file1", file.Name())
|
|
|
|
|
2017-11-18 19:47:21 +08:00
|
|
|
// Path
|
|
|
|
assert.Equal(t, "dir/file1", file.Path())
|
|
|
|
|
2017-10-30 05:14:05 +08:00
|
|
|
// Sys
|
|
|
|
assert.Equal(t, nil, file.Sys())
|
|
|
|
|
2020-05-02 01:30:06 +08:00
|
|
|
// SetSys
|
|
|
|
file.SetSys(42)
|
|
|
|
assert.Equal(t, 42, file.Sys())
|
|
|
|
|
2017-10-30 05:14:05 +08:00
|
|
|
// Inode
|
|
|
|
assert.NotEqual(t, uint64(0), file.Inode())
|
|
|
|
|
|
|
|
// Node
|
|
|
|
assert.Equal(t, file, file.Node())
|
|
|
|
|
|
|
|
// ModTime
|
|
|
|
assert.WithinDuration(t, t1, file.ModTime(), r.Fremote.Precision())
|
|
|
|
|
|
|
|
// Size
|
|
|
|
assert.Equal(t, int64(14), file.Size())
|
|
|
|
|
2017-11-18 23:48:49 +08:00
|
|
|
// Sync
|
|
|
|
assert.NoError(t, file.Sync())
|
2017-10-30 05:14:05 +08:00
|
|
|
|
|
|
|
// DirEntry
|
|
|
|
assert.Equal(t, file.o, file.DirEntry())
|
|
|
|
|
|
|
|
// Dir
|
|
|
|
assert.Equal(t, file.d, file.Dir())
|
|
|
|
|
|
|
|
// VFS
|
|
|
|
assert.Equal(t, vfs, file.VFS())
|
|
|
|
}
|
|
|
|
|
2021-03-14 20:17:29 +08:00
|
|
|
func testFileSetModTime(t *testing.T, cacheMode vfscommon.CacheMode, open bool, write bool) {
|
|
|
|
if !canSetModTimeValue {
|
|
|
|
t.Skip("can't set mod time")
|
|
|
|
}
|
|
|
|
r, vfs, file, file1, cleanup := fileCreate(t, cacheMode)
|
2020-04-17 18:18:58 +08:00
|
|
|
defer cleanup()
|
2019-02-28 01:14:32 +08:00
|
|
|
if !canSetModTime(t, r) {
|
2020-04-17 18:18:58 +08:00
|
|
|
t.Skip("can't set mod time")
|
2019-02-28 01:14:32 +08:00
|
|
|
}
|
2017-10-30 05:14:05 +08:00
|
|
|
|
2021-03-14 20:17:29 +08:00
|
|
|
var (
|
|
|
|
err error
|
|
|
|
fd Handle
|
|
|
|
contents = "file1 contents"
|
|
|
|
)
|
|
|
|
if open {
|
|
|
|
// Open with write intent
|
|
|
|
if cacheMode != vfscommon.CacheModeOff {
|
|
|
|
fd, err = file.Open(os.O_WRONLY)
|
|
|
|
if write {
|
|
|
|
contents = "hello contents"
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Can't write without O_TRUNC with CacheMode Off
|
|
|
|
fd, err = file.Open(os.O_WRONLY | os.O_TRUNC)
|
|
|
|
if write {
|
|
|
|
contents = "hello"
|
|
|
|
} else {
|
|
|
|
contents = ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Write some data
|
|
|
|
if write {
|
|
|
|
_, err = fd.WriteString("hello")
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
err = file.SetModTime(t2)
|
2017-10-30 05:14:05 +08:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-03-14 20:17:29 +08:00
|
|
|
if open {
|
|
|
|
require.NoError(t, fd.Close())
|
|
|
|
vfs.WaitForWriters(waitForWritersDelay)
|
|
|
|
}
|
|
|
|
|
|
|
|
file1 = fstest.NewItem(file1.Path, contents, t2)
|
2021-11-09 19:43:36 +08:00
|
|
|
r.CheckRemoteItems(t, file1)
|
2017-10-30 05:14:05 +08:00
|
|
|
|
|
|
|
vfs.Opt.ReadOnly = true
|
|
|
|
err = file.SetModTime(t2)
|
|
|
|
assert.Equal(t, EROFS, err)
|
|
|
|
}
|
|
|
|
|
2021-03-14 20:17:29 +08:00
|
|
|
// Test various combinations of setting mod times with and
|
|
|
|
// without the cache and with and without opening or writing
|
|
|
|
// to the file.
|
|
|
|
//
|
|
|
|
// Each of these tests a different path through the VFS code.
|
|
|
|
func TestFileSetModTime(t *testing.T) {
|
|
|
|
for _, cacheMode := range []vfscommon.CacheMode{vfscommon.CacheModeOff, vfscommon.CacheModeFull} {
|
|
|
|
for _, open := range []bool{false, true} {
|
|
|
|
for _, write := range []bool{false, true} {
|
|
|
|
if write && !open {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
t.Run(fmt.Sprintf("cache=%v,open=%v,write=%v", cacheMode, open, write), func(t *testing.T) {
|
|
|
|
testFileSetModTime(t, cacheMode, open, write)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-05 20:58:24 +08:00
|
|
|
func fileCheckContents(t *testing.T, file *File) {
|
|
|
|
fd, err := file.Open(os.O_RDONLY)
|
2017-10-30 05:14:05 +08:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
contents, err := ioutil.ReadAll(fd)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "file1 contents", string(contents))
|
|
|
|
|
|
|
|
require.NoError(t, fd.Close())
|
|
|
|
}
|
|
|
|
|
2019-12-05 20:58:24 +08:00
|
|
|
func TestFileOpenRead(t *testing.T) {
|
2020-04-17 18:18:58 +08:00
|
|
|
_, _, file, _, cleanup := fileCreate(t, vfscommon.CacheModeOff)
|
|
|
|
defer cleanup()
|
2019-12-05 20:58:24 +08:00
|
|
|
|
|
|
|
fileCheckContents(t, file)
|
|
|
|
}
|
|
|
|
|
2019-09-14 20:09:07 +08:00
|
|
|
func TestFileOpenReadUnknownSize(t *testing.T) {
|
|
|
|
var (
|
|
|
|
contents = []byte("file contents")
|
|
|
|
remote = "file.txt"
|
|
|
|
ctx = context.Background()
|
|
|
|
)
|
|
|
|
|
|
|
|
// create a mock object which returns size -1
|
|
|
|
o := mockobject.New(remote).WithContent(contents, mockobject.SeekModeNone)
|
|
|
|
o.SetUnknownSize(true)
|
|
|
|
assert.Equal(t, int64(-1), o.Size())
|
|
|
|
|
|
|
|
// add it to a mock fs
|
2020-11-06 00:00:08 +08:00
|
|
|
f := mockfs.NewFs(context.Background(), "test", "root")
|
2019-09-14 20:09:07 +08:00
|
|
|
f.AddObject(o)
|
|
|
|
testObj, err := f.NewObject(ctx, remote)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, int64(-1), testObj.Size())
|
|
|
|
|
|
|
|
// create a VFS from that mockfs
|
|
|
|
vfs := New(f, nil)
|
2020-04-17 18:18:58 +08:00
|
|
|
defer cleanupVFS(t, vfs)
|
2019-09-14 20:09:07 +08:00
|
|
|
|
|
|
|
// find the file
|
|
|
|
node, err := vfs.Stat(remote)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, node.IsFile())
|
|
|
|
file := node.(*File)
|
|
|
|
|
|
|
|
// open it
|
|
|
|
fd, err := file.openRead()
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, int64(0), fd.Size())
|
|
|
|
|
|
|
|
// check the contents are not empty even though size is empty
|
|
|
|
gotContents, err := ioutil.ReadAll(fd)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, contents, gotContents)
|
|
|
|
t.Logf("gotContents = %q", gotContents)
|
|
|
|
|
|
|
|
// check that file size has been updated
|
|
|
|
assert.Equal(t, int64(len(contents)), fd.Size())
|
|
|
|
|
|
|
|
require.NoError(t, fd.Close())
|
|
|
|
}
|
|
|
|
|
2017-10-30 05:14:05 +08:00
|
|
|
func TestFileOpenWrite(t *testing.T) {
|
2020-04-17 18:18:58 +08:00
|
|
|
_, vfs, file, _, cleanup := fileCreate(t, vfscommon.CacheModeOff)
|
|
|
|
defer cleanup()
|
2017-10-30 05:14:05 +08:00
|
|
|
|
2017-12-07 00:38:19 +08:00
|
|
|
fd, err := file.openWrite(os.O_WRONLY | os.O_TRUNC)
|
2017-10-30 05:14:05 +08:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
newContents := []byte("this is some new contents")
|
|
|
|
n, err := fd.Write(newContents)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, len(newContents), n)
|
|
|
|
require.NoError(t, fd.Close())
|
|
|
|
|
|
|
|
assert.Equal(t, int64(25), file.Size())
|
|
|
|
|
|
|
|
vfs.Opt.ReadOnly = true
|
2017-12-07 00:38:19 +08:00
|
|
|
_, err = file.openWrite(os.O_WRONLY | os.O_TRUNC)
|
2017-10-30 05:14:05 +08:00
|
|
|
assert.Equal(t, EROFS, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestFileRemove(t *testing.T) {
|
2020-04-17 18:18:58 +08:00
|
|
|
r, vfs, file, _, cleanup := fileCreate(t, vfscommon.CacheModeOff)
|
|
|
|
defer cleanup()
|
2017-10-30 05:14:05 +08:00
|
|
|
|
|
|
|
err := file.Remove()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-11-09 19:43:36 +08:00
|
|
|
r.CheckRemoteItems(t)
|
2017-10-30 05:14:05 +08:00
|
|
|
|
|
|
|
vfs.Opt.ReadOnly = true
|
|
|
|
err = file.Remove()
|
|
|
|
assert.Equal(t, EROFS, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestFileRemoveAll(t *testing.T) {
|
2020-04-17 18:18:58 +08:00
|
|
|
r, vfs, file, _, cleanup := fileCreate(t, vfscommon.CacheModeOff)
|
|
|
|
defer cleanup()
|
2017-10-30 05:14:05 +08:00
|
|
|
|
|
|
|
err := file.RemoveAll()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-11-09 19:43:36 +08:00
|
|
|
r.CheckRemoteItems(t)
|
2017-10-30 05:14:05 +08:00
|
|
|
|
|
|
|
vfs.Opt.ReadOnly = true
|
|
|
|
err = file.RemoveAll()
|
|
|
|
assert.Equal(t, EROFS, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestFileOpen(t *testing.T) {
|
2020-04-17 18:18:58 +08:00
|
|
|
_, _, file, _, cleanup := fileCreate(t, vfscommon.CacheModeOff)
|
|
|
|
defer cleanup()
|
2017-10-30 05:14:05 +08:00
|
|
|
|
|
|
|
fd, err := file.Open(os.O_RDONLY)
|
2017-11-07 05:38:52 +08:00
|
|
|
require.NoError(t, err)
|
2017-10-30 05:14:05 +08:00
|
|
|
_, ok := fd.(*ReadFileHandle)
|
|
|
|
assert.True(t, ok)
|
|
|
|
require.NoError(t, fd.Close())
|
|
|
|
|
|
|
|
fd, err = file.Open(os.O_WRONLY)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
_, ok = fd.(*WriteFileHandle)
|
|
|
|
assert.True(t, ok)
|
|
|
|
require.NoError(t, fd.Close())
|
|
|
|
|
|
|
|
fd, err = file.Open(os.O_RDWR)
|
2017-11-16 18:55:24 +08:00
|
|
|
assert.NoError(t, err)
|
|
|
|
_, ok = fd.(*WriteFileHandle)
|
|
|
|
assert.True(t, ok)
|
2020-04-17 18:18:58 +08:00
|
|
|
require.NoError(t, fd.Close())
|
2017-10-30 05:14:05 +08:00
|
|
|
|
2020-02-12 19:20:54 +08:00
|
|
|
_, err = file.Open(3)
|
2017-10-30 05:14:05 +08:00
|
|
|
assert.Equal(t, EPERM, err)
|
|
|
|
}
|
2019-12-05 20:58:24 +08:00
|
|
|
|
2020-07-08 18:40:41 +08:00
|
|
|
func testFileRename(t *testing.T, mode vfscommon.CacheMode, inCache bool, forceCache bool) {
|
2020-04-17 18:18:58 +08:00
|
|
|
r, vfs, file, item, cleanup := fileCreate(t, mode)
|
|
|
|
defer cleanup()
|
2019-12-05 20:58:24 +08:00
|
|
|
|
2019-12-13 18:55:13 +08:00
|
|
|
if !operations.CanServerSideMove(r.Fremote) {
|
|
|
|
t.Skip("skip as can't rename files")
|
|
|
|
}
|
|
|
|
|
2019-12-05 20:58:24 +08:00
|
|
|
rootDir, err := vfs.Root()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2020-07-08 18:40:41 +08:00
|
|
|
// force the file into the cache if required
|
|
|
|
if forceCache {
|
|
|
|
// write the file with read and write
|
|
|
|
fd, err := file.Open(os.O_RDWR | os.O_CREATE | os.O_TRUNC)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
n, err := fd.Write([]byte("file1 contents"))
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 14, n)
|
|
|
|
|
2020-08-19 23:58:10 +08:00
|
|
|
require.NoError(t, file.SetModTime(item.ModTime))
|
|
|
|
|
2020-07-08 18:40:41 +08:00
|
|
|
err = fd.Close()
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
2020-08-19 23:58:10 +08:00
|
|
|
vfs.WaitForWriters(waitForWritersDelay)
|
2020-07-08 18:40:41 +08:00
|
|
|
|
2019-12-05 20:58:24 +08:00
|
|
|
// check file in cache
|
2020-07-08 18:40:41 +08:00
|
|
|
if inCache {
|
2019-12-05 20:58:24 +08:00
|
|
|
// read contents to get file in cache
|
|
|
|
fileCheckContents(t, file)
|
2020-02-28 22:44:15 +08:00
|
|
|
assert.True(t, vfs.cache.Exists(item.Path))
|
2019-12-05 20:58:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
dir := file.Dir()
|
|
|
|
|
|
|
|
// start with "dir/file1"
|
2021-11-09 19:43:36 +08:00
|
|
|
r.CheckRemoteItems(t, item)
|
2019-12-05 20:58:24 +08:00
|
|
|
|
|
|
|
// rename file to "newLeaf"
|
|
|
|
err = dir.Rename("file1", "newLeaf", rootDir)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
item.Path = "newLeaf"
|
2021-11-09 19:43:36 +08:00
|
|
|
r.CheckRemoteItems(t, item)
|
2019-12-05 20:58:24 +08:00
|
|
|
|
|
|
|
// check file in cache
|
2020-07-08 18:40:41 +08:00
|
|
|
if inCache {
|
2020-02-28 22:44:15 +08:00
|
|
|
assert.True(t, vfs.cache.Exists(item.Path))
|
2019-12-05 20:58:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// check file exists in the vfs layer at its new name
|
|
|
|
_, err = vfs.Stat("newLeaf")
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// rename it back to "dir/file1"
|
|
|
|
err = rootDir.Rename("newLeaf", "file1", dir)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
item.Path = "dir/file1"
|
2021-11-09 19:43:36 +08:00
|
|
|
r.CheckRemoteItems(t, item)
|
2019-12-05 20:58:24 +08:00
|
|
|
|
|
|
|
// check file in cache
|
2020-07-08 18:40:41 +08:00
|
|
|
if inCache {
|
2020-02-28 22:44:15 +08:00
|
|
|
assert.True(t, vfs.cache.Exists(item.Path))
|
2019-12-05 20:58:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// now try renaming it with the file open
|
2020-05-20 18:39:20 +08:00
|
|
|
// first open it and write to it but don't close it
|
2019-12-05 20:58:24 +08:00
|
|
|
fd, err := file.Open(os.O_WRONLY | os.O_TRUNC)
|
|
|
|
require.NoError(t, err)
|
|
|
|
newContents := []byte("this is some new contents")
|
|
|
|
_, err = fd.Write(newContents)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// rename file to "newLeaf"
|
|
|
|
err = dir.Rename("file1", "newLeaf", rootDir)
|
|
|
|
require.NoError(t, err)
|
|
|
|
newItem := fstest.NewItem("newLeaf", string(newContents), item.ModTime)
|
|
|
|
|
|
|
|
// check file has been renamed immediately in the cache
|
2020-07-08 18:40:41 +08:00
|
|
|
if inCache {
|
2020-02-28 22:44:15 +08:00
|
|
|
assert.True(t, vfs.cache.Exists("newLeaf"))
|
2019-12-05 20:58:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// check file exists in the vfs layer at its new name
|
|
|
|
_, err = vfs.Stat("newLeaf")
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Close the file
|
|
|
|
require.NoError(t, fd.Close())
|
|
|
|
|
|
|
|
// Check file has now been renamed on the remote
|
|
|
|
item.Path = "newLeaf"
|
2020-04-17 18:18:58 +08:00
|
|
|
vfs.WaitForWriters(waitForWritersDelay)
|
2019-12-05 20:58:24 +08:00
|
|
|
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{newItem}, nil, fs.ModTimeNotSupported)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestFileRename(t *testing.T) {
|
2020-07-08 18:40:41 +08:00
|
|
|
for _, test := range []struct {
|
|
|
|
mode vfscommon.CacheMode
|
|
|
|
inCache bool
|
|
|
|
forceCache bool
|
|
|
|
}{
|
|
|
|
{mode: vfscommon.CacheModeOff, inCache: false},
|
|
|
|
{mode: vfscommon.CacheModeMinimal, inCache: false},
|
|
|
|
{mode: vfscommon.CacheModeMinimal, inCache: true, forceCache: true},
|
|
|
|
{mode: vfscommon.CacheModeWrites, inCache: false},
|
|
|
|
{mode: vfscommon.CacheModeWrites, inCache: true, forceCache: true},
|
|
|
|
{mode: vfscommon.CacheModeFull, inCache: true},
|
|
|
|
} {
|
|
|
|
t.Run(fmt.Sprintf("%v,forceCache=%v", test.mode, test.forceCache), func(t *testing.T) {
|
|
|
|
testFileRename(t, test.mode, test.inCache, test.forceCache)
|
|
|
|
})
|
|
|
|
}
|
2019-12-05 20:58:24 +08:00
|
|
|
}
|