Browse Source

accomplish blob interface

jiyeoniya 5 months ago
parent
commit
abc31c20c2
16 changed files with 529 additions and 10 deletions
  1. +11
    -1
      CMakeLists.txt
  2. +35
    -2
      db/db_impl.cc
  3. +9
    -0
      db/db_impl.h
  4. +4
    -0
      db/db_test.cc
  5. +59
    -0
      db/write_batch.cc
  6. +8
    -0
      include/leveldb/db.h
  7. +8
    -0
      include/leveldb/options.h
  8. +3
    -0
      include/leveldb/write_batch.h
  9. +26
    -0
      table/blob_file.cc
  10. +25
    -0
      table/blob_file.h
  11. +46
    -3
      table/table_builder.cc
  12. +122
    -0
      test/field_test.cc
  13. +119
    -0
      test/kv_seperate_test.cc
  14. +3
    -3
      test/ttl_test.cc
  15. +43
    -0
      util/coding.cc
  16. +8
    -1
      util/coding.h

+ 11
- 1
CMakeLists.txt View File

@ -528,4 +528,14 @@ target_link_libraries(db_test2 PRIVATE leveldb)
add_executable(ttl_test
"${PROJECT_SOURCE_DIR}/test/ttl_test.cc"
)
target_link_libraries(ttl_test PRIVATE leveldb gtest)
target_link_libraries(ttl_test PRIVATE leveldb gtest)
add_executable(field_test
"${PROJECT_SOURCE_DIR}/test/field_test.cc"
)
target_link_libraries(field_test PRIVATE leveldb gtest)
# add_executable(kv_seperate_test
# "${PROJECT_SOURCE_DIR}/test/kv_seperate_test.cc"
# )
# target_link_libraries(kv_seperate_test PRIVATE leveldb gtest)

+ 35
- 2
db/db_impl.cc View File

@ -11,6 +11,7 @@
#include <set>
#include <string>
#include <vector>
#include<iostream>
#include "db/builder.h"
#include "db/db_iter.h"
@ -148,6 +149,8 @@ DBImpl::DBImpl(const Options& raw_options, const std::string& dbname)
manual_compaction_(nullptr),
versions_(new VersionSet(dbname_, &options_, table_cache_,
&internal_comparator_)) {}
bool static key_value_separated_; //朴,添加是否kv分离,12.07
DBImpl::~DBImpl() {
// Wait for background work to finish.
@ -1193,11 +1196,27 @@ void DBImpl::ReleaseSnapshot(const Snapshot* snapshot) {
snapshots_.Delete(static_cast<const SnapshotImpl*>(snapshot));
}
// Convenience methods
// Status DBImpl::Put(const WriteOptions& o, const Slice& key, const Slice& val) {
// return DB::Put(o, key, val);
// }
Status DBImpl::Put(const WriteOptions& o, const Slice& key, const Slice& val) {
return DB::Put(o, key, val);
if (key_value_separated_) {
// 分离key和value的逻辑,朴,12.07
//...
} else {
// 不分离key和value的逻辑
return DB::Put(o, key, val);
}
}
Status DBImpl::Put(const WriteOptions& o, const Slice& key, const Slice& val, uint64_t ttl) {
return DB::Put(o, key, val, ttl);
} // 实现新的put接口,心
Status DBImpl::Delete(const WriteOptions& options, const Slice& key) {
return DB::Delete(options, key);
}
@ -1485,18 +1504,32 @@ void DBImpl::GetApproximateSizes(const Range* range, int n, uint64_t* sizes) {
// Default implementations of convenience methods that subclasses of DB
// can call if they wish
Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) {
Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) { //朴
WriteBatch batch;
batch.Put(key, value);
return Write(opt, &batch);
}
// 假设增加一个新的Put接口,包含TTL参数, 单位(秒)
Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value, uint64_t ttl){
WriteBatch batch;
batch.Put(key, value, ttl);
return Write(opt, &batch);
} // 这里应该是新的PUT接口的真正实现的地方,还是由本来的DB类实现,怪?心
Status DB::Delete(const WriteOptions& opt, const Slice& key) {
WriteBatch batch;
batch.Delete(key);
return Write(opt, &batch);
}
DB::~DB() = default;
Status DB::Open(const Options& options, const std::string& dbname, DB** dbptr) {

+ 9
- 0
db/db_impl.h View File

@ -38,6 +38,8 @@ class DBImpl : public DB {
// Implementations of the DB interface
Status Put(const WriteOptions&, const Slice& key,
const Slice& value) override;
Status Put(const WriteOptions&, const Slice& key,
const Slice& value, uint64_t ttl) override; //put接口
Status Delete(const WriteOptions&, const Slice& key) override;
Status Write(const WriteOptions& options, WriteBatch* updates) override;
Status Get(const ReadOptions& options, const Slice& key,
@ -48,8 +50,11 @@ class DBImpl : public DB {
bool GetProperty(const Slice& property, std::string* value) override;
void GetApproximateSizes(const Range* range, int n, uint64_t* sizes) override;
void CompactRange(const Slice* begin, const Slice* end) override;
// kv分离接口12.07
bool static key_value_separated_;
// Extra methods (for testing) that are not in the public DB interface
// Compact any files in the named level that overlap [*begin,*end]
void TEST_CompactRange(int level, const Slice* begin, const Slice* end);
@ -76,6 +81,8 @@ class DBImpl : public DB {
struct CompactionState;
struct Writer;
// Information for a manual compaction
struct ManualCompaction {
int level;
@ -212,6 +219,8 @@ Options SanitizeOptions(const std::string& db,
const InternalFilterPolicy* ipolicy,
const Options& src);
} // namespace leveldb
#endif // STORAGE_LEVELDB_DB_DB_IMPL_H_

+ 4
- 0
db/db_test.cc View File

@ -2117,6 +2117,10 @@ class ModelDB : public DB {
Status Put(const WriteOptions& o, const Slice& k, const Slice& v) override {
return DB::Put(o, k, v);
}
Status Put(const WriteOptions& o, const Slice& k,
const Slice& v, uint64_t ttl) override {
return DB::Put(o, k, v);
}// 实现的是DB里的新put接口,心
Status Delete(const WriteOptions& o, const Slice& key) override {
return DB::Delete(o, key);
}

+ 59
- 0
db/write_batch.cc View File

@ -19,8 +19,13 @@
#include "db/memtable.h"
#include "db/write_batch_internal.h"
#include "leveldb/db.h"
#include "db/db_impl.h" //朴
#include "util/coding.h"
#include <sstream> // For std::ostringstream 心
#include <cstdint>
#include <string>
namespace leveldb {
// WriteBatch header has an 8-byte sequence number followed by a 4-byte count.
@ -102,6 +107,60 @@ void WriteBatch::Put(const Slice& key, const Slice& value) {
PutLengthPrefixedSlice(&rep_, value);
}
// void WriteBatch::Put(const Slice& key, const Slice& value) { // 朴,kv分离,12.07
// if (DBImpl::key_value_separated_) {
// // 分离key和value的逻辑
// // 例如,你可以将key和value分别存储在不同的容器中
// // 这里需要根据你的具体需求来实现
// //...
// if (value.size() > max_value_size_) {
// // 分离key和value的逻辑
// // 将value存进新的数据结构blobfile
// //...
// // 例如,你可以使用以下代码将value写入blobfile
// std::ofstream blobfile("blobfile.dat", std::ios::binary | std::ios::app);
// blobfile.write(value.data(), value.size());
// blobfile.close();
// }
// }
// else {
// // 不分离key和value的逻辑
// WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
// rep_.push_back(static_cast<char>(kTypeValue));
// PutLengthPrefixedSlice(&rep_, key);
// PutLengthPrefixedSlice(&rep_, value);
// }
// }
void WriteBatch::Put(const Slice& key, const Slice& value, uint64_t ttl) {
WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
rep_.push_back(static_cast<char>(kTypeValue));
PutLengthPrefixedSlice(&rep_, key);
// 获取当前时间
auto now = std::chrono::system_clock::now();
// 加上ttl
auto future_time = now + std::chrono::seconds(ttl);
// 转换为 time_t
std::time_t future_time_t = std::chrono::system_clock::to_time_t(future_time);
// 将 time_t 转换为 tm 结构
std::tm* local_tm = std::localtime(&future_time_t);
// 格式化为字符串
char buffer[20]; // 格式化字符串的缓冲区
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", local_tm);
std::string future_time_str(buffer);
// 拼接原本的值和时间字符串
std::string combined_str = value.ToString() + future_time_str;
PutLengthPrefixedSlice(&rep_, Slice(combined_str));
} // 心
void WriteBatch::Delete(const Slice& key) {
WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
rep_.push_back(static_cast<char>(kTypeDeletion));

+ 8
- 0
include/leveldb/db.h View File

@ -11,8 +11,16 @@
#include "leveldb/export.h"
#include "leveldb/iterator.h"
#include "leveldb/options.h"
#include "util/coding.h"
#include <vector>
namespace leveldb {
//
using Field = std::pair<std::string, std::string>; // field_name:field_value
using FieldArray = std::vector<std::pair<std::string, std::string>>;
// Update CMakeLists.txt if you change these
static const int kMajorVersion = 1;

+ 8
- 0
include/leveldb/options.h View File

@ -145,6 +145,14 @@ struct LEVELDB_EXPORT Options {
// Many applications will benefit from passing the result of
// NewBloomFilterPolicy() here.
const FilterPolicy* filter_policy = nullptr;
// KV
bool key_value_separated = false;
Options() {
//
key_value_separated = false;
}
};
// Options that control read operations

+ 3
- 0
include/leveldb/write_batch.h View File

@ -25,6 +25,7 @@
#include "leveldb/export.h"
#include "leveldb/status.h"
#include <cstdint>
namespace leveldb {
@ -50,6 +51,8 @@ class LEVELDB_EXPORT WriteBatch {
// Store the mapping "key->value" in the database.
void Put(const Slice& key, const Slice& value);
void Put(const Slice& key, const Slice& value, uint64_t ttl); //
// If the database contains a mapping for "key", erase it. Else do nothing.
void Delete(const Slice& key);

+ 26
- 0
table/blob_file.cc View File

@ -0,0 +1,26 @@
#include "blob_file.h"
#include <fstream>
namespace leveldb {
BlobFile::BlobFile(const std::string& filename) : filename_(filename) {
// 初始化 BlobFile,例如打开文件
}
BlobFile::~BlobFile() {
// 关闭文件
}
Status BlobFile::Put(const Slice& key, const Slice& value) {
std::ofstream file(filename_, std::ios::app | std::ios::binary);
if (!file.is_open()) {
return Status::IOError("Failed to open blob file");
}
// 简单实现,将 key 和 value 写入文件
file.write(key.data(), key.size());
file.write(value.data(), value.size());
file.close();
return Status::OK();
}
} // namespace leveldb

+ 25
- 0
table/blob_file.h View File

@ -0,0 +1,25 @@
#ifndef LEVELDB_BLOB_FILE_H_
#define LEVELDB_BLOB_FILE_H_
#include <string>
#include "leveldb/status.h"
#include "leveldb/slice.h"
namespace leveldb {
class BlobFile {
public:
BlobFile(const std::string& filename);
~BlobFile();
//
Status Put(const Slice& key, const Slice& value);
private:
std::string filename_;
//
};
} // namespace leveldb
#endif // LEVELDB_BLOB_FILE_H_

+ 46
- 3
table/table_builder.cc View File

@ -15,9 +15,23 @@
#include "table/format.h"
#include "util/coding.h"
#include "util/crc32c.h"
#include "db/db_impl.h" //朴
#include "table/blob_file.h" //朴
#include "table/block.h" //朴
const size_t min_blob_size = 1024; // 设定值大小阈值为 1KB,朴
namespace leveldb {
BlobFile* blobfile = new BlobFile("blob_data"); // 初始化全局 blobfile 对象,朴
class BlobFileManager {
public:
static BlobFile* GetInstance() {
static BlobFile instance("blob_data");
return &instance;
}
};
struct TableBuilder::Rep {
Rep(const Options& opt, WritableFile* f)
: options(opt),
@ -126,12 +140,41 @@ void TableBuilder::Flush() {
Rep* r = rep_;
assert(!r->closed);
if (!ok()) return;
if (r->data_block.empty()) return;
if (r->data_block.empty()) return; //朴,正常判断
assert(!r->pending_index_entry);
WriteBlock(&r->data_block, &r->pending_handle);
if (DBImpl::key_value_separated_) {
// 这里获取数据块内容并初始化 Block 对象,朴
Slice block_content = r->data_block.Finish();
BlockContents contents;
contents.data = block_content;
contents.heap_allocated = false;
contents.cachable = false;
// 初始化 Block
Block data_block(contents);
std::unique_ptr<Iterator> iter(data_block.NewIterator(Options().comparator));
// 遍历数据块中的键值对
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
const Slice& key = iter->key();
const Slice& value = iter->value();
// 检查值是否大于阈值
if (value.size() > min_blob_size) {
// 将值存储到 blobfile 中
Status status = blobfile->Put(key, value);
if (!status.ok()) {
r->status = status;
}
}
}
}
WriteBlock(&r->data_block, &r->pending_handle); //将数据块写入文件,并获取数据块的句柄。
if (ok()) {
r->pending_index_entry = true;
r->status = r->file->Flush();
r->status = r->file->Flush(); //刷新
}
if (r->filter_block != nullptr) {
r->filter_block->StartBlock(r->offset);

+ 122
- 0
test/field_test.cc View File

@ -0,0 +1,122 @@
#include "gtest/gtest.h"
#include "leveldb/env.h"
#include "leveldb/db.h"
#include "util/coding.h"
#include <iostream>
using namespace leveldb;
constexpr int value_size = 2048;
constexpr int data_size = 128 << 20;
// 根据字段值查找所有包含该字段的 key
std::vector<std::string> FindKeysByField(leveldb::DB* db, Field &field) {
Iterator* iter = db->NewIterator(ReadOptions());
std::vector<std::string> ret_keys;
int64_t bytes = 0;
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
auto fields_ret = ParseValue(iter->value().data());
for (Field each_field : fields_ret)
{
std::cout << each_field.first << " " << each_field.second << std::endl;
if (field.first.compare(each_field.first) == 0) {
if (field.second.compare(each_field.second)==0)
{
ret_keys.push_back(iter->key().data());
}
else
break;
}
}
}
delete iter;
return ret_keys;
}
Status OpenDB(std::string dbName, DB **db) {
Options options;
options.create_if_missing = true;
return DB::Open(options, dbName, db);
}
TEST(TestField, Read) {
DB *db;
if(OpenDB("testdb", &db).ok() == false) {
std::cerr << "open db failed" << std::endl;
abort();
}
std::string key = "k_1";
FieldArray fields = {
{"name", "Customer#000000001"},
{"address", "IVhzIApeRb"},
{"phone", "25-989-741-2988"}
};
// 序列化并插入
std::string value = SerializeValue(fields);
db->Put(WriteOptions(), key, value);
// 读取并反序列化
std::string value_ret;
db->Get(ReadOptions(), key, &value_ret);
auto fields_ret = ParseValue(value_ret);
std::cout << "第一个字段名:"<< fields_ret[0].first << "第一个字段值" << fields_ret[0].second<< std::endl;
delete db;
}
TEST(TestField, Find) {
DB *db;
if(OpenDB("testdb", &db).ok() == false) {
std::cerr << "open db failed" << std::endl;
abort();
}
std::vector<std::string> keys = {"s_1", "s_2", "s_3", "s_4"};
// 构造一组字段数组
std::vector<FieldArray> FieldArrays = {
{
{"name", "Sarah"},{"sex", "f"},{"age", "20"}
},
{
{"name", "Mike"},{"sex", "m"},{"age", "19"},{"hobby", "badminton"}
},
{
{"name", "Amy"},{"sex", "f"},{"age", "21"},{"talent", "sing"}
},
{
{"name", "John"}, {"sex", "m"},{"age", "20"}
}
};
// 序列化并插入
for (int i=0; i<FieldArrays.size(); i++)
{
std::string key = keys[i];
FieldArray fields = FieldArrays[i];
std::string value = SerializeValue(fields);
db->Put(WriteOptions(), key, value);
}
// 构建目标字段
Field field = {"sex", "f"};
std::vector<std::string> key_ret;
// 查询得到对应的key
key_ret = FindKeysByField(db, field);
for (int i = 0; i < key_ret.size(); i++) {
std::cout << "找到的键:" << key_ret[i] << std::endl;
}
delete db;
}
int main(int argc, char** argv) {
// All tests currently run with the same read-only file limits.
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

+ 119
- 0
test/kv_seperate_test.cc View File

@ -0,0 +1,119 @@
#include "gtest/gtest.h"
#include "leveldb/env.h"
#include "leveldb/db.h"
#include "table/blob_file.h" // 假设 BlobFile 的头文件
using namespace leveldb;
constexpr int value_size = 2048; // 单个值的大小
constexpr int data_size = 128 << 20; // 总数据大小
constexpr int min_blob_size = 1024; // KV 分离的阈值
Status OpenDB(std::string dbName, DB** db) {
Options options;
options.create_if_missing = true;
options.key_value_separated = true; // 启用 KV 分离
return DB::Open(options, dbName, db);
}
// 插入数据,模拟 KV 分离
void InsertData(DB* db) {
WriteOptions writeOptions;
int key_num = data_size / value_size;
srand(static_cast<unsigned int>(time(0)));
for (int i = 0; i < key_num; i++) {
int key_ = rand() % key_num + 1;
std::string key = std::to_string(key_);
std::string value(value_size, 'a'); // 大 value
db->Put(writeOptions, key, value); // 使用标准 Put 接口插入
}
}
// 检查数据是否被正确存入 BlobFile
void VerifyBlobFile(const std::string& blob_file_path, int expected_entries) {
BlobFile blobfile(blob_file_path, BlobFile::kReadMode);
Status status = blobfile.Open();
ASSERT_TRUE(status.ok());
int entry_count = 0;
BlobFile::Iterator it = blobfile.NewIterator();
for (it.SeekToFirst(); it.Valid(); it.Next()) {
++entry_count;
const Slice& key = it.key();
const Slice& value = it.value();
ASSERT_GT(value.size(), min_blob_size); // 确认 value 大于阈值
}
ASSERT_EQ(entry_count, expected_entries); // 确认条目数是否正确
blobfile.Close();
}
// KV 分离读写测试
TEST(TestKVSeparation, WriteAndRead) {
DB* db;
if (OpenDB("testdb", &db).ok() == false) {
std::cerr << "open db failed" << std::endl;
abort();
}
// 插入数据
InsertData(db);
// 验证 BlobFile 内容
VerifyBlobFile("blob_data", data_size / value_size);
// 随机点查数据
ReadOptions readOptions;
srand(static_cast<unsigned int>(time(0)));
int key_num = data_size / value_size;
for (int i = 0; i < 100; i++) {
int key_ = rand() % key_num + 1;
std::string key = std::to_string(key_);
std::string value;
Status status = db->Get(readOptions, key, &value);
ASSERT_TRUE(status.ok()); // 验证是否成功读取
if (value.size() > min_blob_size) {
ASSERT_TRUE(value == std::string(value_size, 'a')); // 验证大 value 的内容
}
}
delete db;
}
// KV 分离压缩测试
TEST(TestKVSeparation, Compaction) {
DB* db;
if (OpenDB("testdb", &db).ok() == false) {
std::cerr << "open db failed" << std::endl;
abort();
}
// 插入数据
InsertData(db);
leveldb::Range ranges[1];
ranges[0] = leveldb::Range("-", "A");
uint64_t sizes[1];
db->GetApproximateSizes(ranges, 1, sizes);
ASSERT_GT(sizes[0], 0);
// 执行压缩
db->CompactRange(nullptr, nullptr);
// 验证压缩后主数据区的大小
ranges[0] = leveldb::Range("-", "A");
db->GetApproximateSizes(ranges, 1, sizes);
ASSERT_EQ(sizes[0], 0);
// 验证 BlobFile 内容仍然有效
VerifyBlobFile("blob_data", data_size / value_size);
delete db;
}
int main(int argc, char** argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

+ 3
- 3
test/ttl_test.cc View File

@ -74,7 +74,7 @@ TEST(TestTTL, ReadTTL) {
std::string key = std::to_string(key_);
std::string value;
status = db->Get(readOptions, key, &value);
ASSERT_FALSE(status.ok());
ASSERT_FALSE(status.ok()); // 经过超长时间之后所有的键值对应该都过期了,心
}
}
@ -99,9 +99,9 @@ TEST(TestTTL, CompactionTTL) {
db->CompactRange(nullptr, nullptr);
leveldb::Range ranges[1];
// leveldb::Range ranges[1]; // 这里为什么要重复定义?心
ranges[0] = leveldb::Range("-", "A");
uint64_t sizes[1];
// uint64_t sizes[1]; // 心
db->GetApproximateSizes(ranges, 1, sizes);
ASSERT_EQ(sizes[0], 0);
}

+ 43
- 0
util/coding.cc View File

@ -3,6 +3,10 @@
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "util/coding.h"
#include "leveldb/db.h"
#include <iostream>
#include<vector>
namespace leveldb {
@ -49,7 +53,9 @@ char* EncodeVarint32(char* dst, uint32_t v) {
void PutVarint32(std::string* dst, uint32_t v) {
char buf[5];
char* ptr = EncodeVarint32(buf, v);
// printf("加入的长度%d\n", ptr - buf);
dst->append(buf, ptr - buf);
// printf("插入之后的长度为:%d", dst->length());
}
char* EncodeVarint64(char* dst, uint64_t v) {
@ -152,5 +158,42 @@ bool GetLengthPrefixedSlice(Slice* input, Slice* result) {
return false;
}
}
// 序列化为字符串
std::string SerializeValue(const FieldArray& fields) {
std::string serialized_value = "";
uint32_t num = fields.size();
// std::cout<< "加入的值中包含的字段数:"<<num<<std::endl;
PutVarint32(&serialized_value, num); // 编码字段个数
for (uint32_t i = 0; i < num; i++) { // 依次编码字段名和字段值
PutLengthPrefixedSlice(&serialized_value, fields[i].first);
PutLengthPrefixedSlice(&serialized_value, fields[i].second);
}
return serialized_value;
}
// 反序列化为字段数组
FieldArray ParseValue(const std::string& value_str) {
uint32_t num=0;
FieldArray fields;
Slice value_sl(value_str); // 把字符串类型的值转为slice类型
if (GetVarint32(&value_sl, &num)) {
for (uint32_t i = 0; i < num; i++)
{
Slice field_name, field_value;
GetLengthPrefixedSlice(&value_sl, &field_name);
GetLengthPrefixedSlice(&value_sl, &field_value);
fields.push_back({field_name.ToString().substr(0, field_name.size()), field_value.ToString().substr(0, field_value.size())});
// std::string str = field_name.data().substr(0, field_name.size());
//std::cout << "字段名:"<< strncpy(str1, field_name.data(),field_name.size())<< std::endl;
// std::cout<<field_name.data()<<"__"<<field_value.data()<< std::endl;
}
}
return fields;
}
} // namespace leveldb

+ 8
- 1
util/coding.h View File

@ -13,11 +13,17 @@
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include "util/coding.h"
#include "leveldb/db.h"
#include <iostream>
#include "leveldb/slice.h"
#include "port/port.h"
namespace leveldb {
using Field = std::pair<std::string, std::string>; // field_name:field_value
using FieldArray = std::vector<std::pair<std::string, std::string>>;
// Standard Put... routines append to a string
void PutFixed32(std::string* dst, uint32_t value);
@ -116,7 +122,8 @@ inline const char* GetVarint32Ptr(const char* p, const char* limit,
}
return GetVarint32PtrFallback(p, limit, value);
}
std::string SerializeValue(const FieldArray& fields);
FieldArray ParseValue(const std::string& value_str);
} // namespace leveldb
#endif // STORAGE_LEVELDB_UTIL_CODING_H_

||||||
x
 
000:0
Loading…
Cancel
Save