mirror of https://gitee.com/bigwinds/arangodb
563 lines
16 KiB
C++
563 lines
16 KiB
C++
////////////////////////////////////////////////////////////////////////////////
|
|
/// DISCLAIMER
|
|
///
|
|
/// Copyright 2016 ArangoDB GmbH, Cologne, Germany
|
|
///
|
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
/// you may not use this file except in compliance with the License.
|
|
/// You may obtain a copy of the License at
|
|
///
|
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
///
|
|
/// Unless required by applicable law or agreed to in writing, software
|
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
/// See the License for the specific language governing permissions and
|
|
/// limitations under the License.
|
|
///
|
|
/// Copyright holder is ArangoDB GmbH, Cologne, Germany
|
|
///
|
|
/// @author Dr. Frank Cellar
|
|
/// @author Dan Larkin-York
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include "ManagedDirectory.h"
|
|
|
|
#include "Basics/FileUtils.h"
|
|
#include "Basics/StringUtils.h"
|
|
#include "Basics/files.h"
|
|
#include "Logger/Logger.h"
|
|
|
|
namespace {
|
|
|
|
/// @brief size of char buffer to use for file slurping
|
|
constexpr size_t DefaultIOChunkSize = 8192;
|
|
|
|
/// @brief the filename for the encryption file
|
|
constexpr auto EncryptionFilename = "ENCRYPTION";
|
|
|
|
/// @brief encryption type specification for no encryption
|
|
constexpr auto EncryptionTypeNone = "none";
|
|
|
|
/// @brief determines whether a given bit flag is set
|
|
inline bool flagIsSet(int value, int flagToCheck) {
|
|
TRI_ASSERT(0 != flagToCheck); // does not work correctly if flag is 0
|
|
return (flagToCheck == (value & flagToCheck));
|
|
}
|
|
|
|
/// @brief determines whether a given bit flag is not set
|
|
inline bool flagNotSet(int value, int flagToCheck) {
|
|
TRI_ASSERT(0 != flagToCheck); // does not work correctly if flag is 0
|
|
return (flagToCheck != (value & flagToCheck));
|
|
}
|
|
|
|
/// @brief Generates a generic I/O error based on the path and flags
|
|
inline arangodb::Result genericError(std::string const& path, int flags) {
|
|
if (::flagIsSet(flags, O_WRONLY)) {
|
|
return {TRI_ERROR_CANNOT_WRITE_FILE, "error while writing file " + path};
|
|
}
|
|
return {TRI_ERROR_CANNOT_READ_FILE, "error while reading file " + path};
|
|
}
|
|
|
|
/// @brief Assembles the file path from the directory and filename
|
|
inline std::string filePath(arangodb::ManagedDirectory const& directory,
|
|
std::string const& filename) {
|
|
using arangodb::basics::FileUtils::buildFilename;
|
|
return buildFilename(directory.path(), filename);
|
|
}
|
|
|
|
/// @brief Assembles the file path from the directory path and filename
|
|
inline std::string filePath(std::string const& directory, std::string const& filename) {
|
|
using arangodb::basics::FileUtils::buildFilename;
|
|
return buildFilename(directory, filename);
|
|
}
|
|
|
|
/// @brief Opens a file given a path and flags
|
|
inline int openFile(std::string const& path, int flags) {
|
|
return (::flagIsSet(flags, O_CREAT) ? TRI_CREATE(path.c_str(), flags, S_IRUSR | S_IWUSR)
|
|
: TRI_OPEN(path.c_str(), flags));
|
|
}
|
|
|
|
/// @brief Closes an open file and sets the status
|
|
inline void closeFile(int& fd, arangodb::Result& status) {
|
|
TRI_ASSERT(fd >= 0);
|
|
status = arangodb::Result{TRI_CLOSE(fd)};
|
|
fd = -1;
|
|
}
|
|
|
|
/// @brief determines if a file is writable
|
|
bool isWritable(int fd, int flags, std::string const& path, arangodb::Result& status) {
|
|
if (::flagNotSet(flags, O_WRONLY)) {
|
|
status = {TRI_ERROR_CANNOT_WRITE_FILE, "attempted to write to file " + path +
|
|
" opened in read-only mode!"};
|
|
return false;
|
|
}
|
|
if (fd < 0) {
|
|
status = {TRI_ERROR_CANNOT_WRITE_FILE,
|
|
"attempted to write to file " + path + " which is not open"};
|
|
return false;
|
|
}
|
|
return status.ok();
|
|
}
|
|
|
|
/// @brief determines if a file is readable
|
|
bool isReadable(int fd, int flags, std::string const& path, arangodb::Result& status) {
|
|
if (::flagIsSet(flags, O_WRONLY)) {
|
|
status = {TRI_ERROR_CANNOT_READ_FILE, "attempted to read from file " + path +
|
|
" opened in write-only mode!"};
|
|
return false;
|
|
}
|
|
if (fd < 0) {
|
|
status = {TRI_ERROR_CANNOT_READ_FILE,
|
|
"attempted to read from file " + path + " which is not open"};
|
|
return false;
|
|
}
|
|
return status.ok();
|
|
}
|
|
|
|
#ifdef USE_ENTERPRISE
|
|
/// @brief Begins encryption for the file and returns the encryption context
|
|
inline std::unique_ptr<arangodb::EncryptionFeature::Context> getContext(
|
|
arangodb::ManagedDirectory const& directory, int fd, int flags) {
|
|
if (fd < 0 || directory.encryptionFeature() == nullptr) {
|
|
return {nullptr};
|
|
}
|
|
|
|
return (::flagIsSet(flags, O_WRONLY)
|
|
? directory.encryptionFeature()->beginEncryption(fd)
|
|
: directory.encryptionFeature()->beginDecryption(fd));
|
|
}
|
|
#endif
|
|
|
|
/// @brief Generates the initial status for the directory
|
|
#ifdef USE_ENTERPRISE
|
|
arangodb::Result initialStatus(int fd, std::string const& path, int flags,
|
|
arangodb::EncryptionFeature::Context* context)
|
|
#else
|
|
arangodb::Result initialStatus(int fd, std::string const& path, int flags)
|
|
#endif
|
|
{
|
|
if (fd < 0) {
|
|
return ::genericError(path, flags);
|
|
}
|
|
|
|
#ifdef USE_ENTERPRISE
|
|
return (context ? context->status() : arangodb::Result{TRI_ERROR_NO_ERROR});
|
|
#else
|
|
return {TRI_ERROR_NO_ERROR};
|
|
#endif
|
|
}
|
|
|
|
/// @brief Performs a raw (non-encrypted) write
|
|
inline void rawWrite(int fd, char const* data, size_t length,
|
|
arangodb::Result& status, std::string const& path, int flags) {
|
|
while (length > 0) {
|
|
ssize_t written = TRI_WRITE(fd, data, static_cast<TRI_write_t>(length));
|
|
if (written < 0) {
|
|
status = ::genericError(path, flags);
|
|
break;
|
|
}
|
|
length -= written;
|
|
data += written;
|
|
}
|
|
}
|
|
|
|
/// @brief Performs a raw (non-decrypted) read
|
|
inline ssize_t rawRead(int fd, char* buffer, size_t length, arangodb::Result& status,
|
|
std::string const& path, int flags) {
|
|
ssize_t bytesRead = TRI_READ(fd, buffer, static_cast<TRI_read_t>(length));
|
|
if (bytesRead < 0) {
|
|
status = ::genericError(path, flags);
|
|
}
|
|
return bytesRead;
|
|
}
|
|
|
|
void readEncryptionFile(std::string const& directory, std::string& type) {
|
|
using arangodb::basics::FileUtils::slurp;
|
|
using arangodb::basics::StringUtils::trim;
|
|
type = ::EncryptionTypeNone;
|
|
auto filename = ::filePath(directory, ::EncryptionFilename);
|
|
if (TRI_ExistsFile(filename.c_str())) {
|
|
type = trim(slurp(filename));
|
|
}
|
|
}
|
|
|
|
#ifdef USE_ENTERPRISE
|
|
void writeEncryptionFile(std::string const& directory, std::string& type,
|
|
arangodb::EncryptionFeature* encryptionFeature) {
|
|
#else
|
|
void writeEncryptionFile(std::string const& directory, std::string& type) {
|
|
#endif
|
|
using arangodb::basics::FileUtils::spit;
|
|
type = ::EncryptionTypeNone;
|
|
auto filename = ::filePath(directory, ::EncryptionFilename);
|
|
#ifdef USE_ENTERPRISE
|
|
if (nullptr != encryptionFeature) {
|
|
type = encryptionFeature->encryptionType();
|
|
}
|
|
#endif
|
|
spit(filename, type);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace arangodb {
|
|
|
|
ManagedDirectory::ManagedDirectory(std::string const& path, bool requireEmpty, bool create, bool writeGzip)
|
|
:
|
|
#ifdef USE_ENTERPRISE
|
|
_encryptionFeature{
|
|
application_features::ApplicationServer::getFeature<EncryptionFeature>("Encryption")},
|
|
#endif
|
|
_path{path},
|
|
_encryptionType{::EncryptionTypeNone},
|
|
_writeGzip(writeGzip),
|
|
_status{TRI_ERROR_NO_ERROR} {
|
|
if (_path.empty()) {
|
|
_status.reset(TRI_ERROR_BAD_PARAMETER, "must specify a path");
|
|
return;
|
|
}
|
|
|
|
bool exists = TRI_ExistsFile(_path.c_str());
|
|
if (exists) {
|
|
bool isDirectory = TRI_IsDirectory(_path.c_str());
|
|
// path exists, but is a file, not a directory
|
|
if (!isDirectory) {
|
|
_status.reset(TRI_ERROR_FILE_EXISTS,
|
|
"path specified already exists as a non-directory file");
|
|
return;
|
|
}
|
|
|
|
std::vector<std::string> files(TRI_FullTreeDirectory(_path.c_str()));
|
|
bool isEmpty = (files.size() <= 1);
|
|
// TODO: TRI_FullTreeDirectory always returns at least one element ("")
|
|
// even if directory is empty?
|
|
|
|
if (!isEmpty) {
|
|
// directory exists, has files, and we aren't allowed to overwrite
|
|
if (requireEmpty) {
|
|
_status.reset(TRI_ERROR_CANNOT_OVERWRITE_FILE,
|
|
"path specified is a non-empty directory");
|
|
return;
|
|
}
|
|
::readEncryptionFile(_path, _encryptionType);
|
|
return;
|
|
}
|
|
// fall through to write encryption file
|
|
} else {
|
|
// create directory if it doesn't exist
|
|
if (create) {
|
|
long systemError;
|
|
std::string errorMessage;
|
|
int res = TRI_CreateDirectory(_path.c_str(), systemError, errorMessage);
|
|
if (res != TRI_ERROR_NO_ERROR) {
|
|
if (res == TRI_ERROR_SYS_ERROR) {
|
|
res = TRI_ERROR_CANNOT_CREATE_DIRECTORY;
|
|
}
|
|
_status.reset(res, "unable to create output directory '" + _path +
|
|
"': " + errorMessage);
|
|
return;
|
|
}
|
|
// fall through to write encryption file
|
|
} else {
|
|
_status.reset(TRI_ERROR_FILE_NOT_FOUND);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// currently gzip and encryption are mutually exclusive, encryption wins
|
|
if (::EncryptionTypeNone != _encryptionType) {
|
|
_writeGzip = false;
|
|
} // if
|
|
|
|
#ifdef USE_ENTERPRISE
|
|
::writeEncryptionFile(_path, _encryptionType, _encryptionFeature);
|
|
#else
|
|
::writeEncryptionFile(_path, _encryptionType);
|
|
#endif
|
|
}
|
|
|
|
ManagedDirectory::~ManagedDirectory() {}
|
|
|
|
Result const& ManagedDirectory::status() const { return _status; }
|
|
|
|
void ManagedDirectory::resetStatus() { _status.reset(TRI_ERROR_NO_ERROR); }
|
|
|
|
std::string const& ManagedDirectory::path() const { return _path; }
|
|
|
|
std::string ManagedDirectory::pathToFile(std::string const& filename) const {
|
|
return ::filePath(*this, filename);
|
|
}
|
|
|
|
bool ManagedDirectory::isEncrypted() const {
|
|
return (::EncryptionTypeNone != _encryptionType);
|
|
}
|
|
|
|
std::string const& ManagedDirectory::encryptionType() const {
|
|
return _encryptionType;
|
|
}
|
|
|
|
#ifdef USE_ENTERPRISE
|
|
EncryptionFeature const* ManagedDirectory::encryptionFeature() const {
|
|
return _encryptionFeature;
|
|
}
|
|
#endif
|
|
|
|
std::unique_ptr<ManagedDirectory::File> ManagedDirectory::readableFile(std::string const& filename,
|
|
int flags) {
|
|
std::unique_ptr<File> file{nullptr};
|
|
|
|
if (_status.fail()) { // directory is in a bad state
|
|
return file;
|
|
}
|
|
|
|
try {
|
|
bool gzFlag = (0 == filename.substr(filename.size() - 3).compare(".gz"));
|
|
file = std::make_unique<File>(*this, filename,
|
|
(ManagedDirectory::DefaultReadFlags ^ flags), gzFlag);
|
|
} catch (...) {
|
|
_status.reset(TRI_ERROR_CANNOT_READ_FILE, "error opening file " +
|
|
::filePath(*this, filename) +
|
|
" for reading");
|
|
return {nullptr};
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
std::unique_ptr<ManagedDirectory::File> ManagedDirectory::writableFile(
|
|
std::string const& filename, bool overwrite, int flags, bool gzipOk) {
|
|
std::unique_ptr<File> file{nullptr};
|
|
|
|
if (_status.fail()) { // directory is in a bad state
|
|
return file;
|
|
}
|
|
|
|
try {
|
|
std::string filenameCopy = filename;
|
|
if (_writeGzip && gzipOk) {
|
|
filenameCopy.append(".gz");
|
|
} // if
|
|
|
|
// deal with existing file first if it exists
|
|
auto path = ::filePath(*this, filenameCopy);
|
|
bool fileExists = TRI_ExistsFile(path.c_str());
|
|
if (fileExists) {
|
|
if (overwrite) {
|
|
TRI_UnlinkFile(path.c_str());
|
|
} else {
|
|
_status.reset(TRI_ERROR_CANNOT_WRITE_FILE,
|
|
"file " + path + " already exists");
|
|
return {nullptr};
|
|
}
|
|
}
|
|
|
|
file = std::make_unique<File>(*this, filename,
|
|
(ManagedDirectory::DefaultWriteFlags ^ flags), _writeGzip && gzipOk);
|
|
} catch (...) {
|
|
return {nullptr};
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
void ManagedDirectory::spitFile(std::string const& filename, std::string const& content) {
|
|
auto file = writableFile(filename, true);
|
|
if (!file) {
|
|
_status = ::genericError(filename, O_WRONLY);
|
|
} else if (file->status().fail()) {
|
|
_status = file->status();
|
|
}
|
|
file->spit(content);
|
|
}
|
|
|
|
std::string ManagedDirectory::slurpFile(std::string const& filename) {
|
|
std::string content;
|
|
auto file = readableFile(filename);
|
|
if (!file || file->status().fail()) {
|
|
return content;
|
|
}
|
|
content = file->slurp();
|
|
return content;
|
|
}
|
|
|
|
VPackBuilder ManagedDirectory::vpackFromJsonFile(std::string const& filename) {
|
|
VPackBuilder builder;
|
|
auto content = slurpFile(filename);
|
|
if (!content.empty()) {
|
|
// The Parser might throw;
|
|
try {
|
|
VPackParser parser(builder);
|
|
parser.parse(reinterpret_cast<uint8_t const*>(content.data()), content.size());
|
|
} catch (...) {
|
|
throw; // TODO determine what to actually do here?
|
|
}
|
|
}
|
|
return builder;
|
|
}
|
|
|
|
ManagedDirectory::File::File(ManagedDirectory const& directory,
|
|
std::string const& filename, int flags,
|
|
bool isGzip)
|
|
: _directory{directory},
|
|
_path{::filePath(_directory, filename)},
|
|
_flags{flags},
|
|
_fd{::openFile(_path, _flags)},
|
|
_gzfd(-1),
|
|
_gzFile(nullptr),
|
|
#ifdef USE_ENTERPRISE
|
|
_context{::getContext(_directory, _fd, _flags)},
|
|
_status {
|
|
::initialStatus(_fd, _path, _flags, _context.get())
|
|
}
|
|
#else
|
|
_status {
|
|
::initialStatus(_fd, _path, _flags)
|
|
}
|
|
#endif
|
|
{
|
|
TRI_ASSERT(::flagNotSet(_flags, O_RDWR)); // disallow read/write (encryption)
|
|
|
|
if (isGzip) {
|
|
const char * gzFlags(nullptr);
|
|
|
|
// gzip is going to perform a redundant close,
|
|
// simpler code to give it redundant handle
|
|
_gzfd = dup(_fd);
|
|
|
|
if (O_WRONLY & flags) {
|
|
gzFlags = "wb";
|
|
} else {
|
|
gzFlags = "rb";
|
|
} // else
|
|
_gzFile = gzdopen(_gzfd, gzFlags);
|
|
} // if
|
|
}
|
|
|
|
ManagedDirectory::File::~File() {
|
|
try {
|
|
if (_gzfd >=0) {
|
|
gzclose(_gzFile);
|
|
_gzfd = -1;
|
|
_gzFile = nullptr;
|
|
} // if
|
|
|
|
if (_fd >= 0) {
|
|
::closeFile(_fd, _status);
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
Result const& ManagedDirectory::File::status() const { return _status; }
|
|
|
|
std::string const& ManagedDirectory::File::path() const { return _path; }
|
|
|
|
void ManagedDirectory::File::write(char const* data, size_t length) {
|
|
if (!::isWritable(_fd, _flags, _path, _status)) {
|
|
return;
|
|
}
|
|
|
|
#ifdef USE_ENTERPRISE
|
|
if (_context && _directory.isEncrypted()) {
|
|
bool written = _directory.encryptionFeature()->writeData(*_context, data, length);
|
|
if (!written) {
|
|
_status = _context->status();
|
|
}
|
|
} else if (isGzip()) {
|
|
gzwrite(_gzFile, data, length);
|
|
} else {
|
|
::rawWrite(_fd, data, length, _status, _path, _flags);
|
|
}
|
|
#else
|
|
if (isGzip()) {
|
|
gzwrite(_gzFile, data, length);
|
|
} else {
|
|
::rawWrite(_fd, data, length, _status, _path, _flags);
|
|
} // else
|
|
#endif
|
|
}
|
|
|
|
ssize_t ManagedDirectory::File::read(char* buffer, size_t length) {
|
|
ssize_t bytesRead = -1;
|
|
if (!::isReadable(_fd, _flags, _path, _status)) {
|
|
return bytesRead;
|
|
}
|
|
|
|
#ifdef USE_ENTERPRISE
|
|
if (_context && _directory.isEncrypted()) {
|
|
bytesRead = _directory.encryptionFeature()->readData(*_context, buffer, length);
|
|
if (bytesRead < 0) {
|
|
_status = _context->status();
|
|
}
|
|
} else if (isGzip()) {
|
|
bytesRead = gzread(_gzFile, buffer, length);
|
|
} else {
|
|
bytesRead = ::rawRead(_fd, buffer, length, _status, _path, _flags);
|
|
}
|
|
#else
|
|
if (isGzip()) {
|
|
bytesRead = gzread(_gzFile, buffer, length);
|
|
} else {
|
|
bytesRead = ::rawRead(_fd, buffer, length, _status, _path, _flags);
|
|
} // else
|
|
#endif
|
|
return bytesRead;
|
|
}
|
|
|
|
std::string ManagedDirectory::File::slurp() {
|
|
std::string content;
|
|
if (!::isReadable(_fd, _flags, _path, _status)) {
|
|
return content;
|
|
}
|
|
|
|
char buffer[::DefaultIOChunkSize];
|
|
while (true) {
|
|
ssize_t bytesRead = read(buffer, ::DefaultIOChunkSize);
|
|
if (_status.ok()) {
|
|
content.append(buffer, bytesRead);
|
|
}
|
|
if (bytesRead <= 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
void ManagedDirectory::File::spit(std::string const& content) {
|
|
if (!::isWritable(_fd, _flags, _path, _status)) {
|
|
return;
|
|
}
|
|
|
|
size_t leftToWrite = content.size();
|
|
auto data = content.data();
|
|
while (true) {
|
|
size_t n = std::min(leftToWrite, ::DefaultIOChunkSize);
|
|
write(data, n);
|
|
if (_status.fail()) {
|
|
break;
|
|
}
|
|
data += n;
|
|
leftToWrite -= n;
|
|
if (0 == leftToWrite) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Result const& ManagedDirectory::File::close() {
|
|
if (_gzfd >=0) {
|
|
gzclose(_gzFile);
|
|
_gzfd = -1;
|
|
_gzFile = nullptr;
|
|
} // if
|
|
|
|
if (_fd >= 0) {
|
|
::closeFile(_fd, _status);
|
|
}
|
|
return _status;
|
|
}
|
|
|
|
} // namespace arangodb
|