unittest/mysqlshdk/libs/azure/azure_blob_storage_t.cc (392 lines of code) (raw):
/*
* Copyright (c) 2022, 2024, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
* as published by the Free Software Foundation.
*
* This program is designed to work with certain software (including
* but not limited to OpenSSL) that is licensed under separate terms,
* as designated in a particular file or component or in included license
* documentation. The authors of MySQL hereby grant you an additional
* permission to link the program and your derivative works with the
* separately licensed software that they have either included with
* the program or referenced in the documentation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU General Public License, version 2.0, for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "unittest/mysqlshdk/libs/azure/azure_tests.h"
#include "mysqlshdk/libs/storage/backend/object_storage.h"
using mysqlshdk::azure::Blob_container;
using mysqlshdk::rest::Response_error;
using mysqlshdk::storage::Mode;
using mysqlshdk::storage::backend::object_storage::Directory;
namespace mysqlshdk {
namespace azure {
class Azure_blob_storage_tests : public Azure_tests {
public:
static std::string s_container_name;
static void SetUpTestCase() { testing::create_container(s_container_name); }
static void TearDownTestCase() {
testing::delete_container(s_container_name);
}
std::string container_name() override { return s_container_name; }
};
std::string Azure_blob_storage_tests::s_container_name = "storageut";
TEST_F(Azure_blob_storage_tests, directory_list_files) {
SKIP_IF_NO_AZURE_CONFIGURATION;
const auto config = get_config();
Blob_container container(config);
Directory root_directory(config);
Directory sakila(config, "sakila");
// The root directory exists for sure
EXPECT_TRUE(root_directory.exists());
// A subdirectory only exist if created or if there are files/multipart
// uploads
EXPECT_FALSE(sakila.exists());
container.create_multipart_upload("multipart_object.txt");
container.create_multipart_upload("sakila/sakila_multipart_object.txt");
// The directories exist if there's files or multipart uploads
EXPECT_TRUE(root_directory.exists());
EXPECT_TRUE(sakila.exists());
// Normal listing doesn't include multipart uploads
auto root_files = root_directory.list_files();
auto expected_files = root_files;
EXPECT_TRUE(root_files.empty());
// With hidden files we get active multipart uploads
root_files = root_directory.list_files(true);
expected_files = {{"multipart_object.txt"}};
EXPECT_EQ(expected_files, root_files);
create_objects(container);
EXPECT_STREQ("", root_directory.full_path().real().c_str());
root_files = root_directory.list_files();
expected_files = {{"sakila.sql"},
{"sakila_metadata.txt"},
{"sakila_tables.txt"},
{"uncommon%25%name.txt"},
{"uncommon's name.txt"}};
EXPECT_EQ(expected_files, root_files);
root_files = root_directory.list_files(true);
expected_files = {{"sakila.sql"}, {"sakila_metadata.txt"},
{"sakila_tables.txt"}, {"uncommon%25%name.txt"},
{"uncommon's name.txt"}, {"multipart_object.txt"}};
EXPECT_EQ(expected_files, root_files);
EXPECT_STREQ("sakila", sakila.full_path().real().c_str());
auto files = sakila.list_files();
expected_files = {{"actor.csv"}, {"actor_metadata.txt"},
{"address.csv"}, {"address_metadata.txt"},
{"category.csv"}, {"category_metadata.txt"}};
EXPECT_EQ(expected_files, files);
files = sakila.list_files(true);
expected_files = {{"actor.csv"},
{"actor_metadata.txt"},
{"address.csv"},
{"address_metadata.txt"},
{"category.csv"},
{"category_metadata.txt"},
{"sakila_multipart_object.txt"}};
EXPECT_EQ(expected_files, files);
{
auto filtered = root_directory.filter_files("*");
expected_files = {{"sakila.sql"},
{"sakila_metadata.txt"},
{"sakila_tables.txt"},
{"uncommon%25%name.txt"},
{"uncommon's name.txt"}};
EXPECT_EQ(expected_files, filtered);
}
{
auto filtered = root_directory.filter_files("sakila*");
expected_files = {
{"sakila.sql"}, {"sakila_metadata.txt"}, {"sakila_tables.txt"}};
EXPECT_EQ(expected_files, filtered);
}
{
auto filtered = sakila.filter_files("*");
expected_files = {{"actor.csv"}, {"actor_metadata.txt"},
{"address.csv"}, {"address_metadata.txt"},
{"category.csv"}, {"category_metadata.txt"}};
EXPECT_EQ(expected_files, filtered);
}
Directory unexisting(config, "unexisting");
EXPECT_FALSE(unexisting.exists());
unexisting.create();
EXPECT_TRUE(unexisting.exists());
}
TEST_F(Azure_blob_storage_tests, file_errors) {
SKIP_IF_NO_AZURE_CONFIGURATION;
const auto config = get_config();
Blob_container container(config);
Directory root(config, "prefix");
auto file = root.file("sample.txt");
// Attempt to rename unexisting file
EXPECT_THROW_MSG_CONTAINS(
file->rename("other.txt"), std::runtime_error,
"The rename_object operation is not supported in Azure.");
// Attempt to open unexisting file for read
EXPECT_THROW_MSG_CONTAINS(
file->open(Mode::READ), shcore::Exception,
"Failed opening object 'prefix/sample.txt' in READ mode: Failed to get "
"summary for object 'prefix/sample.txt': Not Found (404)");
}
TEST_F(Azure_blob_storage_tests, file_write_simple_upload) {
SKIP_IF_NO_AZURE_CONFIGURATION;
const auto config = get_config();
Blob_container container(config);
testing::clean_container(container);
Directory root(config);
auto file = root.file("sample.txt");
file->open(Mode::WRITE);
file->write("01234", 5);
file->write("56789", 5);
file->write("ABCDE", 5);
auto in_progress_uploads = container.list_multipart_uploads();
EXPECT_TRUE(in_progress_uploads.empty());
file->close();
file->open(Mode::READ);
char buffer[20];
size_t read = file->read(buffer, 20);
EXPECT_EQ(15, read);
std::string data(buffer, read);
EXPECT_STREQ("0123456789ABCDE", data.c_str());
file->close();
container.delete_object("sample.txt");
}
TEST_F(Azure_blob_storage_tests, file_write_multipart_upload) {
SKIP_IF_NO_AZURE_CONFIGURATION;
auto config = get_config();
config->set_part_size(k_min_part_size);
Blob_container container(config);
Directory root(config, "test");
auto file = root.file("sample\".txt");
const auto data = multipart_file_data();
size_t offset = 0;
int writes = 0;
file->open(Mode::WRITE);
while (offset < k_multipart_file_size) {
++writes;
offset += file->write(
data.data() + offset,
std::min(k_min_part_size + 1, k_multipart_file_size - offset));
}
auto uploads = container.list_multipart_uploads();
EXPECT_EQ(1, uploads.size());
EXPECT_STREQ("test/sample\".txt", uploads[0].name.c_str());
auto parts = container.list_multipart_uploaded_parts(uploads[0]);
EXPECT_EQ(writes - 1, parts.size()); // Last part is still on the buffer
file->close();
// NOTE: In Asure this will not fail, will just give an empty list
parts = container.list_multipart_uploaded_parts(uploads[0]);
EXPECT_TRUE(parts.empty());
uploads = container.list_multipart_uploads();
EXPECT_TRUE(uploads.empty());
file->open(Mode::READ);
std::string buffer;
buffer.resize(k_multipart_file_size + 5);
size_t read = file->read(buffer.data(), buffer.size());
EXPECT_EQ(k_multipart_file_size, read);
buffer.resize(read);
EXPECT_EQ(data, buffer);
file->close();
container.delete_object("test/sample\".txt");
}
TEST_F(Azure_blob_storage_tests, file_append_new_file) {
SKIP_IF_NO_AZURE_CONFIGURATION;
auto config = get_config();
Blob_container container(config);
Directory root(config);
auto file = root.file("sample.txt");
file->open(Mode::APPEND);
file->write("01234", 5);
file->write("56789", 5);
file->write("ABCDE", 5);
auto in_progress_uploads = container.list_multipart_uploads();
EXPECT_TRUE(in_progress_uploads.empty());
file->close();
file->open(Mode::READ);
char buffer[20];
size_t read = file->read(buffer, 20);
EXPECT_EQ(15, read);
std::string data(buffer, read);
EXPECT_STREQ("0123456789ABCDE", data.c_str());
file->close();
container.delete_object("sample.txt");
}
TEST_F(Azure_blob_storage_tests, file_append_resume_interrupted_upload) {
SKIP_IF_NO_AZURE_CONFIGURATION;
auto config = get_config();
config->set_part_size(k_min_part_size);
Blob_container container(config);
Directory root(config);
auto initial_file = root.file("sample.txt");
const auto data = multipart_file_data();
size_t offset = 0;
int writes = 0;
initial_file->open(Mode::WRITE);
while (offset + k_min_part_size + 1 < k_multipart_file_size) {
++writes;
offset += initial_file->write(data.data() + offset, k_min_part_size + 1);
}
auto uploads = container.list_multipart_uploads();
EXPECT_EQ(1, uploads.size());
EXPECT_STREQ("sample.txt", uploads[0].name.c_str());
auto parts = container.list_multipart_uploaded_parts(uploads[0]);
EXPECT_EQ(writes, parts.size());
// INTERRUPTION: We stop writing to initial_file as it got interrupted
// At this point the file is an active multipart upload:
// Sent Parts: part1, ..., partN
// Buffered (lost): data which did not fit into a part
// RESUME THE UPLOAD
auto final_file = root.file("sample.txt");
final_file->open(Mode::APPEND);
offset = final_file->file_size();
while (offset < k_multipart_file_size) {
++writes;
offset += final_file->write(
data.data() + offset,
std::min(k_min_part_size + 1, k_multipart_file_size - offset));
}
uploads = container.list_multipart_uploads();
EXPECT_EQ(1, uploads.size());
EXPECT_STREQ("sample.txt", uploads[0].name.c_str());
parts = container.list_multipart_uploaded_parts(uploads[0]);
EXPECT_EQ(writes - 1, parts.size()); // Last part is still on the buffer
final_file->close();
// NOTE: In Asure this will not fail, will just give an empty list
parts = container.list_multipart_uploaded_parts(uploads[0]);
EXPECT_TRUE(parts.empty());
uploads = container.list_multipart_uploads();
EXPECT_TRUE(uploads.empty());
final_file->open(Mode::READ);
std::string buffer;
buffer.resize(k_multipart_file_size + 5);
size_t read = final_file->read(buffer.data(), buffer.size());
EXPECT_EQ(k_multipart_file_size, read);
buffer.resize(read);
EXPECT_EQ(data, buffer);
final_file->close();
container.delete_object("sample.txt");
}
TEST_F(Azure_blob_storage_tests, file_append_existing_file) {
SKIP_IF_NO_AZURE_CONFIGURATION;
auto config = get_config();
Blob_container container(config);
Directory root(config);
auto file = root.file("sample.txt");
file->open(Mode::WRITE);
file->write("01234", 5);
file->close();
// APPEND is forbidden here as the file exist and there' no active multipart
// upload
EXPECT_THROW_MSG_CONTAINS(file->open(Mode::APPEND), std::invalid_argument,
"Object Storage only supports APPEND mode for "
"in-progress multipart uploads or new files.");
// Now APPEND should be allowed
container.create_multipart_upload("sample.txt");
file->open(Mode::APPEND);
file->write("67890", 5);
file->close();
file->open(Mode::READ);
char buffer[10];
size_t read = file->read(buffer, 10);
EXPECT_EQ(5, read);
std::string final_data(buffer, read);
EXPECT_STREQ("67890", final_data.c_str());
file->close();
container.delete_object("sample.txt");
}
TEST_F(Azure_blob_storage_tests, file_write_multipart_errors) {
SKIP_IF_NO_AZURE_CONFIGURATION;
output_handler.set_log_level(shcore::Logger::LOG_LEVEL::LOG_DEBUG2);
auto config = get_config();
config->set_part_size(3);
Blob_container container(config);
Directory root(config);
auto mpo1 = container.create_multipart_upload("sample.txt");
auto mpop1 = container.upload_part(mpo1, 1, "123", 3);
// Now APPEND should be allowed
auto file = root.file("sample.txt");
file->open(Mode::APPEND);
container.abort_multipart_upload(mpo1);
// In azure additional chunks continue writing OK, but the internal chunk list
// on the file is invalid because the abort operation wiped them out, the
// failure will arise when a commit (file close) is attempted
file->write("456", 3);
EXPECT_THROW_MSG_CONTAINS(
file->close(), shcore::Error,
"Failed to commit multipart upload for object 'sample.txt': The "
"specified block list is invalid.");
// CORNER CASE: The abort was done only after multipart object was started,
// even that puts a first blok, it is ignored as it is just a placeholder to
// get the object listed as a multipart upload.
mpo1 = container.create_multipart_upload("sample.txt");
file = root.file("sample.txt");
file->open(Mode::APPEND);
file->write("123", 3);
file->close();
file->open(Mode::READ);
char buffer[10];
size_t read = file->read(buffer, 10);
EXPECT_EQ(3, read);
std::string final_data(buffer, read);
EXPECT_STREQ("123", final_data.c_str());
file->close();
}
TEST_F(Azure_blob_storage_tests, file_writing) {
SKIP_IF_NO_AZURE_CONFIGURATION;
const auto config = get_config();
Directory root(config);
auto file = root.file("sample.txt");
EXPECT_FALSE(file->exists());
file->open(mysqlshdk::storage::Mode::WRITE);
file->write("SOME", 4);
std::string test;
test.append(15, 'a');
file->write(test.data(), test.size());
file->write("END", 3);
file->close();
EXPECT_TRUE(file->exists());
auto another = root.file("sample.txt");
EXPECT_TRUE(another->exists());
char buffer[30];
another->open(Mode::READ);
size_t read = another->read(&buffer, 4);
EXPECT_EQ(4, read);
std::string data(buffer, read);
EXPECT_STREQ("SOME", data.c_str());
read = another->read(&buffer, 15);
EXPECT_EQ(15, read);
data.assign(buffer, read);
EXPECT_STREQ(test.c_str(), data.c_str());
read = another->read(&buffer, 3);
EXPECT_EQ(3, read);
data.assign(buffer, read);
EXPECT_STREQ("END", data.c_str());
another->seek(2);
read = another->read(&buffer, 5);
EXPECT_EQ(5, read);
data.assign(buffer, read);
EXPECT_STREQ("MEaaa", data.c_str());
another->seek(0);
read = another->read(&buffer, 30);
EXPECT_EQ(22, read);
data.assign(buffer, read);
EXPECT_STREQ("SOMEaaaaaaaaaaaaaaaEND", data.c_str());
another->seek(22);
read = another->read(&buffer, 30);
EXPECT_EQ(0, read);
Blob_container container(config);
container.delete_object("sample.txt");
}
TEST_F(Azure_blob_storage_tests, file_rename) {
SKIP_IF_NO_AZURE_CONFIGURATION;
const auto config = get_config();
Blob_container container(config);
Directory root(config);
auto file = root.file("sample.txt");
file->open(Mode::WRITE);
file->write("SOME CONTENT", 12);
file->close();
EXPECT_STREQ("sample.txt", file->filename().c_str());
EXPECT_STREQ("sample.txt", file->full_path().real().c_str());
EXPECT_THROW_MSG_CONTAINS(
file->rename("testing.txt");
, std::runtime_error,
"The rename_object operation is not supported in Azure.");
EXPECT_STREQ("sample.txt", file->filename().c_str());
EXPECT_STREQ("sample.txt", file->full_path().real().c_str());
auto files = root.list_files();
auto expected_files = files;
expected_files = {{"sample.txt"}};
EXPECT_EQ(expected_files, files);
container.delete_object("sample.txt");
}
TEST_F(Azure_blob_storage_tests, file_auto_cancel_multipart_upload) {
SKIP_IF_NO_AZURE_CONFIGURATION;
auto config = get_config();
config->set_part_size(k_min_part_size);
Blob_container container(config);
Directory root(config, "test");
auto file = root.file("sample\".txt");
const auto data = multipart_file_data();
size_t offset = 0;
int writes = 0;
file->open(Mode::WRITE);
while (offset < k_multipart_file_size) {
++writes;
offset += file->write(
data.data() + offset,
std::min(k_min_part_size + 1, k_multipart_file_size - offset));
}
const auto uploads = container.list_multipart_uploads();
EXPECT_EQ(1, uploads.size());
EXPECT_STREQ("test/sample\".txt", uploads[0].name.c_str());
const auto parts = container.list_multipart_uploaded_parts(uploads[0]);
EXPECT_EQ(writes - 1, parts.size()); // Last part is still on the buffer
// release the file, simulating abnormal situation (close() was not called,
// destructor should clean up the upload)
file.reset();
EXPECT_THROW_MSG_CONTAINS(
container.list_multipart_uploaded_parts(uploads[0]), Response_error,
"Failed to list uploaded parts for object 'test/sample\".txt': The "
"specified blob does not exist.");
EXPECT_TRUE(container.list_multipart_uploads().empty());
}
} // namespace azure
} // namespace mysqlshdk