blob: 77204938baece2473a746852acb8a0a97dd97ed2 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
#include "format/atomic_output.h"
#include <fcntl.h>
#include <sys/stat.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <sstream>
#include "cli/exit_codes.h"
#ifdef _WIN32
#include <io.h>
#include <process.h>
#include <windows.h>
#define lstat stat
#define close _close
#define getpid _getpid
#else
#include <unistd.h>
#endif
namespace tsfile_cli {
namespace {
bool is_regular_file(const struct stat& st) {
#ifdef _WIN32
return (st.st_mode & S_IFREG) != 0;
#else
return S_ISREG(st.st_mode);
#endif
}
bool is_same_file(const std::string& source_path,
const std::string& target_path, const struct stat& target) {
#ifdef _WIN32
(void)target;
const DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
HANDLE source = CreateFileA(source_path.c_str(), 0, share, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (source == INVALID_HANDLE_VALUE) {
return false;
}
HANDLE output = CreateFileA(target_path.c_str(), 0, share, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (output == INVALID_HANDLE_VALUE) {
CloseHandle(source);
return false;
}
BY_HANDLE_FILE_INFORMATION source_info;
BY_HANDLE_FILE_INFORMATION output_info;
const bool same =
GetFileInformationByHandle(source, &source_info) != 0 &&
GetFileInformationByHandle(output, &output_info) != 0 &&
source_info.dwVolumeSerialNumber == output_info.dwVolumeSerialNumber &&
source_info.nFileIndexHigh == output_info.nFileIndexHigh &&
source_info.nFileIndexLow == output_info.nFileIndexLow;
CloseHandle(output);
CloseHandle(source);
return same;
#else
(void)target_path;
struct stat source;
return stat(source_path.c_str(), &source) == 0 &&
source.st_dev == target.st_dev && source.st_ino == target.st_ino;
#endif
}
int validate_target(const std::string& target_path,
const std::string& source_path, bool force,
std::ostream& err) {
struct stat target;
if (lstat(target_path.c_str(), &target) != 0) {
if (errno == ENOENT) {
return kExitOk;
}
err << "Error: cannot inspect output target '" << target_path
<< "': " << std::strerror(errno) << "\n";
return kExitRuntime;
}
#ifndef _WIN32
if (S_ISLNK(target.st_mode)) {
err << "Error: output target '" << target_path
<< "' must not be a symbolic link\n";
return kExitRuntime;
}
#else
DWORD attributes = GetFileAttributesA(target_path.c_str());
if (attributes != INVALID_FILE_ATTRIBUTES &&
(attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
err << "Error: output target '" << target_path
<< "' must not be a reparse point\n";
return kExitRuntime;
}
#endif
if (!is_regular_file(target)) {
err << "Error: output target '" << target_path
<< "' is not a regular file\n";
return kExitRuntime;
}
if (!source_path.empty() &&
is_same_file(source_path, target_path, target)) {
err << "Error: output target '" << target_path
<< "' is the same as the input file '" << source_path << "'\n";
return kExitRuntime;
}
if (!force) {
err << "Error: output target '" << target_path
<< "' already exists; use --force to replace a regular file\n";
return kExitRuntime;
}
return kExitOk;
}
} // namespace
int prepare_atomic_output(const std::string& target_path,
const std::string& source_path, bool force,
std::string& temp_path, std::ostream& err) {
int ret = validate_target(target_path, source_path, force, err);
if (ret != kExitOk) {
return ret;
}
for (unsigned int attempt = 0; attempt < 1000; ++attempt) {
std::ostringstream candidate;
candidate << target_path << ".tmp." << getpid() << "." << attempt;
temp_path = candidate.str();
int flags = O_WRONLY | O_CREAT | O_EXCL;
#ifdef _WIN32
flags |= O_BINARY;
#endif
int fd = ::open(temp_path.c_str(), flags, 0600);
if (fd >= 0) {
if (::close(fd) != 0) {
err << "Error: failed to close temporary output '" << temp_path
<< "': " << std::strerror(errno) << "\n";
std::remove(temp_path.c_str());
return kExitRuntime;
}
return kExitOk;
}
if (errno != EEXIST) {
err << "Error: cannot create output temporary file for '"
<< target_path << "': " << std::strerror(errno) << "\n";
return kExitRuntime;
}
}
err << "Error: cannot allocate a unique temporary output for '"
<< target_path << "'\n";
return kExitRuntime;
}
int commit_atomic_output(const std::string& temp_path,
const std::string& target_path, bool force,
std::ostream& err) {
#ifdef _WIN32
DWORD flags = MOVEFILE_WRITE_THROUGH;
if (force) {
flags |= MOVEFILE_REPLACE_EXISTING;
}
if (MoveFileExA(temp_path.c_str(), target_path.c_str(), flags) != 0) {
return kExitOk;
}
#else
if (force) {
if (std::rename(temp_path.c_str(), target_path.c_str()) == 0) {
return kExitOk;
}
} else if (::link(temp_path.c_str(), target_path.c_str()) == 0) {
if (std::remove(temp_path.c_str()) == 0) {
return kExitOk;
}
err << "Error: output was committed but temporary path remains: '"
<< temp_path << "'\n";
return kExitRuntime;
}
#endif
err << "Error: failed to commit output target '" << target_path << "'\n";
return kExitRuntime;
}
bool remove_atomic_temp(const std::string& temp_path, std::ostream& err) {
if (temp_path.empty() || std::remove(temp_path.c_str()) == 0 ||
errno == ENOENT) {
return true;
}
err << "Error: failed to remove temporary output '" << temp_path
<< "': " << std::strerror(errno) << "\n";
return false;
}
} // namespace tsfile_cli