2020-05-04 08:01:05 +00:00
|
|
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
|
2022-06-17 07:37:17 +00:00
|
|
|
#nullable disable
|
|
|
|
|
2020-05-04 08:01:05 +00:00
|
|
|
using System;
|
|
|
|
using System.IO;
|
2020-05-10 04:17:37 +00:00
|
|
|
using System.Linq;
|
2020-09-17 07:12:30 +00:00
|
|
|
using System.Runtime.CompilerServices;
|
2020-05-04 08:01:05 +00:00
|
|
|
using NUnit.Framework;
|
|
|
|
using osu.Framework.Allocation;
|
2020-05-07 10:01:19 +00:00
|
|
|
using osu.Framework.Configuration;
|
2020-05-04 08:01:05 +00:00
|
|
|
using osu.Framework.Platform;
|
2021-08-18 06:32:59 +00:00
|
|
|
using osu.Framework.Testing;
|
2020-05-04 08:01:05 +00:00
|
|
|
using osu.Game.Configuration;
|
2020-05-07 10:01:19 +00:00
|
|
|
using osu.Game.IO;
|
2020-05-04 08:01:05 +00:00
|
|
|
|
|
|
|
namespace osu.Game.Tests.NonVisual
|
|
|
|
{
|
|
|
|
[TestFixture]
|
2020-09-18 09:05:33 +00:00
|
|
|
public class CustomDataDirectoryTest : ImportTest
|
2020-05-04 08:01:05 +00:00
|
|
|
{
|
|
|
|
[Test]
|
|
|
|
public void TestDefaultDirectory()
|
|
|
|
{
|
2020-09-17 07:12:30 +00:00
|
|
|
using (var host = new CustomTestHeadlessGameHost())
|
2020-05-04 08:01:05 +00:00
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2021-12-24 11:24:37 +00:00
|
|
|
string defaultStorageLocation = getDefaultLocationFor(host);
|
2020-07-09 06:08:03 +00:00
|
|
|
|
2020-09-18 09:05:33 +00:00
|
|
|
var osu = LoadOsuIntoHost(host);
|
2020-05-04 08:01:05 +00:00
|
|
|
var storage = osu.Dependencies.Get<Storage>();
|
|
|
|
|
|
|
|
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
|
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
host.Exit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
public void TestCustomDirectory()
|
|
|
|
{
|
2022-06-30 07:02:46 +00:00
|
|
|
using (prepareCustomPath(out string customPath))
|
2020-09-17 07:12:30 +00:00
|
|
|
using (var host = new CustomTestHeadlessGameHost())
|
2020-07-09 06:08:03 +00:00
|
|
|
{
|
|
|
|
using (var storageConfig = new StorageConfigManager(host.InitialStorage))
|
2021-03-17 07:10:16 +00:00
|
|
|
storageConfig.SetValue(StorageConfig.FullPath, customPath);
|
2020-05-04 08:01:05 +00:00
|
|
|
|
|
|
|
try
|
|
|
|
{
|
2020-09-18 09:05:33 +00:00
|
|
|
var osu = LoadOsuIntoHost(host);
|
2020-05-04 08:01:05 +00:00
|
|
|
|
|
|
|
// switch to DI'd storage
|
2020-07-09 06:08:03 +00:00
|
|
|
var storage = osu.Dependencies.Get<Storage>();
|
2020-05-04 08:01:05 +00:00
|
|
|
|
2020-05-07 10:01:19 +00:00
|
|
|
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
|
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
host.Exit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-10 04:17:37 +00:00
|
|
|
[Test]
|
|
|
|
public void TestSubDirectoryLookup()
|
|
|
|
{
|
2022-06-30 07:02:46 +00:00
|
|
|
using (prepareCustomPath(out string customPath))
|
2020-09-17 07:12:30 +00:00
|
|
|
using (var host = new CustomTestHeadlessGameHost())
|
2020-07-09 06:08:03 +00:00
|
|
|
{
|
|
|
|
using (var storageConfig = new StorageConfigManager(host.InitialStorage))
|
2021-03-17 07:10:16 +00:00
|
|
|
storageConfig.SetValue(StorageConfig.FullPath, customPath);
|
2020-05-04 08:01:05 +00:00
|
|
|
|
|
|
|
try
|
|
|
|
{
|
2020-09-18 09:05:33 +00:00
|
|
|
var osu = LoadOsuIntoHost(host);
|
2020-05-04 08:01:05 +00:00
|
|
|
|
|
|
|
// switch to DI'd storage
|
2020-07-09 06:08:03 +00:00
|
|
|
var storage = osu.Dependencies.Get<Storage>();
|
2020-05-04 08:01:05 +00:00
|
|
|
|
2020-05-10 04:17:37 +00:00
|
|
|
string actualTestFile = Path.Combine(customPath, "rulesets", "test");
|
|
|
|
|
|
|
|
File.WriteAllText(actualTestFile, "test");
|
|
|
|
|
|
|
|
var rulesetStorage = storage.GetStorageForDirectory("rulesets");
|
2021-10-27 04:04:41 +00:00
|
|
|
string lookupPath = rulesetStorage.GetFiles(".").Single();
|
2020-05-10 04:17:37 +00:00
|
|
|
|
|
|
|
Assert.That(lookupPath, Is.EqualTo("test"));
|
2020-05-04 08:01:05 +00:00
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
host.Exit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-07 10:01:19 +00:00
|
|
|
[Test]
|
|
|
|
public void TestMigration()
|
|
|
|
{
|
2022-06-30 07:02:46 +00:00
|
|
|
using (prepareCustomPath(out string customPath))
|
2020-09-17 07:12:30 +00:00
|
|
|
using (var host = new CustomTestHeadlessGameHost())
|
2020-05-07 10:01:19 +00:00
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2021-12-24 11:24:37 +00:00
|
|
|
string defaultStorageLocation = getDefaultLocationFor(host);
|
2020-07-09 06:08:03 +00:00
|
|
|
|
2020-09-18 09:05:33 +00:00
|
|
|
var osu = LoadOsuIntoHost(host);
|
2020-05-07 10:01:19 +00:00
|
|
|
var storage = osu.Dependencies.Get<Storage>();
|
2020-06-24 21:01:56 +00:00
|
|
|
var osuStorage = storage as MigratableStorage;
|
2020-05-07 10:01:19 +00:00
|
|
|
|
2020-07-01 14:21:08 +00:00
|
|
|
// Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
|
|
|
|
string originalDirectory = storage.GetFullPath(".");
|
|
|
|
|
2020-05-07 10:01:19 +00:00
|
|
|
// ensure we perform a save
|
|
|
|
host.Dependencies.Get<FrameworkConfigManager>().Save();
|
|
|
|
|
|
|
|
// ensure we "use" cache
|
|
|
|
host.Storage.GetStorageForDirectory("cache");
|
|
|
|
|
2020-05-12 03:39:04 +00:00
|
|
|
// for testing nested files are not ignored (only top level)
|
|
|
|
host.Storage.GetStorageForDirectory("test-nested").GetStorageForDirectory("cache");
|
|
|
|
|
2020-05-07 10:01:19 +00:00
|
|
|
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
|
|
|
|
|
2020-05-11 12:37:07 +00:00
|
|
|
osu.Migrate(customPath);
|
2020-05-07 10:01:19 +00:00
|
|
|
|
|
|
|
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
|
|
|
|
|
2020-05-12 03:39:04 +00:00
|
|
|
// ensure cache was not moved
|
2020-07-01 14:21:08 +00:00
|
|
|
Assert.That(Directory.Exists(Path.Combine(originalDirectory, "cache")));
|
2020-05-12 03:39:04 +00:00
|
|
|
|
|
|
|
// ensure nested cache was moved
|
2020-07-01 14:21:08 +00:00
|
|
|
Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
|
2020-05-12 03:39:04 +00:00
|
|
|
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
|
|
|
|
|
2020-06-24 21:01:56 +00:00
|
|
|
Assert.That(osuStorage, Is.Not.Null);
|
|
|
|
|
2022-01-25 03:54:38 +00:00
|
|
|
// In the following tests, realm files are ignored as
|
2022-03-30 04:34:48 +00:00
|
|
|
// - in the case of checking the source, interacting with the pipe files (.realm.note) may
|
2022-01-25 03:54:38 +00:00
|
|
|
// lead to unexpected behaviour.
|
|
|
|
// - in the case of checking the destination, the files may have already been recreated by the game
|
|
|
|
// as part of the standard migration flow.
|
|
|
|
|
2021-10-27 04:04:41 +00:00
|
|
|
foreach (string file in osuStorage.IgnoreFiles)
|
2020-06-24 21:01:56 +00:00
|
|
|
{
|
2022-03-30 04:34:48 +00:00
|
|
|
if (!file.Contains(".realm", StringComparison.Ordinal))
|
2022-01-25 03:54:38 +00:00
|
|
|
{
|
2021-01-21 13:57:55 +00:00
|
|
|
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
|
2022-01-25 03:54:38 +00:00
|
|
|
Assert.That(storage.Exists(file), Is.False, () => $"{file} exists in destination when it was expected to be ignored");
|
|
|
|
}
|
2020-06-24 21:01:56 +00:00
|
|
|
}
|
|
|
|
|
2021-10-27 04:04:41 +00:00
|
|
|
foreach (string dir in osuStorage.IgnoreDirectories)
|
2020-05-07 10:01:19 +00:00
|
|
|
{
|
2022-03-30 04:34:48 +00:00
|
|
|
if (!dir.Contains(".realm", StringComparison.Ordinal))
|
2022-01-25 03:54:38 +00:00
|
|
|
{
|
|
|
|
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
|
|
|
|
Assert.That(storage.Exists(dir), Is.False, () => $"{dir} exists in destination when it was expected to be ignored");
|
|
|
|
}
|
2020-05-07 10:01:19 +00:00
|
|
|
}
|
|
|
|
|
2020-07-01 14:21:08 +00:00
|
|
|
Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}"));
|
2020-05-04 08:01:05 +00:00
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
host.Exit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-09 11:13:37 +00:00
|
|
|
[Test]
|
|
|
|
public void TestMigrationBetweenTwoTargets()
|
|
|
|
{
|
2022-06-30 07:02:46 +00:00
|
|
|
using (prepareCustomPath(out string customPath))
|
|
|
|
using (prepareCustomPath(out string customPath2))
|
2020-09-17 07:12:30 +00:00
|
|
|
using (var host = new CustomTestHeadlessGameHost())
|
2020-05-09 11:13:37 +00:00
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2020-09-18 09:05:33 +00:00
|
|
|
var osu = LoadOsuIntoHost(host);
|
2020-05-09 11:13:37 +00:00
|
|
|
|
2020-05-11 12:37:07 +00:00
|
|
|
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
2022-03-30 04:34:48 +00:00
|
|
|
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
|
2020-05-09 11:13:37 +00:00
|
|
|
|
2020-05-11 12:37:07 +00:00
|
|
|
Assert.DoesNotThrow(() => osu.Migrate(customPath2));
|
2022-03-30 04:34:48 +00:00
|
|
|
Assert.That(File.Exists(Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME)));
|
2020-05-09 11:13:37 +00:00
|
|
|
|
2021-07-09 03:21:24 +00:00
|
|
|
// some files may have been left behind for whatever reason, but that's not what we're testing here.
|
2021-12-21 09:42:37 +00:00
|
|
|
cleanupPath(customPath);
|
2021-07-09 03:21:24 +00:00
|
|
|
|
2020-05-11 12:37:07 +00:00
|
|
|
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
2022-03-30 04:34:48 +00:00
|
|
|
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
|
2020-05-09 11:13:37 +00:00
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
host.Exit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
public void TestMigrationToSameTargetFails()
|
|
|
|
{
|
2022-06-30 07:02:46 +00:00
|
|
|
using (prepareCustomPath(out string customPath))
|
2020-09-17 07:12:30 +00:00
|
|
|
using (var host = new CustomTestHeadlessGameHost())
|
2020-05-09 11:13:37 +00:00
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2020-09-18 09:05:33 +00:00
|
|
|
var osu = LoadOsuIntoHost(host);
|
2020-05-09 11:13:37 +00:00
|
|
|
|
2020-05-11 12:37:07 +00:00
|
|
|
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
2020-05-14 11:18:57 +00:00
|
|
|
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath));
|
2020-05-09 11:13:37 +00:00
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
host.Exit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-29 14:28:07 +00:00
|
|
|
[Test]
|
|
|
|
public void TestMigrationFailsOnExistingData()
|
|
|
|
{
|
2022-06-30 07:02:46 +00:00
|
|
|
using (prepareCustomPath(out string customPath))
|
|
|
|
using (prepareCustomPath(out string customPath2))
|
2022-03-29 14:28:07 +00:00
|
|
|
using (var host = new CustomTestHeadlessGameHost())
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
var osu = LoadOsuIntoHost(host);
|
|
|
|
|
|
|
|
var storage = osu.Dependencies.Get<Storage>();
|
|
|
|
var osuStorage = storage as OsuStorage;
|
|
|
|
|
|
|
|
string originalDirectory = storage.GetFullPath(".");
|
|
|
|
|
|
|
|
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
2022-03-30 04:34:48 +00:00
|
|
|
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
|
2022-03-29 14:28:07 +00:00
|
|
|
|
|
|
|
Directory.CreateDirectory(customPath2);
|
2022-06-24 07:18:51 +00:00
|
|
|
File.WriteAllText(Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME), "I am a text");
|
2022-03-29 14:28:07 +00:00
|
|
|
|
|
|
|
// Fails because file already exists.
|
|
|
|
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath2));
|
|
|
|
|
|
|
|
osuStorage?.ChangeDataPath(customPath2);
|
|
|
|
|
|
|
|
Assert.That(osuStorage?.CustomStoragePath, Is.EqualTo(customPath2));
|
|
|
|
Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath2}"));
|
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
host.Exit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-14 13:42:42 +00:00
|
|
|
[Test]
|
|
|
|
public void TestMigrationToNestedTargetFails()
|
|
|
|
{
|
2022-06-30 07:02:46 +00:00
|
|
|
using (prepareCustomPath(out string customPath))
|
2020-09-17 07:12:30 +00:00
|
|
|
using (var host = new CustomTestHeadlessGameHost())
|
2020-05-14 13:42:42 +00:00
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2020-09-18 09:05:33 +00:00
|
|
|
var osu = LoadOsuIntoHost(host);
|
2020-05-14 13:42:42 +00:00
|
|
|
|
|
|
|
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
|
|
|
|
|
|
|
string subFolder = Path.Combine(customPath, "sub");
|
|
|
|
|
2020-05-15 01:45:57 +00:00
|
|
|
if (Directory.Exists(subFolder))
|
|
|
|
Directory.Delete(subFolder, true);
|
|
|
|
|
2020-05-14 13:42:42 +00:00
|
|
|
Directory.CreateDirectory(subFolder);
|
|
|
|
|
|
|
|
Assert.Throws<ArgumentException>(() => osu.Migrate(subFolder));
|
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
host.Exit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
public void TestMigrationToSeeminglyNestedTarget()
|
|
|
|
{
|
2022-06-30 07:02:46 +00:00
|
|
|
using (prepareCustomPath(out string customPath))
|
2020-09-17 07:12:30 +00:00
|
|
|
using (var host = new CustomTestHeadlessGameHost())
|
2020-05-14 13:42:42 +00:00
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2020-09-18 09:05:33 +00:00
|
|
|
var osu = LoadOsuIntoHost(host);
|
2020-05-14 13:42:42 +00:00
|
|
|
|
|
|
|
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
|
|
|
|
2020-05-15 01:45:57 +00:00
|
|
|
string seeminglySubFolder = customPath + "sub";
|
2020-05-14 13:42:42 +00:00
|
|
|
|
2020-05-15 01:45:57 +00:00
|
|
|
if (Directory.Exists(seeminglySubFolder))
|
|
|
|
Directory.Delete(seeminglySubFolder, true);
|
|
|
|
|
|
|
|
Directory.CreateDirectory(seeminglySubFolder);
|
2020-05-14 13:42:42 +00:00
|
|
|
|
2020-05-15 01:45:57 +00:00
|
|
|
osu.Migrate(seeminglySubFolder);
|
2020-05-14 13:42:42 +00:00
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
host.Exit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-04 07:45:21 +00:00
|
|
|
[Test]
|
|
|
|
public void TestBackupCreatedOnCorruptRealm()
|
|
|
|
{
|
|
|
|
using (var host = new CustomTestHeadlessGameHost())
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
File.WriteAllText(host.InitialStorage.GetFullPath(OsuGameBase.CLIENT_DATABASE_FILENAME, true), "i am definitely not a realm file");
|
|
|
|
|
|
|
|
LoadOsuIntoHost(host);
|
|
|
|
|
|
|
|
Assert.That(host.InitialStorage.GetFiles(string.Empty, "*_corrupt.realm"), Has.One.Items);
|
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
host.Exit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-24 11:24:37 +00:00
|
|
|
private static string getDefaultLocationFor(CustomTestHeadlessGameHost host)
|
2020-07-09 06:08:03 +00:00
|
|
|
{
|
2021-12-24 11:24:37 +00:00
|
|
|
string path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, host.Name);
|
2020-07-09 06:08:03 +00:00
|
|
|
|
|
|
|
if (Directory.Exists(path))
|
|
|
|
Directory.Delete(path, true);
|
|
|
|
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
|
2022-06-30 07:02:46 +00:00
|
|
|
private static IDisposable prepareCustomPath(out string path)
|
|
|
|
{
|
|
|
|
path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path-{Guid.NewGuid()}");
|
|
|
|
return new InvokeOnDisposal<string>(path, cleanupPath);
|
|
|
|
}
|
2020-07-09 06:08:03 +00:00
|
|
|
|
2021-12-21 09:42:37 +00:00
|
|
|
private static void cleanupPath(string path)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2022-06-30 07:02:46 +00:00
|
|
|
if (Directory.Exists(path)) Directory.Delete(path, true);
|
2021-12-21 09:42:37 +00:00
|
|
|
}
|
|
|
|
catch
|
|
|
|
{
|
|
|
|
}
|
2020-07-09 06:08:03 +00:00
|
|
|
}
|
|
|
|
|
2020-09-17 07:12:30 +00:00
|
|
|
public class CustomTestHeadlessGameHost : CleanRunHeadlessGameHost
|
2020-07-09 06:08:03 +00:00
|
|
|
{
|
|
|
|
public Storage InitialStorage { get; }
|
|
|
|
|
2020-09-17 07:12:30 +00:00
|
|
|
public CustomTestHeadlessGameHost([CallerMemberName] string callingMethodName = @"")
|
2022-07-04 07:45:21 +00:00
|
|
|
: base(callingMethodName: callingMethodName, bypassCleanupOnSetup: true)
|
2020-07-09 06:08:03 +00:00
|
|
|
{
|
2021-12-24 11:24:37 +00:00
|
|
|
string defaultStorageLocation = getDefaultLocationFor(this);
|
2020-07-09 06:08:03 +00:00
|
|
|
|
|
|
|
InitialStorage = new DesktopStorage(defaultStorageLocation, this);
|
|
|
|
InitialStorage.DeleteDirectory(string.Empty);
|
|
|
|
}
|
2021-08-18 06:32:59 +00:00
|
|
|
|
|
|
|
protected override void Dispose(bool isDisposing)
|
|
|
|
{
|
|
|
|
base.Dispose(isDisposing);
|
|
|
|
|
2021-08-20 13:07:13 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
// the storage may have changed from the initial location.
|
|
|
|
// this handles cleanup of the initial location.
|
|
|
|
InitialStorage.DeleteDirectory(string.Empty);
|
|
|
|
}
|
|
|
|
catch { }
|
2021-08-18 06:32:59 +00:00
|
|
|
}
|
2020-07-09 06:08:03 +00:00
|
|
|
}
|
2020-05-04 08:01:05 +00:00
|
|
|
}
|
|
|
|
}
|